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

IJHack / QtPass / 36011684787

24 Sep 2026 02:17PM UTC coverage: 92.981% (+0.05%) from 92.935%
36011684787

Pull #1923

github

web-flow
Merge 9793f9dcb into f78497b22
Pull Request #1923: Split the 10–14 band where a function did several jobs

364 of 384 new or added lines in 10 files covered. (94.79%)

6 existing lines in 1 file now uncovered.

8876 of 9546 relevant lines covered (92.98%)

154.4 hits per line

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

96.17
/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 <QDir>
6
#include <QFileInfo>
7
#include <QStringDecoder>
8
#include <algorithm>
9
#include <utility>
10

11
#include "qtpasslogging.h"
12

13
namespace {
14
/// How often a cancellable blocking run re-checks its cancel flag.
15
constexpr int kBlockingCancelPollMs = 100;
16
/// Grace between terminate() and kill() once a blocking run is cancelled;
17
/// gpg and git exit on SIGTERM well within this.
18
constexpr int kBlockingKillGraceMs = 1000;
19
} // namespace
20

21
Executor::Executor(QObject *parent) : QObject(parent) {
282 ✔
22
  connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
282 ✔
23
  connect(&m_process, &QProcess::started, this, &Executor::starting);
282 ✔
24
}
282 ✔
25

26
void Executor::startProcess(QProcess &process, const QString &app,
836 ✔
27
                            const QStringList &args) {
28
  if (const auto wsl = parseWslCommand(app)) {
836 ✔
29
    process.start(wsl->launcher, wsl->argv(args));
18 ✔
30
  } else {
31
    process.start(resolveExecutable(app), args);
1,654 ✔
32
  }
33
}
836 ✔
34

35
auto Executor::resolveExecutable(const QString &app) -> QString {
833 ✔
36
  if (app.isEmpty() || QDir::isAbsolutePath(app)) {
1,665 ✔
37
    return app;
38
  }
39
  const QDir appDir(QCoreApplication::applicationDirPath());
56 ✔
40
  QStringList candidates{QDir::cleanPath(appDir.absoluteFilePath(app))};
224 ✔
41
#ifdef Q_OS_WIN
42
  candidates << QDir::cleanPath(
43
      appDir.absoluteFilePath(app + QStringLiteral(".exe")));
44
#endif
45
  for (const QString &candidate : std::as_const(candidates)) {
109 ✔
46
    // A stray non-executable file of that name must not mask PATH.
47
    const QFileInfo info(candidate);
56 ✔
48
    if (info.isFile() && info.isExecutable()) {
56 ✔
49
      return candidate;
50
    }
51
  }
56 ✔
52
  return app;
53
}
112 ✔
54

55
auto Executor::WslCommand::argv(const QStringList &args) const -> QStringList {
32 ✔
56
  return options + QStringList{QStringLiteral("--exec"), command} + args;
128 ✔
57
}
58

59
auto Executor::WslCommand::with(const QString &program) const -> WslCommand {
9 ✔
60
  return {launcher, options, program};
9 ✔
61
}
62

63
auto Executor::parseWslCommand(const QString &app)
1,746 ✔
64
    -> std::optional<WslCommand> {
65
  const QStringList parts = QProcess::splitCommand(app);
1,746 ✔
66
  if (parts.size() < 2) {
1,746 ✔
67
    return std::nullopt;
1,704 ✔
68
  }
69
  // The file name after the last separator of either kind: a Windows path
70
  // has to be recognised as such wherever the parser runs.
71
  const QString &launcher = parts.first();
72
  const qsizetype separator = std::max(launcher.lastIndexOf(QLatin1Char('/')),
84 ✔
73
                                       launcher.lastIndexOf(QLatin1Char('\\')));
42 ✔
74
  const QString launcherName = launcher.mid(separator + 1);
42 ✔
75
  const bool isWsl =
76
      launcherName.compare(QLatin1String("wsl"), Qt::CaseInsensitive) == 0 ||
55 ✔
77
      launcherName.compare(QLatin1String("wsl.exe"), Qt::CaseInsensitive) == 0;
16 ✔
78
  if (!isWsl) {
79
    return std::nullopt;
3 ✔
80
  }
81
  WslCommand wsl;
39 ✔
82
#ifdef Q_OS_WIN
83
  // A bare wsl or wsl.exe in any case is looked up on PATH as `wsl`; a path
84
  // stays as is.
85
  wsl.launcher = separator < 0 ? QStringLiteral("wsl") : launcher;
86
#else
87
  // QtPass running inside WSL reaches wsl.exe through interop under exactly
88
  // that name; nothing says `wsl` resolves too, so start what was written.
89
  wsl.launcher = launcher;
39 ✔
90
#endif
91
  // wsl.exe options that take a value; anything else starting with `-` is a
92
  // flag. A user-written -e/--exec is dropped, argv() always adds one.
93
  static const QStringList valued{
94
      QStringLiteral("-d"),   QStringLiteral("--distribution"),
2 ✔
95
      QStringLiteral("-u"),   QStringLiteral("--user"),
2 ✔
96
      QStringLiteral("--cd"), QStringLiteral("--shell-type")};
53 ✔
97
  qsizetype i = 1;
98
  for (; i < parts.size() && parts.at(i).startsWith(QLatin1Char('-')); ++i) {
56 ✔
99
    const QString &option = parts.at(i);
100
    if (option == QLatin1String("-e") || option == QLatin1String("--exec")) {
33 ✔
101
      continue;
7 ✔
102
    }
103
    if (valued.contains(option)) {
11 ✔
104
      if (i + 1 >= parts.size()) {
11 ✔
105
        return std::nullopt;
1 ✔
106
      }
107
      wsl.options << option << parts.at(++i);
108
    } else {
109
      wsl.options << option;
110
    }
111
  }
112
  // Exactly one program. More is a shell command line (`sh -c "..."`),
113
  // which is what --exec exists to keep out.
114
  if (i != parts.size() - 1) {
38 ✔
115
    return std::nullopt;
6 ✔
116
  }
117
  wsl.command = parts.at(i);
32 ✔
118
  return wsl;
119
}
39 ✔
120

