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_
|