// ProcessScanner.h — process-table lookups for the app. // // Two paths share the same `ps -Ao pid=,command=` snapshot: // // 1. SYNCHRONOUS (quit-time): at quit, the Shell asks which panes run a // whitelisted program. scanForeground() spawns one `ps` subprocess and // parses the full table on the calling thread. This must stay // synchronous: it runs from Component.onDestruction, where no event // loop is left to deliver an async result. // // 2. ASYNCHRONOUS (sidebar previews): requestPrograms() runs `ps` off the // GUI thread (QProcess is event-driven, so the subprocess never blocks // the UI) and answers each request with a per-request signal. A single // snapshot is shared: results are cached for a short window // (kCacheTtlMs), so concurrent requests — e.g. every sidebar card's 2s // label timer — are served from ONE subprocess instead of one per card. // Multiple requests arriving while a snapshot is in flight are all // answered when that snapshot completes. // // A pane's "program" is the foreground process group of its PTY // (tcgetpgrp), captured by TerminalQuickItem::foregroundPid; the snapshot // maps those pids to command lines. // // Entries have { "kind": "name"|"path", "value": QString }: // - kind "name": value is a regular expression matched against the running // program's argv[0] basename (e.g. "vim", "^nvim$"). Unanchored: use // ^...$ for an exact-name match. Invalid regexes never match. // - kind "path": value is compared for exact equality against the running // program's argv[0] as `ps` prints it, i.e. the exact program path the // user picked in the file browser (launching via PATH, e.g. `vim` in a // shell, produces argv[0] "vim" and does NOT match "/usr/bin/vim"). #pragma once #include #include #include #include #include #include #include #include class ProcessScanner : public QObject { Q_OBJECT QML_ELEMENT public: explicit ProcessScanner(QObject* parent = nullptr); // --- Synchronous (quit-time) path --- // Returns a map { "": { "matched": true, "argv": QStringList } } // where argv is the full token list from `ps` (argv[0] first). Empty when // the whitelist is empty, the ps snapshot failed, or nothing matched. Q_INVOKABLE static QVariantMap scanForeground(const QVariantList& pids, const QVariantList& entries); // --- Asynchronous (live refresh) path --- // Request the command lines for `pids`. Runs at most one `ps` snapshot at // a time, off the GUI thread. Each call is answered exactly once by // programsUpdated(requestId, pids, { "": { "argv": [...] } }), // where requestId is this function's return value. If a fresh-enough // snapshot is cached the answer is delivered immediately (synchronously) // from cache; otherwise the request joins the in-flight snapshot. Q_INVOKABLE int requestPrograms(const QVariantList& pids); signals: // Emitted on the GUI thread with the answer to one requestPrograms call. // `programs` contains an entry for each of `pids` the snapshot resolved. // NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name) void programsUpdated(int requestId, const QVariantList& pids, const QVariantMap& programs); private: void onFinished(); // All outstanding requests: requestId -> pid set. Entries are answered // exactly once (from a fresh snapshot, or from the cache via a deferred // single-shot), then removed. QHash m_requests; // Requests awaiting the in-flight snapshot (subset of m_requests). QHash m_pendingRequests; // Cached full-table snapshot and when it was taken. QHash m_cache; QElapsedTimer m_cacheAge; bool m_cacheValid = false; bool m_scanInFlight = false; int m_nextRequestId = 1; QProcess m_process; };