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);
83 if (cleaned.mid(
p1,
p2 -
p1).indexOf(
'%') == -1)
86 cleaned = cleaned.remove(
p2, 2);
87 cleaned = cleaned.remove(
p1, 2);
92 cleaned = cleaned.remove(
p1,
p2 -
p1 + 2);
96 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Sanitized: '%1' -> '%2'")
107 LOG(VB_CHANNEL, LOG_DEBUG,
108 QString(
"Replacing variables in '%1'").arg(cmd));
109 QString result = cmd;
111 QMap<QString, QString>::const_iterator Ivar;
115 LOG(VB_CHANNEL, LOG_DEBUG,
116 QString(
"Looking for '%1'").arg(Ivar.key()));
118 QString repl =
"%" + Ivar.key() +
"%";
119 if (result.indexOf(repl) >= 0)
121 result.replace(repl, Ivar.value());
122 LOG(VB_CHANNEL, LOG_DEBUG,
123 QString(
"Replacing '%1' with '%2'")
124 .arg(repl, Ivar.value()));
128 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Did not find '%1' in '%2'")
139 if (
m_proc.processId() > 0)
140 extra = QString(
"(pid %1) ").arg(
m_proc.processId());
145 return QString(
"%1%2 ").arg(extra, desc);
151 QMap<QString, QString>::iterator Ivar;
152 QMap<QString, QString>::iterator Ivar2;
159 if (it->first ==
"command")
169 repl =
"%" + Ivar.key() +
"%";
173 if ((*Ivar2).indexOf(repl) >= 0)
175 (*Ivar2).replace(repl, Ivar.value());
176 LOG(VB_CHANNEL, LOG_DEBUG,
177 QString(
"Replacing '%1' with '%2' in '%3'")
178 .arg(repl, Ivar.value(), *Ivar2));
185 LOG(VB_RECORD, LOG_DEBUG,
"All Variables:");
189 LOG(VB_RECORD, LOG_DEBUG,
190 QString(
"'%1' = '%2'").arg(Ivar.key(), Ivar.value()));
201 m_fatalMsg = QString(
"ERR:Config file '%1' does not exist "
203 .arg(conf_info.fileName(),
204 conf_info.absolutePath());
210 QSettings settings(
m_configIni, QSettings::IniFormat);
212 if (settings.childGroups().contains(
"VARIABLES"))
214 LOG(VB_CHANNEL, LOG_DEBUG,
"Parsing variables");
215 settings.beginGroup(
"VARIABLES");
217 QStringList childKeys = settings.childKeys();
218 for (
const QString & var : std::as_const(childKeys))
221 LOG(VB_CHANNEL, LOG_INFO, QString(
"%1=%2")
222 .arg(var, settings.value(var).toString()));
229 if (!settings.contains(
"RECORDER/command"))
231 m_fatalMsg = QString(
"ERR:Config file %1 file missing "
232 "[RECORDER]/command")
233 .arg(conf_info.absolutePath());
239 m_recCommand = settings.value(
"RECORDER/command").toString();
240 m_recDesc = settings.value(
"RECORDER/desc").toString();
241 m_cleanup = settings.value(
"RECORDER/cleanup").toString();
242 m_tuneCommand = settings.value(
"TUNER/command",
"").toString();
244 m_onDataStart = settings.value(
"TUNER/ondatastart",
"").toString();
245 m_channelsIni = settings.value(
"TUNER/channels",
"").toString();
247 m_scanCommand = settings.value(
"SCANNER/command",
"").toString();
248 m_scanTimeout = settings.value(
"SCANNER/timeout",
"").toInt();
258 settings.beginGroup(
"ENVIRONMENT");
261 QStringList keys = settings.childKeys();
262 QStringList::const_iterator Ienv;
263 for (Ienv = keys.constBegin(); Ienv != keys.constEnd(); ++Ienv)
265 if (!(*Ienv).isEmpty() && (*Ienv)[0] !=
'#')
266 m_appEnv.insert((*Ienv).toLocal8Bit().constData(),
267 settings.value(*Ienv).toString());
276 QDir chan_path = QFileInfo(
m_configIni).absolutePath();
295 LOG(VB_RECORD, LOG_ERR,
LOC +
": No recorder provided.");
296 emit
SendMessage(
"Open",
"0",
"No recorder provided.",
"ERR");
302 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
303 QMap<QString, QString>::const_iterator Ienv;
305 Ienv !=
m_appEnv.constEnd(); ++Ienv)
307 env.insert(Ienv.key(), Ienv.value());
308 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
" ENV: '%1' = '%2'")
309 .arg(Ienv.key(), Ienv.value()));
311 m_proc.setProcessEnvironment(env);
314 QObject::connect(&
m_proc, &QProcess::started,
this,
317 QObject::connect(&
m_proc, &QProcess::readyReadStandardOutput,
this,
320 QObject::connect(&
m_proc, &QProcess::readyReadStandardError,
this,
323 qRegisterMetaType<QProcess::ProcessError>(
"QProcess::ProcessError");
324 QObject::connect(&
m_proc, &QProcess::errorOccurred,
327 qRegisterMetaType<QProcess::ExitStatus>(
"QProcess::ExitStatus");
329 static_cast<void (QProcess::*)
330 (
int,QProcess::ExitStatus exitStatus)
>
331 (&QProcess::finished),
334 qRegisterMetaType<QProcess::ProcessState>(
"QProcess::ProcessState");
335 QObject::connect(&
m_proc, &QProcess::stateChanged,
this,
338 LOG(VB_RECORD, LOG_INFO,
LOC +
": Opened");
347 if (proc.state() == QProcess::Running)
349 LOG(VB_RECORD, LOG_INFO,
LOC +
350 QString(
"Sending SIGTERM to %1(%2)").arg(desc).arg(proc.processId()));
352 proc.waitForFinished();
354 if (proc.state() == QProcess::Running)
356 LOG(VB_RECORD, LOG_INFO,
LOC +
357 QString(
"Sending SIGKILL to %1(%2)").arg(desc).arg(proc.processId()));
359 proc.waitForFinished();
368 LOG(VB_RECORD, LOG_INFO,
LOC +
": Closing application.");
371 std::this_thread::sleep_for(50us);
376 m_tuneProc.closeReadChannel(QProcess::StandardOutput);
380 if (
m_proc.state() == QProcess::Running)
382 m_proc.closeReadChannel(QProcess::StandardOutput);
384 std::this_thread::sleep_for(50us);
398 m_runCond.wait_for(lk, std::chrono::milliseconds(10));
401 if (
m_proc.state() == QProcess::Running)
403 if (
m_proc.waitForReadyRead(50))
411 qApp->processEvents();
414 if (
m_proc.state() == QProcess::Running)
416 m_proc.closeReadChannel(QProcess::StandardOutput);
431 QString cmd =
args.takeFirst();
433 LOG(VB_RECORD, LOG_DEBUG,
LOC +
434 QString(
" Beginning cleanup: '%1'").arg(cmd));
442 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start cleanup process: "
447 if (
cleanup.state() == QProcess::NotRunning)
449 if (
cleanup.exitStatus() != QProcess::NormalExit)
451 LOG(VB_RECORD, LOG_ERR,
LOC +
": Cleanup process failed: " +
ENO);
456 LOG(VB_RECORD, LOG_INFO,
LOC +
": Cleanup finished.");
461 LOG(VB_RECORD, LOG_INFO,
LOC +
"DataStarted");
471 QString cmd = settings.value(
"ONSTART").toString();
481 if (startcmd.isEmpty())
484 LOG(VB_CHANNEL, LOG_INFO,
LOC +
485 QString(
": Data started cmd from '%1': '%2'")
486 .arg(from, startcmd));
488 bool background =
false;
489 int pos = startcmd.lastIndexOf(QChar(
'&'));
493 startcmd = startcmd.left(pos);
497 startcmd =
args.takeFirst();
501 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Finishing tune: '%1' %3")
502 .arg(startcmd, background ?
"in the background" :
""));
507 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to finish tune process: "
519 LOG(VB_RECORD, LOG_ERR,
LOC +
": Finish tune failed: " +
ENO);
525 LOG(VB_RECORD, LOG_INFO,
LOC +
": tunning finished.");
532 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
533 emit
SendMessage(
"LoadChannels", serial,
"No channels configured.",
"ERR");
543 cmd =
args.takeFirst();
546 scanner.start(cmd,
args);
548 if (!scanner.waitForStarted())
550 QString errmsg = QString(
"Failed to start '%1': ").arg(cmd) +
ENO;
551 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
552 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
562 if (scanner.waitForReadyRead(50))
564 buf = scanner.readLine();
567 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + buf);
572 if (scanner.state() != QProcess::Running)
575 if (scanner.waitForFinished(50 ))
580 QString errmsg = QString(
"Timedout waiting for '%1'").arg(cmd);
581 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
582 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
599 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
600 emit
SendMessage(
"FirstChannel", serial,
"No channels configured.",
"ERR");
606 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": Invalid channel configuration.");
607 emit
SendMessage(func, serial,
"Invalid channel configuration.",
"ERR");
613 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": No more channels.");
614 emit
SendMessage(func, serial,
"No more channels",
"ERR");
629 LOG(VB_CHANNEL, LOG_INFO,
LOC +
630 QString(
": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4")
631 .arg(name, callsign, xmltvid, icon));
633 emit
SendMessage(func, serial, QString(
"%1,%2,%3,%4,%5")
634 .arg(channum, name, callsign,
635 xmltvid, icon),
"OK");
652 int pos = cmd.lastIndexOf(QChar(
'&'));
653 bool background =
false;
664 cmd =
args.takeFirst();
666 LOG(VB_RECORD, LOG_WARNING,
LOC +
667 QString(
" New episode starting on current channel: '%1'").arg(cmd));
675 LOG(VB_RECORD, LOG_ERR,
LOC +
676 " NewEpisodeStarting: Failed to start process: " +
ENO);
681 LOG(VB_RECORD, LOG_INFO,
LOC +
682 "NewEpisodeStarting: running in background.");
687 if (
m_tuneProc.state() == QProcess::NotRunning)
689 if (
m_tuneProc.exitStatus() != QProcess::NormalExit)
691 LOG(VB_RECORD, LOG_ERR,
LOC +
692 " NewEpisodeStarting: process failed: " +
ENO);
696 LOG(VB_RECORD, LOG_INFO,
LOC +
"NewEpisodeStarting: finished.");
700 const QVariantMap & chaninfo)
706 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No 'tuner' configured.");
707 emit
SendMessage(
"TuneChannel", serial,
"No 'tuner' configured.",
"ERR");
711 QString channum =
m_chaninfo[
"channum"].toString();
718 LOG(VB_CHANNEL, LOG_INFO,
LOC +
719 QString(
"TuneChannel: Already on %1").arg(channum));
721 QString(
"Tunned to %1").arg(channum),
"OK");
730 bool background =
false;
737 settings.beginGroup(channum);
739 QString cmd = settings.value(
"TUNE").toString();
743 LOG(VB_CHANNEL, LOG_INFO,
LOC +
744 QString(
": Using tune cmd from '%1': '%2'")
749 url = settings.value(
"URL").toString();
752 if (tunecmd.indexOf(
"%URL%") >= 0)
754 tunecmd.replace(
"%URL%", url);
755 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
756 QString(
": '%URL%' replaced with '%1' in tunecmd: '%2'")
763 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
764 QString(
": '%URL%' replaced with '%1' in cmd: '%2'")
769 m_desc.replace(
"%CHANNAME%", settings.value(
"NAME").toString());
770 m_desc.replace(
"%CALLSIGN%", settings.value(
"CALLSIGN").toString());
778 int pos = tunecmd.lastIndexOf(QChar(
'&'));
782 tunecmd = tunecmd.left(pos);
790 LOG(VB_RECORD, LOG_DEBUG,
LOC +
791 QString(
": '%LOGFILE%' replaced with '%1' in cmd: '%2'")
798 LOG(VB_RECORD, LOG_DEBUG,
LOC +
799 QString(
": '%LOGGING%' replaced with '%1' in cmd: '%2'")
803 m_desc.replace(
"%URL%", url);
805 if (!tunecmd.isEmpty())
808 QString cmd =
args.takeFirst();
813 QString errmsg = QString(
"Tune `%1` failed: ").arg(tunecmd) +
ENO;
814 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
815 emit
SendMessage(
"TuneChannel", serial, errmsg,
"ERR");
821 LOG(VB_CHANNEL, LOG_INFO,
LOC +
822 QString(
": Started in background `%1` URL '%2'")
833 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
": Started `%1` URL '%2'")
836 QString(
"InProgress `%1`").arg(tunecmd),
"OK");
852 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
853 QString(
": Tune process(%1) still running").arg(
m_tuneProc.processId()));
854 emit
SendMessage(
"TuneStatus", serial,
"InProgress",
"OK");
859 m_tuneProc.exitStatus() != QProcess::NormalExit)
861 QString errmsg = QString(
"'%1' failed: ")
863 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
864 emit
SendMessage(
"TuneStatus", serial, errmsg,
"WARN");
881 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
882 "Cannot read LockTimeout from config file.");
883 emit
SendMessage(
"LockTimeout", serial,
"Not open",
"ERR");
897 LOG(VB_CHANNEL, LOG_INFO,
LOC +
898 QString(
"Channel defined tune timeout: %1 (chan %2)")
903 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
904 "No channel defined tune timeout");
910 LOG(VB_CHANNEL, LOG_INFO,
LOC +
911 QString(
"Using configured LockTimeout of %1").arg(
m_lockTimeout));
916 LOG(VB_CHANNEL, LOG_INFO,
LOC +
917 "No LockTimeout defined in config, defaulting to 12000ms");
930 emit
SendMessage(
"HasPictureAttributes", serial,
"No",
"OK");
936 emit
SendMessage(
"BlockSize", serial, QString(
"Blocksize %1").arg(blksz),
"OK");
944 LOG(VB_RECORD, LOG_ERR,
LOC +
": No channel has been tuned");
946 "No channel has been tuned",
"ERR");
950 if (
m_proc.state() == QProcess::Running)
952 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application already running");
954 "Application already running",
"WARN");
961 QString cmd =
args.takeFirst();
962#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
963 m_proc.start(cmd,
args, QIODevice::ReadOnly|QIODevice::Unbuffered);
965 m_proc.start(cmd,
args, QIODeviceBase::ReadOnly | QIODeviceBase::Unbuffered);
967 m_proc.setTextModeEnabled(
false);
968 m_proc.setReadChannel(QProcess::StandardOutput);
970 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Starting process '%1' args: '%2'")
973 if (!
m_proc.waitForStarted())
975 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start application.");
977 "Failed to start application.",
"ERR");
981 std::this_thread::sleep_for(50ms);
983 if (
m_proc.state() != QProcess::Running)
985 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application failed to start");
987 "Application failed to start",
"ERR");
991 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Started process '%1' PID %2")
996 emit
SendMessage(
"StartStreaming", serial,
"Streaming Started",
"OK");
1002 if (
m_proc.state() == QProcess::Running)
1006 LOG(VB_RECORD, LOG_INFO,
LOC +
": External application terminated.");
1008 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"STATUS");
1010 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"OK");
1017 "Already not Streaming",
"INFO");
1022 "Already not Streaming",
"WARN");
1032 QString msg = QString(
"Process '%1' started").arg(
m_proc.program());
1033 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
1038 QProcess::ExitStatus exitStatus)
1041 QString msg = QString(
"%1Finished: %2 (exit code: %3)")
1042 .arg(exitStatus != QProcess::NormalExit ?
"WARN:" :
"",
1043 exitStatus == QProcess::NormalExit ?
"OK" :
"Abnormal exit",
1045 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
1054 bool unexpected =
false;
1055 QString msg =
"State Changed: ";
1058 case QProcess::NotRunning:
1059 msg +=
"Not running";
1062 case QProcess::Starting:
1065 case QProcess::Running:
1066 msg += QString(
"Running PID %1").arg(
m_proc.processId());
1070 LOG(VB_RECORD, LOG_INFO,
LOC + msg);
1074 emit
SendMessage(
"STATUS",
"0",
"Unexpected: " + msg,
"ERR");
1082 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": %1")
1083 .arg(
m_proc.errorString()));
1088 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
": Error: %1")
1089 .arg(
m_proc.errorString()));
1096 QByteArray buf =
m_proc.readAllStandardError();
1097 QString msg = QString::fromUtf8(buf).trimmed();
1098 QList<QString> msgs = msg.split(
'\n');
1101 for (
int idx=0; idx < msgs.count(); ++idx)
1104 if (!msgs[idx].isEmpty())
1106 QStringList tokens = QString(msgs[idx])
1107 .split(
':', Qt::SkipEmptyParts);
1108 tokens.removeFirst();
1110 message = msgs[idx];
1112 message = tokens.join(
':');
1113 if (msgs[idx].startsWith(
"err", Qt::CaseInsensitive))
1115 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
">>> %1").arg(msgs[idx]));
1118 else if (msgs[idx].startsWith(
"warn", Qt::CaseInsensitive))
1120 LOG(VB_RECORD, LOG_WARNING,
LOC + QString(
">>> %1").arg(msgs[idx]));
1123 else if (msgs[idx].startsWith(
"damage", Qt::CaseInsensitive))
1125 LOG(VB_RECORD, LOG_WARNING,
LOC + QString(
">>> %1").arg(msgs[idx]));
1126 emit
SendMessage(
"STATUS",
"0", message,
"DAMAGE");
1130 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
">>> %1").arg(msgs[idx]));
1139 LOG(VB_RECORD, LOG_WARNING,
LOC +
": Data ready.");
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)