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, 1);
87 cleaned = cleaned.remove(
p1, 1);
92 cleaned = cleaned.remove(
p1,
p2 -
p1 + 1);
96 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Sanitized: '%1' -> '%2'")
103 const QVariantMap & extra_args)
const
105 QString result = var;
109 for (
auto it = extra_args.keyValueBegin();
110 it != extra_args.keyValueEnd(); ++it)
112 if (it->first ==
"command")
114 result.replace(QString(
"\%%1\%").arg(it->first.toUpper()),
115 it->second.toString());
116 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
117 QString(
"Replaced '%1' with '%2'")
118 .arg(it->first.toUpper(), it->second.toString()));
127 QMap<QString, QString>::const_iterator Ivar;
131 QString repl =
"%" + Ivar.key() +
"%";
132 if (cmd.indexOf(repl) >= 0)
134 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Replacing '%1' with '%2'")
135 .arg(repl, Ivar.value()));
136 cmd.replace(repl, Ivar.value());
140 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Did not find '%1' in '%2'")
150 if (
m_proc.processId() > 0)
151 extra = QString(
"(pid %1) ").arg(
m_proc.processId());
156 return QString(
"%1%2 ").arg(extra, desc);
165 m_fatalMsg = QString(
"ERR:Config file '%1' does not exist "
167 .arg(conf_info.fileName(),
168 conf_info.absolutePath());
174 QSettings settings(
m_configIni, QSettings::IniFormat);
176 if (settings.childGroups().contains(
"VARIABLES"))
178 LOG(VB_CHANNEL, LOG_DEBUG,
"Parsing variables");
179 settings.beginGroup(
"VARIABLES");
181 QStringList childKeys = settings.childKeys();
182 for (
const QString & var : std::as_const(childKeys))
185 LOG(VB_CHANNEL, LOG_INFO, QString(
"%1=%2")
186 .arg(var, settings.value(var).toString()));
191 QMap<QString, QString>::iterator Ivar;
192 QMap<QString, QString>::iterator Ivar2;
196 QString repl =
"%" + Ivar.key() +
"%";
200 if ((*Ivar2).indexOf(repl) >= 0)
202 LOG(VB_CHANNEL, LOG_DEBUG, QString(
"Replacing '%1' with '%2'")
203 .arg(repl, Ivar.value()));
204 (*Ivar2).replace(repl, Ivar.value());
211 LOG(VB_CHANNEL, LOG_DEBUG,
"No VARIABLES section");
214 if (!settings.contains(
"RECORDER/command"))
216 m_fatalMsg = QString(
"ERR:Config file %1 file missing "
217 "[RECORDER]/command")
218 .arg(conf_info.absolutePath());
224 m_recCommand = settings.value(
"RECORDER/command").toString();
225 m_recDesc = settings.value(
"RECORDER/desc").toString();
226 m_cleanup = settings.value(
"RECORDER/cleanup").toString();
227 m_tuneCommand = settings.value(
"TUNER/command",
"").toString();
229 m_onDataStart = settings.value(
"TUNER/ondatastart",
"").toString();
230 m_channelsIni = settings.value(
"TUNER/channels",
"").toString();
232 m_scanCommand = settings.value(
"SCANNER/command",
"").toString();
233 m_scanTimeout = settings.value(
"SCANNER/timeout",
"").toInt();
243 settings.beginGroup(
"ENVIRONMENT");
246 QStringList keys = settings.childKeys();
247 QStringList::const_iterator Ienv;
248 for (Ienv = keys.constBegin(); Ienv != keys.constEnd(); ++Ienv)
250 if (!(*Ienv).isEmpty() && (*Ienv)[0] !=
'#')
251 m_appEnv.insert((*Ienv).toLocal8Bit().constData(),
252 settings.value(*Ienv).toString());
261 QDir chan_path = QFileInfo(
m_configIni).absolutePath();
280 LOG(VB_RECORD, LOG_ERR,
LOC +
": No recorder provided.");
281 emit
SendMessage(
"Open",
"0",
"No recorder provided.",
"ERR");
287 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
288 QMap<QString, QString>::const_iterator Ienv;
290 Ienv !=
m_appEnv.constEnd(); ++Ienv)
292 env.insert(Ienv.key(), Ienv.value());
293 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
" ENV: '%1' = '%2'")
294 .arg(Ienv.key(), Ienv.value()));
296 m_proc.setProcessEnvironment(env);
299 QObject::connect(&
m_proc, &QProcess::started,
this,
302 QObject::connect(&
m_proc, &QProcess::readyReadStandardOutput,
this,
305 QObject::connect(&
m_proc, &QProcess::readyReadStandardError,
this,
308 qRegisterMetaType<QProcess::ProcessError>(
"QProcess::ProcessError");
309 QObject::connect(&
m_proc, &QProcess::errorOccurred,
312 qRegisterMetaType<QProcess::ExitStatus>(
"QProcess::ExitStatus");
314 static_cast<void (QProcess::*)
315 (
int,QProcess::ExitStatus exitStatus)
>
316 (&QProcess::finished),
319 qRegisterMetaType<QProcess::ProcessState>(
"QProcess::ProcessState");
320 QObject::connect(&
m_proc, &QProcess::stateChanged,
this,
323 LOG(VB_RECORD, LOG_INFO,
LOC +
": Opened");
332 if (proc.state() == QProcess::Running)
334 LOG(VB_RECORD, LOG_INFO,
LOC +
335 QString(
"Sending SIGTERM to %1(%2)").arg(desc).arg(proc.processId()));
337 proc.waitForFinished();
339 if (proc.state() == QProcess::Running)
341 LOG(VB_RECORD, LOG_INFO,
LOC +
342 QString(
"Sending SIGKILL to %1(%2)").arg(desc).arg(proc.processId()));
344 proc.waitForFinished();
353 LOG(VB_RECORD, LOG_INFO,
LOC +
": Closing application.");
356 std::this_thread::sleep_for(50us);
361 m_tuneProc.closeReadChannel(QProcess::StandardOutput);
365 if (
m_proc.state() == QProcess::Running)
367 m_proc.closeReadChannel(QProcess::StandardOutput);
369 std::this_thread::sleep_for(50us);
383 m_runCond.wait_for(lk, std::chrono::milliseconds(10));
386 if (
m_proc.state() == QProcess::Running)
388 if (
m_proc.waitForReadyRead(50))
396 qApp->processEvents();
399 if (
m_proc.state() == QProcess::Running)
401 m_proc.closeReadChannel(QProcess::StandardOutput);
416 QString cmd =
args.takeFirst();
418 LOG(VB_RECORD, LOG_WARNING,
LOC +
419 QString(
" Beginning cleanup: '%1'").arg(cmd));
427 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start cleanup process: "
432 if (
cleanup.state() == QProcess::NotRunning)
434 if (
cleanup.exitStatus() != QProcess::NormalExit)
436 LOG(VB_RECORD, LOG_ERR,
LOC +
": Cleanup process failed: " +
ENO);
441 LOG(VB_RECORD, LOG_INFO,
LOC +
": Cleanup finished.");
446 LOG(VB_RECORD, LOG_INFO,
LOC +
"DataStarted");
455 QString cmd = settings.value(
"ONSTART").toString();
459 LOG(VB_CHANNEL, LOG_INFO,
LOC +
460 QString(
": Using ONSTART cmd from '%1': '%2'")
468 if (startcmd.isEmpty())
472 bool background =
false;
473 int pos = startcmd.lastIndexOf(QChar(
'&'));
477 startcmd = startcmd.left(pos);
481 startcmd =
args.takeFirst();
485 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Finishing tune: '%1' %3")
486 .arg(startcmd, background ?
"in the background" :
""));
491 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to finish tune process: "
503 LOG(VB_RECORD, LOG_ERR,
LOC +
": Finish tune failed: " +
ENO);
509 LOG(VB_RECORD, LOG_INFO,
LOC +
": tunning finished.");
516 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
517 emit
SendMessage(
"LoadChannels", serial,
"No channels configured.",
"ERR");
527 cmd =
args.takeFirst();
530 scanner.start(cmd,
args);
532 if (!scanner.waitForStarted())
534 QString errmsg = QString(
"Failed to start '%1': ").arg(cmd) +
ENO;
535 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
536 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
546 if (scanner.waitForReadyRead(50))
548 buf = scanner.readLine();
551 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + buf);
556 if (scanner.state() != QProcess::Running)
559 if (scanner.waitForFinished(50 ))
564 QString errmsg = QString(
"Timedout waiting for '%1'").arg(cmd);
565 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
566 emit
SendMessage(
"LoadChannels", serial, errmsg,
"ERR");
583 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No channels configured.");
584 emit
SendMessage(
"FirstChannel", serial,
"No channels configured.",
"ERR");
590 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": Invalid channel configuration.");
591 emit
SendMessage(func, serial,
"Invalid channel configuration.",
"ERR");
597 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
": No more channels.");
598 emit
SendMessage(func, serial,
"No more channels",
"ERR");
613 LOG(VB_CHANNEL, LOG_INFO,
LOC +
614 QString(
": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4")
615 .arg(name, callsign, xmltvid, icon));
617 emit
SendMessage(func, serial, QString(
"%1,%2,%3,%4,%5")
618 .arg(channum, name, callsign,
619 xmltvid, icon),
"OK");
636 int pos = cmd.lastIndexOf(QChar(
'&'));
637 bool background =
false;
648 cmd =
args.takeFirst();
650 LOG(VB_RECORD, LOG_WARNING,
LOC +
651 QString(
" New episode starting on current channel: '%1'").arg(cmd));
659 LOG(VB_RECORD, LOG_ERR,
LOC +
660 " NewEpisodeStarting: Failed to start process: " +
ENO);
665 LOG(VB_RECORD, LOG_INFO,
LOC +
666 "NewEpisodeStarting: running in background.");
671 if (
m_tuneProc.state() == QProcess::NotRunning)
673 if (
m_tuneProc.exitStatus() != QProcess::NormalExit)
675 LOG(VB_RECORD, LOG_ERR,
LOC +
676 " NewEpisodeStarting: process failed: " +
ENO);
680 LOG(VB_RECORD, LOG_INFO,
LOC +
"NewEpisodeStarting: finished.");
684 const QVariantMap & chaninfo)
690 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": No 'tuner' configured.");
691 emit
SendMessage(
"TuneChannel", serial,
"No 'tuner' configured.",
"ERR");
695 QString channum =
m_chaninfo[
"channum"].toString();
702 LOG(VB_CHANNEL, LOG_INFO,
LOC +
703 QString(
"TuneChannel: Already on %1").arg(channum));
705 QString(
"Tunned to %1").arg(channum),
"OK");
714 bool background =
false;
719 settings.beginGroup(channum);
721 QString cmd = settings.value(
"TUNE").toString();
725 LOG(VB_CHANNEL, LOG_INFO,
LOC +
726 QString(
": Using tune cmd from '%1': '%2'")
731 url = settings.value(
"URL").toString();
734 if (tunecmd.indexOf(
"%URL%") >= 0)
736 tunecmd.replace(
"%URL%", url);
737 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
738 QString(
": '%URL%' replaced with '%1' in tunecmd: '%2'")
745 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
746 QString(
": '%URL%' replaced with '%1' in cmd: '%2'")
751 m_desc.replace(
"%CHANNAME%", settings.value(
"NAME").toString());
752 m_desc.replace(
"%CALLSIGN%", settings.value(
"CALLSIGN").toString());
760 int pos = tunecmd.lastIndexOf(QChar(
'&'));
764 tunecmd = tunecmd.left(pos);
772 LOG(VB_RECORD, LOG_DEBUG,
LOC +
773 QString(
": '%LOGFILE%' replaced with '%1' in cmd: '%2'")
780 LOG(VB_RECORD, LOG_DEBUG,
LOC +
781 QString(
": '%LOGGING%' replaced with '%1' in cmd: '%2'")
785 m_desc.replace(
"%URL%", url);
787 if (!tunecmd.isEmpty())
790 QString cmd =
args.takeFirst();
795 QString errmsg = QString(
"Tune `%1` failed: ").arg(tunecmd) +
ENO;
796 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
797 emit
SendMessage(
"TuneChannel", serial, errmsg,
"ERR");
803 LOG(VB_CHANNEL, LOG_INFO,
LOC +
804 QString(
": Started in background `%1` URL '%2'")
815 LOG(VB_CHANNEL, LOG_INFO,
LOC + QString(
": Started `%1` URL '%2'")
818 QString(
"InProgress `%1`").arg(tunecmd),
"OK");
834 LOG(VB_CHANNEL, LOG_DEBUG,
LOC +
835 QString(
": Tune process(%1) still running").arg(
m_tuneProc.processId()));
836 emit
SendMessage(
"TuneStatus", serial,
"InProgress",
"OK");
841 m_tuneProc.exitStatus() != QProcess::NormalExit)
843 QString errmsg = QString(
"'%1' failed: ")
845 LOG(VB_CHANNEL, LOG_ERR,
LOC +
": " + errmsg);
846 emit
SendMessage(
"TuneStatus", serial, errmsg,
"WARN");
863 LOG(VB_CHANNEL, LOG_WARNING,
LOC +
864 "Cannot read LockTimeout from config file.");
865 emit
SendMessage(
"LockTimeout", serial,
"Not open",
"ERR");
871 LOG(VB_CHANNEL, LOG_INFO,
LOC +
872 QString(
"Using configured LockTimeout of %1").arg(
m_lockTimeout));
876 LOG(VB_CHANNEL, LOG_INFO,
LOC +
877 "No LockTimeout defined in config, defaulting to 12000ms");
890 emit
SendMessage(
"HasPictureAttributes", serial,
"No",
"OK");
896 emit
SendMessage(
"BlockSize", serial, QString(
"Blocksize %1").arg(blksz),
"OK");
904 LOG(VB_RECORD, LOG_ERR,
LOC +
": No channel has been tuned");
906 "No channel has been tuned",
"ERR");
910 if (
m_proc.state() == QProcess::Running)
912 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application already running");
914 "Application already running",
"WARN");
921 QString cmd =
args.takeFirst();
922 m_proc.start(cmd,
args, QIODevice::ReadOnly|QIODevice::Unbuffered);
923 m_proc.setTextModeEnabled(
false);
924 m_proc.setReadChannel(QProcess::StandardOutput);
926 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Starting process '%1' args: '%2'")
929 if (!
m_proc.waitForStarted())
931 LOG(VB_RECORD, LOG_ERR,
LOC +
": Failed to start application.");
933 "Failed to start application.",
"ERR");
937 std::this_thread::sleep_for(50ms);
939 if (
m_proc.state() != QProcess::Running)
941 LOG(VB_RECORD, LOG_ERR,
LOC +
": Application failed to start");
943 "Application failed to start",
"ERR");
947 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": Started process '%1' PID %2")
952 emit
SendMessage(
"StartStreaming", serial,
"Streaming Started",
"OK");
958 if (
m_proc.state() == QProcess::Running)
962 LOG(VB_RECORD, LOG_INFO,
LOC +
": External application terminated.");
964 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"STATUS");
966 emit
SendMessage(
"StopStreaming", serial,
"Streaming Stopped",
"OK");
973 "Already not Streaming",
"INFO");
978 "Already not Streaming",
"WARN");
988 QString msg = QString(
"Process '%1' started").arg(
m_proc.program());
989 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
994 QProcess::ExitStatus exitStatus)
997 QString msg = QString(
"%1Finished: %2 (exit code: %3)")
998 .arg(exitStatus != QProcess::NormalExit ?
"WARN:" :
"",
999 exitStatus == QProcess::NormalExit ?
"OK" :
"Abnormal exit",
1001 LOG(VB_RECORD, LOG_INFO,
LOC +
": " + msg);
1010 bool unexpected =
false;
1011 QString msg =
"State Changed: ";
1014 case QProcess::NotRunning:
1015 msg +=
"Not running";
1018 case QProcess::Starting:
1021 case QProcess::Running:
1022 msg += QString(
"Running PID %1").arg(
m_proc.processId());
1026 LOG(VB_RECORD, LOG_INFO,
LOC + msg);
1030 emit
SendMessage(
"STATUS",
"0",
"Unexpected: " + msg,
"ERR");
1038 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
": %1")
1039 .arg(
m_proc.errorString()));
1044 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
": Error: %1")
1045 .arg(
m_proc.errorString()));
1052 QByteArray buf =
m_proc.readAllStandardError();
1053 QString msg = QString::fromUtf8(buf).trimmed();
1054 QList<QString> msgs = msg.split(
'\n');
1057 for (
int idx=0; idx < msgs.count(); ++idx)
1060 if (!msgs[idx].isEmpty())
1062 QStringList tokens = QString(msgs[idx])
1063 .split(
':', Qt::SkipEmptyParts);
1064 tokens.removeFirst();
1066 message = msgs[idx];
1068 message = tokens.join(
':');
1069 if (msgs[idx].startsWith(
"err", Qt::CaseInsensitive))
1071 LOG(VB_RECORD, LOG_ERR,
LOC + QString(
">>> %1").arg(msgs[idx]));
1074 else if (msgs[idx].startsWith(
"warn", Qt::CaseInsensitive))
1076 LOG(VB_RECORD, LOG_WARNING,
LOC + QString(
">>> %1").arg(msgs[idx]));
1081 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
">>> %1").arg(msgs[idx]));
1090 LOG(VB_RECORD, LOG_WARNING,
LOC +
": Data ready.");