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_status_buf, 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_status_buf);
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  int res_grp, res_kil;
256 
257  res_grp = system(grp.toUtf8().constData());
258  if (WEXITSTATUS(res_grp) == 1)
259  {
260  LOG(VB_RECORD, LOG_DEBUG, QString("'%1' not running.").arg(cmd));
261  return true;
262  }
263 
264  LOG(VB_RECORD, LOG_WARNING, QString("'%1' already running, killing...")
265  .arg(cmd));
266  res_kil = system(kil.toUtf8().constData());
267  if (WEXITSTATUS(res_kil) == 1)
268  LOG(VB_GENERAL, LOG_WARNING, QString("'%1' failed: %2")
269  .arg(kil).arg(ENO));
270 
271  res_grp = system(grp.toUtf8().constData());
272  if (WEXITSTATUS(res_grp) == 1)
273  {
274  LOG(WEXITSTATUS(res_kil) == 0 ? VB_RECORD : VB_GENERAL, LOG_WARNING,
275  QString("'%1' terminated.").arg(cmd));
276  return true;
277  }
278 
279  std::this_thread::sleep_for(std::chrono::milliseconds(50));
280 
281  kil = QString("pkill --signal 9 -x -f \"%1\" 2>&1 > /dev/null").arg(cmd);
282  res_kil = system(kil.toUtf8().constData());
283  if (WEXITSTATUS(res_kil) > 0)
284  LOG(VB_GENERAL, LOG_WARNING, QString("'%1' failed: %2")
285  .arg(kil).arg(ENO));
286 
287  res_grp = system(grp.toUtf8().constData());
288  LOG(WEXITSTATUS(res_kil) == 0 ? VB_RECORD : VB_GENERAL, LOG_WARNING,
289  QString("'%1' %2.").arg(cmd)
290  .arg(WEXITSTATUS(res_grp) == 0 ? "sill running" : "terminated"));
291 
292  return (WEXITSTATUS(res_grp) != 0);
293 #endif
294 }
295 
296 void ExternIO::Fork(void)
297 {
298 #if !defined( USING_MINGW ) && !defined( _MSC_VER )
299  if (Error())
300  {
301  LOG(VB_RECORD, LOG_INFO, QString("ExternIO in bad state: '%1'")
302  .arg(m_error));
303  return;
304  }
305 
306  QString full_command = QString("%1").arg(m_args.join(" "));
307 
308  if (!KillIfRunning(full_command))
309  {
310  // Give it one more chance.
311  std::this_thread::sleep_for(std::chrono::milliseconds(50));
312  if (!KillIfRunning(full_command))
313  {
314  m_error = QString("Unable to kill existing '%1'.")
315  .arg(full_command);
316  LOG(VB_GENERAL, LOG_ERR, m_error);
317  return;
318  }
319  }
320 
321 
322  LOG(VB_RECORD, LOG_INFO, QString("ExternIO::Fork '%1'").arg(full_command));
323 
324  int in[2] = {-1, -1};
325  int out[2] = {-1, -1};
326  int err[2] = {-1, -1};
327 
328  if (pipe(in) < 0)
329  {
330  m_error = "pipe(in) failed: " + ENO;
331  return;
332  }
333  if (pipe(out) < 0)
334  {
335  m_error = "pipe(out) failed: " + ENO;
336  close(in[0]);
337  close(in[1]);
338  return;
339  }
340  if (pipe(err) < 0)
341  {
342  m_error = "pipe(err) failed: " + ENO;
343  close(in[0]);
344  close(in[1]);
345  close(out[0]);
346  close(out[1]);
347  return;
348  }
349 
350  m_pid = fork();
351  if (m_pid < 0)
352  {
353  // Failed
354  m_error = "fork() failed: " + ENO;
355  return;
356  }
357  if (m_pid > 0)
358  {
359  // Parent
360  close(in[0]);
361  close(out[1]);
362  close(err[1]);
363  m_appin = in[1];
364  m_appout = out[0];
365  m_apperr = err[0];
366 
367  bool error = false;
368  error = (fcntl(m_appin, F_SETFL, O_NONBLOCK) == -1);
369  error |= (fcntl(m_appout, F_SETFL, O_NONBLOCK) == -1);
370  error |= (fcntl(m_apperr, F_SETFL, O_NONBLOCK) == -1);
371 
372  if (error)
373  {
374  LOG(VB_GENERAL, LOG_WARNING,
375  "ExternIO::Fork(): Failed to set O_NONBLOCK for FD: " + ENO);
376  std::this_thread::sleep_for(std::chrono::seconds(2));
378  }
379 
380  LOG(VB_RECORD, LOG_INFO, "Spawned");
381  return;
382  }
383 
384  // Child
385  close(in[1]);
386  close(out[0]);
387  close(err[0]);
388  if (dup2( in[0], 0) < 0)
389  {
390  std::cerr << "dup2(stdin) failed: " << strerror(errno);
392  }
393  else if (dup2(out[1], 1) < 0)
394  {
395  std::cerr << "dup2(stdout) failed: " << strerror(errno);
397  }
398  else if (dup2(err[1], 2) < 0)
399  {
400  std::cerr << "dup2(stderr) failed: " << strerror(errno);
402  }
403 
404  /* Close all open file descriptors except stdin/stdout/stderr */
405  for (int i = sysconf(_SC_OPEN_MAX) - 1; i > 2; --i)
406  close(i);
407 
408  /* Set the process group id to be the same as the pid of this
409  * child process. This ensures that any subprocesses launched by this
410  * process can be killed along with the process itself. */
411  if (setpgid(0,0) < 0)
412  {
413  std::cerr << "ExternIO: "
414  << "setpgid() failed: "
415  << strerror(errno) << endl;
416  }
417 
418  /* run command */
419  char *command = strdup(m_app.canonicalFilePath()
420  .toUtf8().constData());
421  char **arguments;
422  int len;
423 
424  // Copy QStringList to char**
425  arguments = new char*[m_args.size() + 1];
426  for (int i = 0; i < m_args.size(); ++i)
427  {
428  len = m_args[i].size() + 1;
429  arguments[i] = new char[len];
430  memcpy(arguments[i], m_args[i].toStdString().c_str(), len);
431  }
432  arguments[m_args.size()] = nullptr;
433 
434  if (execv(command, arguments) < 0)
435  {
436  // Can't use LOG due to locking fun.
437  std::cerr << "ExternIO: "
438  << "execv() failed: "
439  << strerror(errno) << endl;
440  }
441  else
442  {
443  std::cerr << "ExternIO: "
444  << "execv() should not be here?: "
445  << strerror(errno) << endl;
446  }
447 
448 #endif // !defined( USING_MINGW ) && !defined( _MSC_VER )
449 
450  /* Failed to exec */
451  _exit(GENERIC_EXIT_DAEMONIZING_ERROR); // this exit is ok
452 }
453 
454 
455 QMap<int, ExternalStreamHandler*> ExternalStreamHandler::s_handlers;
458 
460  int inputid, int majorid)
461 {
462  QMutexLocker locker(&s_handlers_lock);
463 
464  QMap<int, ExternalStreamHandler*>::iterator it = s_handlers.find(majorid);
465 
466  if (it == s_handlers.end())
467  {
468  ExternalStreamHandler *newhandler =
469  new ExternalStreamHandler(devname, inputid, majorid);
470  s_handlers[majorid] = newhandler;
471  s_handlers_refcnt[majorid] = 1;
472 
473  LOG(VB_RECORD, LOG_INFO,
474  QString("ExternSH[%1]: Creating new stream handler %2 for %3")
475  .arg(inputid).arg(majorid).arg(devname));
476  }
477  else
478  {
479  s_handlers_refcnt[majorid]++;
480  uint rcount = s_handlers_refcnt[majorid];
481  LOG(VB_RECORD, LOG_INFO,
482  QString("ExternSH[%1]: Using existing stream handler for %2")
483  .arg(inputid).arg(majorid) + QString(" (%1 in use)").arg(rcount));
484  }
485 
486  return s_handlers[majorid];
487 }
488 
490  int inputid)
491 {
492  QMutexLocker locker(&s_handlers_lock);
493 
494  int majorid = ref->m_majorid;
495 
496  QMap<int, uint>::iterator rit = s_handlers_refcnt.find(majorid);
497  if (rit == s_handlers_refcnt.end())
498  return;
499 
500  QMap<int, ExternalStreamHandler*>::iterator it =
501  s_handlers.find(majorid);
502 
503  LOG(VB_RECORD, LOG_INFO, QString("ExternSH[%1]: Return %2 in use %3")
504  .arg(inputid).arg(majorid).arg(*rit));
505 
506  if (*rit > 1)
507  {
508  ref = nullptr;
509  --(*rit);
510  return;
511  }
512 
513  if ((it != s_handlers.end()) && (*it == ref))
514  {
515  LOG(VB_RECORD, LOG_INFO, QString("ExternSH[%1]: Closing handler for %2")
516  .arg(inputid).arg(majorid));
517  delete *it;
518  s_handlers.erase(it);
519  }
520  else
521  {
522  LOG(VB_GENERAL, LOG_ERR,
523  QString("ExternSH[%1]: Error: Couldn't find handler for %2")
524  .arg(inputid).arg(majorid));
525  }
526 
527  s_handlers_refcnt.erase(rit);
528  ref = nullptr;
529 }
530 
531 /*
532  ExternalStreamHandler
533  */
534 
536  int inputid,
537  int majorid)
538  : StreamHandler(path, inputid)
539  , m_loc(m_device)
540  , m_majorid(majorid)
541 {
542  setObjectName("ExternSH");
543 
544  m_args = path.split(' ',QString::SkipEmptyParts) +
545  logPropagateArgs.split(' ', QString::SkipEmptyParts);
546  m_app = m_args.first();
547  m_args.removeFirst();
548 
549  // Pass one (and only one) 'quiet'
550  if (!m_args.contains("--quiet") && !m_args.contains("-q"))
551  m_args << "--quiet";
552 
553  m_args << "--inputid" << QString::number(majorid);
554  LOG(VB_RECORD, LOG_INFO, LOC + QString("args \"%1\"")
555  .arg(m_args.join(" ")));
556 
557  if (!OpenApp())
558  {
559  LOG(VB_GENERAL, LOG_ERR, LOC +
560  QString("Failed to start %1").arg(m_device));
561  }
562 }
563 
565 {
566  return m_streaming_cnt.loadAcquire();
567 }
568 
570 {
571  QString cmd;
572  QString result;
573  QString ready_cmd;
574  QByteArray buffer;
575  int sz;
576  uint len, read_len;
577  uint restart_cnt = 0;
578  MythTimer status_timer;
579  MythTimer nodata_timer;
580 
581  bool good_data = false;
582  uint data_proc_err = 0;
583  uint data_short_err = 0;
584 
585  if (!m_io)
586  {
587  LOG(VB_GENERAL, LOG_ERR, LOC +
588  QString("%1 is not running.").arg(m_device));
589  }
590 
591  status_timer.start();
592 
593  RunProlog();
594 
595  LOG(VB_RECORD, LOG_INFO, LOC + "run(): begin");
596 
597  SetRunning(true, true, false);
598 
599  if (m_poll_mode)
600  ready_cmd = "SendBytes";
601  else
602  ready_cmd = "XON";
603 
604  uint remainder = 0;
605  while (m_running_desired && !m_bError)
606  {
607  if (!IsTSOpen())
608  {
609  LOG(VB_RECORD, LOG_WARNING, LOC + "TS not open yet.");
610  std::this_thread::sleep_for(std::chrono::milliseconds(10));
611  continue;
612  }
613 
614  if (StreamingCount() == 0)
615  {
616  std::this_thread::sleep_for(std::chrono::milliseconds(10));
617  continue;
618  }
619 
621 
622  if (!m_xon || m_poll_mode)
623  {
624  if (buffer.size() > TOO_FAST_SIZE)
625  {
626  LOG(VB_RECORD, LOG_WARNING, LOC +
627  "Internal buffer too full to accept more data from "
628  "external application.");
629  }
630  else
631  {
632  if (!ProcessCommand(ready_cmd, result))
633  {
634  if (result.startsWith("ERR"))
635  {
636  LOG(VB_GENERAL, LOG_ERR, LOC +
637  QString("Aborting: %1 -> %2")
638  .arg(ready_cmd).arg(result));
639  m_bError = true;
640  continue;
641  }
642 
643  if (restart_cnt++)
644  std::this_thread::sleep_for(std::chrono::seconds(20));
645  if (!RestartStream())
646  {
647  LOG(VB_RECORD, LOG_ERR, LOC +
648  "Failed to restart stream.");
649  m_bError = true;
650  }
651  continue;
652  }
653  m_xon = true;
654  }
655  }
656 
657  if (m_xon)
658  {
659  if (status_timer.elapsed() >= 2000)
660  {
661  // Since we may never need to send the XOFF
662  // command, occationally check to see if the
663  // External recorder needs to report an issue.
664  if (CheckForError())
665  {
666  if (restart_cnt++)
667  std::this_thread::sleep_for(std::chrono::seconds(20));
668  if (!RestartStream())
669  {
670  LOG(VB_RECORD, LOG_ERR, LOC + "Failed to restart stream.");
671  m_bError = true;
672  }
673  continue;
674  }
675 
676  status_timer.restart();
677  }
678 
679  if (buffer.size() > TOO_FAST_SIZE)
680  {
681  if (!m_poll_mode)
682  {
683  // Data is comming a little too fast, so XOFF
684  // to give us time to process it.
685  if (!ProcessCommand(QString("XOFF"), result))
686  {
687  if (result.startsWith("ERR"))
688  {
689  LOG(VB_GENERAL, LOG_ERR, LOC +
690  QString("Aborting: XOFF -> %2")
691  .arg(result));
692  m_bError = true;
693  }
694  }
695  m_xon = false;
696  }
697  }
698 
699  if (m_io && (sz = PACKET_SIZE - remainder) > 0)
700  read_len = m_io->Read(buffer, sz, 100);
701  else
702  read_len = 0;
703  }
704  else
705  read_len = 0;
706 
707  if (read_len == 0)
708  {
709  if (!nodata_timer.isRunning())
710  nodata_timer.start();
711  else
712  {
713  if (nodata_timer.elapsed() >= 50000)
714  {
715  LOG(VB_GENERAL, LOG_WARNING, LOC +
716  "No data for 50 seconds, Restarting stream.");
717  if (!RestartStream())
718  {
719  LOG(VB_RECORD, LOG_ERR, LOC +
720  "Failed to restart stream.");
721  m_bError = true;
722  }
723  nodata_timer.stop();
724  continue;
725  }
726  }
727 
728  std::this_thread::sleep_for(std::chrono::milliseconds(50));
729 
730  // HLS type streams may only produce data every ~10 seconds
731  if (nodata_timer.elapsed() < 12000 && buffer.size() < TS_PACKET_SIZE)
732  continue;
733  }
734  else
735  {
736  nodata_timer.stop();
737  restart_cnt = 0;
738  }
739 
740  if (m_io == nullptr)
741  {
742  LOG(VB_GENERAL, LOG_ERR, LOC + "I/O thread has disappeared!");
743  m_bError = true;
744  break;
745  }
746  if (m_io->Error())
747  {
748  LOG(VB_GENERAL, LOG_ERR, LOC +
749  QString("Fatal Error from External Recorder: %1")
750  .arg(m_io->ErrorString()));
751  CloseApp();
752  m_bError = true;
753  break;
754  }
755 
756  len = remainder = buffer.size();
757 
758  if (len == 0)
759  continue;
760 
761  if (len < TS_PACKET_SIZE)
762  {
763  if (m_xon && data_short_err++ == 0)
764  LOG(VB_RECORD, LOG_INFO, LOC + "Waiting for a full TS packet.");
765  std::this_thread::sleep_for(std::chrono::microseconds(50));
766  continue;
767  }
768  if (data_short_err)
769  {
770  if (data_short_err > 1)
771  {
772  LOG(VB_RECORD, LOG_INFO, LOC +
773  QString("Waited for a full TS packet %1 times.")
774  .arg(data_short_err));
775  }
776  data_short_err = 0;
777  }
778 
779  if (!m_stream_lock.tryLock())
780  continue;
781 
782  if (!m_listener_lock.tryLock())
783  continue;
784 
785  StreamDataList::const_iterator sit = m_stream_data_list.begin();
786  for (; sit != m_stream_data_list.end(); ++sit)
787  remainder = sit.key()->ProcessData
788  (reinterpret_cast<const uint8_t *>
789  (buffer.constData()), buffer.size());
790 
791  m_listener_lock.unlock();
792 
793  if (m_replay)
794  {
795  m_replay_buffer += buffer.left(len - remainder);
796  if (m_replay_buffer.size() > (50 * PACKET_SIZE))
797  {
798  m_replay_buffer.remove(0, len - remainder);
799  LOG(VB_RECORD, LOG_WARNING, LOC +
800  QString("Replay size truncated to %1 bytes")
801  .arg(m_replay_buffer.size()));
802  }
803  }
804 
805  m_stream_lock.unlock();
806 
807  if (remainder == 0)
808  {
809  buffer.clear();
810  good_data = len;
811  }
812  else if (len > remainder) // leftover bytes
813  {
814  buffer.remove(0, len - remainder);
815  good_data = len;
816  }
817  else if (len == remainder)
818  good_data = false;
819 
820  if (good_data)
821  {
822  if (data_proc_err)
823  {
824  if (data_proc_err > 1)
825  {
826  LOG(VB_RECORD, LOG_WARNING, LOC +
827  QString("Failed to process the data received %1 times.")
828  .arg(data_proc_err));
829  }
830  data_proc_err = 0;
831  }
832  }
833  else
834  {
835  if (data_proc_err++ == 0)
836  {
837  LOG(VB_RECORD, LOG_WARNING, LOC +
838  "Failed to process the data received");
839  }
840  }
841  }
842 
843  LOG(VB_RECORD, LOG_INFO, LOC + "run(): " +
844  QString("%1 shutdown").arg(m_bError ? "Error" : "Normal"));
845 
847  SetRunning(false, true, false);
848 
849  LOG(VB_RECORD, LOG_INFO, LOC + "run(): " + "end");
850 
851  RunEpilog();
852 }
853 
855 {
856  QString result;
857 
858  if (ProcessCommand("APIVersion?", result, 10000))
859  {
860  QStringList tokens = result.split(':', QString::SkipEmptyParts);
861 
862  if (tokens.size() > 1)
863  m_apiVersion = tokens[1].toUInt();
864  m_apiVersion = min(m_apiVersion, static_cast<int>(MAX_API_VERSION));
865  if (m_apiVersion < 1)
866  {
867  LOG(VB_RECORD, LOG_ERR, LOC +
868  QString("Bad response to 'APIVersion?' - '%1'. "
869  "Expecting 1 or 2").arg(result));
870  m_apiVersion = 1;
871  }
872 
873  ProcessCommand(QString("APIVersion:%1").arg(m_apiVersion), result);
874  return true;
875  }
876 
877  return false;
878 }
879 
881 {
882  if (m_apiVersion > 1)
883  {
884  QString result;
885 
886  if (ProcessCommand("Description?", result))
887  m_loc = result.mid(3);
888  else
889  m_loc = m_device;
890  }
891 
892  return m_loc;
893 }
894 
896 {
897  {
898  QMutexLocker locker(&m_io_lock);
899 
900  if (m_io)
901  {
902  LOG(VB_RECORD, LOG_WARNING, LOC + "OpenApp: already open!");
903  return true;
904  }
905 
906  m_io = new ExternIO(m_app, m_args);
907 
908  if (m_io == nullptr)
909  {
910  LOG(VB_GENERAL, LOG_ERR, LOC + "ExternIO failed: " + ENO);
911  m_bError = true;
912  }
913  else
914  {
915  LOG(VB_RECORD, LOG_INFO, LOC + QString("Spawn '%1'").arg(m_device));
916  m_io->Run();
917  if (m_io->Error())
918  {
919  LOG(VB_GENERAL, LOG_ERR,
920  "Failed to start External Recorder: " + m_io->ErrorString());
921  delete m_io;
922  m_io = nullptr;
923  m_bError = true;
924  return false;
925  }
926  }
927  }
928 
929  QString result;
930 
931  if (!SetAPIVersion())
932  {
933  // Try again using API version 2
934  m_apiVersion = 2;
935  if (!SetAPIVersion())
936  m_apiVersion = 1;
937  }
938 
939  if (!IsAppOpen())
940  {
941  LOG(VB_RECORD, LOG_ERR, LOC + "Application is not responding.");
942  m_bError = true;
943  return false;
944  }
945 
947 
948  // Gather capabilities
949  if (!ProcessCommand("HasTuner?", result))
950  {
951  LOG(VB_RECORD, LOG_ERR, LOC +
952  QString("Bad response to 'HasTuner?' - '%1'").arg(result));
953  m_bError = true;
954  return false;
955  }
956  m_hasTuner = result.startsWith("OK:Yes");
957 
958  if (!ProcessCommand("HasPictureAttributes?", result))
959  {
960  LOG(VB_RECORD, LOG_ERR, LOC +
961  QString("Bad response to 'HasPictureAttributes?' - '%1'")
962  .arg(result));
963  m_bError = true;
964  return false;
965  }
966  m_hasPictureAttributes = result.startsWith("OK:Yes");
967 
968  /* Operate in "poll" or "xon/xoff" mode */
969  m_poll_mode = ProcessCommand("FlowControl?", result) &&
970  result.startsWith("OK:Poll");
971 
972  LOG(VB_RECORD, LOG_INFO, LOC + "App opened successfully");
973  LOG(VB_RECORD, LOG_INFO, LOC +
974  QString("Capabilities: tuner(%1) "
975  "Picture attributes(%2) "
976  "Flow control(%3)")
977  .arg(m_hasTuner ? "yes" : "no")
978  .arg(m_hasPictureAttributes ? "yes" : "no")
979  .arg(m_poll_mode ? "Polling" : "XON/XOFF")
980  );
981 
982  /* Let the external app know how many bytes will read without blocking */
983  ProcessCommand(QString("BlockSize:%1").arg(PACKET_SIZE), result);
984 
985  return true;
986 }
987 
989 {
990  if (m_io == nullptr)
991  {
992  LOG(VB_RECORD, LOG_WARNING, LOC +
993  "WARNING: Unable to communicate with external app.");
994  return false;
995  }
996 
997  QString result;
998  return ProcessCommand("Version?", result, 10000);
999 }
1000 
1002 {
1003  if (m_tsopen)
1004  return true;
1005 
1006  QString result;
1007 
1008  if (!ProcessCommand("IsOpen?", result))
1009  return false;
1010 
1011  m_tsopen = true;
1012  return m_tsopen;
1013 }
1014 
1016 {
1017  m_io_lock.lock();
1018  if (m_io)
1019  {
1020  QString result;
1021 
1022  LOG(VB_RECORD, LOG_INFO, LOC + "CloseRecorder");
1023  m_io_lock.unlock();
1024  ProcessCommand("CloseRecorder", result, 10000);
1025  m_io_lock.lock();
1026 
1027  if (!result.startsWith("OK"))
1028  {
1029  LOG(VB_RECORD, LOG_INFO, LOC +
1030  "CloseRecorder failed, sending kill.");
1031 
1032  QString full_command = QString("%1").arg(m_args.join(" "));
1033 
1034  if (!m_io->KillIfRunning(full_command))
1035  {
1036  // Give it one more chance.
1037  std::this_thread::sleep_for(std::chrono::milliseconds(50));
1038  if (!m_io->KillIfRunning(full_command))
1039  {
1040  LOG(VB_GENERAL, LOG_ERR,
1041  QString("Unable to kill existing '%1'.")
1042  .arg(full_command));
1043  return;
1044  }
1045  }
1046  }
1047  delete m_io;
1048  m_io = nullptr;
1049  }
1050  m_io_lock.unlock();
1051 }
1052 
1054 {
1055  bool streaming = (StreamingCount() > 0);
1056 
1057  LOG(VB_RECORD, LOG_INFO, LOC + "Restarting stream.");
1058 
1059  if (streaming)
1060  StopStreaming();
1061 
1062  std::this_thread::sleep_for(std::chrono::seconds(1));
1063 
1064  if (streaming)
1065  return StartStreaming();
1066 
1067  return true;
1068 }
1069 
1071 {
1072  if (m_replay)
1073  {
1074  QString result;
1075 
1076  // Let the external app know that we could be busy for a little while
1077  if (!m_poll_mode)
1078  {
1079  ProcessCommand(QString("XOFF"), result);
1080  m_xon = false;
1081  }
1082 
1083  /* If the input is not a 'broadcast' it may only have one
1084  * copy of the SPS right at the beginning of the stream,
1085  * so make sure we don't miss it!
1086  */
1087  QMutexLocker listen_lock(&m_listener_lock);
1088 
1089  if (!m_stream_data_list.empty())
1090  {
1091  StreamDataList::const_iterator sit = m_stream_data_list.begin();
1092  for (; sit != m_stream_data_list.end(); ++sit)
1093  sit.key()->ProcessData(reinterpret_cast<const uint8_t *>
1094  (m_replay_buffer.constData()),
1095  m_replay_buffer.size());
1096  }
1097  LOG(VB_RECORD, LOG_INFO, LOC + QString("Replayed %1 bytes")
1098  .arg(m_replay_buffer.size()));
1099  m_replay_buffer.clear();
1100  m_replay = false;
1101 
1102  // Let the external app know that we are ready
1103  if (!m_poll_mode)
1104  {
1105  if (ProcessCommand(QString("XON"), result))
1106  m_xon = true;
1107  }
1108  }
1109 }
1110 
1112 {
1113  QString result;
1114 
1115  QMutexLocker locker(&m_stream_lock);
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_streaming_cnt.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_stream_lock);
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_streaming_cnt.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_poll_mode && 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_process_lock);
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  bool okay;
1239 
1240  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("ProcessVer1('%1')")
1241  .arg(cmd));
1242 
1243  for (uint cnt = 0; cnt < retry_cnt; ++cnt)
1244  {
1245  QMutexLocker locker(&m_io_lock);
1246 
1247  if (!m_io)
1248  {
1249  LOG(VB_RECORD, LOG_ERR, LOC + "External I/O not ready!");
1250  return false;
1251  }
1252 
1253  QByteArray buf(cmd.toUtf8(), cmd.size());
1254  buf += '\n';
1255 
1256  if (m_io->Error())
1257  {
1258  LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder in bad state: " +
1259  m_io->ErrorString());
1260  return false;
1261  }
1262 
1263  /* Try to keep in sync, if External app was too slow in responding
1264  * to previous query, consume the response before sending new query */
1265  m_io->GetStatus(0);
1266 
1267  /* Send new query */
1268  m_io->Write(buf);
1269 
1271  while (timer.elapsed() < timeout)
1272  {
1273  result = m_io->GetStatus(timeout);
1274  if (m_io->Error())
1275  {
1276  LOG(VB_GENERAL, LOG_ERR, LOC +
1277  "Failed to read from External Recorder: " +
1278  m_io->ErrorString());
1279  m_bError = true;
1280  return false;
1281  }
1282 
1283  // Out-of-band error message
1284  if (result.startsWith("STATUS:ERR") ||
1285  result.startsWith("0:STATUS:ERR"))
1286  {
1287  LOG(VB_RECORD, LOG_ERR, LOC + result);
1288  result.remove(0, result.indexOf(":ERR") + 1);
1289  return false;
1290  }
1291  // STATUS message are "out of band".
1292  // Ignore them while waiting for a responds to a command
1293  if (!result.startsWith("STATUS") && !result.startsWith("0:STATUS"))
1294  break;
1295  LOG(VB_RECORD, LOG_INFO, LOC +
1296  QString("Ignoring response '%1'").arg(result));
1297  }
1298 
1299  if (result.size() < 1)
1300  {
1301  LOG(VB_GENERAL, LOG_WARNING, LOC +
1302  QString("External Recorder did not respond to '%1'").arg(cmd));
1303  }
1304  else
1305  {
1306  okay = result.startsWith("OK");
1307  if (okay || result.startsWith("WARN") || result.startsWith("ERR"))
1308  {
1309  LogLevel_t level = LOG_INFO;
1310 
1311  m_io_errcnt = 0;
1312  if (!okay)
1313  level = LOG_WARNING;
1314  else if (cmd.startsWith("SendBytes"))
1315  level = LOG_DEBUG;
1316 
1317  LOG(VB_RECORD, level,
1318  LOC + QString("ProcessCommand('%1') = '%2' took %3ms %4")
1319  .arg(cmd).arg(result)
1320  .arg(timer.elapsed())
1321  .arg(okay ? "" : "<-- NOTE"));
1322 
1323  return okay;
1324  }
1325  LOG(VB_GENERAL, LOG_WARNING, LOC +
1326  QString("External Recorder invalid response to '%1': '%2'")
1327  .arg(cmd).arg(result));
1328  }
1329 
1330  if (++m_io_errcnt > 10)
1331  {
1332  LOG(VB_GENERAL, LOG_ERR, LOC + "Too many I/O errors.");
1333  m_bError = true;
1334  break;
1335  }
1336  }
1337 
1338  return false;
1339 }
1340 
1341 bool ExternalStreamHandler::ProcessVer2(const QString & command,
1342  QString & result, int timeout,
1343  uint retry_cnt)
1344 {
1345  bool okay;
1346  bool err;
1347  QString status;
1348  QString raw;
1349 
1350  for (uint cnt = 0; cnt < retry_cnt; ++cnt)
1351  {
1352  QString cmd = QString("%1:%2").arg(++m_serialNo).arg(command);
1353 
1354  LOG(VB_RECORD, LOG_DEBUG, LOC + QString("ProcessVer2('%1') serial(%2)")
1355  .arg(cmd).arg(m_serialNo));
1356 
1357  QMutexLocker locker(&m_io_lock);
1358 
1359  if (!m_io)
1360  {
1361  LOG(VB_RECORD, LOG_ERR, LOC + "External I/O not ready!");
1362  return false;
1363  }
1364 
1365  QByteArray buf(cmd.toUtf8(), cmd.size());
1366  buf += '\n';
1367 
1368  if (m_io->Error())
1369  {
1370  LOG(VB_GENERAL, LOG_ERR, LOC + "External Recorder in bad state: " +
1371  m_io->ErrorString());
1372  return false;
1373  }
1374 
1375  /* Send query */
1376  m_io->Write(buf);
1377 
1378  QStringList tokens;
1379 
1381  while (timer.elapsed() < timeout)
1382  {
1383  result = m_io->GetStatus(timeout);
1384  if (m_io->Error())
1385  {
1386  LOG(VB_GENERAL, LOG_ERR, LOC +
1387  "Failed to read from External Recorder: " +
1388  m_io->ErrorString());
1389  m_bError = true;
1390  return false;
1391  }
1392 
1393  if (!result.isEmpty())
1394  {
1395  raw = result;
1396  tokens = result.split(':', QString::SkipEmptyParts);
1397 
1398  // Look for result with the serial number of this query
1399  if (tokens.size() > 1 && tokens[0].toUInt() >= m_serialNo)
1400  break;
1401 
1402  /* Other messages are "out of band" */
1403 
1404  // Check for error message missing serial#
1405  if (tokens[0].startsWith("ERR"))
1406  break;
1407 
1408  // Remove serial#
1409  tokens.removeFirst();
1410  result = tokens.join(':');
1411  err = (tokens.size() > 1 && tokens[1].startsWith("ERR"));
1412  LOG(VB_RECORD, (err ? LOG_WARNING : LOG_INFO), LOC + raw);
1413  if (err)
1414  {
1415  // Remove "STATUS"
1416  tokens.removeFirst();
1417  result = tokens.join(':');
1418  return false;
1419  }
1420  }
1421  }
1422 
1423  if (timer.elapsed() >= timeout)
1424  {
1425  LOG(VB_RECORD, LOG_ERR, LOC +
1426  QString("ProcessVer2: Giving up waiting for response for "
1427  "command '%2'").arg(cmd));
1428  }
1429  else if (tokens.size() < 2)
1430  {
1431  LOG(VB_RECORD, LOG_ERR, LOC +
1432  QString("Did not receive a valid response "
1433  "for command '%1', received '%2'").arg(cmd).arg(result));
1434  }
1435  else if (tokens[0].toUInt() > m_serialNo)
1436  {
1437  LOG(VB_RECORD, LOG_ERR, LOC +
1438  QString("ProcessVer2: Looking for serial no %1, "
1439  "but received %2 for command '%2'")
1440  .arg(m_serialNo).arg(tokens[0]).arg(cmd));
1441  }
1442  else
1443  {
1444  tokens.removeFirst();
1445  status = tokens[0].trimmed();
1446  result = tokens.join(':');
1447 
1448  okay = (status == "OK");
1449  if (okay || status.startsWith("WARN") || status.startsWith("ERR"))
1450  {
1451  LogLevel_t level = LOG_INFO;
1452 
1453  m_io_errcnt = 0;
1454  if (!okay)
1455  level = LOG_WARNING;
1456  else if (command.startsWith("SendBytes"))
1457  level = LOG_DEBUG;
1458 
1459  LOG(VB_RECORD, level,
1460  LOC + QString("ProcessV2('%1') = '%2' took %3ms %4")
1461  .arg(cmd).arg(result).arg(timer.elapsed())
1462  .arg(okay ? "" : "<-- NOTE"));
1463 
1464  return okay;
1465  }
1466  LOG(VB_GENERAL, LOG_WARNING, LOC +
1467  QString("External Recorder invalid response to '%1': '%2'")
1468  .arg(cmd).arg(result));
1469  }
1470 
1471  if (++m_io_errcnt > 10)
1472  {
1473  LOG(VB_GENERAL, LOG_ERR, LOC + "Too many I/O errors.");
1474  m_bError = true;
1475  break;
1476  }
1477  }
1478 
1479  return false;
1480 }
1481 
1483 {
1484  QString result;
1485  bool err = false;
1486 
1487  QMutexLocker locker(&m_io_lock);
1488 
1489  if (!m_io)
1490  {
1491  LOG(VB_RECORD, LOG_ERR, LOC + "External I/O not ready!");
1492  return true;
1493  }
1494 
1495  if (m_io->Error())
1496  {
1497  LOG(VB_GENERAL, LOG_ERR, "External Recorder in bad state: " +
1498  m_io->ErrorString());
1499  return true;
1500  }
1501 
1502  do
1503  {
1504  result = m_io->GetStatus(0);
1505  if (!result.isEmpty())
1506  {
1507  if (m_apiVersion > 1)
1508  {
1509  QStringList tokens = result.split(':', QString::SkipEmptyParts);
1510 
1511  tokens.removeFirst();
1512  result = tokens.join(':');
1513  for (int idx = 1; idx < tokens.size(); ++idx)
1514  err |= tokens[idx].startsWith("ERR");
1515  }
1516  else
1517  err |= result.startsWith("STATUS:ERR");
1518 
1519  LOG(VB_RECORD, (err ? LOG_WARNING : LOG_INFO), LOC + result);
1520  }
1521  }
1522  while (!result.isEmpty());
1523 
1524  return err;
1525 }
1526 
1528 {
1529  if (m_io)
1530  {
1531  QByteArray buffer;
1532  m_io->Read(buffer, PACKET_SIZE, 1);
1533  m_io->GetStatus(1);
1534  }
1535 }
1536 
1538 {
1539  // TODO report on buffer overruns, etc.
1540 }
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
volatile bool m_running_desired
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)
unsigned int uint
Definition: compat.h:140
QMutex m_listener_lock
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
static QMap< int, uint > s_handlers_refcnt
bool Ready(int fd, int timeout, const QString &what)
bool Error(void) const
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:99
int elapsed(void) const
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:90
#define WEXITSTATUS(w)
Definition: compat.h:328
static ExternalStreamHandler * Get(const QString &devname, int inputid, int majorid)
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)
StreamDataList m_stream_data_list
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
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)
QString m_status_buf