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