MythTV master
jobqueue.cpp
Go to the documentation of this file.
1
2#include <unistd.h>
3#include <sys/types.h>
4#include <sys/stat.h>
5#include <iostream>
6#include <cstdlib>
7#include <thread>
8#include <fcntl.h>
9#include <pthread.h>
10
11#include <QtGlobal> // for Q_OS_XXX
12#if defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD)
13#define PTHREAD_NULL nullptr
14#else
15#define PTHREAD_NULL 0
16#endif
17#include <QDateTime>
18#include <QFileInfo>
19#include <QEvent>
20#include <QCoreApplication>
21#include <QTimeZone>
22
23#include "libmythbase/compat.h"
25#include "libmythbase/mthread.h"
27#include "libmythbase/mythconfig.h"
30#include "libmythbase/mythdb.h"
35
36#include "jobqueue.h"
37#include "previewgenerator.h"
38#include "programinfo.h"
39#include "recordinginfo.h"
40#include "recordingprofile.h"
41
42#define LOC QString("JobQueue: ")
43
44// Consider anything less than 4 hours as a "recent" job.
45static constexpr int64_t kRecentInterval {4LL * 60 * 60};
46
47JobQueue::JobQueue(bool master) :
48 m_hostname(gCoreContext->GetHostName()),
49 m_runningJobsLock(new QRecursiveMutex()),
50 m_isMaster(master),
51 m_queueThread(new MThread("JobQueue", this))
52{
53 m_jobQueueCPU = gCoreContext->GetNumSetting("JobQueueCPU", 0);
54
55#if !CONFIG_VALGRIND
56 QMutexLocker locker(&m_queueThreadCondLock);
57 //NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
58 m_processQueue = true;
60#else
61 LOG(VB_GENERAL, LOG_ERR, LOC +
62 "The JobQueue has been disabled because "
63 "you compiled with the --enable-valgrind option.");
64#endif // CONFIG_VALGRIND
65
67}
68
70{
72 m_processQueue = false;
73 m_queueThreadCond.wakeAll();
74 m_queueThreadCondLock.unlock();
75
77 delete m_queueThread;
78 m_queueThread = nullptr;
79
81
82 delete m_runningJobsLock;
83}
84
85void JobQueue::customEvent(QEvent *e)
86{
87 if (e->type() == MythEvent::kMythEventMessage)
88 {
89 auto *me = dynamic_cast<MythEvent *>(e);
90 if (me == nullptr)
91 return;
92 QString message = me->Message();
93
94 if (message.startsWith("LOCAL_JOB"))
95 {
96 // LOCAL_JOB action ID jobID
97 // LOCAL_JOB action type chanid recstartts hostname
98 QString msg;
99 message = message.simplified();
100 QStringList tokens = message.split(" ", Qt::SkipEmptyParts);
101 const QString& action = tokens[1];
102 int jobID = -1;
103
104 if (tokens[2] == "ID")
105 jobID = tokens[3].toInt();
106 else
107 {
108 jobID = GetJobID(
109 tokens[2].toInt(),
110 tokens[3].toUInt(),
111 MythDate::fromString(tokens[4]));
112 }
113
114 m_runningJobsLock->lock();
115 if (!m_runningJobs.contains(jobID))
116 {
117 msg = QString("Unable to determine jobID for message: "
118 "%1. Program will not be flagged.")
119 .arg(message);
120 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
121 m_runningJobsLock->unlock();
122 return;
123 }
124 m_runningJobsLock->unlock();
125
126 msg = QString("Received message '%1'").arg(message);
127 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
128
129 if ((action == "STOP") ||
130 (action == "PAUSE") ||
131 (action == "RESTART") ||
132 (action == "RESUME" ))
133 {
134 m_runningJobsLock->lock();
135
136 if (action == "STOP")
138 else if (action == "PAUSE")
140 else if (action == "RESUME")
142 else if (action == "RESTART")
144
145 m_runningJobsLock->unlock();
146 }
147 }
148 }
149}
150
152{
154 m_queueThreadCond.wakeAll();
155 m_queueThreadCondLock.unlock();
156
157 RecoverQueue();
158
160 m_queueThreadCond.wait(&m_queueThreadCondLock, 10 * 1000UL);
161 m_queueThreadCondLock.unlock();
162
163 ProcessQueue();
164}
165
167{
168 LOG(VB_JOBQUEUE, LOG_INFO, LOC + "ProcessQueue() started");
169
170 QString logInfo;
171 //int flags;
172 QString hostname;
173
174 QMap<int, int> jobStatus;
175 QString message;
176 QMap<int, JobQueueEntry> jobs;
177 bool atMax = false;
178 QMap<int, RunningJobInfo>::Iterator rjiter;
179
180 QMutexLocker locker(&m_queueThreadCondLock);
181 while (m_processQueue)
182 {
183 locker.unlock();
184
185 bool startedJobAlready = false;
186 auto sleepTime = gCoreContext->GetDurSetting<std::chrono::seconds>("JobQueueCheckFrequency", 30s);
187 int maxJobs = gCoreContext->GetNumSetting("JobQueueMaxSimultaneousJobs", 3);
188 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
189 QString("Currently set to run up to %1 job(s) max.")
190 .arg(maxJobs));
191
192 jobStatus.clear();
193
194 m_runningJobsLock->lock();
195 for (rjiter = m_runningJobs.begin(); rjiter != m_runningJobs.end();
196 ++rjiter)
197 {
198 if ((*rjiter).pginfo)
199 (*rjiter).pginfo->UpdateInUseMark();
200 }
201 m_runningJobsLock->unlock();
202
203 m_jobsRunning = 0;
204 GetJobsInQueue(jobs);
205
206 if (!jobs.empty())
207 {
208 bool inTimeWindow = InJobRunWindow();
209 for (const auto & job : std::as_const(jobs))
210 {
211 int status = job.status;
212 hostname = job.hostname;
213
214 if (((status == JOB_RUNNING) ||
215 (status == JOB_STARTING) ||
216 (status == JOB_PAUSED)) &&
217 (hostname == m_hostname))
219 }
220
221 message = QString("Currently Running %1 jobs.")
222 .arg(m_jobsRunning);
223 if (!inTimeWindow)
224 {
225 message += QString(" Jobs in Queue, but we are outside of the "
226 "Job Queue time window, no new jobs can be "
227 "started.");
228 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
229 }
230 else if (m_jobsRunning >= maxJobs)
231 {
232 message += " (At Maximum, no new jobs can be started until "
233 "a running job completes)";
234
235 if (!atMax)
236 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
237
238 atMax = true;
239 }
240 else
241 {
242 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
243 atMax = false;
244 }
245
246
247 for ( int x = 0;
248 (x < jobs.size()) && (m_jobsRunning < maxJobs); x++)
249 {
250 int jobID = jobs[x].id;
251 int cmds = jobs[x].cmds;
252 //flags = jobs[x].flags;
253 int status = jobs[x].status;
254 hostname = jobs[x].hostname;
255
256 if (!jobs[x].chanid)
257 logInfo = QString("jobID #%1").arg(jobID);
258 else
259 logInfo = QString("chanid %1 @ %2").arg(jobs[x].chanid)
260 .arg(jobs[x].startts);
261
262 // Should we even be looking at this job?
263 if ((inTimeWindow) &&
264 (!hostname.isEmpty()) &&
265 (hostname != m_hostname))
266 {
267 // Setting the status here will prevent us from processing
268 // any other jobs for this recording until this one is
269 // completed on the remote host.
270 jobStatus[jobID] = status;
271
272 message = QString("Skipping '%1' job for %2, "
273 "should run on '%3' instead")
274 .arg(JobText(jobs[x].type), logInfo,
275 hostname);
276 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
277 continue;
278 }
279
280 // Check to see if there was a previous job that is not done
281 if (inTimeWindow)
282 {
283 int otherJobID = GetRunningJobID(jobs[x].chanid,
284 jobs[x].recstartts);
285 if (otherJobID && (jobStatus.contains(otherJobID)) &&
286 (!(jobStatus[otherJobID] & JOB_DONE)))
287 {
288 message =
289 QString("Skipping '%1' job for %2, "
290 "Job ID %3 is already running for "
291 "this recording with a status of '%4'")
292 .arg(JobText(jobs[x].type), logInfo,
293 QString::number(otherJobID),
294 StatusText(jobStatus[otherJobID]));
295 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
296 continue;
297 }
298 }
299
300 jobStatus[jobID] = status;
301
302 // Are we allowed to run this job?
303 if ((inTimeWindow) && (!AllowedToRun(jobs[x])))
304 {
305 message = QString("Skipping '%1' job for %2, "
306 "not allowed to run on this backend.")
307 .arg(JobText(jobs[x].type), logInfo);
308 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
309 continue;
310 }
311
312 // Is this job scheduled for the future
313 if (jobs[x].schedruntime > MythDate::current())
314 {
315 message = QString("Skipping '%1' job for %2, this job is "
316 "not scheduled to run until %3.")
317 .arg(JobText(jobs[x].type), logInfo,
318 jobs[x].schedruntime
320 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
321 continue;
322 }
323
324 if (cmds & JOB_STOP)
325 {
326 // if we're trying to stop a job and it's not queued
327 // then lets send a STOP command
328 if (status != JOB_QUEUED) {
329 message = QString("Stopping '%1' job for %2")
330 .arg(JobText(jobs[x].type), logInfo);
331 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
332
333 m_runningJobsLock->lock();
334 if (m_runningJobs.contains(jobID))
336 m_runningJobsLock->unlock();
337
338 // ChangeJobCmds(m_db, jobID, JOB_RUN);
339 continue;
340
341 // if we're trying to stop a job and it's still queued
342 // then let's just change the status to cancelled so
343 // we don't try to run it from the queue
344 }
345
346 message = QString("Cancelling '%1' job for %2")
347 .arg(JobText(jobs[x].type), logInfo);
348 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
349
350 // at the bottom of this loop we requeue any jobs that
351 // are not currently queued and also not associated
352 // with a hostname so we must claim this job before we
353 // can cancel it
355 {
356 message = QString("Unable to claim '%1' job for %2")
357 .arg(JobText(jobs[x].type), logInfo);
358 LOG(VB_JOBQUEUE, LOG_ERR, LOC + message);
359 continue;
360 }
361
362 ChangeJobStatus(jobID, JOB_CANCELLED, "");
364 continue;
365 }
366
367 if ((cmds & JOB_PAUSE) && (status != JOB_QUEUED))
368 {
369 message = QString("Pausing '%1' job for %2")
370 .arg(JobText(jobs[x].type), logInfo);
371 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
372
373 m_runningJobsLock->lock();
374 if (m_runningJobs.contains(jobID))
376 m_runningJobsLock->unlock();
377
379 continue;
380 }
381
382 if ((cmds & JOB_RESTART) && (status != JOB_QUEUED))
383 {
384 message = QString("Restart '%1' job for %2")
385 .arg(JobText(jobs[x].type), logInfo);
386 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
387
388 m_runningJobsLock->lock();
389 if (m_runningJobs.contains(jobID))
391 m_runningJobsLock->unlock();
392
394 continue;
395 }
396
397 if (status != JOB_QUEUED)
398 {
399
400 if (hostname.isEmpty())
401 {
402 message = QString("Resetting '%1' job for %2 to %3 "
403 "status, because no hostname is set.")
404 .arg(JobText(jobs[x].type),
405 logInfo,
406 StatusText(JOB_QUEUED));
407 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
408
409 ChangeJobStatus(jobID, JOB_QUEUED, "");
411 }
412 else if (inTimeWindow)
413 {
414 message = QString("Skipping '%1' job for %2, "
415 "current job status is '%3'")
416 .arg(JobText(jobs[x].type),
417 logInfo,
418 StatusText(status));
419 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
420 }
421 continue;
422 }
423
424 // never start or claim more than one job in a single run
425 if (startedJobAlready)
426 continue;
427
428 if ((inTimeWindow) &&
429 (hostname.isEmpty()) &&
431 {
432 message = QString("Unable to claim '%1' job for %2")
433 .arg(JobText(jobs[x].type), logInfo);
434 LOG(VB_JOBQUEUE, LOG_ERR, LOC + message);
435 continue;
436 }
437
438 if (!inTimeWindow)
439 {
440 message = QString("Skipping '%1' job for %2, "
441 "current time is outside of the "
442 "Job Queue processing window.")
443 .arg(JobText(jobs[x].type), logInfo);
444 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
445 continue;
446 }
447
448 message = QString("Processing '%1' job for %2, "
449 "current status is '%3'")
450 .arg(JobText(jobs[x].type), logInfo,
451 StatusText(status));
452 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
453
454 ProcessJob(jobs[x]);
455
456 startedJobAlready = true;
457 }
458 }
459
460 if (QCoreApplication::applicationName() == MYTH_APPNAME_MYTHJOBQUEUE)
461 {
462 if (m_jobsRunning > 0)
463 {
465 {
467 LOG(VB_JOBQUEUE, LOG_INFO, QString("%1 jobs running. "
468 "Blocking shutdown.").arg(m_jobsRunning));
469 }
470 }
471 else
472 {
474 {
476 LOG(VB_JOBQUEUE, LOG_INFO, "No jobs running. "
477 "Allowing shutdown.");
478 }
479 }
480 }
481
482
483 locker.relock();
484 if (m_processQueue)
485 {
486 std::chrono::milliseconds st = (startedJobAlready) ? 5s : sleepTime;
487 if (st > 0ms)
488 m_queueThreadCond.wait(locker.mutex(), st.count());
489 }
490 }
491}
492
493bool JobQueue::QueueRecordingJobs(const RecordingInfo &recinfo, int jobTypes)
494{
495 if (jobTypes == JOB_NONE)
496 jobTypes = recinfo.GetAutoRunJobs();
497
498 if (recinfo.IsCommercialFree())
499 jobTypes &= (~JOB_COMMFLAG);
500
501 if (jobTypes != JOB_NONE)
502 {
503 QString jobHost = QString("");
504
505 if (gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
506 jobHost = recinfo.GetHostname();
507
508 return JobQueue::QueueJobs(
509 jobTypes, recinfo.GetChanID(), recinfo.GetRecordingStartTime(),
510 "", "", jobHost);
511 }
512 return false;
513}
514
515bool JobQueue::QueueJob(int jobType, uint chanid, const QDateTime &recstartts,
516 const QString& args, const QString& comment, QString host,
517 int flags, int status, QDateTime schedruntime)
518{
519 int tmpStatus = JOB_UNKNOWN;
520 int tmpCmd = JOB_UNKNOWN;
521 int chanidInt = -1;
522
523 if(!schedruntime.isValid())
524 schedruntime = MythDate::current();
525
527
528 // In order to replace a job, we must have a chanid/recstartts combo
529 if (chanid)
530 {
531 int jobID = -1;
532 query.prepare("SELECT status, id, cmds FROM jobqueue "
533 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
534 "AND type = :JOBTYPE;");
535 query.bindValue(":CHANID", chanid);
536 query.bindValue(":STARTTIME", recstartts);
537 query.bindValue(":JOBTYPE", jobType);
538
539 if (!query.exec())
540 {
541 MythDB::DBError("Error in JobQueue::QueueJob()", query);
542 return false;
543 }
544 if (query.next())
545 {
546 tmpStatus = query.value(0).toInt();
547 jobID = query.value(1).toInt();
548 tmpCmd = query.value(2).toInt();
549 }
550 switch (tmpStatus)
551 {
552 case JOB_UNKNOWN:
553 break;
554 case JOB_STARTING:
555 case JOB_RUNNING:
556 case JOB_PAUSED:
557 case JOB_STOPPING:
558 case JOB_ERRORING:
559 case JOB_ABORTING:
560 return false;
561 default:
563 break;
564 }
565 if (! (tmpStatus & JOB_DONE) && (tmpCmd & JOB_STOP))
566 return false;
567
568 chanidInt = chanid;
569 }
570
571 if (host.isNull())
572 host = QString("");
573
574 query.prepare("INSERT INTO jobqueue (chanid, starttime, inserttime, type, "
575 "status, statustime, schedruntime, hostname, args, comment, "
576 "flags) "
577 "VALUES (:CHANID, :STARTTIME, now(), :JOBTYPE, :STATUS, "
578 "now(), :SCHEDRUNTIME, :HOST, :ARGS, :COMMENT, :FLAGS);");
579
580 query.bindValue(":CHANID", chanidInt);
581 query.bindValue(":STARTTIME", recstartts);
582 query.bindValue(":JOBTYPE", jobType);
583 query.bindValue(":STATUS", status);
584 query.bindValue(":SCHEDRUNTIME", schedruntime);
585 query.bindValue(":HOST", host);
586 query.bindValue(":ARGS", args);
587 query.bindValue(":COMMENT", comment);
588 query.bindValue(":FLAGS", flags);
589
590 if (!query.exec())
591 {
592 MythDB::DBError("Error in JobQueue::StartJob()", query);
593 return false;
594 }
595
596 return true;
597}
598
599bool JobQueue::QueueJobs(int jobTypes, uint chanid, const QDateTime &recstartts,
600 const QString& args, const QString& comment, const QString& host)
601{
602 if (gCoreContext->GetBoolSetting("AutoTranscodeBeforeAutoCommflag", false))
603 {
604 if (jobTypes & JOB_METADATA)
605 QueueJob(JOB_METADATA, chanid, recstartts, args, comment, host);
606 if (jobTypes & JOB_TRANSCODE)
607 QueueJob(JOB_TRANSCODE, chanid, recstartts, args, comment, host);
608 if (jobTypes & JOB_COMMFLAG)
609 QueueJob(JOB_COMMFLAG, chanid, recstartts, args, comment, host);
610 }
611 else
612 {
613 if (jobTypes & JOB_METADATA)
614 QueueJob(JOB_METADATA, chanid, recstartts, args, comment, host);
615 if (jobTypes & JOB_COMMFLAG)
616 QueueJob(JOB_COMMFLAG, chanid, recstartts, args, comment, host);
617 if (jobTypes & JOB_TRANSCODE)
618 {
619 QDateTime schedruntime = MythDate::current();
620
621 int defer = gCoreContext->GetNumSetting("DeferAutoTranscodeDays", 0);
622 if (defer)
623 {
624#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
625 schedruntime = QDateTime(schedruntime.addDays(defer).date(),
626 QTime(0,0,0), Qt::UTC);
627#else
628 schedruntime = QDateTime(schedruntime.addDays(defer).date(),
629 QTime(0,0,0),
630 QTimeZone(QTimeZone::UTC));
631#endif
632 }
633
634 QueueJob(JOB_TRANSCODE, chanid, recstartts, args, comment, host,
635 0, JOB_QUEUED, schedruntime);
636 }
637 }
638
639 if (jobTypes & JOB_USERJOB1)
640 QueueJob(JOB_USERJOB1, chanid, recstartts, args, comment, host);
641 if (jobTypes & JOB_USERJOB2)
642 QueueJob(JOB_USERJOB2, chanid, recstartts, args, comment, host);
643 if (jobTypes & JOB_USERJOB3)
644 QueueJob(JOB_USERJOB3, chanid, recstartts, args, comment, host);
645 if (jobTypes & JOB_USERJOB4)
646 QueueJob(JOB_USERJOB4, chanid, recstartts, args, comment, host);
647
648 return true;
649}
650
651int JobQueue::GetJobID(int jobType, uint chanid, const QDateTime &recstartts)
652{
654
655 query.prepare("SELECT id FROM jobqueue "
656 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
657 "AND type = :JOBTYPE;");
658 query.bindValue(":CHANID", chanid);
659 query.bindValue(":STARTTIME", recstartts);
660 query.bindValue(":JOBTYPE", jobType);
661
662 if (!query.exec())
663 {
664 MythDB::DBError("Error in JobQueue::GetJobID()", query);
665 return -1;
666 }
667 if (query.next())
668 return query.value(0).toInt();
669 return -1;
670}
671
673 int jobID, int &jobType, uint &chanid, QDateTime &recstartts)
674{
676
677 query.prepare("SELECT type, chanid, starttime FROM jobqueue "
678 "WHERE id = :ID;");
679
680 query.bindValue(":ID", jobID);
681
682 if (!query.exec())
683 {
684 MythDB::DBError("Error in JobQueue::GetJobInfoFromID()", query);
685 return false;
686 }
687 if (query.next())
688 {
689 jobType = query.value(0).toInt();
690 chanid = query.value(1).toUInt();
691 recstartts = MythDate::as_utc(query.value(2).toDateTime());
692 return true;
693 }
694 return false;
695}
696
698 int jobID, int &jobType, uint &chanid, QString &recstartts)
699{
700 QDateTime tmpStarttime;
701
702 bool result = JobQueue::GetJobInfoFromID(
703 jobID, jobType, chanid, tmpStarttime);
704
705 if (result)
706 recstartts = MythDate::toString(tmpStarttime, MythDate::kFilename);
707
708 return result;
709}
710
711int JobQueue::GetJobTypeFromName(const QString &name)
712{
713 if (!JobNameToType.contains(name))
714 {
715 LOG(VB_GENERAL, LOG_ERR, QString("'%1' is an invalid Job Name.")
716 .arg(name));
717 return JOB_NONE;
718 }
719 return JobNameToType[name];
720}
721
723{
724 QString message = QString("GLOBAL_JOB PAUSE ID %1").arg(jobID);
725 MythEvent me(message);
727
729}
730
732{
733 QString message = QString("GLOBAL_JOB RESUME ID %1").arg(jobID);
734 MythEvent me(message);
736
738}
739
741{
742 QString message = QString("GLOBAL_JOB RESTART ID %1").arg(jobID);
743 MythEvent me(message);
745
747}
748
750{
751 QString message = QString("GLOBAL_JOB STOP ID %1").arg(jobID);
752 MythEvent me(message);
754
756}
757
758bool JobQueue::DeleteAllJobs(uint chanid, const QDateTime &recstartts)
759{
761 QString message;
762
763 query.prepare("UPDATE jobqueue SET status = :CANCELLED "
764 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
765 "AND status = :QUEUED;");
766
767 query.bindValue(":CANCELLED", JOB_CANCELLED);
768 query.bindValue(":CHANID", chanid);
769 query.bindValue(":STARTTIME", recstartts);
770 query.bindValue(":QUEUED", JOB_QUEUED);
771
772 if (!query.exec())
773 MythDB::DBError("Cancel Pending Jobs", query);
774
775 query.prepare("UPDATE jobqueue SET cmds = :CMD "
776 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
777 "AND status <> :CANCELLED;");
778 query.bindValue(":CMD", JOB_STOP);
779 query.bindValue(":CHANID", chanid);
780 query.bindValue(":STARTTIME", recstartts);
781 query.bindValue(":CANCELLED", JOB_CANCELLED);
782
783 if (!query.exec())
784 {
785 MythDB::DBError("Stop Unfinished Jobs", query);
786 return false;
787 }
788
789 // wait until running job(s) are done
790 bool jobsAreRunning = true;
791 std::chrono::seconds totalSlept = 0s;
792 std::chrono::seconds maxSleep = 90s;
793 while (jobsAreRunning && totalSlept < maxSleep)
794 {
795 std::this_thread::sleep_for(1ms);
796 query.prepare("SELECT id FROM jobqueue "
797 "WHERE chanid = :CHANID and starttime = :STARTTIME "
798 "AND status NOT IN "
799 "(:FINISHED,:ABORTED,:ERRORED,:CANCELLED);");
800 query.bindValue(":CHANID", chanid);
801 query.bindValue(":STARTTIME", recstartts);
802 query.bindValue(":FINISHED", JOB_FINISHED);
803 query.bindValue(":ABORTED", JOB_ABORTED);
804 query.bindValue(":ERRORED", JOB_ERRORED);
805 query.bindValue(":CANCELLED", JOB_CANCELLED);
806
807 if (!query.exec())
808 {
809 MythDB::DBError("Stop Unfinished Jobs", query);
810 return false;
811 }
812
813 if (query.size() == 0)
814 {
815 jobsAreRunning = false;
816 continue;
817 }
818 if ((totalSlept % 5s) == 0s)
819 {
820 message = QString("Waiting on %1 jobs still running for "
821 "chanid %2 @ %3").arg(query.size())
822 .arg(chanid).arg(recstartts.toString(Qt::ISODate));
823 LOG(VB_JOBQUEUE, LOG_INFO, LOC + message);
824 }
825
826 std::this_thread::sleep_for(1s);
827 totalSlept++;
828 }
829
830 if (totalSlept <= maxSleep)
831 {
832 query.prepare("DELETE FROM jobqueue "
833 "WHERE chanid = :CHANID AND starttime = :STARTTIME;");
834 query.bindValue(":CHANID", chanid);
835 query.bindValue(":STARTTIME", recstartts);
836
837 if (!query.exec())
838 MythDB::DBError("Delete All Jobs", query);
839 }
840 else
841 {
842 query.prepare("SELECT id, type, status, comment FROM jobqueue "
843 "WHERE chanid = :CHANID AND starttime = :STARTTIME "
844 "AND status <> :CANCELLED ORDER BY id;");
845
846 query.bindValue(":CHANID", chanid);
847 query.bindValue(":STARTTIME", recstartts);
848 query.bindValue(":CANCELLED", JOB_CANCELLED);
849
850 if (!query.exec())
851 {
852 MythDB::DBError("Error in JobQueue::DeleteAllJobs(), Unable "
853 "to query list of Jobs left in Queue.", query);
854 return false;
855 }
856
857 LOG(VB_GENERAL, LOG_ERR, LOC +
858 QString( "In DeleteAllJobs: There are Jobs "
859 "left in the JobQueue that are still running for "
860 "chanid %1 @ %2.").arg(chanid)
861 .arg(recstartts.toString(Qt::ISODate)));
862
863 while (query.next())
864 {
865 LOG(VB_GENERAL, LOG_ERR, LOC +
866 QString("Job ID %1: '%2' with status '%3' and comment '%4'")
867 .arg(query.value(0).toString(),
868 JobText(query.value(1).toInt()),
869 StatusText(query.value(2).toInt()),
870 query.value(3).toString()));
871 }
872
873 return false;
874 }
875
876 return true;
877}
878
880{
881 return JobQueue::SafeDeleteJob(jobID, 0, 0, QDateTime());
882}
883
884bool JobQueue::SafeDeleteJob(int jobID, int jobType, int chanid,
885 const QDateTime& recstartts)
886{
887 if (jobID < 0)
888 return false;
889
890 if (chanid)
891 {
892
893 int thisJob = GetJobID(jobType, chanid, recstartts);
894 QString msg;
895
896 if( thisJob != jobID)
897 {
898 msg = QString("JobType, chanid and starttime don't match jobID %1");
899 LOG(VB_JOBQUEUE, LOG_ERR, LOC + msg.arg(jobID));
900 return false;
901 }
902
903 if (JobQueue::IsJobRunning(jobType, chanid, recstartts))
904 {
905 msg = QString("Can't remove running JobID %1");
906 LOG(VB_GENERAL, LOG_ERR, LOC + msg.arg(jobID));
907 return false;
908 }
909 }
910
912
913 query.prepare("DELETE FROM jobqueue WHERE id = :ID;");
914
915 query.bindValue(":ID", jobID);
916
917 if (!query.exec())
918 {
919 MythDB::DBError("Error in JobQueue::SafeDeleteJob()", query);
920 return false;
921 }
922
923 return true;
924}
925
926bool JobQueue::ChangeJobCmds(int jobID, int newCmds)
927{
928 if (jobID < 0)
929 return false;
930
932
933 query.prepare("UPDATE jobqueue SET cmds = :CMDS WHERE id = :ID;");
934
935 query.bindValue(":CMDS", newCmds);
936 query.bindValue(":ID", jobID);
937
938 if (!query.exec())
939 {
940 MythDB::DBError("Error in JobQueue::ChangeJobCmds()", query);
941 return false;
942 }
943
944 return true;
945}
946
947bool JobQueue::ChangeJobCmds(int jobType, uint chanid,
948 const QDateTime &recstartts, int newCmds)
949{
951
952 query.prepare("UPDATE jobqueue SET cmds = :CMDS WHERE type = :TYPE "
953 "AND chanid = :CHANID AND starttime = :STARTTIME;");
954
955 query.bindValue(":CMDS", newCmds);
956 query.bindValue(":TYPE", jobType);
957 query.bindValue(":CHANID", chanid);
958 query.bindValue(":STARTTIME", recstartts);
959
960 if (!query.exec())
961 {
962 MythDB::DBError("Error in JobQueue::ChangeJobCmds()", query);
963 return false;
964 }
965
966 return true;
967}
968
969bool JobQueue::ChangeJobFlags(int jobID, int newFlags)
970{
971 if (jobID < 0)
972 return false;
973
975
976 query.prepare("UPDATE jobqueue SET flags = :FLAGS WHERE id = :ID;");
977
978 query.bindValue(":FLAGS", newFlags);
979 query.bindValue(":ID", jobID);
980
981 if (!query.exec())
982 {
983 MythDB::DBError("Error in JobQueue::ChangeJobFlags()", query);
984 return false;
985 }
986
987 return true;
988}
989
990bool JobQueue::ChangeJobStatus(int jobID, int newStatus, const QString& comment)
991{
992 if (jobID < 0)
993 return false;
994
995 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("ChangeJobStatus(%1, %2, '%3')")
996 .arg(jobID).arg(StatusText(newStatus), comment));
997
999
1000 query.prepare("UPDATE jobqueue SET status = :STATUS, comment = :COMMENT "
1001 "WHERE id = :ID AND status <> :NEWSTATUS;");
1002
1003 query.bindValue(":STATUS", newStatus);
1004 query.bindValue(":COMMENT", comment);
1005 query.bindValue(":ID", jobID);
1006 query.bindValue(":NEWSTATUS", newStatus);
1007
1008 if (!query.exec())
1009 {
1010 MythDB::DBError("Error in JobQueue::ChangeJobStatus()", query);
1011 return false;
1012 }
1013
1014 return true;
1015}
1016
1017bool JobQueue::ChangeJobComment(int jobID, const QString& comment)
1018{
1019 if (jobID < 0)
1020 return false;
1021
1022 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("ChangeJobComment(%1, '%2')")
1023 .arg(jobID).arg(comment));
1024
1026
1027 query.prepare("UPDATE jobqueue SET comment = :COMMENT "
1028 "WHERE id = :ID;");
1029
1030 query.bindValue(":COMMENT", comment);
1031 query.bindValue(":ID", jobID);
1032
1033 if (!query.exec())
1034 {
1035 MythDB::DBError("Error in JobQueue::ChangeJobComment()", query);
1036 return false;
1037 }
1038
1039 return true;
1040}
1041
1042bool JobQueue::ChangeJobArgs(int jobID, const QString& args)
1043{
1044 if (jobID < 0)
1045 return false;
1046
1048
1049 query.prepare("UPDATE jobqueue SET args = :ARGS "
1050 "WHERE id = :ID;");
1051
1052 query.bindValue(":ARGS", args);
1053 query.bindValue(":ID", jobID);
1054
1055 if (!query.exec())
1056 {
1057 MythDB::DBError("Error in JobQueue::ChangeJobArgs()", query);
1058 return false;
1059 }
1060
1061 return true;
1062}
1063
1064int JobQueue::GetRunningJobID(uint chanid, const QDateTime &recstartts)
1065{
1066 m_runningJobsLock->lock();
1067 for (const auto& jInfo : std::as_const(m_runningJobs))
1068 {
1069 if ((jInfo.pginfo->GetChanID() == chanid) &&
1070 (jInfo.pginfo->GetRecordingStartTime() == recstartts))
1071 {
1072 m_runningJobsLock->unlock();
1073
1074 return jInfo.id;
1075 }
1076 }
1077 m_runningJobsLock->unlock();
1078
1079 return 0;
1080}
1081
1083{
1084 return (status == JOB_QUEUED);
1085}
1086
1088{
1089 return ((status != JOB_UNKNOWN) && (status != JOB_QUEUED) &&
1090 ((status & JOB_DONE) == 0));
1091}
1092
1093bool JobQueue::IsJobRunning(int jobType,
1094 uint chanid, const QDateTime &recstartts)
1095{
1096 return IsJobStatusRunning(GetJobStatus(jobType, chanid, recstartts));
1097}
1098
1099bool JobQueue::IsJobRunning(int jobType, const ProgramInfo &pginfo)
1100{
1102 jobType, pginfo.GetChanID(), pginfo.GetRecordingStartTime());
1103}
1104
1106 int jobType, uint chanid, const QDateTime &recstartts)
1107{
1108 int tmpStatus = GetJobStatus(jobType, chanid, recstartts);
1109
1110 return (tmpStatus != JOB_UNKNOWN) && ((tmpStatus & JOB_DONE) == 0);
1111}
1112
1114 int jobType, uint chanid, const QDateTime &recstartts)
1115{
1116 return IsJobStatusQueued(GetJobStatus(jobType, chanid, recstartts));
1117}
1118
1119QString JobQueue::JobText(int jobType)
1120{
1121 switch (jobType)
1122 {
1123 case JOB_TRANSCODE: return tr("Transcode");
1124 case JOB_COMMFLAG: return tr("Flag Commercials");
1125 case JOB_METADATA: return tr("Look up Metadata");
1126 case JOB_PREVIEW: return tr("Preview Generation");
1127 }
1128
1129 if (jobType & JOB_USERJOB)
1130 {
1131 QString settingName =
1132 QString("UserJobDesc%1").arg(UserJobTypeToIndex(jobType));
1133 return gCoreContext->GetSetting(settingName, settingName);
1134 }
1135
1136 return tr("Unknown Job");
1137}
1138
1139// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
1140#define JOBSTATUS_STATUSTEXT(A,B,C) case A: return C;
1141
1142QString JobQueue::StatusText(int status)
1143{
1144 switch (status)
1145 {
1147 default: break;
1148 }
1149 return tr("Undefined");
1150}
1151
1152bool JobQueue::InJobRunWindow(std::chrono::minutes orStartsWithinMins)
1153{
1154 QString queueStartTimeStr;
1155 QString queueEndTimeStr;
1156 QTime queueStartTime;
1157 QTime queueEndTime;
1158 QTime curTime = QTime::currentTime();
1159 bool inTimeWindow = false;
1160 orStartsWithinMins = orStartsWithinMins < 0min ? 0min : orStartsWithinMins;
1161
1162 queueStartTimeStr = gCoreContext->GetSetting("JobQueueWindowStart", "00:00");
1163 queueEndTimeStr = gCoreContext->GetSetting("JobQueueWindowEnd", "23:59");
1164
1165 queueStartTime = QTime::fromString(queueStartTimeStr, "hh:mm");
1166 if (!queueStartTime.isValid())
1167 {
1168 LOG(VB_GENERAL, LOG_ERR,
1169 QString("Invalid JobQueueWindowStart time '%1', using 00:00")
1170 .arg(queueStartTimeStr));
1171 queueStartTime = QTime(0, 0);
1172 }
1173
1174 queueEndTime = QTime::fromString(queueEndTimeStr, "hh:mm");
1175 if (!queueEndTime.isValid())
1176 {
1177 LOG(VB_GENERAL, LOG_ERR,
1178 QString("Invalid JobQueueWindowEnd time '%1', using 23:59")
1179 .arg(queueEndTimeStr));
1180 queueEndTime = QTime(23, 59);
1181 }
1182
1183 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1184 QString("Currently set to run new jobs from %1 to %2")
1185 .arg(queueStartTimeStr, queueEndTimeStr));
1186
1187 if ((queueStartTime <= curTime) && (curTime < queueEndTime))
1188 { // NOLINT(bugprone-branch-clone)
1189 inTimeWindow = true;
1190 }
1191 else if ((queueStartTime > queueEndTime) &&
1192 ((curTime < queueEndTime) || (queueStartTime <= curTime)))
1193 {
1194 inTimeWindow = true;
1195 }
1196 else if (orStartsWithinMins > 0min)
1197 {
1198 // Check if the window starts soon
1199 if (curTime <= queueStartTime)
1200 {
1201 // Start time hasn't passed yet today
1202 if (queueStartTime.secsTo(curTime) <= duration_cast<std::chrono::seconds>(orStartsWithinMins).count())
1203 {
1204 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1205 QString("Job run window will start within %1 minutes")
1206 .arg(orStartsWithinMins.count()));
1207 inTimeWindow = true;
1208 }
1209 }
1210 else
1211 {
1212 // We passed the start time for today, try tomorrow
1213 QDateTime curDateTime = MythDate::current();
1214#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1215 QDateTime startDateTime = QDateTime(
1216 curDateTime.date(), queueStartTime, Qt::UTC).addDays(1);
1217#else
1218 QDateTime startDateTime =
1219 QDateTime(curDateTime.date(), queueStartTime,
1220 QTimeZone(QTimeZone::UTC)).addDays(1);
1221#endif
1222
1223 if (curDateTime.secsTo(startDateTime) <= duration_cast<std::chrono::seconds>(orStartsWithinMins).count())
1224 {
1225 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1226 QString("Job run window will start "
1227 "within %1 minutes (tomorrow)")
1228 .arg(orStartsWithinMins.count()));
1229 inTimeWindow = true;
1230 }
1231 }
1232 }
1233
1234 return inTimeWindow;
1235}
1236
1237bool JobQueue::HasRunningOrPendingJobs(std::chrono::minutes startingWithinMins)
1238{
1239 /* startingWithinMins <= 0 - look for any pending jobs
1240 > 0 - only consider pending starting within this time */
1241 QMap<int, JobQueueEntry> jobs;
1242 QMap<int, JobQueueEntry>::Iterator it;
1243 QDateTime maxSchedRunTime = MythDate::current();
1244 bool checkForQueuedJobs = (startingWithinMins <= 0min
1245 || InJobRunWindow(startingWithinMins));
1246
1247 if (checkForQueuedJobs && startingWithinMins > 0min) {
1248 maxSchedRunTime = maxSchedRunTime.addSecs(duration_cast<std::chrono::seconds>(startingWithinMins).count());
1249 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1250 QString("HasRunningOrPendingJobs: checking for jobs "
1251 "starting before: %1")
1252 .arg(maxSchedRunTime.toString(Qt::ISODate)));
1253 }
1254
1256
1257 if (!jobs.empty()) {
1258 for (it = jobs.begin(); it != jobs.end(); ++it)
1259 {
1260 int tmpStatus = (*it).status;
1261 if (tmpStatus == JOB_RUNNING) {
1262 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1263 QString("HasRunningOrPendingJobs: found running job"));
1264 return true;
1265 }
1266
1267 if (checkForQueuedJobs) {
1268 if ((tmpStatus != JOB_UNKNOWN) && (!(tmpStatus & JOB_DONE))) {
1269 if (startingWithinMins <= 0min) {
1270 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1271 "HasRunningOrPendingJobs: found pending job");
1272 return true;
1273 }
1274 if ((*it).schedruntime <= maxSchedRunTime) {
1275 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1276 QString("HasRunningOrPendingJobs: found pending "
1277 "job scheduled to start at: %1")
1278 .arg((*it).schedruntime.toString(Qt::ISODate)));
1279 return true;
1280 }
1281 }
1282 }
1283 }
1284 }
1285 return false;
1286}
1287
1288
1289int JobQueue::GetJobsInQueue(QMap<int, JobQueueEntry> &jobs, int findJobs)
1290{
1291 JobQueueEntry thisJob;
1293 QDateTime recentDate = MythDate::current().addSecs(-kRecentInterval);
1294 QString logInfo;
1295 int jobCount = 0;
1296 bool commflagWhileRecording =
1297 gCoreContext->GetBoolSetting("AutoCommflagWhileRecording", false);
1298
1299 jobs.clear();
1300
1301 query.prepare("SELECT j.id, j.chanid, j.starttime, j.inserttime, j.type, "
1302 "j.cmds, j.flags, j.status, j.statustime, j.hostname, "
1303 "j.args, j.comment, r.endtime, j.schedruntime "
1304 "FROM jobqueue j "
1305 "LEFT JOIN recorded r "
1306 " ON j.chanid = r.chanid AND j.starttime = r.starttime "
1307 "ORDER BY j.schedruntime, j.id;");
1308
1309 if (!query.exec())
1310 {
1311 MythDB::DBError("Error in JobQueue::GetJobs(), Unable to "
1312 "query list of Jobs in Queue.", query);
1313 return 0;
1314 }
1315
1316 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1317 QString("GetJobsInQueue: findJobs search bitmask %1, "
1318 "found %2 total jobs")
1319 .arg(findJobs).arg(query.size()));
1320
1321 while (query.next())
1322 {
1323 bool wantThisJob = false;
1324
1325 thisJob.id = query.value(0).toInt();
1326 thisJob.recstartts = MythDate::as_utc(query.value(2).toDateTime());
1327 thisJob.schedruntime = MythDate::as_utc(query.value(13).toDateTime());
1328 thisJob.type = query.value(4).toInt();
1329 thisJob.status = query.value(7).toInt();
1330 thisJob.statustime = MythDate::as_utc(query.value(8).toDateTime());
1331 thisJob.startts = MythDate::toString(
1333
1334 // -1 indicates the chanid is empty
1335 if (query.value(1).toInt() == -1)
1336 {
1337 thisJob.chanid = 0;
1338 logInfo = QString("jobID #%1").arg(thisJob.id);
1339 }
1340 else
1341 {
1342 thisJob.chanid = query.value(1).toUInt();
1343 logInfo = QString("chanid %1 @ %2").arg(thisJob.chanid)
1344 .arg(thisJob.startts);
1345 }
1346
1347 if ((MythDate::as_utc(query.value(12).toDateTime()) > MythDate::current()) &&
1348 ((!commflagWhileRecording) ||
1349 ((thisJob.type != JOB_COMMFLAG) &&
1350 (thisJob.type != JOB_METADATA))))
1351 {
1352 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1353 QString("GetJobsInQueue: Ignoring '%1' Job "
1354 "for %2 in %3 state. Endtime in future.")
1355 .arg(JobText(thisJob.type),
1356 logInfo, StatusText(thisJob.status)));
1357 continue;
1358 }
1359
1360 if ((findJobs & JOB_LIST_ALL) ||
1361 ((findJobs & JOB_LIST_DONE) &&
1362 (thisJob.status & JOB_DONE)) ||
1363 ((findJobs & JOB_LIST_NOT_DONE) &&
1364 (!(thisJob.status & JOB_DONE))) ||
1365 ((findJobs & JOB_LIST_ERROR) &&
1366 (thisJob.status == JOB_ERRORED)) ||
1367 ((findJobs & JOB_LIST_RECENT) &&
1368 (thisJob.statustime > recentDate)))
1369 wantThisJob = true;
1370
1371 if (!wantThisJob)
1372 {
1373 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1374 QString("GetJobsInQueue: Ignore '%1' Job for %2 in %3 state.")
1375 .arg(JobText(thisJob.type),
1376 logInfo, StatusText(thisJob.status)));
1377 continue;
1378 }
1379
1380 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1381 QString("GetJobsInQueue: Found '%1' Job for %2 in %3 state.")
1382 .arg(JobText(thisJob.type),
1383 logInfo, StatusText(thisJob.status)));
1384
1385 thisJob.inserttime = MythDate::as_utc(query.value(3).toDateTime());
1386 thisJob.cmds = query.value(5).toInt();
1387 thisJob.flags = query.value(6).toInt();
1388 thisJob.hostname = query.value(9).toString();
1389 thisJob.args = query.value(10).toString();
1390 thisJob.comment = query.value(11).toString();
1391
1392 if ((thisJob.type & JOB_USERJOB) &&
1393 (UserJobTypeToIndex(thisJob.type) == 0))
1394 {
1395 thisJob.type = JOB_NONE;
1396 LOG(VB_JOBQUEUE, LOG_INFO, LOC +
1397 QString("GetJobsInQueue: Unknown Job Type: %1")
1398 .arg(thisJob.type));
1399 }
1400
1401 if (thisJob.type != JOB_NONE)
1402 jobs[jobCount++] = thisJob;
1403 }
1404
1405 return jobCount;
1406}
1407
1408bool JobQueue::ChangeJobHost(int jobID, const QString& newHostname)
1409{
1411
1412 if (!newHostname.isEmpty())
1413 {
1414 query.prepare("UPDATE jobqueue SET hostname = :NEWHOSTNAME "
1415 "WHERE hostname = :EMPTY AND id = :ID;");
1416 query.bindValue(":NEWHOSTNAME", newHostname);
1417 query.bindValue(":EMPTY", "");
1418 query.bindValue(":ID", jobID);
1419 }
1420 else
1421 {
1422 query.prepare("UPDATE jobqueue SET hostname = :EMPTY "
1423 "WHERE id = :ID;");
1424 query.bindValue(":EMPTY", "");
1425 query.bindValue(":ID", jobID);
1426 }
1427
1428 if (!query.exec())
1429 {
1430 MythDB::DBError(QString("Error in JobQueue::ChangeJobHost(), "
1431 "Unable to set hostname to '%1' for "
1432 "job %2.").arg(newHostname).arg(jobID),
1433 query);
1434 return false;
1435 }
1436
1437 return query.numRowsAffected() > 0;
1438}
1439
1441{
1442 QString allowSetting;
1443
1444 if ((!job.hostname.isEmpty()) &&
1445 (job.hostname != m_hostname))
1446 return false;
1447
1448 if (job.type & JOB_USERJOB)
1449 {
1450 allowSetting =
1451 QString("JobAllowUserJob%1").arg(UserJobTypeToIndex(job.type));
1452 }
1453 else
1454 {
1455 switch (job.type)
1456 {
1457 case JOB_TRANSCODE: allowSetting = "JobAllowTranscode";
1458 break;
1459 case JOB_COMMFLAG: allowSetting = "JobAllowCommFlag";
1460 break;
1461 case JOB_METADATA: allowSetting = "JobAllowMetadata";
1462 break;
1463 case JOB_PREVIEW: allowSetting = "JobAllowPreview";
1464 break;
1465 default: return false;
1466 }
1467 }
1468
1469 return gCoreContext->GetBoolSetting(allowSetting, true);
1470}
1471
1473{
1475
1476 query.prepare("SELECT cmds FROM jobqueue WHERE id = :ID;");
1477
1478 query.bindValue(":ID", jobID);
1479
1480 if (query.exec())
1481 {
1482 if (query.next())
1483 return (enum JobCmds)query.value(0).toInt();
1484 }
1485 else
1486 {
1487 MythDB::DBError("Error in JobQueue::GetJobCmd()", query);
1488 }
1489
1490 return JOB_RUN;
1491}
1492
1494{
1496
1497 query.prepare("SELECT args FROM jobqueue WHERE id = :ID;");
1498
1499 query.bindValue(":ID", jobID);
1500
1501 if (query.exec())
1502 {
1503 if (query.next())
1504 return query.value(0).toString();
1505 }
1506 else
1507 {
1508 MythDB::DBError("Error in JobQueue::GetJobArgs()", query);
1509 }
1510
1511 return {""};
1512}
1513
1515{
1517
1518 query.prepare("SELECT flags FROM jobqueue WHERE id = :ID;");
1519
1520 query.bindValue(":ID", jobID);
1521
1522 if (query.exec())
1523 {
1524 if (query.next())
1525 return (enum JobFlags)query.value(0).toInt();
1526 }
1527 else
1528 {
1529 MythDB::DBError("Error in JobQueue::GetJobFlags()", query);
1530 }
1531
1532 return JOB_NO_FLAGS;
1533}
1534
1536{
1538
1539 query.prepare("SELECT status FROM jobqueue WHERE id = :ID;");
1540
1541 query.bindValue(":ID", jobID);
1542
1543 if (query.exec())
1544 {
1545 if (query.next())
1546 return (enum JobStatus)query.value(0).toInt();
1547 }
1548 else
1549 {
1550 MythDB::DBError("Error in JobQueue::GetJobStatus()", query);
1551 }
1552 return JOB_UNKNOWN;
1553}
1554
1556 int jobType, uint chanid, const QDateTime &recstartts)
1557{
1559
1560 query.prepare("SELECT status FROM jobqueue WHERE type = :TYPE "
1561 "AND chanid = :CHANID AND starttime = :STARTTIME;");
1562
1563 query.bindValue(":TYPE", jobType);
1564 query.bindValue(":CHANID", chanid);
1565 query.bindValue(":STARTTIME", recstartts);
1566
1567 if (query.exec())
1568 {
1569 if (query.next())
1570 return (enum JobStatus)query.value(0).toInt();
1571 }
1572 else
1573 {
1574 MythDB::DBError("Error in JobQueue::GetJobStatus()", query);
1575 }
1576 return JOB_UNKNOWN;
1577}
1578
1579void JobQueue::RecoverQueue(bool justOld)
1580{
1581 QMap<int, JobQueueEntry> jobs;
1582 QString msg;
1583 QString logInfo;
1584
1585 msg = QString("RecoverQueue: Checking for unfinished jobs to "
1586 "recover.");
1587 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
1588
1589 GetJobsInQueue(jobs);
1590
1591 if (!jobs.empty())
1592 {
1593 QMap<int, JobQueueEntry>::Iterator it;
1594 QDateTime oldDate = MythDate::current().addDays(-1);
1595 QString hostname = gCoreContext->GetHostName();
1596
1597 for (it = jobs.begin(); it != jobs.end(); ++it)
1598 {
1599 int tmpCmds = (*it).cmds;
1600 int tmpStatus = (*it).status;
1601
1602 if (!(*it).chanid)
1603 logInfo = QString("jobID #%1").arg((*it).id);
1604 else
1605 logInfo = QString("chanid %1 @ %2").arg((*it).chanid)
1606 .arg((*it).startts);
1607
1608 if (((tmpStatus == JOB_STARTING) ||
1609 (tmpStatus == JOB_RUNNING) ||
1610 (tmpStatus == JOB_PAUSED) ||
1611 (tmpCmds & JOB_STOP) ||
1612 (tmpStatus == JOB_STOPPING)) &&
1613 (((!justOld) &&
1614 ((*it).hostname == hostname)) ||
1615 ((*it).statustime < oldDate)))
1616 {
1617 msg = QString("RecoverQueue: Recovering '%1' for %2 "
1618 "from '%3' state.")
1619 .arg(JobText((*it).type),
1620 logInfo, StatusText((*it).status));
1621 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
1622
1623 ChangeJobStatus((*it).id, JOB_QUEUED, "");
1624 ChangeJobCmds((*it).id, JOB_RUN);
1625 if (!gCoreContext->GetBoolSetting("JobsRunOnRecordHost", false))
1626 ChangeJobHost((*it).id, "");
1627 }
1628 else
1629 {
1630#if 0
1631 msg = QString("RecoverQueue: Ignoring '%1' for %2 "
1632 "in '%3' state.")
1633 .arg(JobText((*it).type))
1634 .arg(logInfo).arg(StatusText((*it).status));
1635 LOG(VB_JOBQUEUE, LOG_INFO, LOC + msg);
1636#endif
1637 }
1638 }
1639 }
1640}
1641
1643{
1644 MSqlQuery delquery(MSqlQuery::InitCon());
1645 QDateTime donePurgeDate = MythDate::current().addDays(-2);
1646 QDateTime errorsPurgeDate = MythDate::current().addDays(-4);
1647
1648 delquery.prepare("DELETE FROM jobqueue "
1649 "WHERE (status in (:FINISHED, :ABORTED, :CANCELLED) "
1650 "AND statustime < :DONEPURGEDATE) "
1651 "OR (status in (:ERRORED) "
1652 "AND statustime < :ERRORSPURGEDATE) ");
1653 delquery.bindValue(":FINISHED", JOB_FINISHED);
1654 delquery.bindValue(":ABORTED", JOB_ABORTED);
1655 delquery.bindValue(":CANCELLED", JOB_CANCELLED);
1656 delquery.bindValue(":ERRORED", JOB_ERRORED);
1657 delquery.bindValue(":DONEPURGEDATE", donePurgeDate);
1658 delquery.bindValue(":ERRORSPURGEDATE", errorsPurgeDate);
1659
1660 if (!delquery.exec())
1661 {
1662 MythDB::DBError("JobQueue::CleanupOldJobsInQueue: Error deleting "
1663 "old finished jobs.", delquery);
1664 }
1665}
1666
1667bool JobQueue::InJobRunWindow(QDateTime jobstarttsRaw)
1668{
1669 if (!jobstarttsRaw.isValid())
1670 {
1671 jobstarttsRaw = QDateTime::currentDateTime();
1672 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Invalid date/time passed, "
1673 "using %1").arg(
1674 jobstarttsRaw.toString()));
1675 }
1676
1677 QString hostname(gCoreContext->GetHostName());
1678
1680 "JobQueueWindowStart", hostname, "00:00")));
1681
1683 "JobQueueWindowEnd", hostname, "23:59")));
1684
1685 QTime scheduleTime(QTime::fromString(jobstarttsRaw.toString("hh:mm")));
1686
1687 if (scheduleTime < windowStart || scheduleTime > windowEnd)
1688 {
1689 LOG(VB_JOBQUEUE, LOG_ERR, LOC + "Time not within job queue window, " +
1690 "job not queued");
1691 return false;
1692 }
1693
1694 return true;
1695}
1696
1698{
1699 int jobID = job.id;
1700
1702 {
1703 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
1704 "ProcessJob(): Unable to open database connection");
1705 return;
1706 }
1707
1708 ChangeJobStatus(jobID, JOB_PENDING);
1709 ProgramInfo *pginfo = nullptr;
1710
1711 if (job.chanid)
1712 {
1713 pginfo = new ProgramInfo(job.chanid, job.recstartts);
1714
1715 if (!pginfo->GetChanID())
1716 {
1717 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
1718 QString("Unable to retrieve program info for chanid %1 @ %2")
1719 .arg(job.chanid)
1720 .arg(job.recstartts.toString(Qt::ISODate)));
1721
1722 ChangeJobStatus(jobID, JOB_ERRORED,
1723 tr("Unable to retrieve program info from database"));
1724
1725 delete pginfo;
1726
1727 return;
1728 }
1729
1730 pginfo->SetPathname(pginfo->GetPlaybackURL());
1731 }
1732
1733
1734 m_runningJobsLock->lock();
1735
1736 ChangeJobStatus(jobID, JOB_STARTING);
1737 RunningJobInfo jInfo;
1738 jInfo.type = job.type;
1739 jInfo.id = jobID;
1740 jInfo.flag = JOB_RUN;
1741 jInfo.desc = GetJobDescription(job.type);
1742 jInfo.command = GetJobCommand(jobID, job.type, pginfo);
1743 jInfo.pginfo = pginfo;
1744
1745 m_runningJobs[jobID] = jInfo;
1746
1747 if (pginfo)
1748 pginfo->MarkAsInUse(true, kJobQueueInUseID);
1749
1750 if (pginfo && pginfo->GetRecordingGroup() == "Deleted")
1751 {
1752 ChangeJobStatus(jobID, JOB_CANCELLED,
1753 tr("Program has been deleted"));
1755 }
1756 else if ((job.type == JOB_TRANSCODE) ||
1757 (m_runningJobs[jobID].command == "mythtranscode"))
1758 {
1760 }
1761 else if ((job.type == JOB_COMMFLAG) ||
1762 (m_runningJobs[jobID].command == "mythcommflag"))
1763 {
1765 }
1766 else if ((job.type == JOB_METADATA) ||
1767 (m_runningJobs[jobID].command == "mythmetadatalookup"))
1768 {
1770 }
1771 else if (job.type & JOB_USERJOB)
1772 {
1774 }
1775 else
1776 {
1777 ChangeJobStatus(jobID, JOB_ERRORED,
1778 tr("UNKNOWN JobType, unable to process!"));
1780 }
1781
1782 m_runningJobsLock->unlock();
1783}
1784
1785void JobQueue::StartChildJob(void *(*ChildThreadRoutine)(void *), int jobID)
1786{
1787 auto *jts = new JobThreadStruct;
1788 jts->jq = this;
1789 jts->jobID = jobID;
1790
1791 pthread_t childThread = PTHREAD_NULL;
1792 pthread_attr_t attr;
1793 pthread_attr_init(&attr);
1794 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
1795 pthread_create(&childThread, &attr, ChildThreadRoutine, jts);
1796 pthread_attr_destroy(&attr);
1797}
1798
1800{
1801 if (jobType == JOB_TRANSCODE)
1802 return "Transcode";
1803 if (jobType == JOB_COMMFLAG)
1804 return "Commercial Detection";
1805 if (!(jobType & JOB_USERJOB))
1806 return "Unknown Job";
1807
1808 QString descSetting =
1809 QString("UserJobDesc%1").arg(UserJobTypeToIndex(jobType));
1810
1811 return gCoreContext->GetSetting(descSetting, "Unknown Job");
1812}
1813
1814QString JobQueue::GetJobCommand(int id, int jobType, ProgramInfo *tmpInfo)
1815{
1816 QString command;
1818
1819 if (jobType == JOB_TRANSCODE)
1820 {
1821 command = gCoreContext->GetSetting("JobQueueTranscodeCommand");
1822 if (command.trimmed().isEmpty())
1823 command = "mythtranscode";
1824
1825 if (command == "mythtranscode")
1826 return command;
1827 }
1828 else if (jobType == JOB_COMMFLAG)
1829 {
1830 command = gCoreContext->GetSetting("JobQueueCommFlagCommand");
1831 if (command.trimmed().isEmpty())
1832 command = "mythcommflag";
1833
1834 if (command == "mythcommflag")
1835 return command;
1836 }
1837 else if (jobType & JOB_USERJOB)
1838 {
1839 command = gCoreContext->GetSetting(
1840 QString("UserJob%1").arg(UserJobTypeToIndex(jobType)), "");
1841 }
1842
1843 if (!command.isEmpty())
1844 {
1845 command.replace("%JOBID%", QString("%1").arg(id));
1846 }
1847
1848 if (!command.isEmpty() && tmpInfo)
1849 {
1850 tmpInfo->SubstituteMatches(command);
1851
1852 command.replace("%VERBOSELEVEL%", QString("%1").arg(verboseMask));
1853 command.replace("%VERBOSEMODE%", QString("%1").arg(logPropagateArgs));
1854
1855 uint transcoder = tmpInfo->QueryTranscoderID();
1856 command.replace("%TRANSPROFILE%",
1858 "autodetect" : QString::number(transcoder));
1859 }
1860
1861 return command;
1862}
1863
1865{
1866 m_runningJobsLock->lock();
1867
1868 if (m_runningJobs.contains(id))
1869 {
1870 ProgramInfo *pginfo = m_runningJobs[id].pginfo;
1871 if (pginfo)
1872 {
1873 pginfo->MarkAsInUse(false, kJobQueueInUseID);
1874 delete pginfo;
1875 }
1876
1877 m_runningJobs.remove(id);
1878 }
1879
1880 m_runningJobsLock->unlock();
1881}
1882
1884{
1885 // Pretty print "bytes" as KB, MB, GB, TB, etc., subject to the desired
1886 // number of units
1887 struct PpTab_t {
1888 const char *m_suffix;
1889 unsigned int m_max;
1890 int m_precision;
1891 };
1892 static constexpr std::array<const PpTab_t,9> kPpTab {{
1893 { "bytes", 9999, 0 },
1894 { "kB", 999, 0 },
1895 { "MB", 999, 1 },
1896 { "GB", 999, 1 },
1897 { "TB", 999, 1 },
1898 { "PB", 999, 1 },
1899 { "EB", 999, 1 },
1900 { "ZB", 999, 1 },
1901 { "YB", 0, 0 },
1902 }};
1903 float fbytes = bytes;
1904
1905 unsigned int ii = 0;
1906 while (kPpTab[ii].m_max && fbytes > kPpTab[ii].m_max) {
1907 fbytes /= 1024;
1908 ii++;
1909 }
1910
1911 return QString("%1 %2")
1912 .arg(fbytes, 0, 'f', kPpTab[ii].m_precision)
1913 .arg(kPpTab[ii].m_suffix);
1914}
1915
1917{
1918 auto *jts = (JobThreadStruct *)param;
1919 JobQueue *jq = jts->jq;
1920
1921 MThread::ThreadSetup(QString("Transcode_%1").arg(jts->jobID));
1922 jq->DoTranscodeThread(jts->jobID);
1924
1925 delete jts;
1926
1927 return nullptr;
1928}
1929
1931{
1932 // We can't currently transcode non-recording files w/o a ProgramInfo
1933 m_runningJobsLock->lock();
1934 if (!m_runningJobs[jobID].pginfo)
1935 {
1936 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
1937 "The JobQueue cannot currently transcode files that do not "
1938 "have a chanid/starttime in the recorded table.");
1939 ChangeJobStatus(jobID, JOB_ERRORED, "ProgramInfo data not found");
1941 m_runningJobsLock->unlock();
1942 return;
1943 }
1944
1945 ProgramInfo *program_info = m_runningJobs[jobID].pginfo;
1946 m_runningJobsLock->unlock();
1947
1948 ChangeJobStatus(jobID, JOB_RUNNING);
1949
1950 // make sure flags are up to date
1951 program_info->Reload();
1952
1953 bool useCutlist = program_info->HasCutlist() &&
1954 ((GetJobFlags(jobID) & JOB_USE_CUTLIST) != 0);
1955
1956 uint transcoder = program_info->QueryTranscoderID();
1957 QString profilearg =
1959 "autodetect" : QString::number(transcoder);
1960
1961 QString path;
1962 QString command;
1963
1964 m_runningJobsLock->lock();
1965 if (m_runningJobs[jobID].command == "mythtranscode")
1966 {
1967 path = GetAppBinDir() + "mythtranscode";
1968 command = QString("%1 -j %2 --profile %3")
1969 .arg(path).arg(jobID).arg(profilearg);
1970 if (useCutlist)
1971 command += " --honorcutlist";
1972 command += logPropagateArgs;
1973 }
1974 else
1975 {
1976 command = m_runningJobs[jobID].command;
1977
1978 QStringList tokens = command.split(" ", Qt::SkipEmptyParts);
1979 if (!tokens.empty())
1980 path = tokens[0];
1981 }
1982 m_runningJobsLock->unlock();
1983
1984 if (m_jobQueueCPU < 2)
1985 {
1986 myth_nice(17);
1987 myth_ioprio((0 == m_jobQueueCPU) ? 8 : 7);
1988 }
1989
1990 QString transcoderName;
1992 {
1993 transcoderName = "Autodetect";
1994 }
1995 else
1996 {
1998 query.prepare("SELECT name FROM recordingprofiles WHERE id = :ID;");
1999 query.bindValue(":ID", transcoder);
2000 if (query.exec() && query.next())
2001 {
2002 transcoderName = query.value(0).toString();
2003 }
2004 else
2005 {
2006 /* Unexpected value; log it. */
2007 transcoderName = QString("Autodetect(%1)").arg(transcoder);
2008 }
2009 }
2010
2011 bool retry = true;
2012 int retrylimit = 3;
2013 while (retry)
2014 {
2015 retry = false;
2016
2017 ChangeJobStatus(jobID, JOB_STARTING);
2019
2020 QString filename = program_info->GetPlaybackURL(false, true);
2021
2022 long long filesize = 0;
2023 long long origfilesize = QFileInfo(filename).size();
2024
2025 QString msg = QString("Transcode %1")
2027
2028 QString details = QString("%1: %2 (%3)")
2029 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),
2030 transcoderName, PrettyPrint(origfilesize));
2031
2032 LOG(VB_GENERAL, LOG_INFO, LOC + QString("%1 for %2")
2033 .arg(msg, details));
2034
2035 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2036 .arg(command));
2037
2038 GetMythDB()->GetDBManager()->CloseDatabases();
2039 uint result = myth_system(command);
2040 int status = GetJobStatus(jobID);
2041
2042 if ((result == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2043 (result == GENERIC_EXIT_CMD_NOT_FOUND))
2044 {
2045 ChangeJobStatus(jobID, JOB_ERRORED,
2046 tr("ERROR: Unable to find mythtranscode, check backend logs."));
2048
2049 msg = QString("Transcode %1").arg(StatusText(GetJobStatus(jobID)));
2050 details = QString("%1: %2 does not exist or is not executable")
2051 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),path);
2052
2053 LOG(VB_GENERAL, LOG_ERR, LOC +
2054 QString("%1 for %2").arg(msg, details));
2055 }
2056 else if (result == GENERIC_EXIT_RESTART && retrylimit > 0)
2057 {
2058 LOG(VB_JOBQUEUE, LOG_INFO, LOC + "Transcode command restarting");
2059 retry = true;
2060 retrylimit--;
2061
2063 }
2064 else
2065 {
2066 if (status == JOB_FINISHED)
2067 {
2068 ChangeJobStatus(jobID, JOB_FINISHED, tr("Finished."));
2069 retry = false;
2070
2071 program_info->Reload(); // Refresh, the basename may have changed
2072 filename = program_info->GetPlaybackURL(false, true);
2073 QFileInfo st(filename);
2074
2075 if (st.exists())
2076 {
2077 filesize = st.size();
2078 /*: %1 is transcoder name, %2 is the original file size
2079 and %3 is the current file size */
2080 QString comment = tr("%1: %2 => %3")
2081 .arg(transcoderName,
2082 PrettyPrint(origfilesize),
2083 PrettyPrint(filesize));
2084 ChangeJobComment(jobID, comment);
2085
2086 if (filesize > 0)
2087 program_info->SaveFilesize(filesize);
2088
2089 details = QString("%1: %2 (%3)")
2090 .arg(program_info->toString(
2092 transcoderName,
2093 PrettyPrint(filesize));
2094 }
2095 else
2096 {
2097 QString comment =
2098 QString("could not stat '%1'").arg(filename);
2099
2100 ChangeJobStatus(jobID, JOB_FINISHED, comment);
2101
2102 details = QString("%1: %2")
2103 .arg(program_info->toString(
2105 comment);
2106 }
2107
2109 }
2110 else
2111 {
2113
2114 QString comment = tr("exit status %1, job status was \"%2\"")
2115 .arg(result)
2116 .arg(StatusText(status));
2117
2118 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2119
2120 details = QString("%1: %2 (%3)")
2121 .arg(program_info->toString(
2123 transcoderName,
2124 comment);
2125 }
2126
2127 msg = QString("Transcode %1").arg(StatusText(GetJobStatus(jobID)));
2128 LOG(VB_GENERAL, LOG_INFO, LOC + msg + ": " + details);
2129 }
2130 }
2131
2132 if (retrylimit == 0)
2133 {
2134 LOG(VB_JOBQUEUE, LOG_ERR, LOC + "Retry limit exceeded for transcoder, "
2135 "setting job status to errored.");
2136 ChangeJobStatus(jobID, JOB_ERRORED, tr("Retry limit exceeded"));
2137 }
2138
2140}
2141
2143{
2144 auto *jts = (JobThreadStruct *)param;
2145 JobQueue *jq = jts->jq;
2146
2147 MThread::ThreadSetup(QString("Metadata_%1").arg(jts->jobID));
2148 jq->DoMetadataLookupThread(jts->jobID);
2150
2151 delete jts;
2152
2153 return nullptr;
2154}
2155
2157{
2158 // We can't currently lookup non-recording files w/o a ProgramInfo
2159 m_runningJobsLock->lock();
2160 if (!m_runningJobs[jobID].pginfo)
2161 {
2162 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
2163 "The JobQueue cannot currently perform lookups for items which do "
2164 "not have a chanid/starttime in the recorded table.");
2165 ChangeJobStatus(jobID, JOB_ERRORED, "ProgramInfo data not found");
2167 m_runningJobsLock->unlock();
2168 return;
2169 }
2170
2171 ProgramInfo *program_info = m_runningJobs[jobID].pginfo;
2172 m_runningJobsLock->unlock();
2173
2174 QString details = QString("%1 recorded from channel %3")
2175 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),
2176 program_info->toString(ProgramInfo::kRecordingKey));
2177
2179 {
2180 QString msg = QString("Metadata Lookup failed. Could not open "
2181 "new database connection for %1. "
2182 "Program cannot be looked up.")
2183 .arg(details);
2184 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2185
2186 ChangeJobStatus(jobID, JOB_ERRORED,
2187 tr("Could not open new database connection for "
2188 "metadata lookup."));
2189
2190 delete program_info;
2191 return;
2192 }
2193
2194 LOG(VB_GENERAL, LOG_INFO,
2195 LOC + "Metadata Lookup Starting for " + details);
2196
2197 uint retVal = 0;
2198 QString path;
2199 QString command;
2200
2201 path = GetAppBinDir() + "mythmetadatalookup";
2202 command = QString("%1 -j %2")
2203 .arg(path).arg(jobID);
2204 command += logPropagateArgs;
2205
2206 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2207 .arg(command));
2208
2209 GetMythDB()->GetDBManager()->CloseDatabases();
2210 retVal = myth_system(command);
2211 int priority = LOG_NOTICE;
2212 QString comment;
2213
2214 m_runningJobsLock->lock();
2215
2216 if ((retVal == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2217 (retVal == GENERIC_EXIT_CMD_NOT_FOUND))
2218 {
2219 comment = tr("Unable to find mythmetadatalookup");
2220 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2221 priority = LOG_WARNING;
2222 }
2223 else if (m_runningJobs[jobID].flag == JOB_STOP)
2224 {
2225 comment = tr("Aborted by user");
2226 ChangeJobStatus(jobID, JOB_ABORTED, comment);
2227 priority = LOG_WARNING;
2228 }
2229 else if (retVal == GENERIC_EXIT_NO_RECORDING_DATA)
2230 {
2231 comment = tr("Unable to open file or init decoder");
2232 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2233 priority = LOG_WARNING;
2234 }
2235 else if (retVal >= GENERIC_EXIT_NOT_OK) // 256 or above - error
2236 {
2237 comment = tr("Failed with exit status %1").arg(retVal);
2238 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2239 priority = LOG_WARNING;
2240 }
2241 else
2242 {
2243 comment = tr("Metadata Lookup Complete.");
2244 ChangeJobStatus(jobID, JOB_FINISHED, comment);
2245
2246 program_info->SendUpdateEvent();
2247 }
2248
2249 QString msg = tr("Metadata Lookup %1", "Job ID")
2251
2252 if (!comment.isEmpty())
2253 details += QString(" (%1)").arg(comment);
2254
2255 if (priority <= LOG_WARNING)
2256 LOG(VB_GENERAL, LOG_ERR, LOC + msg + ": " + details);
2257
2259 m_runningJobsLock->unlock();
2260}
2261
2263{
2264 auto *jts = (JobThreadStruct *)param;
2265 JobQueue *jq = jts->jq;
2266
2267 MThread::ThreadSetup(QString("Commflag_%1").arg(jts->jobID));
2268 jq->DoFlagCommercialsThread(jts->jobID);
2270
2271 delete jts;
2272
2273 return nullptr;
2274}
2275
2277{
2278 // We can't currently commflag non-recording files w/o a ProgramInfo
2279 m_runningJobsLock->lock();
2280 if (!m_runningJobs[jobID].pginfo)
2281 {
2282 LOG(VB_JOBQUEUE, LOG_ERR, LOC +
2283 "The JobQueue cannot currently commflag files that do not "
2284 "have a chanid/starttime in the recorded table.");
2285 ChangeJobStatus(jobID, JOB_ERRORED, "ProgramInfo data not found");
2287 m_runningJobsLock->unlock();
2288 return;
2289 }
2290
2291 ProgramInfo *program_info = m_runningJobs[jobID].pginfo;
2292 m_runningJobsLock->unlock();
2293
2294 QString details = QString("%1 recorded from channel %3")
2295 .arg(program_info->toString(ProgramInfo::kTitleSubtitle),
2296 program_info->toString(ProgramInfo::kRecordingKey));
2297
2299 {
2300 QString msg = QString("Commercial Detection failed. Could not open "
2301 "new database connection for %1. "
2302 "Program cannot be flagged.")
2303 .arg(details);
2304 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2305
2306 ChangeJobStatus(jobID, JOB_ERRORED,
2307 tr("Could not open new database connection for "
2308 "commercial detector."));
2309
2310 delete program_info;
2311 return;
2312 }
2313
2314 LOG(VB_GENERAL, LOG_INFO,
2315 LOC + "Commercial Detection Starting for " + details);
2316
2317 uint breaksFound = 0;
2318 QString path;
2319 QString command;
2320
2321 m_runningJobsLock->lock();
2322 if (m_runningJobs[jobID].command == "mythcommflag")
2323 {
2324 path = GetAppBinDir() + "mythcommflag";
2325 command = QString("%1 -j %2 --noprogress")
2326 .arg(path).arg(jobID);
2327 command += logPropagateArgs;
2328 }
2329 else
2330 {
2331 command = m_runningJobs[jobID].command;
2332 QStringList tokens = command.split(" ", Qt::SkipEmptyParts);
2333 if (!tokens.empty())
2334 path = tokens[0];
2335 }
2336 m_runningJobsLock->unlock();
2337
2338 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2339 .arg(command));
2340
2341 GetMythDB()->GetDBManager()->CloseDatabases();
2342 breaksFound = myth_system(command, kMSLowExitVal);
2343 int priority = LOG_NOTICE;
2344 QString comment;
2345
2346 m_runningJobsLock->lock();
2347
2348 if ((breaksFound == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2349 (breaksFound == GENERIC_EXIT_CMD_NOT_FOUND))
2350 {
2351 comment = tr("Unable to find mythcommflag");
2352 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2353 priority = LOG_WARNING;
2354 }
2355 else if (m_runningJobs[jobID].flag == JOB_STOP)
2356 {
2357 comment = tr("Aborted by user");
2358 ChangeJobStatus(jobID, JOB_ABORTED, comment);
2359 priority = LOG_WARNING;
2360 }
2361 else if (breaksFound == GENERIC_EXIT_NO_RECORDING_DATA)
2362 {
2363 comment = tr("Unable to open file or init decoder");
2364 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2365 priority = LOG_WARNING;
2366 }
2367 else if (breaksFound >= GENERIC_EXIT_NOT_OK) // 256 or above - error
2368 {
2369 comment = tr("Failed with exit status %1").arg(breaksFound);
2370 ChangeJobStatus(jobID, JOB_ERRORED, comment);
2371 priority = LOG_WARNING;
2372 }
2373 else
2374 {
2375 comment = tr("%n commercial break(s)", "", breaksFound);
2376 ChangeJobStatus(jobID, JOB_FINISHED, comment);
2377
2378 program_info->SendUpdateEvent();
2379
2380 if (!program_info->IsLocal())
2381 program_info->SetPathname(program_info->GetPlaybackURL(false,true));
2382 if (program_info->IsLocal())
2383 {
2384 auto *pg = new PreviewGenerator(program_info, QString(),
2386 pg->Run();
2387 pg->deleteLater();
2388 }
2389 }
2390
2391 QString msg = tr("Commercial Detection %1", "Job ID")
2393
2394 if (!comment.isEmpty())
2395 details += QString(" (%1)").arg(comment);
2396
2397 if (priority <= LOG_WARNING)
2398 LOG(VB_GENERAL, LOG_ERR, LOC + msg + ": " + details);
2399
2401 m_runningJobsLock->unlock();
2402}
2403
2404void *JobQueue::UserJobThread(void *param)
2405{
2406 auto *jts = (JobThreadStruct *)param;
2407 JobQueue *jq = jts->jq;
2408
2409 MThread::ThreadSetup(QString("UserJob_%1").arg(jts->jobID));
2410 jq->DoUserJobThread(jts->jobID);
2412
2413 delete jts;
2414
2415 return nullptr;
2416}
2417
2419{
2420 m_runningJobsLock->lock();
2421 ProgramInfo *pginfo = m_runningJobs[jobID].pginfo;
2422 QString jobDesc = m_runningJobs[jobID].desc;
2423 QString command = m_runningJobs[jobID].command;
2424 m_runningJobsLock->unlock();
2425
2426 ChangeJobStatus(jobID, JOB_RUNNING);
2427
2428 QString msg;
2429
2430 if (pginfo)
2431 {
2432 msg = QString("Started %1 for %2 recorded from channel %3")
2433 .arg(jobDesc,
2436 }
2437 else
2438 {
2439 msg = QString("Started %1 for jobID %2").arg(jobDesc).arg(jobID);
2440 }
2441
2442 LOG(VB_GENERAL, LOG_INFO, LOC + QString(msg.toLocal8Bit().constData()));
2443
2444 switch (m_jobQueueCPU)
2445 {
2446 case 0: myth_nice(17);
2447 myth_ioprio(8);
2448 break;
2449 case 1: myth_nice(10);
2450 myth_ioprio(7);
2451 break;
2452 case 2:
2453 default: break;
2454 }
2455
2456 LOG(VB_JOBQUEUE, LOG_INFO, LOC + QString("Running command: '%1'")
2457 .arg(command));
2458 GetMythDB()->GetDBManager()->CloseDatabases();
2459 uint result = myth_system(command);
2460
2461 if ((result == GENERIC_EXIT_DAEMONIZING_ERROR) ||
2462 (result == GENERIC_EXIT_CMD_NOT_FOUND))
2463 {
2464 msg = QString("User Job '%1' failed, unable to find "
2465 "executable, check your PATH and backend logs.")
2466 .arg(command);
2467 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2468 LOG(VB_GENERAL, LOG_NOTICE, LOC + QString("Current PATH: '%1'")
2469 .arg(qEnvironmentVariable("PATH")));
2470
2471 ChangeJobStatus(jobID, JOB_ERRORED,
2472 tr("ERROR: Unable to find executable, check backend logs."));
2473 }
2474 else if (result != 0)
2475 {
2476 msg = QString("User Job '%1' failed.").arg(command);
2477 LOG(VB_GENERAL, LOG_ERR, LOC + msg);
2478
2479 ChangeJobStatus(jobID, JOB_ERRORED,
2480 tr("ERROR: User Job returned non-zero, check logs."));
2481 }
2482 else
2483 {
2484 if (pginfo)
2485 {
2486 msg = QString("Finished %1 for %2 recorded from channel %3")
2487 .arg(jobDesc,
2490 }
2491 else
2492 {
2493 msg = QString("Finished %1 for jobID %2").arg(jobDesc).arg(jobID);
2494 }
2495
2496 LOG(VB_GENERAL, LOG_INFO, LOC + QString(msg.toLocal8Bit().constData()));
2497
2498 ChangeJobStatus(jobID, JOB_FINISHED, tr("Successfully Completed."));
2499
2500 if (pginfo)
2501 pginfo->SendUpdateEvent();
2502 }
2503
2505}
2506
2508{
2509 if (jobType & JOB_USERJOB)
2510 {
2511 int x = ((jobType & JOB_USERJOB)>> 8);
2512 int bits = 1;
2513 while ((x != 0) && ((x & 0x01) == 0))
2514 {
2515 bits++;
2516 x = x >> 1;
2517 }
2518 if ( bits > 4 )
2519 return JOB_NONE;
2520
2521 return bits;
2522 }
2523 return JOB_NONE;
2524}
2525
2526/* vim: set expandtab tabstop=4 shiftwidth=4: */
static bool QueueRecordingJobs(const RecordingInfo &recinfo, int jobTypes=JOB_NONE)
Definition: jobqueue.cpp:493
QMap< int, RunningJobInfo > m_runningJobs
Definition: jobqueue.h:266
static bool ChangeJobHost(int jobID, const QString &newHostname)
Definition: jobqueue.cpp:1408
static bool ChangeJobFlags(int jobID, int newFlags)
Definition: jobqueue.cpp:969
static void RecoverQueue(bool justOld=false)
Definition: jobqueue.cpp:1579
static QString GetJobCommand(int id, int jobType, ProgramInfo *tmpInfo)
Definition: jobqueue.cpp:1814
static bool RestartJob(int jobID)
Definition: jobqueue.cpp:740
static bool SafeDeleteJob(int jobID, int jobType, int chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:884
static bool ChangeJobCmds(int jobID, int newCmds)
Definition: jobqueue.cpp:926
static QString GetJobArgs(int jobID)
Definition: jobqueue.cpp:1493
QWaitCondition m_queueThreadCond
Definition: jobqueue.h:271
static void CleanupOldJobsInQueue()
Definition: jobqueue.cpp:1642
void DoUserJobThread(int jobID)
Definition: jobqueue.cpp:2418
bool AllowedToRun(const JobQueueEntry &job)
Definition: jobqueue.cpp:1440
static bool GetJobInfoFromID(int jobID, int &jobType, uint &chanid, QDateTime &recstartts)
Definition: jobqueue.cpp:672
static void * TranscodeThread(void *param)
Definition: jobqueue.cpp:1916
static void * UserJobThread(void *param)
Definition: jobqueue.cpp:2404
static bool InJobRunWindow(QDateTime jobstarttsRaw)
Definition: jobqueue.cpp:1667
static int GetJobsInQueue(QMap< int, JobQueueEntry > &jobs, int findJobs=JOB_LIST_NOT_DONE)
Definition: jobqueue.cpp:1289
void DoFlagCommercialsThread(int jobID)
Definition: jobqueue.cpp:2276
static enum JobFlags GetJobFlags(int jobID)
Definition: jobqueue.cpp:1514
static QString JobText(int jobType)
Definition: jobqueue.cpp:1119
QRecursiveMutex * m_runningJobsLock
Definition: jobqueue.h:265
static bool ChangeJobArgs(int jobID, const QString &args="")
Definition: jobqueue.cpp:1042
static enum JobCmds GetJobCmd(int jobID)
Definition: jobqueue.cpp:1472
int m_jobQueueCPU
Definition: jobqueue.h:258
MThread * m_queueThread
Definition: jobqueue.h:270
static bool DeleteAllJobs(uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:758
static bool DeleteJob(int jobID)
Definition: jobqueue.cpp:879
int m_jobsRunning
Definition: jobqueue.h:257
static bool QueueJob(int jobType, uint chanid, const QDateTime &recstartts, const QString &args="", const QString &comment="", QString host="", int flags=0, int status=JOB_QUEUED, QDateTime schedruntime=QDateTime())
Definition: jobqueue.cpp:515
static bool IsJobStatusQueued(int status)
Definition: jobqueue.cpp:1082
static int GetJobID(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:651
void StartChildJob(void *(*ChildThreadRoutine)(void *), int jobID)
Definition: jobqueue.cpp:1785
static bool StopJob(int jobID)
Definition: jobqueue.cpp:749
QMutex m_queueThreadCondLock
Definition: jobqueue.h:272
static bool ResumeJob(int jobID)
Definition: jobqueue.cpp:731
static bool ChangeJobComment(int jobID, const QString &comment="")
Definition: jobqueue.cpp:1017
void ProcessJob(const JobQueueEntry &job)
Definition: jobqueue.cpp:1697
static bool IsJobQueuedOrRunning(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1105
static void * FlagCommercialsThread(void *param)
Definition: jobqueue.cpp:2262
void ProcessQueue(void)
Definition: jobqueue.cpp:166
static bool IsJobRunning(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1093
static QString GetJobDescription(int jobType)
Definition: jobqueue.cpp:1799
static bool ChangeJobStatus(int jobID, int newStatus, const QString &comment="")
Definition: jobqueue.cpp:990
static void * MetadataLookupThread(void *param)
Definition: jobqueue.cpp:2142
static QString PrettyPrint(off_t bytes)
Definition: jobqueue.cpp:1883
static enum JobStatus GetJobStatus(int jobID)
Definition: jobqueue.cpp:1535
bool m_processQueue
Definition: jobqueue.h:273
void RemoveRunningJob(int id)
Definition: jobqueue.cpp:1864
void run(void) override
Definition: jobqueue.cpp:151
int GetRunningJobID(uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1064
static bool PauseJob(int jobID)
Definition: jobqueue.cpp:722
static bool IsJobQueued(int jobType, uint chanid, const QDateTime &recstartts)
Definition: jobqueue.cpp:1113
void DoMetadataLookupThread(int jobID)
Definition: jobqueue.cpp:2156
void DoTranscodeThread(int jobID)
Definition: jobqueue.cpp:1930
static bool QueueJobs(int jobTypes, uint chanid, const QDateTime &recstartts, const QString &args="", const QString &comment="", const QString &host="")
Definition: jobqueue.cpp:599
static QString StatusText(int status)
Definition: jobqueue.cpp:1142
~JobQueue(void) override
Definition: jobqueue.cpp:69
static int GetJobTypeFromName(const QString &name)
Definition: jobqueue.cpp:711
QString m_hostname
Definition: jobqueue.h:255
JobQueue(bool master)
Definition: jobqueue.cpp:47
static bool HasRunningOrPendingJobs(std::chrono::minutes startingWithinMins=0min)
Definition: jobqueue.cpp:1237
static int UserJobTypeToIndex(int JobType)
Definition: jobqueue.cpp:2507
static bool IsJobStatusRunning(int status)
Definition: jobqueue.cpp:1087
void customEvent(QEvent *e) override
Definition: jobqueue.cpp:85
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:128
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:838
QVariant value(int i) const
Definition: mythdbcon.h:204
int size(void) const
Definition: mythdbcon.h:214
static bool testDBConnection()
Checks DB connection + login (login info via Mythcontext)
Definition: mythdbcon.cpp:877
int numRowsAffected() const
Definition: mythdbcon.h:217
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:619
void bindValue(const QString &placeholder, const QVariant &val)
Add a single binding.
Definition: mythdbcon.cpp:889
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:813
static MSqlQueryInfo InitCon(ConnectionReuse _reuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:551
This is a wrapper around QThread that does several additional things.
Definition: mthread.h:49
static void ThreadCleanup(void)
This is to be called on exit in those few threads that haven't been ported to MThread.
Definition: mthread.cpp:224
void start(QThread::Priority p=QThread::InheritPriority)
Tell MThread to start running the thread in the near future.
Definition: mthread.cpp:281
bool wait(std::chrono::milliseconds time=std::chrono::milliseconds::max())
Wait for the MThread to exit, with a maximum timeout.
Definition: mthread.cpp:298
static void ThreadSetup(const QString &name)
This is to be called on startup in those few threads that haven't been ported to MThread.
Definition: mthread.cpp:219
QString GetHostName(void)
QString GetSetting(const QString &key, const QString &defaultval="")
QString GetSettingOnHost(const QString &key, const QString &host, const QString &defaultval="")
void BlockShutdown(void)
void dispatch(const MythEvent &event)
int GetNumSetting(const QString &key, int defaultval=0)
bool IsBlockingClient(void) const
is this client blocking shutdown
void AllowShutdown(void)
std::enable_if_t< std::chrono::__is_duration< T >::value, T > GetDurSetting(const QString &key, T defaultval=T::zero())
bool GetBoolSetting(const QString &key, bool defaultval=false)
static void DBError(const QString &where, const MSqlQuery &query)
Definition: mythdb.cpp:225
This class is used as a container for messages.
Definition: mythevent.h:17
const QString & Message() const
Definition: mythevent.h:65
static const Type kMythEventMessage
Definition: mythevent.h:79
void addListener(QObject *listener)
Add a listener to the observable.
void removeListener(QObject *listener)
Remove a listener to the observable.
This class creates a preview image of a recording.
Holds information on recordings and videos.
Definition: programinfo.h:74
uint GetChanID(void) const
This is the unique key used in the database to locate tuning information.
Definition: programinfo.h:380
void SaveTranscodeStatus(TranscodingStatus trans)
Set "transcoded" field in "recorded" table to "trans".
QString toString(Verbosity v=kLongDescription, const QString &sep=":", const QString &grp="\"") const
virtual void SaveFilesize(uint64_t fsize)
Sets recording file size in database, and sets "filesize" field.
QString GetRecordingGroup(void) const
Definition: programinfo.h:427
uint QueryTranscoderID(void) const
bool HasCutlist(void) const
Definition: programinfo.h:491
QString GetHostname(void) const
Definition: programinfo.h:429
void MarkAsInUse(bool inuse, const QString &usedFor="")
Tracks a recording's in use status, to prevent deletion and to allow the storage scheduler to perform...
QDateTime GetRecordingStartTime(void) const
Approximate time the recording started.
Definition: programinfo.h:412
bool IsLocal(void) const
Definition: programinfo.h:358
bool Reload(void)
bool IsCommercialFree(void) const
Definition: programinfo.h:489
virtual void SubstituteMatches(QString &str)
Subsitute MATCH% type variable names in the given string.
QString GetPlaybackURL(bool checkMaster=false, bool forceCheckLocal=false)
Returns filename or URL to be used to play back this recording.
void SendUpdateEvent(void) const
Sends event out that the ProgramInfo should be reloaded.
void SetPathname(const QString &pn)
Holds information on a TV Program one might wish to record.
Definition: recordinginfo.h:36
int GetAutoRunJobs(void) const
Returns a bitmap of which jobs are attached to this RecordingInfo.
static const uint kTranscoderAutodetect
sentinel value
unsigned int uint
Definition: compat.h:60
@ GENERIC_EXIT_RESTART
Need to restart transcoding.
Definition: exitcodes.h:34
@ GENERIC_EXIT_CMD_NOT_FOUND
Command not found.
Definition: exitcodes.h:15
@ GENERIC_EXIT_DAEMONIZING_ERROR
Error daemonizing or execl.
Definition: exitcodes.h:31
@ GENERIC_EXIT_NO_RECORDING_DATA
No program/recording data.
Definition: exitcodes.h:32
@ GENERIC_EXIT_NOT_OK
Exited with error.
Definition: exitcodes.h:14
#define LOC
Definition: jobqueue.cpp:42
static constexpr int64_t kRecentInterval
Definition: jobqueue.cpp:45
#define PTHREAD_NULL
Definition: jobqueue.cpp:15
#define JOBSTATUS_STATUSTEXT(A, B, C)
Definition: jobqueue.cpp:1140
@ JOB_LIST_DONE
Definition: jobqueue.h:68
@ JOB_LIST_ALL
Definition: jobqueue.h:67
@ JOB_LIST_RECENT
Definition: jobqueue.h:71
@ JOB_LIST_NOT_DONE
Definition: jobqueue.h:69
@ JOB_LIST_ERROR
Definition: jobqueue.h:70
#define JOBSTATUS_MAP(F)
Definition: jobqueue.h:25
@ JOB_USERJOB3
Definition: jobqueue.h:86
@ JOB_METADATA
Definition: jobqueue.h:80
@ JOB_USERJOB1
Definition: jobqueue.h:84
@ JOB_USERJOB
Definition: jobqueue.h:83
@ JOB_USERJOB2
Definition: jobqueue.h:85
@ JOB_PREVIEW
Definition: jobqueue.h:81
@ JOB_NONE
Definition: jobqueue.h:75
@ JOB_COMMFLAG
Definition: jobqueue.h:79
@ JOB_USERJOB4
Definition: jobqueue.h:87
@ JOB_TRANSCODE
Definition: jobqueue.h:78
static QMap< QString, int > JobNameToType
Definition: jobqueue.h:90
JobStatus
Definition: jobqueue.h:44
JobCmds
Definition: jobqueue.h:50
@ JOB_STOP
Definition: jobqueue.h:54
@ JOB_RESTART
Definition: jobqueue.h:55
@ JOB_RESUME
Definition: jobqueue.h:53
@ JOB_RUN
Definition: jobqueue.h:51
@ JOB_PAUSE
Definition: jobqueue.h:52
JobFlags
Definition: jobqueue.h:58
@ JOB_USE_CUTLIST
Definition: jobqueue.h:60
@ JOB_NO_FLAGS
Definition: jobqueue.h:59
uint64_t verboseMask
Definition: logging.cpp:101
QString logPropagateArgs
Definition: logging.cpp:86
static constexpr const char * MYTH_APPNAME_MYTHJOBQUEUE
Definition: mythappname.h:5
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
MythDB * GetMythDB(void)
Definition: mythdb.cpp:50
QString GetAppBinDir(void)
Definition: mythdirs.cpp:282
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
bool myth_nice(int val)
bool myth_ioprio(int)
Allows setting the I/O priority of the current process/thread.
@ kMSLowExitVal
allow exit values 0-127 only
Definition: mythsystem.h:47
uint myth_system(const QString &command, uint flags, std::chrono::seconds timeout)
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:28
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kFilename
Default UTC, "yyyyMMddhhmmss".
Definition: mythdate.h:18
@ ISODate
Default UTC.
Definition: mythdate.h:17
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
string hostname
Definition: caa.py:17
const QString kJobQueueInUseID
@ TRANSCODING_COMPLETE
Definition: programtypes.h:158
@ TRANSCODING_RUNNING
Definition: programtypes.h:159
@ TRANSCODING_NOT_TRANSCODED
Definition: programtypes.h:157
QDateTime schedruntime
Definition: jobqueue.h:104
QString hostname
Definition: jobqueue.h:112
QDateTime statustime
Definition: jobqueue.h:111
QString comment
Definition: jobqueue.h:114
QDateTime recstartts
Definition: jobqueue.h:103
QString args
Definition: jobqueue.h:113
QString startts
Definition: jobqueue.h:105
QDateTime inserttime
Definition: jobqueue.h:106
ProgramInfo * pginfo
Definition: jobqueue.h:123
QString command
Definition: jobqueue.h:122
QString desc
Definition: jobqueue.h:121