121
auto Executor::wslExecArgs(const QString &command, const QStringList &args)
2 ✔
122
    -> QStringList {
123
  return WslCommand{QStringLiteral("wsl"), {}, command}.argv(args);
2 ✔
124
}
125

126
auto Executor::translatePathForWsl(const QString &path, const QString &exe)
556 ✔
127
    -> QString {
128
  QString normalizedPath = QDir::cleanPath(path);
556 ✔
129
  const auto wsl = parseWslCommand(exe);
556 ✔
130
  if (!wsl) {
556 ✔
131
    return normalizedPath;
132
  }
133
  QString wslPath;
1 ✔
134
  const auto wslpath = wsl->with(QStringLiteral("wslpath"));
2 ✔
135
  const int rc = executeBlocking(wslpath.launcher,
1 ✔
136
                                 wslpath.argv({normalizedPath}), &wslPath);
3 ✔
137
  const QString translated = wslPath.trimmed();
138
  return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
1 ✔
139
}
1 ✔
140

141
void Executor::executeNext() {
315 ✔
142
  if (running || m_execQueue.isEmpty()) {
316 ✔
143
    return;
144
  }
145
  const ExecQueueItem &i = m_execQueue.head();
146

147
  // An empty executable never emits finished(); dropping it wedged the queue
148
  // (#1682). Fail it like a failed-to-start process so the queue drains.
149
  if (i.app.isEmpty()) {
157 ✔
150
    qCDebug(lcQtPass) << "No executable set for:" << i.id;
1 ✔
151
    // Capture before dequeue() invalidates the head reference.
152
    const int failedId = i.id;
1 ✔
153
    m_execQueue.dequeue();
1 ✔
154
    QMetaObject::invokeMethod(
1 ✔
155
        this,
156
        [this, failedId]() {
1 ✔
157
          emit error(failedId, -1, QString(),
2 ✔
158
                     tr("No executable configured for this command"));
1 ✔
159
        },
1 ✔
160
        Qt::QueuedConnection);
161
    executeNext();
162
    return;
1 ✔
163
  }
164

165
  running = true;
156 ✔
166
  // Always set it: an item without a directory runs in the application's
167
  // cwd, not wherever the previous item happened to run.
168
  m_process.setWorkingDirectory(i.workingDir);
156 ✔
169
  startProcess(m_process, i.app, i.args);
156 ✔
170

171
  // A failed start never emits finished(), so `running` would stall the queue
172
  // and waiters (the keygen dialog) would hang. The emit is queued so it does
173
  // not re-enter KeygenDialog::done(), still on the stack; -1 goes through
174
  // Pass::finished's non-zero error gate.
175
  if (!m_process.waitForStarted(-1)) {
156 ✔
176
    qCDebug(lcQtPass) << "Process failed to start:" << i.id << " " << i.app;
2 ✔
177
    // Capture before dequeue() invalidates the head reference.
178
    const int failedId = i.id;
2 ✔
179
    const QString failedApp = i.app;
180
    m_process.closeWriteChannel();
2 ✔
181
    running = false;
2 ✔
182
    m_execQueue.dequeue();
2 ✔
183
    QMetaObject::invokeMethod(
2 ✔
184
        this,
185
        [this, failedId, failedApp]() {
4 ✔
186
          emit error(failedId, -1, QString(),
6 ✔
187
                     tr("Failed to start %1").arg(failedApp));
4 ✔
188
        },
2 ✔
189
        Qt::QueuedConnection);
190
    executeNext();
2 ✔
191
    return;
192
  }
193

194
  if (!i.input.isEmpty()) {
154 ✔
195
    QByteArray data = i.input.toUtf8();
58 ✔
196
    if (m_process.write(data) != data.length()) {
58 ✔
197
      qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
×
198
                        << i.app;
×
199
    }
200
  }
201
  m_process.closeWriteChannel();
154 ✔
202
}
203

