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