QtPass Developer documentation
QtPass test coverage
Current view: top level - src/executor.h Coverage Total Hit
Test: QtPass Lines: 100.0 % 4 4
Test Date: 2026-11-14 13:19:18 Functions: - 0 0

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Executor, the FIFO queue every gpg, git and pass child process of
       6              :  * QtPass goes through, and the WSL command parsing it shares with the
       7              :  * settings code.
       8              :  */
       9              : #ifndef SRC_EXECUTOR_H_
      10              : #define SRC_EXECUTOR_H_
      11              : 
      12              : #include <QObject>
      13              : #include <QProcess>
      14              : #include <QProcessEnvironment>
      15              : #include <QQueue>
      16              : #include <atomic>
      17              : #include <optional>
      18              : #include <utility>
      19              : 
      20              : /**
      21              :  * @class Executor
      22              :  * @ingroup process
      23              :  * @brief Runs non-interactive external commands in FIFO order using a single
      24              :  * QProcess.
      25              :  *
      26              :  * Queues execution requests and emits per-request completion or error signals;
      27              :  * supports optional stdin, configurable stdout/stderr capture, and per-request
      28              :  * working directory and environment.
      29              :  */
      30              : 
      31              : class Executor : public QObject {
      32           11 :   Q_OBJECT
      33              : 
      34              :   /**
      35              :    * @struct ExecQueueItem
      36              :    * @brief Execution queue item for non-interactive ordered execution.
      37              :    */
      38              :   struct ExecQueueItem {
      39              :     /** @brief Identifier the caller gave; echoed in finished()/error(). */
      40              :     int id;
      41              :     /** @brief Executable as configured; resolved when started. */
      42              :     QString app;
      43              :     /** @brief Arguments for the executable. */
      44              :     QStringList args;
      45              :     /** @brief Data written to the process's stdin, then closed. */
      46              :     QString input;
      47              :     /** @brief Whether stdout is captured for finished(). */
      48              :     bool readStdout;
      49              :     /**
      50              :      * @brief Whether stderr is captured; it is read regardless when the
      51              :      * process exits non-zero.
      52              :      */
      53              :     bool readStderr;
      54              :     /** @brief Working directory to start in; empty for the current one. */
      55              :     QString workingDir;
      56              :   };
      57              : 
      58              :   /**
      59              :    * @brief Commands waiting to run, head first. The head stays queued while
      60              :    * it runs and is dequeued in onProcessFinished().
      61              :    */
      62              :   QQueue<ExecQueueItem> m_execQueue;
      63              :   /** @brief The one process object; every queued command reuses it. */
      64              :   QProcess m_process;
      65              :   /**
      66              :    * @brief Whether the head of the queue has been started (or is being
      67              :    * dequeued, see dequeuing()) and has not finished yet.
      68              :    */
      69              :   bool running{false};
      70              :   /**
      71              :    * @brief Start the head of the queue unless something runs or the queue
      72              :    * is empty.
      73              :    *
      74              :    * Marks the executor running and emits dequeuing() before touching the
      75              :    * item, so a slot there cannot invalidate it. An item with no executable
      76              :    * is failed with a queued error() (an empty command used to wedge the
      77              :    * queue, #1682) and the next one is tried at once. Called after each
      78              :    * execute() and from onProcessFinished().
      79              :    */
      80              :   void executeNext();
      81              :   /// Post one check for drained(), coalesced: it comes after what is
      82              :   /// being emitted now, and fires only when the executor is idle and no
      83              :   /// error() of a failed start is still on its way.
      84              :   void postDrainedIfIdle();
      85              :   /// error()s of failed starts posted but not delivered yet.
      86              :   int m_pendingErrors = 0;
      87              :   /// A drained() check is posted and not run yet.
      88              :   bool m_drainCheckPosted = false;
      89              :   /**
      90              :    * @brief Start @p process running @p app, handling the "wsl " prefix once
      91              :    * for both the queued and the blocking path.
      92              :    *
      93              :    * Also applies detachFromTerminal() and the umask from setChildUmask(),
      94              :    * so every child QtPass starts gets both.
      95              :    * @param process Process object to start; configured by the caller.
      96              :    * @param app Executable as configured (see resolveExecutable() and
      97              :    *        parseWslCommand()).
      98              :    * @param args Command arguments.
      99              :    */
     100              :   static void startProcess(QProcess &process, const QString &app,
     101              :                            const QStringList &args);
     102              :   /**
     103              :    * @brief Read what a finished process produced, according to the queue
     104              :    * item's readStdout/readStderr flags; stderr is always read on a non-zero
     105              :    * exit so the caller can report it.
     106              :    * @param item The queue item the process ran for.
     107              :    * @param exitCode Its exit code.
     108              :    * @return stdout and stderr, each empty when not read.
     109              :    */
     110              :   auto collectOutput(const ExecQueueItem &item, int exitCode)
     111              :       -> std::pair<QString, QString>;
     112              :   /**
     113              :    * @brief Shared blocking run: start @p process, optionally feed @p input on
     114              :    * stdin, wait, and capture stdout/stderr. Backs the executeBlocking
     115              :    * overloads. The caller configures @p process (e.g. environment) first.
     116              :    *
     117              :    * With a null @p cancel the wait is a plain waitForFinished(-1). With a
     118              :    * non-null one the wait polls the flag; once it is set this thread
     119              :    * terminate()s the child, gives it a short grace period, kill()s it if it
     120              :    * is still running, waits one more grace period at most, and returns -1.
     121              :    * A flag that is already set skips the start altogether.
     122              :    * @param process Pre-configured QProcess to run.
     123              :    * @param app Executable path.
     124              :    * @param args Command arguments.
     125              :    * @param input Data to write to stdin (empty = none).
     126              :    * @param process_out If non-null, receives stdout.
     127              :    * @param process_err If non-null, receives stderr.
     128              :    * @param cancel Optional flag another thread may set to end the run.
     129              :    * @param timeoutMs Time for the whole run, start included; negative for
     130              :    *        none.
     131              :    * @return Process exit code, or -1 on start/crash failure, cancel or
     132              :    *         timeout.
     133              :    */
     134              :   static auto runBlocking(QProcess &process, const QString &app,
     135              :                           const QStringList &args, const QString &input,
     136              :                           QString *process_out, QString *process_err,
     137              :                           const std::atomic_bool *cancel = nullptr,
     138              :                           int timeoutMs = -1) -> int;
     139              : 
     140              : public:
     141              :   /**
     142              :    * @brief Construct an Executor with an optional parent QObject.
     143              :    * @param parent Parent QObject, or nullptr for no parent.
     144              :    */
     145              :   explicit Executor(QObject *parent = nullptr);
     146              : 
     147              :   /**
     148              :    * @brief Queue a command without stdin input.
     149              :    * @param id Caller-assigned process identifier.
     150              :    * @param app Executable path.
     151              :    * @param args Command arguments.
     152              :    * @param readStdout Whether to capture stdout.
     153              :    * @param readStderr Whether to capture stderr.
     154              :    */
     155              :   void execute(int id, const QString &app, const QStringList &args,
     156              :                bool readStdout, bool readStderr = true);
     157              : 
     158              :   /**
     159              :    * @brief Queue a command with an explicit working directory.
     160              :    * @param id Caller-assigned process identifier.
     161              :    * @param workDir Working directory for the process.
     162              :    * @param app Executable path.
     163              :    * @param args Command arguments.
     164              :    * @param readStdout Whether to capture stdout.
     165              :    * @param readStderr Whether to capture stderr.
     166              :    */
     167              :   void execute(int id, const QString &workDir, const QString &app,
     168              :                const QStringList &args, bool readStdout,
     169              :                bool readStderr = true);
     170              : 
     171              :   /**
     172              :    * @brief Queue a command with optional stdin input.
     173              :    * @param id Caller-assigned process identifier.
     174              :    * @param app Executable path.
     175              :    * @param args Command arguments.
     176              :    * @param input Data to write to stdin.
     177              :    * @param readStdout Whether to capture stdout.
     178              :    * @param readStderr Whether to capture stderr.
     179              :    */
     180              :   void execute(int id, const QString &app, const QStringList &args,
     181              :                QString input = QString(), bool readStdout = false,
     182              :                bool readStderr = true);
     183              : 
     184              :   /**
     185              :    * @brief Queue a command with working directory and optional stdin input.
     186              :    * @param id Caller-assigned process identifier.
     187              :    * @param workDir Working directory for the process.
     188              :    * @param app Executable path.
     189              :    * @param args Command arguments.
     190              :    * @param input Data to write to stdin.
     191              :    * @param readStdout Whether to capture stdout.
     192              :    * @param readStderr Whether to capture stderr.
     193              :    */
     194              :   void execute(int id, const QString &workDir, const QString &app,
     195              :                const QStringList &args, QString input = QString(),
     196              :                bool readStdout = false, bool readStderr = true);
     197              : 
     198              :   /**
     199              :    * @brief Run a command synchronously and return its exit code.
     200              :    * @param app Executable path.
     201              :    * @param args Command arguments.
     202              :    * @param input Data to write to stdin.
     203              :    * @param process_out If non-null, receives stdout output.
     204              :    * @param process_err If non-null, receives stderr output.
     205              :    * @return Process exit code.
     206              :    */
     207              :   [[nodiscard]] static auto executeBlocking(const QString &app,
     208              :                                             const QStringList &args,
     209              :                                             const QString &input = QString(),
     210              :                                             QString *process_out = nullptr,
     211              :                                             QString *process_err = nullptr)
     212              :       -> int;
     213              : 
     214              :   /**
     215              :    * @brief Run a command synchronously on a caller-supplied QProcess, with
     216              :    * optional cancellation.
     217              :    *
     218              :    * The caller configures @p process (environment, say) before the call;
     219              :    * it must be a fresh, not yet started object. QProcess is not thread-safe,
     220              :    * so @p process belongs to the calling thread for the whole run: no other
     221              :    * thread may call terminate(), kill() or anything else on it. To let a run
     222              :    * be cancelled, pass @p cancel: the wait then polls the flag, and once it
     223              :    * is set the calling thread itself terminate()s the child, waits a short
     224              :    * grace period, kill()s it if it is still running, waits one more grace
     225              :    * period at most, and returns -1. Setting the flag is the only thing
     226              :    * another thread does; it may do so at any time, before or during the run.
     227              :    * A run given @p timeoutMs ends the same way once that time has passed,
     228              :    * start included, so it returns within the timeout plus twice the grace
     229              :    * even when a kill()ed child cannot exit yet (uninterruptible sleep on a
     230              :    * hung mount). Not timed: QProcess::start() itself, which can block, for
     231              :    * example opening a FIFO named by a redirected channel (no caller
     232              :    * redirects one), and QProcess's destructor, which still waits for such a
     233              :    * child, for up to 30 s.
     234              :    * @param process Process object to run the command on.
     235              :    * @param app Executable path.
     236              :    * @param args Command arguments.
     237              :    * @param input Data to write to stdin.
     238              :    * @param process_out If non-null, receives stdout output.
     239              :    * @param process_err If non-null, receives stderr output.
     240              :    * @param cancel If non-null, a flag that ends the run when set.
     241              :    * @param timeoutMs Time for the whole run, start included; negative (the
     242              :    *        default) for none.
     243              :    * @return Process exit code, or -1 if it failed to start, crashed, was
     244              :    * cancelled or ran out of time.
     245              :    */
     246              :   [[nodiscard]] static auto executeBlocking(
     247              :       QProcess &process, const QString &app, const QStringList &args,
     248              :       const QString &input = QString(), QString *process_out = nullptr,
     249              :       QString *process_err = nullptr, const std::atomic_bool *cancel = nullptr,
     250              :       int timeoutMs = -1) -> int;
     251              : 
     252              :   /**
     253              :    * @brief Run a command synchronously capturing stdout and stderr.
     254              :    * @param app Executable path.
     255              :    * @param args Command arguments.
     256              :    * @param process_out Receives stdout output.
     257              :    * @param process_err If non-null, receives stderr output.
     258              :    * @return Process exit code.
     259              :    */
     260              :   [[nodiscard]] static auto
     261              :   executeBlocking(const QString &app, const QStringList &args,
     262              :                   QString *process_out, QString *process_err = nullptr) -> int;
     263              : 
     264              :   /**
     265              :    * @brief Run a command synchronously with a custom environment.
     266              :    * @param env Process environment for the child process.
     267              :    * @param app Executable path.
     268              :    * @param args Command arguments.
     269              :    * @param process_out If non-null, receives stdout output.
     270              :    * @param process_err If non-null, receives stderr output.
     271              :    * @return Process exit code.
     272              :    */
     273              :   [[nodiscard]] static auto
     274              :   executeBlocking(const QProcessEnvironment &env, const QString &app,
     275              :                   const QStringList &args, QString *process_out = nullptr,
     276              :                   QString *process_err = nullptr) -> int;
     277              : 
     278              :   /**
     279              :    * @brief The one rule for turning a configured executable into what runs.
     280              :    *
     281              :    * Absolute paths and "wsl ..." commands are used as given. A bare or
     282              :    * relative name is looked up next to the application first (the Windows
     283              :    * and macOS bundles ship gpg/git/pass there) and otherwise left for
     284              :    * QProcess's PATH lookup. Both the queued and the blocking path go through
     285              :    * this, so the same setting can no longer run two different binaries.
     286              :    * @param app Executable as configured.
     287              :    * @return What to hand to QProcess::start().
     288              :    */
     289              :   static auto resolveExecutable(const QString &app) -> QString;
     290              : 
     291              :   /**
     292              :    * @brief Make @p process start its child without a controlling terminal.
     293              :    *
     294              :    * On Unix the child starts in a new session (setsid), so it has no
     295              :    * /dev/tty. git and ssh then use an askpass helper when there is one and
     296              :    * fail at once otherwise, instead of prompting in the terminal QtPass was
     297              :    * started from while the queue waits behind them. Other Unix process
     298              :    * flags set on @p process are kept. Does nothing on Windows. Every child
     299              :    * QtPass starts goes through this; startProcess() calls it for the queued
     300              :    * and the blocking path.
     301              :    * @param process Process to configure; call it before start().
     302              :    */
     303              :   static void detachFromTerminal(QProcess &process);
     304              : 
     305              :   /**
     306              :    * @brief The umask every child QtPass starts gets, as pass sets its own.
     307              :    *
     308              :    * pass runs git and gpg under umask 077 (or PASSWORD_STORE_UMASK), so the
     309              :    * folders and files they create in the store, .git included, are the
     310              :    * user's alone. QtPass gives that umask to the processes it starts
     311              :    * (startProcess(), the queue and the blocking calls) and not to itself:
     312              :    * a browser or file manager it opens keeps the session's. Unix only.
     313              :    * @param mask The umask, or a negative value for none (the default).
     314              :    */
     315              :   static void setChildUmask(int mask);
     316              : 
     317              :   /**
     318              :    * @brief The umask setChildUmask() set.
     319              :    * @return The umask, or a negative value when none was set.
     320              :    */
     321              :   [[nodiscard]] static auto childUmask() -> int;
     322              : 
     323              :   /**
     324              :    * @brief A configured executable that runs a Linux program through WSL.
     325              :    *
     326              :    * The one definition of "a WSL command" in QtPass; see parseWslCommand().
     327              :    */
     328              :   struct WslCommand {
     329              :     /**
     330              :      * @brief What to start.
     331              :      *
     332              :      * On Windows a bare `wsl`/`wsl.exe` in any case becomes `wsl`, which the
     333              :      * PATH lookup resolves; elsewhere (QtPass itself running inside WSL,
     334              :      * where `wsl.exe` is reachable through interop but `wsl` need not be)
     335              :      * and for a launcher given as a path it stays as configured.
     336              :      */
     337              :     QString launcher;
     338              :     /**
     339              :      * @brief wsl.exe options that go before the program (`-d Debian`,
     340              :      * `-u me`), without any `-e`/`--exec`: argv() always adds that.
     341              :      */
     342              :     QStringList options;
     343              :     /** @brief The Linux program to run, looked up on the WSL PATH. */
     344              :     QString command;
     345              :     /**
     346              :      * @brief Arguments for @ref launcher that run @ref command with @p args.
     347              :      *
     348              :      * Always through `--exec`, so WSL starts the program itself instead of
     349              :      * handing the joined command line to the distribution's default shell.
     350              :      * Without it every argument (entry paths, .gpg-id recipients, commit
     351              :      * messages) is word-split and `$()`-expanded by that shell, and a
     352              :      * hostile password store gets code execution.
     353              :      * @param args Arguments for @ref command, passed through verbatim.
     354              :      * @return @ref options, `--exec`, @ref command, then @p args.
     355              :      */
     356              :     auto argv(const QStringList &args) const -> QStringList;
     357              :     /**
     358              :      * @brief The same launcher and options running another program.
     359              :      * @param program Linux program to run instead of @ref command.
     360              :      * @return A copy with @ref command replaced by @p program.
     361              :      */
     362              :     auto with(const QString &program) const -> WslCommand;
     363              :   };
     364              : 
     365              :   /**
     366              :    * @brief Parse a configured executable as a WSL command.
     367              :    *
     368              :    * Accepts `wsl` or `wsl.exe` in any case, bare or as a path, followed by
     369              :    * wsl.exe options and exactly one program: `wsl gpg`, `WSL.EXE -d Debian
     370              :    * /usr/bin/gpg`, `C:\Windows\System32\wsl.exe -u me gpg`. A `-e`/`--exec`
     371              :    * the user wrote is dropped (argv() adds it), anything after the program
     372              :    * (`wsl sh -c "gpg"`) or a `wsl` with no program is not a WSL command.
     373              :    *
     374              :    * Every place that starts, probes or translates paths for a configured
     375              :    * executable goes through this, so a form one of them accepts cannot be
     376              :    * started differently by another.
     377              :    * @param app Executable as configured.
     378              :    * @return The parsed command, or nothing when @p app is not one.
     379              :    */
     380              :   static auto parseWslCommand(const QString &app) -> std::optional<WslCommand>;
     381              : 
     382              :   /**
     383              :    * @brief Build the wsl.exe argument list that runs @p command directly.
     384              :    * @param command Linux command to run (looked up on the WSL PATH).
     385              :    * @param args Arguments for @p command, passed through verbatim.
     386              :    * @return Arguments for `wsl`: `--exec`, @p command, then @p args.
     387              :    * @see WslCommand::argv
     388              :    */
     389              :   static auto wslExecArgs(const QString &command, const QStringList &args)
     390              :       -> QStringList;
     391              : 
     392              :   /**
     393              :    * @brief Translate a native path for a WSL-routed executable.
     394              :    *
     395              :    * When @p exe is a WSL command (parseWslCommand()) the path is converted
     396              :    * with `wslpath` in the same distribution (the Linux binary cannot open
     397              :    * `C:\...`); otherwise it is only cleaned.
     398              :    * @param path Native filesystem path.
     399              :    * @param exe Executable the path is meant for.
     400              :    * @return Path suitable for @p exe.
     401              :    */
     402              :   static auto translatePathForWsl(const QString &path, const QString &exe)
     403              :       -> QString;
     404              : 
     405              :   /**
     406              :    * @brief Set the environment passed to all child processes.
     407              :    * @param env Process environment for child processes.
     408              :    */
     409              :   void setEnvironment(const QProcessEnvironment &env);
     410              :   /**
     411              :    * @brief Return the environment passed to child processes.
     412              :    * @return Process environment.
     413              :    */
     414              :   auto environment() const -> QProcessEnvironment;
     415              : 
     416              :   /**
     417              :    * @brief Cancel the next queued command without executing it.
     418              :    * @return The id of the cancelled command, or -1 if the queue was empty.
     419              :    */
     420              :   auto cancelNext() -> int;
     421              :   /**
     422              :    * @brief Whether no command is running and nothing is queued.
     423              :    * @return true when the executor has nothing in flight.
     424              :    */
     425          595 :   auto isIdle() const -> bool { return !running && m_execQueue.isEmpty(); }
     426              : 
     427              :   /**
     428              :    * @brief Whether a command with process id @p id is queued or running.
     429              :    *
     430              :    * The running command stays at the head of the queue until it finishes, so
     431              :    * one scan covers both the queued and the running one. Used to refuse a
     432              :    * store change while a Git pull is still in flight, whose conflict a
     433              :    * call-time check could not yet see.
     434              :    * @param id The process id to look for (an Enums::PROCESS value).
     435              :    * @return true when the queue holds a command with that id.
     436              :    */
     437              :   auto hasQueued(int id) const -> bool {
     438           16 :     for (const ExecQueueItem &item : m_execQueue) {
     439            2 :       if (item.id == id) {
     440              :         return true;
     441              :       }
     442              :     }
     443              :     return false;
     444              :   }
     445              : 
     446              : private slots:
     447              :   /**
     448              :    * @brief Finish the running item when its process exits.
     449              :    *
     450              :    * Dequeues the head, collects its output and emits finished() for a
     451              :    * normal exit or error() with exit code -1 for a crash (Qt leaves the
     452              :    * code undefined then, and 0 or 1 would be mistaken for success or
     453              :    * "no matches"), then starts the next item and posts a drained() check.
     454              :    * @param exitCode Exit code reported by QProcess.
     455              :    * @param exitStatus Whether the process exited normally or crashed.
     456              :    */
     457              :   void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
     458              : signals:
     459              :   /**
     460              :    * @brief Emitted once the last queued item is done and nothing runs or
     461              :    * waits any more (isIdle()), after that item's finished() or error().
     462              :    */
     463              :   void drained();
     464              :   /**
     465              :    * @brief Emitted when a queued process exits normally, whatever its exit
     466              :    * code; a non-zero code is the receiver's to interpret (Pass does).
     467              :    * @param id Caller-assigned id the item was queued with.
     468              :    * @param exitCode Exit code of the process.
     469              :    * @param output stdout, when the item asked for it; empty otherwise.
     470              :    * @param errout stderr, when the item asked for it or the exit code is
     471              :    *        non-zero.
     472              :    */
     473              :   void finished(int id, int exitCode, const QString &output,
     474              :                 const QString &errout);
     475              :   /**
     476              :    * @brief Emitted just before a queued process is started; the main
     477              :    * window shows the busy indicator on it.
     478              :    */
     479              :   void starting();
     480              :   /**
     481              :    * @brief Emitted, synchronously, when the queue's head leaves it: just
     482              :    * before it is started, when it cannot be (no executable, a failed
     483              :    * start), or when cancelNext() drops it. Lets a caller do per-item work
     484              :    * at the moment the item actually runs, not when it was queued.
     485              :    * @param id The item's id.
     486              :    * @param completes Whether finished() or error() follows for it: for
     487              :    *        every item but one cancelNext() dropped. Those that complete
     488              :    *        do so in the order they left the queue.
     489              :    *
     490              :    * A slot may queue more with execute(): that waits behind this item. The
     491              :    * executor counts as running while the signal is out, also for an item
     492              :    * cancelNext() drops, so cancelNext() drops nothing then (-1) and
     493              :    * nothing starts until the signal has returned.
     494              :    */
     495              :   void dequeuing(int id, bool completes);
     496              :   /**
     497              :    * @brief Emitted instead of finished() when a queued process could not be
     498              :    * started, had no executable configured, or crashed.
     499              :    * @param id Caller-assigned id the item was queued with.
     500              :    * @param exitCode Always -1; the real code is unknown in these cases.
     501              :    * @param output stdout read so far, when the item asked for it.
     502              :    * @param errout stderr, or a message saying what went wrong when the
     503              :    *        process left none.
     504              :    */
     505              :   void error(int id, int exitCode, const QString &output,
     506              :              const QString &errout);
     507              : };
     508              : 
     509              : #endif // SRC_EXECUTOR_H_
        

Generated by: LCOV version 2.0-1