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