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

IJHack / QtPass / 35200231838

17 Sep 2026 08:32AM UTC coverage: 74.833% (+0.02%) from 74.81%
35200231838

Pull #1800

github

web-flow
Merge cb81c9bc4 into 333ce5aa4
Pull Request #1800: One resolution rule for external binaries

11 of 11 new or added lines in 1 file covered. (100.0%)

7 existing lines in 1 file now uncovered.

5816 of 7772 relevant lines covered (74.83%)

67.93 hits per line

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

94.56
/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 <utility>
9

10
#include "qtpasslogging.h"
11

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

20
/**
21
 * @brief Executor::Executor executes external applications
22
 * @param parent
23
 */
24
Executor::Executor(QObject *parent) : QObject(parent) {
131 ✔
25
  connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
131 ✔
26
  connect(&m_process, &QProcess::started, this, &Executor::starting);
131 ✔
27
}
131 ✔
28

29
/**
30
 * @brief Executor::startProcess starts @p process, handling the "wsl "
31
 * prefix. One implementation for the queued (m_process) and the blocking
32
 * (caller-owned QProcess) path, so the WSL handling cannot drift between
33
 * them.
34
 * @param process QProcess to start.
35
 * @param app Executable path (may start with "wsl ").
36
 * @param args Arguments to pass to the executable.
37
 */
38
void Executor::startProcess(QProcess &process, const QString &app,
295 ✔
39
                            const QStringList &args) {
40
  if (app.startsWith(QLatin1String("wsl "))) {
295 ✔
41
    process.start(QStringLiteral("wsl"), wslExecArgs(app.mid(4), args));
6 ✔
42
  } else {
43
    process.start(resolveExecutable(app), args);
586 ✔
44
  }
45
}
295 ✔
46

47
auto Executor::resolveExecutable(const QString &app) -> QString {
298 ✔
48
  if (app.isEmpty() || app.startsWith(QLatin1String("wsl ")) ||
891 ✔
49
      QDir::isAbsolutePath(app)) {
50
    return app;
51
  }
52
  const QDir appDir(QCoreApplication::applicationDirPath());
48 ✔
53
  for (const QString &candidate :
90 ✔
54
       {appDir.absoluteFilePath(app),
55
        appDir.absoluteFilePath(app + QStringLiteral(".exe"))}) {
282 ✔
56
    if (QFileInfo(candidate).isFile()) {
93 ✔
57
      return candidate;
9 ✔
58
    }
59
  }
144 ✔
60
  return app;
61
}
48 ✔
62

63
/**
64
 * @brief Executor::wslExecArgs builds the wsl.exe argv for @p command.
65
 *
66
 * `wsl <command> <args>` hands the joined command line to the default Linux
67
 * shell, which word-splits and expands `$()` in every argument. `--exec`
68
 * makes WSL launch the binary directly so arguments arrive verbatim.
69
 * @param command Linux command to run.
70
 * @param args Arguments for @p command.
71
 * @return `--exec`, @p command, then @p args.
72
 */
73
auto Executor::wslExecArgs(const QString &command, const QStringList &args)
4 ✔
74
    -> QStringList {
75
  QStringList wslArgs = args;
76
  wslArgs.prepend(command);
77
  wslArgs.prepend(QStringLiteral("--exec"));
4 ✔
78
  return wslArgs;
4 ✔
79
}
80

81
auto Executor::translatePathForWsl(const QString &path, const QString &exe)
177 ✔
82
    -> QString {
83
  QString normalizedPath = QDir::cleanPath(path);
177 ✔
84
  if (!exe.startsWith(QStringLiteral("wsl ")))
354 ✔
85
    return normalizedPath;
UNCOV
86
  QString wslPath;
×
UNCOV
87
  const int rc = executeBlocking(
×
UNCOV
88
      QStringLiteral("wsl"),
×
UNCOV
89
      wslExecArgs(QStringLiteral("wslpath"), {normalizedPath}), &wslPath);
×
90
  const QString translated = wslPath.trimmed();
91
  return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
×
92
}
93

