• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 41294590088

01 Nov 2026 09:46AM UTC coverage: 92.72% (+0.03%) from 92.69%
41294590088

push

github

web-flow
Executor: optional deadline for blocking runs; safe.directory uses it (#2107)

The safe.directory query (#2102) ran its own QProcess to get a deadline,
so it skipped Executor::startProcess(): no detachFromTerminal(), no
child umask, the one git run unlike the rest. Give Executor's blocking
run an optional timeoutMs instead (the whole run, start included;
default none) and let the query use it again, with five seconds.

A run past its deadline ends as a cancelled one does: terminate(), a
grace period, kill(), and -1. The stop is shared by both paths now.

SECURITY.md also says that a relative safe.directory entry such as ".",
which git resolves against its working directory, trusts nothing in
QtPass (both from Henk on #2102, tracked in #2094).


Claude-Session: https://claude.ai/code/session_01VJygpMUf461yZgfAqwMJ6d

Signed-off-by: Anne Jan Brouwer <brouwer@annejan.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

29 of 30 new or added lines in 2 files covered. (96.67%)

10087 of 10879 relevant lines covered (92.72%)

398.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.8
/src/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

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
Executor::Executor(QObject *parent) : QObject(parent) {
327 ✔
30
  connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
327 ✔
31
  connect(&m_process, &QProcess::started, this, &Executor::starting);
327 ✔
32
}
327 ✔
33

34
void Executor::detachFromTerminal(QProcess &process) {
1,473 ✔
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();
1,473 ✔
58
  params.flags |= QProcess::UnixProcessFlag::CreateNewSession;
59
  process.setUnixProcessParameters(params);
1,473 ✔
60
#else
61
  Q_UNUSED(process);
62
#endif
63
}
1,473 ✔
64

65
void Executor::setChildUmask(int mask) { g_childUmask.store(mask); }
5 ✔
66

67
auto Executor::childUmask() -> int { return g_childUmask.load(); }
31 ✔
68

69
void Executor::startProcess(QProcess &process, const QString &app,
1,468 ✔
70
                            const QStringList &args) {
71
  detachFromTerminal(process);
1,468 ✔
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) {
1,468 ✔
75
    process.setChildProcessModifier(
2 ✔
76
        [mask] { ::umask(static_cast<mode_t>(mask)); });
×
77
  }
78
#endif
79
  if (const auto wsl = parseWslCommand(app)) {
1,468 ✔
80
    process.start(wsl->launcher, wsl->argv(args));
18 ✔
81
  } else {
82
    process.start(resolveExecutable(app), args);
2,918 ✔
83
  }
84
}
1,468 ✔
85

86
auto Executor::resolveExecutable(const QString &app) -> QString {
1,465 ✔
87
  if (app.isEmpty() || QDir::isAbsolutePath(app)) {
2,929 ✔
88
    return app;
89
  }
90
  const QDir appDir(QCoreApplication::applicationDirPath());
61 ✔
91
  QStringList candidates{QDir::cleanPath(appDir.absoluteFilePath(app))};
244 ✔
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)) {
119 ✔
97
    // A stray non-executable file of that name must not mask PATH.
98
    const QFileInfo info(candidate);
61 ✔
99
    if (info.isFile() && info.isExecutable()) {
61 ✔
100
      return candidate;
101
    }
102
  }
61 ✔
103
  return app;
104
}
122 ✔
105

106
auto Executor::WslCommand::argv(const QStringList &args) const -> QStringList {
32 ✔
107
  return options + QStringList{QStringLiteral("--exec"), command} + args;
128 ✔
108
}
109

110
auto Executor::WslCommand::with(const QString &program) const -> WslCommand {
9 ✔
111
  return {launcher, options, program};
9 ✔
112
}
113

114
auto Executor::parseWslCommand(const QString &app)
2,900 ✔
115
    -> std::optional<WslCommand> {
116
  const QStringList parts = QProcess::splitCommand(app);
2,900 ✔
117
  if (parts.size() < 2) {
2,900 ✔
118
    return std::nullopt;
2,858 ✔
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('/')),
84 ✔
124
                                       launcher.lastIndexOf(QLatin1Char('\\')));