204
void Executor::execute(int id, const QString &app, const QStringList &args,
21 ✔
205
                       bool readStdout, bool readStderr) {
206
  execute(id, QString(), app, args, QString(), readStdout, readStderr);
42 ✔
207
}
21 ✔
208

209
void Executor::execute(int id, const QString &workDir, const QString &app,
2 ✔
210
                       const QStringList &args, bool readStdout,
211
                       bool readStderr) {
212
  execute(id, workDir, app, args, QString(), readStdout, readStderr);
2 ✔
213
}
2 ✔
214

215
void Executor::execute(int id, const QString &app, const QStringList &args,
1 ✔
216
                       QString input, bool readStdout, bool readStderr) {
217
  execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
2 ✔
218
}
1 ✔
219

220
void Executor::execute(int id, const QString &workDir, const QString &app,
160 ✔
221
                       const QStringList &args, QString input, bool readStdout,
222
                       bool readStderr) {
223
  // Queue even an empty executable: executeNext() reports it (#1682).
224
  // startProcess resolves the executable, as on the blocking path.
225
  m_execQueue.push_back(
160 ✔
226
      {id, app, args, std::move(input), readStdout, readStderr, workDir});
227
  executeNext();
160 ✔
228
}
480 ✔
229

230
// UTF-8 first; on a decoding error fall back to the system encoding.
231
static auto decodeAssumingUtf8(const QByteArray &in) -> QString {
940 ✔
232
  // Stateless: a truncated trailing byte becomes a replacement character
233
  // instead of being held back for a continuation that never comes.
234
  auto converter =
235
      QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
236
  QString out = converter(in);
940 ✔
237
  if (!converter.hasError()) {
940 ✔
238
    return out;
239
  }
240
  auto fallback =
241
      QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
242
  return fallback(in);
1 ✔
243
}
244

245
// Returns an error code rather than throwing, matching QtPass's error
246
// handling elsewhere.
247
namespace {
248

249
/**
250
 * @brief Wait for @p process to finish, stopping it when @p cancel is set.
251
 *
252
 * Polls the flag; every QProcess call, terminate()/kill() included, stays on
253
 * this thread, so another thread can never act on an exited process (or a
254
 * pid the OS has since reused). A console gpg ignores terminate() on
255
 * Windows (WM_CLOSE), hence the kill after a grace period.
256
 * @return false when it was cancelled.
257
 */
258
auto waitOrCancel(QProcess &process, const std::atomic_bool *cancel) -> bool {
623 ✔
259
  if (cancel == nullptr) {
623 ✔
260
    // waitForFinished(-1) can fail (a poll error) with the child still
261
    // running; that is not completion. Stop it rather than report it done.
262
    if (!process.waitForFinished(-1) &&
340 ✔
NEW
263
        process.state() != QProcess::NotRunning) {
×
NEW
264
      process.kill();
×
NEW
265
      process.waitForFinished(kBlockingKillGraceMs);
×
NEW
266
      return false;
×
267
    }
268
    return true;
340 ✔
269
  }
270
  while (!process.waitForFinished(kBlockingCancelPollMs)) {
295 ✔
271
    if (process.state() == QProcess::NotRunning) {
19 ✔
272
      return true;
273
    }
274
    if (cancel->load()) {
19 ✔
275
      process.terminate();
7 ✔
276
      if (!process.waitForFinished(kBlockingKillGraceMs)) {
7 ✔
277
        process.kill();
2 ✔
278
        process.waitForFinished(-1);
2 ✔
279
      }
280
      return false;
7 ✔
281
    }
282
  }
283
  return true;
284
}
285

286
} // namespace
287

