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

IJHack / QtPass / 41477231087

02 Nov 2026 04:00PM UTC coverage: 92.702% (-0.02%) from 92.72%
41477231087

push

github

web-flow
Executor: bound the wait after kill() in a blocking run (#2110)

* Executor: bound the wait after kill() in a blocking run

stopBlocking() waited forever after kill(). SIGKILL cannot be ignored,
but a child in uninterruptible sleep (a hung NFS or FUSE mount) exits
only when the kernel lets it, and until then held the calling thread
past the run's deadline. Wait one more grace period instead, log a
warning if the child is still there, and return -1 as before: a
blocking run now ends within its timeout plus twice the grace.

QProcess's destructor still waits for such a child, for up to 30 s;
the header says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJygpMUf461yZgfAqwMJ6d
Signed-off-by: Anne Jan Brouwer <brouwer@annejan.com>

* Executor: the timeout does not cover QProcess::start() itself

start() can block, for example opening a FIFO a redirected channel
names, before the deadline is ever checked. No caller redirects one;
say so instead of promising a bound end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJygpMUf461yZgfAqwMJ6d
Signed-off-by: Anne Jan Brouwer <brouwer@annejan.com>

* Executor: "a FIFO named by a redirected channel"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJygpMUf461yZgfAqwMJ6d
Signed-off-by: Anne Jan Brouwer <brouwer@annejan.com>

---------

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

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

10086 of 10880 relevant lines covered (92.7%)

398.0 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,474 ✔
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,474 ✔
58
  params.flags |= QProcess::UnixProcessFlag::CreateNewSession;
59
  process.setUnixProcessParameters(params);
1,474 ✔
60
#else
61
  Q_UNUSED(process);
62
#endif
63
}
1,474 ✔
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,469 ✔
70
                            const QStringList &args) {
71
  detachFromTerminal(process);
1,469 ✔
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,469 ✔
75
    process.setChildProcessModifier(
2 ✔
76
        [mask] { ::umask(static_cast<mode_t>(mask)); });
×
77
  }
78
#endif
79
  if (const auto wsl = parseWslCommand(app)) {
1,469 ✔
80
    process.start(wsl->launcher, wsl->argv(args));
18 ✔
81
  } else {
82
    process.start(resolveExecutable(app), args);
2,920 ✔
83
  }
84
}
1,469 ✔
85

86
auto Executor::resolveExecutable(const QString &app) -> QString {
1,466 ✔
87
  if (app.isEmpty() || QDir::isAbsolutePath(app)) {
2,931 ✔
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,901 ✔
115
    -> std::optional<WslCommand> {
116
  const QStringList parts = QProcess::splitCommand(app);
2,901 ✔
117
  if (parts.size() < 2) {
2,901 ✔
118
    return std::nullopt;
2,859 ✔
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) {
11 ✔
316
  process.terminate();
11 ✔
317
  if (process.waitForFinished(kBlockingKillGraceMs)) {
11 ✔
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,086 ✔
336
  if (deadline.isForever()) {
2,086 ✔
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,184 ✔
353
                  const QDeadlineTimer &deadline) -> bool {
354
  if (cancel == nullptr) {
1,184 ✔
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()) {
29 ✔
373
      stopBlocking(process);
8 ✔
374
      return false;
8 ✔
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,240 ✔
391
                                      : QDeadlineTimer(timeoutMs);
27 ✔
392
  startProcess(process, app, args);
1,240 ✔
393
  if (!process.waitForStarted(msecsLeft(deadline))) {
1,240 ✔
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,184 ✔
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,184 ✔
408
  if (!waitOrCancel(process, cancel, deadline)) {
1,184 ✔
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;
25 ✔
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) {
79 ✔
516
          emit drained();
79 ✔
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