94
/**
95
 * @brief Executor::executeNext consumes executable tasks from the queue
96
 */
97
void Executor::executeNext() {
219 ✔
98
  if (running || m_execQueue.isEmpty()) {
220 ✔
99
    return;
100
  }
101
  const execQueueItem &i = m_execQueue.head();
102

103
  // An empty executable can never produce a finished() signal. Silently
104
  // dropping it used to wedge the queue: the command stayed at the head until
105
  // the next completion, whose signal only caused the dequeue; a second
106
  // completion then stalled everything (#1682). Fail it through the same
107
  // deferred path as a failed-to-start process so the queue keeps draining.
108
  if (i.app.isEmpty()) {
110 ✔
109
    qCDebug(lcQtPass) << "No executable set for:" << i.id;
1 ✔
110
    // Capture before dequeue() invalidates the head reference.
111
    const int failedId = i.id;
1 ✔
112
    m_execQueue.dequeue();
1 ✔
113
    QMetaObject::invokeMethod(
1 ✔
114
        this,
115
        [this, failedId]() {
1 ✔
116
          emit error(failedId, -1, QString(),
2 ✔
117
                     tr("No executable configured for this command"));
1 ✔
118
        },
1 ✔
119
        Qt::QueuedConnection);
120
    executeNext();
121
    return;
1 ✔
122
  }
123

124
  running = true;
109 ✔
125
  // Always set it: an item without a directory runs in the application's
126
  // cwd, not wherever the previous item happened to run.
127
  m_process.setWorkingDirectory(i.workingDir);
109 ✔
128
  startProcess(m_process, i.app, i.args);
109 ✔
129

130
  // Confirm the process actually started, regardless of whether it takes stdin.
131
  // A process that fails to start emits errorOccurred(FailedToStart) but never
132
  // finished(), so without this check `running` would stay true forever and
133
  // stall the whole queue (and, for stdin commands, the input would be dropped
134
  // silently). Surface the failure so callers waiting on a finished/error
135
  // signal (e.g. the GPG keygen dialog) do not hang. Defer the emit via a
136
  // queued call so we do not re-enter a caller still on the stack —
137
  // KeygenDialog::done() drives key generation synchronously — which mirrors
138
  // the normal asynchronous QProcess::finished path. A -1 exit code routes
139
  // through Pass::finished's non-zero error gate.
140
  if (!m_process.waitForStarted(-1)) {
109 ✔
141
    qCDebug(lcQtPass) << "Process failed to start:" << i.id << " " << i.app;
2 ✔
142
    // Capture before dequeue() invalidates the head reference.
143
    const int failedId = i.id;
2 ✔
144
    const QString failedApp = i.app;
145
    m_process.closeWriteChannel();
2 ✔
146
    running = false;
2 ✔
147
    m_execQueue.dequeue();
2 ✔
148
    QMetaObject::invokeMethod(
2 ✔
149
        this,
150
        [this, failedId, failedApp]() {
4 ✔
151
          emit error(failedId, -1, QString(),
6 ✔
152
                     tr("Failed to start %1").arg(failedApp));
4 ✔
153
        },
2 ✔
154
        Qt::QueuedConnection);
155
    executeNext();
2 ✔
156
    return;
157
  }
158

159
  if (!i.input.isEmpty()) {
107 ✔
160
    QByteArray data = i.input.toUtf8();
45 ✔
161
    if (m_process.write(data) != data.length()) {
45 ✔
UNCOV
162
      qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
×
UNCOV
163
                        << i.app;
×
164
    }
165
  }
166
  m_process.closeWriteChannel();
107 ✔
167
}
168

169
/**
170
 * @brief Executor::execute execute an app
171
 * @param id
172
 * @param app
173
 * @param args
174
 * @param readStdout
175
 * @param readStderr
176
 */
