X-Git-Url: https://git.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Ftools%2Fdcpomatic_disk.cc;h=a20a062e36aca851fe781ada4fc1886df546637b;hb=6b2119dcd16c43fd681feace00d4e10f464bb9b7;hp=baccdbce3c9f3fd5c94b17b40e67ff23d8d56f5d;hpb=a1f7bf2d9e5610075fbd898cdf52f4f8373741f2;p=dcpomatic.git diff --git a/src/tools/dcpomatic_disk.cc b/src/tools/dcpomatic_disk.cc index baccdbce3..a20a062e3 100644 --- a/src/tools/dcpomatic_disk.cc +++ b/src/tools/dcpomatic_disk.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2019-2020 Carl Hetherington + Copyright (C) 2019-2021 Carl Hetherington This file is part of DCP-o-matic. @@ -18,69 +18,142 @@ */ + +#include "wx/disk_warning_dialog.h" +#include "wx/drive_wipe_warning_dialog.h" +#include "wx/editable_list.h" +#include "wx/id.h" +#include "wx/job_manager_view.h" +#include "wx/message_dialog.h" +#include "wx/try_unmount_dialog.h" +#include "wx/wx_util.h" #include "wx/wx_signal_manager.h" #include "wx/wx_util.h" -#include "wx/job_manager_view.h" -#include "wx/drive_wipe_warning_dialog.h" -#include "lib/file_log.h" -#include "lib/dcpomatic_log.h" -#include "lib/util.h" +#include "wx/wx_variant.h" #include "lib/config.h" -#include "lib/signal_manager.h" -#include "lib/cross.h" +#include "lib/constants.h" #include "lib/copy_to_drive_job.h" -#include "lib/job_manager.h" +#include "lib/cross.h" +#include "lib/dcpomatic_log.h" #include "lib/disk_writer_messages.h" +#include "lib/file_log.h" +#include "lib/job_manager.h" +#include "lib/signal_manager.h" +#include "lib/util.h" +#include "lib/version.h" +#include +#include +#include +#include #include +LIBDCP_DISABLE_WARNINGS #include +LIBDCP_ENABLE_WARNINGS #ifdef DCPOMATIC_WINDOWS #include #endif #ifdef DCPOMATIC_OSX -#include +#include #endif -using std::string; -using std::exception; -using std::cout; + using std::cerr; -using std::runtime_error; -using boost::shared_ptr; +using std::cout; +using std::exception; +using std::make_shared; +using std::shared_ptr; +using std::string; +using std::vector; +using boost::optional; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +#ifdef DCPOMATIC_OSX +enum { + ID_tools_uninstall = DCPOMATIC_MAIN_MENU, +}; +#endif + + +class DirDialogWrapper : public wxDirDialog +{ +public: + DirDialogWrapper (wxWindow* parent) + : wxDirDialog (parent, _("Choose a DCP folder"), wxT(""), wxDD_DIR_MUST_EXIST) + { + + } + + boost::optional get () const + { + auto const dcp = boost::filesystem::path(wx_to_std(GetPath())); + if (!dcp::filesystem::exists(dcp / "ASSETMAP") && !dcp::filesystem::exists(dcp / "ASSETMAP.xml")) { + error_dialog (nullptr, _("No ASSETMAP or ASSETMAP.xml found in this folder. Please choose a DCP folder.")); + return {}; + } + + return dcp; + } + + void set (boost::filesystem::path) + { + /* Not used */ + } +}; + class DOMFrame : public wxFrame { public: explicit DOMFrame (wxString const & title) - : wxFrame (0, -1, title) + : wxFrame (nullptr, wxID_ANY, title) , _nanomsg (true) , _sizer (new wxBoxSizer(wxVERTICAL)) { +#ifdef DCPOMATIC_OSX + auto bar = new wxMenuBar; + auto tools = new wxMenu; + tools->Append(ID_tools_uninstall, _("Uninstall...")); + bar->Append(tools, _("Tools")); + SetMenuBar (bar); + Bind (wxEVT_MENU, boost::bind(&DOMFrame::uninstall, this), ID_tools_uninstall); +#endif + /* Use a panel as the only child of the Frame so that we avoid the dark-grey background on Windows. */ - wxPanel* overall_panel = new wxPanel (this); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); + auto overall_panel = new wxPanel (this); + auto s = new wxBoxSizer (wxHORIZONTAL); s->Add (overall_panel, 1, wxEXPAND); SetSizer (s); - wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + auto grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); int r = 0; - add_label_to_sizer (grid, overall_panel, _("DCP"), true, wxGBPosition(r, 0)); - wxBoxSizer* dcp_name_sizer = new wxBoxSizer (wxHORIZONTAL); - _dcp_name = new wxStaticText (overall_panel, wxID_ANY, wxEmptyString); - dcp_name_sizer->Add (_dcp_name, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_X_GAP); - _dcp_open = new wxButton (overall_panel, wxID_ANY, _("Open...")); - dcp_name_sizer->Add (_dcp_open, 0); - grid->Add (dcp_name_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND); + add_label_to_sizer(grid, overall_panel, _("DCPs"), true, wxGBPosition(r, 0)); + auto dcp_sizer = new wxBoxSizer (wxHORIZONTAL); + auto dcps = new EditableList( + overall_panel, + { EditableListColumn(_("DCP"), 300, true) }, + boost::bind(&DOMFrame::dcp_paths, this), + boost::bind(&DOMFrame::set_dcp_paths, this, _1), + [](boost::filesystem::path p, int) { return p.filename().string(); }, + EditableListTitle::INVISIBLE, + EditableListButton::NEW | EditableListButton::REMOVE + ); + + dcp_sizer->Add(dcps, 1, wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP); + grid->Add(dcp_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND); ++r; add_label_to_sizer (grid, overall_panel, _("Drive"), true, wxGBPosition(r, 0)); - wxBoxSizer* drive_sizer = new wxBoxSizer (wxHORIZONTAL); + auto drive_sizer = new wxBoxSizer (wxHORIZONTAL); _drive = new wxChoice (overall_panel, wxID_ANY); - drive_sizer->Add (_drive, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, DCPOMATIC_SIZER_X_GAP); + drive_sizer->Add(_drive, 1, wxTOP, 2); _drive_refresh = new wxButton (overall_panel, wxID_ANY, _("Refresh")); - drive_sizer->Add (_drive_refresh, 0); + drive_sizer->Add(_drive_refresh, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); grid->Add (drive_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND); ++r; @@ -88,13 +161,12 @@ public: grid->Add (_jobs, wxGBPosition(r, 0), wxGBSpan(6, 2), wxEXPAND); r += 6; - _copy = new wxButton (overall_panel, wxID_ANY, _("Copy DCP")); + _copy = new wxButton(overall_panel, wxID_ANY, _("Copy DCPs")); grid->Add (_copy, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND); ++r; grid->AddGrowableCol (1); - _dcp_open->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::open, this)); _copy->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::copy, this)); _drive->Bind (wxEVT_CHOICE, boost::bind(&DOMFrame::setup_sensitivity, this)); _drive_refresh->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::drive_refresh, this)); @@ -102,18 +174,29 @@ public: _sizer->Add (grid, 1, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER); overall_panel->SetSizer (_sizer); Fit (); - SetSize (768, GetSize().GetHeight() + 32); + SetSize(768, GetSize().GetHeight() + 32); /* 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.log")); + dcpomatic_log = make_shared(State::write_path("disk.log")); dcpomatic_log->set_types (dcpomatic_log->types() | LogEntry::TYPE_DISK); - LOG_DISK_NC("dcpomatic_disk started"); + LOG_DISK("dcpomatic_disk %1 started", dcpomatic_git_commit); - drive_refresh (); + { + int constexpr seconds_to_look = 3; + wxProgressDialog find_drives_progress(_("Disk Writer"), _("Finding disks"), seconds_to_look * 4, this); + for (auto i = 0; i < seconds_to_look * 4; ++i) { + if (!find_drives_progress.Update(i)) { + break; + } + drive_refresh(); + dcpomatic_sleep_milliseconds(250); + } + } - Bind (wxEVT_SIZE, boost::bind (&DOMFrame::sized, this, _1)); + Bind (wxEVT_SIZE, boost::bind(&DOMFrame::sized, this, _1)); + Bind (wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1)); JobManager::instance()->ActiveJobsChanged.connect(boost::bind(&DOMFrame::setup_sensitivity, this)); @@ -124,55 +207,189 @@ public: #endif #ifdef DCPOMATIC_LINUX - LOG_DISK("Starting writer process %1", disk_writer_path().string()); - _writer = new boost::process::child (disk_writer_path()); + if (getenv("DCPOMATIC_NO_START_WRITER")) { + LOG_DISK_NC("Not starting writer process as DCPOMATIC_NO_START_WRITER is set"); + } else { + LOG_DISK("Starting writer process %1", disk_writer_path().string()); + _writer = new boost::process::child (disk_writer_path()); + } #endif - /* _writer is always running on macOS at the moment */ +#ifdef DCPOMATIC_OSX + LOG_DISK_NC("Sending notification to writer daemon"); + notify_post ("com.dcpomatic.disk.writer.start"); +#endif } ~DOMFrame () { - _nanomsg.blocking_send(DISK_WRITER_QUIT "\n"); + _nanomsg.send(DISK_WRITER_QUIT "\n", 2000); + /* This seems really horrible but it's suggested by the examples on nanomsg.org, so... + * Without this the quit is not received (at least sometimes) causing #2018. + */ + dcpomatic_sleep_seconds (1); + } + + void set_dcp_paths (vector dcps) + { + _dcp_paths = dcps; + setup_sensitivity(); } private: + vector dcp_paths() const + { + return _dcp_paths; + } + void sized (wxSizeEvent& ev) { _sizer->Layout (); ev.Skip (); } - void open () + +#ifdef DCPOMATIC_OSX + void uninstall() + { + system(String::compose("osascript \"%1/uninstall_disk.applescript\"", resources_path().string()).c_str()); + } +#endif + + + bool should_close () { - wxDirDialog* d = new wxDirDialog (this, _("Choose a DCP folder"), wxT(""), wxDD_DIR_MUST_EXIST); - int r = d->ShowModal (); - boost::filesystem::path const path (wx_to_std(d->GetPath())); - d->Destroy (); + if (!JobManager::instance()->work_to_do()) { + return true; + } + + auto d = make_wx( + nullptr, + _("There are unfinished jobs; are you sure you want to quit?"), + _("Unfinished jobs"), + wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION + ); + + return d->ShowModal() == wxID_YES; + } - if (r != wxID_OK) { + + void close (wxCloseEvent& ev) + { + if (!should_close()) { + ev.Veto (); return; } - _dcp_path = path; - _dcp_name->SetLabel (std_to_wx(_dcp_path->filename().string())); - setup_sensitivity (); + ev.Skip (); + JobManager::drop (); } void copy () { + /* Check that the selected drive still exists and update its properties if so */ + drive_refresh (); + if (_drive->GetSelection() == wxNOT_FOUND) { + error_dialog (this, _("The disk you selected is no longer available. Please choose another.")); + return; + } + DCPOMATIC_ASSERT (_drive->GetSelection() != wxNOT_FOUND); - DCPOMATIC_ASSERT (static_cast(_dcp_path)); - DriveWipeWarningDialog* d = new DriveWipeWarningDialog (this, _drive->GetString(_drive->GetSelection())); - int const r = d->ShowModal (); - bool ok = r == wxID_OK && d->confirmed(); - d->Destroy (); + DCPOMATIC_ASSERT (!_dcp_paths.empty()); + + auto ping = [this](int attempt) { + if (_nanomsg.send(DISK_WRITER_PING "\n", 1000)) { + auto reply = DiskWriterBackEndResponse::read_from_nanomsg(_nanomsg, 1000); + if (reply && reply->type() == DiskWriterBackEndResponse::Type::PONG) { + return true; + } else if (reply) { + LOG_DISK("Unexpected response %1 to ping received (attempt %2)", static_cast(reply->type()), attempt); + } else { + LOG_DISK("No reply received from ping (attempt %1)", attempt); + } + } else { + LOG_DISK("Could not send ping to writer (attempt %1)", attempt); + } + dcpomatic_sleep_seconds (1); + return false; + }; + + bool have_writer = false; + for (int i = 0; i < 8; ++i) { + if (ping(i + 1)) { + have_writer = true; + break; + } + } - if (!ok) { + if (!have_writer) { +#if defined(DCPOMATIC_WINDOWS) + auto m = make_wx( + this, + variant::wx::dcpomatic_disk_writer(), + _("Do you see a 'User Account Control' dialogue asking about dcpomatic2_disk_writer.exe? If so, click 'Yes', then try again.") + ); + m->ShowModal (); + return; +#elif defined(DCPOMATIC_OSX) + auto m = make_wx( + this, + variant::wx::dcpomatic_disk_writer(), + variant::wx::insert_dcpomatic(_("Did you install the %s Disk Writer.pkg from the .dmg? Please check and try again.")) + ); + m->ShowModal (); + return; +#else + LOG_DISK_NC ("Failed to ping writer"); + throw CommunicationFailedError (); +#endif + } + + auto const& drive = _drives[_drive->GetSelection()]; + if (drive.mounted()) { + auto d = make_wx(this, drive.description()); + int const r = d->ShowModal (); + if (r != wxID_OK) { + return; + } + + LOG_DISK("Sending unmount request to disk writer for %1", drive.as_xml()); + if (!_nanomsg.send(DISK_WRITER_UNMOUNT "\n", 2000)) { + LOG_DISK_NC("Failed to send unmount request."); + throw CommunicationFailedError (); + } + if (!_nanomsg.send(drive.as_xml(), 2000)) { + LOG_DISK_NC("Failed to send drive for unmount request."); + throw CommunicationFailedError (); + } + /* The reply may have to wait for the user to authenticate, so let's wait a while */ + auto const reply = DiskWriterBackEndResponse::read_from_nanomsg(_nanomsg, 30000); + if (!reply || reply->type() != DiskWriterBackEndResponse::Type::OK) { + auto m = make_wx( + this, + variant::wx::dcpomatic_disk_writer(), + wxString::Format( + _("The drive %s could not be unmounted.\nClose any application that is using it, then try again. (%s)"), + std_to_wx(drive.description()), + reply->error_message() + ) + ); + m->ShowModal (); + return; + } + } + + + auto d = make_wx(this, _drive->GetString(_drive->GetSelection())); + if (d->ShowModal() != wxID_OK) { + return; + } + if (!d->confirmed()) { + message_dialog(this, _("You did not correctly confirm that you read the warning that was just shown. Please try again.")); return; } - JobManager::instance()->add(shared_ptr(new CopyToDriveJob(*_dcp_path, _drives[_drive->GetSelection()], _nanomsg))); + JobManager::instance()->add(make_shared(_dcp_paths, _drives[_drive->GetSelection()], _nanomsg)); setup_sensitivity (); } @@ -186,14 +403,9 @@ private: _drive->Clear (); int re_select = wxNOT_FOUND; int j = 0; - _drives.clear (); - BOOST_FOREACH (Drive i, get_drives()) { - if (!i.mounted()) { - _drives.push_back (i); - } - } - BOOST_FOREACH (Drive i, _drives) { - wxString const s = std_to_wx(i.description()); + _drives = Drive::get (); + for (auto i: _drives) { + auto const s = std_to_wx(i.description()); if (s == current) { re_select = j; } @@ -206,36 +418,44 @@ private: void setup_sensitivity () { - _copy->Enable (static_cast(_dcp_path) && _drive->GetSelection() != wxNOT_FOUND && !JobManager::instance()->work_to_do()); + _copy->Enable (!_dcp_paths.empty() && _drive->GetSelection() != wxNOT_FOUND && !JobManager::instance()->work_to_do()); } - wxStaticText* _dcp_name; - wxButton* _dcp_open; wxChoice* _drive; wxButton* _drive_refresh; wxButton* _copy; JobManagerView* _jobs; - boost::optional _dcp_path; + std::vector _dcp_paths; std::vector _drives; +#ifndef DCPOMATIC_OSX boost::process::child* _writer; +#endif Nanomsg _nanomsg; wxSizer* _sizer; }; + +static const wxCmdLineEntryDesc command_line_description[] = { + { wxCMD_LINE_OPTION, "d", "dcp", "DCP to write", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_SWITCH, "s", "sure", "skip alpha test warnings", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 } +}; + + class App : public wxApp { public: App () - : _frame (0) + : _frame (nullptr) {} - bool OnInit () + bool OnInit () override { try { Config::FailedToLoad.connect (boost::bind (&App::config_failed_to_load, this)); Config::Warning.connect (boost::bind (&App::config_warning, this, _1)); - SetAppName (_("DCP-o-matic Disk Writer")); + SetAppName(variant::wx::dcpomatic_disk_writer()); if (!wxApp::OnInit()) { return false; @@ -245,10 +465,9 @@ public: unsetenv ("UBUNTU_MENUPROXY"); #endif -#ifdef __WXOSX__ - ProcessSerialNumber serial; - GetCurrentProcess (&serial); - TransformProcessType (&serial, kProcessTransformToForegroundApplication); +#ifdef DCPOMATIC_OSX + dcpomatic_sleep_seconds (1); + make_foreground_application (); #endif dcpomatic_setup_path_encoding (); @@ -271,17 +490,56 @@ public: */ Config::drop (); - _frame = new DOMFrame (_("DCP-o-matic Disk Writer")); + if (!_skip_alpha_check) { + auto warning = make_wx(); + if (warning->ShowModal() != wxID_OK) { + return false; + } + if (!warning->confirmed()) { + message_dialog( + nullptr, + variant::wx::insert_dcpomatic_disk_writer( + _("You did not correctly confirm that you read the warning that was just shown. %s will close now. Please try again.") + ) + ); + return false; + } + } + + _frame = new DOMFrame(variant::wx::dcpomatic_disk_writer()); SetTopWindow (_frame); _frame->Show (); + if (_dcp_to_write) { + _frame->set_dcp_paths({*_dcp_to_write}); + } + signal_manager = new wxSignalManager (this); Bind (wxEVT_IDLE, boost::bind (&App::idle, this, _1)); } catch (exception& e) { - error_dialog (0, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what())); + error_dialog(nullptr, wxString::Format(_("%s could not start"), variant::wx::dcpomatic_disk_writer()), std_to_wx(e.what())); + return false; + } + + return true; + } + + void OnInitCmdLine (wxCmdLineParser& parser) override + { + parser.SetDesc (command_line_description); + parser.SetSwitchChars (wxT ("-")); + } + + bool OnCmdLineParsed (wxCmdLineParser& parser) override + { + _skip_alpha_check = parser.Found(wxT("sure")); + + wxString dcp; + if (parser.Found(wxT("dcp"), &dcp)) { + _dcp_to_write = wx_to_std (dcp); } return true; @@ -303,7 +561,49 @@ public: ev.Skip (); } + void report_exception () + { + try { + throw; + } catch (FileError& e) { + error_dialog ( + 0, + wxString::Format ( + _("An exception occurred: %s (%s)\n\n%s"), + std_to_wx(e.what()), + std_to_wx(e.file().string().c_str()), + wx::report_problem() + ) + ); + } catch (exception& e) { + error_dialog ( + 0, + wxString::Format ( + _("An exception occurred: %s.\n\n%s"), + std_to_wx(e.what()), + wx::report_problem() + ) + ); + } catch (...) { + error_dialog(nullptr, _("An unknown exception occurred.") + " " + wx::report_problem()); + } + } + + bool OnExceptionInMainLoop () override + { + report_exception (); + /* This will terminate the program */ + return false; + } + + void OnUnhandledException () override + { + report_exception (); + } + DOMFrame* _frame; + bool _skip_alpha_check = false; + boost::optional _dcp_to_write; }; IMPLEMENT_APP (App)