Line data Source code
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 :
19 : namespace {
20 : /// Executor::setChildUmask(); negative for none.
21 : std::atomic_int g_childUmask{-1};
22 : /// How often a cancellable blocking run re-checks its cancel flag.
23 : constexpr int kBlockingCancelPollMs = 100;
24 : /// Grace between terminate() and kill() once a blocking run is cancelled;
25 : /// gpg and git exit on SIGTERM well within this.
26 : constexpr int kBlockingKillGraceMs = 1000;
27 : } // namespace
28 :
29 352 : Executor::Executor(QObject *parent) : QObject(parent) {
30 352 : connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
31 352 : connect(&m_process, &QProcess::started, this, &Executor::starting);
32 352 : }
33 :
34 1625 : void 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 1625 : QProcess::UnixProcessParameters params = process.unixProcessParameters();
58 : params.flags |= QProcess::UnixProcessFlag::CreateNewSession;
59 1625 : process.setUnixProcessParameters(params);
60 : #else
61 : Q_UNUSED(process);
62 : #endif
63 1625 : }
64 :
65 5 : void Executor::setChildUmask(int mask) { g_childUmask.store(mask); }
66 :
67 31 : auto Executor::childUmask() -> int { return g_childUmask.load(); }
68 :
69 1620 : void Executor::startProcess(QProcess &process, const QString &app,
70 : const QStringList &args) {
71 1620 : detachFromTerminal(process);
72 : #ifdef Q_OS_UNIX
73 : // In the child, between fork and exec: umask() is async-signal-safe.
74 1620 : if (const int mask = g_childUmask.load(); mask >= 0) {
75 1 : process.setChildProcessModifier(
76 1 : [mask] { ::umask(static_cast<mode_t>(mask)); });
77 : }
78 : #endif
79 1620 : if (const auto wsl = parseWslCommand(app)) {
80 18 : process.start(wsl->launcher, wsl->argv(args));
81 : } else {
82 3222 : process.start(resolveExecutable(app), args);
83 : }
84 1620 : }
85 :
86 1617 : auto Executor::resolveExecutable(const QString &app) -> QString {
87 3233 : if (app.isEmpty() || QDir::isAbsolutePath(app)) {
88 : return app;
89 : }
90 61 : const QDir appDir(QCoreApplication::applicationDirPath());
91 244 : 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 119 : for (const QString &candidate : std::as_const(candidates)) {
97 : // A stray non-executable file of that name must not mask PATH.
98 61 : const QFileInfo info(candidate);
99 61 : if (info.isFile() && info.isExecutable()) {
100 : return candidate;
101 : }
102 61 : }
103 : return app;
104 122 : }
105 :
106 32 : auto Executor::WslCommand::argv(const QStringList &args) const -> QStringList {
107 128 : return options + QStringList{QStringLiteral("--exec"), command} + args;
108 : }
109 :
110 9 : auto Executor::WslCommand::with(const QString &program) const -> WslCommand {
111 9 : return {launcher, options, program};
112 : }
113 :
114 3163 : auto Executor::parseWslCommand(const QString &app)
115 : -> std::optional<WslCommand> {
116 3163 : const QStringList parts = QProcess::splitCommand(app);
117 3163 : if (parts.size() < 2) {
118 3121 : 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 42 : const qsizetype separator = std::max(launcher.lastIndexOf(QLatin1Char('/')),
124 42 : launcher.lastIndexOf(QLatin1Char('\\')));
125 42 : const QString launcherName = launcher.mid(separator + 1);
126 : const bool isWsl =
127 55 : launcherName.compare(QLatin1String("wsl"), Qt::CaseInsensitive) == 0 ||
128 13 : launcherName.compare(QLatin1String("wsl.exe"), Qt::CaseInsensitive) == 0;
129 : if (!isWsl) {
130 3 : return std::nullopt;
131 : }
132 39 : 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 39 : 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 2 : QStringLiteral("-d"), QStringLiteral("--distribution"),
146 2 : QStringLiteral("-u"), QStringLiteral("--user"),
147 53 : QStringLiteral("--cd"), QStringLiteral("--shell-type")};
148 : qsizetype i = 1;
149 56 : for (; i < parts.size() && parts.at(i).startsWith(QLatin1Char('-')); ++i) {
150 : const QString &option = parts.at(i);
151 18 : if (option == QLatin1String("-e") || option == QLatin1String("--exec")) {
152 7 : continue;
153 : }
154 11 : if (valued.contains(option)) {
155 11 : if (i + 1 >= parts.size()) {
156 1 : 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 38 : if (i != parts.size() - 1) {
166 6 : return std::nullopt;
167 : }
168 32 : wsl.command = parts.at(i);
169 : return wsl;
170 39 : }
171 :
172 2 : auto Executor::wslExecArgs(const QString &command, const QStringList &args)
173 : -> QStringList {
174 2 : return WslCommand{QStringLiteral("wsl"), {}, command}.argv(args);
175 : }
176 :
177 1167 : auto Executor::translatePathForWsl(const QString &path, const QString &exe)
178 : -> QString {
179 1167 : QString normalizedPath = QDir::cleanPath(path);
180 1167 : const auto wsl = parseWslCommand(exe);
181 1167 : if (!wsl) {
182 1166 : return normalizedPath;
183 : }
184 1 : QString wslPath;
185 2 : const auto wslpath = wsl->with(QStringLiteral("wslpath"));
186 1 : const int rc = executeBlocking(wslpath.launcher,
187 3 : wslpath.argv({normalizedPath}), &wslPath);
188 : const QString translated = wslPath.trimmed();
189 1 : return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
190 1 : }
191 :
192 503 : void Executor::executeNext() {
193 503 : if (running || m_execQueue.isEmpty()) {
194 263 : 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 248 : const ExecQueueItem i = m_execQueue.head();
200 248 : running = true;
201 248 : 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 248 : if (i.app.isEmpty()) {
206 5 : qCDebug(lcQtPass) << "No executable set for:" << i.id;
207 5 : const int failedId = i.id;
208 5 : running = false;
209 5 : m_execQueue.dequeue();
210 5 : ++m_pendingErrors;
211 5 : QMetaObject::invokeMethod(
212 : this,
213 5 : [this, failedId]() {
214 10 : emit error(failedId, -1, QString(),
215 5 : tr("No executable configured for this command"));
216 5 : --m_pendingErrors;
217 5 : postDrainedIfIdle();
218 5 : },
219 : Qt::QueuedConnection);
220 5 : 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 243 : m_process.setWorkingDirectory(i.workingDir);
227 243 : startProcess(m_process, i.app, i.args);
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 243 : if (!m_process.waitForStarted(-1)) {
234 3 : qCDebug(lcQtPass) << "Process failed to start:" << i.id << " " << i.app;
235 3 : const int failedId = i.id;
236 : const QString failedApp = i.app;
237 3 : m_process.closeWriteChannel();
238 3 : running = false;
239 3 : m_execQueue.dequeue();
240 3 : ++m_pendingErrors;
241 3 : QMetaObject::invokeMethod(
242 : this,
243 6 : [this, failedId, failedApp]() {
244 9 : emit error(failedId, -1, QString(),
245 6 : tr("Failed to start %1").arg(failedApp));
246 3 : --m_pendingErrors;
247 3 : postDrainedIfIdle();
248 3 : },
249 : Qt::QueuedConnection);
250 3 : executeNext();
251 : return;
252 : }
253 :
254 240 : if (!i.input.isEmpty()) {
255 : QByteArray data = i.input.toUtf8();
256 78 : if (m_process.write(data) != data.length()) {
257 0 : qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
258 0 : << i.app;
259 : }
260 : }
261 240 : m_process.closeWriteChannel();
262 248 : }
263 :
264 76 : void Executor::execute(int id, const QString &app, const QStringList &args,
265 : bool readStdout, bool readStderr) {
266 152 : execute(id, QString(), app, args, QString(), readStdout, readStderr);
267 76 : }
268 :
269 2 : void Executor::execute(int id, const QString &workDir, const QString &app,
270 : const QStringList &args, bool readStdout,
271 : bool readStderr) {
272 2 : execute(id, workDir, app, args, QString(), readStdout, readStderr);
273 2 : }
274 :
275 1 : void Executor::execute(int id, const QString &app, const QStringList &args,
276 : QString input, bool readStdout, bool readStderr) {
277 2 : execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
278 1 : }
279 :
280 256 : void 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 256 : m_execQueue.push_back(
286 : {id, app, args, std::move(input), readStdout, readStderr, workDir});
287 256 : executeNext();
288 768 : }
289 :
290 : // UTF-8 first; on a decoding error fall back to the system encoding.
291 1720 : static 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 1720 : auto converter =
295 : QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
296 1720 : QString out = converter(in);
297 1720 : if (!converter.hasError()) {
298 1719 : return out;
299 : }
300 1 : auto fallback =
301 : QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
302 1 : return fallback(in);
303 : }
304 :
305 : // Returns an error code rather than throwing, matching QtPass's error
306 : // handling elsewhere.
307 : namespace {
308 :
309 : /**
310 : * @brief Stop a running @p process from the thread that owns it:
311 : * terminate(), then kill() if it is still running after a grace period,
312 : * then at most one more grace period for it to exit.
313 : * A console gpg ignores terminate() on Windows (WM_CLOSE), hence the kill.
314 : */
315 10 : void stopBlocking(QProcess &process) {
316 10 : process.terminate();
317 10 : if (process.waitForFinished(kBlockingKillGraceMs)) {
318 : return;
319 : }
320 3 : 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 3 : if (!process.waitForFinished(kBlockingKillGraceMs)) {
326 0 : qCWarning(lcQtPass) << "Process still running after kill():"
327 0 : << process.program();
328 : }
329 : }
330 :
331 : /**
332 : * @brief What is left of @p deadline, as QProcess's wait functions take it:
333 : * -1 for none.
334 : */
335 2340 : auto msecsLeft(const QDeadlineTimer &deadline) -> int {
336 2340 : if (deadline.isForever()) {
337 : return -1;
338 : }
339 : return static_cast<int>(
340 56 : std::clamp<qint64>(deadline.remainingTime(), 0, INT_MAX));
341 : }
342 :
343 : /**
344 : * @brief Wait for @p process to finish, stopping it when @p cancel is set
345 : * or @p deadline passes.
346 : *
347 : * Polls the flag; every QProcess call, terminate()/kill() included, stays on
348 : * this thread, so another thread can never act on an exited process (or a
349 : * pid the OS has since reused).
350 : * @return false when it was cancelled or ran out of time.
351 : */
352 1321 : auto waitOrCancel(QProcess &process, const std::atomic_bool *cancel,
353 : const QDeadlineTimer &deadline) -> bool {
354 1321 : 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 963 : if (!process.waitForFinished(msecsLeft(deadline)) &&
359 3 : process.state() != QProcess::NotRunning) {
360 3 : stopBlocking(process);
361 3 : return false;
362 : }
363 957 : return true;
364 : }
365 750 : while (!process.waitForFinished(
366 : deadline.isForever()
367 : ? kBlockingCancelPollMs
368 3 : : qMin(kBlockingCancelPollMs, msecsLeft(deadline)))) {
369 21 : if (process.state() == QProcess::NotRunning) {
370 : return true;
371 : }
372 28 : if (cancel->load() || deadline.hasExpired()) {
373 7 : stopBlocking(process);
374 7 : return false;
375 : }
376 : }
377 : return true;
378 : }
379 :
380 : } // namespace
381 :
382 1385 : auto 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 1385 : if (cancel != nullptr && cancel->load())
388 : return -1;
389 : const QDeadlineTimer deadline = timeoutMs < 0
390 1377 : ? QDeadlineTimer(QDeadlineTimer::Forever)
391 1377 : : QDeadlineTimer(timeoutMs);
392 1377 : startProcess(process, app, args);
393 1377 : if (!process.waitForStarted(msecsLeft(deadline))) {
394 56 : qCDebug(lcQtPass) << "Process failed to start:" << app;
395 56 : if (process.state() != QProcess::NotRunning) {
396 0 : stopBlocking(process);
397 : }
398 56 : return -1;
399 : }
400 1321 : if (!input.isEmpty()) {
401 : QByteArray data = input.toUtf8();
402 191 : if (process.write(data) != data.length()) {
403 0 : qCDebug(lcQtPass) << "Not all input written:" << app;
404 : }
405 : }
406 : // Always close stdin so a child blocking on EOF doesn't hang.
407 1321 : process.closeWriteChannel();
408 1321 : 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 1311 : if (process_out != nullptr) {
414 2326 : *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
415 : }
416 1311 : if (process_err != nullptr) {
417 382 : *process_err = decodeAssumingUtf8(process.readAllStandardError());
418 : }
419 1311 : return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
420 : }
421 :
422 236 : auto Executor::executeBlocking(const QString &app, const QStringList &args,
423 : const QString &input, QString *process_out,
424 : QString *process_err) -> int {
425 236 : QProcess internal;
426 472 : return runBlocking(internal, app, args, input, process_out, process_err);
427 236 : }
428 :
429 1113 : auto 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 1113 : return runBlocking(process, app, args, input, process_out, process_err,
435 1113 : cancel, timeoutMs);
436 : }
437 :
438 199 : auto Executor::executeBlocking(const QString &app, const QStringList &args,
439 : QString *process_out, QString *process_err)
440 : -> int {
441 398 : return executeBlocking(app, args, QString(), process_out, process_err);
442 : }
443 :
444 36 : auto Executor::executeBlocking(const QProcessEnvironment &env,
445 : const QString &app, const QStringList &args,
446 : QString *process_out, QString *process_err)
447 : -> int {
448 36 : QProcess process;
449 36 : process.setProcessEnvironment(env);
450 72 : return runBlocking(process, app, args, QString(), process_out, process_err);
451 36 : }
452 :
453 486 : void Executor::setEnvironment(const QProcessEnvironment &env) {
454 486 : m_process.setProcessEnvironment(env);
455 486 : }
456 :
457 79 : auto Executor::environment() const -> QProcessEnvironment {
458 79 : return m_process.processEnvironment();
459 : }
460 :
461 : // Also -1 while the head is already running: that one is not cancelled.
462 12 : auto Executor::cancelNext() -> int {
463 12 : if (running || m_execQueue.isEmpty()) {
464 : return -1;
465 : }
466 8 : 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 8 : running = true;
473 8 : emit dequeuing(id, false);
474 8 : running = false;
475 8 : return id;
476 : }
477 :
478 239 : void Executor::onProcessFinished(int exitCode,
479 : QProcess::ExitStatus exitStatus) {
480 : ExecQueueItem i = m_execQueue.dequeue();
481 239 : running = false;
482 239 : auto [output, err] = collectOutput(i, exitCode);
483 239 : if (exitStatus == QProcess::NormalExit) {
484 236 : if (exitCode != 0) {
485 26 : qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
486 : }
487 236 : 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 3 : if (err.trimmed().isEmpty()) {
492 6 : 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 3 : emit error(i.id, -1, output, err);
498 : }
499 239 : executeNext();
500 239 : postDrainedIfIdle();
501 239 : }
502 :
503 247 : void Executor::postDrainedIfIdle() {
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 247 : if (m_drainCheckPosted) {
508 : return;
509 : }
510 245 : m_drainCheckPosted = true;
511 245 : QMetaObject::invokeMethod(
512 : this,
513 245 : [this]() {
514 190 : m_drainCheckPosted = false;
515 94 : if (isIdle() && m_pendingErrors == 0) {
516 94 : emit drained();
517 : }
518 190 : },
519 : Qt::QueuedConnection);
520 : }
521 :
522 239 : auto Executor::collectOutput(const ExecQueueItem &item, int exitCode)
523 : -> std::pair<QString, QString> {
524 239 : QString output;
525 239 : QString err;
526 239 : if (item.readStdout) {
527 378 : output = decodeAssumingUtf8(m_process.readAllStandardOutput());
528 : }
529 239 : if (item.readStderr || exitCode != 0) {
530 354 : err = decodeAssumingUtf8(m_process.readAllStandardError());
531 : }
532 239 : return {output, err};
533 : }
|