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"
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 
59  m_desc = m_recDesc;
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 {} */
74 QString 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 
102 QString 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 
125 void 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 
146 QString 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 
329 void 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 
349 Q_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 
408 Q_SLOT void MythExternRecApp::Cleanup(void)
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 
480  QStringList args = MythCommandLineParser::MythSplitCommandString(startcmd);
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 
512 Q_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 
579 void 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 
622 Q_SLOT void MythExternRecApp::FirstChannel(const QString & serial)
623 {
624  m_channelIdx = 0;
625  GetChannel(serial, "FirstChannel");
626 }
627 
628 Q_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)
654  TerminateProcess(m_tuneProc, "Tune");
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 
683 Q_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 
709  m_desc = m_recDesc;
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)
758  TerminateProcess(m_tuneProc, "Tune");
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 
830 Q_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 
859 Q_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 
882 Q_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 
888 Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial)
889 {
890  emit SendMessage("HasPictureAttributes", serial, "No", "OK");
891 }
892 
893 Q_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 
899 Q_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 
920  QStringList args = MythCommandLineParser::MythSplitCommandString(streamcmd);
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 
955 Q_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 
993 Q_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 
1008 Q_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 
1034 Q_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 }
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:882
MythExternRecApp::m_chaninfo
QVariantMap m_chaninfo
Definition: MythExternRecApp.h:124
MythExternRecApp::TuneChannel
void TuneChannel(const QString &serial, const QVariantMap &chaninfo)
Definition: MythExternRecApp.cpp:683
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:74
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:955
MythExternRecApp::Streaming
void Streaming(bool val)
MythExternRecApp::MythExternRecApp
MythExternRecApp(QString command, QString conf_file, QString log_file, QString logging)
Definition: MythExternRecApp.cpp:41
MythExternRecApp::ProcFinished
void ProcFinished(int exitCode, QProcess::ExitStatus exitStatus)
Definition: MythExternRecApp.cpp:993
MythExternRecApp::m_blockSize
uint m_blockSize
Definition: MythExternRecApp.h:113
MythExternRecApp::NewEpisodeStarting
void NewEpisodeStarting(void)
Definition: MythExternRecApp.cpp:633
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:329
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:375
MythExternRecApp::Done
void Done(void)
MythExternRecApp::m_channels
QStringList m_channels
Definition: MythExternRecApp.h:147
LOC
#define LOC
Definition: MythExternRecApp.cpp:39
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythExternRecApp::Open
bool Open(void)
Definition: MythExternRecApp.cpp:270
MythExternRecApp::SetBlockSize
void SetBlockSize(const QString &serial, int blksz)
Definition: MythExternRecApp.cpp:893
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:888
MythExternRecApp::TuneStatus
void TuneStatus(const QString &serial)
Definition: MythExternRecApp.cpp:830
MythExternRecApp::LockTimeout
void LockTimeout(const QString &serial)
Definition: MythExternRecApp.cpp:859
MythExternRecApp::m_cleanup
QString m_cleanup
Definition: MythExternRecApp.h:117
MythExternRecApp::Close
void Close(void)
Definition: MythExternRecApp.cpp:349
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:512
MythExternRecApp::m_tuningChannel
QString m_tuningChannel
Definition: MythExternRecApp.h:142
mythlogging.h
MythExternRecApp::m_result
int m_result
Definition: MythExternRecApp.h:110
MythExternRecApp::Cleanup
void Cleanup(void)
Definition: MythExternRecApp.cpp:408
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:1088
MythExternRecApp::m_terminating
bool m_terminating
Definition: MythExternRecApp.h:102
MythExternRecApp::ProcReadStandardError
void ProcReadStandardError(void)
Definition: MythExternRecApp.cpp:1050
MythExternRecApp::GetChannel
void GetChannel(const QString &serial, const QString &func)
Definition: MythExternRecApp.cpp:579
MythExternRecApp::sanitize_var
static QString sanitize_var(const QString &var)
Definition: MythExternRecApp.cpp:74
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:628
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:1034
MythExternRecApp::~MythExternRecApp
~MythExternRecApp(void) override
Definition: MythExternRecApp.cpp:67
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:622
MythExternRecApp::config
bool config(void)
Definition: MythExternRecApp.cpp:159
MythExternRecApp::m_fatal
bool m_fatal
Definition: MythExternRecApp.h:103
MythExternRecApp::DataStarted
void DataStarted(void)
Definition: MythExternRecApp.cpp:444
mythchrono.h
MythExternRecApp::ReplaceVariables
void ReplaceVariables(QString &cmd) const
Definition: MythExternRecApp.cpp:125
MythExternRecApp::Desc
QString Desc(void) const
Definition: MythExternRecApp.cpp:146
MythExternRecApp::StartStreaming
void StartStreaming(const QString &serial)
Definition: MythExternRecApp.cpp:899
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:102
MythExternRecApp::m_channelsIni
QString m_channelsIni
Definition: MythExternRecApp.h:131
MythExternRecApp::ProcStarted
void ProcStarted(void)
Definition: MythExternRecApp.cpp:986
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:1008