Something went wrong. Try again.
The agentic engineering control plane for the posthuman future
Something went wrong. Try again.
8.4 kB · 245 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245// ProcessScanner.cpp — see ProcessScanner.h.#include "ProcessScanner.h"
#include <QFileInfo>#include <QHash>#include <QProcess>#include <QRegularExpression>#include <QVector>
#include <algorithm>#include <cstdint>
// Scroll-hitch instrumentation: enable with XENO_SIDEBAR_DEBUG=1. Kept// permanent so the timing evidence is reproducible; silent by default.static const bool kScrollDebug = qEnvironmentVariableIsSet("XENO_SIDEBAR_DEBUG");
// A fresh-enough snapshot answers a request from cache without spawning// another `ps`. The label refresh is approximate by design (2s cadence),// so a 1s cache serves every card's staggered 2s timer from one subprocess// per ~snapshot window.static constexpr qint64 kCacheTtlMs = 1000;
namespace {
// Match one pane command line against one whitelist entry.struct Matcher { enum class Kind : std::uint8_t { Name, Path };
Kind kind = Kind::Name; QString path; QRegularExpression regex;};
// Build the entry matchers. Entries are { "kind": "name"|"path", "value" }.// An entry with a missing/unknown kind or a trimmed-empty value is skipped// (an invalid pattern in a name entry makes that entry inert, not the whole// whitelist). An empty whitelist restarts nothing.QVector<Matcher> buildMatchers(const QVariantList& entries) { QVector<Matcher> matchers; matchers.reserve(entries.size()); for (const QVariant& entryVariant : entries) { const QVariantMap entry = entryVariant.toMap(); const QString kind = entry.value(QStringLiteral("kind")).toString().trimmed(); const QString value = entry.value(QStringLiteral("value")).toString().trimmed(); if (value.isEmpty()) { continue; } Matcher matcher; if (kind == QLatin1String("path")) { matcher.kind = Matcher::Kind::Path; matcher.path = value; } else { // Default to name. Only a full-line match counts; a path value in a // name entry is impossible (regexes never match "/"). if (value.contains(QLatin1Char('/'))) { continue; } matcher.kind = Matcher::Kind::Name; matcher.regex = QRegularExpression(value); if (!matcher.regex.isValid()) { // Invalid regex: entry is inert, never matches. continue; } } matchers.append(matcher); } return matchers;}
// True when `command` (the ps command line) satisfies any entry.bool matchesAny(const QString& command, const QVector<Matcher>& matchers) { return std::ranges::any_of(matchers, [&command](const Matcher& matcher) { if (matcher.kind == Matcher::Kind::Path) { return command == matcher.path; } return matcher.regex.match(command).hasMatch(); });}
// Parse a full `ps -Ao pid=,command=` stdout dump into a pid -> command map.// One snapshot is generated per subprocess; both the sync and async paths// share this parser.QHash<int, QString> parseSnapshot(const QByteArray& out) { QHash<int, QString> commandsByPid; const QList<QByteArray> lines = out.split('\n'); for (const QByteArray& rawLine : lines) { const QByteArray line = rawLine.trimmed(); if (line.isEmpty()) { continue; } const qsizetype sep = line.indexOf(' '); if (sep <= 0) { continue; } bool ok = false; const int pid = line.left(sep).toInt(&ok); if (!ok || pid <= 0) { continue; } commandsByPid.insert(pid, QString::fromUtf8(line.mid(sep + 1).trimmed())); } return commandsByPid;}
} // namespace
ProcessScanner::ProcessScanner(QObject* parent) : QObject(parent) { connect(&m_process, &QProcess::finished, this, &ProcessScanner::onFinished);}
namespace {
// One synchronous snapshot of every process, keyed by pid. Returns an empty// map when ps fails or times out (the caller degrades to "no data" rather// than blocking).QHash<int, QString> snapshotCommands() { QHash<int, QString> commandsByPid; QElapsedTimer scanTimer; scanTimer.start(); QProcess psScan; psScan.start(QStringLiteral("ps"), {QStringLiteral("-Ao"), QStringLiteral("pid=,command=")}); if (!psScan.waitForFinished(2000)) { if (kScrollDebug) { qDebug() << "[xeno-process] ps scan TIMED OUT at" << scanTimer.elapsed() << "ms"; } return commandsByPid; } const QByteArray out = psScan.readAllStandardOutput(); commandsByPid = parseSnapshot(out); if (kScrollDebug) { qDebug() << "[xeno-process] ps scan:" << scanTimer.elapsed() << "ms," << commandsByPid.size() << "pids, from" << out.size() << "bytes"; } return commandsByPid;}
// Build the per-pid result map { "<pid>": { "argv": QVariantList } } for// the given pids from the snapshot. Pids with no resolvable command line// are skipped.QVariantMap commandsForPids(const QVariantList& pids, const QHash<int, QString>& commandsByPid) { QVariantMap result; for (const QVariant& pidVariant : pids) { const int pid = pidVariant.toInt(); if (pid <= 0) { continue; } const auto it = commandsByPid.constFind(pid); if (it == commandsByPid.cend()) { continue; } const QString& command = it.value(); QVariantMap entry; QVariantList argv; const QStringList tokens = command.split(QLatin1Char(' '), Qt::SkipEmptyParts); for (const QString& token : tokens) { argv.append(token); } entry.insert(QStringLiteral("argv"), argv); result.insert(QString::number(pid), entry); } return result;}
} // namespace
QVariantMap ProcessScanner::scanForeground(const QVariantList& pids, const QVariantList& entries) { QVariantMap result; const QVector<Matcher> matchers = buildMatchers(entries); if (matchers.isEmpty()) { return result; } const QHash<int, QString> commandsByPid = snapshotCommands(); const QVariantMap resolved = commandsForPids(pids, commandsByPid); const QList<QString> keys = resolved.keys(); for (const QString& key : keys) { const QVariantMap entry = resolved.value(key).toMap(); const QString command = entry.value(QStringLiteral("argv")) .toStringList() .join(QLatin1Char(' ')); if (!matchesAny(command, matchers)) { continue; } QVariantMap match = entry; match.insert(QStringLiteral("matched"), true); result.insert(key, match); } if (kScrollDebug) { qDebug() << "[xeno-process] scanForeground:" << pids.size() << "requested," << result.size() << "matched"; } return result;}
int ProcessScanner::requestPrograms(const QVariantList& pids) { const int requestId = m_nextRequestId++; // Fresh cache answers without another subprocess, but NEVER synchronously: // the caller must receive the requestId (its return value) before the // answer arrives, so the signal is deferred to the event loop. A direct // emit here would fire before QML can store pendingRequestId. m_requests.insert(requestId, pids); if (m_cacheValid && m_cacheAge.elapsed() < kCacheTtlMs) { QTimer::singleShot(0, this, [this, requestId]() { const auto it = m_requests.constFind(requestId); if (it == m_requests.cend()) { return; } const QVariantMap resolved = commandsForPids(it.value(), m_cache); Q_EMIT programsUpdated(requestId, it.value(), resolved); m_requests.remove(requestId); }); return requestId; } m_pendingRequests.insert(requestId, pids); if (!m_scanInFlight) { m_scanInFlight = true; m_process.start(QStringLiteral("ps"), {QStringLiteral("-Ao"), QStringLiteral("pid=,command=")}); } return requestId;}
void ProcessScanner::onFinished() { m_scanInFlight = false; const QByteArray out = m_process.readAllStandardOutput(); m_cache = parseSnapshot(out); m_cacheValid = true; m_cacheAge.restart(); if (kScrollDebug) { qDebug() << "[xeno-process] async ps snapshot:" << m_cache.size() << "pids, from" << out.size() << "bytes"; } // Answer every request that rode this snapshot. const auto requests = m_pendingRequests; m_pendingRequests.clear(); for (auto it = requests.constBegin(); it != requests.constEnd(); ++it) { const QVariantMap resolved = commandsForPids(it.value(), m_cache); Q_EMIT programsUpdated(it.key(), it.value(), resolved); m_requests.remove(it.key()); }}