Emit no audio from DCPs if none is mapped
[dcpomatic.git] / src / tools / dcpomatic_disk_writer.cc
index 695a0b0334ae3d1581f92e70ba105a0a96434d3e..3bb6fcd3ffe78780484a5af80d46bfd6134d5c91 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2019-2020 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2019-2021 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -18,6 +18,7 @@
 
 */
 
+
 #include "lib/compose.hpp"
 #include "lib/cross.h"
 #include "lib/dcpomatic_log.h"
 #include "lib/exceptions.h"
 #include "lib/ext.h"
 #include "lib/file_log.h"
+#include "lib/state.h"
 #include "lib/nanomsg.h"
+#include "lib/util.h"
 #include "lib/version.h"
-#include "lib/warnings.h"
+#include <dcp/warnings.h>
 
 #ifdef DCPOMATIC_POSIX
 #include <sys/ioctl.h>
@@ -42,6 +45,7 @@
 extern "C" {
 #include <lwext4/file_dev.h>
 }
+#include <unistd.h>
 #include <xpc/xpc.h>
 #endif
 
@@ -56,17 +60,17 @@ extern "C" {
 }
 #endif
 
-DCPOMATIC_DISABLE_WARNINGS
+LIBDCP_DISABLE_WARNINGS
 #include <glibmm.h>
-DCPOMATIC_ENABLE_WARNINGS
+LIBDCP_ENABLE_WARNINGS
 
 #include <unistd.h>
 #include <sys/types.h>
 #include <boost/filesystem.hpp>
 #include <boost/algorithm/string.hpp>
-#include <boost/foreach.hpp>
 #include <iostream>
 
+
 using std::cin;
 using std::min;
 using std::string;
@@ -81,28 +85,42 @@ using boost::optional;
 
 
 #ifdef DCPOMATIC_LINUX
-static PolkitAuthority* polkit_authority = 0;
+static PolkitAuthority* polkit_authority = nullptr;
 #endif
-static Nanomsg* nanomsg = 0;
+static Nanomsg* nanomsg = nullptr;
 
-struct Parameters
-{
-       boost::filesystem::path dcp_path;
-       std::string device;
-       std::string posix_partition;
-};
 
 #ifdef DCPOMATIC_LINUX
