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