695a0b0334ae3d1581f92e70ba105a0a96434d3e
[dcpomatic.git] / src / tools / dcpomatic_disk_writer.cc
1 /*
2     Copyright (C) 2019-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "lib/compose.hpp"
22 #include "lib/cross.h"
23 #include "lib/dcpomatic_log.h"
24 #include "lib/digester.h"
25 #include "lib/disk_writer_messages.h"
26 #include "lib/exceptions.h"
27 #include "lib/ext.h"
28 #include "lib/file_log.h"
29 #include "lib/nanomsg.h"
30 #include "lib/version.h"
31 #include "lib/warnings.h"
32
33 #ifdef DCPOMATIC_POSIX
34 #include <sys/ioctl.h>
35 #include <sys/types.h>
36 #include <sys/stat.h>
37 #endif
38
39 #ifdef DCPOMATIC_OSX
40 #include "lib/stdout_log.h"
41 #undef nil
42 extern "C" {
43 #include <lwext4/file_dev.h>
44 }
45 #include <xpc/xpc.h>
46 #endif
47
48 #ifdef DCPOMATIC_LINUX
49 #include <polkit/polkit.h>
50 #include <poll.h>
51 #endif
52
53 #ifdef DCPOMATIC_WINDOWS
54 extern "C" {
55 #include <lwext4/file_windows.h>
56 }
57 #endif
58
59 DCPOMATIC_DISABLE_WARNINGS
60 #include <glibmm.h>
61 DCPOMATIC_ENABLE_WARNINGS
62
63 #include <unistd.h>
64 #include <sys/types.h>
65 #include <boost/filesystem.hpp>
66 #include <boost/algorithm/string.hpp>
67 #include <boost/foreach.hpp>
68 #include <iostream>
69
70 using std::cin;
71 using std::min;
72 using std::string;
73 using std::runtime_error;
74 using std::exception;
75 using std::vector;
76 using boost::optional;
77
78
79 #define SHORT_TIMEOUT 100
80 #define LONG_TIMEOUT 2000
81
82
83 #ifdef DCPOMATIC_LINUX
84 static PolkitAuthority* polkit_authority = 0;
85 #endif
86 static Nanomsg* nanomsg = 0;
87
88 struct Parameters
89 {
90         boost::filesystem::path dcp_path;
91         std::string device;
92         std::string posix_partition;
93 };
94
95 #ifdef DCPOMATIC_LINUX
96 static
97 void
98 polkit_callback (GObject *, GAsyncResult* res, gpointer data)
99 {
100         Parameters* parameters = reinterpret_cast<Parameters*> (data);
101         PolkitAuthorizationResult* result = polkit_authority_check_authorization_finish (polkit_authority, res, 0);
102         if (result && polkit_authorization_result_get_is_authorized(result)) {
103                 dcpomatic::write (parameters->dcp_path, parameters->device, parameters->posix_partition, nanomsg);
104         }
105         delete parameters;
106         if (result) {
107                 g_object_unref (result);
108         }
109 }
110 #endif
111
112
113 bool
114 idle ()
115 try
116 {
117         using namespace boost::algorithm;
118
119         optional<string> s = nanomsg->receive (0);
120         if (!s) {
121                 return true;
122         }
123
124         LOG_DISK("Writer receives command: %1", *s);
125
126         if (*s == DISK_WRITER_QUIT) {
127                 exit (EXIT_SUCCESS);
128         } else if (*s == DISK_WRITER_PING) {
129                 nanomsg->send(DISK_WRITER_PONG "\n", LONG_TIMEOUT);
130         } else if (*s == DISK_WRITER_UNMOUNT) {
131                 /* XXX: should do Linux polkit stuff here */
132                 optional<string> xml_head = nanomsg->receive (LONG_TIMEOUT);
133                 optional<string> xml_body = nanomsg->receive (LONG_TIMEOUT);
134                 if (!xml_head || !xml_body) {
135                         LOG_DISK_NC("Failed to receive unmount request");
136                         throw CommunicationFailedError ();
137                 }
138                 bool const success = Drive(*xml_head + *xml_body).unmount();
139                 if (!nanomsg->send (success ? (DISK_WRITER_OK "\n") : (DISK_WRITER_ERROR "\n"), LONG_TIMEOUT)) {
140                         LOG_DISK_NC("CommunicationFailedError in unmount_finished");
141                         throw CommunicationFailedError ();
142                 }
143         } else if (*s == DISK_WRITER_WRITE) {
144                 optional<string> dcp_path = nanomsg->receive (LONG_TIMEOUT);
145                 optional<string> device = nanomsg->receive (LONG_TIMEOUT);
146                 if (!dcp_path || !device) {
147                         LOG_DISK_NC("Failed to receive write request");
148                         throw CommunicationFailedError();
149                 }
150
151                 /* Do some basic sanity checks; this is a bit belt-and-braces but it can't hurt... */
152
153 #ifdef DCPOMATIC_OSX
154                 if (!starts_with(*device, "/dev/disk")) {
155                         LOG_DISK ("Will not write to %1", *device);
156                         nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
157                         return true;
158                 }
159 #endif
160 #ifdef DCPOMATIC_LINUX
161                 if (!starts_with(*device, "/dev/sd") && !starts_with(*device, "/dev/hd")) {
162                         LOG_DISK ("Will not write to %1", *device);
163                         nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
164                         return true;
165                 }
166 #endif
167 #ifdef DCPOMATIC_WINDOWS
168                 if (!starts_with(*device, "\\\\.\\PHYSICALDRIVE")) {
169                         LOG_DISK ("Will not write to %1", *device);
170                         nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
171                         return true;
172                 }
173 #endif
174
175                 bool on_drive_list = false;
176                 bool mounted = false;
177                 for (auto const& i: Drive::get()) {
178                         if (i.device() == *device) {
179                                 on_drive_list = true;
180                                 mounted = i.mounted();
181                         }
182                 }
183
184                 if (!on_drive_list) {
185                         LOG_DISK ("Will not write to %1 as it's not recognised as a drive", *device);
186                         nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
187                         return true;
188                 }
189                 if (mounted) {
190                         LOG_DISK ("Will not write to %1 as it's mounted", *device);
191                         nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
192                         return true;
193                 }
194
195                 LOG_DISK ("Here we go writing %1 to %2", *dcp_path, *device);
196
197 #ifdef DCPOMATIC_LINUX
198                 polkit_authority = polkit_authority_get_sync (0, 0);
199                 PolkitSubject* subject = polkit_unix_process_new_for_owner (getppid(), 0, -1);
200                 Parameters* parameters = new Parameters;
201                 parameters->dcp_path = *dcp_path;
202                 parameters->device = *device;
203                 parameters->posix_partition = *device;
204                 /* XXX: don't know if this logic is sensible */
205                 if (parameters->posix_partition.size() > 0 && isdigit(parameters->posix_partition[parameters->posix_partition.length() - 1])) {
206                         parameters->posix_partition += "p1";
207                 } else {
208                         parameters->posix_partition += "1";
209                 }
210                 polkit_authority_check_authorization (
211                                 polkit_authority, subject, "com.dcpomatic.write-drive", 0, POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, 0, polkit_callback, parameters
212                                 );
213 #else
214                 string posix_partition = "";
215 #ifdef DCPOMATIC_OSX
216                 posix_partition = *device + "s1";
217 #endif
218                 dcpomatic::write (*dcp_path, *device, posix_partition, nanomsg);
219 #endif
220         }
221
222         return true;
223 } catch (exception& e) {
224         LOG_DISK("Exception (from idle): %1", e.what());
225         return true;
226 }
227
228 int
229 main ()
230 {
231 #ifdef DCPOMATIC_OSX
232         /* On macOS this is running as root, so config_path() will be somewhere in root's
233          * home.  Instead, just write to stdout as the macOS process control stuff will
234          * redirect this to a file in /var/log
235          */
236         dcpomatic_log.reset(new StdoutLog(LogEntry::TYPE_DISK));
237         LOG_DISK("dcpomatic_disk_writer %1 started", dcpomatic_git_commit);
238 #else
239         /* XXX: this is a hack, but I expect we'll need logs and I'm not sure if there's
240          * a better place to put them.
241          */
242         dcpomatic_log.reset(new FileLog(config_path() / "disk_writer.log", LogEntry::TYPE_DISK));
243         LOG_DISK_NC("dcpomatic_disk_writer started");
244 #endif
245
246 #ifdef DCPOMATIC_OSX
247         /* I *think* this consumes the notifyd event that we used to start the process, so we only
248          * get started once per notification.
249          */
250         xpc_set_event_stream_handler("com.apple.notifyd.matching", DISPATCH_TARGET_QUEUE_DEFAULT, ^(xpc_object_t) {});
251 #endif
252
253         try {
254                 nanomsg = new Nanomsg (false);
255         } catch (runtime_error& e) {
256                 LOG_DISK_NC("Could not set up nanomsg socket");
257                 exit (EXIT_FAILURE);
258         }
259
260         Glib::RefPtr<Glib::MainLoop> ml = Glib::MainLoop::create ();
261         Glib::signal_timeout().connect(sigc::ptr_fun(&idle), 500);
262         ml->run ();
263 }