4#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
5#include <QtSystemDetection>
26#include <QJsonDocument>
30#include "libmythbase/mythconfig.h"
43#define LOC QString("ExternSH[%1](%2): ").arg(m_inputId).arg(m_loc)
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)
51 m_status(&m_statusBuf, QIODeviceBase::ReadWrite)
56 m_error = QString(
"ExternIO: '%1' does not exist.").arg(app);
61 m_error = QString(
"ExternIO: '%1' is not readable.")
62 .arg(
m_app.canonicalFilePath());
65 if (!
m_app.isExecutable())
67 m_error = QString(
"ExternIO: '%1' is not executable.")
68 .arg(
m_app.canonicalFilePath());
89 [[maybe_unused]] std::chrono::milliseconds
timeout,
90 [[maybe_unused]]
const QString & what)
93 std::array<struct pollfd,2> m_poll {};
96 m_poll[0].events = POLLIN | POLLPRI;
97 int ret = poll(m_poll.data(), 1,
timeout.count());
99 if (m_poll[0].revents & POLLHUP)
101 m_error = what +
" poll eof (POLLHUP)";
104 if (m_poll[0].revents & POLLNVAL)
106 LOG(VB_GENERAL, LOG_ERR,
"poll error");
109 if (m_poll[0].revents & POLLIN)
114 if ((EOVERFLOW == errno))
126 LOG(VB_RECORD, LOG_ERR,
127 QString(
"ExternIO::Read: already in error state: '%1'")
150 m_error =
"Failed to read from External Recorder: " +
ENO;
151 LOG(VB_RECORD, LOG_WARNING,
152 "External Recorder not ready. Giving up.");
156 LOG(VB_RECORD, LOG_WARNING,
157 QString(
"External Recorder not ready. Will retry (%1/%2).")
159 std::this_thread::sleep_for(100ms);
164 m_error =
"Failed to read from External Recorder: " +
ENO;
178 LOG(VB_RECORD, LOG_DEBUG,
179 QString(
"ExternIO::Read '%1' bytes, buffer size %2")
180 .arg(len).arg(buffer.size()));
189 LOG(VB_RECORD, LOG_ERR,
190 QString(
"ExternIO::GetStatus: already in error state: '%1'")
198 std::array<char,2048> buffer {};
200 m_status << QString::fromLatin1(buffer.data(), len);
208 LOG(VB_RECORD, LOG_DEBUG, QString(
"ExternIO::GetStatus '%1'")
218 LOG(VB_RECORD, LOG_ERR,
219 QString(
"ExternIO::Write: already in error state: '%1'")
224 LOG(VB_RECORD, LOG_DEBUG, QString(
"ExternIO::Write('%1')")
225 .arg(QString(buffer).simplified()));
227 int len =
write(
m_appIn, buffer.constData(), buffer.size());
228 if (len != buffer.size())
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)));
238 m_error = QString(
"ExternIO: Failed to write '%1' to app's stdin: ")
239 .arg(QString(buffer)) +
ENO;
249 LOG(VB_RECORD, LOG_INFO, QString(
"ExternIO::Run()"));
262#elif defined( Q_OS_WINDOWS )
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")
269 int res_grp = system(grp.toUtf8().constData());
272 LOG(VB_RECORD, LOG_DEBUG, QString(
"'%1' not running.").arg(cmd));
276 LOG(VB_RECORD, LOG_WARNING, QString(
"'%1' already running, killing...")
278 int res_kil = system(kil.toUtf8().constData());
280 LOG(VB_GENERAL, LOG_WARNING, QString(
"'%1' failed: %2")
283 res_grp = system(grp.toUtf8().constData());
286 LOG(
WEXITSTATUS(res_kil) == 0 ? VB_RECORD : VB_GENERAL, LOG_WARNING,
287 QString(
"'%1' terminated.").arg(cmd));
291 std::this_thread::sleep_for(50ms);
293 kil = QString(
"pkill --signal 9 -x -f \"%1\" 2>&1 > /dev/null").arg(cmd);
294 res_kil = system(kil.toUtf8().constData());
296 LOG(VB_GENERAL, LOG_WARNING, QString(
"'%1' failed: %2")
299 res_grp = system(grp.toUtf8().constData());
300 LOG(
WEXITSTATUS(res_kil) == 0 ? VB_RECORD : VB_GENERAL, LOG_WARNING,
302 .arg(cmd,
WEXITSTATUS(res_grp) == 0 ?
"sill running" :
"terminated"));
313 LOG(VB_RECORD, LOG_INFO, QString(
"ExternIO in bad state: '%1'")
318 QString full_command = QString(
"%1").arg(
m_args.join(
" "));
323 std::this_thread::sleep_for(50ms);
326 m_error = QString(
"Unable to kill existing '%1'.")
334 LOG(VB_RECORD, LOG_INFO, QString(
"ExternIO::Fork '%1'").arg(full_command));
336 std::array<int,2> in = {-1, -1};
337 std::array<int,2> out = {-1, -1};
338 std::array<int,2> err = {-1, -1};
340 if (pipe(in.data()) < 0)
345 if (pipe(out.data()) < 0)
352 if (pipe(err.data()) < 0)
386 LOG(VB_GENERAL, LOG_WARNING,
387 "ExternIO::Fork(): Failed to set O_NONBLOCK for FD: " +
ENO);
388 std::this_thread::sleep_for(2s);
392 LOG(VB_RECORD, LOG_INFO,
"Spawned");
400 if (dup2( in[0], 0) < 0)
402 std::cerr <<
"dup2(stdin) failed: " << strerror(errno);
405 else if (dup2(out[1], 1) < 0)
407 std::cerr <<
"dup2(stdout) failed: " << strerror(errno);
410 else if (dup2(err[1], 2) < 0)
412 std::cerr <<
"dup2(stderr) failed: " << strerror(errno);
418 close_range(3, sysconf(_SC_OPEN_MAX) - 1, 0);
420 for (
int i = sysconf(_SC_OPEN_MAX) - 1; i > 2; --i)
427 if (setpgid(0,0) < 0)
429 std::cerr <<
"ExternIO: "
430 <<
"setpgid() failed: "
431 << strerror(errno) <<
'\n';
435 char *command = strdup(
m_app.canonicalFilePath()
436 .toUtf8().constData());
438 char **arguments =
new char*[
m_args.size() + 1];
439 for (
int i = 0; i <
m_args.size(); ++i)
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);
445 arguments[
m_args.size()] =
nullptr;
447 if (execv(command, arguments) < 0)
450 std::cerr <<
"ExternIO: "
451 <<
"execv() failed: "
452 << strerror(errno) <<
'\n';
456 std::cerr <<
"ExternIO: "
457 <<
"execv() should not be here?: "
458 << strerror(errno) <<
'\n';
473 int inputid,
int majorid)
477 QMap<int, ExternalStreamHandler*>::iterator it =
s_handlers.find(majorid);
485 LOG(VB_RECORD, LOG_INFO,
486 QString(
"ExternSH[%1:%2]: Creating new stream handler for %3 "
488 .arg(inputid).arg(majorid).arg(devname));
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));
514 QMap<int, ExternalStreamHandler*>::iterator it =
522 LOG(VB_RECORD, LOG_INFO,
523 QString(
"ExternSH[%1:%2]: Return handler (%3 still in use)")
524 .arg(inputid).arg(majorid).arg(*rit));
531 LOG(VB_RECORD, LOG_INFO,
532 QString(
"ExternSH[%1:%2]: Closing handler (0 in use)")
533 .arg(inputid).arg(majorid));
539 LOG(VB_GENERAL, LOG_ERR,
540 QString(
"ExternSH[%1:%2]: Error: No handler to return!")
541 .arg(inputid).arg(majorid));
561 m_args = path.split(
' ',Qt::SkipEmptyParts) +
568 if (!
m_args.contains(
"--quiet") && !
m_args.contains(
"-q"))
571 m_args <<
"--inputid" << QString::number(majorid);
572 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"args \"%1\"")
577 LOG(VB_GENERAL, LOG_ERR,
LOC +
578 QString(
"Failed to start %1").arg(
m_device));
595 uint restart_cnt = 0;
599 bool good_data =
false;
600 uint data_proc_err = 0;
601 uint data_short_err = 0;
605 LOG(VB_GENERAL, LOG_ERR,
LOC +
606 QString(
"%1 is not running.").arg(
m_device));
609 status_timer.
start();
613 LOG(VB_RECORD, LOG_INFO,
LOC +
"run(): begin");
618 ready_cmd =
"SendBytes";
627 LOG(VB_RECORD, LOG_WARNING,
LOC +
"TS not open yet.");
628 std::this_thread::sleep_for(10ms);
634 std::this_thread::sleep_for(10ms);
644 LOG(VB_RECORD, LOG_WARNING,
LOC +
645 "Internal buffer too full to accept more data from "
646 "external application.");
652 if (result.startsWith(
"ERR"))
654 LOG(VB_GENERAL, LOG_ERR,
LOC +
655 QString(
"Aborting: %1 -> %2")
656 .arg(ready_cmd, result));
662 std::this_thread::sleep_for(20s);
665 LOG(VB_RECORD, LOG_ERR,
LOC +
666 "Failed to restart stream.");
677 if (status_timer.
elapsed() >= 2s)
685 std::this_thread::sleep_for(20s);
688 LOG(VB_RECORD, LOG_ERR,
LOC +
689 "Failed to restart stream.");
706 if (result.startsWith(
"ERR"))
708 LOG(VB_GENERAL, LOG_ERR,
LOC +
709 QString(
"Aborting: XOFF -> %2")
723 read_len =
m_io->
Read(buffer, sz, 100ms);
734 nodata_timer.
start();
737 if (nodata_timer.
elapsed() >= 50s)
739 LOG(VB_GENERAL, LOG_WARNING,
LOC +
740 "No data for 50 seconds, Restarting stream.");
743 LOG(VB_RECORD, LOG_ERR,
LOC +
744 "Failed to restart stream.");
752 std::this_thread::sleep_for(50ms);
766 LOG(VB_GENERAL, LOG_ERR,
LOC +
"I/O thread has disappeared!");
772 LOG(VB_GENERAL, LOG_ERR,
LOC +
773 QString(
"Fatal Error from External Recorder: %1")
780 len = remainder = buffer.size();
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);
794 if (data_short_err > 1)
796 LOG(VB_RECORD, LOG_INFO,
LOC +
797 QString(
"Waited for a full TS packet %1 times.")
798 .arg(data_short_err));
812 remainder = sit.key()->ProcessData
813 (
reinterpret_cast<const uint8_t *
>
814 (buffer.constData()), buffer.size());
825 LOG(VB_RECORD, LOG_WARNING,
LOC +
826 QString(
"Replay size truncated to %1 bytes")
836 good_data = (len != 0U);
838 else if (len > remainder)
840 buffer.remove(0, len - remainder);
841 good_data = (len != 0U);
843 else if (len == remainder)
852 if (data_proc_err > 1)
854 LOG(VB_RECORD, LOG_WARNING,
LOC +
855 QString(
"Failed to process the data received %1 times.")
856 .arg(data_proc_err));
863 if (data_proc_err++ == 0)
865 LOG(VB_RECORD, LOG_WARNING,
LOC +
866 "Failed to process the data received");
871 LOG(VB_RECORD, LOG_INFO,
LOC +
"run(): " +
872 QString(
"%1 shutdown").arg(
m_bError ?
"Error" :
"Normal"));
877 LOG(VB_RECORD, LOG_INFO,
LOC +
"run(): " +
"end");
888 QStringList tokens = result.split(
':', Qt::SkipEmptyParts);
889 if (tokens.size() > 1)
894 LOG(VB_RECORD, LOG_ERR,
LOC +
895 QString(
"Bad response to 'APIVersion?' - '%1'. "
896 "Expecting 1, 2 or 3").arg(result));
914 m_loc = result.mid(3);
929 LOG(VB_RECORD, LOG_WARNING,
LOC +
"OpenApp: already open!");
937 LOG(VB_GENERAL, LOG_ERR,
LOC +
"ExternIO failed: " +
ENO);
942 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Spawn '%1'").arg(
m_device));
946 LOG(VB_GENERAL, LOG_ERR,
968 LOG(VB_RECORD, LOG_ERR,
LOC +
"Application is not responding.");
978 LOG(VB_RECORD, LOG_ERR,
LOC +
979 QString(
"Bad response to 'HasTuner?' - '%1'").arg(result));
987 LOG(VB_RECORD, LOG_ERR,
LOC +
988 QString(
"Bad response to 'HasPictureAttributes?' - '%1'")
997 result.startsWith(
"OK:Poll");
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) "
1017 if (
m_io ==
nullptr)
1019 LOG(VB_RECORD, LOG_WARNING,
LOC +
1020 "WARNING: Unable to communicate with external app.");
1049 LOG(VB_RECORD, LOG_INFO,
LOC +
"CloseRecorder");
1054 if (!result.startsWith(
"OK"))
1056 LOG(VB_RECORD, LOG_INFO,
LOC +
1057 "CloseRecorder failed, sending kill.");
1059 QString full_command = QString(
"%1").arg(
m_args.join(
" "));
1064 std::this_thread::sleep_for(50ms);
1067 LOG(VB_GENERAL, LOG_ERR,
1068 QString(
"Unable to kill existing '%1'.")
1069 .arg(full_command));
1084 LOG(VB_RECORD, LOG_WARNING,
LOC +
"Restarting stream.");
1090 std::this_thread::sleep_for(1s);
1122 sit.key()->ProcessData(
reinterpret_cast<const uint8_t *
>
1127 LOG(VB_RECORD, LOG_INFO,
LOC + QString(
"Replayed %1 bytes")
1149 LOG(VB_RECORD, LOG_INFO,
LOC +
1150 QString(
"StartStreaming with %1 current listeners")
1155 LOG(VB_GENERAL, LOG_ERR,
LOC +
"External Recorder not started.");
1163 LogLevel_t level = LOG_ERR;
1164 if (result.startsWith(
"warn", Qt::CaseInsensitive))
1165 level = LOG_WARNING;
1169 LOG(VB_GENERAL, level,
LOC + QString(
"StartStreaming failed: '%1'")
1174 LOG(VB_RECORD, LOG_INFO,
LOC +
"Streaming started");
1178 LOG(VB_RECORD, LOG_INFO,
LOC +
"Already streaming");
1184 LOG(VB_RECORD, LOG_INFO,
LOC +
1185 QString(
"StartStreaming %1 listeners")
1195 LOG(VB_RECORD, LOG_INFO,
LOC +
1196 QString(
"StopStreaming %1 listeners")
1201 LOG(VB_RECORD, LOG_INFO,
LOC +
1202 "StopStreaming requested, but we are not streaming!");
1208 LOG(VB_RECORD, LOG_INFO,
LOC +
1209 QString(
"StopStreaming delayed, still have %1 listeners")
1214 LOG(VB_RECORD, LOG_INFO,
LOC +
"StopStreaming");
1225 LOG(VB_GENERAL, LOG_ERR,
LOC +
"External Recorder not started.");
1232 LogLevel_t level = LOG_ERR;
1233 if (result.startsWith(
"warn", Qt::CaseInsensitive))
1234 level = LOG_WARNING;
1238 LOG(VB_GENERAL, level,
LOC + QString(
"StopStreaming: '%1'")
1246 LOG(VB_RECORD, LOG_INFO,
LOC +
"Streaming stopped");
1253 std::chrono::milliseconds
timeout,
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];
1268 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1269 QString(
"Arguments: %1").arg(tokens.join(
"\n")));
1272 result = QString(
"%1:%2").arg(vresult[
"status"].
toString(),
1281 LOG(VB_RECORD, LOG_ERR,
LOC +
1282 QString(
"Invalid API version %1. Expected 1 or 2").arg(
m_apiVersion));
1288 std::chrono::milliseconds
timeout,
1291 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"ProcessVer1('%1')")
1294 for (
uint cnt = 0; cnt < retry_cnt; ++cnt)
1300 LOG(VB_RECORD, LOG_ERR,
LOC +
"External I/O not ready!");
1304 QByteArray buf = cmd.toUtf8();
1309 LOG(VB_GENERAL, LOG_ERR,
LOC +
"External Recorder in bad state: " +
1327 LOG(VB_GENERAL, LOG_ERR,
LOC +
1328 "Failed to read from External Recorder: " +
1335 if (result.startsWith(
"STATUS:ERR") ||
1336 result.startsWith(
"0:STATUS:ERR"))
1338 LOG(VB_RECORD, LOG_ERR,
LOC + result);
1339 result.remove(0, result.indexOf(
":ERR") + 1);
1344 if (!result.startsWith(
"STATUS") && !result.startsWith(
"0:STATUS"))
1346 LOG(VB_RECORD, LOG_INFO,
LOC +
1347 QString(
"Ignoring response '%1'").arg(result));
1350 if (result.size() < 1)
1352 LOG(VB_GENERAL, LOG_WARNING,
LOC +
1353 QString(
"External Recorder did not respond to '%1'").arg(cmd));
1357 bool okay = result.startsWith(
"OK");
1358 if (okay || result.startsWith(
"WARN") || result.startsWith(
"ERR"))
1360 LogLevel_t level = LOG_INFO;
1364 level = LOG_WARNING;
1365 else if (cmd.startsWith(
"SendBytes"))
1368 LOG(VB_RECORD, level,
1369 LOC + QString(
"ProcessCommand('%1') = '%2' took %3ms %4")
1371 QString::number(timer.
elapsed().count()),
1372 okay ?
"" :
"<-- NOTE"));
1376 LOG(VB_GENERAL, LOG_WARNING,
LOC +
1377 QString(
"External Recorder invalid response to '%1': '%2'")
1383 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Too many I/O errors.");
1394 std::chrono::milliseconds
timeout,
1400 for (
uint cnt = 0; cnt < retry_cnt; ++cnt)
1402 QString cmd = QString(
"%1:%2").arg(++
m_serialNo).arg(command);
1404 LOG(VB_RECORD, LOG_DEBUG,
LOC + QString(
"ProcessVer2('%1') serial(%2)")
1411 LOG(VB_RECORD, LOG_ERR,
LOC +
"External I/O not ready!");
1415 QByteArray buf = cmd.toUtf8();
1420 LOG(VB_GENERAL, LOG_ERR,
LOC +
"External Recorder in bad state: " +
1436 LOG(VB_GENERAL, LOG_ERR,
LOC +
1437 "Failed to read from External Recorder: " +
1443 if (!result.isEmpty())
1446 tokens = result.split(
':', Qt::SkipEmptyParts);
1449 if (tokens.size() > 1 && tokens[0].toUInt() >=
m_serialNo)
1455 if (tokens[0].startsWith(
"ERR"))
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);
1466 tokens.removeFirst();
1467 result = tokens.join(
':');
1475 LOG(VB_RECORD, LOG_ERR,
LOC +
1476 QString(
"ProcessVer2: Giving up waiting for response for "
1477 "command '%2'").arg(cmd));
1479 else if (tokens.size() < 2)
1481 LOG(VB_RECORD, LOG_ERR,
LOC +
1482 QString(
"Did not receive a valid response "
1483 "for command '%1', received '%2'").arg(cmd, result));
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));
1494 tokens.removeFirst();
1495 status = tokens[0].trimmed();
1496 result = tokens.join(
':');
1498 bool okay = (status ==
"OK");
1499 if (okay || status.startsWith(
"WARN") || status.startsWith(
"ERR"))
1501 LogLevel_t level = LOG_INFO;
1505 level = LOG_WARNING;
1506 else if (command.startsWith(
"SendBytes") ||
1507 (command.startsWith(
"TuneStatus") &&
1508 result ==
"OK:InProgress"))
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"));
1518 LOG(VB_GENERAL, LOG_WARNING,
LOC +
1519 QString(
"External Recorder invalid response to '%1': '%2'")
1525 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Too many I/O errors.");
1535 QVariantMap & elements,
1536 QByteArray & response,
1537 std::chrono::milliseconds
timeout,
1540 for (
uint cnt = 0; cnt < retry_cnt; ++cnt)
1542 QVariantMap query(vmsg);
1545 query[
"serial"] = serial;
1546 QString cmd = query[
"command"].toString();
1549 qdoc = QJsonDocument::fromVariant(query);
1550 QByteArray cmdbuf = qdoc.toJson(QJsonDocument::Compact);
1552 LOG(VB_RECORD, LOG_DEBUG,
LOC +
1553 QString(
"ProcessJson: %1").arg(QString(cmdbuf)));
1557 LOG(VB_GENERAL, LOG_ERR,
LOC +
"External Recorder in bad state: " +
1572 LOG(VB_GENERAL, LOG_ERR,
LOC +
1573 "Failed to read from External Recorder: " +
1579 if (!response.isEmpty())
1581 QJsonParseError parseError {};
1584 doc = QJsonDocument::fromJson(response, &parseError);
1586 if (parseError.error != QJsonParseError::NoError)
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(),
1597 elements = doc.toVariant().toMap();
1598 if (!elements.contains(
"serial"))
1601 serial = elements[
"serial"].toInt();
1605 if (elements.contains(
"status"))
1607 LogLevel_t level { LOG_INFO };
1609 if (elements[
"status"] ==
"ERR")
1611 else if (elements[
"status"] ==
"WARN")
1612 level = LOG_WARNING;
1614 LOG(VB_RECORD, level,
LOC + QString(
"%1: %2")
1615 .arg(elements[
"status"].
toString(),
1624 LOG(VB_RECORD, LOG_ERR,
LOC +
1625 QString(
"ProcessJson: Giving up waiting for response for "
1626 "command '%2'").arg(QString(cmdbuf)));
1632 LOG(VB_RECORD, LOG_ERR,
LOC +
1633 QString(
"ProcessJson: Looking for serial no %1, "
1634 "but received %2 for command '%2'")
1637 .arg(QString(cmdbuf)));
1639 else if (!elements.contains(
"status"))
1641 LOG(VB_RECORD, LOG_ERR,
LOC +
1642 QString(
"ProcessJson: ExternalRecorder 'status' not found in %1")
1643 .arg(QString(response)));
1647 QString status = elements[
"status"].toString();
1648 bool okay = (status ==
"OK");
1649 if (okay || status ==
"WARN" || status ==
"ERR")
1651 LogLevel_t level = LOG_INFO;
1655 level = LOG_WARNING;
1656 else if (cmd ==
"SendBytes" ||
1657 (cmd ==
"TuneStatus?" &&
1658 elements[
"message"] ==
"InProgress"))
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(),
1667 QString::number(timer.
elapsed().count()),
1668 okay ?
"" :
"<-- NOTE")
1673 LOG(VB_GENERAL, LOG_WARNING,
LOC +
1674 QString(
"External Recorder invalid response to '%1': '%2'")
1675 .arg(QString(cmdbuf),
1676 QString(response)));
1681 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Too many I/O errors.");
1692 QByteArray response;
1699 LOG(VB_RECORD, LOG_ERR,
LOC +
"External I/O not ready!");
1705 LOG(VB_GENERAL, LOG_ERR,
"External Recorder in bad state: " +
1711 while (!response.isEmpty())
1715 QJsonParseError parseError {};
1717 QVariantMap elements;
1719 doc = QJsonDocument::fromJson(response, &parseError);
1721 if (parseError.error != QJsonParseError::NoError)
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)));
1730 elements = doc.toVariant().toMap();
1731 if (elements.contains(
"command") &&
1732 (QString::compare(elements[
"command"].toString(),
1734 Qt::CaseInsensitive) == 0))
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))
1743 if (status.startsWith(
"err", Qt::CaseInsensitive))
1747 else if (status.startsWith(
"warn",
1748 Qt::CaseInsensitive))
1750 level = LOG_WARNING;
1752 else if (status.startsWith(
"debug",
1753 Qt::CaseInsensitive))
1757 else if (status.startsWith(
"trace",
1758 Qt::CaseInsensitive))
1762 else if (status.startsWith(
"damage",
1763 Qt::CaseInsensitive))
1765 level = LOG_WARNING;
1770 if (message.trimmed().startsWith(
"damage",
1771 Qt::CaseInsensitive))
1777 LOG(VB_RECORD, level,
1778 LOC + QString(
"%1:%2%3")
1779 .arg(status, message,
1786 QString res = QString(response);
1789 QStringList tokens = res.split(
':', Qt::SkipEmptyParts);
1790 tokens.removeFirst();
1791 res = tokens.join(
':');
1792 for (
int idx = 1; idx < tokens.size(); ++idx)
1794 err |= tokens[idx].startsWith(
"ERR",
1795 Qt::CaseInsensitive);
1797 m_damaged |= tokens[idx].startsWith(
"damage",
1798 Qt::CaseInsensitive);
1803 err |= res.startsWith(
"STATUS:ERR",
1804 Qt::CaseInsensitive);
1806 m_damaged |= res.startsWith(
"STATUS:DAMAGE",
1807 Qt::CaseInsensitive);
1810 LOG(VB_RECORD, (err ? LOG_WARNING : LOG_INFO),
LOC + res);
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)
ExternIO(const QString &app, const QStringList &args)
QString ErrorString(void) const
QByteArray m_replayBuffer
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.
int StreamingCount(void) const
static QMutex s_handlersLock
bool m_hasPictureAttributes
bool StartStreaming(bool recording)
ExternalStreamHandler(const QString &path, int inputid, int majorid)
QAtomicInt m_streamingCnt
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)
QString UpdateDescription(void)
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().
void RunEpilog(void)
Cleans up a thread's resources, call this if you reimplement run().
void setObjectName(const QString &name)
A QElapsedTimer based timer to replace use of QTime as a timer.
std::chrono::milliseconds restart(void)
Returns milliseconds elapsed since last start() or restart() and resets the count.
std::chrono::milliseconds elapsed(void)
Returns milliseconds elapsed since last start() or restart()
bool isRunning(void) const
Returns true if start() or restart() has been called at least once since construction and since any c...
void stop(void)
Stops timer, next call to isRunning() will return false and any calls to elapsed() or restart() will ...
void start(void)
starts measuring elapsed time.
StreamDataList m_streamDataList
volatile bool m_runningDesired
bool RemoveAllPIDFilters(void)
void SetRunning(bool running, bool using_buffering, bool using_section_reader)
bool UpdateFiltersFromStreamData(void)
QRecursiveMutex m_listenerLock
@ GENERIC_EXIT_DAEMONIZING_ERROR
Error daemonizing or execl.
@ GENERIC_EXIT_PIPE_FAILURE
Error creating I/O pipes.
#define ENO
This can be appended to the LOG args with "+".
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Convenience inline random number generator functions.
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
def read(device=None, features=[])
def write(text, progress=True)