MythTV  master
ExternalStreamHandler.cpp
Go to the documentation of this file.
1 // -*- Mode: c++ -*-
2 
3 // POSIX headers
4 #include <thread>
5 #include <iostream>
6 #include <fcntl.h>
7 #include <unistd.h>
8 #include <algorithm>
9 #if !defined( USING_MINGW ) && !defined( _MSC_VER )
10 #include <poll.h>
11 #include <sys/ioctl.h>
12 #endif
13 #ifdef ANDROID
14 #include <sys/wait.h>
15 #endif
16 
17 // Qt headers
18 #include <QString>
19 #include <QFile>
20 
21 // MythTV headers
22 #include "ExternalStreamHandler.h"
23 #include "ExternalChannel.h"
24 //#include "ThreadedFileWriter.h"
25 #include "dtvsignalmonitor.h"
26 #include "streamlisteners.h"
27 #include "mpegstreamdata.h"
28 #include "cardutil.h"
29 #include "exitcodes.h"
30 
31 #define LOC QString("ExternSH[%1](%2): ").arg(m_inputId).arg(m_loc)
32 
33 ExternIO::ExternIO(const QString & app,
34  const QStringList & args)
35  : m_status(&m_statusBuf, QIODevice::ReadWrite)
36 {
37  m_app = (app);
38 
39  if (!m_app.exists())
40  {
41  m_error = QString("ExternIO: '%1' does not exist.").arg(app);
42  return;
43  }
44  if (!m_app.isReadable() || !m_app.isFile())
45  {
46  m_error = QString("ExternIO: '%1' is not readable.")
47  .arg(m_app.canonicalFilePath());
48  return;
49  }
50  if (!m_app.isExecutable())
51  {
52  m_error = QString("ExternIO: '%1' is not executable.")
53  .arg(m_app.canonicalFilePath());
54  return;
55  }
56 
57  m_args = args;
58  m_args.prepend(m_app.baseName());
59 
60  m_status.setString(&m_statusBuf);
61 }
62 
64 {
65  close(m_appIn);
66  close(m_appOut);
67  close(m_appErr);
68 
69  // waitpid(m_pid, &status, 0);
70  delete[] m_buffer;
71 }
72 
73 bool ExternIO::Ready(int fd, int timeout, const QString & what)
74 {
75 #if !defined( USING_MINGW ) && !defined( _MSC_VER )
76  struct pollfd m_poll[2];
77  memset(m_poll, 0, sizeof(m_poll));
78 
79  m_poll[0].fd = fd;
80  m_poll[0].events = POLLIN | POLLPRI;
81  int ret = poll(m_poll, 1, timeout);
82 
83  if (m_poll[0].revents & POLLHUP)
84  {
85  m_error = what + " poll eof (POLLHUP)";
86  return false;
87  }
88  if (m_poll[0].revents & POLLNVAL)
89  {
90  LOG(VB_GENERAL, LOG_ERR, "poll error");
91  return false;
92  }
93  if (m_poll[0].revents & POLLIN)
94  {
95  if (ret > 0)
96  return true;
97 
98  if ((EOVERFLOW == errno))
99  m_error = "poll overflow";
100  return false;
101  }
102 #endif // !defined( USING_MINGW ) && !defined( _MSC_VER )
103  return false;
104 }
105 
106 int ExternIO::Read(QByteArray & buffer, int maxlen, int timeout)
107 {
108  if (Error())
109  {
110  LOG(VB_RECORD, LOG_ERR,
111  QString("ExternIO::Read: already in error state: '%1'")
112  .arg(m_error));
113  return 0;
114  }
115 
116  if (!Ready(m_appOut, timeout, "data"))
117  return 0;
118 
119  if (m_bufSize < maxlen)
120  {
121  m_bufSize = maxlen;
122  delete m_buffer;
123  m_buffer = new char[m_bufSize];
124  }
125 
126  int len = read(m_appOut, m_buffer, maxlen);
127 
128  if (len < 0)
129  {
130  if (errno == EAGAIN)
131  {
132  if (++m_errCnt > kMaxErrorCnt)
133  {
134  m_error = "Failed to read from External Recorder: " + ENO;
135  LOG(VB_RECORD, LOG_WARNING,
136  "External Recorder not ready. Giving up.");
137  }
138  else
139  {
140  LOG(VB_RECORD, LOG_WARNING,
141  QString("External Recorder not ready. Will retry (%1/%2).")
142  .arg(m_errCnt).arg(kMaxErrorCnt));
143  std::this_thread::sleep_for(std::chrono::milliseconds(100));
144  }
145  }
146  else
147  {
148  m_error = "Failed to read from External Recorder: " + ENO;
149  LOG(VB_RECORD, LOG_ERR, m_error);
150  }
151  }
152  else
153  m_errCnt = 0;
154 
155  if (len == 0)
156  return 0;
157 
158  buffer.append(m_buffer, len);
159 
160  LOG(VB_RECORD, LOG_DEBUG,
161  QString("ExternIO::Read '%1' bytes, buffer size %2")
162  .arg(len).arg(buffer.size()));
163 
164  return len;
165 }
166 
168 {
169  if (Error())
170  {
171  LOG(VB_RECORD, LOG_ERR,
172  QString("ExternIO::GetStatus: already in error state: '%1'")
173  .arg(m_error));
174  return QByteArray();
175  }
176 
177  int waitfor = m_status.atEnd() ? timeout : 0;
178  if (Ready(m_appErr, waitfor, "status"))
179  {
180  char buffer[2048];
181  int len = read(m_appErr, buffer, 2048);
182  m_status << QString::fromLatin1(buffer, len);
183  }
184 
185  if (m_status.atEnd())
186  return QByteArray();
187 
188  QString msg = m_status.readLine();
189 
190  LOG(VB_RECORD, LOG_DEBUG, QString("ExternIO::GetStatus '%1'")
191  .arg(msg));
192 
193  return msg;
194 }
195 
196 int ExternIO::Write(const QByteArray & buffer)
197 {
198  if (Error())
199  {
200  LOG(VB_RECORD, LOG_ERR,
201  QString("ExternIO::Write: already in error state: '%1'")
202  .arg(m_error));
203  return -1;
204  }
205 
206  LOG(VB_RECORD, LOG_DEBUG, QString("ExternIO::Write('%1')")
207  .arg(QString(buffer)));
208 
209  int len = write(m_appIn, buffer.constData(), buffer.size());
210  if (len != buffer.size())
211  {
212  if (len > 0)
213  {
214  LOG(VB_RECORD, LOG_WARNING,
215  QString("ExternIO::Write: only wrote %1 of %2 bytes '%3'")
216  .arg(len).arg(buffer.size()).arg(QString(buffer)));
217  }
218  else
219  {
220  m_error = QString("ExternIO: Failed to write '%1' to app's stdin: ")
221  .arg(QString(buffer)) + ENO;
222  return -1;
223  }
224  }
225 
226  return len;
227 }
228 
229 bool ExternIO::Run(void)
230 {
231  LOG(VB_RECORD, LOG_INFO, QString("ExternIO::Run()"));
232 
233  Fork();
234  GetStatus(10);
235 
236  return true;
237 }
238 
239 /* Return true if the process is not, or is no longer running */
240 bool ExternIO::KillIfRunning(const QString & cmd)
241 {
242 #if CONFIG_DARWIN || (__FreeBSD__) || defined(__OpenBSD__)
243  Q_UNUSED(cmd);
244  return false;
245 #elif defined USING_MINGW
246  Q_UNUSED(cmd);
247  return false;
248 #elif defined( _MSC_VER )
249  Q_UNUSED(cmd);
250  return false;
251 #else
252  QString grp = QString("pgrep -x -f -- \"%1\" 2>&1 > /dev/null").arg(cmd);
253  QString kil = QString("pkill --signal 15 -x -f -- \"%1\" 2>&1 > /dev/null")
254  .arg(cmd);
255 
256  int res_grp = system(grp.toUtf8().constData());
257  if (WEXITSTATUS(res_grp) == 1)
258  {
259  LOG(VB_RECORD, LOG_DEBUG, QString("'%1' not running.").arg(cmd));
260  return true;
261  }
262 
263  LOG(VB_RECORD, LOG_WARNING, QString("'%1' already running, killing...")
264  .arg(cmd));
265  int res_kil = system(kil.toUtf8().constData());
266  if (WEXITSTATUS(res_kil) == 1)
267  LOG(VB_GENERAL, LOG_WARNING, QString("'%1' failed: %2")
268  .arg(kil).arg(ENO));
269 
270  res_grp = system(grp.toUtf8().constData());
271  if (WEXITSTATUS(res_grp) == 1)
272  {
273  LOG(WEXITSTATUS(res_kil) == 0 ? VB_RECORD : VB_GENERAL, LOG_WARNING,
274  QString("'%1' terminated.").arg(cmd));
275  return true;
276  }
277 
278  std::this_thread::sleep_for(std::chrono::milliseconds(50));
279 
280  kil = QString("pkill --signal 9 -x -f \"%1\" 2>&1 > /dev/null").arg(cmd);
281  res_kil = system(kil.toUtf8().constData());
282  if (WEXITSTATUS(res_kil) > 0)
283  LOG(VB_GENERAL, LOG_WARNING, QString("'%1' failed: %2")
284  .arg(kil).arg(ENO));
285 
286  res_grp = system(grp.toUtf8().constData());
287  LOG(WEXITSTATUS(res_kil) == 0 ? VB_RECORD : VB_GENERAL, LOG_WARNING,
288  QString("'%1' %2.").arg(cmd)
289  .arg(WEXITSTATUS(res_grp) == 0 ? "sill running" : "terminated"));
290 
291  return (WEXITSTATUS(res_grp) != 0);
292 #endif
293 }
294 
295 void ExternIO::Fork(void)
296 {
297 #if !defined( USING_MINGW ) && !defined( _MSC_VER )
298  if (Error())
299  {
300  LOG(VB_RECORD, LOG_INFO, QString("ExternIO in bad state: '%1'")
301  .arg(m_error));
302  return;
303  }
304 
305  QString full_command = QString("%1").arg(m_args.join(" "));
306 
307  if (!KillIfRunning(full_command))
308  {
309  // Give it one more chance.
310  std::this_thread::sleep_for(std::chrono::milliseconds(50));
311  if (!KillIfRunning(full_command))
312  {
313  m_error = QString("Unable to kill existing '%1'.")
314  .arg(full_command);
315  LOG(VB_GENERAL, LOG_ERR, m_error);
316  return;
317  }
318  }
319 
320 
321  LOG(VB_RECORD, LOG_INFO, QString("ExternIO::Fork '%1'").arg(full_command));
322 
323  int in[2] = {-1, -1};
324  int out[2] = {-1, -1};
325  int err[2] = {-1, -1};
326 
327  if (pipe(in) < 0)
328  {
329  m_error = "pipe(in) failed: " + ENO;
330  return;
331  }
332  if (pipe(out) < 0)
333  {
334  m_error = "pipe(out) failed: " + ENO;
335  close(in[0]);
336  close(in[1]);
337  return;
338  }
339  if (pipe(err) < 0)
340  {
341  m_error = "pipe(err) failed: " + ENO;
342  close(in[0]);
343  close(in[1]);
344  close(out[0]);
345  close(out[1]);
346  return;
347  }
348 
349  m_pid = fork();
350  if (m_pid < 0)
351  {
352  // Failed
353  m_error = "fork() failed: " + ENO;
354  return;
355  }
356  if (m_pid > 0)
357  {
358  // Parent
359  close(in[0]);
360  close(out[1]);
361  close(err[1]);
362  m_appIn = in[1];
363  m_appOut = out[0];
364  m_appErr = err[0];
365 
366  bool error = false;
367  error = (fcntl(m_appIn, F_SETFL, O_NONBLOCK) == -1);
368  error |= (fcntl(m_appOut, F_SETFL, O_NONBLOCK) == -1);
369  error |= (fcntl(m_appErr, F_SETFL, O_NONBLOCK) == -1);
370 
371  if (error)
372  {
373  LOG(VB_GENERAL, LOG_WARNING,
374  "ExternIO::Fork(): Failed to set O_NONBLOCK for FD: " + ENO);
375  std::this_thread::sleep_for(std::chrono::seconds(2));
377  }
378 
379  LOG(VB_RECORD, LOG_INFO, "Spawned");
380  return;
381  }
382 
383  // Child
384  close(in[1]);
385  close(out[0]);
386  close(err[0]);
387  if (dup2( in[0], 0) < 0)
388  {
389  std::cerr << "dup2(stdin) failed: " << strerror(errno);
391  }
392  else if (dup2(out[1], 1) < 0)
393  {
394  std::cerr << "dup2(stdout) failed: " << strerror(errno);
396  }
397  else if (dup2(err[1], 2) < 0)
398  {
399  std::cerr << "dup2(stderr) failed: " << strerror(errno);
401  }
402 
403  /* Close all open file descriptors except stdin/stdout/stderr */
404  for (int i = sysconf(_SC_OPEN_MAX) - 1; i > 2; --i)
405  close(i);
406 
407  /* Set the process group id to be the same as the pid of this
408  * child process. This ensures that any subprocesses launched by this
409  * process can be killed along with the process itself. */
410  if (setpgid(0,0) < 0)
411  {
412  std::cerr << "ExternIO: "
413  << "setpgid() failed: "
414  << strerror(errno) << endl;
415  }
416 
417  /* run command */
418  char *command = strdup(m_app.canonicalFilePath()
419  .toUtf8().constData());
420  // Copy QStringList to char**
421  char **arguments = new char*[m_args.size() + 1];
422  for (int i = 0; i < m_args.size(); ++i)
423  {
424  int len = m_args[i].size() + 1;
425  arguments[i] = new char[len];
426  memcpy(arguments[i], m_args[i].toStdString().c_str(), len);
427  }
428  arguments[m_args.size()] = nullptr;
429 
430  if (execv(command, arguments) < 0)
431  {
432  // Can't use LOG due to locking fun.
433  std::cerr << "ExternIO: "
434  << "execv() failed: "
435  << strerror(errno) << endl;
436  }
437  else
438  {
439  std::cerr << "ExternIO: "
440  << "execv() should not be here?: "
441  << strerror(errno) << endl;
442  }
443 
444 #endif // !defined( USING_MINGW ) && !defined( _MSC_VER )
445 
446  /* Failed to exec */
447  _exit(GENERIC_EXIT_DAEMONIZING_ERROR); // this exit is ok
448 }
449 
450 
451 QMap<int, ExternalStreamHandler*> ExternalStreamHandler::s_handlers;
454 
456  int inputid, int majorid)
457 {
458  QMutexLocker locker(&s_handlersLock);
459 
460  QMap<int, ExternalStreamHandler*>::iterator it = s_handlers.find(majorid);
461 
462  if (it == s_handlers.end())
463  {
464  auto *newhandler = new ExternalStreamHandler(devname, inputid, majorid);
465  s_handlers[majorid] = newhandler;
466  s_handlersRefCnt[majorid] = 1;
467 
468  LOG(VB_RECORD, LOG_INFO,
469  QString("ExternSH[%1]: Creating new stream handler %2 for %3")
470  .arg(inputid).arg(majorid).arg(devname));
471  }
472  else
473  {
474  s_handlersRefCnt[majorid]++;
475  uint rcount = s_handlersRefCnt[majorid];
476  LOG(VB_RECORD, LOG_INFO,
477  QString("ExternSH[%1]: Using existing stream handler for %2")
478  .arg(inputid).arg(majorid) + QString(" (%1 in use)").arg(rcount));
479  }
480 
481  return s_handlers[majorid];
482 }
483 
485  int inputid)
486 {
487  QMutexLocker locker(&s_handlersLock);
488 
489  int majorid = ref->m_majorId;
490 
491  QMap<int, uint>::iterator rit = s_handlersRefCnt.find(majorid);
492  if (rit == s_handlersRefCnt.end())
493  return;
494 
495  QMap<int, ExternalStreamHandler*>::iterator it =
496  s_handlers.find(majorid);
497 
498  LOG(VB_RECORD, LOG_INFO, QString("ExternSH[%1]: Return %2 in use %3")
499  .arg(inputid).arg(majorid).arg(*rit));
500 
501  if (*rit > 1)
502  {
503  ref = nullptr;
504  --(*rit);
505  return;
506  }
507 
508  if ((it != s_handlers.end()) && (*it == ref))
509  {
510  LOG(VB_RECORD, LOG_INFO, QString("ExternSH[%1]: Closing handler for %2")
511  .arg(inputid).arg(majorid));
512  delete *it;
513  s_handlers.erase(it);
514  }
515  else
516  {
517  LOG(VB_GENERAL, LOG_ERR,
518  QString("ExternSH[%1]: Error: Couldn't find handler for %2")
519  .arg(inputid).arg(majorid));
520  }
521 
522  s_handlersRefCnt.erase(rit);
523  ref = nullptr;
524 }
525 
526 /*
527  ExternalStreamHandler
528  */
529 
531  int inputid,
532  int majorid)
533  : StreamHandler(path, inputid)
534  , m_loc(m_device)
535  , m_majorId(majorid)
536 {
537  setObjectName("ExternSH");
538 
539  m_args = path.split(' ',QString::SkipEmptyParts) +
540  logPropagateArgs.split(' ', QString::SkipEmptyParts);
541  m_app = m_args.first();
542  m_args.removeFirst();
543 
544  // Pass one (and only one) 'quiet'
545  if (!m_args.contains("--quiet") && !m_args.contains("-q"))
546  m_args << "--quiet";
547 
548  m_args << "--inputid" << QString::number(majorid);
549  LOG(VB_RECORD, LOG_INFO, LOC + QString("args \"%1\"")
550  .arg(m_args.join(" ")));
551 
552  if (!OpenApp())
553  {
554  LOG(VB_GENERAL, LOG_ERR, LOC +
555  QString("Failed to start %1").arg(m_device));
556  }
557 }
558 
560 {
561  return m_streamingCnt.loadAcquire();
562 }
563 
565 {
566  QString cmd;
567  QString result;
568  QString ready_cmd;
569  QByteArray buffer;
570  int sz = 0;
571  uint len = 0;
572  uint read_len = 0;
573  uint restart_cnt = 0;
574  MythTimer status_timer;
575  MythTimer nodata_timer;
576 
577  bool good_data = false;
578  uint data_proc_err = 0;
579  uint data_short_err = 0;
580 
581  if (!m_io)
582  {
583  LOG(VB_GENERAL, LOG_ERR, LOC +
584  QString("%1 is not running.").arg(m_device));
585  }
586 
587  status_timer.start();
588 
589  RunProlog();
590 
591  LOG(VB_RECORD, LOG_INFO, LOC + "run(): begin");
592 
593  SetRunning(true, true, false);
594 
595  if (m_pollMode)
596  ready_cmd = "SendBytes";
597  else
598  ready_cmd = "XON";
599 
600  uint remainder = 0;
601  while (m_runningDesired && !m_bError)
602  {
603  if (!IsTSOpen())
604  {
605  LOG(VB_RECORD, LOG_WARNING, LOC + "TS not open yet.");
606  std::this_thread::sleep_for(std::chrono::milliseconds(10));
607  continue;
608  }
609 
610  if (StreamingCount() == 0)
611  {
612  std::this_thread::sleep_for(std::chrono::milliseconds(10));
613  continue;
614  }
615 
617 
618  if (!m_xon || m_pollMode)
619  {
620  if (buffer.size() > TOO_FAST_SIZE)
621  {
622  LOG(VB_RECORD, LOG_WARNING, LOC +
623  "Internal buffer too full to accept more data from "
624  "external application.");
625  }
626  else
627  {
628  if (!ProcessCommand(ready_cmd, result))
629  {
630  if (result.startsWith("ERR"))
631  {
632  LOG(VB_GENERAL, LOG_ERR, LOC +
633  QString("Aborting: %1 -> %2")
634  .arg(ready_cmd).arg(result));
635  m_bError = true;
636  continue;
637  }
638 
639  if (restart_cnt++)
640  std::this_thread::sleep_for(std::chrono::seconds(20));
641  if (!RestartStream())
642  {
643  LOG(VB_RECORD, LOG_ERR, LOC +
644  "Failed to restart stream.");
645  m_bError = true;
646  }
647  continue;
648  }
649  m_xon = true;
650  }
651  }
652 
653  if (m_xon)
654  {
655  if (status_timer.elapsed() >= 2000)
656  {
657  // Since we may never need to send the XOFF
658  // command, occationally check to see if the
659  // External recorder needs to report an issue.
660  if (CheckForError())
661  {
662  if (restart_cnt++)
663  std::this_thread::sleep_for(std::chrono::seconds(20));
664  if (!RestartStream())
665  {
666  LOG(VB_RECORD, LOG_ERR, LOC + "Failed to restart stream.");
667  m_bError = true;
668  }
669  continue;
670  }
671 
672  status_timer.restart();
673  }
674 
675  if (buffer.size() > TOO_FAST_SIZE)
676  {
677  if (!m_pollMode)
678  {
679  // Data is comming a little too fast, so XOFF
680  // to give us time to process it.
681  if (!ProcessCommand(QString("XOFF"), result))
682  {
683  if (result.startsWith("ERR"))
684  {
685  LOG(VB_GENERAL, LOG_ERR, LOC +
686  QString("Aborting: XOFF -> %2")
687  .arg(result));
688  m_bError = true;
689  }
690  }
691  m_xon = false;
692  }
693  }
694 
695  if (m_io && (sz = PACKET_SIZE - remainder) > 0)
696  read_len = m_io->Read(buffer, sz, 100);
697  else
698  read_len = 0;
699  }
700  else
701  read_len = 0;
702 
703  if (read_len == 0)
704  {
705  if (!nodata_timer.isRunning())
706  nodata_timer.start();
707  else
708  {
709  if (nodata_timer.elapsed() >= 50000)
710  {
711  LOG(VB_GENERAL, LOG_WARNING, LOC +
712  "No data for 50 seconds, Restarting stream.");
713  if (!RestartStream())
714  {
715  LOG(VB_RECORD, LOG_ERR, LOC +
716  "Failed to restart stream.");
717  m_bError = true;
718  }
719  nodata_timer.stop();
720  continue;
721  }
722  }
723 
724  std::this_thread::sleep_for(std::chrono::milliseconds(50));
725 
726  // HLS type streams may only produce data every ~10 seconds
727  if (nodata_timer.elapsed() < 12000 && buffer.size() < TS_PACKET_SIZE)
728  continue;
729  }
730  else
731  {
732  nodata_timer.stop();
733  restart_cnt = 0;
734  }
735 
736  if (m_io == nullptr)
737  {
738  LOG(VB_GENERAL, LOG_ERR, LOC + "I/O thread has disappeared!");
739  m_bError = true;
740  break;
741  }
742  if (m_io->Error())
743  {
744  LOG(VB_GENERAL, LOG_ERR, LOC +
745  QString("Fatal Error from External Recorder: %1")
746  .arg(m_io->ErrorString()));
747  CloseApp();
748  m_bError = true;
749  break;
750  }
751 
752  len = remainder = buffer.size();
753 
754  if (len == 0)
755  continue;
756 
757  if (len < TS_PACKET_SIZE)
758  {
759  if (m_xon && data_short_err++ == 0)
760  LOG(VB_RECORD, LOG_INFO, LOC + "Waiting for a full TS packet.");
761  std::this_thread::sleep_for(std::chrono::microseconds(50));
762  continue;
763  }
764  if (data_short_err)
765  {
766  if (data_short_err > 1)
767  {
768  LOG(VB_RECORD, LOG_INFO, LOC +
769  QString("Waited for a full TS packet %1 times.")
770  .arg(data_short_err));
771  }
772  data_short_err = 0;
773  }
774 
775  if (!m_streamLock.tryLock())
776  continue;
777 
778  if (!m_listenerLock.tryLock())
779  continue;
780 
781  for (auto sit = m_streamDataList.cbegin();
782  sit != m_streamDataList.cend(); ++sit)
783  {
784  remainder = sit.key()->ProcessData
785  (reinterpret_cast<const uint8_t *>
786  (buffer.constData()), buffer.size());
787  }
788 
789  m_listenerLock.unlock();
790 
791  if (m_replay)
792  {
793  m_replayBuffer += buffer.left(len - remainder);
794  if (m_replayBuffer.size() > (50 * PACKET_SIZE))
795  {
796  m_replayBuffer.remove(0, len - remainder);
797  LOG(VB_RECORD, LOG_WARNING, LOC +
798  QString("Replay size truncated to %1 bytes")
799  .arg(m_replayBuffer.size()));
800  }
801  }
802 
803  m_streamLock.unlock();
804 
805  if (remainder == 0)
806  {
807  buffer.clear();
808  good_data = len;
809  }
810  else if (len > remainder) // leftover bytes
811  {
812  buffer.remove(0, len - remainder);
813  good_data = len;
814  }
815  else if (len == remainder)
816  good_data = false;
817 
818  if (good_data)
819  {
820  if (data_proc_err)
821  {
822  if (data_proc_err > 1)
823  {
824  LOG(VB_RECORD, LOG_WARNING, LOC +
825  QString("Failed to process the data received %1 times.")
826  .arg(data_proc_err));
827  }
828  data_proc_err = 0;
829  }
830  }
831  else
832  {
833  if (data_proc_err++ == 0)
834  {
835  LOG(VB_RECORD, LOG_WARNING, LOC +
836  "Failed to process the data received");
837  }
838  }
839  }
840 
841  LOG(VB_RECORD, LOG_INFO, LOC + "run(): " +
842  QString("%1 shutdown").arg(m_bError ? "Error" : "Normal"));
843 
845  SetRunning(false, true, false);
846 
847  LOG(VB_RECORD, LOG_INFO, LOC + "run(): " + "end");
848 
849  RunEpilog();
850 }
851 
853 {
854  QString result;
855 
856  if (ProcessCommand("APIVersion?", result, 10000))
857  {
858  QStringList tokens = result.split(':', QString::SkipEmptyParts);
859 
860  if (tokens.size() > 1)
861  m_apiVersion = tokens[1].toUInt();
862  m_apiVersion = min(m_apiVersion, static_cast<int>(MAX_API_VERSION));
863  if (m_apiVersion < 1)
864  {
865  LOG(VB_RECORD, LOG_ERR, LOC +
866  QString("Bad response to 'APIVersion?' - '%1'. "
867  "Expecting 1 or 2").arg(result));
868  m_apiVersion = 1;
869  }
870 
871  ProcessCommand(QString("APIVersion:%1").arg(m_apiVersion), result);
872  return true;
873  }
874 
875  return false;
876 }
877 
879 {
880  if (m_apiVersion > 1)
881  {
882  QString result;
883 
884  if (ProcessCommand("Description?", result))
885  m_loc = result.mid(3);
886  else
887  m_loc = m_device;
888  }
889 
890  return m_loc;
891 }
892 
894 {
895  {
896  QMutexLocker locker(&m_ioLock);
897 
898  if (m_io)
899  {
900  LOG(VB_RECORD, LOG_WARNING, LOC + "OpenApp: already open!");
901  return true;
902  }
903 
904  m_io = new ExternIO(m_app, m_args);
905 
906  if (m_io == nullptr)
907  {
908  LOG(VB_GENERAL, LOG_ERR, LOC + "ExternIO failed: " + ENO);
909  m_bError = true;
910  }
911  else
912  {
913  LOG(VB_RECORD, LOG_INFO, LOC + QString("Spawn '%1'").arg(m_device));
914  m_io->Run();
915  if (m_io->Error())
916  {
917  LOG(VB_GENERAL, LOG_ERR,
918  "Failed to start External Recorder: " + m_io->ErrorString());
919  delete m_io;
920  m_io = nullptr;
921  m_bError = true;
922  return false;
923  }
924  }
925  }
926 
927  QString result;
928 
929  if (!SetAPIVersion())
930  {
931  // Try again using API version 2
932  m_apiVersion = 2;
933  if (!SetAPIVersion())
934  m_apiVersion = 1;
935  }
936 
937  if (!IsAppOpen())
938  {
939  LOG(VB_RECORD, LOG_ERR, LOC + "Application is not responding.");
940  m_bError = true;
941  return false;
942  }
943 
945 
946  // Gather capabilities
947  if (!ProcessCommand("HasTuner?", result))
948  {
949  LOG(VB_RECORD, LOG_ERR, LOC +
950  QString("Bad response to 'HasTuner?' - '%1'").arg(result));
951  m_bError = true;
952  return false;
953  }
954  m_hasTuner = result.startsWith("OK:Yes");
955 
956  if (!ProcessCommand("HasPictureAttributes?", result))
957  {
958  LOG(VB_RECORD, LOG_ERR, LOC +
959  QString("Bad response to 'HasPictureAttributes?' - '%1'")
960  .arg(result));
961  m_bError = true;
962  return false;
963  }
964  m_hasPictureAttributes = result.startsWith("OK:Yes");
965 
966  /* Operate in "poll" or "xon/xoff" mode */
967  m_pollMode = ProcessCommand("FlowControl?", result) &&
968  result.startsWith("OK:Poll");
969 
970  LOG(VB_RECORD, LOG_INFO, LOC + "App opened successfully");
971  LOG(VB_RECORD, LOG_INFO, LOC +
972  QString("Capabilities: tuner(%1) "
973  "Picture attributes(%2) "
974  "Flow control(%3)")
975  .arg(m_hasTuner ? "yes" : "no")
976  .arg(m_hasPictureAttributes ? "yes" : "no")
977  .arg(m_pollMode ? "Polling" : "XON/XOFF")
978  );
979 
980  /* Let the external app know how many bytes will read without blocking */
981  ProcessCommand(QString("BlockSize:%1").arg(PACKET_SIZE), result);
982 
983  return true;
984 }
985 
987 {
988  if (m_io == nullptr)
989  {
990  LOG(VB_RECORD, LOG_WARNING, LOC +
991  "WARNING: Unable to communicate with external app.");
992  return false;
993  }
994 
995  QString result;
996  return ProcessCommand("Version?", result, 10000);
997 }
998 
1000 {
1001  if (m_tsOpen)
1002  return true;
1003 
1004  QString result;
1005 
1006  if (!ProcessCommand("IsOpen?", result))
1007  return false;
1008 
1009  m_tsOpen = true;
1010  return m_tsOpen;
1011 }
1012 
1014 {
1015  m_ioLock.lock();
1016  if (m_io)
1017  {
1018  QString result;
1019 
1020  LOG(VB_RECORD, LOG_INFO, LOC + "CloseRecorder");
1021  m_ioLock.unlock();
1022  ProcessCommand("CloseRecorder", result, 10000);
1023  m_ioLock.lock();
1024 
1025  if (!result.startsWith("OK"))
1026  {
1027  LOG(VB_RECORD, LOG_INFO, LOC +
1028  "CloseRecorder failed, sending kill.");
1029 
1030  QString full_command = QString("%1").arg(m_args.join(" "));
1031 
1032  if (!m_io->KillIfRunning(full_command))
1033  {
1034  // Give it one more chance.
1035  std::this_thread::sleep_for(std::chrono::milliseconds(50));
1036  if (!m_io->KillIfRunning(full_command))
1037  {
1038  LOG(VB_GENERAL, LOG_ERR,
1039  QString("Unable to kill existing '%1'.")
1040  .arg(full_command));
1041  return;
1042  }
1043  }
1044  }
1045  delete m_io;
1046  m_io = nullptr;
1047  }
1048  m_ioLock.unlock();
1049 }
1050 
1052 {
1053  bool streaming = (StreamingCount() > 0);
1054 
1055  LOG(VB_RECORD, LOG_INFO, LOC + "Restarting stream.");
1056 
1057  if (streaming)
1058  StopStreaming();
1059 
1060  std::this_thread::sleep_for(std::chrono::seconds(1));
1061 
1062  if (streaming)
1063  return StartStreaming();
1064 
1065  return true;
1066 }
1067 
1069 {
1070  if (m_replay)
1071  {
1072  QString result;
1073 
1074  // Let the external app know that we could be busy for a little while
1075  if (!m_pollMode)
1076  {
1077  ProcessCommand(QString("XOFF"), result);
1078  m_xon = false;
1079  }
1080 
1081  /* If the input is not a 'broadcast' it may only have one
1082  * copy of the SPS right at the beginning of the stream,
1083  * so make sure we don't miss it!
1084  */
1085  QMutexLocker listen_lock(&m_listenerLock);
1086 
1087  if (!m_streamDataList.empty())
1088  {
1089  for (auto sit = m_streamDataList.cbegin();
1090  sit != m_streamDataList.cend(); ++sit)
1091  {
1092  sit.key()->ProcessData(reinterpret_cast<const uint8_t *>
1093  (m_replayBuffer.constData()),
1094  m_replayBuffer.size());
1095  }
1096  }
1097  LOG(VB_RECORD, LOG_INFO, LOC + QString("Replayed %1 bytes")
1098  .arg(m_replayBuffer.size()));
1099  m_replayBuffer.clear();
1100  m_replay = false;
1101 
1102  // Let the external app know that we are ready
1103  if (!m_pollMode)
1104  {
1105  if (ProcessCommand(QString("XON"), result))
1106  m_xon = true;
1107  }
1108  }
1109 }
1110 
1112 {
1113  QString result;
1114 
1115  QMutexLocker locker(&m_streamLock);
1116 
1118 
1119  LOG(VB_RECORD, LOG_INFO, LOC +
1120  QString("StartStreaming with %1 current listeners")
1121  .arg(StreamingCount()));
1122 
1123  if (!IsAppOpen())
1124  {
1125  LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder not started.");
1126  return false;
1127  }
1128 
1129  if (StreamingCount() == 0)
1130  {
1131  if (!ProcessCommand("StartStreaming", result, 15000))
1132  {
1133  LogLevel_t level = LOG_ERR;
1134  if (result.toLower().startsWith("warn"))
1135  level = LOG_WARNING;
1136  else
1137  m_bError = true;
1138 
1139  LOG(VB_GENERAL, level, LOC + QString("StartStreaming failed: '%1'")
1140  .arg(result));
1141 
1142  return false;
1143  }
1144 
1145  LOG(VB_RECORD, LOG_INFO, LOC + "Streaming started");
1146  }
1147  else
1148  LOG(VB_RECORD, LOG_INFO, LOC + "Already streaming");
1149 
1150  m_streamingCnt.ref();
1151 
1152  LOG(VB_RECORD, LOG_INFO, LOC +
1153  QString("StartStreaming %1 listeners")
1154  .arg(StreamingCount()));
1155 
1156  return true;
1157 }
1158 
1160 {
1161  QMutexLocker locker(&m_streamLock);
1162 
1163  LOG(VB_RECORD, LOG_INFO, LOC +
1164  QString("StopStreaming %1 listeners")
1165  .arg(StreamingCount()));
1166 
1167  if (StreamingCount() == 0)
1168  {
1169  LOG(VB_RECORD, LOG_INFO, LOC +
1170  "StopStreaming requested, but we are not streaming!");
1171  return true;
1172  }
1173 
1174  if (m_streamingCnt.deref())
1175  {
1176  LOG(VB_RECORD, LOG_INFO, LOC +
1177  QString("StopStreaming delayed, still have %1 listeners")
1178  .arg(StreamingCount()));
1179  return true;
1180  }
1181 
1182  LOG(VB_RECORD, LOG_INFO, LOC + "StopStreaming");
1183 
1184  if (!m_pollMode && m_xon)
1185  {
1186  QString result;
1187  ProcessCommand(QString("XOFF"), result);
1188  m_xon = false;
1189  }
1190 
1191  if (!IsAppOpen())
1192  {
1193  LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder not started.");
1194  return false;
1195  }
1196 
1197  QString result;
1198  if (!ProcessCommand("StopStreaming", result, 10000))
1199  {
1200  LogLevel_t level = LOG_ERR;
1201  if (result.toLower().startsWith("warn"))
1202  level = LOG_WARNING;
1203  else
1204  m_bError = true;
1205 
1206  LOG(VB_GENERAL, level, LOC + QString("StopStreaming: '%1'")
1207  .arg(result));
1208 
1209  return false;
1210  }
1211 
1212  PurgeBuffer();
1213  LOG(VB_RECORD, LOG_INFO, LOC + "Streaming stopped");
1214 
1215  return true;
1216 }
1217 
1218 bool ExternalStreamHandler::ProcessCommand(const QString & cmd,
1219  QString & result, int timeout,
1220  uint retry_cnt)
1221 {
1222  QMutexLocker locker(&m_processLock);
1223 
1224  if (m_apiVersion == 2)
1225  return ProcessVer2(cmd, result, timeout, retry_cnt);
1226  if (m_apiVersion == 1)
1227  return ProcessVer1(cmd, result, timeout, retry_cnt);
1228 
1229  LOG(VB_RECORD, LOG_ERR, LOC +
1230  QString("Invalid API version %1. Expected 1 or 2").arg(m_apiVersion));
1231  return false;
1232 }
1233 
1234 bool ExternalStreamHandler::ProcessVer1(const QString & cmd,
1235  QString & result, int timeout,
1236  uint retry_cnt)
1237 {
1238  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("ProcessVer1('%1')")
1239  .arg(cmd));
1240 
1241  for (uint cnt = 0; cnt < retry_cnt; ++cnt)
1242  {
1243  QMutexLocker locker(&m_ioLock);
1244 
1245  if (!m_io)
1246  {
1247  LOG(VB_RECORD, LOG_ERR, LOC + "External I/O not ready!");
1248  return false;
1249  }
1250 
1251  QByteArray buf(cmd.toUtf8(), cmd.size());
1252  buf += '\n';
1253 
1254  if (m_io->Error())
1255  {
1256  LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder in bad state: " +
1257  m_io->ErrorString());
1258  return false;
1259  }
1260 
1261  /* Try to keep in sync, if External app was too slow in responding
1262  * to previous query, consume the response before sending new query */
1263  m_io->GetStatus(0);
1264 
1265  /* Send new query */
1266  m_io->Write(buf);
1267 
1269  while (timer.elapsed() < timeout)
1270  {
1271  result = m_io->GetStatus(timeout);
1272  if (m_io->Error())
1273  {
1274  LOG(VB_GENERAL, LOG_ERR, LOC +
1275  "Failed to read from External Recorder: " +
1276  m_io->ErrorString());
1277  m_bError = true;
1278  return false;
1279  }
1280 
1281  // Out-of-band error message
1282  if (result.startsWith("STATUS:ERR") ||
1283  result.startsWith("0:STATUS:ERR"))
1284  {
1285  LOG(VB_RECORD, LOG_ERR, LOC + result);
1286  result.remove(0, result.indexOf(":ERR") + 1);
1287  return false;
1288  }
1289  // STATUS message are "out of band".
1290  // Ignore them while waiting for a responds to a command
1291  if (!result.startsWith("STATUS") && !result.startsWith("0:STATUS"))
1292  break;
1293  LOG(VB_RECORD, LOG_INFO, LOC +
1294  QString("Ignoring response '%1'").arg(result));
1295  }
1296 
1297  if (result.size() < 1)
1298  {
1299  LOG(VB_GENERAL, LOG_WARNING, LOC +
1300  QString("External Recorder did not respond to '%1'").arg(cmd));
1301  }
1302  else
1303  {
1304  bool okay = result.startsWith("OK");
1305  if (okay || result.startsWith("WARN") || result.startsWith("ERR"))
1306  {
1307  LogLevel_t level = LOG_INFO;
1308 
1309  m_ioErrCnt = 0;
1310  if (!okay)
1311  level = LOG_WARNING;
1312  else if (cmd.startsWith("SendBytes"))
1313  level = LOG_DEBUG;
1314 
1315  LOG(VB_RECORD, level,
1316  LOC + QString("ProcessCommand('%1') = '%2' took %3ms %4")
1317  .arg(cmd).arg(result)
1318  .arg(timer.elapsed())
1319  .arg(okay ? "" : "<-- NOTE"));
1320 
1321  return okay;
1322  }
1323  LOG(VB_GENERAL, LOG_WARNING, LOC +
1324  QString("External Recorder invalid response to '%1': '%2'")
1325  .arg(cmd).arg(result));
1326  }
1327 
1328  if (++m_ioErrCnt > 10)
1329  {
1330  LOG(VB_GENERAL, LOG_ERR, LOC + "Too many I/O errors.");
1331  m_bError = true;
1332  break;
1333  }
1334  }
1335 
1336  return false;
1337 }
1338 
1339 bool ExternalStreamHandler::ProcessVer2(const QString & command,
1340  QString & result, int timeout,
1341  uint retry_cnt)
1342 {
1343  QString status;
1344  QString raw;
1345 
1346  for (uint cnt = 0; cnt < retry_cnt; ++cnt)
1347  {
1348  QString cmd = QString("%1:%2").arg(++m_serialNo).arg(command);
1349 
1350  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("ProcessVer2('%1') serial(%2)")
1351  .arg(cmd).arg(m_serialNo));
1352 
1353  QMutexLocker locker(&m_ioLock);
1354 
1355  if (!m_io)
1356  {
1357  LOG(VB_RECORD, LOG_ERR, LOC + "External I/O not ready!");
1358  return false;
1359  }
1360 
1361  QByteArray buf(cmd.toUtf8(), cmd.size());
1362  buf += '\n';
1363 
1364  if (m_io->Error())
1365  {
1366  LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder in bad state: " +
1367  m_io->ErrorString());
1368  return false;
1369  }
1370 
1371  /* Send query */
1372  m_io->Write(buf);
1373 
1374  QStringList tokens;
1375 
1377  while (timer.elapsed() < timeout)
1378  {
1379  result = m_io->GetStatus(timeout);
1380  if (m_io->Error())
1381  {
1382  LOG(VB_GENERAL, LOG_ERR, LOC +
1383  "Failed to read from External Recorder: " +
1384  m_io->ErrorString());
1385  m_bError = true;
1386  return false;
1387  }
1388 
1389  if (!result.isEmpty())
1390  {
1391  raw = result;
1392  tokens = result.split(':', QString::SkipEmptyParts);
1393 
1394  // Look for result with the serial number of this query
1395  if (tokens.size() > 1 && tokens[0].toUInt() >= m_serialNo)
1396  break;
1397 
1398  /* Other messages are "out of band" */
1399 
1400  // Check for error message missing serial#
1401  if (tokens[0].startsWith("ERR"))
1402  break;
1403 
1404  // Remove serial#
1405  tokens.removeFirst();
1406  result = tokens.join(':');
1407  bool err = (tokens.size() > 1 && tokens[1].startsWith("ERR"));
1408  LOG(VB_RECORD, (err ? LOG_WARNING : LOG_INFO), LOC + raw);
1409  if (err)
1410  {
1411  // Remove "STATUS"
1412  tokens.removeFirst();
1413  result = tokens.join(':');
1414  return false;
1415  }
1416  }
1417  }
1418 
1419  if (timer.elapsed() >= timeout)
1420  {
1421  LOG(VB_RECORD, LOG_ERR, LOC +
1422  QString("ProcessVer2: Giving up waiting for response for "
1423  "command '%2'").arg(cmd));
1424  }
1425  else if (tokens.size() < 2)
1426  {
1427  LOG(VB_RECORD, LOG_ERR, LOC +
1428  QString("Did not receive a valid response "
1429  "for command '%1', received '%2'").arg(cmd).arg(result));
1430  }
1431  else if (tokens[0].toUInt() > m_serialNo)
1432  {
1433  LOG(VB_RECORD, LOG_ERR, LOC +
1434  QString("ProcessVer2: Looking for serial no %1, "
1435  "but received %2 for command '%2'")
1436  .arg(m_serialNo).arg(tokens[0]).arg(cmd));
1437  }
1438  else
1439  {
1440  tokens.removeFirst();
1441  status = tokens[0].trimmed();
1442  result = tokens.join(':');
1443 
1444  bool okay = (status == "OK");
1445  if (okay || status.startsWith("WARN") || status.startsWith("ERR"))
1446  {
1447  LogLevel_t level = LOG_INFO;
1448 
1449  m_ioErrCnt = 0;
1450  if (!okay)
1451  level = LOG_WARNING;
1452  else if (command.startsWith("SendBytes"))
1453  level = LOG_DEBUG;
1454 
1455  LOG(VB_RECORD, level,
1456  LOC + QString("ProcessV2('%1') = '%2' took %3ms %4")
1457  .arg(cmd).arg(result).arg(timer.elapsed())
1458  .arg(okay ? "" : "<-- NOTE"));
1459 
1460  return okay;
1461  }
1462  LOG(VB_GENERAL, LOG_WARNING, LOC +
1463  QString("External Recorder invalid response to '%1': '%2'")
1464  .arg(cmd).arg(result));
1465  }
1466 
1467  if (++m_ioErrCnt > 10)
1468  {
1469  LOG(VB_GENERAL, LOG_ERR, LOC + "Too many I/O errors.");
1470  m_bError = true;
1471  break;
1472  }
1473  }
1474 
1475  return false;
1476 }
1477 
1479 {
1480  QString result;
1481  bool err = false;
1482 
1483  QMutexLocker locker(&m_ioLock);
1484 
1485  if (!m_io)
1486  {
1487  LOG(VB_RECORD, LOG_ERR, LOC + "External I/O not ready!");
1488  return true;
1489  }
1490 
1491  if (m_io->Error())
1492  {
1493  LOG(VB_GENERAL, LOG_ERR, "External Recorder in bad state: " +
1494  m_io->ErrorString());
1495  return true;
1496  }
1497 
1498  do
1499  {
1500  result = m_io->GetStatus(0);
1501  if (!result.isEmpty())
1502  {
1503  if (m_apiVersion > 1)
1504  {
1505  QStringList tokens = result.split(':', QString::SkipEmptyParts);
1506 
1507  tokens.removeFirst();
1508  result = tokens.join(':');
1509  for (int idx = 1; idx < tokens.size(); ++idx)
1510  err |= tokens[idx].startsWith("ERR");
1511  }
1512  else
1513  err |= result.startsWith("STATUS:ERR");
1514 
1515  LOG(VB_RECORD, (err ? LOG_WARNING : LOG_INFO), LOC + result);
1516  }
1517  }
1518  while (!result.isEmpty());
1519 
1520  return err;
1521 }
1522 
1524 {
1525  if (m_io)
1526  {
1527  QByteArray buffer;
1528  m_io->Read(buffer, PACKET_SIZE, 1);
1529  m_io->GetStatus(1);
1530  }
1531 }
1532 
1534 {
1535  // TODO report on buffer overruns, etc.
1536 }
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
Definition: mthread.cpp:215
static QMap< int, ExternalStreamHandler * > s_handlers
def write(text, progress=True)
Definition: mythburn.py:279
int restart(void)
Returns milliseconds elapsed since last start() or restart() and resets the count.
Definition: mythtimer.cpp:62
QString GetStatus(int timeout=2500)
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:13
#define O_NONBLOCK
Definition: mythmedia.cpp:25
#define GENERIC_EXIT_DAEMONIZING_ERROR
Error daemonizing or execl.
Definition: exitcodes.h:28
void PriorityEvent(int fd) override
static QMap< int, uint > s_handlersRefCnt
static void error(const char *str,...)
Definition: vbi.c:42
bool ProcessVer2(const QString &command, QString &result, int timeout, uint retry_cnt)
bool UpdateFiltersFromStreamData(void)
#define LOC
QString ErrorString(void) const
QString m_device
QString logPropagateArgs
Definition: logging.cpp:89
bool ProcessVer1(const QString &cmd, QString &result, int timeout, uint retry_cnt)
int Write(const QByteArray &buffer)
void setObjectName(const QString &name)
Definition: mthread.cpp:249
bool isRunning(void) const
Returns true if start() or restart() has been called at least once since construction and since any c...
Definition: mythtimer.cpp:134
def read(device=None, features=[])
Definition: disc.py:35
QStringList m_args
void run(void) override
Runs the Qt event loop unless we have a QRunnable, in which case we run the runnable run instead.
void SetRunning(bool running, bool using_buffering, bool using_section_reader)
#define GENERIC_EXIT_PIPE_FAILURE
Error creating I/O pipes.
Definition: exitcodes.h:26
#define close
Definition: compat.h:16
bool Ready(int fd, int timeout, const QString &what)
QMutex m_listenerLock
unsigned int uint
Definition: compat.h:140
bool Error(void) const
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:99
volatile bool m_runningDesired
#define WEXITSTATUS(w)
Definition: compat.h:328
static ExternalStreamHandler * Get(const QString &devname, int inputid, int majorid)
static bool KillIfRunning(const QString &cmd)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
bool ProcessCommand(const QString &cmd, QString &result, int timeout=4000, uint retry_cnt=3)
ExternIO(const QString &app, const QStringList &args)
int elapsed(void)
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:90
void RunProlog(void)
Sets up a thread, call this if you reimplement run().
Definition: mthread.cpp:202
volatile bool m_bError
void stop(void)
Stops timer, next call to isRunning() will return false and any calls to elapsed() or restart() will ...
Definition: mythtimer.cpp:77
StreamDataList m_streamDataList
int Read(QByteArray &buffer, int maxlen, int timeout=2500)
void start(void)
starts measuring elapsed time.
Definition: mythtimer.cpp:47
ExternalStreamHandler(const QString &path, int inputid, int majorid)
QTextStream m_status
static void Return(ExternalStreamHandler *&ref, int inputid)
bool RemoveAllPIDFilters(void)