27 #include <QCoreApplication>
28 #include <QElapsedTimer>
44 : m_recCommand(std::move(command))
45 , m_logFile(std::move(log_file))
46 , m_logging(std::move(logging))
47 , m_configIni(std::move(conf_file))
54 LOG(VB_CHANNEL, LOG_INFO,
LOC +
55 QString(
"Channels in '%1', Tuner: '%2', Scanner: '%3'")
59 m_desc.replace(
"%URL%",
"");
60 m_desc.replace(
"%CHANNUM%",
"");
61 m_desc.replace(
"%CHANNAME%",
"");
62 m_desc.replace(
"%CALLSIGN%",
"");
77 QString cleaned = var;
79 while ((
p1 = cleaned.indexOf(
'{')) != -1)
81 p2 = cleaned.indexOf(
'}',
p1);
82 if (cleaned.mid(
p1,
p2 -
p1).indexOf(
'%') == -1)
85 cleaned = cleaned.remove(
p2, 1);
86 cleaned = cleaned.remove(
p1, 1);
91 cleaned = cleaned.remove(
p1,
p2 -
p1 + 1);
95 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Sanitized: '%1' -> '%2'")
102 const QVariantMap & extra_args)
const
104 QString result = var;
108 for (
auto it = extra_args.keyValueBegin();
109 it != extra_args.keyValueEnd(); ++it)
111 if (it->first ==
"command")
113 result.replace(QString(
"\%%1\%").arg(it->first.toUpper()),
114 it->second.toString());
115 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
116 QString(
"Replaced '%1' with '%2'")
117 .arg(it->first.toUpper(), it->second.toString()));
126 QMap<QString, QString>::const_iterator Ivar;
130 QString repl =
"%" + Ivar.key() +
"%";
131 if (cmd.indexOf(repl) >= 0)
133 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Replacing '%1' with '%2'")
134 .arg(repl, Ivar.value()));
135 cmd.replace(repl, Ivar.value());
139 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Did not find '%1' in '%2'")
149 if (
m_proc.processId() > 0)
150 extra = QString(
"(pid %1) ").arg(
m_proc.processId());
155 return QString(
"%1%2 ").arg(extra, desc);
164 m_fatalMsg = QString(
"ERR:Config file '%1' does not exist "
166 .arg(conf_info.fileName(),
167 conf_info.absolutePath());
173 QSettings settings(
m_configIni, QSettings::IniFormat);
175 if (settings.childGroups().contains(
"VARIABLES"))
177 LOG(VB_CHANNEL, LOG_DEBUG,
"Parsing variables");
178 settings.beginGroup(
"VARIABLES");
180 QStringList childKeys = settings.childKeys();
181 for (
const QString & var : std::as_const(childKeys))
184 LOG(VB_CHANNEL, LOG_INFO, QString(
"%1=%2")
185 .arg(var, settings.value(var).toString()));
190 QMap<QString, QString>::iterator Ivar;
191 QMap<QString, QString>::iterator Ivar2;
195 QString repl =
"%" + Ivar.key() +
"%";
199 if ((*Ivar2).indexOf(repl) >= 0)
201 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Replacing '%1' with '%2'")
202 .arg(repl, Ivar.value()));
203 (*Ivar2).replace(repl, Ivar.value());
210 LOG(VB_CHANNEL, LOG_DEBUG,
"No VARIABLES section");
213 if (!settings.contains(
"RECORDER/command"))
215 m_fatalMsg = QString(
"ERR:Config file %1 file missing "
216 "[RECORDER]/command")
217 .arg(conf_info.absolutePath());
223 m_recCommand = settings.value(
"RECORDER/command").toString();
224 m_recDesc = settings.value(
"RECORDER/desc").toString();
225 m_cleanup = settings.value(
"RECORDER/cleanup").toString();
226 m_tuneCommand = settings.value(
"TUNER/command",
"").toString();
228 m_onDataStart = settings.value(
"TUNER/ondatastart",
"").toString();
229 m_channelsIni = settings.value(
"TUNER/channels",
"").toString();
231 m_scanCommand = settings.value(
"SCANNER/command",
"").toString();
232 m_scanTimeout = settings.value(
"SCANNER/timeout",
"").toInt();
242 settings.beginGroup(
"ENVIRONMENT");
245 QStringList keys = settings.childKeys();
246 QStringList::const_iterator Ienv;
247 for (Ienv = keys.constBegin(); Ienv != keys.constEnd(); ++Ienv)
249 if (!(*Ienv).isEmpty() && (*Ienv)[0] !=
'#')
250 m_appEnv.insert((*Ienv).toLocal8Bit().constData(),
251 settings.value(*Ienv).toString());
260 QDir chan_path = QFileInfo(
m_configIni).absolutePath();
279 LOG(VB_RECORD, LOG_ERR,
LOC +
": No recorder provided.");
280 emit
SendMessage(
"Open",
"0",
"No recorder provided.",
"ERR");
286 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
287 QMap<QString, QString>::const_iterator Ienv;
289 Ienv !=
m_appEnv.constEnd(); ++Ienv)
291 env.insert(Ienv.key(), Ienv.value());
292 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
" ENV: '%1' = '%2'")
293 .arg(Ienv.key(), Ienv.value()));
295 m_proc.setProcessEnvironment(env);
298 QObject::connect(&
m_proc, &QProcess::started,
this,
301 QObject::connect(&
m_proc, &QProcess::readyReadStandardOutput,
this,
304 QObject::connect(&
m_proc, &QProcess::readyReadStandardError,
this,
307 qRegisterMetaType<QProcess::ProcessError>(
"QProcess::ProcessError");
308 QObject::connect(&
m_proc, &QProcess::errorOccurred,
311 qRegisterMetaType<QProcess::ExitStatus>(
"QProcess::ExitStatus");
313 static_cast<void (QProcess::*)
314 (
int,QProcess::ExitStatus exitStatus)
>
315 (&QProcess::finished),
318 qRegisterMetaType<QProcess::ProcessState>(
"QProcess::ProcessState");
319 QObject::connect(&
m_proc, &QProcess::stateChanged,
this,
322 LOG(VB_RECORD, LOG_INFO,
LOC +
": Opened");
331 if (proc.state() == QProcess::Running)
333 LOG(VB_RECORD, LOG_INFO,
LOC +
334 QString(
"Sending SIGTERM to %1(%2)").arg(desc).arg(proc.processId()));
336 proc.waitForFinished();
338 if (proc.state() == QProcess::Running)
340 LOG(VB_RECORD, LOG_INFO,
LOC +
341 QString(
"Sending SIGKILL to %1(%2)").arg(desc).arg(proc.processId()));
343 proc.waitForFinished();
352 LOG(VB_RECORD, LOG_INFO,
LOC +
": Closing application.");
355 std::this_thread::sleep_for(50us);
360 m_tuneProc.closeReadChannel(QProcess::StandardOutput);
364 if (
m_proc.state() == QProcess::Running)
366 m_proc.closeReadChannel(QProcess::StandardOutput);
368 std::this_thread::sleep_for(50us);
382 m_runCond.wait_for(lk, std::chrono::milliseconds(10));
385 if (
m_proc.state() == QProcess::Running)
387 if (
m_proc.waitForReadyRead(50))
395 qApp->processEvents();
398 if (
m_proc.state() == QProcess::Running)
400 m_proc.closeReadChannel(QProcess::StandardOutput);
415 QString cmd =
args.takeFirst();
417 LOG(VB_RECORD, LOG_WARNING,
LOC +
418 QString(
" Beginning cleanup: '%1'").arg(cmd));
426 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start cleanup process: "
431 if (
cleanup.state() == QProcess::NotRunning)
433 if (
cleanup.exitStatus() != QProcess::NormalExit)
435 LOG(VB_RECORD, LOG_ERR,
LOC +
": Cleanup process failed: " +
ENO);
440 LOG(VB_RECORD, LOG_INFO,
LOC +
": Cleanup finished.");
445 LOG(VB_RECORD, LOG_INFO,
LOC +
"DataStarted");
454 QString cmd = settings.value(
"ONSTART").toString();
458 LOG(VB_CHANNEL, LOG_INFO,
LOC +
459 QString(
": Using ONSTART cmd from '%1': '%2'")
467 if (startcmd.isEmpty())
471 bool background =
false;
472 int pos = startcmd.lastIndexOf(QChar(
'&'));
476 startcmd = startcmd.left(pos);
480 startcmd =
args.takeFirst();
484 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Finishing tune: '%1' %3")
485 .arg(startcmd, background ?
"in the background" :
""));
490 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to finish tune process: "
502 LOG(VB_RECORD, LOG_ERR,
LOC +
": Finish tune failed: " +
ENO);
508 LOG(VB_RECORD, LOG_INFO,
LOC +
": tunning finished.");
515 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
516 emit
SendMessage(
"LoadChannels", serial,
"No channels configured.",
"ERR");
526 cmd =
args.takeFirst();
529 scanner.start(cmd,
args);
531 if (!scanner.waitForStarted())
533 QString errmsg = QString(
"Failed to start '%1': ").arg(cmd) +
ENO;
534 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
535 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
545 if (scanner.waitForReadyRead(50))
547 buf = scanner.readLine();
550 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + buf);
555 if (scanner.state() != QProcess::Running)
558 if (scanner.waitForFinished(50 ))
563 QString errmsg = QString(
"Timedout waiting for '%1'").arg(cmd);
564 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
565 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
582 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
583 emit
SendMessage(
"FirstChannel", serial,
"No channels configured.",
"ERR");
589 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": Invalid channel configuration.");
590 emit
SendMessage(func, serial,
"Invalid channel configuration.",
"ERR");
596 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": No more channels.");
597 emit
SendMessage(func, serial,
"No more channels",
"ERR");
612 LOG(VB_CHANNEL, LOG_INFO,
LOC +
613 QString(
": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4")
614 .arg(name, callsign, xmltvid, icon));
616 emit
SendMessage(func, serial, QString(
"%1,%2,%3,%4,%5")
617 .arg(channum, name, callsign,
618 xmltvid, icon),
"OK");
635 int pos = cmd.lastIndexOf(QChar(
'&'));
636 bool background =
false;
647 cmd =
args.takeFirst();
649 LOG(VB_RECORD, LOG_WARNING,
LOC +
650 QString(
" New episode starting on current channel: '%1'").arg(cmd));
658 LOG(VB_RECORD, LOG_ERR,
LOC +
659 " NewEpisodeStarting: Failed to start process: " +
ENO);
664 LOG(VB_RECORD, LOG_INFO,
LOC +
665 "NewEpisodeStarting: running in background.");
670 if (
m_tuneProc.state() == QProcess::NotRunning)
672 if (
m_tuneProc.exitStatus() != QProcess::NormalExit)
674 LOG(VB_RECORD, LOG_ERR,
LOC +
675 " NewEpisodeStarting: process failed: " +
ENO);
679 LOG(VB_RECORD, LOG_INFO,
LOC +
"NewEpisodeStarting: finished.");
683 const QVariantMap & chaninfo)
689 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No 'tuner' configured.");
690 emit
SendMessage(
"TuneChannel", serial,
"No 'tuner' configured.",
"ERR");
694 QString channum =
m_chaninfo[
"channum"].toString();
701 LOG(VB_CHANNEL, LOG_INFO,
LOC +
702 QString(
"TuneChannel: Already on %1").arg(channum));
704 QString(
"Tunned to %1").arg(channum),
"OK");
713 bool background =
false;
718 settings.beginGroup(channum);
720 QString cmd = settings.value(
"TUNE").toString();
724 LOG(VB_CHANNEL, LOG_INFO,
LOC +
725 QString(
": Using tune cmd from '%1': '%2'")
730 url = settings.value(
"URL").toString();
733 if (tunecmd.indexOf(
"%URL%") >= 0)
735 tunecmd.replace(
"%URL%", url);
736 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
737 QString(
": '%URL%' replaced with '%1' in tunecmd: '%2'")
744 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
745 QString(
": '%URL%' replaced with '%1' in cmd: '%2'")
750 m_desc.replace(
"%CHANNAME%", settings.value(
"NAME").toString());
751 m_desc.replace(
"%CALLSIGN%", settings.value(
"CALLSIGN").toString());
759 int pos = tunecmd.lastIndexOf(QChar(
'&'));
763 tunecmd = tunecmd.left(pos);
771 LOG(VB_RECORD, LOG_DEBUG,
LOC +
772 QString(
": '%LOGFILE%' replaced with '%1' in cmd: '%2'")
779 LOG(VB_RECORD, LOG_DEBUG,
LOC +
780 QString(
": '%LOGGING%' replaced with '%1' in cmd: '%2'")
784 m_desc.replace(
"%URL%", url);
786 if (!tunecmd.isEmpty())
789 QString cmd =
args.takeFirst();
794 QString errmsg = QString(
"Tune `%1` failed: ").arg(tunecmd) +
ENO;
795 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
796 emit
SendMessage(
"TuneChannel", serial, errmsg,
"ERR");
802 LOG(VB_CHANNEL, LOG_INFO,
LOC +
803 QString(
": Started in background `%1` URL '%2'")
814 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
": Started `%1` URL '%2'")
817 QString(
"InProgress `%1`").arg(tunecmd),
"OK");
833 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
834 QString(
": Tune process(%1) still running").arg(
m_tuneProc.processId()));
835 emit
SendMessage(
"TuneStatus", serial,
"InProgress",
"OK");
840 m_tuneProc.exitStatus() != QProcess::NormalExit)
842 QString errmsg = QString(
"'%1' failed: ")
844 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
845 emit
SendMessage(
"TuneStatus", serial, errmsg,
"WARN");
862 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
863 "Cannot read LockTimeout from config file.");
864 emit
SendMessage(
"LockTimeout", serial,
"Not open",
"ERR");
870 LOG(VB_CHANNEL, LOG_INFO,
LOC +
871 QString(
"Using configured LockTimeout of %1").arg(
m_lockTimeout));
875 LOG(VB_CHANNEL, LOG_INFO,
LOC +
876 "No LockTimeout defined in config, defaulting to 12000ms");
889 emit
SendMessage(
"HasPictureAttributes", serial,
"No",
"OK");
895 emit
SendMessage(
"BlockSize", serial, QString(
"Blocksize %1").arg(blksz),
"OK");
903 LOG(VB_RECORD, LOG_ERR,
LOC +
": No channel has been tuned");
905 "No channel has been tuned",
"ERR");
909 if (
m_proc.state() == QProcess::Running)
911 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application already running");
913 "Application already running",
"WARN");
920 QString cmd =
args.takeFirst();
921 m_proc.start(cmd,
args, QIODevice::ReadOnly|QIODevice::Unbuffered);
922 m_proc.setTextModeEnabled(
false);
923 m_proc.setReadChannel(QProcess::StandardOutput);
925 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Starting process '%1' args: '%2'")
928 if (!
m_proc.waitForStarted())
930 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start application.");
932 "Failed to start application.",
"ERR");
936 std::this_thread::sleep_for(50ms);
938 if (
m_proc.state() != QProcess::Running)
940 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application failed to start");
942 "Application failed to start",
"ERR");
946 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Started process '%1' PID %2")
951 emit
SendMessage(
"StartStreaming", serial,
"Streaming Started",
"OK");
957 if (
m_proc.state() == QProcess::Running)
961 LOG(VB_RECORD, LOG_INFO,
LOC +
": External application terminated.");
963 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"STATUS");
965 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"OK");
972 "Already not Streaming",
"INFO");
977 "Already not Streaming",
"WARN");
987 QString msg = QString(
"Process '%1' started").arg(
m_proc.program());
988 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
993 QProcess::ExitStatus exitStatus)
996 QString msg = QString(
"%1Finished: %2 (exit code: %3)")
997 .arg(exitStatus != QProcess::NormalExit ?
"WARN:" :
"",
998 exitStatus == QProcess::NormalExit ?
"OK" :
"Abnormal exit",
1000 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
1009 bool unexpected =
false;
1010 QString msg =
"State Changed: ";
1013 case QProcess::NotRunning:
1014 msg +=
"Not running";
1017 case QProcess::Starting:
1020 case QProcess::Running:
1021 msg += QString(
"Running PID %1").arg(
m_proc.processId());
1025 LOG(VB_RECORD, LOG_INFO,
LOC + msg);
1029 emit
SendMessage(
"STATUS",
"0",
"Unexpected: " + msg,
"ERR");
1037 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": %1")
1038 .arg(
m_proc.errorString()));
1043 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
": Error: %1")
1044 .arg(
m_proc.errorString()));
1051 QByteArray buf =
m_proc.readAllStandardError();
1052 QString msg = QString::fromUtf8(buf).trimmed();
1053 QList<QString> msgs = msg.split(
'\n');
1056 for (
int idx=0; idx < msgs.count(); ++idx)
1059 if (!msgs[idx].isEmpty())
1061 QStringList tokens = QString(msgs[idx])
1062 .split(
':', Qt::SkipEmptyParts);
1063 tokens.removeFirst();
1065 message = msgs[idx];
1067 message = tokens.join(
':');
1068 if (msgs[idx].startsWith(
"err", Qt::CaseInsensitive))
1070 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
">>> %1").arg(msgs[idx]));
1073 else if (msgs[idx].startsWith(
"warn", Qt::CaseInsensitive))
1075 LOG(VB_RECORD, LOG_WARNING,
LOC + QString(
">>> %1").arg(msgs[idx]));
1080 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
">>> %1").arg(msgs[idx]));
1089 LOG(VB_RECORD, LOG_WARNING,
LOC +
": Data ready.");