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

IJHack / QtPass / 35467906150

19 Sep 2026 08:36PM UTC coverage: 77.868% (-0.3%) from 78.167%
35467906150

Pull #1851

github

web-flow
Merge 27c744ca6 into d67d8e6cc
Pull Request #1851: Store walks visit real directories only; junctions and links are not entries

74 of 117 new or added lines in 6 files covered. (63.25%)

97 existing lines in 6 files now uncovered.

6843 of 8788 relevant lines covered (77.87%)

74.55 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) {
155✔
25
  connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
155✔
26
  connect(&m_process, &QProcess::started, this, &Executor::starting);
155✔
27
}
155✔
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,
356✔
39
                            const QStringList &args) {
40
  if (app.startsWith(QLatin1String("wsl "))) {
356✔
41
    process.start(QStringLiteral("wsl"), wslExecArgs(app.mid(4), args));
6✔
42
  } else {
43
    process.start(resolveExecutable(app), args);
708✔
44
  }
45
}
356✔
46

47
auto Executor::resolveExecutable(const QString &app) -> QString {
360✔
48
  if (app.isEmpty() || app.startsWith(QLatin1String("wsl ")) ||
1,077✔
49
      QDir::isAbsolutePath(app)) {
50
    return app;
51
  }
52
  const QDir appDir(QCoreApplication::applicationDirPath());
50✔
53
  QStringList candidates{QDir::cleanPath(appDir.absoluteFilePath(app))};
200✔
54
#ifdef Q_OS_WIN
55
  candidates << QDir::cleanPath(
56
      appDir.absoluteFilePath(app + QStringLiteral(".exe")));
57
#endif
58
  for (const QString &candidate : std::as_const(candidates)) {
97✔
59
    // A stray non-executable file of that name must not mask PATH.
60
    const QFileInfo info(candidate);
50✔
61
    if (info.isFile() && info.isExecutable()) {
50✔
62
      return candidate;
63
    }
64
  }
50✔
65
  return app;
66
}
100✔
67

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

86
auto Executor::translatePathForWsl(const QString &path, const QString &exe)
233✔
87
    -> QString {
88
  QString normalizedPath = QDir::cleanPath(path);
233✔
89
  if (!exe.startsWith(QStringLiteral("wsl ")))
466✔
90
    return normalizedPath;
UNCOV
91
  QString wslPath;
×
UNCOV
92
  const int rc = executeBlocking(
×
UNCOV
93
      QStringLiteral("wsl"),
×
UNCOV
94
      wslExecArgs(QStringLiteral("wslpath"), {normalizedPath}), &wslPath);
×
95
  const QString translated = wslPath.trimmed();
UNCOV
96
  return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
×
97
}
98

99
/**
100
 * @brief Executor::executeNext consumes executable tasks from the queue
101
 */
102
void Executor::executeNext() {
233✔
103
  if (running || m_execQueue.isEmpty()) {
234✔
104
    return;
105
  }
106
  const execQueueItem &i = m_execQueue.head();
107

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

129
  running = true;
116✔
130
  // Always set it: an item without a directory runs in the application's
131
  // cwd, not wherever the previous item happened to run.
132
  m_process.setWorkingDirectory(i.workingDir);
116✔
133
  startProcess(m_process, i.app, i.args);
116✔
134

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

164
  if (!i.input.isEmpty()) {
114✔
165
    QByteArray data = i.input.toUtf8();
46✔
166
    if (m_process.write(data) != data.length()) {
46✔
UNCOV
167
      qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
×
UNCOV
168
                        << i.app;
×
169
    }
170
  }
171
  m_process.closeWriteChannel();
114✔
172
}
173

174
/**
175
 * @brief Executor::execute execute an app
176
 * @param id
177
 * @param app
178
 * @param args
179
 * @param readStdout
180
 * @param readStderr
181
 */
182
void Executor::execute(int id, const QString &app, const QStringList &args,
21✔
183
                       bool readStdout, bool readStderr) {
184
  execute(id, QString(), app, args, QString(), readStdout, readStderr);
42✔
185
}
21✔
186

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

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

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

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

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

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

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

362
/**
363
 * @brief Executor::executeBlocking blocking version of the executor
364
 * @param app
365
 * @param args
366
 * @param process_out
367
 * @param process_err
368
 * @return
369
 */
370
auto Executor::executeBlocking(const QString &app, const QStringList &args,
63✔
371
                               QString *process_out, QString *process_err)
372
    -> int {
373
  return executeBlocking(app, args, QString(), process_out, process_err);
126✔
374
}
375

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

394
/**
395
 * @brief Executor::setEnvironment set environment variables
396
 * for executor processes
397
 * @param env
398
 */
399
void Executor::setEnvironment(const QProcessEnvironment &env) {
98✔
400
  m_process.setProcessEnvironment(env);
98✔
401
}
98✔
402

403
auto Executor::environment() const -> QProcessEnvironment {
19✔
404
  return m_process.processEnvironment();
19✔
405
}
406

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

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

450
auto Executor::collectOutput(const execQueueItem &item, int exitCode)
113✔
451
    -> std::pair<QString, QString> {
452
  QString output;
113✔
453
  QString err;
113✔
454
  if (item.readStdout) {
113✔
455
    output = decodeAssumingUtf8(m_process.readAllStandardOutput());
218✔
456
  }
457
  if (item.readStderr || exitCode != 0) {
113✔
458
    err = decodeAssumingUtf8(m_process.readAllStandardError());
198✔
459
  }
460
  return {output, err};
113✔
461
}
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