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