42 ✔
125
  const QString launcherName = launcher.mid(separator + 1);
42 ✔
126
  const bool isWsl =
127
      launcherName.compare(QLatin1String("wsl"), Qt::CaseInsensitive) == 0 ||
55 ✔
128
      launcherName.compare(QLatin1String("wsl.exe"), Qt::CaseInsensitive) == 0;
16 ✔
129
  if (!isWsl) {
130
    return std::nullopt;
3 ✔
131
  }
132
  WslCommand wsl;
39 ✔
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;
39 ✔
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"),
2 ✔
146
      QStringLiteral("-u"),   QStringLiteral("--user"),
2 ✔
147
      QStringLiteral("--cd"), QStringLiteral("--shell-type")};
53 ✔
148
  qsizetype i = 1;
149
  for (; i < parts.size() && parts.at(i).startsWith(QLatin1Char('-')); ++i) {
56 ✔
150
    const QString &option = parts.at(i);
151
    if (option == QLatin1String("-e") || option == QLatin1String("--exec")) {
33 ✔
152
      continue;
7 ✔
153
    }
154
    if (valued.contains(option)) {
11 ✔
155
      if (i + 1 >= parts.size()) {
11 ✔
156
        return std::nullopt;
1 ✔
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) {
38 ✔
166
    return std::nullopt;
6 ✔
167
  }
168
  wsl.command = parts.at(i);
32 ✔
169
  return wsl;
170
}
39 ✔
171

172
auto Executor::wslExecArgs(const QString &command, const QStringList &args)
2 ✔
173
    -> QStringList {
174
  return WslCommand{QStringLiteral("wsl"), {}, command}.argv(args);
2 ✔
175
}
176

177
auto Executor::translatePathForWsl(const QString &path, const QString &exe)
1,062 ✔
178
    -> QString {
179
  QString normalizedPath = QDir::cleanPath(path);
1,062 ✔
180
  const auto wsl = parseWslCommand(exe);
1,062 ✔
181
  if (!wsl) {
1,062 ✔
182
    return normalizedPath;
183
  }
184
  QString wslPath;
1 ✔
185
  const auto wslpath = wsl->with(QStringLiteral("wslpath"));
2 ✔
186
  const int rc = executeBlocking(wslpath.launcher,
1 ✔
187
                                 wslpath.argv({normalizedPath}), &wslPath);
3 ✔
188
  const QString translated = wslPath.trimmed();
189
  return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
1 ✔
190
}
1 ✔
191

192
void Executor::executeNext() {
473 ✔
193
  if (running || m_execQueue.isEmpty()) {
473 ✔
194
    return;
247 ✔
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();
234 ✔
200
  running = true;
234 ✔
201
  emit dequeuing(i.id, true);
234 ✔
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()) {
234 ✔
206
    qCDebug(lcQtPass) << "No executable set for:" << i.id;
5 ✔
207
    const int failedId = i.id;
5 ✔
208
    running = false;
5 ✔
209
    m_execQueue.dequeue();
5 ✔
210
    ++m_pendingErrors;
5 ✔
211
    QMetaObject::invokeMethod(
5 ✔
212
        this,
213
        [this, failedId]() {
5 ✔
214
          emit error(failedId, -1, QString(),
10 ✔
215
                     tr("No executable configured for this command"));
5 ✔
216
          --m_pendingErrors;
5 ✔
217
          postDrainedIfIdle();
5 ✔
218
        },
5 ✔
219
        Qt::QueuedConnection);
220
    executeNext();
5 ✔
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);
229 ✔
227
  startProcess(m_process, i.app, i.args);
229 ✔
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)) {
229 ✔
234
    qCDebug(lcQtPass) << "Process failed to start:" << i.id << " " << i.app;
3 ✔
235
    const int failedId = i.id;
3 ✔
236
    const QString failedApp = i.app;
237
    m_process.closeWriteChannel();
3 ✔
238
    running = false;
3 ✔
239
    m_execQueue.dequeue();
3 ✔
240
    ++m_pendingErrors;
3 ✔
241
    QMetaObject::invokeMethod(
3 ✔
242
        this,
243
        [this, failedId, failedApp]() {
6 ✔
244
          emit error(failedId, -1, QString(),
9 ✔
245
                     tr("Failed to start %1").arg(failedApp));
6 ✔
246
          --m_pendingErrors;
3 ✔
247
          postDrainedIfIdle();
3 ✔
248
        },
3 ✔
249
        Qt::QueuedConnection);
250
    executeNext();
3 ✔
251
    return;
252
  }
