/* Copyright (C) 2019-2021 Carl Hetherington This file is part of DCP-o-matic. DCP-o-matic is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. DCP-o-matic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with DCP-o-matic. If not, see . */ #include "wx/drive_wipe_warning_dialog.h" #include "wx/editable_list.h" #include "wx/i18n_setup.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/wx_variant.h" #include "lib/config.h" #include "lib/constants.h" #include "lib/copy_to_drive_job.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 #endif using std::cerr; 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"), {}, wxDD_DIR_MUST_EXIST) { } vector 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(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. */ auto overall_panel = new wxPanel(this); auto s = new wxBoxSizer(wxHORIZONTAL); s->Add(overall_panel, 1, wxEXPAND); SetSizer(s); auto grid = new wxGridBagSizer(DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); int r = 0; 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), EditableList::add_with_dialog, EditableList::edit_with_dialog, [](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)); auto drive_sizer = new wxBoxSizer(wxHORIZONTAL); _drive = new wxChoice(overall_panel, wxID_ANY); drive_sizer->Add(_drive, 1, wxTOP, 2); _drive_refresh = new wxButton(overall_panel, wxID_ANY, _("Refresh")); drive_sizer->Add(_drive_refresh, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); grid->Add(drive_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND); ++r; _jobs = new JobManagerView(overall_panel, false); grid->Add(_jobs, wxGBPosition(r, 0), wxGBSpan(6, 2), wxEXPAND); r += 6; _copy = new wxButton(overall_panel, wxID_ANY, _("Copy DCPs")); grid->Add(_copy, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND); ++r; grid->AddGrowableCol(1); _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)); _sizer->Add(grid, 1, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER); overall_panel->SetSizer(_sizer); Fit(); SetSize(768, GetSize().GetHeight() + 96); /* 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 = make_shared(State::write_path("disk.log")); dcpomatic_log->set_types(dcpomatic_log->types() | LogEntry::TYPE_DISK); LOG_DISK("dcpomatic_disk {} started", dcpomatic_git_commit); { 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_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1)); JobManager::instance()->ActiveJobsChanged.connect(boost::bind(&DOMFrame::setup_sensitivity, this)); #ifdef DCPOMATIC_WINDOWS /* We must use ::shell here, it seems, to avoid error code 740 (related to privilege escalation) */ LOG_DISK("Starting writer process {}", disk_writer_path().string()); _writer = new boost::process::child(disk_writer_path(), boost::process::shell, boost::process::windows::hide); #endif #ifdef DCPOMATIC_LINUX if (getenv("DCPOMATIC_NO_START_WRITER")) { LOG_DISK("Not starting writer process as DCPOMATIC_NO_START_WRITER is set"); } else { LOG_DISK("Starting writer process {}", disk_writer_path().string()); #ifdef DCPOMATIC_BOOST_PROCESS_V1 _writer = new boost::process::child(disk_writer_path()); #else _writer = new boost::process::v2::process(_context, disk_writer_path(), {}); #endif } #endif #ifdef DCPOMATIC_OSX LOG_DISK("Sending notification to writer daemon"); notify_post("com.dcpomatic.disk.writer.start"); #endif } ~DOMFrame() { _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(); } #ifdef DCPOMATIC_OSX void uninstall() { system(fmt::format("osascript \"{}/uninstall_disk.applescript\"", resources_path().string()).c_str()); } #endif bool should_close() { if (!JobManager::instance()->work_to_do()) { return true; } wxMessageDialog dialog( nullptr, _("There are unfinished jobs; are you sure you want to quit?"), _("Unfinished jobs"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION ); return dialog.ShowModal() == wxID_YES; } void close(wxCloseEvent& ev) { if (!should_close()) { ev.Veto(); return; } 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(!_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 {} to ping received (attempt {})", static_cast(reply->type()), attempt); } else { LOG_DISK("No reply received from ping (attempt {})", attempt); } } else { LOG_DISK("Could not send ping to writer (attempt {})", 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 (!have_writer) { #if defined(DCPOMATIC_WINDOWS) MessageDialog dialog( 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.") ); dialog.ShowModal(); return; #elif defined(DCPOMATIC_OSX) MessageDialog dialog( 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.")) ); dialog.ShowModal(); return; #else LOG_DISK("Failed to ping writer"); throw CommunicationFailedError(); #endif } auto const& drive = _drives[_drive->GetSelection()]; if (drive.mounted()) { TryUnmountDialog dialog(this, std_to_wx(drive.description())); int const r = dialog.ShowModal(); if (r != wxID_OK) { return; } LOG_DISK("Sending unmount request to disk writer for {}", drive.as_xml()); if (!_nanomsg.send(DISK_WRITER_UNMOUNT "\n", 2000)) { LOG_DISK("Failed to send unmount request."); throw CommunicationFailedError(); } if (!_nanomsg.send(drive.as_xml(), 2000)) { LOG_DISK("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) { MessageDialog dialog( 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()), std_to_wx(reply->error_message()) ) ); dialog.ShowModal(); return; } } DriveWipeWarningDialog dialog(this, _drive->GetString(_drive->GetSelection())); if (dialog.ShowModal() != wxID_OK) { return; } if (!dialog.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(make_shared(_dcp_paths, _drives[_drive->GetSelection()], _nanomsg)); setup_sensitivity(); } void drive_refresh() { int const sel = _drive->GetSelection(); wxString current; if (sel != wxNOT_FOUND) { current = _drive->GetString(sel); } _drive->Clear(); int re_select = wxNOT_FOUND; int j = 0; _drives = Drive::get(); for (auto i: _drives) { auto const s = std_to_wx(i.description()); if (s == current) { re_select = j; } _drive->Append(s); ++j; } _drive->SetSelection(re_select); setup_sensitivity(); } void setup_sensitivity() { _copy->Enable(!_dcp_paths.empty() && _drive->GetSelection() != wxNOT_FOUND && !JobManager::instance()->work_to_do()); } wxChoice* _drive; wxButton* _drive_refresh; wxButton* _copy; JobManagerView* _jobs; std::vector _dcp_paths; std::vector _drives; #ifndef DCPOMATIC_OSX #ifdef DCPOMATIC_BOOST_PROCESS_V1 boost::process::child* _writer; #else boost::process::v2::process* _writer; boost::asio::io_context _context; #endif #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(nullptr) {} 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(variant::wx::dcpomatic_disk_writer()); if (!wxApp::OnInit()) { return false; } #ifdef DCPOMATIC_LINUX unsetenv("UBUNTU_MENUPROXY"); #endif #ifdef DCPOMATIC_OSX dcpomatic_sleep_seconds(1); make_foreground_application(); #endif dcpomatic_setup_path_encoding(); /* Enable i18n; this will create a Config object to look for a force-configured language. This Config object will be wrong, however, because dcpomatic_setup hasn't yet been called and there aren't any filters etc. set up yet. */ dcpomatic::wx::setup_i18n(); /* Set things up, including filters etc. which will now be internationalised correctly. */ dcpomatic_setup(); /* Force the configuration to be re-loaded correctly next time it is needed. */ Config::drop(); _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(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(char_to_wx("-")); } bool OnCmdLineParsed(wxCmdLineParser& parser) override { wxString dcp; if (parser.Found(char_to_wx("dcp"), &dcp)) { _dcp_to_write = wx_to_std(dcp); } return true; } void config_failed_to_load() { message_dialog(_frame, _("The existing configuration failed to load. Default values will be used instead. These may take a short time to create.")); } void config_warning(string m) { message_dialog(_frame, std_to_wx(m)); } void idle(wxIdleEvent& ev) { signal_manager->ui_idle(); 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()), dcpomatic::wx::report_problem() ) ); } catch (exception& e) { error_dialog( 0, wxString::Format( _("An exception occurred: %s.\n\n%s"), std_to_wx(e.what()), dcpomatic::wx::report_problem() ) ); } catch (...) { error_dialog(nullptr, wxString::Format(_("An unknown exception occurred. %s"), dcpomatic::wx::report_problem())); } } bool OnExceptionInMainLoop() override { report_exception(); /* This will terminate the program */ return false; } void OnUnhandledException() override { report_exception(); } DOMFrame* _frame; boost::optional _dcp_to_write; }; IMPLEMENT_APP(App)