27#include <QCoreApplication>
28#include <QElapsedTimer>
45 : m_recCommand(std::move(command))
46 , m_logFile(std::move(log_file))
47 , m_logging(std::move(logging))
48 , m_configIni(std::move(conf_file))
55 LOG(VB_CHANNEL, LOG_INFO,
LOC +
56 QString(
"Channels in '%1', Tuner: '%2', Scanner: '%3'")
60 m_desc.replace(
"%URL%",
"");
61 m_desc.replace(
"%CHANNUM%",
"");
62 m_desc.replace(
"%CHANNAME%",
"");
63 m_desc.replace(
"%CALLSIGN%",
"");
78 QString cleaned = var;
80 while ((
p1 = cleaned.indexOf(
"[{")) != -1)
82 p2 = cleaned.indexOf(
"}]",
p1);
84#
if QT_VERSION < QT_VERSION_CHECK(6,0,0)
85 cleaned.midRef(
p1,
p2 -
p1).indexOf(
'%') == -1
87 QStringView(cleaned).mid(
p1,
p2 -
p1).indexOf(
'%') == -1
92 cleaned = cleaned.remove(
p2, 2);
93 cleaned = cleaned.remove(
p1, 2);
98 cleaned = cleaned.remove(
p1,
p2 -
p1 + 2);
102 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Sanitized: '%1' -> '%2'")
113 LOG(VB_CHANNEL, LOG_DEBUG,
114 QString(
"Replacing variables in '%1'").arg(cmd));
115 QString result = cmd;
117 QMap<QString, QString>::const_iterator Ivar;
121 LOG(VB_CHANNEL, LOG_DEBUG,
122 QString(
"Looking for '%1'").arg(Ivar.key()));
124 QString repl =
"%" + Ivar.key() +
"%";
125 if (result.indexOf(repl) >= 0)
127 result.replace(repl, Ivar.value());
128 LOG(VB_CHANNEL, LOG_DEBUG,
129 QString(
"Replacing '%1' with '%2'")
130 .arg(repl, Ivar.value()));
134 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Did not find '%1' in '%2'")
145 if (
m_proc.processId() > 0)
146 extra = QString(
"(pid %1) ").arg(
m_proc.processId());
151 return QString(
"%1%2 ").arg(extra, desc);
157 QMap<QString, QString>::iterator Ivar;
158 QMap<QString, QString>::iterator Ivar2;
165 if (it->first ==
"command")
175 repl =
"%" + Ivar.key() +
"%";
179 if ((*Ivar2).indexOf(repl) >= 0)
181 (*Ivar2).replace(repl, Ivar.value());
182 LOG(VB_CHANNEL, LOG_DEBUG,
183 QString(
"Replacing '%1' with '%2' in '%3'")
184 .arg(repl, Ivar.value(), *Ivar2));
191 LOG(VB_RECORD, LOG_DEBUG,
"All Variables:");
195 LOG(VB_RECORD, LOG_DEBUG,
196 QString(
"'%1' = '%2'").arg(Ivar.key(), Ivar.value()));
207 m_fatalMsg = QString(
"ERR:Config file '%1' does not exist "
209 .arg(conf_info.fileName(),
210 conf_info.absolutePath());
216 QSettings settings(
m_configIni, QSettings::IniFormat);
218 if (settings.childGroups().contains(
"VARIABLES"))
220 LOG(VB_CHANNEL, LOG_DEBUG,
"Parsing variables");
221 settings.beginGroup(
"VARIABLES");
223 QStringList childKeys = settings.childKeys();
224 for (
const QString & var : std::as_const(childKeys))
227 LOG(VB_CHANNEL, LOG_INFO, QString(
"%1=%2")
228 .arg(var, settings.value(var).toString()));
235 if (!settings.contains(
"RECORDER/command"))
237 m_fatalMsg = QString(
"ERR:Config file %1 file missing "
238 "[RECORDER]/command")
239 .arg(conf_info.absolutePath());
245 m_recCommand = settings.value(
"RECORDER/command").toString();
246 m_recDesc = settings.value(
"RECORDER/desc").toString();
247 m_cleanup = settings.value(
"RECORDER/cleanup").toString();
248 m_tuneCommand = settings.value(
"TUNER/command",
"").toString();
250 m_onDataStart = settings.value(
"TUNER/ondatastart",
"").toString();
251 m_channelsIni = settings.value(
"TUNER/channels",
"").toString();
253 m_scanCommand = settings.value(
"SCANNER/command",
"").toString();
254 m_scanTimeout = settings.value(
"SCANNER/timeout",
"").toInt();
264 settings.beginGroup(
"ENVIRONMENT");
267 QStringList keys = settings.childKeys();
268 QStringList::const_iterator Ienv;
269 for (Ienv = keys.constBegin(); Ienv != keys.constEnd(); ++Ienv)
271 if (!(*Ienv).isEmpty() && (*Ienv)[0] !=
'#')
272 m_appEnv.insert((*Ienv).toLocal8Bit().constData(),
273 settings.value(*Ienv).toString());
282 QDir chan_path = QFileInfo(
m_configIni).absolutePath();
301 LOG(VB_RECORD, LOG_ERR,
LOC +
": No recorder provided.");
302 emit
SendMessage(
"Open",
"0",
"No recorder provided.",
"ERR");
308 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
309 QMap<QString, QString>::const_iterator Ienv;
311 Ienv !=
m_appEnv.constEnd(); ++Ienv)
313 env.insert(Ienv.key(), Ienv.value());
314 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
" ENV: '%1' = '%2'")
315 .arg(Ienv.key(), Ienv.value()));
317 m_proc.setProcessEnvironment(env);
320 QObject::connect(&
m_proc, &QProcess::started,
this,
323 QObject::connect(&
m_proc, &QProcess::readyReadStandardOutput,
this,
326 QObject::connect(&
m_proc, &QProcess::readyReadStandardError,
this,
329 qRegisterMetaType<QProcess::ProcessError>(
"QProcess::ProcessError");
330 QObject::connect(&
m_proc, &QProcess::errorOccurred,
333 qRegisterMetaType<QProcess::ExitStatus>(
"QProcess::ExitStatus");
335 static_cast<void (QProcess::*)
336 (
int,QProcess::ExitStatus exitStatus)
>
337 (&QProcess::finished),
340 qRegisterMetaType<QProcess::ProcessState>(
"QProcess::ProcessState");
341 QObject::connect(&
m_proc, &QProcess::stateChanged,
this,
344 LOG(VB_RECORD, LOG_INFO,
LOC +
": Opened");
353 if (proc.state() == QProcess::Running)
355 LOG(VB_RECORD, LOG_INFO,
LOC +
356 QString(
"Sending SIGTERM to %1(%2)").arg(desc).arg(proc.processId()));
358 proc.waitForFinished();
360 if (proc.state() == QProcess::Running)
362 LOG(VB_RECORD, LOG_INFO,
LOC +
363 QString(
"Sending SIGKILL to %1(%2)").arg(desc).arg(proc.processId()));
365 proc.waitForFinished();
374 LOG(VB_RECORD, LOG_INFO,
LOC +
": Closing application.");
377 std::this_thread::sleep_for(50us);
382 m_tuneProc.closeReadChannel(QProcess::StandardOutput);
386 if (
m_proc.state() == QProcess::Running)
388 m_proc.closeReadChannel(QProcess::StandardOutput);
390 std::this_thread::sleep_for(50us);
404 m_runCond.wait_for(lk, std::chrono::milliseconds(10));
407 if (
m_proc.state() == QProcess::Running)
409 if (
m_proc.waitForReadyRead(50))
417 qApp->processEvents();
420 if (
m_proc.state() == QProcess::Running)
422 m_proc.closeReadChannel(QProcess::StandardOutput);
437 QString cmd =
args.takeFirst();
439 LOG(VB_RECORD, LOG_DEBUG,
LOC +
440 QString(
" Beginning cleanup: '%1'").arg(cmd));
448 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start cleanup process: "
453 if (
cleanup.state() == QProcess::NotRunning)
455 if (
cleanup.exitStatus() != QProcess::NormalExit)
457 LOG(VB_RECORD, LOG_ERR,
LOC +
": Cleanup process failed: " +
ENO);
462 LOG(VB_RECORD, LOG_INFO,
LOC +
": Cleanup finished.");
467 LOG(VB_RECORD, LOG_INFO,
LOC +
"DataStarted");
477 QString cmd = settings.value(
"ONSTART").toString();
487 if (startcmd.isEmpty())
490 LOG(VB_CHANNEL, LOG_INFO,
LOC +
491 QString(
": Data started cmd from '%1': '%2'")
492 .arg(from, startcmd));
494 bool background =
false;
495 int pos = startcmd.lastIndexOf(QChar(
'&'));
499 startcmd = startcmd.left(pos);
503 startcmd =
args.takeFirst();
507 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Finishing tune: '%1' %3")
508 .arg(startcmd, background ?
"in the background" :
""));
513 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to finish tune process: "
525 LOG(VB_RECORD, LOG_ERR,
LOC +
": Finish tune failed: " +
ENO);
531 LOG(VB_RECORD, LOG_INFO,
LOC +
": tunning finished.");
538 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
539 emit
SendMessage(
"LoadChannels", serial,
"No channels configured.",
"ERR");
549 cmd =
args.takeFirst();
552 scanner.start(cmd,
args);
554 if (!scanner.waitForStarted())
556 QString errmsg = QString(
"Failed to start '%1': ").arg(cmd) +
ENO;
557 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
558 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
568 if (scanner.waitForReadyRead(50))
570 buf = scanner.readLine();
573 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + buf);
578 if (scanner.state() != QProcess::Running)
581 if (scanner.waitForFinished(50 ))
586 QString errmsg = QString(
"Timedout waiting for '%1'").arg(cmd);
587 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
588 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
605 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
606 emit
SendMessage(
"FirstChannel", serial,
"No channels configured.",
"ERR");
612 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": Invalid channel configuration.");
613 emit
SendMessage(func, serial,
"Invalid channel configuration.",
"ERR");
619 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": No more channels.");
620 emit
SendMessage(func, serial,
"No more channels",
"ERR");
635 LOG(VB_CHANNEL, LOG_INFO,
LOC +
636 QString(
": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4")
637 .arg(name, callsign, xmltvid, icon));
639 emit
SendMessage(func, serial, QString(
"%1,%2,%3,%4,%5")
640 .arg(channum, name, callsign,
641 xmltvid, icon),
"OK");
658 int pos = cmd.lastIndexOf(QChar(
'&'));
659 bool background =
false;
670 cmd =
args.takeFirst();
672 LOG(VB_RECORD, LOG_WARNING,
LOC +
673 QString(
" New episode starting on current channel: '%1'").arg(cmd));
681 LOG(VB_RECORD, LOG_ERR,
LOC +
682 " NewEpisodeStarting: Failed to start process: " +
ENO);
687 LOG(VB_RECORD, LOG_INFO,
LOC +
688 "NewEpisodeStarting: running in background.");
693 if (
m_tuneProc.state() == QProcess::NotRunning)
695 if (
m_tuneProc.exitStatus() != QProcess::NormalExit)
697 LOG(VB_RECORD, LOG_ERR,
LOC +
698 " NewEpisodeStarting: process failed: " +
ENO);
702 LOG(VB_RECORD, LOG_INFO,
LOC +
"NewEpisodeStarting: finished.");
706 const QVariantMap & chaninfo)
712 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No 'tuner' configured.");
713 emit
SendMessage(
"TuneChannel", serial,
"No 'tuner' configured.",
"ERR");
717 QString channum =
m_chaninfo[
"channum"].toString();
724 LOG(VB_CHANNEL, LOG_INFO,
LOC +
725 QString(
"TuneChannel: Already on %1").arg(channum));
727 QString(
"Tunned to %1").arg(channum),
"OK");
736 bool background =
false;
743 settings.beginGroup(channum);
745 QString cmd = settings.value(
"TUNE").toString();
749 LOG(VB_CHANNEL, LOG_INFO,
LOC +
750 QString(
": Using tune cmd from '%1': '%2'")
755 url = settings.value(
"URL").toString();
758 if (tunecmd.indexOf(
"%URL%") >= 0)
760 tunecmd.replace(
"%URL%", url);
761 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
762 QString(
": '%URL%' replaced with '%1' in tunecmd: '%2'")
769 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
770 QString(
": '%URL%' replaced with '%1' in cmd: '%2'")
775 m_desc.replace(
"%CHANNAME%", settings.value(
"NAME").toString());
776 m_desc.replace(
"%CALLSIGN%", settings.value(
"CALLSIGN").toString());
784 int pos = tunecmd.lastIndexOf(QChar(
'&'));
788 tunecmd = tunecmd.left(pos);
796 LOG(VB_RECORD, LOG_DEBUG,
LOC +
797 QString(
": '%LOGFILE%' replaced with '%1' in cmd: '%2'")
804 LOG(VB_RECORD, LOG_DEBUG,
LOC +
805 QString(
": '%LOGGING%' replaced with '%1' in cmd: '%2'")
809 m_desc.replace(
"%URL%", url);
811 if (!tunecmd.isEmpty())
814 QString cmd =
args.takeFirst();
819 QString errmsg = QString(
"Tune `%1` failed: ").arg(tunecmd) +
ENO;
820 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
821 emit
SendMessage(
"TuneChannel", serial, errmsg,
"ERR");
827 LOG(VB_CHANNEL, LOG_INFO,
LOC +
828 QString(
": Started in background `%1` URL '%2'")
839 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
": Started `%1` URL '%2'")
842 QString(
"InProgress `%1`").arg(tunecmd),
"OK");
858 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
859 QString(
": Tune process(%1) still running").arg(
m_tuneProc.processId()));
860 emit
SendMessage(
"TuneStatus", serial,
"InProgress",
"OK");
865 m_tuneProc.exitStatus() != QProcess::NormalExit)
867 QString errmsg = QString(
"'%1' failed: ")
869 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
870 emit
SendMessage(
"TuneStatus", serial, errmsg,
"WARN");
887 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
888 "Cannot read LockTimeout from config file.");
889 emit
SendMessage(
"LockTimeout", serial,
"Not open",
"ERR");
903 LOG(VB_CHANNEL, LOG_INFO,
LOC +
904 QString(
"Channel defined tune timeout: %1 (chan %2)")
909 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
910 "No channel defined tune timeout");
916 LOG(VB_CHANNEL, LOG_INFO,
LOC +
917 QString(
"Using configured LockTimeout of %1").arg(
m_lockTimeout));
922 LOG(VB_CHANNEL, LOG_INFO,
LOC +
923 "No LockTimeout defined in config, defaulting to 12000ms");
936 emit
SendMessage(
"HasPictureAttributes", serial,
"No",
"OK");
942 emit
SendMessage(
"BlockSize", serial, QString(
"Blocksize %1").arg(blksz),
"OK");
950 LOG(VB_RECORD, LOG_ERR,
LOC +
": No channel has been tuned");
952 "No channel has been tuned",
"ERR");
956 if (
m_proc.state() == QProcess::Running)
958 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application already running");
960 "Application already running",
"WARN");
967 QString cmd =
args.takeFirst();
968#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
969 m_proc.start(cmd,
args, QIODevice::ReadOnly|QIODevice::Unbuffered);
971 m_proc.start(cmd,
args, QIODeviceBase::ReadOnly | QIODeviceBase::Unbuffered);
973 m_proc.setTextModeEnabled(
false);
974 m_proc.setReadChannel(QProcess::StandardOutput);
976 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Starting process '%1' args: '%2'")
979 if (!
m_proc.waitForStarted())
981 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start application.");
983 "Failed to start application.",
"ERR");
987 std::this_thread::sleep_for(50ms);
989 if (
m_proc.state() != QProcess::Running)
991 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application failed to start");
993 "Application failed to start",
"ERR");
997 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Started process '%1' PID %2")
1002 emit
SendMessage(
"StartStreaming", serial,
"Streaming Started",
"OK");
1008 if (
m_proc.state() == QProcess::Running)
1012 LOG(VB_RECORD, LOG_INFO,
LOC +
": External application terminated.");
1014 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"STATUS");
1016 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"OK");
1023 "Already not Streaming",
"INFO");
1028 "Already not Streaming",
"WARN");
1038 QString msg = QString(
"Process '%1' started").arg(
m_proc.program());
1039 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
1044 QProcess::ExitStatus exitStatus)
1047 QString msg = QString(
"%1Finished: %2 (exit code: %3)")
1048 .arg(exitStatus != QProcess::NormalExit ?
"WARN:" :
"",
1049 exitStatus == QProcess::NormalExit ?
"OK" :
"Abnormal exit",
1051 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
1060 bool unexpected =
false;
1061 QString msg =
"State Changed: ";
1064 case QProcess::NotRunning:
1065 msg +=
"Not running";
1068 case QProcess::Starting:
1071 case QProcess::Running:
1072 msg += QString(
"Running PID %1").arg(
m_proc.processId());
1076 LOG(VB_RECORD, LOG_INFO,
LOC + msg);
1080 emit
SendMessage(
"STATUS",
"0",
"Unexpected: " + msg,
"ERR");
1088 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": %1")
1089 .arg(
m_proc.errorString()));
1094 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
": Error: %1")
1095 .arg(
m_proc.errorString()));
1102 QByteArray buf =
m_proc.readAllStandardError();
1103 QString msg = QString::fromUtf8(buf).trimmed();
1104 QList<QString> msgs = msg.split(
'\n');
1107 for (
int idx=0; idx < msgs.count(); ++idx)
1110 if (!msgs[idx].isEmpty())
1112 QStringList tokens = QString(msgs[idx])
1113 .split(
':', Qt::SkipEmptyParts);
1114 tokens.removeFirst();
1116 message = msgs[idx];
1118 message = tokens.join(
':');
1119 if (msgs[idx].startsWith(
"err", Qt::CaseInsensitive))
1121 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
">>> %1").arg(msgs[idx]));
1124 else if (msgs[idx].startsWith(
"warn", Qt::CaseInsensitive))
1126 LOG(VB_RECORD, LOG_WARNING,
LOC + QString(
">>> %1").arg(msgs[idx]));
1129 else if (msgs[idx].startsWith(
"damage", Qt::CaseInsensitive))
1131 LOG(VB_RECORD, LOG_WARNING,
LOC + QString(
">>> %1").arg(msgs[idx]));
1132 emit
SendMessage(
"STATUS",
"0", message,
"DAMAGE");
1136 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
">>> %1").arg(msgs[idx]));
1145 LOG(VB_RECORD, LOG_WARNING,
LOC +
": Data ready.");
1152#include "moc_MythExternRecApp.cpp"
static QStringList MythSplitCommandString(const QString &line)
Parse a string into separate tokens.
void LockTimeout(const QString &serial)
std::atomic< bool > m_run
void StartStreaming(const QString &serial)
void TerminateProcess(QProcess &proc, const QString &desc) const
void ProcFinished(int exitCode, QProcess::ExitStatus exitStatus)
void SetDescription(const QString &desc)
void ProcReadStandardError(void)
QProcess m_finishTuneProc
QString m_newEpisodeCommand
void NewEpisodeStarting(void)
std::condition_variable m_runCond
void GetChannel(const QString &serial, const QString &func)
MythExternRecApp(QString command, QString conf_file, QString log_file, QString logging)
void ProcReadStandardOutput(void)
~MythExternRecApp(void) override
QMap< QString, QString > m_settingVars
QMap< QString, QString > m_appEnv
void TuneChannel(const QString &serial, const QVariantMap &chaninfo)
void replace_variables(void)
void SendMessage(const QString &command, const QString &serial, const QString &message, const QString &status="")
void MythLog(const QString &msg)
void ProcStateChanged(QProcess::ProcessState newState)
void HasTuner(const QString &serial)
void FirstChannel(const QString &serial)
void Fill(const QByteArray &buffer)
void StopStreaming(const QString &serial, bool silent)
std::atomic< bool > m_streaming
void LoadChannels(const QString &serial)
QSettings * m_chanSettings
void NextChannel(const QString &serial)
QString ReplaceCmdVariables(const QString &cmd) const
void TuneStatus(const QString &serial)
void ProcError(QProcess::ProcessError error)
void SetBlockSize(const QString &serial, int blksz)
static QString sanitize_var(const QString &var)
void HasPictureAttributes(const QString &serial)
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
#define ENO
This can be appended to the LOG args with "+".
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
static QString cleanup(const QString &str)