Extract part of analyse_media_path to cross_common for tests.
[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 struct Disk
391 {
392         string mount_point;
393         optional<string> vendor;
394         optional<string> model;
395         bool real;
396         string prt;
397         bool whole;
398         vector<boost::filesystem::path> mount_points;
399         unsigned long size;
400 };
401
402
403 static void
404 disk_appeared (DADiskRef disk, void* context)
405 {
406         auto bsd_name = DADiskGetBSDName (disk);
407         if (!bsd_name) {
408                 LOG_DISK_NC("Disk with no BSDName appeared");
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         LOG_DISK("Mount point is %1", this_disk.mount_point);
417
418         CFDictionaryRef description = DADiskCopyDescription (disk);
419
420         this_disk.vendor = get_vendor (description);
421         this_disk.model = get_model (description);
422         LOG_DISK("Vendor/model: %1 %2", this_disk.vendor.get_value_or("[none]"), this_disk.model.get_value_or("[none]"));
423
424         auto media_path = analyse_media_path (description);
425         if (!media_path) {
426                 LOG_DISK("Finding media path for %1 failed", bsd_name);
427                 return;
428         }
429
430         this_disk.real = media_path->real;
431         this_disk.prt = media_path->prt;
432         this_disk.whole = is_whole_drive (disk);
433         auto mp = mount_point (description);
434         if (mp) {
435                 this_disk.mount_points.push_back (*mp);
436         }
437
438         LOG_DISK(
439                 "%1 prt=%2 %3 %4",
440                  this_disk.real ? "Real" : "Synth",
441                  this_disk.prt,
442                  this_disk.whole ? "whole" : "part",
443                  mp ? ("mounted at " + mp->string()) : "unmounted"
444                 );
445
446         auto media_size_cstr = CFDictionaryGetValue (description, kDADiskDescriptionMediaSizeKey);
447         if (!media_size_cstr) {
448                 LOG_DISK_NC("Could not read media size");
449                 return;
450         }
451
452         CFNumberGetValue ((CFNumberRef) media_size_cstr, kCFNumberLongType, &this_disk.size);
453         CFRelease (description);
454
455         reinterpret_cast<vector<Disk>*>(context)->push_back(this_disk);
456 }
457
458
459 vector<Drive>
460 Drive::get ()
461 {
462         using namespace boost::algorithm;
463         vector<Disk> disks;
464
465         auto session = DASessionCreate(kCFAllocatorDefault);
466         if (!session) {
467                 return {};
468         }
469
470         DARegisterDiskAppearedCallback (session, NULL, disk_appeared, &disks);
471         auto run_loop = CFRunLoopGetCurrent ();
472         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
473         CFRunLoopStop (run_loop);
474         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0);
475         DAUnregisterCallback(session, (void *) disk_appeared, &disks);
476         CFRelease(session);
477
478         /* Mark disks containing mounted partitions as themselves mounted */
479         for (auto& i: disks) {
480                 if (!i.whole) {
481                         continue;
482                 }
483                 for (auto& j: disks) {
484                         if (!j.mount_points.empty() && starts_with(j.mount_point, i.mount_point)) {
485                                 LOG_DISK("Marking %1 as mounted because %2 is", i.mount_point, j.mount_point);
486                                 std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
487                         }
488                 }
489         }
490
491         /* Make a map of the PRT codes and mount points of mounted, synthesized disks */
492         map<string, vector<boost::filesystem::path>> mounted_synths;
493         for (auto& i: disks) {
494                 if (!i.real && !i.mount_points.empty()) {
495                         LOG_DISK("Found a mounted synth %1 with %2", i.mount_point, i.prt);
496                         mounted_synths[i.prt] = i.mount_points;
497                 }
498         }
499
500         /* Mark containers of those mounted synths as themselves mounted */
501         for (auto& i: disks) {
502                 if (i.real) {
503                         auto j = mounted_synths.find(i.prt);
504                         if (j != mounted_synths.end()) {
505                                 LOG_DISK("Marking %1 (%2) as mounted because it contains a mounted synth", i.mount_point, i.prt);
506                                 std::copy(j->second.begin(), j->second.end(), back_inserter(i.mount_points));
507                         }
508                 }
509         }
510
511         vector<Drive> drives;
512         for (auto& i: disks) {
513                 if (i.whole) {
514                         /* A whole disk that is not a container for a mounted synth */
515                         drives.push_back(Drive(i.mount_point, i.mount_points, i.size, i.vendor, i.model));
516                         LOG_DISK_NC(drives.back().log_summary());
517                 }
518         }
519         return drives;
520 }
521
522
523 boost::filesystem::path
524 config_path (optional<string> version)
525 {
526         boost::filesystem::path p;
527         p /= g_get_home_dir ();
528         p /= "Library";
529         p /= "Preferences";
530         p /= "com.dcpomatic";
531         p /= "2";
532         if (version) {
533                 p /= *version;
534         }
535         return p;
536 }
537
538
539 void done_callback(DADiskRef, DADissenterRef dissenter, void* context)
540 {
541         LOG_DISK_NC("Unmount finished");
542         bool* success = reinterpret_cast<bool*> (context);
543         if (dissenter) {
544                 LOG_DISK("Error: %1", DADissenterGetStatus(dissenter));
545                 *success = false;
546         } else {
547                 LOG_DISK_NC("Successful");
548                 *success = true;
549         }
550 }
551
552
553 bool
554 Drive::unmount ()
555 {
556         LOG_DISK_NC("Unmount operation started");
557
558         auto session = DASessionCreate(kCFAllocatorDefault);
559         if (!session) {
560                 return false;
561         }
562
563         auto disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, _device.c_str());
564         if (!disk) {
565                 return false;
566         }
567         LOG_DISK("Requesting unmount of %1 from %2", _device, thread_id());
568         bool success = false;
569         DADiskUnmount(disk, kDADiskUnmountOptionWhole, &done_callback, &success);
570         CFRelease (disk);
571
572         CFRunLoopRef run_loop = CFRunLoopGetCurrent ();
573         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
574         CFRunLoopStop (run_loop);
575         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, 0);
576         CFRelease(session);
577
578         LOG_DISK_NC("End of unmount");
579         return success;
580 }
581
582
583 void
584 disk_write_finished ()
585 {
586
587 }
588
589
590 void
591 make_foreground_application ()
592 {
593         ProcessSerialNumber serial;
594 DCPOMATIC_DISABLE_WARNINGS
595         GetCurrentProcess (&serial);
596 DCPOMATIC_ENABLE_WARNINGS
597         TransformProcessType (&serial, kProcessTransformToForegroundApplication);
598 }
599
600
601 string
602 dcpomatic::get_process_id ()
603 {
604         return dcp::raw_convert<string>(getpid());
605 }
606
607
608 boost::filesystem::path
609 fix_long_path (boost::filesystem::path path)
610 {
611         return path;
612 }
613
614
615 bool
616 show_in_file_manager (boost::filesystem::path, boost::filesystem::path select)
617 {
618         int r = system (String::compose("open -R \"%1\"", select.string()).c_str());
619         return static_cast<bool>(WEXITSTATUS(r));
620 }
621