018516abbf0a337e3eb319715f995629a860da3b
[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::filesystem::canonical(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 static optional<OSXMediaPath>
313 analyse_media_path (CFDictionaryRef& description)
314 {
315         using namespace boost::algorithm;
316
317         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionMediaPathKey);
318         if (!str) {
319                 LOG_DISK_NC("There is no MediaPathKey (no dictionary value)");
320                 return {};
321         }
322
323         auto path_key_cstr = CFStringGetCStringPtr((CFStringRef) str, kCFStringEncodingUTF8);
324         if (!path_key_cstr) {
325                 LOG_DISK_NC("There is no MediaPathKey (no cstring)");
326                 return {};
327         }
328
329         string path(path_key_cstr);
330         LOG_DISK("MediaPathKey is %1", path);
331         return analyse_osx_media_path (path);
332 }
333
334
335 static bool
336 is_whole_drive (DADiskRef& disk)
337 {
338         io_service_t service = DADiskCopyIOMedia (disk);
339         CFTypeRef whole_media_ref = IORegistryEntryCreateCFProperty (service, CFSTR(kIOMediaWholeKey), kCFAllocatorDefault, 0);
340         bool whole_media = false;
341         if (whole_media_ref) {
342                 whole_media = CFBooleanGetValue((CFBooleanRef) whole_media_ref);
343                 CFRelease (whole_media_ref);
344         }
345         IOObjectRelease (service);
346         return whole_media;
347 }
348
349
350 static optional<boost::filesystem::path>
351 mount_point (CFDictionaryRef& description)
352 {
353         auto volume_path_key = (CFURLRef) CFDictionaryGetValue (description, kDADiskDescriptionVolumePathKey);
354         if (!volume_path_key) {
355                 return {};
356         }
357
358         char mount_path_buffer[1024];
359         if (!CFURLGetFileSystemRepresentation(volume_path_key, false, (UInt8 *) mount_path_buffer, sizeof(mount_path_buffer))) {
360                 return {};
361         }
362         return boost::filesystem::path(mount_path_buffer);
363 }
364
365
366 /* Here follows some rather intricate and (probably) fragile code to find the list of available
367  * "real" drives on macOS that we might want to write a DCP to.
368  *
369  * We use the Disk Arbitration framework to give us a series of mount_points (/dev/disk0, /dev/disk1,
370  * /dev/disk1s1 and so on) and we use the API to gather useful information about these mount_points into
371  * a vector of Disk structs.
372  *
373  * Then we read the Disks that we found and try to derive a list of drives that we should offer to the
374  * user, with details of whether those drives are currently mounted or not.
375  *
376  * At the basic level we find the "disk"-level mount_points, looking at whether any of their partitions are mounted.
377  *
378  * This is complicated enormously by recent-ish macOS versions' habit of making `synthesized' volumes which
379  * reflect data in `real' partitions.  So, for example, we might have a real (physical) drive /dev/disk2 with
380  * a partition /dev/disk2s2 whose content is made into a synthesized /dev/disk3, itself containing some partitions
381  * which are mounted.  /dev/disk2s2 is not considered to be mounted, in this case.  So we need to know that
382  * disk2s2 is related to disk3 so we can consider disk2s2 as mounted if any parts of disk3 are.  In order to do
383  * this I am picking out what looks like a suitable identifier prefixed with PRT from the MediaContentKey.
384  * If disk2s2 and disk3 have the same PRT code I am assuming they are linked.
385  *
386  * Lots of this is guesswork and may be broken.  In my defence the documentation that I have been able to
387  * unearth is, to put it impolitely, crap.
388  */
389
390 static void
391 disk_appeared (DADiskRef disk, void* context)
392 {
393         auto bsd_name = DADiskGetBSDName (disk);
394         if (!bsd_name) {
395                 LOG_DISK_NC("Disk with no BSDName appeared");
396                 return;
397         }
398         LOG_DISK("%1 appeared", bsd_name);
399
400         OSXDisk this_disk;
401
402         this_disk.mount_point = string("/dev/") + bsd_name;
403         LOG_DISK("Mount point is %1", this_disk.mount_point);
404
405         CFDictionaryRef description = DADiskCopyDescription (disk);
406
407         this_disk.vendor = get_vendor (description);
408         this_disk.model = get_model (description);
409         LOG_DISK("Vendor/model: %1 %2", this_disk.vendor.get_value_or("[none]"), this_disk.model.get_value_or("[none]"));
410
411         auto media_path = analyse_media_path (description);
412         if (!media_path) {
413                 LOG_DISK("Finding media path for %1 failed", bsd_name);
414                 return;
415         }
416
417         this_disk.real = media_path->real;
418         this_disk.prt = media_path->prt;
419         this_disk.whole = is_whole_drive (disk);
420         auto mp = mount_point (description);
421         if (mp) {
422                 this_disk.mount_points.push_back (*mp);
423         }
424
425         LOG_DISK(
426                 "%1 prt=%2 %3 %4",
427                  this_disk.real ? "Real" : "Synth",
428                  this_disk.prt,
429                  this_disk.whole ? "whole" : "part",
430                  mp ? ("mounted at " + mp->string()) : "unmounted"
431                 );
432
433         auto media_size_cstr = CFDictionaryGetValue (description, kDADiskDescriptionMediaSizeKey);
434         if (!media_size_cstr) {
435                 LOG_DISK_NC("Could not read media size");
436                 return;
437         }
438
439         CFNumberGetValue ((CFNumberRef) media_size_cstr, kCFNumberLongType, &this_disk.size);
440         CFRelease (description);
441
442         reinterpret_cast<vector<OSXDisk>*>(context)->push_back(this_disk);
443 }
444
445
446 vector<Drive>
447 Drive::get ()
448 {
449         using namespace boost::algorithm;
450         vector<OSXDisk> disks;
451
452         auto session = DASessionCreate(kCFAllocatorDefault);
453         if (!session) {
454                 return {};
455         }
456
457         DARegisterDiskAppearedCallback (session, NULL, disk_appeared, &disks);
458         auto run_loop = CFRunLoopGetCurrent ();
459         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
460         CFRunLoopStop (run_loop);
461         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0);
462         DAUnregisterCallback(session, (void *) disk_appeared, &disks);
463         CFRelease(session);
464
465         return osx_disks_to_drives (disks);
466 }
467
468
469 boost::filesystem::path
470 config_path (optional<string> version)
471 {
472         boost::filesystem::path p;
473         p /= g_get_home_dir ();
474         p /= "Library";
475         p /= "Preferences";
476         p /= "com.dcpomatic";
477         p /= "2";
478         if (version) {
479                 p /= *version;
480         }
481         return p;
482 }
483
484
485 void done_callback(DADiskRef, DADissenterRef dissenter, void* context)
486 {
487         LOG_DISK_NC("Unmount finished");
488         bool* success = reinterpret_cast<bool*> (context);
489         if (dissenter) {
490                 LOG_DISK("Error: %1", DADissenterGetStatus(dissenter));
491                 *success = false;
492         } else {
493                 LOG_DISK_NC("Successful");
494                 *success = true;
495         }
496 }
497
498
499 bool
500 Drive::unmount ()
501 {
502         LOG_DISK_NC("Unmount operation started");
503
504         auto session = DASessionCreate(kCFAllocatorDefault);
505         if (!session) {
506                 return false;
507         }
508
509         auto disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, _device.c_str());
510         if (!disk) {
511                 return false;
512         }
513         LOG_DISK("Requesting unmount of %1 from %2", _device, thread_id());
514         bool success = false;
515         DADiskUnmount(disk, kDADiskUnmountOptionWhole, &done_callback, &success);
516         CFRelease (disk);
517
518         CFRunLoopRef run_loop = CFRunLoopGetCurrent ();
519         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
520         CFRunLoopStop (run_loop);
521         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, 0);
522         CFRelease(session);
523
524         LOG_DISK_NC("End of unmount");
525         return success;
526 }
527
528
529 void
530 disk_write_finished ()
531 {
532
533 }
534
535
536 void
537 make_foreground_application ()
538 {
539         ProcessSerialNumber serial;
540 DCPOMATIC_DISABLE_WARNINGS
541         GetCurrentProcess (&serial);
542 DCPOMATIC_ENABLE_WARNINGS
543         TransformProcessType (&serial, kProcessTransformToForegroundApplication);
544 }
545
546
547 string
548 dcpomatic::get_process_id ()
549 {
550         return dcp::raw_convert<string>(getpid());
551 }
552
553
554 boost::filesystem::path
555 fix_long_path (boost::filesystem::path path)
556 {
557         return path;
558 }
559
560
561 bool
562 show_in_file_manager (boost::filesystem::path, boost::filesystem::path select)
563 {
564         int r = system (String::compose("open -R \"%1\"", select.string()).c_str());
565         return static_cast<bool>(WEXITSTATUS(r));
566 }
567