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