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