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