#include "web/renderer_watchdog.h" #include #include #include #include #include #include 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 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::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::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"