1 #include "common/common_pch.h"
2 
3 #include <QDebug>
4 #include <QDesktopServices>
5 #include <QFile>
6 #include <QFileInfo>
7 #include <QSettings>
8 #include <QUrl>
9 
10 #include "common/list_utils.h"
11 #include "common/logger.h"
12 #include "common/qt.h"
13 #include "mkvtoolnix-gui/app.h"
14 #include "mkvtoolnix-gui/info/job_settings.h"
15 #include "mkvtoolnix-gui/jobs/info_job.h"
16 #include "mkvtoolnix-gui/jobs/job.h"
17 #include "mkvtoolnix-gui/jobs/job_p.h"
18 #include "mkvtoolnix-gui/jobs/mux_job.h"
19 #include "mkvtoolnix-gui/merge/mux_config.h"
20 #include "mkvtoolnix-gui/util/config_file.h"
21 #include "mkvtoolnix-gui/util/file.h"
22 #include "mkvtoolnix-gui/util/settings.h"
23 
24 namespace mtx::gui::Jobs {
25 
26 static uint64_t s_next_id = 0;
27 
JobPrivate(Job::Status pStatus)28 JobPrivate::JobPrivate(Job::Status pStatus)
29   : id{s_next_id++}
30   , status{pStatus}
31 {
32 }
33 
Job(Status status)34 Job::Job(Status status)
35   : p_ptr{new JobPrivate{status}}
36 {
37   setupJobConnections();
38 }
39 
Job(JobPrivate & p)40 Job::Job(JobPrivate &p)
41   : p_ptr{&p}
42 {
43   setupJobConnections();
44 }
45 
~Job()46 Job::~Job() {
47 }
48 
49 void
setupJobConnections()50 Job::setupJobConnections() {
51   connect(this, &Job::lineRead, this, &Job::addLineToInternalLogs);
52 }
53 
54 uint64_t
id() const55 Job::id()
56   const {
57   return p_func()->id;
58 }
59 
60 QUuid
uuid() const61 Job::uuid()
62   const {
63   return p_func()->uuid;
64 }
65 
66 Job::Status
status() const67 Job::status()
68   const {
69   return p_func()->status;
70 }
71 
72 QString
description() const73 Job::description()
74   const {
75   return p_func()->description;
76 }
77 
78 unsigned int
progress() const79 Job::progress()
80   const {
81   return p_func()->progress;
82 }
83 
84 QStringList const &
output() const85 Job::output()
86   const {
87   return p_func()->output;
88 }
89 
90 QStringList const &
warnings() const91 Job::warnings()
92   const {
93   return p_func()->warnings;
94 }
95 
96 QStringList const &
errors() const97 Job::errors()
98   const {
99   return p_func()->errors;
100 }
101 
102 QStringList const &
fullOutput() const103 Job::fullOutput()
104   const {
105   return p_func()->fullOutput;
106 }
107 
108 QDateTime
dateAdded() const109 Job::dateAdded()
110   const {
111   return p_func()->dateAdded;
112 }
113 
114 QDateTime
dateStarted() const115 Job::dateStarted()
116   const {
117   return p_func()->dateStarted;
118 }
119 
120 QDateTime
dateFinished() const121 Job::dateFinished()
122   const {
123   return p_func()->dateFinished;
124 }
125 
126 bool
isModified() const127 Job::isModified()
128   const {
129   return p_func()->modified;
130 }
131 
132 void
setDescription(QString const & pDescription)133 Job::setDescription(QString const &pDescription) {
134   auto p         = p_func();
135   p->description = pDescription;
136   p->modified    = true;
137 }
138 
139 void
setDateAdded(QDateTime const & pDateAdded)140 Job::setDateAdded(QDateTime const &pDateAdded) {
141   auto p       = p_func();
142   p->dateAdded = pDateAdded;
143   p->modified  = true;
144 }
145 
146 void
setDateFinished(QDateTime const & pDateFinished)147 Job::setDateFinished(QDateTime const &pDateFinished) {
148   auto p          = p_func();
149   p->dateFinished = pDateFinished;
150   p->modified     = true;
151 }
152 
153 void
setQuitAfterFinished(bool pQuitAfterFinished)154 Job::setQuitAfterFinished(bool pQuitAfterFinished) {
155   auto p               = p_func();
156   p->quitAfterFinished = pQuitAfterFinished;
157   p->modified          = true;
158 }
159 
160 void
action(std::function<void ()> code)161 Job::action(std::function<void()> code) {
162   auto p = p_func();
163 
164   QMutexLocker locked{&p->mutex};
165 
166   code();
167 }
168 
169 void
setStatus(Status status)170 Job::setStatus(Status status) {
171   auto p = p_func();
172 
173   QMutexLocker locked{&p->mutex};
174 
175   if (status == p->status)
176     return;
177 
178   auto oldStatus = p->status;
179   p->status       = status;
180   p->modified     = true;
181 
182   if (Running == status) {
183     p->dateStarted = QDateTime::currentDateTime();
184     p->fullOutput.clear();
185     p->output.clear();
186     p->warnings.clear();
187     p->errors.clear();
188     p->warningsAcknowledged = 0;
189     p->errorsAcknowledged   = 0;
190 
191   } else if ((DoneOk == status) || (DoneWarnings == status) || (Failed == status) || (Aborted == status))
192     p->dateFinished = QDateTime::currentDateTime();
193 
194   if (oldStatus == Running)
195     runProgramsAfterCompletion();
196 
197   qDebug() << "setStatus emitting statusChanged(" << oldStatus << status << ")";
198   Q_EMIT statusChanged(p->id, oldStatus, status);
199 }
200 
201 bool
isToBeProcessed() const202 Job::isToBeProcessed()
203   const {
204   return mtx::included_in(p_func()->status, Running, PendingAuto);
205 }
206 
207 void
setProgress(unsigned int progress)208 Job::setProgress(unsigned int progress) {
209   auto p = p_func();
210 
211   QMutexLocker locked{&p->mutex};
212 
213   if (progress == p->progress)
214     return;
215 
216   p->progress = progress;
217   p->modified = true;
218   Q_EMIT progressChanged(p->id, p->progress);
219 }
220 
221 void
setPendingAuto()222 Job::setPendingAuto() {
223   auto p = p_func();
224 
225   QMutexLocker locked{&p->mutex};
226 
227   if (!mtx::included_in(p->status, PendingAuto, Running)) {
228     setProgress(0);
229     setStatus(PendingAuto);
230   }
231 }
232 
233 void
setPendingManual()234 Job::setPendingManual() {
235   auto p = p_func();
236 
237   QMutexLocker locked{&p->mutex};
238 
239   if ((PendingManual != p->status) && (Running != p->status)) {
240     setProgress(0);
241     setStatus(PendingManual);
242   }
243 }
244 
245 void
addLineToInternalLogs(QString const & line,LineType type)246 Job::addLineToInternalLogs(QString const &line,
247                            LineType type) {
248   auto p        = p_func();
249   auto &storage = InfoLine    == type ? p->output
250                 : WarningLine == type ? p->warnings
251                 :                       p->errors;
252 
253   auto prefix   = InfoLine    == type ? Q("")
254                 : WarningLine == type ? Q("%1 ").arg(QY("Warning:"))
255                 :                       Q("%1 ").arg(QY("Error:"));
256 
257   p->fullOutput << Q("%1%2").arg(prefix).arg(line);
258   storage       << line;
259 
260   p->modified    = true;
261 
262   if ((WarningLine == type) || (ErrorLine == type))
263     updateUnacknowledgedWarningsAndErrors();
264 }
265 
266 QString
displayableStatus(Status status)267 Job::displayableStatus(Status status) {
268   return PendingManual == status ? QY("Pending manual start")
269        : PendingAuto   == status ? QY("Pending automatic start")
270        : Running       == status ? QY("Running")
271        : DoneOk        == status ? QY("Completed OK")
272        : DoneWarnings  == status ? QY("Completed with warnings")
273        : Failed        == status ? QY("Failed")
274        : Aborted       == status ? QY("Aborted by user")
275        : Disabled      == status ? QY("Disabled")
276        :                           QY("Unknown");
277 }
278 
279 QString
outputFolder() const280 Job::outputFolder()
281   const {
282   auto destination = destinationFileName();
283   return destination.isEmpty() ? QString{} : QFileInfo{destination}.dir().path();
284 }
285 
286 void
openOutputFolder() const287 Job::openOutputFolder()
288   const {
289   auto folder = outputFolder();
290 
291   if (!folder.isEmpty())
292     QDesktopServices::openUrl(Util::pathToFileUrl(folder));
293 }
294 
295 QString
queueLocation()296 Job::queueLocation() {
297   return Q("%1/%2").arg(Util::Settings::iniFileLocation()).arg("jobQueue");
298 }
299 
300 QString
queueFileName() const301 Job::queueFileName()
302   const {
303   return Q("%1/%2.mtxcfg").arg(queueLocation()).arg(p_func()->uuid.toString());
304 }
305 
306 void
removeQueueFile() const307 Job::removeQueueFile()
308   const {
309   QFile{queueFileName()}.remove();
310 }
311 
312 void
saveQueueFile()313 Job::saveQueueFile() {
314   auto p = p_func();
315 
316   if (p->uuid.isNull())
317     p->uuid = QUuid::createUuid();
318 
319   auto fileName = queueFileName();
320   if (!isModified() && QFileInfo{ fileName }.exists())
321     return;
322 
323   auto settings = Util::ConfigFile::create(fileName);
324   saveJob(*settings);
325   settings->save();
326 }
327 
328 void
saveJob(Util::ConfigFile & settings)329 Job::saveJob(Util::ConfigFile &settings) {
330   auto p             = p_func();
331   auto resetCounters = Util::Settings::get().m_resetJobWarningErrorCountersOnExit;
332 
333   settings.setValue("uuid",                 p->uuid);
334   settings.setValue("status",               static_cast<unsigned int>(p->status));
335   settings.setValue("description",          p->description);
336   settings.setValue("output",               p->output);
337   settings.setValue("warnings",             p->warnings);
338   settings.setValue("errors",               p->errors);
339   settings.setValue("fullOutput",           p->fullOutput);
340   settings.setValue("progress",             p->progress);
341   settings.setValue("exitCode",             p->exitCode);
342   settings.setValue("warningsAcknowledged", resetCounters ? p->warnings.count() : p->warningsAcknowledged);
343   settings.setValue("errorsAcknowledged",   resetCounters ? p->errors.count()   : p->errorsAcknowledged);
344   settings.setValue("dateAdded",            p->dateAdded);
345   settings.setValue("dateStarted",          p->dateStarted);
346   settings.setValue("dateFinished",         p->dateFinished);
347 
348   saveJobInternal(settings);
349 
350   p->modified = false;
351 }
352 
353 void
loadJobBasis(Util::ConfigFile & settings)354 Job::loadJobBasis(Util::ConfigFile &settings) {
355   auto p                  = p_func();
356   p->modified             = false;
357   p->uuid                 = settings.value("uuid").toUuid();
358   p->status               = static_cast<Status>(settings.value("status", static_cast<unsigned int>(PendingManual)).toUInt());
359   p->description          = settings.value("description").toString();
360   p->output               = settings.value("output").toStringList();
361   p->warnings             = settings.value("warnings").toStringList();
362   p->errors               = settings.value("errors").toStringList();
363   p->fullOutput           = settings.value("fullOutput").toStringList();
364   p->progress             = settings.value("progress").toUInt();
365   p->exitCode             = settings.value("exitCode").toUInt();
366   p->warningsAcknowledged = settings.value("warningsAcknowledged", 0).toUInt();
367   p->errorsAcknowledged   = settings.value("errorsAcknowledged",   0).toUInt();
368   p->dateAdded            = settings.value("dateAdded").toDateTime();
369   p->dateStarted          = settings.value("dateStarted").toDateTime();
370   p->dateFinished         = settings.value("dateFinished").toDateTime();
371 
372   if (p->uuid.isNull())
373     p->uuid = QUuid::createUuid();
374 
375   if (Running == p->status)
376     p->status = Aborted;
377 
378   if (Util::Settings::get().m_resetJobWarningErrorCountersOnExit) {
379     p->warningsAcknowledged = p->warnings.count();
380     p->errorsAcknowledged   = p->errors.count();
381   }
382 }
383 
384 JobPtr
loadJob(QString const & fileName)385 Job::loadJob(QString const &fileName) {
386   if (!QFileInfo{fileName}.exists())
387     return {};
388 
389   auto settings = Util::ConfigFile::open(fileName);
390   if (!settings)
391     throw Merge::InvalidSettingsX{};
392 
393   return loadJob(*settings);
394 }
395 
396 JobPtr
loadJob(Util::ConfigFile & settings)397 Job::loadJob(Util::ConfigFile &settings) {
398   auto jobType = settings.value("jobType").toString();
399 
400   if (jobType.isEmpty() && settings.childKeys().contains("muxConfig"))
401     // Older versions didn't write a jobType attribute and only
402     // supported MuxJobs.
403     jobType = "MuxJob";
404 
405   if (jobType == "MuxJob")
406     return MuxJob::loadMuxJob(settings);
407 
408   if (jobType == "InfoJob")
409     return InfoJob::loadInfoJob(settings);
410 
411   log_it(fmt::format("MTX Job::loadJob: Unknown job type encountered ({0}) in {1}", jobType, settings.fileName()));
412   throw Merge::InvalidSettingsX{};
413 }
414 
415 void
acknowledgeWarnings()416 Job::acknowledgeWarnings() {
417   auto p = p_func();
418 
419   if (p->warnings.count() == p->warningsAcknowledged)
420     return;
421 
422   p->warningsAcknowledged = p->warnings.count();
423   p->modified             = true;
424   updateUnacknowledgedWarningsAndErrors();
425 }
426 
427 void
acknowledgeErrors()428 Job::acknowledgeErrors() {
429   auto p = p_func();
430 
431   if (p->errors.count() == p->errorsAcknowledged)
432     return;
433 
434   p->errorsAcknowledged = p->errors.count();
435   p->modified           = true;
436   updateUnacknowledgedWarningsAndErrors();
437 }
438 
439 void
updateUnacknowledgedWarningsAndErrors()440 Job::updateUnacknowledgedWarningsAndErrors() {
441   Q_EMIT numUnacknowledgedWarningsOrErrorsChanged(p_func()->id, numUnacknowledgedWarnings(), numUnacknowledgedErrors());
442 }
443 
444 int
numUnacknowledgedWarnings() const445 Job::numUnacknowledgedWarnings()
446   const {
447   auto p = p_func();
448 
449   return p->warnings.count() - p->warningsAcknowledged;
450 }
451 
452 int
numUnacknowledgedErrors() const453 Job::numUnacknowledgedErrors()
454   const {
455   auto p = p_func();
456 
457   return p->errors.count() - p->errorsAcknowledged;
458 }
459 
460 void
runProgramsAfterCompletion()461 Job::runProgramsAfterCompletion() {
462   auto p = p_func();
463 
464   if (p->status == Aborted) {
465     maybeRemoveOutputFile();
466     return;
467   }
468 
469   if (!mtx::included_in(p->status, DoneOk, DoneWarnings, Failed))
470     return;
471 
472   auto event = p->status == Failed       ? Util::Settings::RunAfterJobCompletesWithErrors
473              : p->status == DoneWarnings ? Util::Settings::RunAfterJobCompletesWithWarnings
474              :                             Util::Settings::RunAfterJobCompletesSuccessfully;
475 
476   App::programRunner().run(event, [this](ProgramRunner::VariableMap &variables) {
477     runProgramSetupVariables(variables);
478   });
479 
480   App::programRunner().executeActionsAfterJobFinishes(*this);
481 
482   maybeRemoveOutputFile();
483 }
484 
485 void
runProgramSetupVariables(ProgramRunner::VariableMap & variables) const486 Job::runProgramSetupVariables(ProgramRunner::VariableMap &variables)
487   const{
488   auto p = p_func();
489 
490   variables[Q("JOB_START_TIME")]  << p->dateStarted.toString(Qt::ISODate);
491   variables[Q("JOB_END_TIME")]    << p->dateFinished.toString(Qt::ISODate);
492   variables[Q("JOB_DESCRIPTION")] << p->description;
493   variables[Q("JOB_EXIT_CODE")]   << QString::number(p->exitCode);
494 }
495 
496 void
maybeRemoveOutputFile()497 Job::maybeRemoveOutputFile() {
498   auto p = p_func();
499 
500   if (   !Util::Settings::get().m_removeOutputFileOnJobFailure
501       || !mtx::included_in(p->status, Aborted, Failed))
502     return;
503 
504   auto fileName = destinationFileName();
505 
506   qDebug() << "maybeRemoveOutputFile:" << fileName;
507 
508   if (!fileName.isEmpty())
509     QFile::remove(fileName);
510 }
511 
512 }
513