177
void Executor::execute(int id, const QString &app, const QStringList &args,
21 ✔
178
                       bool readStdout, bool readStderr) {
179
  execute(id, QString(), app, args, QString(), readStdout, readStderr);
42 ✔
180
}
21 ✔
181

182
/**
183
 * @brief Executor::execute executes an app from a workDir
184
 * @param id
185
 * @param workDir
186
 * @param app
187
 * @param args
188
 * @param readStdout
189
 * @param readStderr
190
 */
191
void Executor::execute(int id, const QString &workDir, const QString &app,
2 ✔
192
                       const QStringList &args, bool readStdout,
193
                       bool readStderr) {
194
  execute(id, workDir, app, args, QString(), readStdout, readStderr);
2 ✔
195
}
2 ✔
196

197
/**
198
 * @brief Executor::execute an app, takes input and presents it as stdin
199
 * @param id
200
 * @param app
201
 * @param args
202
 * @param input
203
 * @param readStdout
204
 * @param readStderr
205
 */
206
void Executor::execute(int id, const QString &app, const QStringList &args,
1 ✔
207
                       QString input, bool readStdout, bool readStderr) {
208
  execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
2 ✔
209
}
1 ✔
210

211
/**
212
 * @brief Executor::execute  executes an app from a workDir, takes input and
213
 * presents it as stdin
214
 * @param id
215
 * @param workDir
216
 * @param app
217
 * @param args
218
 * @param input
219
 * @param readStdout
220
 * @param readStderr
221
 */
222
void Executor::execute(int id, const QString &workDir, const QString &app,
111 ✔
223
                       const QStringList &args, QString input, bool readStdout,
224
                       bool readStderr) {
225
  // An empty executable (e.g. git not configured yet) used to be dropped
226
  // here. That left its command permanently at the head of the queue: no
227
  // process ever runs, no finished()/error() is emitted, and every later
228
  // completion signal is swallowed for the rest of the session (#1682).
229
  // ExecuteNext() now surfaces it as an error instead, so keep queueing it.
230
  // The executable is resolved when the process starts (startProcess), the
231
  // same way the blocking path resolves it.
232
  m_execQueue.push_back(
111 ✔
233
      {id, app, args, std::move(input), readStdout, readStderr, workDir});
234
  executeNext();
111 ✔
235
}
333 ✔
236

237
/**
238
 * @brief decodes the input into a string assuming UTF-8 encoding.
239
 * If this fails (which is likely if it is not actually UTF-8)
240
 * it will then fall back to Qt's decoding function, which
241
 * will try based on BOM and if that fails fall back to local encoding.
242
 *
243
 * @param in input data
244
 * @return Input bytes decoded to string
245
 */
246
static auto decodeAssumingUtf8(const QByteArray &in) -> QString {
393 ✔
247
  // Stateless: the whole output is decoded in one go, so a truncated or
248
  // stray byte at the end becomes a replacement character instead of being
249
  // held back for a continuation that never comes (and thereby dropped).
250
  auto converter =
251
      QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
252
  QString out = converter(in);
393 ✔
253
  if (!converter.hasError()) {
393 ✔
254
    return out;
255
  }
256
  // Fallback if UTF-8 decoding failed - try system encoding
257
  auto fallback =
258
      QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
259
  return fallback(in);
1 ✔
260
}
261

262
/**
263
 * @brief Executor::executeBlocking blocking version of the executor,
264
 * takes input and presents it as stdin
265
 * @param app
266
 * @param args
267
 * @param input
268
 * @param process_out
269
 * @param process_err
270
 * @param cancel
271
 * @return
272
 *
273
 * Note: Returning error code instead of throwing to maintain compatibility
274
 * with the existing error handling pattern used throughout QtPass.
275
 */