288
auto Executor::runBlocking(QProcess &process, const QString &app,
687 ✔
289
                           const QStringList &args, const QString &input,
290
                           QString *process_out, QString *process_err,
291
                           const std::atomic_bool *cancel) -> int {
292
  if (cancel != nullptr && cancel->load())
687 ✔
293
    return -1;
294
  startProcess(process, app, args);
680 ✔
295
  if (!process.waitForStarted(-1)) {
680 ✔
296
    qCDebug(lcQtPass) << "Process failed to start:" << app;
57 ✔
297
    return -1;
57 ✔
298
  }
299
  if (!input.isEmpty()) {
623 ✔
300
    QByteArray data = input.toUtf8();
301
    if (process.write(data) != data.length()) {
119 ✔
302
      qCDebug(lcQtPass) << "Not all input written:" << app;
×
303
    }
304
  }
305
  // Always close stdin so a child blocking on EOF doesn't hang.
306
  process.closeWriteChannel();
623 ✔
307
  if (!waitOrCancel(process, cancel)) {
623 ✔
308
    return -1;
309
  }
310
  // Read before judging the exit: a process that crashed may have said why,
311
  // as the queued path (onProcessFinished) keeps it too.
312
  if (process_out != nullptr) {
616 ✔
313
    *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
1,004 ✔
314
  }
315
  if (process_err != nullptr) {
616 ✔
316
    *process_err = decodeAssumingUtf8(process.readAllStandardError());
300 ✔
317
  }
318
  return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
616 ✔
319
}
320

321
auto Executor::executeBlocking(const QString &app, const QStringList &args,
298 ✔
322
                               const QString &input, QString *process_out,
323
                               QString *process_err) -> int {
324
  QProcess internal;
298 ✔
325
  return runBlocking(internal, app, args, input, process_out, process_err);
596 ✔
326
}
298 ✔
327

328
auto Executor::executeBlocking(QProcess &process, const QString &app,
377 ✔
329
                               const QStringList &args, const QString &input,
330
                               QString *process_out, QString *process_err,
331
                               const std::atomic_bool *cancel) -> int {
332
  return runBlocking(process, app, args, input, process_out, process_err,
377 ✔
333
                     cancel);
377 ✔
334
}
335

336
auto Executor::executeBlocking(const QString &app, const QStringList &args,
171 ✔
337
                               QString *process_out, QString *process_err)
338
    -> int {
339
  return executeBlocking(app, args, QString(), process_out, process_err);
342 ✔
340
}
341

342
auto Executor::executeBlocking(const QProcessEnvironment &env,
12 ✔
343
                               const QString &app, const QStringList &args,
344
                               QString *process_out, QString *process_err)
345
    -> int {
346
  QProcess process;
12 ✔
347
  process.setProcessEnvironment(env);
12 ✔
348
  return runBlocking(process, app, args, QString(), process_out, process_err);
24 ✔
349
}
12 ✔
350

351
void Executor::setEnvironment(const QProcessEnvironment &env) {
173 ✔
352
  m_process.setProcessEnvironment(env);
173 ✔
353
}
173 ✔
354

355
auto Executor::environment() const -> QProcessEnvironment {
28 ✔
356
  return m_process.processEnvironment();
28 ✔
357
}
358

359
// Also -1 while the head is already running: that one is not cancelled.
360
auto Executor::cancelNext() -> int {
5 ✔
361
  if (running || m_execQueue.isEmpty()) {
5 ✔
362
    return -1;
363
  }
364
  return m_execQueue.dequeue().id;
3 ✔
365
}
366

367
void Executor::onProcessFinished(int exitCode,
153 ✔
368
                                 QProcess::ExitStatus exitStatus) {
369
  ExecQueueItem i = m_execQueue.dequeue();
370
  running = false;
153 ✔
371
  auto [output, err] = collectOutput(i, exitCode);
153 ✔
372
  if (exitStatus == QProcess::NormalExit) {
153 ✔
373
    if (exitCode != 0) {
151 ✔
374
      qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
16 ✔
375
    }
376
    emit finished(i.id, exitCode, output, err);
151 ✔
377
  } else {
378
    // A signal-killed process usually leaves stderr empty; without this the
379
    // password pane would show nothing at all for the failure.
380
    if (err.trimmed().isEmpty()) {
2 ✔
381
      err = tr("%1 crashed or was killed").arg(i.app);
4 ✔
382
    }
383
    // Qt leaves exitCode undefined after a CrashExit (the signal number on
384
    // Unix), and Pass::finished gates on it: 0 would pass as success and 1
385
    // as grep's "no matches". Report -1, like the failed-to-start path.
386
    emit error(i.id, -1, output, err);
2 ✔
387
  }
388
  executeNext();
153 ✔
389
}
153 ✔
390

391
auto Executor::collectOutput(const ExecQueueItem &item, int exitCode)
153 ✔
392
    -> std::pair<QString, QString> {
393
  QString output;
153 ✔
394
  QString err;
153 ✔
395
  if (item.readStdout) {
153 ✔
396
    output = decodeAssumingUtf8(m_process.readAllStandardOutput());
298 ✔
397
  }
398
  if (item.readStderr || exitCode != 0) {
153 ✔
399
    err = decodeAssumingUtf8(m_process.readAllStandardError());
278 ✔
400
  }
401
  return {output, err};
153 ✔
402
}
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