Check some unsanitized network inputs before allocating memory using them.
[dcpomatic.git] / src / tools / dcpomatic_batch.cc
1 /*
2     Copyright (C) 2013-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/about_dialog.h"
23 #include "wx/dcpomatic_button.h"
24 #include "wx/full_config_dialog.h"
25 #include "wx/job_manager_view.h"
26 #include "wx/servers_list_dialog.h"
27 #include "wx/wx_ptr.h"
28 #include "wx/wx_signal_manager.h"
29 #include "wx/wx_util.h"
30 #include "lib/compose.hpp"
31 #include "lib/config.h"
32 #include "lib/dcpomatic_socket.h"
33 #include "lib/film.h"
34 #include "lib/job.h"
35 #include "lib/job_manager.h"
36 #include "lib/make_dcp.h"
37 #include "lib/transcode_job.h"
38 #include "lib/util.h"
39 #include "lib/version.h"
40 #include <dcp/filesystem.h>
41 #include <dcp/warnings.h>
42 LIBDCP_DISABLE_WARNINGS
43 #include <wx/aboutdlg.h>
44 #include <wx/cmdline.h>
45 #include <wx/dnd.h>
46 #include <wx/preferences.h>
47 #include <wx/splash.h>
48 #include <wx/stdpaths.h>
49 #include <wx/wx.h>
50 LIBDCP_ENABLE_WARNINGS
51 #include <iostream>
52 #include <set>
53
54
55 using std::cout;
56 using std::dynamic_pointer_cast;
57 using std::exception;
58 using std::list;
59 using std::make_shared;
60 using std::set;
61 using std::shared_ptr;
62 using std::string;
63 using boost::scoped_array;
64 using boost::thread;
65 #if BOOST_VERSION >= 106100
66 using namespace boost::placeholders;
67 #endif
68
69
70 static list<boost::filesystem::path> films_to_load;
71
72
73 enum {
74         ID_file_add_film = 1,
75         ID_tools_encoding_servers,
76         ID_help_about
77 };
78
79
80 void
81 setup_menu (wxMenuBar* m)
82 {
83         auto file = new wxMenu;
84         file->Append (ID_file_add_film, _("&Add Film...\tCtrl-A"));
85 #ifdef DCPOMATIC_OSX
86         file->Append (wxID_EXIT, _("&Exit"));
87 #else
88         file->Append (wxID_EXIT, _("&Quit"));
89 #endif
90
91 #ifdef DCPOMATIC_OSX
92         file->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
93 #else
94         auto edit = new wxMenu;
95         edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
96 #endif
97
98         auto tools = new wxMenu;
99         tools->Append (ID_tools_encoding_servers, _("Encoding servers..."));
100
101         auto help = new wxMenu;
102         help->Append (ID_help_about, _("About"));
103
104         m->Append (file, _("&File"));
105 #ifndef DCPOMATIC_OSX
106         m->Append (edit, _("&Edit"));
107 #endif
108         m->Append (tools, _("&Tools"));
109         m->Append (help, _("&Help"));
110 }
111
112
113 class DOMFrame : public wxFrame
114 {
115 public:
116         enum class Tool {
117                 ADD,
118                 PAUSE
119         };
120
121         class DCPDropTarget : public wxFileDropTarget
122         {
123         public:
124                 DCPDropTarget(DOMFrame* owner)
125                         : _frame(owner)
126                 {}
127
128                 bool OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) override
129                 {
130                         if (filenames.GetCount() == 1) {
131                                 /* Try to load a directory */
132                                 auto path = boost::filesystem::path(wx_to_std(filenames[0]));
133                                 if (dcp::filesystem::is_directory(path)) {
134                                         _frame->start_job(wx_to_std(filenames[0]));
135                                         return true;
136                                 }
137                         }
138
139                         return false;
140                 }
141
142         private:
143                 DOMFrame* _frame;
144         };
145
146         explicit DOMFrame (wxString const & title)
147                 : wxFrame (nullptr, -1, title)
148                 , _sizer (new wxBoxSizer(wxVERTICAL))
149         {
150                 auto bar = new wxMenuBar;
151                 setup_menu (bar);
152                 SetMenuBar (bar);
153
154                 Config::instance()->Changed.connect (boost::bind (&DOMFrame::config_changed, this, _1));
155
156                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_add_film, this),    ID_file_add_film);
157                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_quit, this),        wxID_EXIT);
158                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
159                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_encoding_servers, this), ID_tools_encoding_servers);
160                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),       ID_help_about);
161
162                 auto panel = new wxPanel (this);
163                 auto s = new wxBoxSizer (wxHORIZONTAL);
164                 s->Add (panel, 1, wxEXPAND);
165                 SetSizer (s);
166
167                 wxBitmap add(icon_path("add"), wxBITMAP_TYPE_PNG);
168                 wxBitmap pause(icon_path("pause"), wxBITMAP_TYPE_PNG);
169
170                 auto toolbar = new wxToolBar(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
171                 toolbar->SetMargins(4, 4);
172                 toolbar->SetToolBitmapSize(wxSize(32, 32));
173                 toolbar->AddTool(static_cast<int>(Tool::ADD), _("Add film"), add, _("Add film for conversion"));
174                 toolbar->AddCheckTool(static_cast<int>(Tool::PAUSE), _("Pause/resume"), pause, wxNullBitmap, _("Pause or resume conversion"));
175                 toolbar->Realize();
176                 _sizer->Add(toolbar, 0, wxALL, 6);
177
178                 toolbar->Bind(wxEVT_TOOL, bind(&DOMFrame::tool_clicked, this, _1));
179
180                 auto job_manager_view = new JobManagerView (panel, true);
181                 _sizer->Add (job_manager_view, 1, wxALL | wxEXPAND, 6);
182
183                 panel->SetSizer (_sizer);
184
185                 Bind (wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1));
186                 Bind (wxEVT_SIZE, boost::bind(&DOMFrame::sized, this, _1));
187
188                 SetDropTarget(new DCPDropTarget(this));
189         }
190
191         void tool_clicked(wxCommandEvent& ev)
192         {
193                 switch (static_cast<Tool>(ev.GetId())) {
194                 case Tool::ADD:
195                         add_film();
196                         break;
197                 case Tool::PAUSE:
198                 {
199                         auto jm = JobManager::instance();
200                         if (jm->paused()) {
201                                 jm->resume();
202                         } else {
203                                 jm->pause();
204                         }
205                         break;
206                 }
207                 }
208         }
209
210         void start_job (boost::filesystem::path path)
211         {
212                 try {
213                         auto film = make_shared<Film>(path);
214                         film->read_metadata ();
215
216                         double total_required;
217                         double available;
218                         bool can_hard_link;
219
220                         film->should_be_enough_disk_space (total_required, available, can_hard_link);
221
222                         set<shared_ptr<const Film>> films;
223
224                         for (auto i: JobManager::instance()->get()) {
225                                 films.insert (i->film());
226                         }
227
228                         for (auto i: films) {
229                                 double progress = 0;
230                                 for (auto j: JobManager::instance()->get()) {
231                                         if (i == j->film() && dynamic_pointer_cast<TranscodeJob>(j)) {
232                                                 progress = j->progress().get_value_or(0);
233                                         }
234                                 }
235
236                                 double required;
237                                 i->should_be_enough_disk_space (required, available, can_hard_link);
238                                 total_required += (1 - progress) * required;
239                         }
240
241                         if ((total_required - available) > 1) {
242                                 if (!confirm_dialog (
243                                             this,
244                                             wxString::Format(
245                                                     _("The DCPs for this film and the films already in the queue will take up about %.1f GB.  The "
246                                                       "disks that you are using only have %.1f GB available.  Do you want to add this film to the queue anyway?"),
247                                                     total_required, available))) {
248                                         return;
249                                 }
250                         }
251
252                         make_dcp (film, TranscodeJob::ChangedBehaviour::STOP);
253                 } catch (std::exception& e) {
254                         auto p = std_to_wx (path.string ());
255                         auto b = p.ToUTF8 ();
256                         error_dialog (this, wxString::Format(_("Could not open film at %s"), p.data()), std_to_wx(e.what()));
257                 }
258         }
259
260 private:
261         void sized (wxSizeEvent& ev)
262         {
263                 _sizer->Layout ();
264                 ev.Skip ();
265         }
266
267         bool should_close ()
268         {
269                 if (!JobManager::instance()->work_to_do()) {
270                         return true;
271                 }
272
273                 auto d = make_wx<wxMessageDialog>(
274                         nullptr,
275                         _("There are unfinished jobs; are you sure you want to quit?"),
276                         _("Unfinished jobs"),
277                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
278                         );
279
280                 return d->ShowModal() == wxID_YES;
281         }
282
283         void close (wxCloseEvent& ev)
284         {
285                 if (!should_close()) {
286                         ev.Veto ();
287                         return;
288                 }
289
290                 ev.Skip ();
291         }
292
293         void file_add_film ()
294         {
295                 add_film ();
296         }
297
298         void file_quit ()
299         {
300                 if (should_close()) {
301                         Close (true);
302                 }
303         }
304
305         void edit_preferences ()
306         {
307                 if (!_config_dialog) {
308                         _config_dialog = create_full_config_dialog ();
309                 }
310                 _config_dialog->Show (this);
311         }
312
313         void tools_encoding_servers ()
314         {
315                 if (!_servers_list_dialog) {
316                         _servers_list_dialog = new ServersListDialog (this);
317                 }
318
319                 _servers_list_dialog->Show ();
320         }
321
322         void help_about ()
323         {
324                 auto d = make_wx<AboutDialog>(this);
325                 d->ShowModal ();
326         }
327
328         void add_film ()
329         {
330                 auto dialog = make_wx<wxDirDialog>(this, _("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
331                 if (_last_parent) {
332                         dialog->SetPath(std_to_wx(_last_parent.get().string()));
333                 }
334
335                 int r;
336                 while (true) {
337                         r = dialog->ShowModal();
338                         if (r == wxID_OK && dialog->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
339                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
340                         } else {
341                                 break;
342                         }
343                 }
344
345                 if (r == wxID_OK) {
346                         start_job(wx_to_std(dialog->GetPath()));
347                 }
348
349                 _last_parent = boost::filesystem::path(wx_to_std(dialog->GetPath())).parent_path();
350         }
351
352         void config_changed (Config::Property what)
353         {
354                 /* Instantly save any config changes when using the DCP-o-matic GUI */
355                 if (what == Config::CINEMAS) {
356                         try {
357                                 Config::instance()->write_cinemas();
358                         } catch (exception& e) {
359                                 error_dialog (
360                                         this,
361                                         wxString::Format(
362                                                 _("Could not write to cinemas file at %s.  Your changes have not been saved."),
363                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
364                                                 )
365                                         );
366                         }
367                 } else {
368                         try {
369                                 Config::instance()->write_config();
370                         } catch (exception& e) {
371                                 error_dialog (
372                                         this,
373                                         wxString::Format(
374                                                 _("Could not write to config file at %s.  Your changes have not been saved."),
375                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
376                                                 )
377                                         );
378                         }
379                 }
380         }
381
382         boost::optional<boost::filesystem::path> _last_parent;
383         wxSizer* _sizer;
384         wxPreferencesEditor* _config_dialog = nullptr;
385         ServersListDialog* _servers_list_dialog = nullptr;
386 };
387
388
389 static const wxCmdLineEntryDesc command_line_description[] = {
390         { wxCMD_LINE_PARAM, 0, 0, "film to load", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL },
391         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
392 };
393
394
395 class JobServer : public Server, public Signaller
396 {
397 public:
398         JobServer()
399                 : Server (BATCH_JOB_PORT)
400         {}
401
402         void handle (shared_ptr<Socket> socket) override
403         {
404                 try {
405                         auto const length = socket->read_uint32();
406                         if (length < 65536) {
407                                 scoped_array<char> buffer(new char[length]);
408                                 socket->read(reinterpret_cast<uint8_t*>(buffer.get()), length);
409                                 string s(buffer.get());
410                                 emit(boost::bind(boost::ref(StartJob), s));
411                                 socket->write (reinterpret_cast<uint8_t const *>("OK"), 3);
412                         }
413                 } catch (...) {
414
415                 }
416         }
417
418         boost::signals2::signal<void(std::string)> StartJob;
419 };
420
421
422 class App : public wxApp
423 {
424         bool OnInit () override
425         {
426                 wxInitAllImageHandlers ();
427
428                 SetAppName (_("DCP-o-matic Batch Converter"));
429                 is_batch_converter = true;
430
431                 Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
432                 Config::Warning.connect (boost::bind(&App::config_warning, this, _1));
433
434                 auto splash = maybe_show_splash ();
435
436                 if (!wxApp::OnInit()) {
437                         return false;
438                 }
439
440 #ifdef DCPOMATIC_LINUX
441                 unsetenv ("UBUNTU_MENUPROXY");
442 #endif
443
444                 dcpomatic_setup_path_encoding ();
445
446                 /* Enable i18n; this will create a Config object
447                    to look for a force-configured language.  This Config
448                    object will be wrong, however, because dcpomatic_setup
449                    hasn't yet been called and there aren't any filters etc.
450                    set up yet.
451                 */
452                 dcpomatic_setup_i18n ();
453
454                 /* Set things up, including filters etc.
455                    which will now be internationalised correctly.
456                 */
457                 dcpomatic_setup ();
458
459                 /* Force the configuration to be re-loaded correctly next
460                    time it is needed.
461                 */
462                 Config::drop ();
463
464                 _frame = new DOMFrame (_("DCP-o-matic Batch Converter"));
465                 SetTopWindow (_frame);
466                 _frame->Maximize ();
467                 if (splash) {
468                         splash->Destroy ();
469                 }
470                 _frame->Show ();
471
472                 try {
473                         auto server = new JobServer();
474                         server->StartJob.connect(bind(&DOMFrame::start_job, _frame, _1));
475                         new thread (boost::bind (&JobServer::run, server));
476                 } catch (boost::system::system_error& e) {
477                         error_dialog(_frame, _("Could not listen for new batch jobs.  Perhaps another instance of the DCP-o-matic Batch Converter is running."));
478                 }
479
480                 signal_manager = new wxSignalManager (this);
481                 this->Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
482
483                 shared_ptr<Film> film;
484                 for (auto i: films_to_load) {
485                         if (dcp::filesystem::is_directory(i)) {
486                                 try {
487                                         film = make_shared<Film>(i);
488                                         film->read_metadata ();
489                                         make_dcp (film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
490                                 } catch (exception& e) {
491                                         error_dialog (
492                                                 0,
493                                                 std_to_wx(String::compose(wx_to_std(_("Could not load film %1")), i.string())),
494                                                 std_to_wx(e.what())
495                                                 );
496                                 }
497                         }
498                 }
499
500                 return true;
501         }
502
503         void idle ()
504         {
505                 signal_manager->ui_idle ();
506         }
507
508         void OnInitCmdLine (wxCmdLineParser& parser) override
509         {
510                 parser.SetDesc (command_line_description);
511                 parser.SetSwitchChars (wxT ("-"));
512         }
513
514         bool OnCmdLineParsed (wxCmdLineParser& parser) override
515         {
516                 for (size_t i = 0; i < parser.GetParamCount(); ++i) {
517                         films_to_load.push_back (wx_to_std(parser.GetParam(i)));
518                 }
519
520                 return true;
521         }
522
523         void config_failed_to_load(Config::LoadFailure what)
524         {
525                 report_config_load_failure(_frame, what);
526         }
527
528         void config_warning (string m)
529         {
530                 message_dialog (_frame, std_to_wx(m));
531         }
532
533         DOMFrame* _frame;
534 };
535
536
537 IMPLEMENT_APP (App)