QtPass Developer documentation
QtPass test coverage
Current view: top level - src/executor.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 97.4 % 232 226
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 34 34

            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              : }
        

Generated by: LCOV version 2.0-1