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 #include <thread>
22 #include <csignal>
23 #include "commandlineparser.h"
24 #include "MythExternRecApp.h"
25 
26 #include <QElapsedTimer>
27 #include <QFileInfo>
28 #include <QProcess>
29 #include <QtCore/QtCore>
30 
31 #define LOC Desc()
32 
34  QString conf_file,
35  QString log_file,
36  QString logging)
37  : m_recCommand(std::move(command))
38  , m_logFile(std::move(log_file))
39  , m_logging(std::move(logging))
40  , m_configIni(std::move(conf_file))
41 {
42  if (m_configIni.isEmpty() || !config())
44 
45  if (m_tuneCommand.isEmpty())
47 
48  LOG(VB_CHANNEL, LOG_INFO, LOC +
49  QString("Channels in '%1', Tuner: '%2', Scanner: '%3'")
50  .arg(m_channelsIni).arg(m_tuneCommand).arg(m_scanCommand));
51 
52  m_desc = m_recDesc;
53  m_desc.replace("%URL%", "");
54  m_desc.replace("%CHANNUM%", "");
55  m_desc.replace("%CHANNAME%", "");
56  m_desc.replace("%CALLSIGN%", "");
57  emit SetDescription(m_desc);
58 }
59 
61 {
62  Close();
63 }
64 
65 QString MythExternRecApp::Desc(void) const
66 {
67  QString extra;
68 
69  if (m_proc.processId() > 0)
70  extra = QString("(pid %1) ").arg(m_proc.processId());
71 
72  return QString("%1%2 ").arg(extra).arg(m_desc);
73 }
74 
76 {
77  QSettings settings(m_configIni, QSettings::IniFormat);
78 
79  if (!settings.contains("RECORDER/command"))
80  {
81  LOG(VB_GENERAL, LOG_CRIT, "ini file missing [RECORDER]/command");
82  m_fatal = true;
83  return false;
84  }
85 
86  m_recCommand = settings.value("RECORDER/command").toString();
87  m_recDesc = settings.value("RECORDER/desc").toString();
88  m_tuneCommand = settings.value("TUNER/command", "").toString();
89  m_channelsIni = settings.value("TUNER/channels", "").toString();
90  m_lockTimeout = settings.value("TUNER/timeout", "").toInt();
91  m_scanCommand = settings.value("SCANNER/command", "").toString();
92  m_scanTimeout = settings.value("SCANNER/timeout", "").toInt();
93 
94  settings.beginGroup("ENVIRONMENT");
95 
96  m_appEnv.clear();
97  QStringList keys = settings.childKeys();
98  QStringList::const_iterator Ienv;
99  for (Ienv = keys.constBegin(); Ienv != keys.constEnd(); ++Ienv)
100  {
101  if (!(*Ienv).isEmpty() && (*Ienv)[0] != '#')
102  m_appEnv.insert((*Ienv).toLocal8Bit().constData(),
103  settings.value(*Ienv).toString());
104  }
105 
106  if (!m_channelsIni.isEmpty())
107  {
108  if (!QFileInfo::exists(m_channelsIni))
109  {
110  // Assume the channels config is in the same directory as
111  // main config
112  QDir conf_path = QFileInfo(m_configIni).absolutePath();
113  QFileInfo ini(conf_path, m_channelsIni);
114  m_channelsIni = ini.absoluteFilePath();
115  }
116  }
117 
118  return true;
119 }
120 
122 {
123  if (m_fatal)
124  {
125  emit SendMessage("Open", "0", "ERR:Already dead.");
126  return false;
127  }
128 
129  if (m_command.isEmpty())
130  {
131  LOG(VB_RECORD, LOG_ERR, LOC + ": No recorder provided.");
132  emit SendMessage("Open", "0", "ERR:No recorder provided.");
133  return false;
134  }
135 
136  if (!m_appEnv.isEmpty())
137  {
138  QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
139  QMap<QString, QString>::const_iterator Ienv;
140  for (Ienv = m_appEnv.constBegin();
141  Ienv != m_appEnv.constEnd(); ++Ienv)
142  {
143  env.insert(Ienv.key(), Ienv.value());
144  LOG(VB_RECORD, LOG_INFO, LOC + QString(" ENV: '%1' = '%2'")
145  .arg(Ienv.key()).arg(Ienv.value()));
146  }
147  m_proc.setProcessEnvironment(env);
148  }
149 
150  QObject::connect(&m_proc, &QProcess::started, this,
152 #if 0
153  QObject::connect(&m_proc, &QProcess::readyReadStandardOutput, this,
155 #endif
156  QObject::connect(&m_proc, &QProcess::readyReadStandardError, this,
158 
159  qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
160  QObject::connect(&m_proc, &QProcess::errorOccurred,
162 
163  qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");
164  QObject::connect(&m_proc,
165  static_cast<void (QProcess::*)
166  (int,QProcess::ExitStatus exitStatus)>
167  (&QProcess::finished),
169 
170  qRegisterMetaType<QProcess::ProcessState>("QProcess::ProcessState");
171  QObject::connect(&m_proc, &QProcess::stateChanged, this,
173 
174  LOG(VB_RECORD, LOG_INFO, LOC + ": Opened");
175 
176  emit Opened();
177  return true;
178 }
179 
181 {
182  if (m_proc.state() == QProcess::Running)
183  {
184  LOG(VB_RECORD, LOG_INFO, LOC +
185  QString("Sending SIGINT to %1").arg(m_proc.pid()));
186  kill(m_proc.pid(), SIGINT);
187  m_proc.waitForFinished(5000);
188  }
189  if (m_proc.state() == QProcess::Running)
190  {
191  LOG(VB_RECORD, LOG_INFO, LOC +
192  QString("Sending SIGTERM to %1").arg(m_proc.pid()));
193  m_proc.terminate();
194  m_proc.waitForFinished();
195  }
196  if (m_proc.state() == QProcess::Running)
197  {
198  LOG(VB_RECORD, LOG_INFO, LOC +
199  QString("Sending SIGKILL to %1").arg(m_proc.pid()));
200  m_proc.kill();
201  m_proc.waitForFinished();
202  }
203 }
204 
205 Q_SLOT void MythExternRecApp::Close(void)
206 {
207  if (m_run)
208  {
209  LOG(VB_RECORD, LOG_INFO, LOC + ": Closing application.");
210  m_run = false;
211  m_runCond.notify_all();
212  std::this_thread::sleep_for(std::chrono::microseconds(50));
213  }
214 
215  if (m_proc.state() == QProcess::Running)
216  {
217  m_proc.closeReadChannel(QProcess::StandardOutput);
219  std::this_thread::sleep_for(std::chrono::microseconds(50));
220  }
221 
222  emit Done();
223 }
224 
226 {
227  QByteArray buf;
228 
229  while (m_run)
230  {
231  {
232  std::unique_lock<std::mutex> lk(m_runMutex);
233  m_runCond.wait_for(lk, std::chrono::milliseconds(10));
234  }
235 
236  if (m_proc.state() == QProcess::Running)
237  {
238  if (m_proc.waitForReadyRead(50))
239  {
240  buf = m_proc.read(m_blockSize);
241  if (!buf.isEmpty())
242  emit Fill(buf);
243  }
244  }
245 
246  qApp->processEvents();
247  }
248 
249  if (m_proc.state() == QProcess::Running)
250  {
251  m_proc.closeReadChannel(QProcess::StandardOutput);
253  }
254 
255  emit Done();
256 }
257 
258 Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial)
259 {
260  if (m_channelsIni.isEmpty())
261  {
262  LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
263  emit SendMessage("LoadChannels", serial, "ERR:No channels configured.");
264  return;
265  }
266 
267  if (!m_scanCommand.isEmpty())
268  {
269  QString cmd = m_scanCommand;
270  cmd.replace("%CHANCONF%", m_channelsIni);
271 
272  QProcess scanner;
273  scanner.start(cmd);
274 
275  if (!scanner.waitForStarted())
276  {
277  QString errmsg = QString("Failed to start '%1': ").arg(cmd) + ENO;
278  LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
279  emit SendMessage("LoadChannels", serial,
280  QString("ERR:%1").arg(errmsg));
281  return;
282  }
283 
284  QByteArray buf;
285  QElapsedTimer timer;
286 
287  timer.start();
288  while (timer.elapsed() < m_scanTimeout)
289  {
290  if (scanner.waitForReadyRead(50))
291  {
292  buf = scanner.readLine();
293  if (!buf.isEmpty())
294  {
295  LOG(VB_RECORD, LOG_INFO, LOC + ": " + buf);
296  MythLog(buf);
297  }
298  }
299 
300  if (scanner.state() != QProcess::Running)
301  break;
302 
303  if (scanner.waitForFinished(50 /* msecs */))
304  break;
305  }
306  if (timer.elapsed() >= m_scanTimeout)
307  {
308  QString errmsg = QString("Timedout waiting for '%1'").arg(cmd);
309  LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
310  emit SendMessage("LoadChannels", serial,
311  QString("ERR:%1").arg(errmsg));
312  return;
313  }
314  }
315 
316  if (m_chanSettings == nullptr)
317  m_chanSettings = new QSettings(m_channelsIni, QSettings::IniFormat);
318  m_chanSettings->sync();
319  m_channels = m_chanSettings->childGroups();
320 
321  emit SendMessage("LoadChannels", serial,
322  QString("OK:%1").arg(m_channels.size()));
323 }
324 
325 void MythExternRecApp::GetChannel(const QString & serial, const QString & func)
326 {
327  if (m_channelsIni.isEmpty() || m_channels.empty())
328  {
329  LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
330  emit SendMessage("FirstChannel", serial,
331  QString("ERR:No channels configured."));
332  return;
333  }
334 
335  if (m_chanSettings == nullptr)
336  {
337  LOG(VB_CHANNEL, LOG_WARNING, LOC + ": Invalid channel configuration.");
338  emit SendMessage(func, serial,
339  "ERR:Invalid channel configuration.");
340  return;
341  }
342 
343  if (m_channels.size() <= m_channelIdx)
344  {
345  LOG(VB_CHANNEL, LOG_WARNING, LOC + ": No more channels.");
346  emit SendMessage(func, serial, "ERR:No more channels.");
347  return;
348  }
349 
350  QString channum = m_channels[m_channelIdx++];
351 
352  m_chanSettings->beginGroup(channum);
353 
354  QString name = m_chanSettings->value("NAME").toString();
355  QString callsign = m_chanSettings->value("CALLSIGN").toString();
356  QString xmltvid = m_chanSettings->value("XMLTVID").toString();
357 
358  m_chanSettings->endGroup();
359 
360  LOG(VB_CHANNEL, LOG_INFO, LOC +
361  QString(": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3")
362  .arg(name).arg(callsign).arg(xmltvid));
363 
364  emit SendMessage(func, serial, QString("OK:%1,%2,%3,%4")
365  .arg(channum).arg(name).arg(callsign).arg(xmltvid));
366 }
367 
368 Q_SLOT void MythExternRecApp::FirstChannel(const QString & serial)
369 {
370  m_channelIdx = 0;
371  GetChannel(serial, "FirstChannel");
372 }
373 
374 Q_SLOT void MythExternRecApp::NextChannel(const QString & serial)
375 {
376  GetChannel(serial, "NextChannel");
377 }
378 
379 Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
380  const QString & channum)
381 {
382  if (m_channelsIni.isEmpty())
383  {
384  LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
385  emit SendMessage("TuneChannel", serial, "ERR:No channels configured.");
386  return;
387  }
388 
389  QSettings settings(m_channelsIni, QSettings::IniFormat);
390  settings.beginGroup(channum);
391 
392  QString url(settings.value("URL").toString());
393 
394  if (url.isEmpty())
395  {
396  QString msg = QString("Channel number [%1] is missing a URL.")
397  .arg(channum);
398 
399  LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg);
400 
401  emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(msg));
402  return;
403  }
404 
405  if (!m_tuneCommand.isEmpty())
406  {
407  // Repalce URL in command and execute it
408  QString tune = m_tuneCommand;
409  tune.replace("%URL%", url);
410 
411  if (system(tune.toUtf8().constData()) != 0)
412  {
413  QString errmsg = QString("'%1' failed: ").arg(tune) + ENO;
414  LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg);
415  emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(errmsg));
416  return;
417  }
418  LOG(VB_CHANNEL, LOG_INFO, LOC +
419  QString(": TuneChannel, ran '%1'").arg(tune));
420  }
421 
422  // Replace URL in recorder command
424 
425  if (!url.isEmpty() && m_command.indexOf("%URL%") >= 0)
426  {
427  m_command.replace("%URL%", url);
428  LOG(VB_CHANNEL, LOG_DEBUG, LOC +
429  QString(": '%URL%' replaced with '%1' in cmd: '%2'")
430  .arg(url).arg(m_command));
431  }
432 
433  if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >= 0)
434  {
435  m_command.replace("%LOGFILE%", m_logFile);
436  LOG(VB_RECORD, LOG_DEBUG, LOC +
437  QString(": '%LOGFILE%' replaced with '%1' in cmd: '%2'")
438  .arg(m_logFile).arg(m_command));
439  }
440 
441  if (!m_logging.isEmpty() && m_command.indexOf("%LOGGING%") >= 0)
442  {
443  m_command.replace("%LOGGING%", m_logging);
444  LOG(VB_RECORD, LOG_DEBUG, LOC +
445  QString(": '%LOGGING%' replaced with '%1' in cmd: '%2'")
446  .arg(m_logging).arg(m_command));
447  }
448 
449  m_desc = m_recDesc;
450  m_desc.replace("%URL%", url);
451  m_desc.replace("%CHANNUM%", channum);
452  m_desc.replace("%CHANNAME%", settings.value("NAME").toString());
453  m_desc.replace("%CALLSIGN%", settings.value("CALLSIGN").toString());
454 
455  settings.endGroup();
456 
457  LOG(VB_CHANNEL, LOG_INFO, LOC +
458  QString(": TuneChannel %1: URL '%2'").arg(channum).arg(url));
459  m_tuned = true;
460 
461  emit SetDescription(Desc());
462  emit SendMessage("TuneChannel", serial,
463  QString("OK:Tunned to %1").arg(channum));
464 }
465 
466 Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial)
467 {
468  if (!Open())
469  return;
470 
471  if (m_lockTimeout > 0)
472  emit SendMessage("LockTimeout", serial,
473  QString("OK:%1").arg(m_lockTimeout));
474  emit SendMessage("LockTimeout", serial, QString("OK:%1")
475  .arg(m_scanCommand.isEmpty() ? 12000 : 120000));
476 }
477 
478 Q_SLOT void MythExternRecApp::HasTuner(const QString & serial)
479 {
480  emit SendMessage("HasTuner", serial, QString("OK:%1")
481  .arg(m_channelsIni.isEmpty() ? "No" : "Yes"));
482 }
483 
484 Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial)
485 {
486  emit SendMessage("HasPictureAttributes", serial, "OK:No");
487 }
488 
489 Q_SLOT void MythExternRecApp::SetBlockSize(const QString & serial, int blksz)
490 {
491  m_blockSize = blksz;
492  emit SendMessage("BlockSize", serial, "OK");
493 }
494 
495 Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial)
496 {
497  m_streaming = true;
498  if (!m_tuned && !m_channelsIni.isEmpty())
499  {
500  LOG(VB_RECORD, LOG_ERR, LOC + ": No channel has been tuned");
501  emit SendMessage("StartStreaming", serial,
502  "ERR:No channel has been tuned");
503  return;
504  }
505 
506  if (m_proc.state() == QProcess::Running)
507  {
508  LOG(VB_RECORD, LOG_ERR, LOC + ": Application already running");
509  emit SendMessage("StartStreaming", serial,
510  "WARN:Application already running");
511  return;
512  }
513 
514  m_proc.start(m_command, QIODevice::ReadOnly|QIODevice::Unbuffered);
515  m_proc.setTextModeEnabled(false);
516  m_proc.setReadChannel(QProcess::StandardOutput);
517 
518  LOG(VB_RECORD, LOG_INFO, LOC + QString(": Starting process '%1' args: '%2'")
519  .arg(m_proc.program()).arg(m_proc.arguments().join(' ')));
520 
521  if (!m_proc.waitForStarted())
522  {
523  LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start application.");
524  emit SendMessage("StartStreaming", serial,
525  "ERR:Failed to start application.");
526  return;
527  }
528 
529  std::this_thread::sleep_for(std::chrono::milliseconds(50));
530 
531  if (m_proc.state() != QProcess::Running)
532  {
533  LOG(VB_RECORD, LOG_ERR, LOC + ": Application failed to start");
534  emit SendMessage("StartStreaming", serial,
535  "ERR:Application failed to start");
536  return;
537  }
538 
539  LOG(VB_RECORD, LOG_INFO, LOC + QString(": Started process '%1' PID %2")
540  .arg(m_proc.program()).arg(m_proc.processId()));
541 
542  emit Streaming(true);
543  emit SetDescription(Desc());
544  emit SendMessage("StartStreaming", serial, "OK:Streaming Started");
545 }
546 
547 Q_SLOT void MythExternRecApp::StopStreaming(const QString & serial, bool silent)
548 {
549  m_streaming = false;
550  if (m_proc.state() == QProcess::Running)
551  {
553 
554  LOG(VB_RECORD, LOG_INFO, LOC + ": External application terminated.");
555  if (silent)
556  emit SendMessage("StopStreaming", serial, "STATUS:Streaming Stopped");
557  else
558  emit SendMessage("StopStreaming", serial, "OK:Streaming Stopped");
559  }
560  else
561  {
562  if (silent)
563  {
564  emit SendMessage("StopStreaming", serial,
565  "STATUS:Already not Streaming");
566  }
567  else
568  {
569  emit SendMessage("StopStreaming", serial,
570  "WARN:Already not Streaming");
571  }
572  }
573 
574  emit Streaming(false);
575  emit SetDescription(Desc());
576 }
577 
579 {
580  QString msg = QString("Process '%1' started").arg(m_proc.program());
581  LOG(VB_RECORD, LOG_INFO, LOC + ": " + msg);
582  MythLog(msg);
583 }
584 
585 Q_SLOT void MythExternRecApp::ProcFinished(int exitCode,
586  QProcess::ExitStatus exitStatus)
587 {
588  m_result = exitCode;
589  QString msg = QString("%1Finished: %2 (exit code: %3)")
590  .arg(exitStatus != QProcess::NormalExit ? "WARN:" : "")
591  .arg(exitStatus == QProcess::NormalExit ? "OK" :
592  "Abnormal exit")
593  .arg(m_result);
594  LOG(VB_RECORD, LOG_INFO, LOC + ": " + msg);
595 
596  if (m_streaming)
597  emit Streaming(false);
598  MythLog(msg);
599 }
600 
601 Q_SLOT void MythExternRecApp::ProcStateChanged(QProcess::ProcessState newState)
602 {
603  bool unexpected = false;
604  QString msg = "State Changed: ";
605  switch (newState)
606  {
607  case QProcess::NotRunning:
608  msg += "Not running";
609  unexpected = m_streaming;
610  break;
611  case QProcess::Starting:
612  msg += "Starting ";
613  break;
614  case QProcess::Running:
615  msg += QString("Running PID %1").arg(m_proc.processId());
616  break;
617  }
618 
619  LOG(VB_RECORD, LOG_INFO, LOC + msg);
620  if (unexpected)
621  {
622  emit Streaming(false);
623  MythLog("ERR Unexpected " + msg);
624  }
625 }
626 
627 Q_SLOT void MythExternRecApp::ProcError(QProcess::ProcessError /*error */)
628 {
629  LOG(VB_RECORD, LOG_ERR, LOC + QString(": Error: %1")
630  .arg(m_proc.errorString()));
631  MythLog(m_proc.errorString());
632 }
633 
635 {
636  QByteArray buf = m_proc.readAllStandardError();
637  QString msg = QString::fromUtf8(buf).trimmed();
638 
639  // Log any error messages
640  if (!msg.isEmpty())
641  {
642  LOG(VB_RECORD, LOG_INFO, LOC + QString(">>> %1")
643  .arg(msg));
644  if (msg.size() > 79)
645  msg = QString("Application message: see '%1'").arg(m_logFile);
646 
647  MythLog(msg);
648  }
649 }
650 
652 {
653  LOG(VB_RECORD, LOG_WARNING, LOC + ": Data ready.");
654 
655  QByteArray buf = m_proc.read(m_blockSize);
656  if (!buf.isEmpty())
657  emit Fill(buf);
658 }
void ProcReadStandardOutput(void)
QSettings * m_chanSettings
void NextChannel(const QString &serial)
void ProcError(QProcess::ProcessError error)
void FirstChannel(const QString &serial)
~MythExternRecApp(void) override
void TerminateProcess(void)
void SendMessage(const QString &func, const QString &serial, const QString &msg)
QString Desc(void) const
QMap< QString, QString > m_appEnv
std::condition_variable m_runCond
void StartStreaming(const QString &serial)
void HasTuner(const QString &serial)
void Fill(const QByteArray &buffer)
void ProcStateChanged(QProcess::ProcessState newState)
void Streaming(bool val)
MythExternRecApp(QString command, QString conf_file, QString log_file, QString logging)
void SetDescription(const QString &desc)
void ProcFinished(int exitCode, QProcess::ExitStatus exitStatus)
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:99
std::mutex m_runMutex
void StopStreaming(const QString &serial, bool silent)
std::atomic< bool > m_run
void Done(void)
QStringList m_channels
#define LOC
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
void SetBlockSize(const QString &serial, int blksz)
void MythLog(const QString &msg)
void LockTimeout(const QString &serial)
void TuneChannel(const QString &serial, const QString &channum)
void HasPictureAttributes(const QString &serial)
void LoadChannels(const QString &serial)
void Opened(void)
std::atomic< bool > m_streaming
void GetChannel(const QString &serial, const QString &func)
void ProcReadStandardError(void)