253

254
  if (!i.input.isEmpty()) {
226 ✔
255
    QByteArray data = i.input.toUtf8();
256
    if (m_process.write(data) != data.length()) {
70 ✔
257
      qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
×
258
                        << i.app;
×
259
    }
260
  }
261
  m_process.closeWriteChannel();
226 ✔
262
}
234 ✔
263

264
void Executor::execute(int id, const QString &app, const QStringList &args,
76 ✔
265
                       bool readStdout, bool readStderr) {
266
  execute(id, QString(), app, args, QString(), readStdout, readStderr);
152 ✔
267
}
76 ✔
268

269
void Executor::execute(int id, const QString &workDir, const QString &app,
2 ✔
270
                       const QStringList &args, bool readStdout,
271
                       bool readStderr) {
272
  execute(id, workDir, app, args, QString(), readStdout, readStderr);
2 ✔
273
}
2 ✔
274

275
void Executor::execute(int id, const QString &app, const QStringList &args,
1 ✔
276
                       QString input, bool readStdout, bool readStderr) {
277
  execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
2 ✔
278
}
1 ✔
279

280
void Executor::execute(int id, const QString &workDir, const QString &app,
240 ✔
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(
240 ✔
286
      {id, app, args, std::move(input), readStdout, readStderr, workDir});
287
  executeNext();
240 ✔
288
}
720 ✔
289

290
// UTF-8 first; on a decoding error fall back to the system encoding.
291
static auto decodeAssumingUtf8(const QByteArray &in) -> QString {
1,548 ✔
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);
1,548 ✔
297
  if (!converter.hasError()) {
1,548 ✔
298
    return out;
299
  }
300
  auto fallback =
301
      QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
302
  return fallback(in);
1 ✔
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
 * A console gpg ignores terminate() on Windows (WM_CLOSE), hence the kill.
313
 */
314
void stopBlocking(QProcess &process) {
10 ✔
315
  process.terminate();
10 ✔
316
  if (!process.waitForFinished(kBlockingKillGraceMs)) {
10 ✔
317
    process.kill();
3 ✔
318
    process.waitForFinished(-1);
3 ✔
319
  }
320
}
10 ✔
321

322
/**
323
 * @brief What is left of @p deadline, as QProcess's wait functions take it:
324
 * -1 for none.
325
 */
326
auto msecsLeft(const QDeadlineTimer &deadline) -> int {
2,085 ✔
327
  if (deadline.isForever()) {
2,085 ✔
328
    return -1;
329
  }
330
  return static_cast<int>(
331
      std::clamp<qint64>(deadline.remainingTime(), 0, INT_MAX));
112 ✔
332
}
333

334
/**
335
 * @brief Wait for @p process to finish, stopping it when @p cancel is set
336
 * or @p deadline passes.
337
 *
338
 * Polls the flag; every QProcess call, terminate()/kill() included, stays on
339
 * this thread, so another thread can never act on an exited process (or a
340
 * pid the OS has since reused).
341
 * @return false when it was cancelled or ran out of time.
342
 */
