58ec8e3c46cd6cb6d4b2a228de5086eab0469b2c
[dcpomatic.git] / src / lib / cross_osx.cc
1 /*
2     Copyright (C) 2012-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 "cross.h"
22 #include "compose.hpp"
23 #include "log.h"
24 #include "dcpomatic_log.h"
25 #include "config.h"
26 #include "exceptions.h"
27 #include "warnings.h"
28 #include <dcp/raw_convert.h>
29 #include <glib.h>
30 extern "C" {
31 #include <libavformat/avio.h>
32 }
33 #include <boost/algorithm/string.hpp>
34 #include <boost/regex.hpp>
35 #if BOOST_VERSION >= 106100
36 #include <boost/dll/runtime_symbol_info.hpp>
37 #endif
38 #include <ApplicationServices/ApplicationServices.h>
39 #include <sys/sysctl.h>
40 #include <mach-o/dyld.h>
41 #include <IOKit/pwr_mgt/IOPMLib.h>
42 #include <IOKit/storage/IOMedia.h>
43 #include <DiskArbitration/DADisk.h>
44 #include <DiskArbitration/DiskArbitration.h>
45 #include <CoreFoundation/CFURL.h>
46 #include <sys/types.h>
47 #include <ifaddrs.h>
48 #include <netinet/in.h>
49 #include <arpa/inet.h>
50 #include <fstream>
51 #include <cstring>
52
53 #include "i18n.h"
54
55 using std::pair;
56 using std::list;
57 using std::ifstream;
58 using std::string;
59 using std::wstring;
60 using std::make_pair;
61 using std::vector;
62 using std::cerr;
63 using std::cout;
64 using std::runtime_error;
65 using std::map;
66 using std::shared_ptr;
67 using boost::optional;
68 using std::function;
69
70 /** @param s Number of seconds to sleep for */
71 void
72 dcpomatic_sleep_seconds (int s)
73 {
74         sleep (s);
75 }
76
77 void
78 dcpomatic_sleep_milliseconds (int ms)
79 {
80         usleep (ms * 1000);
81 }
82
83 /** @return A string of CPU information (model name etc.) */
84 string
85 cpu_info ()
86 {
87         string info;
88
89         char buffer[64];
90         size_t N = sizeof (buffer);
91         if (sysctlbyname ("machdep.cpu.brand_string", buffer, &N, 0, 0) == 0) {
92                 info = buffer;
93         }
94
95         return info;
96 }
97
98
99 boost::filesystem::path
100 directory_containing_executable ()
101 {
102         return boost::dll::program_location().parent_path();
103 }
104
105
106 boost::filesystem::path
107 resources_path ()
108 {
109         return directory_containing_executable().parent_path() / "Resources";
110 }
111
112
113 boost::filesystem::path
114 xsd_path ()
115 {
116         return resources_path() / "xsd";
117 }
118
119
120 boost::filesystem::path
121 tags_path ()
122 {
123         return resources_path() / "tags";
124 }
125
126
127 void
128 run_ffprobe (boost::filesystem::path content, boost::filesystem::path out)
129 {
130         boost::filesystem::path path = directory_containing_executable () / "ffprobe";
131
132         string ffprobe = "\"" + path.string() + "\" \"" + content.string() + "\" 2> \"" + out.string() + "\"";
133         LOG_GENERAL (N_("Probing with %1"), ffprobe);
134         system (ffprobe.c_str ());
135 }
136
137
138 list<pair<string, string> >
139 mount_info ()
140 {
141         list<pair<string, string> > m;
142         return m;
143 }
144
145 boost::filesystem::path
146 openssl_path ()
147 {
148         return directory_containing_executable() / "openssl";
149 }
150
151
152 #ifdef DCPOMATIC_DISK
153 /* Note: this isn't actually used at the moment as the disk writer is started as a service */
154 boost::filesystem::path
155 disk_writer_path ()
156 {
157         return directory_containing_executable() / "dcpomatic2_disk_writer";
158 }
159 #endif
160
161 /* Apparently there is no way to create an ofstream using a UTF-8
162    filename under Windows.  We are hence reduced to using fopen
163    with this wrapper.
164 */
165 FILE *
166 fopen_boost (boost::filesystem::path p, string t)
167 {
168         return fopen (p.c_str(), t.c_str ());
169 }
170
171 int
172 dcpomatic_fseek (FILE* stream, int64_t offset, int whence)
173 {
174         return fseek (stream, offset, whence);
175 }
176
177 void
178 Waker::nudge ()
179 {
180
181 }
182
183 Waker::Waker ()
184 {
185         boost::mutex::scoped_lock lm (_mutex);
186         IOPMAssertionCreateWithName (kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, CFSTR ("Encoding DCP"), &_assertion_id);
187 }
188
189 Waker::~Waker ()
190 {
191         boost::mutex::scoped_lock lm (_mutex);
192         IOPMAssertionRelease (_assertion_id);
193 }
194
195 void
196 start_tool (string executable, string app)
197 {
198         boost::filesystem::path exe_path = directory_containing_executable();
199         exe_path = exe_path.parent_path (); // Contents
200         exe_path = exe_path.parent_path (); // DCP-o-matic 2.app
201         exe_path = exe_path.parent_path (); // Applications
202         exe_path /= app;
203         exe_path /= "Contents";
204         exe_path /= "MacOS";
205         exe_path /= executable;
206
207         pid_t pid = fork ();
208         if (pid == 0) {
209                 LOG_GENERAL ("start_tool %1 %2 with path %3", executable, app, exe_path.string());
210                 int const r = system (exe_path.string().c_str());
211                 exit (WEXITSTATUS (r));
212         } else if (pid == -1) {
213                 LOG_ERROR_NC("Fork failed in start_tool");
214         }
215 }
216
217
218 void
219 start_batch_converter ()
220 {
221         start_tool ("dcpomatic2_batch", "DCP-o-matic\\ 2\\ Batch\\ Converter.app");
222 }
223
224
225 void
226 start_player ()
227 {
228         start_tool ("dcpomatic2_player", "DCP-o-matic\\ 2\\ Player.app");
229 }
230
231
232 uint64_t
233 thread_id ()
234 {
235         return (uint64_t) pthread_self ();
236 }
237
238 int
239 avio_open_boost (AVIOContext** s, boost::filesystem::path file, int flags)
240 {
241         return avio_open (s, file.c_str(), flags);
242 }
243
244 boost::filesystem::path
245 home_directory ()
246 {
247                 return getenv("HOME");
248 }
249
250 /** @return true if this process is a 32-bit one running on a 64-bit-capable OS */
251 bool
252 running_32_on_64 ()
253 {
254         /* I'm assuming nobody does this on OS X */
255         return false;
256 }
257
258 static optional<string>
259 get_vendor (CFDictionaryRef& description)
260 {
261         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionDeviceVendorKey);
262         if (!str) {
263                 return {};
264         }
265
266         auto c_str = CFStringGetCStringPtr ((CFStringRef) str, kCFStringEncodingUTF8);
267         if (!c_str) {
268                 return {};
269         }
270
271         string s (c_str);
272         boost::algorithm::trim (s);
273         return s;
274 }
275
276 static optional<string>
277 get_model (CFDictionaryRef& description)
278 {
279         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionDeviceModelKey);
280         if (!str) {
281                 return {};
282         }
283
284         auto c_str = CFStringGetCStringPtr ((CFStringRef) str, kCFStringEncodingUTF8);
285         if (!c_str) {
286                 return {};
287         }
288
289         string s (c_str);
290         boost::algorithm::trim (s);
291         return s;
292 }
293
294 struct MediaPath
295 {
296         bool real;       ///< true for a "real" disk, false for a synthesized APFS one
297         std::string prt; ///< "PRT" entry from the media path
298 };
299
300 static optional<MediaPath>
301 analyse_media_path (CFDictionaryRef& description)
302 {
303         using namespace boost::algorithm;
304
305         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionMediaPathKey);
306         if (!str) {
307                 LOG_DISK_NC("There is no MediaPathKey (no dictionary value)");
308                 return {};
309         }
310
311         auto path_key_cstr = CFStringGetCStringPtr((CFStringRef) str, kCFStringEncodingUTF8);
312         if (!path_key_cstr) {
313                 LOG_DISK_NC("There is no MediaPathKey (no cstring)");
314                 return {};
315         }
316
317         string path(path_key_cstr);
318         LOG_DISK("MediaPathKey is %1", path);
319
320         if (path.find("/IOHDIXController") != string::npos) {
321                 /* This is a disk image, so we completely ignore it */
322                 LOG_DISK_NC("Ignoring this as it seems to be a disk image");
323                 return {};
324         }
325
326         MediaPath mp;
327         if (starts_with(path, "IODeviceTree:")) {
328                 mp.real = true;
329         } else if (starts_with(path, "IOService:")) {
330                 mp.real = false;
331         } else {
332                 return {};
333         }
334
335         vector<string> bits;
336         split(bits, path, boost::is_any_of("/"));
337         for (auto i: bits) {
338                 if (starts_with(i, "PRT")) {
339                         mp.prt = i;
340                 }
341         }
342
343         return mp;
344 }
345
346 static bool
347 is_whole_drive (DADiskRef& disk)
348 {
349         io_service_t service = DADiskCopyIOMedia (disk);
350         CFTypeRef whole_media_ref = IORegistryEntryCreateCFProperty (service, CFSTR(kIOMediaWholeKey), kCFAllocatorDefault, 0);
351         bool whole_media = false;
352         if (whole_media_ref) {
353                 whole_media = CFBooleanGetValue((CFBooleanRef) whole_media_ref);
354                 CFRelease (whole_media_ref);
355         }
356         IOObjectRelease (service);
357         return whole_media;
358 }
359
360 static optional<boost::filesystem::path>
361 mount_point (CFDictionaryRef& description)
362 {
363         auto volume_path_key = (CFURLRef) CFDictionaryGetValue (description, kDADiskDescriptionVolumePathKey);
364         if (!volume_path_key) {
365                 return {};
366         }
367
368         char mount_path_buffer[1024];
369         if (!CFURLGetFileSystemRepresentation(volume_path_key, false, (UInt8 *) mount_path_buffer, sizeof(mount_path_buffer))) {
370                 return {};
371         }
372         return boost::filesystem::path(mount_path_buffer);
373 }
374
375 /* Here follows some rather intricate and (probably) fragile code to find the list of available
376  * "real" drives on macOS that we might want to write a DCP to.
377  *
378  * We use the Disk Arbitration framework to give us a series of mount_points (/dev/disk0, /dev/disk1,
379  * /dev/disk1s1 and so on) and we use the API to gather useful information about these mount_points into
380  * a vector of Disk structs.
381  *
382  * Then we read the Disks that we found and try to derive a list of drives that we should offer to the
383  * user, with details of whether those drives are currently mounted or not.
384  *
385  * At the basic level we find the "disk"-level mount_points, looking at whether any of their partitions are mounted.
386  *
387  * This is complicated enormously by recent-ish macOS versions' habit of making `synthesized' volumes which
388  * reflect data in `real' partitions.  So, for example, we might have a real (physical) drive /dev/disk2 with
389  * a partition /dev/disk2s2 whose content is made into a synthesized /dev/disk3, itself containing some partitions
390  * which are mounted.  /dev/disk2s2 is not considered to be mounted, in this case.  So we need to know that
391  * disk2s2 is related to disk3 so we can consider disk2s2 as mounted if any parts of disk3 are.  In order to do
392  * this I am picking out what looks like a suitable identifier prefixed with PRT from the MediaContentKey.
393  * If disk2s2 and disk3 have the same PRT code I am assuming they are linked.
394  *
395  * Lots of this is guesswork and may be broken.  In my defence the documentation that I have been able to
396  * unearth is, to put it impolitely, crap.
397  */
398
399 struct Disk
400 {
401         string mount_point;
402         optional<string> vendor;
403         optional<string> model;
404         bool real;
405         string prt;
406         bool whole;
407         vector<boost::filesystem::path> mount_points;
408         unsigned long size;
409 };
410
411 static void
412 disk_appeared (DADiskRef disk, void* context)
413 {
414         auto bsd_name = DADiskGetBSDName (disk);
415         if (!bsd_name) {
416                 LOG_DISK_NC("Disk with no BSDName appeared");
417                 return;
418         }
419         LOG_DISK("%1 appeared", bsd_name);
420
421         Disk this_disk;
422
423         this_disk.mount_point = string("/dev/") + bsd_name;
424         LOG_DISK("Mount point is %1", this_disk.mount_point);
425
426         CFDictionaryRef description = DADiskCopyDescription (disk);
427
428         this_disk.vendor = get_vendor (description);
429         this_disk.model = get_model (description);
430         LOG_DISK("Vendor/model: %1 %2", this_disk.vendor.get_value_or("[none]"), this_disk.model.get_value_or("[none]"));
431
432         auto media_path = analyse_media_path (description);
433         if (!media_path) {
434                 LOG_DISK("Finding media path for %1 failed", bsd_name);
435                 return;
436         }
437
438         this_disk.real = media_path->real;
439         this_disk.prt = media_path->prt;
440         this_disk.whole = is_whole_drive (disk);
441         auto mp = mount_point (description);
442         if (mp) {
443                 this_disk.mount_points.push_back (*mp);
444         }
445
446         LOG_DISK(
447                 "%1 prt %2 whole %3 mounted %4",
448                  this_disk.real ? "Real" : "Synth",
449                  this_disk.prt,
450                  this_disk.whole ? "whole" : "part",
451                  mp ? ("mounted at " + mp->string()) : "unmounted"
452                 );
453
454         auto media_size_cstr = CFDictionaryGetValue (description, kDADiskDescriptionMediaSizeKey);
455         if (!media_size_cstr) {
456                 LOG_DISK_NC("Could not read media size");
457                 return;
458         }
459
460         CFNumberGetValue ((CFNumberRef) media_size_cstr, kCFNumberLongType, &this_disk.size);
461         CFRelease (description);
462
463         reinterpret_cast<vector<Disk>*>(context)->push_back(this_disk);
464 }
465
466 vector<Drive>
467 Drive::get ()
468 {
469         using namespace boost::algorithm;
470         vector<Disk> disks;
471
472         DASessionRef session = DASessionCreate(kCFAllocatorDefault);
473         if (!session) {
474                 return {};
475         }
476
477         DARegisterDiskAppearedCallback (session, NULL, disk_appeared, &disks);
478         CFRunLoopRef run_loop = CFRunLoopGetCurrent ();
479         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
480         CFRunLoopStop (run_loop);
481         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0);
482         DAUnregisterCallback(session, (void *) disk_appeared, &disks);
483         CFRelease(session);
484
485         /* Mark disks containing mounted partitions as themselves mounted */
486         for (auto& i: disks) {
487                 if (!i.whole) {
488                         continue;
489                 }
490                 for (auto& j: disks) {
491                         if (!j.mount_points.empty() && starts_with(j.mount_point, i.mount_point)) {
492                                 LOG_DISK("Marking %1 as mounted because %2 is", i.mount_point, j.mount_point);
493                                 std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
494                         }
495                 }
496         }
497
498         /* Make a map of the PRT codes and mount points of mounted, synthesized disks */
499         map<string, vector<boost::filesystem::path> > mounted_synths;
500         for (auto& i: disks) {
501                 if (!i.real && !i.mount_points.empty()) {
502                         LOG_DISK("Found a mounted synth %1 with %2", i.mount_point, i.prt);
503                         mounted_synths[i.prt] = i.mount_points;
504                 }
505         }
506
507         /* Mark containers of those mounted synths as themselves mounted */
508         for (auto& i: disks) {
509                 if (i.real) {
510                         auto j = mounted_synths.find(i.prt);
511                         if (j != mounted_synths.end()) {
512                                 LOG_DISK("Marking %1 (%2) as mounted because it contains a mounted synth", i.mount_point, i.prt);
513                                 std::copy(j->second.begin(), j->second.end(), back_inserter(i.mount_points));
514                         }
515                 }
516         }
517
518         vector<Drive> drives;
519         for (auto& i: disks) {
520                 if (i.whole) {
521                         /* A whole disk that is not a container for a mounted synth */
522                         drives.push_back(Drive(i.mount_point, i.mount_points, i.size, i.vendor, i.model));
523                         LOG_DISK_NC(drives.back().log_summary());
524                 }
525         }
526         return drives;
527 }
528
529
530 boost::filesystem::path
531 config_path ()
532 {
533         boost::filesystem::path p;
534         p /= g_get_home_dir ();
535         p /= "Library";
536         p /= "Preferences";
537         p /= "com.dcpomatic";
538         p /= "2";
539         return p;
540 }
541
542
543 void done_callback(DADiskRef, DADissenterRef dissenter, void* context)
544 {
545         LOG_DISK_NC("Unmount finished");
546         bool* success = reinterpret_cast<bool*> (context);
547         if (dissenter) {
548                 LOG_DISK("Error: %1", DADissenterGetStatus(dissenter));
549                 *success = false;
550         } else {
551                 LOG_DISK_NC("Successful");
552                 *success = true;
553         }
554 }
555
556
557 bool
558 Drive::unmount ()
559 {
560         LOG_DISK_NC("Unmount operation started");
561
562         DASessionRef session = DASessionCreate(kCFAllocatorDefault);
563         if (!session) {
564                 return false;
565         }
566
567         DADiskRef disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, _device.c_str());
568         if (!disk) {
569                 return false;
570         }
571         LOG_DISK("Requesting unmount of %1 from %2", _device, thread_id());
572         bool success = false;
573         DADiskUnmount(disk, kDADiskUnmountOptionWhole, &done_callback, &success);
574         CFRelease (disk);
575
576         CFRunLoopRef run_loop = CFRunLoopGetCurrent ();
577         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
578         CFRunLoopStop (run_loop);
579         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, 0);
580         CFRelease(session);
581
582         LOG_DISK_NC("End of unmount");
583         return success;
584 }
585
586
587 void
588 disk_write_finished ()
589 {
590
591 }
592
593
594 void
595 make_foreground_application ()
596 {
597         ProcessSerialNumber serial;
598 DCPOMATIC_DISABLE_WARNINGS
599         GetCurrentProcess (&serial);
600 DCPOMATIC_ENABLE_WARNINGS
601         TransformProcessType (&serial, kProcessTransformToForegroundApplication);
602 }
603
604
605 string
606 dcpomatic::get_process_id ()
607 {
608         return dcp::raw_convert<string>(getpid());
609 }
610
611
612 boost::filesystem::path
613 fix_long_path (boost::filesystem::path path)
614 {
615         return path;
616 }