Add dist tool.
[dcpomatic.git] / src / lib / cross.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 <dcp/raw_convert.h>
28 #include <glib.h>
29 extern "C" {
30 #include <libavformat/avio.h>
31 }
32 #include <boost/algorithm/string.hpp>
33 #include <boost/foreach.hpp>
34 #ifdef DCPOMATIC_LINUX
35 #include <unistd.h>
36 #include <mntent.h>
37 #endif
38 #ifdef DCPOMATIC_WINDOWS
39 #include <windows.h>
40 #undef DATADIR
41 #include <shlwapi.h>
42 #include <shellapi.h>
43 #include <fcntl.h>
44 #endif
45 #ifdef DCPOMATIC_OSX
46 #include <sys/sysctl.h>
47 #include <mach-o/dyld.h>
48 #include <IOKit/pwr_mgt/IOPMLib.h>
49 #endif
50 #ifdef DCPOMATIC_POSIX
51 #include <sys/types.h>
52 #include <ifaddrs.h>
53 #include <netinet/in.h>
54 #include <arpa/inet.h>
55 #endif
56 #include <fstream>
57
58 #include "i18n.h"
59
60 using std::pair;
61 using std::list;
62 using std::ifstream;
63 using std::string;
64 using std::wstring;
65 using std::make_pair;
66 using std::vector;
67 using std::cerr;
68 using std::cout;
69 using std::runtime_error;
70 using boost::shared_ptr;
71 using boost::optional;
72
73 /** @param s Number of seconds to sleep for */
74 void
75 dcpomatic_sleep_seconds (int s)
76 {
77 #ifdef DCPOMATIC_POSIX
78         sleep (s);
79 #endif
80 #ifdef DCPOMATIC_WINDOWS
81         Sleep (s * 1000);
82 #endif
83 }
84
85 void
86 dcpomatic_sleep_milliseconds (int ms)
87 {
88 #ifdef DCPOMATIC_POSIX
89         usleep (ms * 1000);
90 #endif
91 #ifdef DCPOMATIC_WINDOWS
92         Sleep (ms);
93 #endif
94 }
95
96 /** @return A string of CPU information (model name etc.) */
97 string
98 cpu_info ()
99 {
100         string info;
101
102 #ifdef DCPOMATIC_LINUX
103         /* This use of ifstream is ok; the filename can never
104            be non-Latin
105         */
106         ifstream f ("/proc/cpuinfo");
107         while (f.good ()) {
108                 string l;
109                 getline (f, l);
110                 if (boost::algorithm::starts_with (l, "model name")) {
111                         string::size_type const c = l.find (':');
112                         if (c != string::npos) {
113                                 info = l.substr (c + 2);
114                         }
115                 }
116         }
117 #endif
118
119 #ifdef DCPOMATIC_OSX
120         char buffer[64];
121         size_t N = sizeof (buffer);
122         if (sysctlbyname ("machdep.cpu.brand_string", buffer, &N, 0, 0) == 0) {
123                 info = buffer;
124         }
125 #endif
126
127 #ifdef DCPOMATIC_WINDOWS
128         HKEY key;
129         if (RegOpenKeyEx (HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", 0, KEY_READ, &key) != ERROR_SUCCESS) {
130                 return info;
131         }
132
133         DWORD type;
134         DWORD data;
135         if (RegQueryValueEx (key, L"ProcessorNameString", 0, &type, 0, &data) != ERROR_SUCCESS) {
136                 return info;
137         }
138
139         if (type != REG_SZ) {
140                 return info;
141         }
142
143         wstring value (data / sizeof (wchar_t), L'\0');
144         if (RegQueryValueEx (key, L"ProcessorNameString", 0, 0, reinterpret_cast<LPBYTE> (&value[0]), &data) != ERROR_SUCCESS) {
145                 RegCloseKey (key);
146                 return info;
147         }
148
149         info = string (value.begin(), value.end());
150
151         RegCloseKey (key);
152
153 #endif
154
155         return info;
156 }
157
158 #ifdef DCPOMATIC_OSX
159 /** @return Path of the Contents directory in the .app */
160 boost::filesystem::path
161 app_contents ()
162 {
163         uint32_t size = 1024;
164         char buffer[size];
165         if (_NSGetExecutablePath (buffer, &size)) {
166                 throw runtime_error ("_NSGetExecutablePath failed");
167         }
168
169         boost::filesystem::path path (buffer);
170         path = boost::filesystem::canonical (path);
171         path = path.parent_path ();
172         path = path.parent_path ();
173         return path;
174 }
175 #endif
176
177 boost::filesystem::path
178 shared_path ()
179 {
180 #ifdef DCPOMATIC_LINUX
181         char const * p = getenv ("DCPOMATIC_LINUX_SHARE_PREFIX");
182         if (p) {
183                 return p;
184         }
185         return boost::filesystem::canonical (LINUX_SHARE_PREFIX);
186 #endif
187 #ifdef DCPOMATIC_WINDOWS
188         wchar_t dir[512];
189         GetModuleFileName (GetModuleHandle (0), dir, sizeof (dir));
190         PathRemoveFileSpec (dir);
191         boost::filesystem::path path = dir;
192         return path.parent_path();
193 #endif
194 #ifdef DCPOMATIC_OSX
195         return app_contents() / "Resources";
196 #endif
197 }
198
199 void
200 run_ffprobe (boost::filesystem::path content, boost::filesystem::path out)
201 {
202 #ifdef DCPOMATIC_WINDOWS
203         SECURITY_ATTRIBUTES security;
204         security.nLength = sizeof (security);
205         security.bInheritHandle = TRUE;
206         security.lpSecurityDescriptor = 0;
207
208         HANDLE child_stderr_read;
209         HANDLE child_stderr_write;
210         if (!CreatePipe (&child_stderr_read, &child_stderr_write, &security, 0)) {
211                 LOG_ERROR_NC ("ffprobe call failed (could not CreatePipe)");
212                 return;
213         }
214
215         wchar_t dir[512];
216         GetModuleFileName (GetModuleHandle (0), dir, sizeof (dir));
217         PathRemoveFileSpec (dir);
218         SetCurrentDirectory (dir);
219
220         STARTUPINFO startup_info;
221         ZeroMemory (&startup_info, sizeof (startup_info));
222         startup_info.cb = sizeof (startup_info);
223         startup_info.hStdError = child_stderr_write;
224         startup_info.dwFlags |= STARTF_USESTDHANDLES;
225
226         wchar_t command[512];
227         wcscpy (command, L"ffprobe.exe \"");
228
229         wchar_t file[512];
230         MultiByteToWideChar (CP_UTF8, 0, content.string().c_str(), -1, file, sizeof(file));
231         wcscat (command, file);
232
233         wcscat (command, L"\"");
234
235         PROCESS_INFORMATION process_info;
236         ZeroMemory (&process_info, sizeof (process_info));
237         if (!CreateProcess (0, command, 0, 0, TRUE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
238                 LOG_ERROR_NC (N_("ffprobe call failed (could not CreateProcess)"));
239                 return;
240         }
241
242         FILE* o = fopen_boost (out, "w");
243         if (!o) {
244                 LOG_ERROR_NC (N_("ffprobe call failed (could not create output file)"));
245                 return;
246         }
247
248         CloseHandle (child_stderr_write);
249
250         while (true) {
251                 char buffer[512];
252                 DWORD read;
253                 if (!ReadFile(child_stderr_read, buffer, sizeof(buffer), &read, 0) || read == 0) {
254                         break;
255                 }
256                 fwrite (buffer, read, 1, o);
257         }
258
259         fclose (o);
260
261         WaitForSingleObject (process_info.hProcess, INFINITE);
262         CloseHandle (process_info.hProcess);
263         CloseHandle (process_info.hThread);
264         CloseHandle (child_stderr_read);
265 #endif
266
267 #ifdef DCPOMATIC_LINUX
268         string ffprobe = "ffprobe \"" + content.string() + "\" 2> \"" + out.string() + "\"";
269         LOG_GENERAL (N_("Probing with %1"), ffprobe);
270         system (ffprobe.c_str ());
271 #endif
272
273 #ifdef DCPOMATIC_OSX
274         boost::filesystem::path path = app_contents();
275         path /= "MacOS";
276         path /= "ffprobe";
277
278         string ffprobe = "\"" + path.string() + "\" \"" + content.string() + "\" 2> \"" + out.string() + "\"";
279         LOG_GENERAL (N_("Probing with %1"), ffprobe);
280         system (ffprobe.c_str ());
281 #endif
282 }
283
284 list<pair<string, string> >
285 mount_info ()
286 {
287         list<pair<string, string> > m;
288
289 #ifdef DCPOMATIC_LINUX
290         FILE* f = setmntent ("/etc/mtab", "r");
291         if (!f) {
292                 return m;
293         }
294
295         while (true) {
296                 struct mntent* mnt = getmntent (f);
297                 if (!mnt) {
298                         break;
299                 }
300
301                 m.push_back (make_pair (mnt->mnt_dir, mnt->mnt_type));
302         }
303
304         endmntent (f);
305 #endif
306
307         return m;
308 }
309
310 boost::filesystem::path
311 openssl_path ()
312 {
313 #ifdef DCPOMATIC_WINDOWS
314         wchar_t dir[512];
315         GetModuleFileName (GetModuleHandle (0), dir, sizeof (dir));
316         PathRemoveFileSpec (dir);
317
318         boost::filesystem::path path = dir;
319         path /= "openssl.exe";
320         return path;
321 #endif
322
323 #ifdef DCPOMATIC_OSX
324         boost::filesystem::path path = app_contents();
325         path /= "MacOS";
326         path /= "openssl";
327         return path;
328 #endif
329
330 #ifdef DCPOMATIC_LINUX
331         return "dcpomatic2_openssl";
332 #endif
333
334 }
335
336 /* Apparently there is no way to create an ofstream using a UTF-8
337    filename under Windows.  We are hence reduced to using fopen
338    with this wrapper.
339 */
340 FILE *
341 fopen_boost (boost::filesystem::path p, string t)
342 {
343 #ifdef DCPOMATIC_WINDOWS
344         wstring w (t.begin(), t.end());
345         /* c_str() here should give a UTF-16 string */
346         return _wfopen (p.c_str(), w.c_str ());
347 #else
348         return fopen (p.c_str(), t.c_str ());
349 #endif
350 }
351
352 int
353 dcpomatic_fseek (FILE* stream, int64_t offset, int whence)
354 {
355 #ifdef DCPOMATIC_WINDOWS
356         return _fseeki64 (stream, offset, whence);
357 #else
358         return fseek (stream, offset, whence);
359 #endif
360 }
361
362 void
363 Waker::nudge ()
364 {
365 #ifdef DCPOMATIC_WINDOWS
366         boost::mutex::scoped_lock lm (_mutex);
367         SetThreadExecutionState (ES_SYSTEM_REQUIRED);
368 #endif
369 }
370
371 Waker::Waker ()
372 {
373 #ifdef DCPOMATIC_OSX
374         boost::mutex::scoped_lock lm (_mutex);
375         /* We should use this */
376         // IOPMAssertionCreateWithName (kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, CFSTR ("Encoding DCP"), &_assertion_id);
377         /* but it's not available on 10.5, so we use this */
378         IOPMAssertionCreate (kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn, &_assertion_id);
379 #endif
380 }
381
382 Waker::~Waker ()
383 {
384 #ifdef DCPOMATIC_OSX
385         boost::mutex::scoped_lock lm (_mutex);
386         IOPMAssertionRelease (_assertion_id);
387 #endif
388 }
389
390 void
391 start_tool (boost::filesystem::path dcpomatic, string executable,
392 #ifdef DCPOMATIC_OSX
393             string app
394 #else
395             string
396 #endif
397         )
398 {
399 #if defined(DCPOMATIC_LINUX) || defined(DCPOMATIC_WINDOWS)
400         boost::filesystem::path batch = dcpomatic.parent_path() / executable;
401 #endif
402
403 #ifdef DCPOMATIC_OSX
404         boost::filesystem::path batch = dcpomatic.parent_path ();
405         batch = batch.parent_path (); // MacOS
406         batch = batch.parent_path (); // Contents
407         batch = batch.parent_path (); // DCP-o-matic.app
408         batch = batch.parent_path (); // Applications
409         batch /= app;
410         batch /= "Contents";
411         batch /= "MacOS";
412         batch /= executable;
413 #endif
414
415 #if defined(DCPOMATIC_LINUX) || defined(DCPOMATIC_OSX)
416         pid_t pid = fork ();
417         if (pid == 0) {
418                 int const r = system (batch.string().c_str());
419                 exit (WEXITSTATUS (r));
420         }
421 #endif
422
423 #ifdef DCPOMATIC_WINDOWS
424         STARTUPINFO startup_info;
425         ZeroMemory (&startup_info, sizeof (startup_info));
426         startup_info.cb = sizeof (startup_info);
427
428         PROCESS_INFORMATION process_info;
429         ZeroMemory (&process_info, sizeof (process_info));
430
431         wchar_t cmd[512];
432         MultiByteToWideChar (CP_UTF8, 0, batch.string().c_str(), -1, cmd, sizeof(cmd));
433         CreateProcess (0, cmd, 0, 0, FALSE, 0, 0, 0, &startup_info, &process_info);
434 #endif
435 }
436
437 void
438 start_batch_converter (boost::filesystem::path dcpomatic)
439 {
440         start_tool (dcpomatic, "dcpomatic2_batch", "DCP-o-matic\\ 2\\ Batch\\ Converter.app");
441 }
442
443 void
444 start_player (boost::filesystem::path dcpomatic)
445 {
446         start_tool (dcpomatic, "dcpomatic2_player", "DCP-o-matic\\ 2\\ Player.app");
447 }
448
449 uint64_t
450 thread_id ()
451 {
452 #ifdef DCPOMATIC_WINDOWS
453         return (uint64_t) GetCurrentThreadId ();
454 #else
455         return (uint64_t) pthread_self ();
456 #endif
457 }
458
459 int
460 avio_open_boost (AVIOContext** s, boost::filesystem::path file, int flags)
461 {
462 #ifdef DCPOMATIC_WINDOWS
463         int const length = (file.string().length() + 1) * 2;
464         char* utf8 = new char[length];
465         WideCharToMultiByte (CP_UTF8, 0, file.c_str(), -1, utf8, length, 0, 0);
466         int const r = avio_open (s, utf8, flags);
467         delete[] utf8;
468         return r;
469 #else
470         return avio_open (s, file.c_str(), flags);
471 #endif
472 }
473
474 #ifdef DCPOMATIC_WINDOWS
475 void
476 maybe_open_console ()
477 {
478         if (Config::instance()->win32_console ()) {
479                 AllocConsole();
480
481                 HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
482                 int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT);
483                 FILE* hf_out = _fdopen(hCrt, "w");
484                 setvbuf(hf_out, NULL, _IONBF, 1);
485                 *stdout = *hf_out;
486
487                 HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE);
488                 hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT);
489                 FILE* hf_in = _fdopen(hCrt, "r");
490                 setvbuf(hf_in, NULL, _IONBF, 128);
491                 *stdin = *hf_in;
492         }
493 }
494 #endif
495
496 boost::filesystem::path
497 home_directory ()
498 {
499 #if defined(DCPOMATIC_LINUX) || defined(DCPOMATIC_OSX)
500                 return getenv("HOME");
501 #endif
502 #ifdef DCPOMATIC_WINDOWS
503                 return boost::filesystem::path(getenv("HOMEDRIVE")) / boost::filesystem::path(getenv("HOMEPATH"));
504 #endif
505 }
506
507 string
508 command_and_read (string cmd)
509 {
510 #ifdef DCPOMATIC_LINUX
511         FILE* pipe = popen (cmd.c_str(), "r");
512         if (!pipe) {
513                 throw runtime_error ("popen failed");
514         }
515
516         string result;
517         char buffer[128];
518         try {
519                 while (fgets(buffer, sizeof(buffer), pipe)) {
520                         result += buffer;
521                 }
522         } catch (...) {
523                 pclose (pipe);
524                 throw;
525         }
526
527         pclose (pipe);
528         return result;
529 #endif
530
531         return "";
532 }
533
534 /** @return true if this process is a 32-bit one running on a 64-bit-capable OS */
535 bool
536 running_32_on_64 ()
537 {
538 #ifdef DCPOMATIC_WINDOWS
539         BOOL p;
540         IsWow64Process (GetCurrentProcess(), &p);
541         return p;
542 #endif
543         /* XXX: assuming nobody does this on Linux / OS X */
544         return false;
545 }
546
547 vector<Drive>
548 get_drives ()
549 {
550         vector<Drive> drives;
551 #ifdef DCPOMATIC_LINUX
552
553         using namespace boost::filesystem;
554         list<string> mounted_devices;
555         std::ifstream f("/proc/mounts");
556         string line;
557         while (f.good()) {
558                 getline(f, line);
559                 vector<string> bits;
560                 boost::algorithm::split (bits, line, boost::is_any_of(" "));
561                 if (bits.size() > 0 && boost::algorithm::starts_with(bits[0], "/dev/")) {
562                         mounted_devices.push_back(bits[0]);
563                         LOG_DIST("Mounted device %1", bits[0]);
564                 }
565         }
566
567         for (directory_iterator i = directory_iterator("/sys/block"); i != directory_iterator(); ++i) {
568                 string const name = i->path().filename().string();
569                 path device_type_file("/sys/block/" + name + "/device/type");
570                 optional<string> device_type;
571                 if (exists(device_type_file)) {
572                         device_type = dcp::file_to_string (device_type_file);
573                         boost::trim(*device_type);
574                 }
575                 /* Device type 5 is "SCSI_TYPE_ROM" in blkdev.h; seems usually to be a CD/DVD drive */
576                 if (!boost::algorithm::starts_with(name, "loop") && (!device_type || *device_type != "5")) {
577                         uint64_t const size = dcp::raw_convert<uint64_t>(dcp::file_to_string(*i / "size")) * 512;
578                         if (size == 0) {
579                                 continue;
580                         }
581                         bool mounted = false;
582                         optional<string> vendor;
583                         try {
584                                 vendor = dcp::file_to_string("/sys/block/" + name + "/device/vendor");
585                                 boost::trim(*vendor);
586                         } catch (...) {}
587                         optional<string> model;
588                         try {
589                                 model = dcp::file_to_string("/sys/block/" + name + "/device/model");
590                                 boost::trim(*model);
591                         } catch (...) {}
592                         BOOST_FOREACH (string j, mounted_devices) {
593                                 if (boost::algorithm::starts_with(j, "/dev/" + name)) {
594                                         mounted = true;
595                                 }
596                         }
597                         drives.push_back(Drive(i->path().filename().string(), size, mounted, vendor, model));
598                         LOG_DIST("Block device %1 size %2 %3 vendor %4 model %5", name, size, mounted ? "mounted" : "not mounted", vendor.get_value_or("[none]"), model.get_value_or("[none]"));
599                 }
600         }
601 #endif
602         return drives;
603 }
604
605 string
606 Drive::description () const
607 {
608         char gb[64];
609         snprintf(gb, 64, "%.1f", _size / 1000000000.0);
610
611         string name;
612         if (_vendor) {
613                 name += *_vendor;
614         }
615         if (_model) {
616                 if (name.size() > 0) {
617                         name += " " + *_model;
618                 }
619         }
620         if (name.size() == 0) {
621                 name = _("Unknown");
622         }
623
624         return String::compose("%1 (%2 GB) [%3]", name, gb, _internal_name);
625 }
626
627 #ifdef DCPOMATIC_LINUX
628 void
629 unprivileged ()
630 {
631         uid_t ruid, euid, suid;
632         if (getresuid(&ruid, &euid, &suid) == -1) {
633                 cerr << "getresuid() failed.\n";
634                 exit (EXIT_FAILURE);
635         }
636         seteuid (ruid);
637 }
638
639 PrivilegeEscalator::~PrivilegeEscalator ()
640 {
641         unprivileged ();
642 }
643
644 PrivilegeEscalator::PrivilegeEscalator ()
645 {
646         seteuid (0);
647 }
648 #endif
649
650 boost::filesystem::path
651 config_path ()
652 {
653         boost::filesystem::path p;
654 #ifdef DCPOMATIC_OSX
655                 p /= g_get_home_dir ();
656                 p /= "Library";
657                 p /= "Preferences";
658                 p /= "com.dcpomatic";
659                 p /= "2";
660 #else
661                 p /= g_get_user_config_dir ();
662                 p /= "dcpomatic2";
663 #endif
664         return p;
665 }
666