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