C++11 and whitespace cleanups.
[dcpomatic.git] / src / lib / job.cc
1 /*
2     Copyright (C) 2012-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 /** @file  src/job.cc
23  *  @brief A parent class to represent long-running tasks which are run in their own thread.
24  */
25
26
27 #include "compose.hpp"
28 #include "cross.h"
29 #include "dcpomatic_log.h"
30 #include "exceptions.h"
31 #include "film.h"
32 #include "job.h"
33 #include "log.h"
34 #include "util.h"
35 #include <dcp/exceptions.h>
36 #include <sub/exceptions.h>
37 #include <boost/date_time/posix_time/posix_time.hpp>
38 #include <boost/filesystem.hpp>
39 #include <boost/thread.hpp>
40 #include <iostream>
41
42 #include "i18n.h"
43
44
45 using std::cout;
46 using std::function;
47 using std::list;
48 using std::shared_ptr;
49 using std::string;
50 using boost::optional;
51 using namespace dcpomatic;
52
53
54 /** @param film Associated film, or 0 */
55 Job::Job (shared_ptr<const Film> film)
56         : _film (film)
57         , _state (NEW)
58         , _start_time (0)
59         , _sub_start_time (0)
60         , _progress (0)
61         , _ran_for (0)
62 {
63
64 }
65
66
67 Job::~Job ()
68 {
69 #ifdef DCPOMATIC_DEBUG
70         /* Any subclass should have called stop_thread in its destructor */
71         assert (!_thread.joinable());
72 #endif
73 }
74
75
76 void
77 Job::stop_thread ()
78 {
79         boost::this_thread::disable_interruption dis;
80
81         _thread.interrupt ();
82         try {
83                 _thread.join ();
84         } catch (...) {}
85 }
86
87
88 /** Start the job in a separate thread, returning immediately */
89 void
90 Job::start ()
91 {
92         set_state (RUNNING);
93         _start_time = time (0);
94         _sub_start_time = time (0);
95         _thread = boost::thread (boost::bind(&Job::run_wrapper, this));
96 #ifdef DCPOMATIC_LINUX
97         pthread_setname_np (_thread.native_handle(), "job-wrapper");
98 #endif
99 }
100
101
102 /** A wrapper for the ::run() method to catch exceptions */
103 void
104 Job::run_wrapper ()
105 {
106         start_of_thread (String::compose("Job-%1", json_name()));
107
108         try {
109
110                 run ();
111
112         } catch (dcp::FileError& e) {
113
114                 string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf());
115
116                 try {
117                         auto const s = boost::filesystem::space (e.filename());
118                         if (s.available < pow (1024, 3)) {
119                                 m += N_("\n\n");
120                                 m += _("The drive that the film is stored on is low in disc space.  Free some more space and try again.");
121                         }
122                 } catch (...) {
123
124                 }
125
126                 set_error (e.what(), m);
127                 set_progress (1);
128                 set_state (FINISHED_ERROR);
129
130         } catch (dcp::StartCompressionError& e) {
131
132                 bool done = false;
133
134 #ifdef DCPOMATIC_WINDOWS
135 #if (__GNUC__ && !__x86_64__)
136                 /* 32-bit */
137                 set_error (
138                         _("Failed to encode the DCP."),
139                         _("This error has probably occurred because you are running the 32-bit version of DCP-o-matic and "
140                           "trying to use too many encoding threads.  Please reduce the 'number of threads DCP-o-matic should "
141                           "use' in the General tab of Preferences and try again.")
142                         );
143                 done = true;
144 #else
145                 /* 64-bit */
146                 if (running_32_on_64()) {
147                         set_error (
148                                 _("Failed to encode the DCP."),
149                                 _("This error has probably occurred because you are running the 32-bit version of DCP-o-matic.  Please re-install DCP-o-matic with the 64-bit installer and try again.")
150                                 );
151                         done = true;
152                 }
153 #endif
154 #endif
155
156                 if (!done) {
157                         set_error (
158                                 e.what (),
159                                 string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
160                                 );
161                 }
162
163                 set_progress (1);
164                 set_state (FINISHED_ERROR);
165
166         } catch (OpenFileError& e) {
167
168                 set_error (
169                         String::compose (_("Could not open %1"), e.file().string()),
170                         String::compose (
171                                 _("DCP-o-matic could not open the file %1 (%2).  Perhaps it does not exist or is in an unexpected format."),
172                                 boost::filesystem::absolute (e.file()).string(),
173                                 e.what()
174                                 )
175                         );
176
177                 set_progress (1);
178                 set_state (FINISHED_ERROR);
179
180         } catch (boost::filesystem::filesystem_error& e) {
181
182                 if (e.code() == boost::system::errc::no_such_file_or_directory) {
183                         set_error (
184                                 String::compose (_("Could not open %1"), e.path1().string ()),
185                                 String::compose (
186                                         _("DCP-o-matic could not open the file %1 (%2).  Perhaps it does not exist or is in an unexpected format."),
187                                         boost::filesystem::absolute (e.path1()).string(),
188                                         e.what()
189                                         )
190                                 );
191                 } else {
192                         set_error (
193                                 e.what (),
194                                 string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
195                                 );
196                 }
197
198                 set_progress (1);
199                 set_state (FINISHED_ERROR);
200
201         } catch (boost::thread_interrupted &) {
202
203                 set_state (FINISHED_CANCELLED);
204
205         } catch (sub::SubripError& e) {
206
207                 string extra = "Error is near:\n";
208                 for (auto i: e.context()) {
209                         extra += i + "\n";
210                 }
211
212                 set_error (e.what (), extra);
213                 set_progress (1);
214                 set_state (FINISHED_ERROR);
215
216         } catch (std::bad_alloc& e) {
217
218                 set_error (_("Out of memory"), _("There was not enough memory to do this.  If you are running a 32-bit operating system try reducing the number of encoding threads in the General tab of Preferences."));
219                 set_progress (1);
220                 set_state (FINISHED_ERROR);
221
222         } catch (dcp::ReadError& e) {
223
224                 set_error (e.message(), e.detail().get_value_or(""));
225                 set_progress (1);
226                 set_state (FINISHED_ERROR);
227
228         } catch (KDMError& e) {
229
230                 set_error (e.summary(), e.detail());
231                 set_progress (1);
232                 set_state (FINISHED_ERROR);
233
234         } catch (FileError& e) {
235
236                 set_error (e.what(), e.what());
237                 set_progress (1);
238                 set_state (FINISHED_ERROR);
239
240         } catch (std::exception& e) {
241
242                 set_error (
243                         e.what (),
244                         string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
245                         );
246
247                 set_progress (1);
248                 set_state (FINISHED_ERROR);
249
250         } catch (...) {
251
252                 set_error (
253                         _("Unknown error"),
254                         string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
255                         );
256
257                 set_progress (1);
258                 set_state (FINISHED_ERROR);
259         }
260 }
261
262
263 /** @return true if this job is new (ie has not started running) */
264 bool
265 Job::is_new () const
266 {
267         boost::mutex::scoped_lock lm (_state_mutex);
268         return _state == NEW;
269 }
270
271
272 /** @return true if the job is running */
273 bool
274 Job::running () const
275 {
276         boost::mutex::scoped_lock lm (_state_mutex);
277         return _state == RUNNING;
278 }
279
280
281 /** @return true if the job has finished (either successfully or unsuccessfully) */
282 bool
283 Job::finished () const
284 {
285         boost::mutex::scoped_lock lm (_state_mutex);
286         return _state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED;
287 }
288
289
290 /** @return true if the job has finished successfully */
291 bool
292 Job::finished_ok () const
293 {
294         boost::mutex::scoped_lock lm (_state_mutex);
295         return _state == FINISHED_OK;
296 }
297
298
299 /** @return true if the job has finished unsuccessfully */
300 bool
301 Job::finished_in_error () const
302 {
303         boost::mutex::scoped_lock lm (_state_mutex);
304         return _state == FINISHED_ERROR;
305 }
306
307
308 bool
309 Job::finished_cancelled () const
310 {
311         boost::mutex::scoped_lock lm (_state_mutex);
312         return _state == FINISHED_CANCELLED;
313 }
314
315
316 bool
317 Job::paused_by_user () const
318 {
319         boost::mutex::scoped_lock lm (_state_mutex);
320         return _state == PAUSED_BY_USER;
321 }
322
323
324 bool
325 Job::paused_by_priority () const
326 {
327         boost::mutex::scoped_lock lm (_state_mutex);
328         return _state == PAUSED_BY_PRIORITY;
329 }
330
331
332 /** Set the state of this job.
333  *  @param s New state.
334  */
335 void
336 Job::set_state (State s)
337 {
338         bool finished = false;
339
340         {
341                 boost::mutex::scoped_lock lm (_state_mutex);
342                 _state = s;
343
344                 if (_state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED) {
345                         _ran_for = time(0) - _start_time;
346                         finished = true;
347                         _sub_name.clear ();
348                 }
349         }
350
351         if (finished) {
352                 emit (boost::bind (boost::ref (Finished)));
353                 FinishedImmediate ();
354         }
355 }
356
357
358 /** @return DCPTime (in seconds) that this sub-job has been running */
359 int
360 Job::elapsed_sub_time () const
361 {
362         if (_sub_start_time == 0) {
363                 return 0;
364         }
365
366         return time (0) - _sub_start_time;
367 }
368
369
370 /** Check to see if this job has been interrupted or paused */
371 void
372 Job::check_for_interruption_or_pause ()
373 {
374         boost::this_thread::interruption_point ();
375
376         boost::mutex::scoped_lock lm (_state_mutex);
377         while (_state == PAUSED_BY_USER || _state == PAUSED_BY_PRIORITY) {
378                 emit (boost::bind (boost::ref (Progress)));
379                 _pause_changed.wait (lm);
380         }
381 }
382
383
384 optional<float>
385 Job::seconds_since_last_progress_update () const
386 {
387         boost::mutex::scoped_lock lm (_progress_mutex);
388         if (!_last_progress_update) {
389                 return {};
390         }
391
392         struct timeval now;
393         gettimeofday (&now, 0);
394
395         return seconds(now) - seconds(*_last_progress_update);
396 }
397
398
399 /** Set the progress of the current part of the job.
400  *  @param p Progress (from 0 to 1)
401  *  @param force Do not ignore this update, even if it hasn't been long since the last one.
402  */
403 void
404 Job::set_progress (float p, bool force)
405 {
406         check_for_interruption_or_pause ();
407
408         if (!force) {
409                 /* Check for excessively frequent progress reporting */
410                 boost::mutex::scoped_lock lm (_progress_mutex);
411                 struct timeval now;
412                 gettimeofday (&now, 0);
413                 if (_last_progress_update && _last_progress_update->tv_sec > 0) {
414                         double const elapsed = seconds(now) - seconds(*_last_progress_update);
415                         if (elapsed < 0.5) {
416                                 return;
417                         }
418                 }
419                 _last_progress_update = now;
420         }
421
422         set_progress_common (p);
423 }
424
425
426 void
427 Job::set_progress_common (optional<float> p)
428 {
429         {
430                 boost::mutex::scoped_lock lm (_progress_mutex);
431                 _progress = p;
432         }
433
434         emit (boost::bind (boost::ref (Progress)));
435 }
436
437
438 /** @return fractional progress of the current sub-job, if known */
439 optional<float>
440 Job::progress () const
441 {
442         boost::mutex::scoped_lock lm (_progress_mutex);
443         return _progress;
444 }
445
446
447 void
448 Job::sub (string n)
449 {
450         {
451                 boost::mutex::scoped_lock lm (_progress_mutex);
452                 LOG_GENERAL ("Sub-job %1 starting", n);
453                 _sub_name = n;
454         }
455
456         set_progress (0, true);
457         _sub_start_time = time (0);
458 }
459
460
461 string
462 Job::error_details () const
463 {
464         boost::mutex::scoped_lock lm (_state_mutex);
465         return _error_details;
466 }
467
468
469 /** @return A summary of any error that the job has generated */
470 string
471 Job::error_summary () const
472 {
473         boost::mutex::scoped_lock lm (_state_mutex);
474         return _error_summary;
475 }
476
477
478 /** Set the current error string.
479  *  @param s New error string.
480  *  @param d New error detail string.
481  */
482 void
483 Job::set_error (string s, string d)
484 {
485         if (_film) {
486                 _film->log()->log (String::compose ("Error in job: %1 (%2)", s, d), LogEntry::TYPE_ERROR);
487         }
488
489         boost::mutex::scoped_lock lm (_state_mutex);
490         _error_summary = s;
491         _error_details = d;
492 }
493
494
495 /** Say that this job's progress will be unknown until further notice */
496 void
497 Job::set_progress_unknown ()
498 {
499         check_for_interruption_or_pause ();
500         set_progress_common (optional<float> ());
501 }
502
503
504 /** @return Human-readable status of this job */
505 string
506 Job::status () const
507 {
508         optional<float> p = progress ();
509         int const t = elapsed_sub_time ();
510         int const r = remaining_time ();
511
512         string s;
513         if (!finished () && p) {
514                 int pc = lrintf (p.get() * 100);
515                 if (pc == 100) {
516                         /* 100% makes it sound like we've finished when we haven't */
517                         pc = 99;
518                 }
519
520                 char buffer[64];
521                 snprintf (buffer, sizeof(buffer), "%d%%", pc);
522                 s += buffer;
523
524                 if (t > 10 && r > 0) {
525                         auto now = boost::posix_time::second_clock::local_time();
526                         auto finish = now + boost::posix_time::seconds(r);
527                         char finish_string[16];
528                         snprintf (finish_string, sizeof(finish_string), "%02d:%02d", int(finish.time_of_day().hours()), int(finish.time_of_day().minutes()));
529                         string day;
530                         if (now.date() != finish.date()) {
531                                 /// TRANSLATORS: the %1 in this string will be filled in with a day of the week
532                                 /// to say what day a job will finish.
533                                 day = String::compose (_(" on %1"), day_of_week_to_string(finish.date().day_of_week()));
534                         }
535                         /// TRANSLATORS: "remaining; finishing at" here follows an amount of time that is remaining
536                         /// on an operation; after it is an estimated wall-clock completion time.
537                         s += String::compose(
538                                 _("; %1 remaining; finishing at %2%3"),
539                                 seconds_to_approximate_hms(r), finish_string, day
540                                 );
541                 }
542         } else if (finished_ok ()) {
543                 s = String::compose (_("OK (ran for %1)"), seconds_to_hms (_ran_for));
544         } else if (finished_in_error ()) {
545                 s = String::compose (_("Error: %1"), error_summary ());
546         } else if (finished_cancelled ()) {
547                 s = _("Cancelled");
548         }
549
550         return s;
551 }
552
553
554 string
555 Job::json_status () const
556 {
557         boost::mutex::scoped_lock lm (_state_mutex);
558
559         switch (_state) {
560         case NEW:
561                 return N_("new");
562         case RUNNING:
563                 return N_("running");
564         case PAUSED_BY_USER:
565         case PAUSED_BY_PRIORITY:
566                 return N_("paused");
567         case FINISHED_OK:
568                 return N_("finished_ok");
569         case FINISHED_ERROR:
570                 return N_("finished_error");
571         case FINISHED_CANCELLED:
572                 return N_("finished_cancelled");
573         }
574
575         return "";
576 }
577
578
579 /** @return An estimate of the remaining time for this sub-job, in seconds */
580 int
581 Job::remaining_time () const
582 {
583         if (progress().get_value_or(0) == 0) {
584                 return elapsed_sub_time ();
585         }
586
587         return elapsed_sub_time() / progress().get() - elapsed_sub_time();
588 }
589
590
591 void
592 Job::cancel ()
593 {
594         if (!_thread.joinable()) {
595                 return;
596         }
597
598         if (paused_by_user() || paused_by_priority()) {
599                 resume ();
600         }
601
602         _thread.interrupt ();
603         _thread.join ();
604 }
605
606
607 /** @return true if the job was paused, false if it was not running */
608 bool
609 Job::pause_by_user ()
610 {
611         bool paused = false;
612         {
613                 boost::mutex::scoped_lock lm (_state_mutex);
614                 /* We can set _state here directly because we have a lock and we aren't
615                    setting the job to FINISHED_*
616                 */
617                 if (_state == RUNNING) {
618                         paused = true;
619                         _state = PAUSED_BY_USER;
620                 }
621         }
622
623         if (paused) {
624                 _pause_changed.notify_all ();
625         }
626
627         return paused;
628 }
629
630
631 void
632 Job::pause_by_priority ()
633 {
634         if (running ()) {
635                 set_state (PAUSED_BY_PRIORITY);
636                 _pause_changed.notify_all ();
637         }
638 }
639
640
641 void
642 Job::resume ()
643 {
644         if (paused_by_user() || paused_by_priority()) {
645                 set_state (RUNNING);
646                 _pause_changed.notify_all ();
647         }
648 }
649
650
651 void
652 Job::when_finished (boost::signals2::connection& connection, function<void()> finished)
653 {
654         boost::mutex::scoped_lock lm (_state_mutex);
655         if (_state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED) {
656                 finished ();
657         } else {
658                 connection = Finished.connect (finished);
659         }
660 }
661
662
663 optional<string>
664 Job::message () const
665 {
666         boost::mutex::scoped_lock lm (_state_mutex);
667         return _message;
668 }
669
670
671 void
672 Job::set_message (string m)
673 {
674         boost::mutex::scoped_lock lm (_state_mutex);
675         _message = m;
676 }