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