Supporters update.
[dcpomatic.git] / src / tools / dcpomatic_disk_writer.cc
1 /*
2     Copyright (C) 2019-2021 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
22 #include "lib/compose.hpp"
23 #include "lib/cross.h"
24 #include "lib/dcpomatic_log.h"
25 #include "lib/digester.h"
26 #include "lib/disk_writer_messages.h"
27 #include "lib/exceptions.h"
28 #include "lib/ext.h"
29 #include "lib/file_log.h"
30 #include "lib/state.h"
31 #include "lib/nanomsg.h"
32 #include "lib/util.h"
33 #include "lib/version.h"
34 #include <dcp/warnings.h>
35
36 #ifdef DCPOMATIC_POSIX
37 #include <sys/ioctl.h>
38 #include <sys/types.h>
39 #include <sys/stat.h>
40 #endif
41
42 #ifdef DCPOMATIC_OSX
43 #include "lib/stdout_log.h"
44 #undef nil
45 extern "C" {
46 #include <lwext4/file_dev.h>
47 }
48 #include <unistd.h>
49 #include <xpc/xpc.h>
50 #endif
51
52 #ifdef DCPOMATIC_LINUX
53 #include <polkit/polkit.h>
54 #include <poll.h>
55 #endif
56
57 #ifdef DCPOMATIC_WINDOWS
58 extern "C" {
59 #include <lwext4/file_windows.h>
60 }
61 #endif
62
63 LIBDCP_DISABLE_WARNINGS
64 #include <glibmm.h>
65 LIBDCP_ENABLE_WARNINGS
66
67 #include <unistd.h>
68 #include <sys/types.h>
69 #include <boost/filesystem.hpp>
70 #include <boost/algorithm/string.hpp>
71 #include <iostream>
72
73
74 using std::cin;
75 using std::min;
76 using std::string;
77 using std::runtime_error;
78 using std::exception;
79 using std::vector;
80 using boost::optional;
81
82
83 #define SHORT_TIMEOUT 100
84 #define LONG_TIMEOUT 2000
85
86
87 #ifdef DCPOMATIC_LINUX
88 static PolkitAuthority* polkit_authority = nullptr;
89 #endif
90 static Nanomsg* nanomsg = nullptr;
91
92
93 #ifdef DCPOMATIC_LINUX
94 void
95 polkit_callback (GObject *, GAsyncResult* res, gpointer data)
96 {
97         auto parameters = reinterpret_cast<std::pair<std::function<void ()>, std::function<void ()>>*> (data);
98         GError* error = nullptr;
99         auto result = polkit_authority_check_authorization_finish (polkit_authority, res, &error);
100         bool failed = false;
101
102         if (error) {
103                 LOG_DISK("polkit authority check failed (check_authorization_finish failed with %1)", error->message);
104                 failed = true;
105         } else {
106                 if (polkit_authorization_result_get_is_authorized(result)) {
107                         parameters->first();
108                 } else {
109                         failed = true;
110                         if (polkit_authorization_result_get_is_challenge(result)) {
111                                 LOG_DISK_NC("polkit authority check failed (challenge)");
112                         } else {
113                                 LOG_DISK_NC("polkit authority check failed (not authorized)");
114                         }
115                 }
116         }
117
118         if (failed) {
119                 parameters->second();
120         }
121
122         delete parameters;
123
124         if (result) {
125                 g_object_unref (result);
126         }
127 }
128 #endif
129
130
131 #ifdef DCPOMATIC_LINUX
132 void request_privileges (string action, std::function<void ()> granted, std::function<void ()> denied)
133 #else
134 void request_privileges (string, std::function<void ()> granted, std::function<void ()>)
135 #endif
136 {
137 #ifdef DCPOMATIC_LINUX
138         polkit_authority = polkit_authority_get_sync (0, 0);
139         auto subject = polkit_unix_process_new_for_owner (getppid(), 0, -1);
140
141         auto parameters = new std::pair<std::function<void ()>, std::function<void ()>>(granted, denied);
142         polkit_authority_check_authorization (
143                 polkit_authority, subject, action.c_str(), 0, POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, 0, polkit_callback, parameters
144                 );
145 #else
146         granted ();
147 #endif
148 }
149
150
151 bool
152 idle ()
153 try
154 {
155         using namespace boost::algorithm;
156
157         auto s = nanomsg->receive (0);
158         if (!s) {
159                 return true;
160         }
161
162         LOG_DISK("Writer receives command: %1", *s);
163
164         if (*s == DISK_WRITER_QUIT) {
165                 exit (EXIT_SUCCESS);
166         } else if (*s == DISK_WRITER_PING) {
167                 DiskWriterBackEndResponse::pong().write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
168         } else if (*s == DISK_WRITER_UNMOUNT) {
169                 auto xml_head = nanomsg->receive (LONG_TIMEOUT);
170                 auto xml_body = nanomsg->receive (LONG_TIMEOUT);
171                 if (!xml_head || !xml_body) {
172                         LOG_DISK_NC("Failed to receive unmount request");
173                         throw CommunicationFailedError ();
174                 }
175                 auto xml = *xml_head + *xml_body;
176                 request_privileges (
177                         "com.dcpomatic.write-drive",
178                         [xml]() {
179                                 bool const success = Drive(xml).unmount();
180                                 bool sent_reply = false;
181                                 if (success) {
182                                         sent_reply = DiskWriterBackEndResponse::ok().write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
183                                 } else {
184                                         sent_reply = DiskWriterBackEndResponse::error("Could not unmount drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
185                                 }
186                                 if (!sent_reply) {
187                                         LOG_DISK_NC("CommunicationFailedError in unmount_finished");
188                                         throw CommunicationFailedError ();
189                                 }
190                         },
191                         []() {
192                                 if (!DiskWriterBackEndResponse::error("Could not get permission to unmount drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT)) {
193                                         LOG_DISK_NC("CommunicationFailedError in unmount_finished");
194                                         throw CommunicationFailedError ();
195                                 }
196                         });
197         } else if (*s == DISK_WRITER_WRITE) {
198                 auto device_opt = nanomsg->receive (LONG_TIMEOUT);
199                 if (!device_opt) {
200                         LOG_DISK_NC("Failed to receive write request");
201                         throw CommunicationFailedError();
202                 }
203                 auto device = *device_opt;
204
205                 vector<boost::filesystem::path> dcp_paths;
206                 while (true) {
207                         auto dcp_path_opt = nanomsg->receive (LONG_TIMEOUT);
208                         if (!dcp_path_opt) {
209                                 LOG_DISK_NC("Failed to receive write request");
210                                 throw CommunicationFailedError();
211                         }
212                         if (*dcp_path_opt != "") {
213                                 dcp_paths.push_back(*dcp_path_opt);
214                         } else {
215                                 break;
216                         }
217                 }
218
219                 /* Do some basic sanity checks; this is a bit belt-and-braces but it can't hurt... */
220
221 #ifdef DCPOMATIC_OSX
222                 if (!starts_with(device, "/dev/disk")) {
223                         LOG_DISK ("Will not write to %1", device);
224                         DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
225                         return true;
226                 }
227 #endif
228 #ifdef DCPOMATIC_LINUX
229                 if (!starts_with(device, "/dev/sd") && !starts_with(device, "/dev/hd")) {
230                         LOG_DISK ("Will not write to %1", device);
231                         DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
232                         return true;
233                 }
234 #endif
235 #ifdef DCPOMATIC_WINDOWS
236                 if (!starts_with(device, "\\\\.\\PHYSICALDRIVE")) {
237                         LOG_DISK ("Will not write to %1", device);
238                         DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
239                         return true;
240                 }
241 #endif
242
243                 bool on_drive_list = false;
244                 bool mounted = false;
245                 for (auto const& i: Drive::get()) {
246                         if (i.device() == device) {
247                                 on_drive_list = true;
248                                 mounted = i.mounted();
249                         }
250                 }
251
252                 if (!on_drive_list) {
253                         LOG_DISK ("Will not write to %1 as it's not recognised as a drive", device);
254                         DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
255                         return true;
256                 }
257                 if (mounted) {
258                         LOG_DISK ("Will not write to %1 as it's mounted", device);
259                         DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
260                         return true;
261                 }
262
263                 LOG_DISK("Here we go writing these to %1", device);
264                 for (auto dcp: dcp_paths) {
265                         LOG_DISK("  %1", dcp);
266                 }
267
268                 request_privileges (
269                         "com.dcpomatic.write-drive",
270                         [dcp_paths, device]() {
271 #if defined(DCPOMATIC_LINUX)
272                                 auto posix_partition = device;
273                                 /* XXX: don't know if this logic is sensible */
274                                 if (posix_partition.size() > 0 && isdigit(posix_partition[posix_partition.length() - 1])) {
275                                         posix_partition += "p1";
276                                 } else {
277                                         posix_partition += "1";
278                                 }
279                                 dcpomatic::write (dcp_paths, device, posix_partition, nanomsg);
280 #elif defined(DCPOMATIC_OSX)
281                                 auto fast_device = boost::algorithm::replace_first_copy (device, "/dev/disk", "/dev/rdisk");
282                                 dcpomatic::write (dcp_paths, fast_device, fast_device + "s1", nanomsg);
283 #elif defined(DCPOMATIC_WINDOWS)
284                                 dcpomatic::write (dcp_paths, device, "", nanomsg);
285 #endif
286                         },
287                         []() {
288                                 if (nanomsg) {
289                                         DiskWriterBackEndResponse::error("Could not obtain authorization to write to the drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
290                                 }
291                         });
292         }
293
294         return true;
295 } catch (exception& e) {
296         LOG_DISK("Exception (from idle): %1", e.what());
297         return true;
298 }
299
300 int
301 main ()
302 {
303         dcpomatic_setup_path_encoding();
304
305 #ifdef DCPOMATIC_OSX
306         /* On macOS this is running as root, so config_path() will be somewhere in root's
307          * home.  Instead, just write to stdout as the macOS process control stuff will
308          * redirect this to a file in /var/log
309          */
310         dcpomatic_log.reset(new StdoutLog(LogEntry::TYPE_DISK));
311         LOG_DISK("dcpomatic_disk_writer %1 started uid=%2 euid=%3", dcpomatic_git_commit, getuid(), geteuid());
312 #else
313         /* XXX: this is a hack, but I expect we'll need logs and I'm not sure if there's
314          * a better place to put them.
315          */
316         dcpomatic_log.reset(new FileLog(State::write_path("disk_writer.log"), LogEntry::TYPE_DISK));
317         LOG_DISK_NC("dcpomatic_disk_writer started");
318 #endif
319
320 #ifdef DCPOMATIC_OSX
321         /* I *think* this consumes the notifyd event that we used to start the process, so we only
322          * get started once per notification.
323          */
324         xpc_set_event_stream_handler("com.apple.notifyd.matching", DISPATCH_TARGET_QUEUE_DEFAULT, ^(xpc_object_t) {});
325 #endif
326
327         try {
328                 nanomsg = new Nanomsg (false);
329         } catch (runtime_error& e) {
330                 LOG_DISK_NC("Could not set up nanomsg socket");
331                 exit (EXIT_FAILURE);
332         }
333
334         LOG_DISK_NC("Entering main loop");
335         auto ml = Glib::MainLoop::create ();
336         Glib::signal_timeout().connect(sigc::ptr_fun(&idle), 500);
337         ml->run ();
338 }