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

IJHack / QtPass / 41468883384

02 Nov 2026 03:09PM UTC coverage: 92.702% (-0.02%) from 92.72%
41468883384

Pull #2110

github

web-flow
Merge 23712a69d into 9d87aa394
Pull Request #2110: Executor: bound the wait after kill() in a blocking run

2 of 4 new or added lines in 1 file covered. (50.0%)

10086 of 10880 relevant lines covered (92.7%)

397.87 hits per line

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

96.93
/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
 * 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
void stopBlocking(QProcess &process) {
10 ✔
316
  process.terminate();
10 ✔
317
  if (process.waitForFinished(kBlockingKillGraceMs)) {
10 ✔
318
    return;
319
  }
320
  process.kill();
3 ✔
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)) {
3 ✔
NEW
326
    qCWarning(lcQtPass) << "Process still running after kill():"
×
NEW
327
                        << 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
auto msecsLeft(const QDeadlineTimer &deadline) -> int {
2,085 ✔
336
  if (deadline.isForever()) {
2,085 ✔
337
    return -1;
338
  }
339
  return static_cast<int>(
340
      std::clamp<qint64>(deadline.remainingTime(), 0, INT_MAX));
112 ✔
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
auto waitOrCancel(QProcess &process, const std::atomic_bool *cancel,
1,183 ✔
353
                  const QDeadlineTimer &deadline) -> bool {
354
  if (cancel == nullptr) {
1,183 ✔
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)) &&
846 ✔
359
        process.state() != QProcess::NotRunning) {
3 ✔
360
      stopBlocking(process);
3 ✔
361
      return false;
3 ✔
362
    }
363
    return true;
840 ✔
364
  }
365
  while (!process.waitForFinished(
357 ✔
366
      deadline.isForever()
367
          ? kBlockingCancelPollMs
368
          : qMin(kBlockingCancelPollMs, msecsLeft(deadline)))) {
357 ✔
369
    if (process.state() == QProcess::NotRunning) {
21 ✔
370
      return true;
371
    }
372
    if (cancel->load() || deadline.hasExpired()) {
28 ✔
373
      stopBlocking(process);
7 ✔
374
      return false;
7 ✔
375
    }
376
  }
377
  return true;
378
}
379

380
} // namespace
381

382
auto Executor::runBlocking(QProcess &process, const QString &app,
1,247 ✔
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())
1,247 ✔
388
    return -1;
389
  const QDeadlineTimer deadline = timeoutMs < 0
390
                                      ? QDeadlineTimer(QDeadlineTimer::Forever)
1,239 ✔
391
                                      : QDeadlineTimer(timeoutMs);
27 ✔
392
  startProcess(process, app, args);
1,239 ✔
393
  if (!process.waitForStarted(msecsLeft(deadline))) {
1,239 ✔
394
    qCDebug(lcQtPass) << "Process failed to start:" << app;
56 ✔
395
    if (process.state() != QProcess::NotRunning) {
56 ✔
396
      stopBlocking(process);
×
397
    }
398
    return -1;
56 ✔
399
  }
400
  if (!input.isEmpty()) {
1,183 ✔
401
    QByteArray data = input.toUtf8();
402
    if (process.write(data) != data.length()) {
186 ✔
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();
1,183 ✔
408
  if (!waitOrCancel(process, cancel, deadline)) {
1,183 ✔
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) {
1,173 ✔
414
    *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
2,054 ✔
415
  }
416
  if (process_err != nullptr) {
1,173 ✔
417
    *process_err = decodeAssumingUtf8(process.readAllStandardError());
366 ✔
418
  }
419
  return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
1,173 ✔
420
}
421

422
auto Executor::executeBlocking(const QString &app, const QStringList &args,
203 ✔
423
                               const QString &input, QString *process_out,
424
                               QString *process_err) -> int {
425
  QProcess internal;
203 ✔
426
  return runBlocking(internal, app, args, input, process_out, process_err);
406 ✔
427
}
203 ✔
428

429
auto Executor::executeBlocking(QProcess &process, const QString &app,
1,019 ✔
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,
1,019 ✔
435
                     cancel, timeoutMs);
1,019 ✔
436
}
437

438
auto Executor::executeBlocking(const QString &app, const QStringList &args,
166 ✔
439
                               QString *process_out, QString *process_err)
440
    -> int {
441
  return executeBlocking(app, args, QString(), process_out, process_err);
332 ✔
442
}
443

444
auto Executor::executeBlocking(const QProcessEnvironment &env,
25 ✔
445
                               const QString &app, const QStringList &args,
446
                               QString *process_out, QString *process_err)
447
    -> int {
448
  QProcess process;
25 ✔
449
  process.setProcessEnvironment(env);
25 ✔
450
  return runBlocking(process, app, args, QString(), process_out, process_err);
50 ✔
451
}
25 ✔
452

453
void Executor::setEnvironment(const QProcessEnvironment &env) {
461 ✔
454
  m_process.setProcessEnvironment(env);
461 ✔
455
}
461 ✔
456

457
auto Executor::environment() const -> QProcessEnvironment {
72 ✔
458
  return m_process.processEnvironment();
72 ✔
459
}
460

461
// Also -1 while the head is already running: that one is not cancelled.
462
auto Executor::cancelNext() -> int {
10 ✔
463
  if (running || m_execQueue.isEmpty()) {
10 ✔
464
    return -1;
465
  }
466
  const int id = m_execQueue.dequeue().id;
6 ✔
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;
6 ✔
473
  emit dequeuing(id, false);
6 ✔
474
  running = false;
6 ✔
475
  return id;
6 ✔
476
}
477

478
void Executor::onProcessFinished(int exitCode,
225 ✔
479
                                 QProcess::ExitStatus exitStatus) {
480
  ExecQueueItem i = m_execQueue.dequeue();
481
  running = false;
225 ✔
482
  auto [output, err] = collectOutput(i, exitCode);
225 ✔
483
  if (exitStatus == QProcess::NormalExit) {
225 ✔
484
    if (exitCode != 0) {
223 ✔
485
      qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
24 ✔
486
    }
487
    emit finished(i.id, exitCode, output, err);
223 ✔
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()) {
2 ✔
492
      err = tr("%1 crashed or was killed").arg(i.app);
4 ✔
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);
2 ✔
498
  }
499
  executeNext();
225 ✔
500
  postDrainedIfIdle();
225 ✔
501
}
225 ✔
502

503
void Executor::postDrainedIfIdle() {
233 ✔
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) {
233 ✔
508
    return;
509
  }
510
  m_drainCheckPosted = true;
231 ✔
511
  QMetaObject::invokeMethod(
231 ✔
512
      this,
513
      [this]() {
231 ✔
514
        m_drainCheckPosted = false;
179 ✔
515
        if (isIdle() && m_pendingErrors == 0) {
80 ✔
516
          emit drained();
80 ✔
517
        }
518
      },
179 ✔
519
      Qt::QueuedConnection);
520
}
521

522
auto Executor::collectOutput(const ExecQueueItem &item, int exitCode)
225 ✔
523
    -> std::pair<QString, QString> {
524
  QString output;
225 ✔
525
  QString err;
225 ✔
526
  if (item.readStdout) {
225 ✔
527
    output = decodeAssumingUtf8(m_process.readAllStandardOutput());
350 ✔
528
  }
529
  if (item.readStderr || exitCode != 0) {
225 ✔
530
    err = decodeAssumingUtf8(m_process.readAllStandardError());
326 ✔
531
  }
532
  return {output, err};
225 ✔
533
}
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