Rename VerifyDCPDialog -> VerifyDCPResultDialog.
[dcpomatic.git] / src / tools / dcpomatic_disk.cc
1 /*
2     Copyright (C) 2019-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 "wx/disk_warning_dialog.h"
23 #include "wx/drive_wipe_warning_dialog.h"
24 #include "wx/editable_list.h"
25 #include "wx/id.h"
26 #include "wx/job_manager_view.h"
27 #include "wx/message_dialog.h"
28 #include "wx/try_unmount_dialog.h"
29 #include "wx/wx_util.h"
30 #include "wx/wx_signal_manager.h"
31 #include "wx/wx_util.h"
32 #include "lib/config.h"
33 #include "lib/constants.h"
34 #include "lib/copy_to_drive_job.h"
35 #include "lib/cross.h"
36 #include "lib/dcpomatic_log.h"
37 #include "lib/disk_writer_messages.h"
38 #include "lib/file_log.h"
39 #include "lib/job_manager.h"
40 #include "lib/signal_manager.h"
41 #include "lib/util.h"
42 #include "lib/version.h"
43 #include <dcp/filesystem.h>
44 #include <dcp/warnings.h>
45 #include <wx/cmdline.h>
46 #include <wx/progdlg.h>
47 #include <wx/wx.h>
48 LIBDCP_DISABLE_WARNINGS
49 #include <boost/process.hpp>
50 LIBDCP_ENABLE_WARNINGS
51 #ifdef DCPOMATIC_WINDOWS
52 #include <boost/process/windows.hpp>
53 #endif
54 #ifdef DCPOMATIC_OSX
55 #include <notify.h>
56 #endif
57
58
59 using std::cerr;
60 using std::cout;
61 using std::exception;
62 using std::make_shared;
63 using std::shared_ptr;
64 using std::string;
65 using std::vector;
66 using boost::optional;
67 #if BOOST_VERSION >= 106100
68 using namespace boost::placeholders;
69 #endif
70
71
72 #ifdef DCPOMATIC_OSX
73 enum {
74         ID_tools_uninstall = DCPOMATIC_MAIN_MENU,
75 };
76 #endif
77
78
79 class DirDialogWrapper : public wxDirDialog
80 {
81 public:
82         DirDialogWrapper (wxWindow* parent)
83                 : wxDirDialog (parent, _("Choose a DCP folder"), wxT(""), wxDD_DIR_MUST_EXIST)
84         {
85
86         }
87
88         boost::optional<boost::filesystem::path> get () const
89         {
90                 auto const dcp = boost::filesystem::path(wx_to_std(GetPath()));
91                 if (!dcp::filesystem::exists(dcp / "ASSETMAP") && !dcp::filesystem::exists(dcp / "ASSETMAP.xml")) {
92                         error_dialog (nullptr, _("No ASSETMAP or ASSETMAP.xml found in this folder.  Please choose a DCP folder."));
93                         return {};
94                 }
95
96                 return dcp;
97         }
98
99         void set (boost::filesystem::path)
100         {
101                 /* Not used */
102         }
103 };
104
105
106 class DOMFrame : public wxFrame
107 {
108 public:
109         explicit DOMFrame (wxString const & title)
110                 : wxFrame (nullptr, wxID_ANY, title)
111                 , _nanomsg (true)
112                 , _sizer (new wxBoxSizer(wxVERTICAL))
113         {
114 #ifdef DCPOMATIC_OSX
115                 auto bar = new wxMenuBar;
116                 auto tools = new wxMenu;
117                 tools->Append(ID_tools_uninstall, _("Uninstall..."));
118                 bar->Append(tools, _("Tools"));
119                 SetMenuBar (bar);
120                 Bind (wxEVT_MENU, boost::bind(&DOMFrame::uninstall, this), ID_tools_uninstall);
121 #endif
122
123                 /* Use a panel as the only child of the Frame so that we avoid
124                    the dark-grey background on Windows.
125                 */
126                 auto overall_panel = new wxPanel (this);
127                 auto s = new wxBoxSizer (wxHORIZONTAL);
128                 s->Add (overall_panel, 1, wxEXPAND);
129                 SetSizer (s);
130
131                 auto grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
132
133                 int r = 0;
134                 add_label_to_sizer(grid, overall_panel, _("DCPs"), true, wxGBPosition(r, 0));
135                 auto dcp_sizer = new wxBoxSizer (wxHORIZONTAL);
136                 auto dcps = new EditableList<boost::filesystem::path, DirDialogWrapper>(
137                         overall_panel,
138                         { EditableListColumn(_("DCP"), 300, true) },
139                         boost::bind(&DOMFrame::dcp_paths, this),
140                         boost::bind(&DOMFrame::set_dcp_paths, this, _1),
141                         [](boost::filesystem::path p, int) { return p.filename().string(); },
142                         EditableListTitle::INVISIBLE,
143                         EditableListButton::NEW | EditableListButton::REMOVE
144                         );
145
146                 dcp_sizer->Add(dcps, 1, wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP);
147                 grid->Add(dcp_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
148                 ++r;
149
150                 add_label_to_sizer (grid, overall_panel, _("Drive"), true, wxGBPosition(r, 0));
151                 auto drive_sizer = new wxBoxSizer (wxHORIZONTAL);
152                 _drive = new wxChoice (overall_panel, wxID_ANY);
153                 drive_sizer->Add(_drive, 1, wxTOP, 2);
154                 _drive_refresh = new wxButton (overall_panel, wxID_ANY, _("Refresh"));
155                 drive_sizer->Add(_drive_refresh, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP);
156                 grid->Add (drive_sizer, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND);
157                 ++r;
158
159                 _jobs = new JobManagerView (overall_panel, false);
160                 grid->Add (_jobs, wxGBPosition(r, 0), wxGBSpan(6, 2), wxEXPAND);
161                 r += 6;
162
163                 _copy = new wxButton(overall_panel, wxID_ANY, _("Copy DCPs"));
164                 grid->Add (_copy, wxGBPosition(r, 0), wxGBSpan(1, 2), wxEXPAND);
165                 ++r;
166
167                 grid->AddGrowableCol (1);
168
169                 _copy->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::copy, this));
170                 _drive->Bind (wxEVT_CHOICE, boost::bind(&DOMFrame::setup_sensitivity, this));
171                 _drive_refresh->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::drive_refresh, this));
172
173                 _sizer->Add (grid, 1, wxALL | wxEXPAND, DCPOMATIC_DIALOG_BORDER);
174                 overall_panel->SetSizer (_sizer);
175                 Fit ();
176                 SetSize(768, GetSize().GetHeight() + 32);
177
178                 /* XXX: this is a hack, but I expect we'll need logs and I'm not sure if there's
179                  * a better place to put them.
180                  */
181                 dcpomatic_log = make_shared<FileLog>(State::write_path("disk.log"));
182                 dcpomatic_log->set_types (dcpomatic_log->types() | LogEntry::TYPE_DISK);
183                 LOG_DISK("dcpomatic_disk %1 started", dcpomatic_git_commit);
184
185                 {
186                         int constexpr seconds_to_look = 3;
187                         wxProgressDialog find_drives_progress(_("Disk Writer"), _("Finding disks"), seconds_to_look * 4, this);
188                         for (auto i = 0; i < seconds_to_look * 4; ++i) {
189                                 if (!find_drives_progress.Update(i)) {
190                                         break;
191                                 }
192                                 drive_refresh();
193                                 dcpomatic_sleep_milliseconds(250);
194                         }
195                 }
196
197                 Bind (wxEVT_SIZE, boost::bind(&DOMFrame::sized, this, _1));
198                 Bind (wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1));
199
200                 JobManager::instance()->ActiveJobsChanged.connect(boost::bind(&DOMFrame::setup_sensitivity, this));
201
202 #ifdef DCPOMATIC_WINDOWS
203                 /* We must use ::shell here, it seems, to avoid error code 740 (related to privilege escalation) */
204                 LOG_DISK("Starting writer process %1", disk_writer_path().string());
205                 _writer = new boost::process::child (disk_writer_path(), boost::process::shell, boost::process::windows::hide);
206 #endif
207
208 #ifdef DCPOMATIC_LINUX
209                 if (getenv("DCPOMATIC_NO_START_WRITER")) {
210                         LOG_DISK_NC("Not starting writer process as DCPOMATIC_NO_START_WRITER is set");
211                 } else {
212                         LOG_DISK("Starting writer process %1", disk_writer_path().string());
213                         _writer = new boost::process::child (disk_writer_path());
214                 }
215 #endif
216
217 #ifdef DCPOMATIC_OSX
218                 LOG_DISK_NC("Sending notification to writer daemon");
219                 notify_post ("com.dcpomatic.disk.writer.start");
220 #endif
221         }
222
223         ~DOMFrame ()
224         {
225                 _nanomsg.send(DISK_WRITER_QUIT "\n", 2000);
226                 /* This seems really horrible but it's suggested by the examples on nanomsg.org, so...
227                  * Without this the quit is not received (at least sometimes) causing #2018.
228                  */
229                 dcpomatic_sleep_seconds (1);
230         }
231
232         void set_dcp_paths (vector<boost::filesystem::path> dcps)
233         {
234                 _dcp_paths = dcps;
235                 setup_sensitivity();
236         }
237
238 private:
239         vector<boost::filesystem::path> dcp_paths() const
240         {
241                 return _dcp_paths;
242         }
243
244         void sized (wxSizeEvent& ev)
245         {
246                 _sizer->Layout ();
247                 ev.Skip ();
248         }
249
250
251 #ifdef DCPOMATIC_OSX
252         void uninstall()
253         {
254                 system(String::compose("osascript \"%1/uninstall_disk.applescript\"", resources_path().string()).c_str());
255         }
256 #endif
257
258
259         bool should_close ()
260         {
261                 if (!JobManager::instance()->work_to_do()) {
262                         return true;
263                 }
264
265                 auto d = make_wx<wxMessageDialog>(
266                         nullptr,
267                         _("There are unfinished jobs; are you sure you want to quit?"),
268                         _("Unfinished jobs"),
269                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
270                         );
271
272                 return d->ShowModal() == wxID_YES;
273         }
274
275
276         void close (wxCloseEvent& ev)
277         {
278                 if (!should_close()) {
279                         ev.Veto ();
280                         return;
281                 }
282
283                 ev.Skip ();
284                 JobManager::drop ();
285         }
286
287         void copy ()
288         {
289                 /* Check that the selected drive still exists and update its properties if so */
290                 drive_refresh ();
291                 if (_drive->GetSelection() == wxNOT_FOUND) {
292                         error_dialog (this, _("The disk you selected is no longer available.  Please choose another."));
293                         return;
294                 }
295
296                 DCPOMATIC_ASSERT (_drive->GetSelection() != wxNOT_FOUND);
297                 DCPOMATIC_ASSERT (!_dcp_paths.empty());
298
299                 auto ping = [this](int attempt) {
300                         if (_nanomsg.send(DISK_WRITER_PING "\n", 1000)) {
301                                 auto reply = DiskWriterBackEndResponse::read_from_nanomsg(_nanomsg, 1000);
302                                 if (reply && reply->type() == DiskWriterBackEndResponse::Type::PONG) {
303                                         return true;
304                                 } else if (reply) {
305                                         LOG_DISK("Unexpected response %1 to ping received (attempt %2)", static_cast<int>(reply->type()), attempt);
306                                 } else {
307                                         LOG_DISK("No reply received from ping (attempt %1)", attempt);
308                                 }
309                         } else {
310                                 LOG_DISK("Could not send ping to writer (attempt %1)", attempt);
311                         }
312                         dcpomatic_sleep_seconds (1);
313                         return false;
314                 };
315
316                 bool have_writer = false;
317                 for (int i = 0; i < 8; ++i) {
318                         if (ping(i + 1)) {
319                                 have_writer = true;
320                                 break;
321                         }
322                 }
323
324                 if (!have_writer) {
325 #if defined(DCPOMATIC_WINDOWS)
326                         auto m = make_wx<MessageDialog>(
327                                 this,
328                                 _("DCP-o-matic Disk Writer"),
329                                 _("Do you see a 'User Account Control' dialogue asking about dcpomatic2_disk_writer.exe?  If so, click 'Yes', then try again.")
330                                 );
331                         m->ShowModal ();
332                         return;
333 #elif defined(DCPOMATIC_OSX)
334                         auto m = make_wx<MessageDialog>(
335                                 this,
336                                 _("DCP-o-matic Disk Writer"),
337                                 _("Did you install the DCP-o-matic Disk Writer.pkg from the .dmg?  Please check and try again.")
338                                 );
339                         m->ShowModal ();
340                         return;
341 #else
342                         LOG_DISK_NC ("Failed to ping writer");
343                         throw CommunicationFailedError ();
344 #endif
345                 }
346
347                 auto const& drive = _drives[_drive->GetSelection()];
348                 if (drive.mounted()) {
349                         auto d = make_wx<TryUnmountDialog>(this, drive.description());
350                         int const r = d->ShowModal ();
351                         if (r != wxID_OK) {
352                                 return;
353                         }
354
355                         LOG_DISK("Sending unmount request to disk writer for %1", drive.as_xml());
356                         if (!_nanomsg.send(DISK_WRITER_UNMOUNT "\n", 2000)) {
357                                 LOG_DISK_NC("Failed to send unmount request.");
358                                 throw CommunicationFailedError ();
359                         }
360                         if (!_nanomsg.send(drive.as_xml(), 2000)) {
361                                 LOG_DISK_NC("Failed to send drive for unmount request.");
362                                 throw CommunicationFailedError ();
363                         }
364                         /* The reply may have to wait for the user to authenticate, so let's wait a while */
365                         auto const reply = DiskWriterBackEndResponse::read_from_nanomsg(_nanomsg, 30000);
366                         if (!reply || reply->type() != DiskWriterBackEndResponse::Type::OK) {
367                                 auto m = make_wx<MessageDialog>(
368                                                 this,
369                                                 _("DCP-o-matic Disk Writer"),
370                                                 wxString::Format(
371                                                         _("The drive %s could not be unmounted.\nClose any application that is using it, then try again. (%s)"),
372                                                         std_to_wx(drive.description()),
373                                                         reply->error_message()
374                                                         )
375                                                 );
376                                 m->ShowModal ();
377                                 return;
378                         }
379                 }
380
381
382                 auto d = make_wx<DriveWipeWarningDialog>(this, _drive->GetString(_drive->GetSelection()));
383                 if (d->ShowModal() != wxID_OK) {
384                         return;
385                 }
386                 if (!d->confirmed()) {
387                         message_dialog(this, _("You did not correctly confirm that you read the warning that was just shown.  Please try again."));
388                         return;
389                 }
390
391                 JobManager::instance()->add(make_shared<CopyToDriveJob>(_dcp_paths, _drives[_drive->GetSelection()], _nanomsg));
392                 setup_sensitivity ();
393         }
394
395         void drive_refresh ()
396         {
397                 int const sel = _drive->GetSelection ();
398                 wxString current;
399                 if (sel != wxNOT_FOUND) {
400                         current = _drive->GetString (sel);
401                 }
402                 _drive->Clear ();
403                 int re_select = wxNOT_FOUND;
404                 int j = 0;
405                 _drives = Drive::get ();
406                 for (auto i: _drives) {
407                         auto const s = std_to_wx(i.description());
408                         if (s == current) {
409                                 re_select = j;
410                         }
411                         _drive->Append(s);
412                         ++j;
413                 }
414                 _drive->SetSelection (re_select);
415                 setup_sensitivity ();
416         }
417
418         void setup_sensitivity ()
419         {
420                 _copy->Enable (!_dcp_paths.empty() && _drive->GetSelection() != wxNOT_FOUND && !JobManager::instance()->work_to_do());
421         }
422
423         wxChoice* _drive;
424         wxButton* _drive_refresh;
425         wxButton* _copy;
426         JobManagerView* _jobs;
427         std::vector<boost::filesystem::path> _dcp_paths;
428         std::vector<Drive> _drives;
429 #ifndef DCPOMATIC_OSX
430         boost::process::child* _writer;
431 #endif
432         Nanomsg _nanomsg;
433         wxSizer* _sizer;
434 };
435
436
437 static const wxCmdLineEntryDesc command_line_description[] = {
438         { wxCMD_LINE_OPTION, "d", "dcp", "DCP to write", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL },
439         { wxCMD_LINE_SWITCH, "s", "sure", "skip alpha test warnings", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL },
440         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
441 };
442
443
444 class App : public wxApp
445 {
446 public:
447         App ()
448                 : _frame (nullptr)
449         {}
450
451         bool OnInit () override
452         {
453                 try {
454                         Config::FailedToLoad.connect (boost::bind (&App::config_failed_to_load, this));
455                         Config::Warning.connect (boost::bind (&App::config_warning, this, _1));
456
457                         SetAppName (_("DCP-o-matic Disk Writer"));
458
459                         if (!wxApp::OnInit()) {
460                                 return false;
461                         }
462
463 #ifdef DCPOMATIC_LINUX
464                         unsetenv ("UBUNTU_MENUPROXY");
465 #endif
466
467 #ifdef DCPOMATIC_OSX
468                         dcpomatic_sleep_seconds (1);
469                         make_foreground_application ();
470 #endif
471
472                         dcpomatic_setup_path_encoding ();
473
474                         /* Enable i18n; this will create a Config object
475                            to look for a force-configured language.  This Config
476                            object will be wrong, however, because dcpomatic_setup
477                            hasn't yet been called and there aren't any filters etc.
478                            set up yet.
479                         */
480                         dcpomatic_setup_i18n ();
481
482                         /* Set things up, including filters etc.
483                            which will now be internationalised correctly.
484                         */
485                         dcpomatic_setup ();
486
487                         /* Force the configuration to be re-loaded correctly next
488                            time it is needed.
489                         */
490                         Config::drop ();
491
492                         if (!_skip_alpha_check) {
493                                 auto warning = make_wx<DiskWarningDialog>();
494                                 if (warning->ShowModal() != wxID_OK) {
495                                         return false;
496                                 }
497                                 if (!warning->confirmed()) {
498                                         message_dialog(nullptr, _("You did not correctly confirm that you read the warning that was just shown.  DCP-o-matic Disk Writer will close now.  Please try again."));
499                                         return false;
500                                 }
501                         }
502
503                         _frame = new DOMFrame (_("DCP-o-matic Disk Writer"));
504                         SetTopWindow (_frame);
505
506                         _frame->Show ();
507
508                         if (_dcp_to_write) {
509                                 _frame->set_dcp_paths({*_dcp_to_write});
510                         }
511
512                         signal_manager = new wxSignalManager (this);
513                         Bind (wxEVT_IDLE, boost::bind (&App::idle, this, _1));
514                 }
515                 catch (exception& e)
516                 {
517                         error_dialog (0, wxString::Format ("DCP-o-matic could not start."), std_to_wx(e.what()));
518                         return false;
519                 }
520
521                 return true;
522         }
523
524         void OnInitCmdLine (wxCmdLineParser& parser) override
525         {
526                 parser.SetDesc (command_line_description);
527                 parser.SetSwitchChars (wxT ("-"));
528         }
529
530         bool OnCmdLineParsed (wxCmdLineParser& parser) override
531         {
532                 _skip_alpha_check = parser.Found(wxT("sure"));
533
534                 wxString dcp;
535                 if (parser.Found(wxT("dcp"), &dcp)) {
536                         _dcp_to_write = wx_to_std (dcp);
537                 }
538
539                 return true;
540         }
541
542         void config_failed_to_load ()
543         {
544                 message_dialog (_frame, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
545         }
546
547         void config_warning (string m)
548         {
549                 message_dialog (_frame, std_to_wx(m));
550         }
551
552         void idle (wxIdleEvent& ev)
553         {
554                 signal_manager->ui_idle ();
555                 ev.Skip ();
556         }
557
558         void report_exception ()
559         {
560                 try {
561                         throw;
562                 } catch (FileError& e) {
563                         error_dialog (
564                                 0,
565                                 wxString::Format (
566                                         _("An exception occurred: %s (%s)\n\n") + REPORT_PROBLEM,
567                                         std_to_wx (e.what()),
568                                         std_to_wx (e.file().string().c_str ())
569                                         )
570                                 );
571                 } catch (exception& e) {
572                         error_dialog (
573                                 0,
574                                 wxString::Format (
575                                         _("An exception occurred: %s.\n\n") + REPORT_PROBLEM,
576                                         std_to_wx (e.what ())
577                                         )
578                                 );
579                 } catch (...) {
580                         error_dialog (0, _("An unknown exception occurred.") + "  " + REPORT_PROBLEM);
581                 }
582         }
583
584         bool OnExceptionInMainLoop () override
585         {
586                 report_exception ();
587                 /* This will terminate the program */
588                 return false;
589         }
590
591         void OnUnhandledException () override
592         {
593                 report_exception ();
594         }
595
596         DOMFrame* _frame;
597         bool _skip_alpha_check = false;
598         boost::optional<boost::filesystem::path> _dcp_to_write;
599 };
600
601 IMPLEMENT_APP (App)