343
auto waitOrCancel(QProcess &process, const std::atomic_bool *cancel,
1,183 ✔
344
                  const QDeadlineTimer &deadline) -> bool {
345
  if (cancel == nullptr) {
1,183 ✔
346
    // waitForFinished() can also fail (a poll error) with the child still
347
    // running; that is not completion either. Stop it rather than report
348
    // it done.
349
    if (!process.waitForFinished(msecsLeft(deadline)) &&
846 ✔
350
        process.state() != QProcess::NotRunning) {
3 ✔
351
      stopBlocking(process);
3 ✔
352
      return false;
3 ✔
353
    }
354
    return true;
840 ✔
355
  }
356
  while (!process.waitForFinished(
356 ✔
357
      deadline.isForever()
358
          ? kBlockingCancelPollMs
359
          : qMin(kBlockingCancelPollMs, msecsLeft(deadline)))) {
356 ✔
360
    if (process.state() == QProcess::NotRunning) {
20 ✔
361
      return true;
362
    }
363
    if (cancel->load() || deadline.hasExpired()) {
27 ✔
364
      stopBlocking(process);
7 ✔
365
      return false;
7 ✔
366
    }
367
  }
368
  return true;
369
}
370

371
} // namespace
372

373
auto Executor::runBlocking(QProcess &process, const QString &app,
1,247 ✔
374
                           const QStringList &args, const QString &input,
375
                           QString *process_out, QString *process_err,
376
                           const std::atomic_bool *cancel, int timeoutMs)
377
    -> int {
378
  if (cancel != nullptr && cancel->load())
1,247 ✔
379
    return -1;
380
  const QDeadlineTimer deadline = timeoutMs < 0
381
                                      ? QDeadlineTimer(QDeadlineTimer::Forever)
1,239 ✔
382
                                      : QDeadlineTimer(timeoutMs);
27 ✔
383
  startProcess(process, app, args);
1,239 ✔
384
  if (!process.waitForStarted(msecsLeft(deadline))) {
1,239 ✔
385
    qCDebug(lcQtPass) << "Process failed to start:" << app;
56 ✔
386
    if (process.state() != QProcess::NotRunning) {
56 ✔
NEW
387
      stopBlocking(process);
×
388
    }
389
    return -1;
56 ✔
390
  }
391
  if (!input.isEmpty()) {
1,183 ✔
392
    QByteArray data = input.toUtf8();
393
    if (process.write(data) != data.length()) {
186 ✔
394
      qCDebug(lcQtPass) << "Not all input written:" << app;
×
395
    }
396
  }
397
  // Always close stdin so a child blocking on EOF doesn't hang.
398
  process.closeWriteChannel();
1,183 ✔
399
  if (!waitOrCancel(process, cancel, deadline)) {
1,183 ✔
400
    return -1;
401
  }
402
  // Read before judging the exit: a process that crashed may have said why,
403
  // as the queued path (onProcessFinished) keeps it too.
404
  if (process_out != nullptr) {
1,173 ✔
405
    *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
2,054 ✔
406
  }
407
  if (process_err != nullptr) {
1,173 ✔
408
    *process_err = decodeAssumingUtf8(process.readAllStandardError());
366 ✔
409
  }
410
  return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
1,173 ✔
411
}
412

413
auto Executor::executeBlocking(const QString &app, const QStringList &args,
203 ✔
414
                               const QString &input, QString *process_out,
415
                               QString *process_err) -> int {
416
  QProcess internal;
203 ✔
417
  return runBlocking(internal, app, args, input, process_out, process_err);
406 ✔
418
}
203 ✔
419

420
auto Executor::executeBlocking(QProcess &process, const QString &app,
1,019 ✔
421
                               const QStringList &args, const QString &input,
422
                               QString *process_out, QString *process_err,
423
                               const std::atomic_bool *cancel, int timeoutMs)
424
    -> int {
425
  return runBlocking(process, app, args, input, process_out, process_err,
1,019 ✔
426
                     cancel, timeoutMs);
1,019 ✔
427
}
428

429
auto Executor::executeBlocking(const QString &app, const QStringList &args,
166 ✔
430
                               QString *process_out, QString *process_err)
431
    -> int {
432
  return executeBlocking(app, args, QString(), process_out, process_err);
332 ✔
433
}
434

435
auto Executor::executeBlocking(const QProcessEnvironment &env,
25 ✔
436
                               const QString &app, const QStringList &args,
437
                               QString *process_out, QString *process_err)
