208 lines
9.0 KiB
C++
208 lines
9.0 KiB
C++
#include "common/ipc.h"
|
|
#include "common/sandbox.h"
|
|
#include "common/worker_process.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QCryptographicHash>
|
|
#include <QElapsedTimer>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QLocalSocket>
|
|
#include <QSaveFile>
|
|
#include <QSignalSpy>
|
|
#include <QTemporaryDir>
|
|
#include <QTimer>
|
|
#include <QtTest>
|
|
#include <cstdlib>
|
|
|
|
using namespace docview;
|
|
|
|
namespace {
|
|
int stalledPeer(QCoreApplication &app) {
|
|
const auto args = app.arguments();
|
|
const int socketIndex = args.indexOf("--socket");
|
|
if (socketIndex < 0 || socketIndex + 1 >= args.size())
|
|
return 120;
|
|
QLocalSocket socket;
|
|
socket.connectToServer(args[socketIndex + 1]);
|
|
if (!socket.waitForConnected(5000))
|
|
return 121;
|
|
QString error;
|
|
if (!enterSandbox({}, {}, &error))
|
|
return 122;
|
|
IpcChannel channel(&socket);
|
|
QCborMap extracting;
|
|
QTimer chunks;
|
|
chunks.setInterval(1000);
|
|
const auto reply = [&](QCborMap request, const QCborMap &result) {
|
|
request.remove(QStringLiteral("payload"));
|
|
request.insert(QStringLiteral("result"), result);
|
|
if (!channel.send(request))
|
|
std::_Exit(123);
|
|
};
|
|
QObject::connect(&chunks, &QTimer::timeout, &app, [&] {
|
|
// Progress is deliberately nonterminal: it must not reset the resource
|
|
// deadline or allow a queued request to overtake this extraction.
|
|
reply(extracting, {{"more", true}, {"data", QByteArray("progress")}});
|
|
});
|
|
QObject::connect(&socket, &QLocalSocket::disconnected, &app, &QCoreApplication::quit);
|
|
QObject::connect(&channel, &IpcChannel::protocolError, &app, [] { std::_Exit(124); });
|
|
QObject::connect(&channel, &IpcChannel::messageReceived, &app, [&](const QCborMap &request) {
|
|
const auto op = request.value("operation").toString();
|
|
if (op == "ping")
|
|
reply(request, {{"sandboxed", true}, {"pid", QCoreApplication::applicationPid()}});
|
|
else if (op == "extract") {
|
|
extracting = request;
|
|
chunks.start();
|
|
}
|
|
// Rendering deliberately never replies. Keep the real socket and
|
|
// event loop alive until the production broker terminates this peer.
|
|
});
|
|
return app.exec();
|
|
}
|
|
|
|
QString digest(const QString &path) {
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
return {};
|
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
|
if (!hash.addData(&file))
|
|
return {};
|
|
return QString::fromLatin1(hash.result().toHex());
|
|
}
|
|
} // namespace
|
|
|
|
class WorkerDeadlinesTest : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void realBudgetsKeepTheEventLoopResponsive() {
|
|
QTemporaryDir directory;
|
|
QVERIFY(directory.isValid());
|
|
const auto archivePath = directory.filePath("docview-archive-worker");
|
|
QVERIFY(QFile::link(QCoreApplication::applicationFilePath(), archivePath));
|
|
WorkerProcess pdf("deadline-pdf", 1), archive("deadline-archive", 1), cancelled("cancel-pdf", 1);
|
|
QSignalSpy pdfFailures(&pdf, &WorkerProcess::failed),
|
|
archiveFailures(&archive, &WorkerProcess::failed);
|
|
QSignalSpy cancelFailures(&cancelled, &WorkerProcess::failed);
|
|
qint64 pdfPid = 0, archivePid = 0, cancelPid = 0;
|
|
int handshakes = 0;
|
|
for (auto pair : {qMakePair(&pdf, &pdfPid), qMakePair(&archive, &archivePid),
|
|
qMakePair(&cancelled, &cancelPid)}) {
|
|
QVERIFY(pair.first->start(pair.first == &archive ? archivePath
|
|
: QCoreApplication::applicationFilePath(),
|
|
{"--stalled-peer"}));
|
|
pair.first->request("ping", {}, [&, pid = pair.second](const QCborMap &value) {
|
|
const auto result = value.value("result").toMap();
|
|
if (result.value("sandboxed").toBool()) {
|
|
*pid = result.value("pid").toInteger();
|
|
++handshakes;
|
|
}
|
|
});
|
|
}
|
|
QTRY_COMPARE_WITH_TIMEOUT(handshakes, 3, 5000);
|
|
QElapsedTimer clock;
|
|
clock.start();
|
|
qint64 pdfSlow = -1, archiveSlow = -1, pdfStopped = -1, archiveStopped = -1;
|
|
qint64 cancelDuration = -1, lastHeartbeat = 0, maxHeartbeatGap = 0;
|
|
int pdfSlowCount = 0, archiveSlowCount = 0, terminalReplies = 0, chunks = 0, heartbeats = 0;
|
|
connect(&pdf, &WorkerProcess::slow, this, [&] {
|
|
pdfSlow = clock.elapsed();
|
|
++pdfSlowCount;
|
|
});
|
|
connect(&archive, &WorkerProcess::slow, this, [&] {
|
|
archiveSlow = clock.elapsed();
|
|
++archiveSlowCount;
|
|
});
|
|
connect(&pdf, &WorkerProcess::failed, this, [&] { pdfStopped = clock.elapsed(); });
|
|
connect(&archive, &WorkerProcess::failed, this, [&] { archiveStopped = clock.elapsed(); });
|
|
QTimer heartbeat;
|
|
heartbeat.setInterval(10);
|
|
connect(&heartbeat, &QTimer::timeout, this, [&] {
|
|
const auto now = clock.elapsed();
|
|
maxHeartbeatGap = qMax(maxHeartbeatGap, now - lastHeartbeat);
|
|
lastHeartbeat = now;
|
|
++heartbeats;
|
|
});
|
|
heartbeat.start();
|
|
pdf.request("render", {}, [&](const QCborMap &) { ++terminalReplies; });
|
|
pdf.request("ping", {}, [&](const QCborMap &) { ++terminalReplies; });
|
|
archive.request("extract", {}, [&](const QCborMap &value) {
|
|
if (value.value("result").toMap().value("more").toBool())
|
|
++chunks;
|
|
else
|
|
++terminalReplies;
|
|
});
|
|
archive.request("ping", {}, [&](const QCborMap &) { ++terminalReplies; });
|
|
cancelled.request("render", {}, [&](const QCborMap &) { ++terminalReplies; });
|
|
cancelled.request("ping", {}, [&](const QCborMap &) { ++terminalReplies; });
|
|
QTimer::singleShot(200, &cancelled, [&] {
|
|
QElapsedTimer stopClock;
|
|
stopClock.start();
|
|
cancelled.stop();
|
|
cancelDuration = stopClock.elapsed();
|
|
});
|
|
QTRY_VERIFY_WITH_TIMEOUT(pdfStopped >= 0 && archiveStopped >= 0, 125000);
|
|
QTest::qWait(100);
|
|
heartbeat.stop();
|
|
QCOMPARE(pdfFailures.size(), 1);
|
|
QCOMPARE(archiveFailures.size(), 1);
|
|
QCOMPARE(cancelFailures.size(), 0);
|
|
QCOMPARE(pdfFailures[0][0].toString(), "E_WORKER_TIMEOUT");
|
|
QCOMPARE(archiveFailures[0][0].toString(), "E_WORKER_TIMEOUT");
|
|
QCOMPARE(pdfSlowCount, 1);
|
|
QCOMPARE(archiveSlowCount, 1);
|
|
QVERIFY2(pdfSlow >= 30000 && pdfSlow < 34000, qPrintable(QString::number(pdfSlow)));
|
|
QVERIFY2(archiveSlow >= 10000 && archiveSlow < 14000, qPrintable(QString::number(archiveSlow)));
|
|
QVERIFY2(pdfStopped >= 120000 && pdfStopped < 124000, qPrintable(QString::number(pdfStopped)));
|
|
QVERIFY2(archiveStopped >= 30000 && archiveStopped < 34000,
|
|
qPrintable(QString::number(archiveStopped)));
|
|
QVERIFY(cancelDuration >= 0 && cancelDuration < 1000);
|
|
QVERIFY(heartbeats > 5000);
|
|
QVERIFY2(maxHeartbeatGap < 1500, qPrintable(QString::number(maxHeartbeatGap)));
|
|
QVERIFY(chunks >= 20);
|
|
QCOMPARE(terminalReplies, 0);
|
|
QVERIFY(!pdf.isConnected() && !archive.isConnected() && !cancelled.isConnected());
|
|
for (const auto pid : {pdfPid, archivePid, cancelPid}) {
|
|
QVERIFY(pid > 0);
|
|
QVERIFY(!QFileInfo::exists(QStringLiteral("/proc/%1").arg(pid)));
|
|
}
|
|
const QJsonObject report{
|
|
{"success", true},
|
|
{"platform", "Linux; sandboxed stalled IPC fixture; production WorkerProcess timers"},
|
|
{"testExecutableSha256", digest(QCoreApplication::applicationFilePath())},
|
|
{"commonLibrarySha256", digest(QCoreApplication::applicationDirPath() + "/libdocview_common.a")},
|
|
{"pdfSlowMs", pdfSlow},
|
|
{"pdfStopMs", pdfStopped},
|
|
{"archiveSlowMs", archiveSlow},
|
|
{"archiveStopMs", archiveStopped},
|
|
{"cancelDurationMs", cancelDuration},
|
|
{"heartbeatCount", heartbeats},
|
|
{"maxHeartbeatGapMs", maxHeartbeatGap},
|
|
{"archiveProgressChunks", chunks},
|
|
{"queuedOrTerminalReplies", terminalReplies},
|
|
{"childrenReaped", true},
|
|
{"unrelatedUserDocuments", "none"}};
|
|
qInfo().noquote() << QJsonDocument(report).toJson(QJsonDocument::Compact);
|
|
const auto reportPath = qEnvironmentVariable("DOCVIEW_WATCHDOG_REPORT");
|
|
if (!reportPath.isEmpty()) {
|
|
QSaveFile output(reportPath);
|
|
QVERIFY(output.open(QIODevice::WriteOnly));
|
|
const auto data = QJsonDocument(report).toJson();
|
|
QCOMPARE(output.write(data), data.size());
|
|
QVERIFY(output.commit());
|
|
}
|
|
}
|
|
};
|
|
|
|
int main(int argc, char **argv) {
|
|
QCoreApplication app(argc, argv);
|
|
if (app.arguments().contains("--stalled-peer"))
|
|
return stalledPeer(app);
|
|
WorkerDeadlinesTest test;
|
|
return QTest::qExec(&test, argc, argv);
|
|
}
|
|
|
|
#include "test_worker_deadlines.moc"
|