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

IJHack / QtPass / 35158407238

16 Sep 2026 10:35PM UTC coverage: 72.336% (+0.05%) from 72.284%
35158407238

push

github

web-flow
Initialise new profiles without the active store's backend (#1782)

* Initialise new profiles without the active store's backend (#1774)

initializeNewProfiles() ran UsersDialog::accept() -> Pass::Init() and
GitInit() on the shared backend, whose settings snapshot, PASSWORD_STORE_DIR
and process queue belong to the active store: pass init nested the new
.gpg-id under the active store and git init ran there. Making the store
switch effective on that backend is not safe either (a late pass init
would re-encrypt the active store).

ProfileInit::initialise() now does the work synchronously and only inside
the new directory: writes .gpg-id (enabled keys, owner-only), signs it when
a signing key is configured, and with git runs init/add/commit through a
QProcess of its own. Existing *.gpg files are left alone and the user is
told to re-encrypt from within that profile. UsersDialog gained
setInitOnAccept(false) + selectedUsers() so it can hand the selection back
instead of calling Pass::Init.

Found on the way: a folder without .gpg-id made loadRecipients() call
listKeys() with an empty filter, which lists the whole keyring, so every
key came up pre-selected in the first-run wizard and for new profiles.

Closes #1774.

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

* Profile init: per-profile signing key, WSL-safe gpg path, no OK without a recipient

- initializeNewProfiles() signs the new .gpg-id with the profile's own
  signingKey column, the way the profile switch applies it, not with the
  active store's key.
- ProfileInit::signGpgId() hands gpg a wslpath-translated file name when
  the executable is WSL-routed; Executor::translatePathForWsl() is the
  former ImitatePass helper made static so both can use it.
- UsersDialog: OK is disabled until a key is ticked and accept() refuses an
  empty selection. Now that a new store no longer comes up with the whole
  keyring pre-selec... (continued)

67 of 107 new or added lines in 7 files covered. (62.62%)

11 existing lines in 2 files now uncovered.

5478 of 7573 relevant lines covered (72.34%)

60.52 hits per line

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

96.18
/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
#ifdef QT_DEBUG
10
#include "debughelper.h"
11
#endif
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
/**
22
 * @brief Executor::Executor executes external applications
23
 * @param parent
24
 */
25
Executor::Executor(QObject *parent) : QObject(parent) {
110 ✔
26
  connect(&m_process, &QProcess::finished, this, &Executor::onProcessFinished);
110 ✔
27
  connect(&m_process, &QProcess::started, this, &Executor::starting);
110 ✔
28
}
110 ✔
29

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

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

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

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

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

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

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

148
  if (!i.input.isEmpty()) {
88 ✔
149
    QByteArray data = i.input.toUtf8();
43 ✔
150
    if (m_process.write(data) != data.length()) {
43 ✔
151
#ifdef QT_DEBUG
152
      dbg() << "Not all data written to process:" << i.id << " " << i.app;
153
#endif
154
    }
155
  }
156
  m_process.closeWriteChannel();
88 ✔
157
}
158

159
/**
160
 * @brief Executor::execute execute an app
161
 * @param id
162
 * @param app
163
 * @param args
164
 * @param readStdout
165
 * @param readStderr
166
 */
167
void Executor::execute(int id, const QString &app, const QStringList &args,
19 ✔
168
                       bool readStdout, bool readStderr) {
169
  execute(id, QString(), app, args, QString(), readStdout, readStderr);
38 ✔
170
}
19 ✔
171

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

187
/**
188
 * @brief Executor::execute an app, takes input and presents it as stdin
189
 * @param id
190
 * @param app
191
 * @param args
192
 * @param input
193
 * @param readStdout
194
 * @param readStderr
195
 */
196
void Executor::execute(int id, const QString &app, const QStringList &args,
1 ✔
197
                       QString input, bool readStdout, bool readStderr) {
198
  execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
2 ✔
199
}
1 ✔
200

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

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

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

328
auto Executor::executeBlocking(const QString &app, const QStringList &args,
73 ✔
329
                               const QString &input, QString *process_out,
330
                               QString *process_err) -> int {
331
  QProcess internal;
73 ✔
332
  return runBlocking(internal, app, args, input, process_out, process_err);
146 ✔
333
}
73 ✔
334

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

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

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

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

395
auto Executor::environment() const -> QProcessEnvironment {
18 ✔
396
  return m_process.processEnvironment();
18 ✔
397
}
398

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

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

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