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

IJHack / QtPass / 36012989720

24 Sep 2026 02:27PM UTC coverage: 92.971% (+0.04%) from 92.935%
36012989720

push

github

web-flow
Split the 10–14 band where a function did several jobs (#1923)

* Split the 10-14 band where a function did several jobs

After no function reached 15, 46 sat between 10 and 14. The grammar
checkers among them (Base32, TOTP URI, WSL command, gpg colon output, the
URL gate, path and link validators) stay as they are: each is one flat list
of rules, and splitting it would scatter the rules. The ones that did several
jobs in one body are now the sequence they were:

- recoverReencryptLeftovers: dropStaleTemporary, restoreBackup and one
  reportUnrestorable for the message it gave twice (14 -> 9).
- Executor::runBlocking: waitOrCancel (14 -> 10).
- UsersDialog::on_importKeyButton_clicked: selectImportedKey (14 -> 4).
- ImitatePass::Insert and reencryption's encryptFor build their gpg
  arguments with one encryptArgs(), as the comment asked ("both belong on
  every encrypt call"); recipientsForEntry() (13 -> 11).
- ImitatePass::Copy: copyDestination (12 -> 8).
- RealPass::passMoveOrCopy: passName (13 -> 11).
- Pass::getGpgIdPath: nearestGpgId walks up (13 -> 6).
- Pass::recipientsForEditing: the verdict-to-preselection mapping is its
  own file-local function (12 -> 5).
- SshAuthSock::initialise: the gpgconf and launchctl probes were the same
  code twice; adoptSocketFrom (12 -> 5).
- TemplateIO::readTemplates and getFolderTemplate: sectionHeader,
  defaultTemplateIn (13 -> 10, 12 -> 8).
- MainWindow's 120-line constructor: connectBackends, initTextBrowser,
  initDisplayPanel, initTimers, initProcessOutput, in the same order
  (11 -> 4).
- PasswordDisplayPanel::addField: addActionButtons, addHiddenPassword,
  valueBrowser, urlButton (10 -> 4).
- ConfigDialog::on_accepted: warnAboutSshAuthSockOverride,
  followActiveProfile (11 -> 1); initializeNewProfiles:
  ensureProfileFolder, initialiseProfileStore (10 -> 5).

No behaviour change. src/: 717 functions, 581 at <= 5, 100 at 6-9, 36 at
10-14, none above; mean 3.38.

Co-Authored-By: Claude Opus 5.5 <noreply@... (continued)

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

6 existing lines in 1 file now uncovered.

8875 of 9546 relevant lines covered (92.97%)

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