QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
executor.cpp
1// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
3#include "executor.h"
4#include <QCoreApplication>
5#include <QDeadlineTimer>
6#include <QDir>
7#include <QFileInfo>
8#include <QStringDecoder>
9#include <algorithm>
10#include <climits>
11#include <utility>
12
13#include "qtpasslogging.h"
14
15#ifdef Q_OS_UNIX
16#include <sys/stat.h>
17#endif
18
19namespace {
21std::atomic_int g_childUmask{-1};
23constexpr int kBlockingCancelPollMs = 100;
26constexpr int kBlockingKillGraceMs = 1000;
27} // namespace
28
29Executor::Executor(QObject *parent) : QObject(parent) {
30 connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
31 connect(&m_process, &QProcess::started, this, &Executor::starting);
32}
33
34void Executor::detachFromTerminal(QProcess &process) {
35#if defined(Q_OS_UNIX) && QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
36 // A child inherits QtPass's controlling terminal when QtPass was started
37 // from one. git (HTTPS credentials) and ssh (key passphrase, host key) then
38 // prompt on that /dev/tty, and as the queue runs one command at a time,
39 // every decrypt and save waits behind a prompt nobody sees: QtPass looks
40 // frozen. In a session of its own the child has no terminal, so git and
41 // ssh use SSH_ASKPASS/GIT_ASKPASS when set and otherwise fail with an
42 // error QtPass shows. GIT_TERMINAL_PROMPT=0 is not set on top: without a
43 // tty git has nothing to prompt on, and some credential helpers read it as
44 // "no interaction at all" and would skip their own dialogs.
45 //
46 // gpg is unaffected: pinentry is started by gpg-agent, which finds the
47 // terminal through GPG_TTY (set by the user's shell) or the tty on gpg's
48 // stdin, here always a pipe. pass is a shell script running that gpg and
49 // git, so the same holds for it.
50 //
51 // Signal dispositions are left alone (no ResetSignalHandlers): exec()
52 // already resets caught signals, and QtPass sets no SIG_IGN of its own, so
53 // that flag would only undo what QtPass's own parent chose (nohup, say).
54 // Terminal signals (Ctrl+C, the SIGHUP of a closed terminal) now reach
55 // QtPass but not the child; the children are short-lived and, with no
56 // terminal, cannot sit waiting for input.
57 QProcess::UnixProcessParameters params = process.unixProcessParameters();
58 params.flags |= QProcess::UnixProcessFlag::CreateNewSession;
59 process.setUnixProcessParameters(params);
60#else
61 Q_UNUSED(process);
62#endif
63}
64
65void Executor::setChildUmask(int mask) { g_childUmask.store(mask); }
66
67auto Executor::childUmask() -> int { return g_childUmask.load(); }
68
69void Executor::startProcess(QProcess &process, const QString &app,
70 const QStringList &args) {
71 detachFromTerminal(process);
72#ifdef Q_OS_UNIX
73 // In the child, between fork and exec: umask() is async-signal-safe.
74 if (const int mask = g_childUmask.load(); mask >= 0) {
75 process.setChildProcessModifier(
76 [mask] { ::umask(static_cast<mode_t>(mask)); });
77 }
78#endif
79 if (const auto wsl = parseWslCommand(app)) {
80 process.start(wsl->launcher, wsl->argv(args));
81 } else {
82 process.start(resolveExecutable(app), args);
83 }
84}
85
86auto Executor::resolveExecutable(const QString &app) -> QString {
87 if (app.isEmpty() || QDir::isAbsolutePath(app)) {
88 return app;
89 }
90 const QDir appDir(QCoreApplication::applicationDirPath());
91 QStringList candidates{QDir::cleanPath(appDir.absoluteFilePath(app))};
92#ifdef Q_OS_WIN
93 candidates << QDir::cleanPath(
94 appDir.absoluteFilePath(app + QStringLiteral(".exe")));
95#endif
96 for (const QString &candidate : std::as_const(candidates)) {
97 // A stray non-executable file of that name must not mask PATH.
98 const QFileInfo info(candidate);
99 if (info.isFile() && info.isExecutable()) {
100 return candidate;
101 }
102 }
103 return app;
104}
105
106auto Executor::WslCommand::argv(const QStringList &args) const -> QStringList {
107 return options + QStringList{QStringLiteral("--exec"), command} + args;
108}
109
110auto Executor::WslCommand::with(const QString &program) const -> WslCommand {
111 return {launcher, options, program};
112}
113
114auto Executor::parseWslCommand(const QString &app)
115 -> std::optional<WslCommand> {
116 const QStringList parts = QProcess::splitCommand(app);
117 if (parts.size() < 2) {
118 return std::nullopt;
119 }
120 // The file name after the last separator of either kind: a Windows path
121 // has to be recognised as such wherever the parser runs.
122 const QString &launcher = parts.first();
123 const qsizetype separator = std::max(launcher.lastIndexOf(QLatin1Char('/')),
124 launcher.lastIndexOf(QLatin1Char('\\')));
125 const QString launcherName = launcher.mid(separator + 1);
126 const bool isWsl =
127 launcherName.compare(QLatin1String("wsl"), Qt::CaseInsensitive) == 0 ||
128 launcherName.compare(QLatin1String("wsl.exe"), Qt::CaseInsensitive) == 0;
129 if (!isWsl) {
130 return std::nullopt;
131 }
132 WslCommand wsl;
133#ifdef Q_OS_WIN
134 // A bare wsl or wsl.exe in any case is looked up on PATH as `wsl`; a path
135 // stays as is.
136 wsl.launcher = separator < 0 ? QStringLiteral("wsl") : launcher;
137#else
138 // QtPass running inside WSL reaches wsl.exe through interop under exactly
139 // that name; nothing says `wsl` resolves too, so start what was written.
140 wsl.launcher = launcher;
141#endif
142 // wsl.exe options that take a value; anything else starting with `-` is a
143 // flag. A user-written -e/--exec is dropped, argv() always adds one.
144 static const QStringList valued{
145 QStringLiteral("-d"), QStringLiteral("--distribution"),
146 QStringLiteral("-u"), QStringLiteral("--user"),
147 QStringLiteral("--cd"), QStringLiteral("--shell-type")};
148 qsizetype i = 1;
149 for (; i < parts.size() && parts.at(i).startsWith(QLatin1Char('-')); ++i) {
150 const QString &option = parts.at(i);
151 if (option == QLatin1String("-e") || option == QLatin1String("--exec")) {
152 continue;
153 }
154 if (valued.contains(option)) {
155 if (i + 1 >= parts.size()) {
156 return std::nullopt;
157 }
158 wsl.options << option << parts.at(++i);
159 } else {
160 wsl.options << option;
161 }
162 }
163 // Exactly one program. More is a shell command line (`sh -c "..."`),
164 // which is what --exec exists to keep out.
165 if (i != parts.size() - 1) {
166 return std::nullopt;
167 }
168 wsl.command = parts.at(i);
169 return wsl;
170}
171
172auto Executor::wslExecArgs(const QString &command, const QStringList &args)
173 -> QStringList {
174 return WslCommand{QStringLiteral("wsl"), {}, command}.argv(args);
175}
176
177auto Executor::translatePathForWsl(const QString &path, const QString &exe)
178 -> QString {
179 QString normalizedPath = QDir::cleanPath(path);
180 const auto wsl = parseWslCommand(exe);
181 if (!wsl) {
182 return normalizedPath;
183 }
184 QString wslPath;
185 const auto wslpath = wsl->with(QStringLiteral("wslpath"));
186 const int rc = executeBlocking(wslpath.launcher,
187 wslpath.argv({normalizedPath}), &wslPath);
188 const QString translated = wslPath.trimmed();
189 return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
190}
191
193 if (running || m_execQueue.isEmpty()) {
194 return;
195 }
196 // A copy, and marked running, before anyone hears of it: a slot on
197 // dequeuing() that queues more cannot invalidate it or start it a second
198 // time, and one that calls cancelNext() cannot drop it.
199 const ExecQueueItem i = m_execQueue.head();
200 running = true;
201 emit dequeuing(i.id, true);
202
203 // An empty executable never emits finished(); dropping it wedged the queue
204 // (#1682). Fail it like a failed-to-start process so the queue drains.
205 if (i.app.isEmpty()) {
206 qCDebug(lcQtPass) << "No executable set for:" << i.id;
207 const int failedId = i.id;
208 running = false;
209 m_execQueue.dequeue();
211 QMetaObject::invokeMethod(
212 this,
213 [this, failedId]() {
214 emit error(failedId, -1, QString(),
215 tr("No executable configured for this command"));
218 },
219 Qt::QueuedConnection);
220 executeNext();
221 return;
222 }
223
224 // Always set it: an item without a directory runs in the application's
225 // cwd, not wherever the previous item happened to run.
226 m_process.setWorkingDirectory(i.workingDir);
228
229 // A failed start never emits finished(), so `running` would stall the queue
230 // and waiters (the keygen dialog) would hang. The emit is queued so it does
231 // not re-enter KeygenDialog::done(), still on the stack; -1 goes through
232 // Pass::finished's non-zero error gate.
233 if (!m_process.waitForStarted(-1)) {
234 qCDebug(lcQtPass) << "Process failed to start:" << i.id << " " << i.app;
235 const int failedId = i.id;
236 const QString failedApp = i.app;
237 m_process.closeWriteChannel();
238 running = false;
239 m_execQueue.dequeue();
241 QMetaObject::invokeMethod(
242 this,
243 [this, failedId, failedApp]() {
244 emit error(failedId, -1, QString(),
245 tr("Failed to start %1").arg(failedApp));
248 },
249 Qt::QueuedConnection);
250 executeNext();
251 return;
252 }
253
254 if (!i.input.isEmpty()) {
255 QByteArray data = i.input.toUtf8();
256 if (m_process.write(data) != data.length()) {
257 qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
258 << i.app;
259 }
260 }
261 m_process.closeWriteChannel();
262}
263
264void Executor::execute(int id, const QString &app, const QStringList &args,
265 bool readStdout, bool readStderr) {
266 execute(id, QString(), app, args, QString(), readStdout, readStderr);
267}
268
269void Executor::execute(int id, const QString &workDir, const QString &app,
270 const QStringList &args, bool readStdout,
271 bool readStderr) {
272 execute(id, workDir, app, args, QString(), readStdout, readStderr);
273}
274
275void Executor::execute(int id, const QString &app, const QStringList &args,
276 QString input, bool readStdout, bool readStderr) {
277 execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
278}
279
280void Executor::execute(int id, const QString &workDir, const QString &app,
281 const QStringList &args, QString input, bool readStdout,
282 bool readStderr) {
283 // Queue even an empty executable: executeNext() reports it (#1682).
284 // startProcess resolves the executable, as on the blocking path.
285 m_execQueue.push_back(
286 {id, app, args, std::move(input), readStdout, readStderr, workDir});
287 executeNext();
288}
289
290// UTF-8 first; on a decoding error fall back to the system encoding.
291static auto decodeAssumingUtf8(const QByteArray &in) -> QString {
292 // Stateless: a truncated trailing byte becomes a replacement character
293 // instead of being held back for a continuation that never comes.
294 auto converter =
295 QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
296 QString out = converter(in);
297 if (!converter.hasError()) {
298 return out;
299 }
300 auto fallback =
301 QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
302 return fallback(in);
303}
304
305// Returns an error code rather than throwing, matching QtPass's error
306// handling elsewhere.
307namespace {
308
315void stopBlocking(QProcess &process) {
316 process.terminate();
317 if (process.waitForFinished(kBlockingKillGraceMs)) {
318 return;
319 }
320 process.kill();
321 // SIGKILL cannot be ignored, but a child in uninterruptible sleep (a hung
322 // NFS or FUSE mount) dies only when the kernel lets it. Wait one more grace
323 // period, not forever: the run is reported as failed either way, and a
324 // blocking run then lasts at most its timeout plus twice the grace.
325 if (!process.waitForFinished(kBlockingKillGraceMs)) {
326 qCWarning(lcQtPass) << "Process still running after kill():"
327 << process.program();
328 }
329}
330
335auto msecsLeft(const QDeadlineTimer &deadline) -> int {
336 if (deadline.isForever()) {
337 return -1;
338 }
339 return static_cast<int>(
340 std::clamp<qint64>(deadline.remainingTime(), 0, INT_MAX));
341}
342
352auto waitOrCancel(QProcess &process, const std::atomic_bool *cancel,
353 const QDeadlineTimer &deadline) -> bool {
354 if (cancel == nullptr) {
355 // waitForFinished() can also fail (a poll error) with the child still
356 // running; that is not completion either. Stop it rather than report
357 // it done.
358 if (!process.waitForFinished(msecsLeft(deadline)) &&
359 process.state() != QProcess::NotRunning) {
360 stopBlocking(process);
361 return false;
362 }
363 return true;
364 }
365 while (!process.waitForFinished(
366 deadline.isForever()
367 ? kBlockingCancelPollMs
368 : qMin(kBlockingCancelPollMs, msecsLeft(deadline)))) {
369 if (process.state() == QProcess::NotRunning) {
370 return true;
371 }
372 if (cancel->load() || deadline.hasExpired()) {
373 stopBlocking(process);
374 return false;
375 }
376 }
377 return true;
378}
379
380} // namespace
381
382auto Executor::runBlocking(QProcess &process, const QString &app,
383 const QStringList &args, const QString &input,
384 QString *process_out, QString *process_err,
385 const std::atomic_bool *cancel, int timeoutMs)
386 -> int {
387 if (cancel != nullptr && cancel->load())
388 return -1;
389 const QDeadlineTimer deadline = timeoutMs < 0
390 ? QDeadlineTimer(QDeadlineTimer::Forever)
391 : QDeadlineTimer(timeoutMs);
392 startProcess(process, app, args);
393 if (!process.waitForStarted(msecsLeft(deadline))) {
394 qCDebug(lcQtPass) << "Process failed to start:" << app;
395 if (process.state() != QProcess::NotRunning) {
396 stopBlocking(process);
397 }
398 return -1;
399 }
400 if (!input.isEmpty()) {
401 QByteArray data = input.toUtf8();
402 if (process.write(data) != data.length()) {
403 qCDebug(lcQtPass) << "Not all input written:" << app;
404 }
405 }
406 // Always close stdin so a child blocking on EOF doesn't hang.
407 process.closeWriteChannel();
408 if (!waitOrCancel(process, cancel, deadline)) {
409 return -1;
410 }
411 // Read before judging the exit: a process that crashed may have said why,
412 // as the queued path (onProcessFinished) keeps it too.
413 if (process_out != nullptr) {
414 *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
415 }
416 if (process_err != nullptr) {
417 *process_err = decodeAssumingUtf8(process.readAllStandardError());
418 }
419 return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
420}
421
422auto Executor::executeBlocking(const QString &app, const QStringList &args,
423 const QString &input, QString *process_out,
424 QString *process_err) -> int {
425 QProcess internal;
426 return runBlocking(internal, app, args, input, process_out, process_err);
427}
428
429auto Executor::executeBlocking(QProcess &process, const QString &app,
430 const QStringList &args, const QString &input,
431 QString *process_out, QString *process_err,
432 const std::atomic_bool *cancel, int timeoutMs)
433 -> int {
434 return runBlocking(process, app, args, input, process_out, process_err,
435 cancel, timeoutMs);
436}
437
438auto Executor::executeBlocking(const QString &app, const QStringList &args,
439 QString *process_out, QString *process_err)
440 -> int {
441 return executeBlocking(app, args, QString(), process_out, process_err);
442}
443
444auto Executor::executeBlocking(const QProcessEnvironment &env,
445 const QString &app, const QStringList &args,
446 QString *process_out, QString *process_err)
447 -> int {
448 QProcess process;
449 process.setProcessEnvironment(env);
450 return runBlocking(process, app, args, QString(), process_out, process_err);
451}
452
453void Executor::setEnvironment(const QProcessEnvironment &env) {
454 m_process.setProcessEnvironment(env);
455}
456
457auto Executor::environment() const -> QProcessEnvironment {
458 return m_process.processEnvironment();
459}
460
461// Also -1 while the head is already running: that one is not cancelled.
462auto Executor::cancelNext() -> int {
463 if (running || m_execQueue.isEmpty()) {
464 return -1;
465 }
466 const int id = m_execQueue.dequeue().id;
467 // As in executeNext(): while the signal is out, a slot's cancelNext()
468 // drops nothing and its execute() only queues. Nothing is started here
469 // afterwards: the caller (ImitatePass dropping the rest of a failed
470 // transaction from its completion handler) may cancel again, and the
471 // queue moves on when that handler returns (onProcessFinished()).
472 running = true;
473 emit dequeuing(id, false);
474 running = false;
475 return id;
476}
477
479 QProcess::ExitStatus exitStatus) {
480 ExecQueueItem i = m_execQueue.dequeue();
481 running = false;
482 auto [output, err] = collectOutput(i, exitCode);
483 if (exitStatus == QProcess::NormalExit) {
484 if (exitCode != 0) {
485 qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
486 }
487 emit finished(i.id, exitCode, output, err);
488 } else {
489 // A signal-killed process usually leaves stderr empty; without this the
490 // password pane would show nothing at all for the failure.
491 if (err.trimmed().isEmpty()) {
492 err = tr("%1 crashed or was killed").arg(i.app);
493 }
494 // Qt leaves exitCode undefined after a CrashExit (the signal number on
495 // Unix), and Pass::finished gates on it: 0 would pass as success and 1
496 // as grep's "no matches". Report -1, like the failed-to-start path.
497 emit error(i.id, -1, output, err);
498 }
499 executeNext();
501}
502
504 // One check at a time, run after what is emitted now; it fires only once
505 // nothing runs or waits and every posted error() of a failed start is
506 // out (the last of those posts the check again).
507 if (m_drainCheckPosted) {
508 return;
509 }
510 m_drainCheckPosted = true;
511 QMetaObject::invokeMethod(
512 this,
513 [this]() {
514 m_drainCheckPosted = false;
515 if (isIdle() && m_pendingErrors == 0) {
516 emit drained();
517 }
518 },
519 Qt::QueuedConnection);
520}
521
522auto Executor::collectOutput(const ExecQueueItem &item, int exitCode)
523 -> std::pair<QString, QString> {
524 QString output;
525 QString err;
526 if (item.readStdout) {
527 output = decodeAssumingUtf8(m_process.readAllStandardOutput());
528 }
529 if (item.readStderr || exitCode != 0) {
530 err = decodeAssumingUtf8(m_process.readAllStandardError());
531 }
532 return {output, err};
533}
void postDrainedIfIdle()
Definition executor.cpp:503
void dequeuing(int id, bool completes)
Emitted, synchronously, when the queue's head leaves it: just before it is started,...
QQueue< ExecQueueItem > m_execQueue
Commands waiting to run, head first. The head stays queued while it runs and is dequeued in onProcess...
Definition executor.h:62
auto isIdle() const -> bool
Whether no command is running and nothing is queued.
Definition executor.h:425
static auto childUmask() -> int
The umask setChildUmask() set.
Definition executor.cpp:67
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
Definition executor.cpp:422
static auto translatePathForWsl(const QString &path, const QString &exe) -> QString
Translate a native path for a WSL-routed executable.
Definition executor.cpp:177
auto environment() const -> QProcessEnvironment
Return the environment passed to child processes.
Definition executor.cpp:457
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
Finish the running item when its process exits.
Definition executor.cpp:478
int m_pendingErrors
error()s of failed starts posted but not delivered yet.
Definition executor.h:86
auto collectOutput(const ExecQueueItem &item, int exitCode) -> std::pair< QString, QString >
Read what a finished process produced, according to the queue item's readStdout/readStderr flags; std...
Definition executor.cpp:522
bool m_drainCheckPosted
A drained() check is posted and not run yet.
Definition executor.h:88
static void startProcess(QProcess &process, const QString &app, const QStringList &args)
Start process running app, handling the "wsl " prefix once for both the queued and the blocking path.
Definition executor.cpp:69
void execute(int id, const QString &app, const QStringList &args, bool readStdout, bool readStderr=true)
Queue a command without stdin input.
Definition executor.cpp:264
static auto resolveExecutable(const QString &app) -> QString
The one rule for turning a configured executable into what runs.
Definition executor.cpp:86
void drained()
Emitted once the last queued item is done and nothing runs or waits any more (isIdle()),...
void error(int id, int exitCode, const QString &output, const QString &errout)
Emitted instead of finished() when a queued process could not be started, had no executable configure...
static auto wslExecArgs(const QString &command, const QStringList &args) -> QStringList
Build the wsl.exe argument list that runs command directly.
Definition executor.cpp:172
static void setChildUmask(int mask)
The umask every child QtPass starts gets, as pass sets its own.
Definition executor.cpp:65
Executor(QObject *parent=nullptr)
Construct an Executor with an optional parent QObject.
Definition executor.cpp:29
bool running
Whether the head of the queue has been started (or is being dequeued, see dequeuing()) and has not fi...
Definition executor.h:69
void finished(int id, int exitCode, const QString &output, const QString &errout)
Emitted when a queued process exits normally, whatever its exit code; a non-zero code is the receiver...
void starting()
Emitted just before a queued process is started; the main window shows the busy indicator on it.
void setEnvironment(const QProcessEnvironment &env)
Set the environment passed to all child processes.
Definition executor.cpp:453
static auto runBlocking(QProcess &process, const QString &app, const QStringList &args, const QString &input, QString *process_out, QString *process_err, const std::atomic_bool *cancel=nullptr, int timeoutMs=-1) -> int
Shared blocking run: start process, optionally feed input on stdin, wait, and capture stdout/stderr....
Definition executor.cpp:382
static void detachFromTerminal(QProcess &process)
Make process start its child without a controlling terminal.
Definition executor.cpp:34
void executeNext()
Start the head of the queue unless something runs or the queue is empty.
Definition executor.cpp:192
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
Definition executor.cpp:114
auto cancelNext() -> int
Cancel the next queued command without executing it.
Definition executor.cpp:462
QProcess m_process
The one process object; every queued command reuses it.
Definition executor.h:64
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
The "qtpass" logging category.
Execution queue item for non-interactive ordered execution.
Definition executor.h:38
QString workingDir
Working directory to start in; empty for the current one.
Definition executor.h:55
QString input
Data written to the process's stdin, then closed.
Definition executor.h:46
int id
Identifier the caller gave; echoed in finished()/error().
Definition executor.h:40
QStringList args
Arguments for the executable.
Definition executor.h:44
QString app
Executable as configured; resolved when started.
Definition executor.h:42
A configured executable that runs a Linux program through WSL.
Definition executor.h:328
auto argv(const QStringList &args) const -> QStringList
Arguments for launcher that run command with args.
Definition executor.cpp:106
QString command
The Linux program to run, looked up on the WSL PATH.
Definition executor.h:344
QString launcher
What to start.
Definition executor.h:337
QStringList options
wsl.exe options that go before the program (-d Debian, -u me), without any -e/--exec: argv() always a...
Definition executor.h:342
auto with(const QString &program) const -> WslCommand
The same launcher and options running another program.
Definition executor.cpp:110