MythTV master
MythExternRecApp.cpp
Go to the documentation of this file.
1/* -*- Mode: c++ -*-
2 *
3 * Copyright (C) John Poet 2018
4 *
5 * This file is part of MythTV
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21// C/C++
22#include <csignal>
23#include <thread>
24#include <unistd.h>
25
26// Qt
27#include <QCoreApplication>
28#include <QElapsedTimer>
29#include <QFileInfo>
30
31// MythTV
34
35// MythExternRecorder
36#include "MythExternRecApp.h"
38
39#define LOC Desc()
40
42 QString conf_file,
43 QString log_file,
44 QString logging)
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))
49{
50 if (m_configIni.isEmpty() || !config())
52
54
55 LOG(VB_CHANNEL, LOG_INFO, LOC +
56 QString("Channels in '%1', Tuner: '%2', Scanner: '%3'")
58
60 m_desc.replace("%URL%", "");
61 m_desc.replace("%CHANNUM%", "");
62 m_desc.replace("%CHANNAME%", "");
63 m_desc.replace("%CALLSIGN%", "");
65}
66
68{
69 Close();
70}
71
72/* Remove any non-replaced variables along with any dependant strings.
73 Dependant strings are wrapped in [{ }] */
74QString MythExternRecApp::sanitize_var(const QString & var)
75{
76 qsizetype p1 { -1 };
77 qsizetype p2 { -1 };
78 QString cleaned = var;
79
80 while ((p1 = cleaned.indexOf("[{")) != -1)
81 {
82 p2 = cleaned.indexOf("}]", p1);
83 if (cleaned.mid(p1, p2 - p1).indexOf('%') == -1)
84 {
85 // Just remove the '[{' and '}]'
86 cleaned = cleaned.remove(p2, 2);
87 cleaned = cleaned.remove(p1, 2);
88 }
89 else
90 {
91 // Remove the contents of [{ ... }]
92 cleaned = cleaned.remove(p1, p2 - p1 + 2);
93 }
94 }
95
96 LOG(VB_CHANNEL, LOG_DEBUG, QString("Sanitized: '%1' -> '%2'")
97 .arg(var, cleaned));
98
99 return cleaned;
100}
101
102QString MythExternRecApp::ReplaceCmdVariables(const QString & cmd) const
103{
104 if (cmd.isEmpty())
105 return cmd;
106
107 LOG(VB_CHANNEL, LOG_DEBUG,
108 QString("Replacing variables in '%1'").arg(cmd));
109 QString result = cmd;
110
111 QMap<QString, QString>::const_iterator Ivar;
112 for (Ivar = m_settingVars.constBegin();
113 Ivar != m_settingVars.constEnd(); ++Ivar)
114 {
115 LOG(VB_CHANNEL, LOG_DEBUG,
116 QString("Looking for '%1'").arg(Ivar.key()));
117
118 QString repl = "%" + Ivar.key() + "%";
119 if (result.indexOf(repl) >= 0)
120 {
121 result.replace(repl, Ivar.value());
122 LOG(VB_CHANNEL, LOG_DEBUG,
123 QString("Replacing '%1' with '%2'")
124 .arg(repl, Ivar.value()));
125 }
126 else
127 {
128 LOG(VB_CHANNEL, LOG_DEBUG, QString("Did not find '%1' in '%2'")
129 .arg(repl, cmd));
130 }
131 }
132 return sanitize_var(result);
133}
134
135QString MythExternRecApp::Desc(void) const
136{
137 QString extra;
138
139 if (m_proc.processId() > 0)
140 extra = QString("(pid %1) ").arg(m_proc.processId());
141
142 QString desc = m_desc;
143 desc = ReplaceCmdVariables(desc);
144
145 return QString("%1%2 ").arg(extra, desc);
146}
147
149{
150 QString repl;
151 QMap<QString, QString>::iterator Ivar;
152 QMap<QString, QString>::iterator Ivar2;
153
154 if (!m_chaninfo.isEmpty())
155 {
156 for (auto it = m_chaninfo.keyValueBegin();
157 it != m_chaninfo.keyValueEnd(); ++it)
158 {
159 if (it->first == "command")
160 continue;
161 m_settingVars[it->first.toUpper()] = it->second.toString();
162 }
163 }
164
165 /* Replace defined VARs in other defined VARs */
166 for (Ivar = m_settingVars.begin();
167 Ivar != m_settingVars.end(); ++Ivar)
168 {
169 repl = "%" + Ivar.key() + "%";
170 for (Ivar2 = m_settingVars.begin();
171 Ivar2 != m_settingVars.end(); ++Ivar2)
172 {
173 if ((*Ivar2).indexOf(repl) >= 0)
174 {
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));
179 }
180 }
181 }
182
183 if (VERBOSE_LEVEL_CHECK(VB_RECORD, LOG_DEBUG))
184 {
185 LOG(VB_RECORD, LOG_DEBUG, "All Variables:");
186 for (Ivar = m_settingVars.begin();
187 Ivar != m_settingVars.end(); ++Ivar)
188 {
189 LOG(VB_RECORD, LOG_DEBUG,
190 QString("'%1' = '%2'").arg(Ivar.key(), Ivar.value()));
191 }
192 }
193}
194
196{
197 QFileInfo conf_info = QFileInfo(m_configIni);
198
200 {
201 m_fatalMsg = QString("ERR:Config file '%1' does not exist "
202 "in '%2'")
203 .arg(conf_info.fileName(),
204 conf_info.absolutePath());
205 LOG(VB_GENERAL, LOG_CRIT, m_fatalMsg);
206 m_fatal = true;
207 return false;
208 }
209
210 QSettings settings(m_configIni, QSettings::IniFormat);
211
212 if (settings.childGroups().contains("VARIABLES"))
213 {
214 LOG(VB_CHANNEL, LOG_DEBUG, "Parsing variables");
215 settings.beginGroup("VARIABLES");
216
217 QStringList childKeys = settings.childKeys();
218 for (const QString & var : std::as_const(childKeys))
219 {
220 m_settingVars[var] = settings.value(var).toString();
221 LOG(VB_CHANNEL, LOG_INFO, QString("%1=%2")
222 .arg(var, settings.value(var).toString()));
223 }
224 settings.endGroup();
225 }
226
228
229 if (!settings.contains("RECORDER/command"))
230 {
231 m_fatalMsg = QString("ERR:Config file %1 file missing "
232 "[RECORDER]/command")
233 .arg(conf_info.absolutePath());
234 LOG(VB_GENERAL, LOG_CRIT, m_fatalMsg);
235 m_fatal = true;
236 return false;
237 }
238
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();
243 m_newEpisodeCommand = settings.value("TUNER/newepisodecommand", "").toString();
244 m_onDataStart = settings.value("TUNER/ondatastart", "").toString();
245 m_channelsIni = settings.value("TUNER/channels", "").toString();
246 m_lockTimeout = settings.value("TUNER/timeout", "").toInt();
247 m_scanCommand = settings.value("SCANNER/command", "").toString();
248 m_scanTimeout = settings.value("SCANNER/timeout", "").toInt();
249
257
258 settings.beginGroup("ENVIRONMENT");
259
260 m_appEnv.clear();
261 QStringList keys = settings.childKeys();
262 QStringList::const_iterator Ienv;
263 for (Ienv = keys.constBegin(); Ienv != keys.constEnd(); ++Ienv)
264 {
265 if (!(*Ienv).isEmpty() && (*Ienv)[0] != '#')
266 m_appEnv.insert((*Ienv).toLocal8Bit().constData(),
267 settings.value(*Ienv).toString());
268 }
269
270 if (!m_channelsIni.isEmpty())
271 {
273 {
274 // Assume the channels config is in the same directory as
275 // main config
276 QDir chan_path = QFileInfo(m_configIni).absolutePath();
277 QFileInfo ini(chan_path, m_channelsIni);
278 m_channelsIni = ini.absoluteFilePath();
279 }
280 }
281
282 return true;
283}
284
286{
287 if (m_fatal)
288 {
289 emit SendMessage("Open", "0", m_fatalMsg, "ERR");
290 return false;
291 }
292
293 if (m_command.isEmpty())
294 {
295 LOG(VB_RECORD, LOG_ERR, LOC + ": No recorder provided.");
296 emit SendMessage("Open", "0", "No recorder provided.", "ERR");
297 return false;
298 }
299
300 if (!m_appEnv.isEmpty())
301 {
302 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
303 QMap<QString, QString>::const_iterator Ienv;
304 for (Ienv = m_appEnv.constBegin();
305 Ienv != m_appEnv.constEnd(); ++Ienv)
306 {
307 env.insert(Ienv.key(), Ienv.value());
308 LOG(VB_RECORD, LOG_INFO, LOC + QString(" ENV: '%1' = '%2'")
309 .arg(Ienv.key(), Ienv.value()));
310 }
311 m_proc.setProcessEnvironment(env);
312 }
313
314 QObject::connect(&m_proc, &QProcess::started, this,
316#if 0
317 QObject::connect(&m_proc, &QProcess::readyReadStandardOutput, this,
319#endif
320 QObject::connect(&m_proc, &QProcess::readyReadStandardError, this,
322
323 qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
324 QObject::connect(&m_proc, &QProcess::errorOccurred,
326
327 qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");
328 QObject::connect(&m_proc,
329 static_cast<void (QProcess::*)
330 (int,QProcess::ExitStatus exitStatus)>
331 (&QProcess::finished),
333
334 qRegisterMetaType<QProcess::ProcessState>("QProcess::ProcessState");
335 QObject::connect(&m_proc, &QProcess::stateChanged, this,
337
338 LOG(VB_RECORD, LOG_INFO, LOC + ": Opened");
339
340 emit Opened();
341 return true;
342}
343
344void MythExternRecApp::TerminateProcess(QProcess & proc, const QString & desc) const
345{
346 m_terminating = true;
347 if (proc.state() == QProcess::Running)
348 {
349 LOG(VB_RECORD, LOG_INFO, LOC +
350 QString("Sending SIGTERM to %1(%2)").arg(desc).arg(proc.processId()));
351 proc.terminate();
352 proc.waitForFinished();
353 }
354 if (proc.state() == QProcess::Running)
355 {
356 LOG(VB_RECORD, LOG_INFO, LOC +
357 QString("Sending SIGKILL to %1(%2)").arg(desc).arg(proc.processId()));
358 proc.kill();
359 proc.waitForFinished();
360 }
361 m_terminating = false;
362}
363
364Q_SLOT void MythExternRecApp::Close(void)
365{
366 if (m_run)
367 {
368 LOG(VB_RECORD, LOG_INFO, LOC + ": Closing application.");
369 m_run = false;
370 m_runCond.notify_all();
371 std::this_thread::sleep_for(50us);
372 }
373
374 if (m_tuneProc.state() == QProcess::Running)
375 {
376 m_tuneProc.closeReadChannel(QProcess::StandardOutput);
377 TerminateProcess(m_tuneProc, "Close app");
378 }
379
380 if (m_proc.state() == QProcess::Running)
381 {
382 m_proc.closeReadChannel(QProcess::StandardOutput);
383 TerminateProcess(m_proc, "Close app");
384 std::this_thread::sleep_for(50us);
385 }
386
387 emit Done();
388}
389
391{
392 QByteArray buf;
393
394 while (m_run)
395 {
396 {
397 std::unique_lock<std::mutex> lk(m_runMutex);
398 m_runCond.wait_for(lk, std::chrono::milliseconds(10));
399 }
400
401 if (m_proc.state() == QProcess::Running)
402 {
403 if (m_proc.waitForReadyRead(50))
404 {
405 buf = m_proc.read(m_blockSize);
406 if (!buf.isEmpty())
407 emit Fill(buf);
408 }
409 }
410
411 qApp->processEvents();
412 }
413
414 if (m_proc.state() == QProcess::Running)
415 {
416 m_proc.closeReadChannel(QProcess::StandardOutput);
417 TerminateProcess(m_proc, "No longer running app");
418 }
419
420 emit Done();
421}
422
424{
425 m_tunedChannel.clear();
426
427 if (m_cleanup.isEmpty())
428 return;
429
431 QString cmd = args.takeFirst();
432
433 LOG(VB_RECORD, LOG_DEBUG, LOC +
434 QString(" Beginning cleanup: '%1'").arg(cmd));
435
436 cmd = ReplaceCmdVariables(cmd);
437
438 QProcess cleanup;
439 cleanup.start(cmd, args);
440 if (!cleanup.waitForStarted())
441 {
442 LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start cleanup process: "
443 + ENO);
444 return;
445 }
446 cleanup.waitForFinished(5000);
447 if (cleanup.state() == QProcess::NotRunning)
448 {
449 if (cleanup.exitStatus() != QProcess::NormalExit)
450 {
451 LOG(VB_RECORD, LOG_ERR, LOC + ": Cleanup process failed: " + ENO);
452 return;
453 }
454 }
455
456 LOG(VB_RECORD, LOG_INFO, LOC + ": Cleanup finished.");
457}
458
460{
461 LOG(VB_RECORD, LOG_INFO, LOC + "DataStarted");
462
463 QString startcmd = m_onDataStart;
464 QString from = m_configIni;
465
466 if (!m_channelsIni.isEmpty())
467 {
468 QSettings settings(m_channelsIni, QSettings::IniFormat);
469 settings.beginGroup(m_tunedChannel);
470
471 QString cmd = settings.value("ONSTART").toString();
472 if (!cmd.isEmpty())
473 {
474 startcmd = cmd;
475 from = m_channelsIni;
476 }
477
478 settings.endGroup();
479 }
480
481 if (startcmd.isEmpty())
482 return;
483 startcmd = ReplaceCmdVariables(startcmd);
484 LOG(VB_CHANNEL, LOG_INFO, LOC +
485 QString(": Data started cmd from '%1': '%2'")
486 .arg(from, startcmd));
487
488 bool background = false;
489 int pos = startcmd.lastIndexOf(QChar('&'));
490 if (pos > 0)
491 {
492 background = true;
493 startcmd = startcmd.left(pos);
494 }
495
497 startcmd = args.takeFirst();
498
499 TerminateProcess(m_finishTuneProc, "Finish tuning");
500
501 LOG(VB_RECORD, LOG_INFO, LOC + QString("Finishing tune: '%1' %3")
502 .arg(startcmd, background ? "in the background" : ""));
503
504 m_finishTuneProc.start(startcmd, args);
505 if (!m_finishTuneProc.waitForStarted())
506 {
507 LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to finish tune process: "
508 + ENO);
509 return;
510 }
511
512 if (!background)
513 {
514 m_finishTuneProc.waitForFinished(5000);
515 if (m_finishTuneProc.state() == QProcess::NotRunning)
516 {
517 if (m_finishTuneProc.exitStatus() != QProcess::NormalExit)
518 {
519 LOG(VB_RECORD, LOG_ERR, LOC + ": Finish tune failed: " + ENO);
520 return;
521 }
522 }
523 }
524
525 LOG(VB_RECORD, LOG_INFO, LOC + ": tunning finished.");
526}
527
528Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial)
529{
530 if (m_channelsIni.isEmpty())
531 {
532 LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
533 emit SendMessage("LoadChannels", serial, "No channels configured.", "ERR");
534 return;
535 }
536
537 if (!m_scanCommand.isEmpty())
538 {
539 QString cmd = m_scanCommand;
540 cmd.replace("%CHANCONF%", m_channelsIni);
541
543 cmd = args.takeFirst();
544
545 QProcess scanner;
546 scanner.start(cmd, args);
547
548 if (!scanner.waitForStarted())
549 {
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");
553 return;
554 }
555
556 QByteArray buf;
557 QElapsedTimer timer;
558
559 timer.start();
560 while (timer.elapsed() < m_scanTimeout)
561 {
562 if (scanner.waitForReadyRead(50))
563 {
564 buf = scanner.readLine();
565 if (!buf.isEmpty())
566 {
567 LOG(VB_RECORD, LOG_INFO, LOC + ": " + buf);
568 MythLog(buf);
569 }
570 }
571
572 if (scanner.state() != QProcess::Running)
573 break;
574
575 if (scanner.waitForFinished(50 /* msecs */))
576 break;
577 }
578 if (timer.elapsed() >= m_scanTimeout)
579 {
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");
583 return;
584 }
585 }
586
587 if (m_chanSettings == nullptr)
588 m_chanSettings = new QSettings(m_channelsIni, QSettings::IniFormat);
589 m_chanSettings->sync();
590 m_channels = m_chanSettings->childGroups();
591
592 emit SendMessage("LoadChannels", serial, QString::number(m_channels.size()), "OK");
593}
594
595void MythExternRecApp::GetChannel(const QString & serial, const QString & func)
596{
597 if (m_channelsIni.isEmpty() || m_channels.empty())
598 {
599 LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
600 emit SendMessage("FirstChannel", serial, "No channels configured.", "ERR");
601 return;
602 }
603
604 if (m_chanSettings == nullptr)
605 {
606 LOG(VB_CHANNEL, LOG_WARNING, LOC + ": Invalid channel configuration.");
607 emit SendMessage(func, serial, "Invalid channel configuration.", "ERR");
608 return;
609 }
610
611 if (m_channels.size() <= m_channelIdx)
612 {
613 LOG(VB_CHANNEL, LOG_WARNING, LOC + ": No more channels.");
614 emit SendMessage(func, serial, "No more channels", "ERR");
615 return;
616 }
617
618 QString channum = m_channels[m_channelIdx++];
619
620 m_chanSettings->beginGroup(channum);
621
622 QString name = m_chanSettings->value("NAME").toString();
623 QString callsign = m_chanSettings->value("CALLSIGN").toString();
624 QString xmltvid = m_chanSettings->value("XMLTVID").toString();
625 QString icon = m_chanSettings->value("ICON").toString();
626
627 m_chanSettings->endGroup();
628
629 LOG(VB_CHANNEL, LOG_INFO, LOC +
630 QString(": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4")
631 .arg(name, callsign, xmltvid, icon));
632
633 emit SendMessage(func, serial, QString("%1,%2,%3,%4,%5")
634 .arg(channum, name, callsign,
635 xmltvid, icon), "OK");
636}
637
638Q_SLOT void MythExternRecApp::FirstChannel(const QString & serial)
639{
640 m_channelIdx = 0;
641 GetChannel(serial, "FirstChannel");
642}
643
644Q_SLOT void MythExternRecApp::NextChannel(const QString & serial)
645{
646 GetChannel(serial, "NextChannel");
647}
648
650{
651 QString cmd = m_newEpisodeCommand;
652 int pos = cmd.lastIndexOf(QChar('&'));
653 bool background = false;
654
655 if (pos > 0)
656 {
657 background = true;
658 cmd = cmd.left(pos);
659 }
660
661 cmd = ReplaceCmdVariables(cmd);
662
664 cmd = args.takeFirst();
665
666 LOG(VB_RECORD, LOG_WARNING, LOC +
667 QString(" New episode starting on current channel: '%1'").arg(cmd));
668
669 if (m_tuneProc.state() == QProcess::Running)
671
672 m_tuneProc.start(cmd, args);
673 if (!m_tuneProc.waitForStarted())
674 {
675 LOG(VB_RECORD, LOG_ERR, LOC +
676 " NewEpisodeStarting: Failed to start process: " + ENO);
677 return;
678 }
679 if (background)
680 {
681 LOG(VB_RECORD, LOG_INFO, LOC +
682 "NewEpisodeStarting: running in background.");
683 return;
684 }
685
686 m_tuneProc.waitForFinished(5000);
687 if (m_tuneProc.state() == QProcess::NotRunning)
688 {
689 if (m_tuneProc.exitStatus() != QProcess::NormalExit)
690 {
691 LOG(VB_RECORD, LOG_ERR, LOC +
692 " NewEpisodeStarting: process failed: " + ENO);
693 return;
694 }
695 }
696 LOG(VB_RECORD, LOG_INFO, LOC + "NewEpisodeStarting: finished.");
697}
698
699Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
700 const QVariantMap & chaninfo)
701{
702 m_chaninfo = chaninfo;
703
704 if (m_tuneCommand.isEmpty() && m_channelsIni.isEmpty())
705 {
706 LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured.");
707 emit SendMessage("TuneChannel", serial, "No 'tuner' configured.", "ERR");
708 return;
709 }
710
711 QString channum = m_chaninfo["channum"].toString();
712
713 if (m_tunedChannel == channum)
714 {
715 if (!m_newEpisodeCommand.isEmpty())
717
718 LOG(VB_CHANNEL, LOG_INFO, LOC +
719 QString("TuneChannel: Already on %1").arg(channum));
720 emit SendMessage("TuneChannel", serial,
721 QString("Tunned to %1").arg(channum), "OK");
722 return;
723 }
724
727
728 QString tunecmd = m_tuneCommand;
729 QString url;
730 bool background = false;
731
733
734 if (!m_channelsIni.isEmpty())
735 {
736 QSettings settings(m_channelsIni, QSettings::IniFormat);
737 settings.beginGroup(channum);
738
739 QString cmd = settings.value("TUNE").toString();
740 if (!cmd.isEmpty())
741 {
742 cmd = ReplaceCmdVariables(cmd);
743 LOG(VB_CHANNEL, LOG_INFO, LOC +
744 QString(": Using tune cmd from '%1': '%2'")
745 .arg(m_channelsIni, cmd));
746 tunecmd = cmd;
747 }
748
749 url = settings.value("URL").toString();
750 if (!url.isEmpty())
751 {
752 if (tunecmd.indexOf("%URL%") >= 0)
753 {
754 tunecmd.replace("%URL%", url);
755 LOG(VB_CHANNEL, LOG_DEBUG, LOC +
756 QString(": '%URL%' replaced with '%1' in tunecmd: '%2'")
757 .arg(url, tunecmd));
758 }
759
760 if (m_command.indexOf("%URL%") >= 0)
761 {
762 m_command.replace("%URL%", url);
763 LOG(VB_CHANNEL, LOG_DEBUG, LOC +
764 QString(": '%URL%' replaced with '%1' in cmd: '%2'")
765 .arg(url, m_command));
766 }
767 }
768
769 m_desc.replace("%CHANNAME%", settings.value("NAME").toString());
770 m_desc.replace("%CALLSIGN%", settings.value("CALLSIGN").toString());
771
772 settings.endGroup();
773 }
774
775 if (m_tuneProc.state() == QProcess::Running)
777
778 int pos = tunecmd.lastIndexOf(QChar('&'));
779 if (pos > 0)
780 {
781 background = true;
782 tunecmd = tunecmd.left(pos);
783 }
784
785 tunecmd = ReplaceCmdVariables(tunecmd);
786
787 if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >= 0)
788 {
789 m_command.replace("%LOGFILE%", m_logFile);
790 LOG(VB_RECORD, LOG_DEBUG, LOC +
791 QString(": '%LOGFILE%' replaced with '%1' in cmd: '%2'")
792 .arg(m_logFile, m_command));
793 }
794
795 if (!m_logging.isEmpty() && m_command.indexOf("%LOGGING%") >= 0)
796 {
797 m_command.replace("%LOGGING%", m_logging);
798 LOG(VB_RECORD, LOG_DEBUG, LOC +
799 QString(": '%LOGGING%' replaced with '%1' in cmd: '%2'")
800 .arg(m_logging, m_command));
801 }
802
803 m_desc.replace("%URL%", url);
804
805 if (!tunecmd.isEmpty())
806 {
808 QString cmd = args.takeFirst();
809 m_tuningChannel = channum;
810 m_tuneProc.start(cmd, args);
811 if (!m_tuneProc.waitForStarted())
812 {
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");
816 return;
817 }
818
819 if (background)
820 {
821 LOG(VB_CHANNEL, LOG_INFO, LOC +
822 QString(": Started in background `%1` URL '%2'")
823 .arg(tunecmd, url));
824
826 m_tuningChannel.clear();
827 emit SetDescription(Desc());
828 emit SendMessage("TuneChannel", serial,
829 QString("Tuned `%1`").arg(m_tunedChannel), "OK");
830 }
831 else
832 {
833 LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Started `%1` URL '%2'")
834 .arg(tunecmd, url));
835 emit SendMessage("TuneChannel", serial,
836 QString("InProgress `%1`").arg(tunecmd), "OK");
837 }
838 }
839 else
840 {
841 m_tunedChannel = channum;
842 emit SetDescription(Desc());
843 emit SendMessage("TuneChannel", serial,
844 QString("Tuned to %1").arg(m_tunedChannel), "OK");
845 }
846}
847
848Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial)
849{
850 if (m_tuneProc.state() == QProcess::Running)
851 {
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");
855 return;
856 }
857
858 if (!m_tuneCommand.isEmpty() &&
859 m_tuneProc.exitStatus() != QProcess::NormalExit)
860 {
861 QString errmsg = QString("'%1' failed: ")
862 .arg(m_tuneProc.program()) + ENO;
863 LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
864 emit SendMessage("TuneStatus", serial, errmsg, "WARN");
865 return;
866 }
867
869 m_tuningChannel.clear();
870
871 LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Tuned %1").arg(m_tunedChannel));
872 emit SetDescription(Desc());
873 emit SendMessage("TuneChannel", serial,
874 QString("Tuned to %1").arg(m_tunedChannel), "OK");
875}
876
877Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial)
878{
879 if (!Open())
880 {
881 LOG(VB_CHANNEL, LOG_WARNING, LOC +
882 "Cannot read LockTimeout from config file.");
883 emit SendMessage("LockTimeout", serial, "Not open", "ERR");
884 return;
885 }
886
887 if (!m_channelsIni.isEmpty())
888 {
889 bool ok { false };
890 QSettings settings(m_channelsIni, QSettings::IniFormat);
891 settings.beginGroup(m_tuningChannel);
892 m_lockTimeout = settings.value("TIMEOUT", m_lockTimeout).toInt(&ok);
893 settings.endGroup();
894
895 if (ok)
896 {
897 LOG(VB_CHANNEL, LOG_INFO, LOC +
898 QString("Channel defined tune timeout: %1 (chan %2)")
899 .arg(m_lockTimeout).arg(m_tuningChannel));
900 }
901 else
902 {
903 LOG(VB_CHANNEL, LOG_DEBUG, LOC +
904 "No channel defined tune timeout");
905 }
906 }
907
908 if (m_lockTimeout > 0)
909 {
910 LOG(VB_CHANNEL, LOG_INFO, LOC +
911 QString("Using configured LockTimeout of %1").arg(m_lockTimeout));
912 emit SendMessage("LockTimeout", serial, QString::number(m_lockTimeout), "OK");
913 return;
914 }
915
916 LOG(VB_CHANNEL, LOG_INFO, LOC +
917 "No LockTimeout defined in config, defaulting to 12000ms");
918 emit SendMessage("LockTimeout", serial,
919 m_scanCommand.isEmpty() ? "12000" : "120000", "OK");
920}
921
922Q_SLOT void MythExternRecApp::HasTuner(const QString & serial)
923{
924 emit SendMessage("HasTuner", serial, m_tuneCommand.isEmpty() &&
925 m_channelsIni.isEmpty() ? "No" : "Yes", "OK");
926}
927
928Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial)
929{
930 emit SendMessage("HasPictureAttributes", serial, "No", "OK");
931}
932
933Q_SLOT void MythExternRecApp::SetBlockSize(const QString & serial, int blksz)
934{
935 m_blockSize = blksz;
936 emit SendMessage("BlockSize", serial, QString("Blocksize %1").arg(blksz), "OK");
937}
938
939Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial)
940{
941 m_streaming = true;
942 if (m_tunedChannel.isEmpty() && !m_channelsIni.isEmpty())
943 {
944 LOG(VB_RECORD, LOG_ERR, LOC + ": No channel has been tuned");
945 emit SendMessage("StartStreaming", serial,
946 "No channel has been tuned", "ERR");
947 return;
948 }
949
950 if (m_proc.state() == QProcess::Running)
951 {
952 LOG(VB_RECORD, LOG_ERR, LOC + ": Application already running");
953 emit SendMessage("StartStreaming", serial,
954 "Application already running", "WARN");
955 return;
956 }
957
958 QString streamcmd = m_command;
959
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);
964#else
965 m_proc.start(cmd, args, QIODeviceBase::ReadOnly | QIODeviceBase::Unbuffered);
966#endif
967 m_proc.setTextModeEnabled(false);
968 m_proc.setReadChannel(QProcess::StandardOutput);
969
970 LOG(VB_RECORD, LOG_INFO, LOC + QString(": Starting process '%1' args: '%2'")
971 .arg(m_proc.program(), m_proc.arguments().join(' ')));
972
973 if (!m_proc.waitForStarted())
974 {
975 LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start application.");
976 emit SendMessage("StartStreaming", serial,
977 "Failed to start application.", "ERR");
978 return;
979 }
980
981 std::this_thread::sleep_for(50ms);
982
983 if (m_proc.state() != QProcess::Running)
984 {
985 LOG(VB_RECORD, LOG_ERR, LOC + ": Application failed to start");
986 emit SendMessage("StartStreaming", serial,
987 "Application failed to start", "ERR");
988 return;
989 }
990
991 LOG(VB_RECORD, LOG_INFO, LOC + QString(": Started process '%1' PID %2")
992 .arg(m_proc.program()).arg(m_proc.processId()));
993
994 emit Streaming(true);
995 emit SetDescription(Desc());
996 emit SendMessage("StartStreaming", serial, "Streaming Started", "OK");
997}
998
999Q_SLOT void MythExternRecApp::StopStreaming(const QString & serial, bool silent)
1000{
1001 m_streaming = false;
1002 if (m_proc.state() == QProcess::Running)
1003 {
1004 TerminateProcess(m_proc, "Stop streaming app");
1005
1006 LOG(VB_RECORD, LOG_INFO, LOC + ": External application terminated.");
1007 if (silent)
1008 emit SendMessage("StopStreaming", serial, "Streaming Stopped", "STATUS");
1009 else
1010 emit SendMessage("StopStreaming", serial, "Streaming Stopped", "OK");
1011 }
1012 else
1013 {
1014 if (silent)
1015 {
1016 emit SendMessage("StopStreaming", serial,
1017 "Already not Streaming", "INFO");
1018 }
1019 else
1020 {
1021 emit SendMessage("StopStreaming", serial,
1022 "Already not Streaming", "WARN");
1023 }
1024 }
1025
1026 emit Streaming(false);
1027 emit SetDescription(Desc());
1028}
1029
1031{
1032 QString msg = QString("Process '%1' started").arg(m_proc.program());
1033 LOG(VB_RECORD, LOG_INFO, LOC + ": " + msg);
1034 MythLog(msg);
1035}
1036
1037Q_SLOT void MythExternRecApp::ProcFinished(int exitCode,
1038 QProcess::ExitStatus exitStatus)
1039{
1040 m_result = exitCode;
1041 QString msg = QString("%1Finished: %2 (exit code: %3)")
1042 .arg(exitStatus != QProcess::NormalExit ? "WARN:" : "",
1043 exitStatus == QProcess::NormalExit ? "OK" : "Abnormal exit",
1044 QString::number(m_result));
1045 LOG(VB_RECORD, LOG_INFO, LOC + ": " + msg);
1046
1047 if (m_streaming)
1048 emit Streaming(false);
1049 MythLog(msg);
1050}
1051
1052Q_SLOT void MythExternRecApp::ProcStateChanged(QProcess::ProcessState newState)
1053{
1054 bool unexpected = false;
1055 QString msg = "State Changed: ";
1056 switch (newState)
1057 {
1058 case QProcess::NotRunning:
1059 msg += "Not running";
1060 unexpected = m_streaming;
1061 break;
1062 case QProcess::Starting:
1063 msg += "Starting ";
1064 break;
1065 case QProcess::Running:
1066 msg += QString("Running PID %1").arg(m_proc.processId());
1067 break;
1068 }
1069
1070 LOG(VB_RECORD, LOG_INFO, LOC + msg);
1071 if (unexpected)
1072 {
1073 emit Streaming(false);
1074 emit SendMessage("STATUS", "0", "Unexpected: " + msg, "ERR");
1075 }
1076}
1077
1078Q_SLOT void MythExternRecApp::ProcError(QProcess::ProcessError /*error */)
1079{
1080 if (m_terminating)
1081 {
1082 LOG(VB_RECORD, LOG_INFO, LOC + QString(": %1")
1083 .arg(m_proc.errorString()));
1084 emit SendMessage("STATUS", "0", m_proc.errorString(), "INFO");
1085 }
1086 else
1087 {
1088 LOG(VB_RECORD, LOG_ERR, LOC + QString(": Error: %1")
1089 .arg(m_proc.errorString()));
1090 emit SendMessage("STATUS", "0", m_proc.errorString(), "ERR");
1091 }
1092}
1093
1095{
1096 QByteArray buf = m_proc.readAllStandardError();
1097 QString msg = QString::fromUtf8(buf).trimmed();
1098 QList<QString> msgs = msg.split('\n');
1099 QString message;
1100
1101 for (int idx=0; idx < msgs.count(); ++idx)
1102 {
1103 // Log any error messages
1104 if (!msgs[idx].isEmpty())
1105 {
1106 QStringList tokens = QString(msgs[idx])
1107 .split(':', Qt::SkipEmptyParts);
1108 tokens.removeFirst();
1109 if (tokens.empty())
1110 message = msgs[idx];
1111 else
1112 message = tokens.join(':');
1113 if (msgs[idx].startsWith("err", Qt::CaseInsensitive))
1114 {
1115 LOG(VB_RECORD, LOG_ERR, LOC + QString(">>> %1").arg(msgs[idx]));
1116 emit SendMessage("STATUS", "0", message, "ERR");
1117 }
1118 else if (msgs[idx].startsWith("warn", Qt::CaseInsensitive))
1119 {
1120 LOG(VB_RECORD, LOG_WARNING, LOC + QString(">>> %1").arg(msgs[idx]));
1121 emit SendMessage("STATUS", "0", message, "WARN");
1122 }
1123 else if (msgs[idx].startsWith("damage", Qt::CaseInsensitive))
1124 {
1125 LOG(VB_RECORD, LOG_WARNING, LOC + QString(">>> %1").arg(msgs[idx]));
1126 emit SendMessage("STATUS", "0", message, "DAMAGE");
1127 }
1128 else
1129 {
1130 LOG(VB_RECORD, LOG_DEBUG, LOC + QString(">>> %1").arg(msgs[idx]));
1131 emit SendMessage("STATUS", "0", message, "INFO");
1132 }
1133 }
1134 }
1135}
1136
1138{
1139 LOG(VB_RECORD, LOG_WARNING, LOC + ": Data ready.");
1140
1141 QByteArray buf = m_proc.read(m_blockSize);
1142 if (!buf.isEmpty())
1143 emit Fill(buf);
1144}
#define LOC
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)
void Streaming(bool val)
void NewEpisodeStarting(void)
std::condition_variable m_runCond
void Done(void)
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
QStringList m_channels
QMap< QString, QString > m_appEnv
void TuneChannel(const QString &serial, const QVariantMap &chaninfo)
void replace_variables(void)
void Opened(void)
void SendMessage(const QString &command, const QString &serial, const QString &message, const QString &status="")
QVariantMap m_chaninfo
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 Desc(void) const
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 uint32_t * p1
Definition: goom_core.cpp:28
static uint32_t * p2
Definition: goom_core.cpp:28
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:74
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
STL namespace.
bool exists(str path)
Definition: xbmcvfs.py:51
static QString cleanup(const QString &str)