b214b6359767b21749095b5f83a06b3466fe6ff8
[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 <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/regex.hpp>
35 #if BOOST_VERSION >= 106100
36 #include <boost/dll/runtime_symbol_info.hpp>
37 #endif
38 #include <ApplicationServices/ApplicationServices.h>
39 #include <sys/sysctl.h>
40 #include <mach-o/dyld.h>
41 #include <IOKit/pwr_mgt/IOPMLib.h>
42 #include <IOKit/storage/IOMedia.h>
43 #include <DiskArbitration/DADisk.h>
44 #include <DiskArbitration/DiskArbitration.h>
45 #include <CoreFoundation/CFURL.h>
46 #include <sys/types.h>
47 #include <ifaddrs.h>
48 #include <netinet/in.h>
49 #include <arpa/inet.h>
50 #include <fstream>
51 #include <cstring>
52
53 #include "i18n.h"
54
55
56 using std::pair;
57 using std::list;
58 using std::ifstream;
59 using std::string;
60 using std::make_pair;
61 using std::vector;
62 using std::cerr;
63 using std::cout;
64 using std::runtime_error;
65 using std::map;
66 using std::shared_ptr;
67 using boost::optional;
68 using std::function;
69
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
79 void
80 dcpomatic_sleep_milliseconds (int ms)
81 {
82         usleep (ms * 1000);
83 }
84
85
86 /** @return A string of CPU information (model name etc.) */
87 string
88 cpu_info ()
89 {
90         string info;
91
92         char buffer[64];
93         size_t N = sizeof (buffer);
94         if (sysctlbyname("machdep.cpu.brand_string", buffer, &N, 0, 0) == 0) {
95                 info = buffer;
96         }
97
98         return info;
99 }
100
101
102 boost::filesystem::path
103 directory_containing_executable ()
104 {
105         return boost::filesystem::canonical(boost::dll::program_location()).parent_path();
106 }
107
108
109 boost::filesystem::path
110 resources_path ()
111 {
112         return directory_containing_executable().parent_path() / "Resources";
113 }
114
115
116 boost::filesystem::path
117 libdcp_resources_path ()
118 {
119         return resources_path();
120 }
121
122
123 void
124 run_ffprobe (boost::filesystem::path content, boost::filesystem::path out)
125 {
126         auto path = directory_containing_executable () / "ffprobe";
127
128         string ffprobe = "\"" + path.string() + "\" \"" + content.string() + "\" 2> \"" + out.string() + "\"";
129         LOG_GENERAL (N_("Probing with %1"), ffprobe);
130         system (ffprobe.c_str ());
131 }
132
133
134
135 list<pair<string, string>>
136 mount_info ()
137 {
138         return {};
139 }
140
141
142 boost::filesystem::path
143 openssl_path ()
144 {
145         return directory_containing_executable() / "openssl";
146 }
147
148
149 #ifdef DCPOMATIC_DISK
150 /* Note: this isn't actually used at the moment as the disk writer is started as a service */
151 boost::filesystem::path
152 disk_writer_path ()
153 {
154         return directory_containing_executable() / "dcpomatic2_disk_writer";
155 }
156 #endif
157
158
159 void
160 Waker::nudge ()
161 {
162
163 }
164
165
166 Waker::Waker ()
167 {
168         boost::mutex::scoped_lock lm (_mutex);
169         IOPMAssertionCreateWithName (kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, CFSTR ("Encoding DCP"), &_assertion_id);
170 }
171
172
173 Waker::~Waker ()
174 {
175         boost::mutex::scoped_lock lm (_mutex);
176         IOPMAssertionRelease (_assertion_id);
177 }
178
179
180 void
181 start_tool (string executable, string app)
182 {
183         auto exe_path = directory_containing_executable();
184         exe_path = exe_path.parent_path(); // Contents
185         exe_path = exe_path.parent_path(); // DCP-o-matic 2.app
186         exe_path = exe_path.parent_path(); // Applications
187         exe_path /= app;
188         exe_path /= "Contents";
189         exe_path /= "MacOS";
190         exe_path /= executable;
191
192         pid_t pid = fork ();
193         if (pid == 0) {
194                 LOG_GENERAL ("start_tool %1 %2 with path %3", executable, app, exe_path.string());
195                 int const r = system (exe_path.string().c_str());
196                 exit (WEXITSTATUS (r));
197         } else if (pid == -1) {
198                 LOG_ERROR_NC("Fork failed in start_tool");
199         }
200 }
201
202
203 void
204 start_batch_converter ()
205 {
206         start_tool ("dcpomatic2_batch", "DCP-o-matic\\ 2\\ Batch\\ Converter.app");
207 }
208
209
210 void
211 start_player ()
212 {
213         start_tool ("dcpomatic2_player", "DCP-o-matic\\ 2\\ Player.app");
214 }
215
216
217 uint64_t
218 thread_id ()
219 {
220         return (uint64_t) pthread_self ();
221 }
222
223
224 int
225 avio_open_boost (AVIOContext** s, boost::filesystem::path file, int flags)
226 {
227         return avio_open (s, file.c_str(), flags);
228 }
229
230
231 boost::filesystem::path
232 home_directory ()
233 {
234         return getenv("HOME");
235 }
236
237
238 /** @return true if this process is a 32-bit one running on a 64-bit-capable OS */
239 bool
240 running_32_on_64 ()
241 {
242         /* I'm assuming nobody does this on OS X */
243         return false;
244 }
245
246
247 static optional<string>
248 get_vendor (CFDictionaryRef& description)
249 {
250         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionDeviceVendorKey);
251         if (!str) {
252                 return {};
253         }
254
255         auto c_str = CFStringGetCStringPtr ((CFStringRef) str, kCFStringEncodingUTF8);
256         if (!c_str) {
257                 return {};
258         }
259
260         string s (c_str);
261         boost::algorithm::trim (s);
262         return s;
263 }
264
265
266 static optional<string>
267 get_model (CFDictionaryRef& description)
268 {
269         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionDeviceModelKey);
270         if (!str) {
271                 return {};
272         }
273
274         auto c_str = CFStringGetCStringPtr ((CFStringRef) str, kCFStringEncodingUTF8);
275         if (!c_str) {
276                 return {};
277         }
278
279         string s (c_str);
280         boost::algorithm::trim (s);
281         return s;
282 }
283
284
285 static optional<OSXMediaPath>
286 analyse_media_path (CFDictionaryRef& description)
287 {
288         using namespace boost::algorithm;
289
290         void const* str = CFDictionaryGetValue (description, kDADiskDescriptionMediaPathKey);
291         if (!str) {
292                 LOG_DISK_NC("There is no MediaPathKey (no dictionary value)");
293                 return {};
294         }
295
296         auto path_key_cstr = CFStringGetCStringPtr((CFStringRef) str, kCFStringEncodingUTF8);
297         if (!path_key_cstr) {
298                 LOG_DISK_NC("There is no MediaPathKey (no cstring)");
299                 return {};
300         }
301
302         string path(path_key_cstr);
303         LOG_DISK("MediaPathKey is %1", path);
304         return analyse_osx_media_path (path);
305 }
306
307
308 static bool
309 is_whole_drive (DADiskRef& disk)
310 {
311         io_service_t service = DADiskCopyIOMedia (disk);
312         CFTypeRef whole_media_ref = IORegistryEntryCreateCFProperty (service, CFSTR(kIOMediaWholeKey), kCFAllocatorDefault, 0);
313         bool whole_media = false;
314         if (whole_media_ref) {
315                 whole_media = CFBooleanGetValue((CFBooleanRef) whole_media_ref);
316                 CFRelease (whole_media_ref);
317         }
318         IOObjectRelease (service);
319         return whole_media;
320 }
321
322
323 static optional<boost::filesystem::path>
324 mount_point (CFDictionaryRef& description)
325 {
326         auto volume_path_key = (CFURLRef) CFDictionaryGetValue (description, kDADiskDescriptionVolumePathKey);
327         if (!volume_path_key) {
328                 return {};
329         }
330
331         char mount_path_buffer[1024];
332         if (!CFURLGetFileSystemRepresentation(volume_path_key, false, (UInt8 *) mount_path_buffer, sizeof(mount_path_buffer))) {
333                 return {};
334         }
335         return boost::filesystem::path(mount_path_buffer);
336 }
337
338
339 /* Here follows some rather intricate and (probably) fragile code to find the list of available
340  * "real" drives on macOS that we might want to write a DCP to.
341  *
342  * We use the Disk Arbitration framework to give us a series of mount_points (/dev/disk0, /dev/disk1,
343  * /dev/disk1s1 and so on) and we use the API to gather useful information about these mount_points into
344  * a vector of Disk structs.
345  *
346  * Then we read the Disks that we found and try to derive a list of drives that we should offer to the
347  * user, with details of whether those drives are currently mounted or not.
348  *
349  * At the basic level we find the "disk"-level mount_points, looking at whether any of their partitions are mounted.
350  *
351  * This is complicated enormously by recent-ish macOS versions' habit of making `synthesized' volumes which
352  * reflect data in `real' partitions.  So, for example, we might have a real (physical) drive /dev/disk2 with
353  * a partition /dev/disk2s2 whose content is made into a synthesized /dev/disk3, itself containing some partitions
354  * which are mounted.  /dev/disk2s2 is not considered to be mounted, in this case.  So we need to know that
355  * disk2s2 is related to disk3 so we can consider disk2s2 as mounted if any parts of disk3 are.  In order to do
356  * this I am taking the first two parts of the IODeviceTree and seeing if they exist anywhere in a
357  * IOService identifier.  If they do, I am assuming the IOService device is on the matching IODeviceTree device.
358  *
359  * Lots of this is guesswork and may be broken.  In my defence the documentation that I have been able to
360  * unearth is, to put it impolitely, crap.
361  */
362
363 static void
364 disk_appeared (DADiskRef disk, void* context)
365 {
366         auto bsd_name = DADiskGetBSDName (disk);
367         if (!bsd_name) {
368                 LOG_DISK_NC("Disk with no BSDName appeared");
369                 return;
370         }
371         LOG_DISK("%1 appeared", bsd_name);
372
373         OSXDisk this_disk;
374
375         this_disk.device = string("/dev/") + bsd_name;
376         LOG_DISK("Device is %1", this_disk.device);
377
378         CFDictionaryRef description = DADiskCopyDescription (disk);
379
380         this_disk.vendor = get_vendor (description);
381         this_disk.model = get_model (description);
382         LOG_DISK("Vendor/model: %1 %2", this_disk.vendor.get_value_or("[none]"), this_disk.model.get_value_or("[none]"));
383
384         auto media_path = analyse_media_path (description);
385         if (!media_path) {
386                 LOG_DISK("Finding media path for %1 failed", bsd_name);
387                 return;
388         }
389
390         this_disk.media_path = *media_path;
391         this_disk.whole = is_whole_drive (disk);
392         auto mp = mount_point (description);
393         if (mp) {
394                 this_disk.mount_points.push_back (*mp);
395         }
396
397         LOG_DISK(
398                 "%1 %2 mounted at %3",
399                  this_disk.media_path.real ? "Real" : "Synth",
400                  this_disk.whole ? "whole" : "part",
401                  mp ? mp->string() : "[nowhere]"
402                 );
403
404         auto media_size_cstr = CFDictionaryGetValue (description, kDADiskDescriptionMediaSizeKey);
405         if (!media_size_cstr) {
406                 LOG_DISK_NC("Could not read media size");
407                 return;
408         }
409
410         CFNumberGetValue ((CFNumberRef) media_size_cstr, kCFNumberLongType, &this_disk.size);
411         CFRelease (description);
412
413         reinterpret_cast<vector<OSXDisk>*>(context)->push_back(this_disk);
414 }
415
416
417 vector<Drive>
418 Drive::get ()
419 {
420         using namespace boost::algorithm;
421         vector<OSXDisk> disks;
422
423         auto session = DASessionCreate(kCFAllocatorDefault);
424         if (!session) {
425                 return {};
426         }
427
428         DARegisterDiskAppearedCallback (session, NULL, disk_appeared, &disks);
429         auto run_loop = CFRunLoopGetCurrent ();
430         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
431         CFRunLoopStop (run_loop);
432         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0);
433         DAUnregisterCallback(session, (void *) disk_appeared, &disks);
434         CFRelease(session);
435
436         return osx_disks_to_drives (disks);
437 }
438
439
440 boost::filesystem::path
441 config_path (optional<string> version)
442 {
443         boost::filesystem::path p;
444         p /= g_get_home_dir ();
445         p /= "Library";
446         p /= "Preferences";
447         p /= "com.dcpomatic";
448         p /= "2";
449         if (version) {
450                 p /= *version;
451         }
452         return p;
453 }
454
455
456 void done_callback(DADiskRef, DADissenterRef dissenter, void* context)
457 {
458         LOG_DISK_NC("Unmount finished");
459         bool* success = reinterpret_cast<bool*> (context);
460         if (dissenter) {
461                 LOG_DISK("Error: %1", DADissenterGetStatus(dissenter));
462                 *success = false;
463         } else {
464                 LOG_DISK_NC("Successful");
465                 *success = true;
466         }
467 }
468
469
470 bool
471 Drive::unmount ()
472 {
473         LOG_DISK_NC("Unmount operation started");
474
475         auto session = DASessionCreate(kCFAllocatorDefault);
476         if (!session) {
477                 return false;
478         }
479
480         auto disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, _device.c_str());
481         if (!disk) {
482                 return false;
483         }
484         LOG_DISK("Requesting unmount of %1 from %2", _device, thread_id());
485         bool success = false;
486         DADiskUnmount(disk, kDADiskUnmountOptionWhole, &done_callback, &success);
487         CFRelease (disk);
488
489         CFRunLoopRef run_loop = CFRunLoopGetCurrent ();
490         DASessionScheduleWithRunLoop (session, run_loop, kCFRunLoopDefaultMode);
491         CFRunLoopStop (run_loop);
492         CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, 0);
493         CFRelease(session);
494
495         LOG_DISK_NC("End of unmount");
496         return success;
497 }
498
499
500 void
501 disk_write_finished ()
502 {
503
504 }
505
506
507 void
508 make_foreground_application ()
509 {
510         ProcessSerialNumber serial;
511 LIBDCP_DISABLE_WARNINGS
512         GetCurrentProcess (&serial);
513 LIBDCP_ENABLE_WARNINGS
514         TransformProcessType (&serial, kProcessTransformToForegroundApplication);
515 }
516
517
518 string
519 dcpomatic::get_process_id ()
520 {
521         return dcp::raw_convert<string>(getpid());
522 }
523
524
525 bool
526 show_in_file_manager (boost::filesystem::path, boost::filesystem::path select)
527 {
528         int r = system (String::compose("open -R \"%1\"", select.string()).c_str());
529         return static_cast<bool>(WEXITSTATUS(r));
530 }
531