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