276
auto Executor::runBlocking(QProcess &process, const QString &app,
193 ✔
277
                           const QStringList &args, const QString &input,
278
                           QString *process_out, QString *process_err,
279
                           const std::atomic_bool *cancel) -> int {
280
  if (cancel != nullptr && cancel->load())
193 ✔
281
    return -1;
282
  startProcess(process, app, args);
186 ✔
283
  if (!process.waitForStarted(-1)) {
186 ✔
284
    qCDebug(lcQtPass) << "Process failed to start:" << app;
13 ✔
285
    return -1;
13 ✔
286
  }
287
  if (!input.isEmpty()) {
173 ✔
288
    QByteArray data = input.toUtf8();
289
    if (process.write(data) != data.length()) {
13 ✔
UNCOV
290
      qCDebug(lcQtPass) << "Not all input written:" << app;
×
291
    }
292
  }
293
  // Always close stdin so a child blocking on EOF doesn't hang when no
294
  // input is written (these are one-shot blocking runs that never stream).
295
  process.closeWriteChannel();
173 ✔
296
  if (cancel == nullptr) {
173 ✔
297
    process.waitForFinished(-1);
100 ✔
298
  } else {
299
    // Poll so a flag set by another thread is noticed within one interval.
300
    // Every QProcess call, including the terminate()/kill() that end the
301
    // child, stays on this thread: the other thread only sets the flag, so
302
    // it can never act on a process that has already exited (or on a pid
303
    // the OS has since handed to something else).
304
    while (!process.waitForFinished(kBlockingCancelPollMs)) {
83 ✔
305
      if (process.state() == QProcess::NotRunning)
16 ✔
306
        break;
307
      if (cancel->load()) {
16 ✔
308
        process.terminate();
6 ✔
309
        if (!process.waitForFinished(kBlockingKillGraceMs)) {
6 ✔
310
          process.kill();
2 ✔
311
          process.waitForFinished(-1);
2 ✔
312
        }
313
        return -1;
6 ✔
314
      }
315
    }
316
  }
317
  if (process.exitStatus() != QProcess::NormalExit) {
167 ✔
318
    // Process failed to start or crashed; return -1 to indicate error.
319
    // The calling code checks for non-zero exit codes for error handling.
320
    return -1;
321
  }
322
  if (process_out != nullptr) {
167 ✔
323
    *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
286 ✔
324
  }
325
  if (process_err != nullptr) {
167 ✔
326
    *process_err = decodeAssumingUtf8(process.readAllStandardError());
112 ✔
327
  }
328
  return process.exitCode();
167 ✔
329
}
330

331
auto Executor::executeBlocking(const QString &app, const QStringList &args,
84 ✔
332
                               const QString &input, QString *process_out,
333
                               QString *process_err) -> int {
334
  QProcess internal;
84 ✔
335
  return runBlocking(internal, app, args, input, process_out, process_err);
168 ✔
336
}
84 ✔
337

338
/**
339
 * @brief Executor::executeBlocking blocking run on a caller-supplied QProcess
340
 * @param process Process object to run the command on.
341
 * @param app
342
 * @param args
343
 * @param input
344
 * @param process_out
345
 * @param process_err
346
 * @param cancel Optional flag that ends the run when set (see the header).
347
 * @return
348
 */
349
auto Executor::executeBlocking(QProcess &process, const QString &app,
93 ✔
350
                               const QStringList &args, const QString &input,
351
                               QString *process_out, QString *process_err,
352
                               const std::atomic_bool *cancel) -> int {
353
  return runBlocking(process, app, args, input, process_out, process_err,
93 ✔
354
                     cancel);
93 ✔
355
}
356

357
/**
358
 * @brief Executor::executeBlocking blocking version of the executor
359
 * @param app
360
 * @param args
361
 * @param process_out
362
 * @param process_err
363
 * @return
364
 */
