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