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

IJHack / QtPass / 35159963146

16 Sep 2026 10:55PM UTC coverage: 72.009% (-0.3%) from 72.336%
35159963146

push

github

web-flow
One "qtpass" logging category instead of dbg() and 69 QT_DEBUG guards (#1785)

* One "qtpass" logging category instead of dbg() and 69 QT_DEBUG guards

debughelper.h promised file:line output "only in debug builds" while
src.pro already compiled qDebug out of release builds, and the #ifdef
blocks spliced control flow (an else in trayicon.cpp, an else-if in
sshauthsock.cpp) and have broken the build before (55149eca1, 0b6a80bdf).

Q_LOGGING_CATEGORY(lcQtPass, "qtpass", QtInfoMsg): every dbg() is a
qCDebug(lcQtPass), the debug-only qWarning()s are qCWarning(lcQtPass), the
guards are gone, and QT_NO_DEBUG_OUTPUT is no longer defined for release,
so a shipped build can produce a trace with
QT_LOGGING_RULES="qtpass.debug=true". FAQ entry and fixing skill updated;
tst_util checks the category is quiet until asked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* Convert the last bare qDebug(); FAQ: key IDs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

36 of 78 new or added lines in 15 files covered. (46.15%)

23 existing lines in 8 files now uncovered.

5513 of 7656 relevant lines covered (72.01%)

60.03 hits per line

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

94.29
/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 <QStringDecoder>
7
#include <utility>
8

9
#include "qtpasslogging.h"
10

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

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

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

46
/**
47
 * @brief Executor::wslExecArgs builds the wsl.exe argv for @p command.
48
 *
49
 * `wsl <command> <args>` hands the joined command line to the default Linux
50
 * shell, which word-splits and expands `$()` in every argument. `--exec`
51
 * makes WSL launch the binary directly so arguments arrive verbatim.
52
 * @param command Linux command to run.
53
 * @param args Arguments for @p command.
54
 * @return `--exec`, @p command, then @p args.
55
 */
56
auto Executor::wslExecArgs(const QString &command, const QStringList &args)
4 ✔
57
    -> QStringList {
58
  QStringList wslArgs = args;
59
  wslArgs.prepend(command);
60
  wslArgs.prepend(QStringLiteral("--exec"));
4 ✔
61
  return wslArgs;
4 ✔
62
}
63

64
auto Executor::translatePathForWsl(const QString &path, const QString &exe)
174 ✔
65
    -> QString {
66
  QString normalizedPath = QDir::cleanPath(path);
174 ✔
67
  if (!exe.startsWith(QStringLiteral("wsl ")))
348 ✔
68
    return normalizedPath;
69
  QString wslPath;
×
70
  const int rc = executeBlocking(
×
71
      QStringLiteral("wsl"),
×
72
      wslExecArgs(QStringLiteral("wslpath"), {normalizedPath}), &wslPath);
×
73
  const QString translated = wslPath.trimmed();
74
  return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
×
75
}
76

77
/**
78
 * @brief Executor::executeNext consumes executable tasks from the queue
79
 */
80
void Executor::executeNext() {
181 ✔
81
  if (running || m_execQueue.isEmpty()) {
182 ✔
82
    return;
83
  }
84
  const execQueueItem &i = m_execQueue.head();
85

86
  // An empty executable can never produce a finished() signal. Silently
87
  // dropping it used to wedge the queue: the command stayed at the head until
88
  // the next completion, whose signal only caused the dequeue; a second
89
  // completion then stalled everything (#1682). Fail it through the same
90
  // deferred path as a failed-to-start process so the queue keeps draining.
91
  if (i.app.isEmpty()) {
91 ✔
92
    qCDebug(lcQtPass) << "No executable set for:" << i.id;
1 ✔
93
    // Capture before dequeue() invalidates the head reference.
94
    const int failedId = i.id;
1 ✔
95
    m_execQueue.dequeue();
1 ✔
96
    QMetaObject::invokeMethod(
1 ✔
97
        this,
98
        [this, failedId]() {
1 ✔
99
          emit error(failedId, -1, QString(),
2 ✔
100
                     tr("No executable configured for this command"));
1 ✔
101
        },
1 ✔
102
        Qt::QueuedConnection);
103
    executeNext();
104
    return;
1 ✔
105
  }
106

107
  running = true;
90 ✔
108
  // Always set it: an item without a directory runs in the application's
109
  // cwd, not wherever the previous item happened to run.
110
  m_process.setWorkingDirectory(i.workingDir);
90 ✔
111
  startProcess(m_process, i.app, i.args);
90 ✔
112

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

142
  if (!i.input.isEmpty()) {
88 ✔
143
    QByteArray data = i.input.toUtf8();
43 ✔
144
    if (m_process.write(data) != data.length()) {
43 ✔
NEW
145
      qCDebug(lcQtPass) << "Not all data written to process:" << i.id << " "
×
NEW
146
                        << i.app;
×
147
    }
148
  }
149
  m_process.closeWriteChannel();
88 ✔
150
}
151

152
/**
153
 * @brief Executor::execute execute an app
154
 * @param id
155
 * @param app
156
 * @param args
157
 * @param readStdout
158
 * @param readStderr
159
 */
160
void Executor::execute(int id, const QString &app, const QStringList &args,
19 ✔
161
                       bool readStdout, bool readStderr) {
162
  execute(id, QString(), app, args, QString(), readStdout, readStderr);
38 ✔
163
}
19 ✔
164

165
/**
166
 * @brief Executor::execute executes an app from a workDir
167
 * @param id
168
 * @param workDir
169
 * @param app
170
 * @param args
171
 * @param readStdout
172
 * @param readStderr
173
 */
174
void Executor::execute(int id, const QString &workDir, const QString &app,
2 ✔
175
                       const QStringList &args, bool readStdout,
176
                       bool readStderr) {
177
  execute(id, workDir, app, args, QString(), readStdout, readStderr);
2 ✔
178
}
2 ✔
179

180
/**
181
 * @brief Executor::execute an app, takes input and presents it as stdin
182
 * @param id
183
 * @param app
184
 * @param args
185
 * @param input
186
 * @param readStdout
187
 * @param readStderr
188
 */
189
void Executor::execute(int id, const QString &app, const QStringList &args,
1 ✔
190
                       QString input, bool readStdout, bool readStderr) {
191
  execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
2 ✔
192
}
1 ✔
193

194
/**
195
 * @brief Executor::execute  executes an app from a workDir, takes input and
196
 * presents it as stdin
197
 * @param id
198
 * @param workDir
199
 * @param app
200
 * @param args
201
 * @param input
202
 * @param readStdout
203
 * @param readStderr
204
 */
205
void Executor::execute(int id, const QString &workDir, const QString &app,
92 ✔
206
                       const QStringList &args, QString input, bool readStdout,
207
                       bool readStderr) {
208
  // An empty executable (e.g. git not configured yet) used to be dropped
209
  // here. That left its command permanently at the head of the queue: no
210
  // process ever runs, no finished()/error() is emitted, and every later
211
  // completion signal is swallowed for the rest of the session (#1682).
212
  // ExecuteNext() now surfaces it as an error instead, so keep queueing it.
213
  QString appPath = app;
214
  if (!appPath.isEmpty() && !appPath.startsWith("wsl ")) {
183 ✔
215
    appPath =
216
        QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(app);
270 ✔
217
  }
218
  m_execQueue.push_back(
92 ✔
219
      {id, appPath, args, std::move(input), readStdout, readStderr, workDir});
220
  executeNext();
92 ✔
221
}
276 ✔
222

223
/**
224
 * @brief decodes the input into a string assuming UTF-8 encoding.
225
 * If this fails (which is likely if it is not actually UTF-8)
226
 * it will then fall back to Qt's decoding function, which
227
 * will try based on BOM and if that fails fall back to local encoding.
228
 *
229
 * @param in input data
230
 * @return Input bytes decoded to string
231
 */
232
static auto decodeAssumingUtf8(const QByteArray &in) -> QString {
341 ✔
233
  // Stateless: the whole output is decoded in one go, so a truncated or
234
  // stray byte at the end becomes a replacement character instead of being
235
  // held back for a continuation that never comes (and thereby dropped).
236
  auto converter =
237
      QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
238
  QString out = converter(in);
341 ✔
239
  if (!converter.hasError()) {
341 ✔
240
    return out;
241
  }
242
  // Fallback if UTF-8 decoding failed - try system encoding
243
  auto fallback =
244
      QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
245
  return fallback(in);
1 ✔
246
}
247

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

317
auto Executor::executeBlocking(const QString &app, const QStringList &args,
73 ✔
318
                               const QString &input, QString *process_out,
319
                               QString *process_err) -> int {
320
  QProcess internal;
73 ✔
321
  return runBlocking(internal, app, args, input, process_out, process_err);
146 ✔
322
}
73 ✔
323

324
/**
325
 * @brief Executor::executeBlocking blocking run on a caller-supplied QProcess
326
 * @param process Process object to run the command on.
327
 * @param app
328
 * @param args
329
 * @param input
330
 * @param process_out
331
 * @param process_err
332
 * @param cancel Optional flag that ends the run when set (see the header).
333
 * @return
334
 */
335
auto Executor::executeBlocking(QProcess &process, const QString &app,
93 ✔
336
                               const QStringList &args, const QString &input,
337
                               QString *process_out, QString *process_err,
338
                               const std::atomic_bool *cancel) -> int {
339
  return runBlocking(process, app, args, input, process_out, process_err,
93 ✔
340
                     cancel);
93 ✔
341
}
342

343
/**
344
 * @brief Executor::executeBlocking blocking version of the executor
345
 * @param app
346
 * @param args
347
 * @param process_out
348
 * @param process_err
349
 * @return
350
 */
351
auto Executor::executeBlocking(const QString &app, const QStringList &args,
49 ✔
352
                               QString *process_out, QString *process_err)
353
    -> int {
354
  return executeBlocking(app, args, QString(), process_out, process_err);
98 ✔
355
}
356

357
/**
358
 * @brief Executor::executeBlocking blocking version with custom environment
359
 * @param env Environment variables to set
360
 * @param app Executable path
361
 * @param args Arguments
362
 * @param process_out Standard output
363
 * @param process_err Standard error
364
 * @return Exit code
365
 */
366
auto Executor::executeBlocking(const QProcessEnvironment &env,
16 ✔
367
                               const QString &app, const QStringList &args,
368
                               QString *process_out, QString *process_err)
369
    -> int {
370
  QProcess process;
16 ✔
371
  process.setProcessEnvironment(env);
16 ✔
372
  return runBlocking(process, app, args, QString(), process_out, process_err);
32 ✔
373
}
16 ✔
374

375
/**
376
 * @brief Executor::setEnvironment set environment variables
377
 * for executor processes
378
 * @param env
379
 */
380
void Executor::setEnvironment(const QProcessEnvironment &env) {
81 ✔
381
  m_process.setProcessEnvironment(env);
81 ✔
382
}
81 ✔
383

384
auto Executor::environment() const -> QProcessEnvironment {
18 ✔
385
  return m_process.processEnvironment();
18 ✔
386
}
387

388
/**
389
 * @brief Executor::cancelNext  cancels execution of first process in queue
390
 *                              if it's not already running
391
 *
392
 * @return  id of the cancelled process or -1 on error
393
 */
394
auto Executor::cancelNext() -> int {
3 ✔
395
  if (running || m_execQueue.isEmpty()) {
3 ✔
396
    return -1; // Return -1 to indicate no process was cancelled
397
               // (queue empty or currently executing).
398
  }
399
  return m_execQueue.dequeue().id;
1 ✔
400
}
401

402
/**
403
 * @brief Executor::onProcessFinished called when an executed process finishes
404
 * @param exitCode
405
 * @param exitStatus
406
 */
407
void Executor::onProcessFinished(int exitCode,
87 ✔
408
                                 QProcess::ExitStatus exitStatus) {
409
  execQueueItem i = m_execQueue.dequeue();
410
  running = false;
87 ✔
411
  auto [output, err] = collectOutput(i, exitCode);
87 ✔
412
  if (exitStatus == QProcess::NormalExit) {
87 ✔
413
    if (exitCode != 0) {
85 ✔
414
      qCDebug(lcQtPass) << i.app << "exited with" << exitCode << err;
2 ✔
415
    }
416
    emit finished(i.id, exitCode, output, err);
85 ✔
417
  } else {
418
    // A signal-killed process usually leaves stderr empty; without this the
419
    // password pane would show nothing at all for the failure.
420
    if (err.trimmed().isEmpty()) {
2 ✔
421
      err = tr("%1 crashed or was killed").arg(i.app);
4 ✔
422
    }
423
    // Qt leaves exitCode undefined after a CrashExit (the signal number on
424
    // Unix), and Pass::finished gates on it: 0 would pass as success and 1
425
    // as grep's "no matches". Report -1, like the failed-to-start path.
426
    emit error(i.id, -1, output, err);
2 ✔
427
  }
428
  executeNext();
87 ✔
429
}
87 ✔
430

431
auto Executor::collectOutput(const execQueueItem &item, int exitCode)
87 ✔
432
    -> std::pair<QString, QString> {
433
  QString output;
87 ✔
434
  QString err;
87 ✔
435
  if (item.readStdout) {
87 ✔
436
    output = decodeAssumingUtf8(m_process.readAllStandardOutput());
166 ✔
437
  }
438
  if (item.readStderr || exitCode != 0) {
87 ✔
439
    err = decodeAssumingUtf8(m_process.readAllStandardError());
150 ✔
440
  }
441
  return {output, err};
87 ✔
442
}
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