365
auto Executor::executeBlocking(const QString &app, const QStringList &args,
60 ✔
366
                               QString *process_out, QString *process_err)
367
    -> int {
368
  return executeBlocking(app, args, QString(), process_out, process_err);
120 ✔
369
}
370

371
/**
372
 * @brief Executor::executeBlocking blocking version with custom environment
373
 * @param env Environment variables to set
374
 * @param app Executable path
375
 * @param args Arguments
376
 * @param process_out Standard output
377
 * @param process_err Standard error
378
 * @return Exit code
379
 */
380
auto Executor::executeBlocking(const QProcessEnvironment &env,
16 ✔
381
                               const QString &app, const QStringList &args,
382
                               QString *process_out, QString *process_err)
383
    -> int {
384
  QProcess process;
16 ✔
385
  process.setProcessEnvironment(env);
16 ✔
386
  return runBlocking(process, app, args, QString(), process_out, process_err);
32 ✔
387
}
16 ✔
388

389
/**
390
 * @brief Executor::setEnvironment set environment variables
391
 * for executor processes
392
 * @param env
393
 */
394
void Executor::setEnvironment(const QProcessEnvironment &env) {
91 ✔
395
  m_process.setProcessEnvironment(env);
91 ✔
396
}
91 ✔
397

398
auto Executor::environment() const -> QProcessEnvironment {
18 ✔
399
  return m_process.processEnvironment();
18 ✔
400
}
401

402
/**
403
 * @brief Executor::cancelNext  cancels execution of first process in queue
404
 *                              if it's not already running
405
 *
406
 * @return  id of the cancelled process or -1 on error
407
 */
408
auto Executor::cancelNext() -> int {
3 ✔
409
  if (running || m_execQueue.isEmpty()) {
3 ✔
410
    return -1; // Return -1 to indicate no process was cancelled
411
               // (queue empty or currently executing).
412
  }
413
  return m_execQueue.dequeue().id;
1 ✔
414
}
415

416
/**
417
 * @brief Executor::onProcessFinished called when an executed process finishes
418
 * @param exitCode
419
 * @param exitStatus
420
 */
421
void Executor::onProcessFinished(int exitCode,
106 ✔
422
                                 QProcess::ExitStatus exitStatus) {
423
  execQueueItem i = m_execQueue.dequeue();
424
  running = false;
106 ✔
425
  auto [output, err] = collectOutput(i, exitCode);
106 ✔
426
  if (exitStatus == QProcess::NormalExit) {
106 ✔
427
    if (exitCode != 0) {
104 ✔
428
      qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
6 ✔
429
    }
430
    emit finished(i.id, exitCode, output, err);
104 ✔
431
  } else {
432
    // A signal-killed process usually leaves stderr empty; without this the
433
    // password pane would show nothing at all for the failure.
434
    if (err.trimmed().isEmpty()) {
2 ✔
435
      err = tr("%1 crashed or was killed").arg(i.app);
4 ✔
436
    }
437
    // Qt leaves exitCode undefined after a CrashExit (the signal number on
438
    // Unix), and Pass::finished gates on it: 0 would pass as success and 1
439
    // as grep's "no matches". Report -1, like the failed-to-start path.
440
    emit error(i.id, -1, output, err);
2 ✔
441
  }
442
  executeNext();
106 ✔
443
}
106 ✔
444

445
auto Executor::collectOutput(const execQueueItem &item, int exitCode)
106 ✔
446
    -> std::pair<QString, QString> {
447
  QString output;
106 ✔
448
  QString err;
106 ✔
449
  if (item.readStdout) {
106 ✔
450
    output = decodeAssumingUtf8(m_process.readAllStandardOutput());
204 ✔
451
  }
452
  if (item.readStderr || exitCode != 0) {
106 ✔
453
    err = decodeAssumingUtf8(m_process.readAllStandardError());
184 ✔
454
  }
455
  return {output, err};
106 ✔
456
}
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