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