No-op; fix GPL address and use the explicit-program-name version.
[dcpomatic.git] / src / lib / job.cc
1 /*
2     Copyright (C) 2012-2015 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 /** @file  src/job.cc
22  *  @brief A parent class to represent long-running tasks which are run in their own thread.
23  */
24
25 #include "job.h"
26 #include "util.h"
27 #include "cross.h"
28 #include "exceptions.h"
29 #include "film.h"
30 #include "log.h"
31 #include "compose.hpp"
32 #include <dcp/exceptions.h>
33 #include <boost/thread.hpp>
34 #include <boost/filesystem.hpp>
35 #include <iostream>
36
37 #include "i18n.h"
38
39 using std::string;
40 using std::list;
41 using std::cout;
42 using boost::shared_ptr;
43 using boost::optional;
44 using boost::function;
45
46 #define LOG_ERROR_NC(...) _film->log()->log (__VA_ARGS__, LogEntry::TYPE_ERROR);
47 #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
48
49 /** @param film Associated film, or 0 */
50 Job::Job (shared_ptr<const Film> film)
51         : _film (film)
52         , _thread (0)
53         , _state (NEW)
54         , _start_time (0)
55         , _progress (0)
56         , _ran_for (0)
57 {
58
59 }
60
61 Job::~Job ()
62 {
63         if (_thread) {
64                 _thread->interrupt ();
65                 DCPOMATIC_ASSERT (_thread->joinable ());
66                 _thread->join ();
67         }
68
69         delete _thread;
70 }
71
72 /** Start the job in a separate thread, returning immediately */
73 void
74 Job::start ()
75 {
76         set_state (RUNNING);
77         _start_time = time (0);
78         _thread = new boost::thread (boost::bind (&Job::run_wrapper, this));
79 }
80
81 /** A wrapper for the ::run() method to catch exceptions */
82 void
83 Job::run_wrapper ()
84 {
85         try {
86
87                 run ();
88
89         } catch (dcp::FileError& e) {
90
91                 string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf());
92
93                 try {
94                         boost::filesystem::space_info const s = boost::filesystem::space (e.filename());
95                         if (s.available < pow (1024, 3)) {
96                                 m += N_("\n\n");
97                                 m += _("The drive that the film is stored on is low in disc space.  Free some more space and try again.");
98                         }
99                 } catch (...) {
100
101                 }
102
103                 set_error (e.what(), m);
104                 set_progress (1);
105                 set_state (FINISHED_ERROR);
106
107         } catch (OpenFileError& e) {
108
109                 set_error (
110                         String::compose (_("Could not open %1"), e.file().string()),
111                         String::compose (
112                                 _("DCP-o-matic could not open the file %1.  Perhaps it does not exist or is in an unexpected format."),
113                                 boost::filesystem::absolute (e.file()).string()
114                                 )
115                         );
116
117                 set_progress (1);
118                 set_state (FINISHED_ERROR);
119
120         } catch (boost::filesystem::filesystem_error& e) {
121
122                 if (e.code() == boost::system::errc::no_such_file_or_directory) {
123                         set_error (
124                                 String::compose (_("Could not open %1"), e.path1().string ()),
125                                 String::compose (
126                                         _("DCP-o-matic could not open the file %1.  Perhaps it does not exist or is in an unexpected format."),
127                                         boost::filesystem::absolute (e.path1()).string()
128                                         )
129                                 );
130                 } else {
131                         set_error (
132                                 e.what (),
133                                 string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
134                                 );
135                 }
136
137                 set_progress (1);
138                 set_state (FINISHED_ERROR);
139
140         } catch (boost::thread_interrupted &) {
141
142                 set_state (FINISHED_CANCELLED);
143
144         } catch (std::bad_alloc& e) {
145
146                 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."));
147                 set_progress (1);
148                 set_state (FINISHED_ERROR);
149
150         } catch (std::exception& e) {
151
152                 set_error (
153                         e.what (),
154                         string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
155                         );
156
157                 set_progress (1);
158                 set_state (FINISHED_ERROR);
159
160         } catch (...) {
161
162                 set_error (
163                         _("Unknown error"),
164                         string (_("It is not known what caused this error.")) + "  " + REPORT_PROBLEM
165                         );
166
167                 set_progress (1);
168                 set_state (FINISHED_ERROR);
169         }
170 }
171
172 /** @return true if this job is new (ie has not started running) */
173 bool
174 Job::is_new () const
175 {
176         boost::mutex::scoped_lock lm (_state_mutex);
177         return _state == NEW;
178 }
179
180 /** @return true if the job is running */
181 bool
182 Job::running () const
183 {
184         boost::mutex::scoped_lock lm (_state_mutex);
185         return _state == RUNNING;
186 }
187
188 /** @return true if the job has finished (either successfully or unsuccessfully) */
189 bool
190 Job::finished () const
191 {
192         boost::mutex::scoped_lock lm (_state_mutex);
193         return _state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED;
194 }
195
196 /** @return true if the job has finished successfully */
197 bool
198 Job::finished_ok () const
199 {
200         boost::mutex::scoped_lock lm (_state_mutex);
201         return _state == FINISHED_OK;
202 }
203
204 /** @return true if the job has finished unsuccessfully */
205 bool
206 Job::finished_in_error () const
207 {
208         boost::mutex::scoped_lock lm (_state_mutex);
209         return _state == FINISHED_ERROR;
210 }
211
212 bool
213 Job::finished_cancelled () const
214 {
215         boost::mutex::scoped_lock lm (_state_mutex);
216         return _state == FINISHED_CANCELLED;
217 }
218
219 bool
220 Job::paused () const
221 {
222         boost::mutex::scoped_lock lm (_state_mutex);
223         return _state == PAUSED;
224 }
225
226 /** Set the state of this job.
227  *  @param s New state.
228  */
229 void
230 Job::set_state (State s)
231 {
232         bool finished = false;
233
234         {
235                 boost::mutex::scoped_lock lm (_state_mutex);
236                 _state = s;
237
238                 if (_state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED) {
239                         _ran_for = elapsed_time ();
240                         finished = true;
241                         _sub_name.clear ();
242                 }
243         }
244
245         if (finished) {
246                 emit (boost::bind (boost::ref (Finished)));
247         }
248 }
249
250 /** @return DCPTime (in seconds) that this sub-job has been running */
251 int
252 Job::elapsed_time () const
253 {
254         if (_start_time == 0) {
255                 return 0;
256         }
257
258         return time (0) - _start_time;
259 }
260
261 /** Set the progress of the current part of the job.
262  *  @param p Progress (from 0 to 1)
263  */
264 void
265 Job::set_progress (float p, bool force)
266 {
267         if (!force && fabs (p - progress().get_value_or(0)) < 0.01) {
268                 /* Calm excessive progress reporting */
269                 return;
270         }
271
272         set_progress_common (p);
273 }
274
275 void
276 Job::set_progress_common (optional<float> p)
277 {
278         boost::mutex::scoped_lock lm (_progress_mutex);
279         _progress = p;
280         boost::this_thread::interruption_point ();
281
282         boost::mutex::scoped_lock lm2 (_state_mutex);
283         while (_state == PAUSED) {
284                 _pause_changed.wait (lm2);
285         }
286
287         lm.unlock ();
288         lm2.unlock ();
289
290         emit (boost::bind (boost::ref (Progress)));
291 }
292
293 /** @return fractional progress of the current sub-job, if known */
294 optional<float>
295 Job::progress () const
296 {
297         boost::mutex::scoped_lock lm (_progress_mutex);
298         return _progress;
299 }
300
301 void
302 Job::sub (string n)
303 {
304         {
305                 boost::mutex::scoped_lock lm (_progress_mutex);
306                 LOG_GENERAL ("Sub-job %1 starting", n);
307                 _sub_name = n;
308         }
309
310         set_progress (0, true);
311 }
312
313 string
314 Job::error_details () const
315 {
316         boost::mutex::scoped_lock lm (_state_mutex);
317         return _error_details;
318 }
319
320 /** @return A summary of any error that the job has generated */
321 string
322 Job::error_summary () const
323 {
324         boost::mutex::scoped_lock lm (_state_mutex);
325         return _error_summary;
326 }
327
328 /** Set the current error string.
329  *  @param e New error string.
330  */
331 void
332 Job::set_error (string s, string d)
333 {
334         if (_film) {
335                 LOG_ERROR_NC (s);
336                 LOG_ERROR_NC (d);
337                 _film->log()->log (String::compose ("Error in job: %1 (%2)", s, d), LogEntry::TYPE_ERROR);
338         }
339
340         boost::mutex::scoped_lock lm (_state_mutex);
341         _error_summary = s;
342         _error_details = d;
343 }
344
345 /** Say that this job's progress will be unknown until further notice */
346 void
347 Job::set_progress_unknown ()
348 {
349         set_progress_common (optional<float> ());
350 }
351
352 /** @return Human-readable status of this job */
353 string
354 Job::status () const
355 {
356         optional<float> p = progress ();
357         int const t = elapsed_time ();
358         int const r = remaining_time ();
359
360         SafeStringStream s;
361         if (!finished () && p) {
362                 int pc = lrintf (p.get() * 100);
363                 if (pc == 100) {
364                         /* 100% makes it sound like we've finished when we haven't */
365                         pc = 99;
366                 }
367
368                 s << pc << N_("%");
369
370                 if (t > 10 && r > 0) {
371                         /// TRANSLATORS: remaining here follows an amount of time that is remaining
372                         /// on an operation.
373                         s << "; " << seconds_to_approximate_hms (r) << " " << _("remaining");
374                 }
375         } else if (finished_ok ()) {
376                 s << String::compose (_("OK (ran for %1)"), seconds_to_hms (_ran_for));
377         } else if (finished_in_error ()) {
378                 s << String::compose (_("Error: %1"), error_summary ());
379         } else if (finished_cancelled ()) {
380                 s << _("Cancelled");
381         }
382
383         return s.str ();
384 }
385
386 string
387 Job::json_status () const
388 {
389         boost::mutex::scoped_lock lm (_state_mutex);
390
391         switch (_state) {
392         case NEW:
393                 return N_("new");
394         case RUNNING:
395                 return N_("running");
396         case PAUSED:
397                 return N_("paused");
398         case FINISHED_OK:
399                 return N_("finished_ok");
400         case FINISHED_ERROR:
401                 return N_("finished_error");
402         case FINISHED_CANCELLED:
403                 return N_("finished_cancelled");
404         }
405
406         return "";
407 }
408
409 /** @return An estimate of the remaining time for this sub-job, in seconds */
410 int
411 Job::remaining_time () const
412 {
413         if (progress().get_value_or(0) == 0) {
414                 return elapsed_time ();
415         }
416
417         return elapsed_time() / progress().get() - elapsed_time();
418 }
419
420 void
421 Job::cancel ()
422 {
423         if (!_thread) {
424                 return;
425         }
426
427         if (paused ()) {
428                 resume ();
429         }
430
431         _thread->interrupt ();
432         DCPOMATIC_ASSERT (_thread->joinable ());
433         _thread->join ();
434         delete _thread;
435         _thread = 0;
436 }
437
438 void
439 Job::pause ()
440 {
441         if (running ()) {
442                 set_state (PAUSED);
443                 _pause_changed.notify_all ();
444         }
445 }
446
447 void
448 Job::resume ()
449 {
450         if (paused ()) {
451                 set_state (RUNNING);
452                 _pause_changed.notify_all ();
453         }
454 }
455
456 void
457 Job::when_finished (boost::signals2::connection& connection, function<void()> finished)
458 {
459         boost::mutex::scoped_lock lm (_state_mutex);
460         if (_state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED) {
461                 finished ();
462         } else {
463                 connection = Finished.connect (finished);
464         }
465 }