-static
 void
 polkit_callback (GObject *, GAsyncResult* res, gpointer data)
 {
-       Parameters* parameters = reinterpret_cast<Parameters*> (data);
-       PolkitAuthorizationResult* result = polkit_authority_check_authorization_finish (polkit_authority, res, 0);
-       if (result && polkit_authorization_result_get_is_authorized(result)) {
-               dcpomatic::write (parameters->dcp_path, parameters->device, parameters->posix_partition, nanomsg);
+       auto parameters = reinterpret_cast<std::pair<std::function<void ()>, std::function<void ()>>*> (data);
+       GError* error = nullptr;
+       auto result = polkit_authority_check_authorization_finish (polkit_authority, res, &error);
+       bool failed = false;
+
+       if (error) {
+               LOG_DISK("polkit authority check failed (check_authorization_finish failed with %1)", error->message);
+               failed = true;
+       } else {
+               if (polkit_authorization_result_get_is_authorized(result)) {
+                       parameters->first();
+               } else {
+                       failed = true;
+                       if (polkit_authorization_result_get_is_challenge(result)) {
+                               LOG_DISK_NC("polkit authority check failed (challenge)");
+                       } else {
+                               LOG_DISK_NC("polkit authority check failed (not authorized)");
+                       }
+               }
+       }
+
+       if (failed) {
+               parameters->second();
        }
+
        delete parameters;
+
        if (result) {
                g_object_unref (result);
        }
@@ -110,13 +128,33 @@ polkit_callback (GObject *, GAsyncResult* res, gpointer data)
 #endif
 
 
+#ifdef DCPOMATIC_LINUX
+void request_privileges (string action, std::function<void ()> granted, std::function<void ()> denied)
+#else
+void request_privileges (string, std::function<void ()> granted, std::function<void ()>)
+#endif
+{
+#ifdef DCPOMATIC_LINUX
+       polkit_authority = polkit_authority_get_sync (0, 0);
+       auto subject = polkit_unix_process_new_for_owner (getppid(), 0, -1);
+
+       auto parameters = new std::pair<std::function<void ()>, std::function<void ()>>(granted, denied);
+       polkit_authority_check_authorization (
+               polkit_authority, subject, action.c_str(), 0, POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, 0, polkit_callback, parameters
+               );
+#else
+       granted ();
+#endif
+}
+
+
 bool
 idle ()
 try
 {
        using namespace boost::algorithm;
 
-       optional<string> s = nanomsg->receive (0);
+       auto s = nanomsg->receive (0);
        if (!s) {
                return true;
        }
@@ -126,48 +164,78 @@ try
        if (*s == DISK_WRITER_QUIT) {
                exit (EXIT_SUCCESS);
        } else if (*s == DISK_WRITER_PING) {
-               nanomsg->send(DISK_WRITER_PONG "\n", LONG_TIMEOUT);
+               DiskWriterBackEndResponse::pong().write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
        } else if (*s == DISK_WRITER_UNMOUNT) {
-               /* XXX: should do Linux polkit stuff here */
-               optional<string> xml_head = nanomsg->receive (LONG_TIMEOUT);
-               optional<string> xml_body = nanomsg->receive (LONG_TIMEOUT);
+               auto xml_head = nanomsg->receive (LONG_TIMEOUT);
+               auto xml_body = nanomsg->receive (LONG_TIMEOUT);
                if (!xml_head || !xml_body) {
                        LOG_DISK_NC("Failed to receive unmount request");
                        throw CommunicationFailedError ();
                }
-               bool const success = Drive(*xml_head + *xml_body).unmount();
-               if (!nanomsg->send (success ? (DISK_WRITER_OK "\n") : (DISK_WRITER_ERROR "\n"), LONG_TIMEOUT)) {
-                       LOG_DISK_NC("CommunicationFailedError in unmount_finished");
-                       throw CommunicationFailedError ();
-               }
+               auto xml = *xml_head + *xml_body;
+               request_privileges (
+                       "com.dcpomatic.write-drive",
+                       [xml]() {
+                               bool const success = Drive(xml).unmount();
+                               bool sent_reply = false;
+                               if (success) {
+                                       sent_reply = DiskWriterBackEndResponse::ok().write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
+                               } else {
+                                       sent_reply = DiskWriterBackEndResponse::error("Could not unmount drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
+                               }
+                               if (!sent_reply) {
+                                       LOG_DISK_NC("CommunicationFailedError in unmount_finished");
+                                       throw CommunicationFailedError ();
+                               }
+                       },
+                       []() {
+                               if (!DiskWriterBackEndResponse::error("Could not get permission to unmount drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT)) {
+                                       LOG_DISK_NC("CommunicationFailedError in unmount_finished");
+                                       throw CommunicationFailedError ();
+                               }
+                       });
        } else if (*s == DISK_WRITER_WRITE) {
-               optional<string> dcp_path = nanomsg->receive (LONG_TIMEOUT);
-               optional<string> device = nanomsg->receive (LONG_TIMEOUT);
-               if (!dcp_path || !device) {
+               auto device_opt = nanomsg->receive (LONG_TIMEOUT);
+               if (!device_opt) {
                        LOG_DISK_NC("Failed to receive write request");
                        throw CommunicationFailedError();
                }
+               auto device = *device_opt;
+
+               vector<boost::filesystem::path> dcp_paths;
+               while (true) {
+                       auto dcp_path_opt = nanomsg->receive (LONG_TIMEOUT);
+                       if (!dcp_path_opt) {
+                               LOG_DISK_NC("Failed to receive write request");
+                               throw CommunicationFailedError();
+                       }
+                       if (*dcp_path_opt != "") {
+                               dcp_paths.push_back(*dcp_path_opt);
+                       } else {
+                               break;
+                       }
+               }
 
                /* Do some basic sanity checks; this is a bit belt-and-braces but it can't hurt... */
 
 #ifdef DCPOMATIC_OSX
-               if (!starts_with(*device, "/dev/disk")) {
-                       LOG_DISK ("Will not write to %1", *device);
-                       nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
+               if (!starts_with(device, "/dev/disk")) {
+                       LOG_DISK ("Will not write to %1", device);
+                       DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
                        return true;
                }
 #endif
 #ifdef DCPOMATIC_LINUX
-               if (!starts_with(*device, "/dev/sd") && !starts_with(*device, "/dev/hd")) {
-                       LOG_DISK ("Will not write to %1", *device);
-                       nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
+               if (!starts_with(device, "/dev/sd") && !starts_with(device, "/dev/hd")) {
+                       LOG_DISK ("Will not write to %1", device);
+                       DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
                        return true;
                }
 #endif
 #ifdef DCPOMATIC_WINDOWS
-               if (!starts_with(*device, "\\\\.\\PHYSICALDRIVE")) {
-                       LOG_DISK ("Will not write to %1", *device);
-                       nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
+               if (!starts_with(device, "\\\\.\\PHYSICALDRIVE")) {
+                       LOG_DISK ("Will not write to %1", device);
+                       DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
                        return true;
                }
 #endif
@@ -175,48 +243,52 @@ try
                bool on_drive_list = false;
                bool mounted = false;
                for (auto const& i: Drive::get()) {
-                       if (i.device() == *device) {
+                       if (i.device() == device) {
                                on_drive_list = true;
                                mounted = i.mounted();
                        }
                }
 
                if (!on_drive_list) {
-                       LOG_DISK ("Will not write to %1 as it's not recognised as a drive", *device);
-                       nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
+                       LOG_DISK ("Will not write to %1 as it's not recognised as a drive", device);
+                       DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
                        return true;
                }
                if (mounted) {
-                       LOG_DISK ("Will not write to %1 as it's mounted", *device);
-                       nanomsg->send(DISK_WRITER_ERROR "\nRefusing to write to this drive\n1\n", LONG_TIMEOUT);
+                       LOG_DISK ("Will not write to %1 as it's mounted", device);
+                       DiskWriterBackEndResponse::error("Refusing to write to this drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
                        return true;
                }
 
-               LOG_DISK ("Here we go writing %1 to %2", *dcp_path, *device);
-
-#ifdef DCPOMATIC_LINUX
-               polkit_authority = polkit_authority_get_sync (0, 0);
-               PolkitSubject* subject = polkit_unix_process_new_for_owner (getppid(), 0, -1);
-               Parameters* parameters = new Parameters;
-               parameters->dcp_path = *dcp_path;
-               parameters->device = *device;
-               parameters->posix_partition = *device;
-               /* XXX: don't know if this logic is sensible */
-               if (parameters->posix_partition.size() > 0 && isdigit(parameters->posix_partition[parameters->posix_partition.length() - 1])) {
-                       parameters->posix_partition += "p1";
-               } else {
-                       parameters->posix_partition += "1";
+               LOG_DISK("Here we go writing these to %1", device);
+               for (auto dcp: dcp_paths) {
+                       LOG_DISK("  %1", dcp);
                }
-               polkit_authority_check_authorization (
-                               polkit_authority, subject, "com.dcpomatic.write-drive", 0, POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, 0, polkit_callback, parameters
-                               );
-#else
-               string posix_partition = "";
-#ifdef DCPOMATIC_OSX
-               posix_partition = *device + "s1";
-#endif
-               dcpomatic::write (*dcp_path, *device, posix_partition, nanomsg);
+
+               request_privileges (
+                       "com.dcpomatic.write-drive",
+                       [dcp_paths, device]() {
+#if defined(DCPOMATIC_LINUX)
+                               auto posix_partition = device;
+                               /* XXX: don't know if this logic is sensible */
+                               if (posix_partition.size() > 0 && isdigit(posix_partition[posix_partition.length() - 1])) {
+                                       posix_partition += "p1";
+                               } else {
+                                       posix_partition += "1";
+                               }
+                               dcpomatic::write (dcp_paths, device, posix_partition, nanomsg);
+#elif defined(DCPOMATIC_OSX)
+                               auto fast_device = boost::algorithm::replace_first_copy (device, "/dev/disk", "/dev/rdisk");
+                               dcpomatic::write (dcp_paths, fast_device, fast_device + "s1", nanomsg);
+#elif defined(DCPOMATIC_WINDOWS)
+                               dcpomatic::write (dcp_paths, device, "", nanomsg);
 #endif
+                       },
+                       []() {
+                               if (nanomsg) {
+                                       DiskWriterBackEndResponse::error("Could not obtain authorization to write to the drive", 1, 0).write_to_nanomsg(*nanomsg, LONG_TIMEOUT);
+                               }
+                       });
        }
 
        return true;
@@ -228,18 +300,20 @@ try
 int
 main ()
 {
+       dcpomatic_setup_path_encoding();
+
 #ifdef DCPOMATIC_OSX
        /* On macOS this is running as root, so config_path() will be somewhere in root's
         * home.  Instead, just write to stdout as the macOS process control stuff will
         * redirect this to a file in /var/log
         */
        dcpomatic_log.reset(new StdoutLog(LogEntry::TYPE_DISK));
-       LOG_DISK("dcpomatic_disk_writer %1 started", dcpomatic_git_commit);
+       LOG_DISK("dcpomatic_disk_writer %1 started uid=%2 euid=%3", dcpomatic_git_commit, getuid(), geteuid());
 #else
        /* XXX: this is a hack, but I expect we'll need logs and I'm not sure if there's
         * a better place to put them.
         */
-       dcpomatic_log.reset(new FileLog(config_path() / "disk_writer.log", LogEntry::TYPE_DISK));
+       dcpomatic_log.reset(new FileLog(State::write_path("disk_writer.log"), LogEntry::TYPE_DISK));
        LOG_DISK_NC("dcpomatic_disk_writer started");
 #endif
 
@@ -257,7 +331,8 @@ main ()
                exit (EXIT_FAILURE);
        }
 
-       Glib::RefPtr<Glib::MainLoop> ml = Glib::MainLoop::create ();
+       LOG_DISK_NC("Entering main loop");
+       auto ml = Glib::MainLoop::create ();
        Glib::signal_timeout().connect(sigc::ptr_fun(&idle), 500);
        ml->run ();
 }