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