438
    -> int {
439
  QProcess process;
25 ✔
440
  process.setProcessEnvironment(env);
25 ✔
441
  return runBlocking(process, app, args, QString(), process_out, process_err);
50 ✔
442
}
25 ✔
443

444
void Executor::setEnvironment(const QProcessEnvironment &env) {
461 ✔
445
  m_process.setProcessEnvironment(env);
461 ✔
446
}
461 ✔
447

448
auto Executor::environment() const -> QProcessEnvironment {
72 ✔
449
  return m_process.processEnvironment();
72 ✔
450
}
451

452
// Also -1 while the head is already running: that one is not cancelled.
453
auto Executor::cancelNext() -> int {
10 ✔
454
  if (running || m_execQueue.isEmpty()) {
10 ✔
455
    return -1;
456
  }
457
  const int id = m_execQueue.dequeue().id;
6 ✔
458
  // As in executeNext(): while the signal is out, a slot's cancelNext()
459
  // drops nothing and its execute() only queues. Nothing is started here
460
  // afterwards: the caller (ImitatePass dropping the rest of a failed
461
  // transaction from its completion handler) may cancel again, and the
462
  // queue moves on when that handler returns (onProcessFinished()).
463
  running = true;
6 ✔
464
  emit dequeuing(id, false);
6 ✔
465
  running = false;
6 ✔
466
  return id;
6 ✔
467
}
468

469
void Executor::onProcessFinished(int exitCode,
225 ✔
470
                                 QProcess::ExitStatus exitStatus) {
471
  ExecQueueItem i = m_execQueue.dequeue();
472
  running = false;
225 ✔
473
  auto [output, err] = collectOutput(i, exitCode);
225 ✔
474
  if (exitStatus == QProcess::NormalExit) {
225 ✔
475
    if (exitCode != 0) {
223 ✔
476
      qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
24 ✔
477
    }
478
    emit finished(i.id, exitCode, output, err);
223 ✔
479
  } else {
480
    // A signal-killed process usually leaves stderr empty; without this the
481
    // password pane would show nothing at all for the failure.
482
    if (err.trimmed().isEmpty()) {
2 ✔
483
      err = tr("%1 crashed or was killed").arg(i.app);
4 ✔
484
    }
485
    // Qt leaves exitCode undefined after a CrashExit (the signal number on
486
    // Unix), and Pass::finished gates on it: 0 would pass as success and 1
487
    // as grep's "no matches". Report -1, like the failed-to-start path.
488
    emit error(i.id, -1, output, err);
2 ✔
489
  }
490
  executeNext();
225 ✔
491
  postDrainedIfIdle();
225 ✔
492
}
225 ✔
493

494
void Executor::postDrainedIfIdle() {
233 ✔
495
  // One check at a time, run after what is emitted now; it fires only once
496
  // nothing runs or waits and every posted error() of a failed start is
497
  // out (the last of those posts the check again).
498
  if (m_drainCheckPosted) {
233 ✔
499
    return;
500
  }
501
  m_drainCheckPosted = true;
231 ✔
502
  QMetaObject::invokeMethod(
231 ✔
503
      this,
504
      [this]() {
231 ✔
505
        m_drainCheckPosted = false;
179 ✔
506
        if (isIdle() && m_pendingErrors == 0) {
79 ✔
507
          emit drained();
79 ✔
508
        }
509
      },
179 ✔
510
      Qt::QueuedConnection);
511
}
512

513
auto Executor::collectOutput(const ExecQueueItem &item, int exitCode)
225 ✔
514
    -> std::pair<QString, QString> {
515
  QString output;
225 ✔
516
  QString err;
225 ✔
517
  if (item.readStdout) {
225 ✔
518
    output = decodeAssumingUtf8(m_process.readAllStandardOutput());
350 ✔
519
  }
520
  if (item.readStderr || exitCode != 0) {
225 ✔
521
    err = decodeAssumingUtf8(m_process.readAllStandardError());
326 ✔
522
  }
523
  return {output, err};
225 ✔
524
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc