239 lines
13 KiB
C++
239 lines
13 KiB
C++
#include "web/renderer_watchdog.h"
|
|
#include <QCoreApplication>
|
|
#include <QProcess>
|
|
#include <QSignalSpy>
|
|
#include <QTest>
|
|
#include <QTimer>
|
|
#include <limits>
|
|
|
|
class RendererWatchdogTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void procStatParsing();
|
|
void rendererArgumentParsing();
|
|
void rejectsForeignAndNonRenderer();
|
|
void terminatesOnlyRegisteredRenderer();
|
|
void forgetsExitedRenderer();
|
|
void boundedSpreadSlots();
|
|
void responseProbeLifetime();
|
|
void responseTimeoutStopsOnlyStalledSlot();
|
|
void suspensionRestartsResponseBudget();
|
|
};
|
|
|
|
void RendererWatchdogTests::procStatParsing() {
|
|
QList<QByteArray> fields;
|
|
for (int i = 0; i < 22; ++i) fields << "0";
|
|
fields[0] = "S"; fields[1] = "123"; fields[19] = "456"; fields[21] = "789";
|
|
auto data = QByteArray("999 (name with ) parens) ") + fields.join(' ');
|
|
docview::RendererProcessInfo info;
|
|
QVERIFY(docview::parseRendererProcStat(data, 4096, &info));
|
|
QCOMPARE(info.parentPid, 123); QCOMPARE(info.startTime, 456u); QCOMPARE(info.residentBytes, 789ull * 4096);
|
|
QVERIFY(!docview::parseRendererProcStat(data, 0, &info));
|
|
QVERIFY(!docview::parseRendererProcStat(data, std::numeric_limits<quint64>::max(), &info));
|
|
QVERIFY(!docview::parseRendererProcStat("invalid", 4096, &info));
|
|
fields[21] = "-1";
|
|
QVERIFY(!docview::parseRendererProcStat(QByteArray("999 (name) ") + fields.join(' '), 4096, &info));
|
|
}
|
|
|
|
void RendererWatchdogTests::rejectsForeignAndNonRenderer() {
|
|
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath());
|
|
QString error;
|
|
QVERIFY(!watchdog.registerRenderer("foreign", 1, &error));
|
|
QVERIFY(!watchdog.registerRenderer("self", QCoreApplication::applicationPid(), &error));
|
|
QVERIFY(!watchdog.registerRenderer("overflow", std::numeric_limits<qint64>::max(), &error));
|
|
QProcess child;
|
|
child.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--allocate", "1"});
|
|
QVERIFY(child.waitForStarted()); QVERIFY(child.waitForReadyRead());
|
|
QVERIFY(!watchdog.registerRenderer("wrong-type", child.processId(), &error));
|
|
QCOMPARE(watchdog.monitoredCount(), 0);
|
|
QCOMPARE(child.state(), QProcess::Running);
|
|
child.kill(); QVERIFY(child.waitForFinished());
|
|
}
|
|
void RendererWatchdogTests::rendererArgumentParsing() {
|
|
const QByteArray executable("/trusted path/QtWebEngineProcess");
|
|
QVERIFY(docview::isRendererCommandLine(executable + '\0' + "--type=renderer" + '\0' + "--other=x" + '\0', executable));
|
|
QVERIFY(docview::isRendererCommandLine(executable + " --type=renderer --other=x" + '\0', executable));
|
|
QVERIFY(!docview::isRendererCommandLine(executable + " --type=renderer-other" + '\0', executable));
|
|
QVERIFY(!docview::isRendererCommandLine(executable + " --url=space --type=renderer" + '\0', executable));
|
|
QVERIFY(!docview::isRendererCommandLine(executable + "-other --type=renderer" + '\0', executable));
|
|
QVERIFY(!docview::isRendererCommandLine(executable + '\0' + "--url= --type=renderer" + '\0', executable));
|
|
QVERIFY(!docview::isRendererCommandLine(QByteArray(65537, 'x'), executable));
|
|
}
|
|
|
|
void RendererWatchdogTests::terminatesOnlyRegisteredRenderer() {
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
|
|
QProcess renderer, sibling;
|
|
renderer.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--type=renderer", "--allocate", "64"});
|
|
sibling.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--allocate", "1"});
|
|
QVERIFY(renderer.waitForStarted()); QVERIFY(renderer.waitForReadyRead());
|
|
QVERIFY(sibling.waitForStarted()); QVERIFY(sibling.waitForReadyRead());
|
|
docview::RendererWatchdog watchdog(nullptr, 32ull * 1024 * 1024, QCoreApplication::applicationFilePath());
|
|
QString error;
|
|
QVERIFY2(watchdog.registerRenderer("test-session", renderer.processId(), &error), qPrintable(error));
|
|
QSignalSpy exceeded(&watchdog, &docview::RendererWatchdog::limitExceeded);
|
|
watchdog.checkNow();
|
|
QTRY_COMPARE(exceeded.count(), 1);
|
|
QCOMPARE(exceeded.first().first().toString(), "test-session");
|
|
QVERIFY(renderer.waitForFinished());
|
|
#ifdef Q_OS_WIN
|
|
QCOMPARE(renderer.exitCode(), 137);
|
|
#else
|
|
QCOMPARE(renderer.exitStatus(), QProcess::CrashExit);
|
|
#endif
|
|
QCOMPARE(sibling.state(), QProcess::Running);
|
|
sibling.kill(); QVERIFY(sibling.waitForFinished());
|
|
QCOMPARE(watchdog.monitoredCount(), 0);
|
|
#else
|
|
docview::RendererWatchdog watchdog; QString error;
|
|
QVERIFY(!watchdog.registerRenderer("session", 123, &error)); QCOMPARE(error, "E_SANDBOX_UNAVAILABLE");
|
|
#endif
|
|
}
|
|
|
|
void RendererWatchdogTests::forgetsExitedRenderer() {
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
|
|
QProcess renderer;
|
|
renderer.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--type=renderer", "--allocate", "1"});
|
|
QVERIFY(renderer.waitForStarted()); QVERIFY(renderer.waitForReadyRead());
|
|
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath());
|
|
QString error;
|
|
QVERIFY2(watchdog.registerRenderer("session", renderer.processId(), &error), qPrintable(error));
|
|
QSignalSpy exceeded(&watchdog, &docview::RendererWatchdog::limitExceeded);
|
|
renderer.kill(); QVERIFY(renderer.waitForFinished());
|
|
watchdog.checkNow();
|
|
QCOMPARE(watchdog.monitoredCount(), 0); QCOMPARE(exceeded.count(), 0);
|
|
#endif
|
|
}
|
|
|
|
void RendererWatchdogTests::boundedSpreadSlots() {
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
|
|
QProcess first, second;
|
|
const QStringList args{"--watchdog-child", "--type=renderer", "--allocate", "1"};
|
|
first.start(QCoreApplication::applicationFilePath(), args);
|
|
second.start(QCoreApplication::applicationFilePath(), args);
|
|
QVERIFY(first.waitForStarted()); QVERIFY(first.waitForReadyRead());
|
|
QVERIFY(second.waitForStarted()); QVERIFY(second.waitForReadyRead());
|
|
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath());
|
|
QString error;
|
|
QVERIFY(watchdog.registerRenderer("spread", first.processId(), &error, 0));
|
|
QVERIFY(watchdog.registerRenderer("spread", second.processId(), &error, 1));
|
|
QCOMPARE(watchdog.monitoredCount(), 2);
|
|
QVERIFY(!watchdog.registerRenderer("spread", first.processId(), &error, 2));
|
|
QVERIFY(!watchdog.registerRenderer("spread", 0, &error, -1));
|
|
QCOMPARE(watchdog.monitoredCount(), 2);
|
|
QVERIFY(watchdog.registerRenderer("spread", first.processId(), &error, 1));
|
|
QCOMPARE(watchdog.monitoredCount(), 2); // Shared renderer, independently owned slots.
|
|
watchdog.removeSlot("spread", 1);
|
|
QCOMPARE(watchdog.monitoredCount(), 1);
|
|
watchdog.checkNow();
|
|
QCOMPARE(first.state(), QProcess::Running); QCOMPARE(second.state(), QProcess::Running);
|
|
QVERIFY(watchdog.registerRenderer("spread", second.processId(), &error, 1));
|
|
first.kill(); QVERIFY(first.waitForFinished()); watchdog.checkNow();
|
|
QCOMPARE(watchdog.monitoredCount(), 1); // Exited primary must not unmonitor companion.
|
|
watchdog.removeSession("spread"); QCOMPARE(watchdog.monitoredCount(), 0);
|
|
second.kill(); QVERIFY(second.waitForFinished());
|
|
#endif
|
|
}
|
|
|
|
void RendererWatchdogTests::responseProbeLifetime() {
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
|
|
QProcess first, replacement;
|
|
const QStringList args{"--watchdog-child", "--type=renderer", "--allocate", "1"};
|
|
first.start(QCoreApplication::applicationFilePath(), args);
|
|
replacement.start(QCoreApplication::applicationFilePath(), args);
|
|
QVERIFY(first.waitForStarted()); QVERIFY(first.waitForReadyRead());
|
|
QVERIFY(replacement.waitForStarted()); QVERIFY(replacement.waitForReadyRead());
|
|
docview::RendererWatchdog productionExecutable;
|
|
QVERIFY(!productionExecutable.registerRenderer("impostor", first.processId()));
|
|
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath(), 1000);
|
|
QVERIFY(!watchdog.beginProbe("absent", 0));
|
|
QVERIFY(watchdog.registerRenderer("session", first.processId()));
|
|
const auto firstProbe = watchdog.beginProbe("session", 0);
|
|
QVERIFY(firstProbe); QCOMPARE(watchdog.beginProbe("session", 0), 0u);
|
|
QVERIFY(!watchdog.acknowledgeProbe("session", 1, firstProbe));
|
|
QVERIFY(watchdog.acknowledgeProbe("session", 0, firstProbe));
|
|
const auto oldDocumentProbe = watchdog.beginProbe("session", 0);
|
|
QVERIFY(oldDocumentProbe != firstProbe);
|
|
// Navigation cancellation invalidates only the exact outstanding probe.
|
|
QVERIFY(watchdog.acknowledgeProbe("session", 0, oldDocumentProbe));
|
|
const auto oldProcessProbe = watchdog.beginProbe("session", 0);
|
|
QVERIFY(!watchdog.acknowledgeProbe("session", 0, oldDocumentProbe));
|
|
QVERIFY(watchdog.registerRenderer("session", replacement.processId()));
|
|
const auto current = watchdog.beginProbe("session", 0);
|
|
QVERIFY(current && current != oldProcessProbe);
|
|
QVERIFY(!watchdog.acknowledgeProbe("session", 0, oldProcessProbe));
|
|
// Two visible views can share a renderer but retain independent deadlines.
|
|
QVERIFY(watchdog.registerRenderer("session", replacement.processId(), nullptr, 1));
|
|
const auto companion = watchdog.beginProbe("session", 1);
|
|
QVERIFY(companion && companion != current);
|
|
QVERIFY(watchdog.acknowledgeProbe("session", 0, current));
|
|
QCOMPARE(watchdog.beginProbe("session", 1), 0u);
|
|
watchdog.removeSession("session");
|
|
QVERIFY(!watchdog.acknowledgeProbe("session", 1, companion));
|
|
watchdog.checkNow();
|
|
QCOMPARE(first.state(), QProcess::Running); QCOMPARE(replacement.state(), QProcess::Running);
|
|
first.kill(); replacement.kill();
|
|
QVERIFY(first.waitForFinished()); QVERIFY(replacement.waitForFinished());
|
|
#endif
|
|
}
|
|
|
|
void RendererWatchdogTests::responseTimeoutStopsOnlyStalledSlot() {
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
|
|
QProcess primary, companion;
|
|
const QStringList args{"--watchdog-child", "--type=renderer", "--allocate", "1"};
|
|
primary.start(QCoreApplication::applicationFilePath(), args);
|
|
companion.start(QCoreApplication::applicationFilePath(), args);
|
|
QVERIFY(primary.waitForStarted()); QVERIFY(primary.waitForReadyRead());
|
|
QVERIFY(companion.waitForStarted()); QVERIFY(companion.waitForReadyRead());
|
|
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath(), 100);
|
|
QVERIFY(watchdog.registerRenderer("spread", primary.processId()));
|
|
QVERIFY(watchdog.registerRenderer("spread", companion.processId(), nullptr, 1));
|
|
const auto successful = watchdog.beginProbe("spread", 0);
|
|
QVERIFY(watchdog.acknowledgeProbe("spread", 0, successful));
|
|
QVERIFY(watchdog.beginProbe("spread", 1));
|
|
QSignalSpy timedOut(&watchdog, &docview::RendererWatchdog::responseTimedOut);
|
|
QSignalSpy memory(&watchdog, &docview::RendererWatchdog::limitExceeded);
|
|
QTest::qSleep(150); watchdog.checkNow();
|
|
QCOMPARE(timedOut.count(), 1); QCOMPARE(memory.count(), 0);
|
|
QCOMPARE(timedOut.first(), QVariantList({"spread", "E_RENDERER_TIMEOUT"}));
|
|
QVERIFY(companion.waitForFinished());
|
|
QCOMPARE(primary.state(), QProcess::Running);
|
|
QCOMPARE(watchdog.monitoredCount(), 0); // Controller disposes the whole failed session.
|
|
primary.kill(); QVERIFY(primary.waitForFinished());
|
|
#endif
|
|
}
|
|
|
|
void RendererWatchdogTests::suspensionRestartsResponseBudget() {
|
|
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
|
|
QProcess renderer;
|
|
renderer.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--type=renderer", "--allocate", "1"});
|
|
QVERIFY(renderer.waitForStarted()); QVERIFY(renderer.waitForReadyRead());
|
|
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath(), 100);
|
|
QVERIFY(watchdog.registerRenderer("session", renderer.processId()));
|
|
const auto probe = watchdog.beginProbe("session", 0);
|
|
QSignalSpy timedOut(&watchdog, &docview::RendererWatchdog::responseTimedOut);
|
|
QTest::qSleep(5100); watchdog.checkNow(); // No GUI callbacks during a host pause.
|
|
QCOMPARE(timedOut.count(), 0); QCOMPARE(renderer.state(), QProcess::Running);
|
|
QVERIFY(watchdog.acknowledgeProbe("session", 0, probe));
|
|
QVERIFY(watchdog.beginProbe("session", 0));
|
|
QTest::qSleep(150); watchdog.checkNow();
|
|
QCOMPARE(timedOut.count(), 1); QVERIFY(renderer.waitForFinished());
|
|
#endif
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
QCoreApplication app(argc, argv);
|
|
if (app.arguments().contains("--watchdog-child")) {
|
|
const auto index = app.arguments().indexOf("--allocate");
|
|
const int mib = index >= 0 && index + 1 < app.arguments().size() ? app.arguments()[index + 1].toInt() : 1;
|
|
if (mib < 1 || mib > 128) return 2;
|
|
QByteArray memory(qsizetype(mib) * 1024 * 1024, 'x');
|
|
fwrite("ready\n", 1, 6, stdout); fflush(stdout);
|
|
QTimer::singleShot(15000, &app, &QCoreApplication::quit);
|
|
const int result = app.exec();
|
|
return result + (memory.at(0) == 'x' ? 0 : 1);
|
|
}
|
|
RendererWatchdogTests tests;
|
|
return QTest::qExec(&tests, argc, argv);
|
|
}
|
|
#include "test_renderer_watchdog.moc"
|