4#include <QCoreApplication>
5#include <QDeadlineTimer>
8#include <QStringDecoder>
21std::atomic_int g_childUmask{-1};
23constexpr int kBlockingCancelPollMs = 100;
26constexpr int kBlockingKillGraceMs = 1000;
35#if defined(Q_OS_UNIX) && QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
57 QProcess::UnixProcessParameters params = process.unixProcessParameters();
58 params.flags |= QProcess::UnixProcessFlag::CreateNewSession;
59 process.setUnixProcessParameters(params);
70 const QStringList &args) {
74 if (
const int mask = g_childUmask.load(); mask >= 0) {
75 process.setChildProcessModifier(
76 [mask] { ::umask(
static_cast<mode_t
>(mask)); });
80 process.start(wsl->launcher, wsl->argv(args));
87 if (app.isEmpty() || QDir::isAbsolutePath(app)) {
90 const QDir appDir(QCoreApplication::applicationDirPath());
91 QStringList candidates{QDir::cleanPath(appDir.absoluteFilePath(app))};
93 candidates << QDir::cleanPath(
94 appDir.absoluteFilePath(app + QStringLiteral(
".exe")));
96 for (
const QString &candidate : std::as_const(candidates)) {
98 const QFileInfo info(candidate);
99 if (info.isFile() && info.isExecutable()) {
107 return options + QStringList{QStringLiteral(
"--exec"),
command} + args;
115 -> std::optional<WslCommand> {
116 const QStringList parts = QProcess::splitCommand(app);
117 if (parts.size() < 2) {
122 const QString &launcher = parts.first();
123 const qsizetype separator = std::max(launcher.lastIndexOf(QLatin1Char(
'/')),
124 launcher.lastIndexOf(QLatin1Char(
'\\')));
125 const QString launcherName = launcher.mid(separator + 1);
127 launcherName.compare(QLatin1String(
"wsl"), Qt::CaseInsensitive) == 0 ||
128 launcherName.compare(QLatin1String(
"wsl.exe"), Qt::CaseInsensitive) == 0;
136 wsl.
launcher = separator < 0 ? QStringLiteral(
"wsl") : launcher;
144 static const QStringList valued{
145 QStringLiteral(
"-d"), QStringLiteral(
"--distribution"),
146 QStringLiteral(
"-u"), QStringLiteral(
"--user"),
147 QStringLiteral(
"--cd"), QStringLiteral(
"--shell-type")};
149 for (; i < parts.size() && parts.at(i).startsWith(QLatin1Char(
'-')); ++i) {
150 const QString &option = parts.at(i);
151 if (option == QLatin1String(
"-e") || option == QLatin1String(
"--exec")) {
154 if (valued.contains(option)) {
155 if (i + 1 >= parts.size()) {
158 wsl.
options << option << parts.at(++i);
165 if (i != parts.size() - 1) {
179 QString normalizedPath = QDir::cleanPath(path);
182 return normalizedPath;
185 const auto wslpath = wsl->with(QStringLiteral(
"wslpath"));
187 wslpath.argv({normalizedPath}), &wslPath);
188 const QString translated = wslPath.trimmed();
189 return (rc == 0 && !translated.isEmpty()) ? translated : normalizedPath;
205 if (i.
app.isEmpty()) {
206 qCDebug(lcQtPass) <<
"No executable set for:" << i.
id;
207 const int failedId = i.
id;
211 QMetaObject::invokeMethod(
214 emit
error(failedId, -1, QString(),
215 tr(
"No executable configured for this command"));
219 Qt::QueuedConnection);
234 qCDebug(lcQtPass) <<
"Process failed to start:" << i.
id <<
" " << i.
app;
235 const int failedId = i.
id;
236 const QString failedApp = i.
app;
241 QMetaObject::invokeMethod(
243 [
this, failedId, failedApp]() {
244 emit
error(failedId, -1, QString(),
245 tr(
"Failed to start %1").arg(failedApp));
249 Qt::QueuedConnection);
254 if (!i.
input.isEmpty()) {
255 QByteArray data = i.
input.toUtf8();
256 if (
m_process.write(data) != data.length()) {
257 qCDebug(lcQtPass) <<
"Not all data written to process:" << i.
id <<
" "
265 bool readStdout,
bool readStderr) {
266 execute(
id, QString(), app, args, QString(), readStdout, readStderr);
270 const QStringList &args,
bool readStdout,
272 execute(
id, workDir, app, args, QString(), readStdout, readStderr);
276 QString input,
bool readStdout,
bool readStderr) {
277 execute(
id, QString(), app, args, std::move(input), readStdout, readStderr);
281 const QStringList &args, QString input,
bool readStdout,
286 {id, app, args, std::move(input), readStdout, readStderr, workDir});
291static auto decodeAssumingUtf8(
const QByteArray &in) -> QString {
295 QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
296 QString out = converter(in);
297 if (!converter.hasError()) {
301 QStringDecoder(QStringDecoder::System, QStringDecoder::Flag::Stateless);
315void stopBlocking(QProcess &process) {
317 if (process.waitForFinished(kBlockingKillGraceMs)) {
325 if (!process.waitForFinished(kBlockingKillGraceMs)) {
326 qCWarning(lcQtPass) <<
"Process still running after kill():"
327 << process.program();
335auto msecsLeft(
const QDeadlineTimer &deadline) ->
int {
336 if (deadline.isForever()) {
339 return static_cast<int>(
340 std::clamp<qint64>(deadline.remainingTime(), 0, INT_MAX));
352auto waitOrCancel(QProcess &process,
const std::atomic_bool *cancel,
353 const QDeadlineTimer &deadline) ->
bool {
354 if (cancel ==
nullptr) {
358 if (!process.waitForFinished(msecsLeft(deadline)) &&
359 process.state() != QProcess::NotRunning) {
360 stopBlocking(process);
365 while (!process.waitForFinished(
367 ? kBlockingCancelPollMs
368 : qMin(kBlockingCancelPollMs, msecsLeft(deadline)))) {
369 if (process.state() == QProcess::NotRunning) {
372 if (cancel->load() || deadline.hasExpired()) {
373 stopBlocking(process);
383 const QStringList &args,
const QString &input,
384 QString *process_out, QString *process_err,
385 const std::atomic_bool *cancel,
int timeoutMs)
387 if (cancel !=
nullptr && cancel->load())
389 const QDeadlineTimer deadline = timeoutMs < 0
390 ? QDeadlineTimer(QDeadlineTimer::Forever)
391 : QDeadlineTimer(timeoutMs);
393 if (!process.waitForStarted(msecsLeft(deadline))) {
394 qCDebug(lcQtPass) <<
"Process failed to start:" << app;
395 if (process.state() != QProcess::NotRunning) {
396 stopBlocking(process);
400 if (!input.isEmpty()) {
401 QByteArray data = input.toUtf8();
402 if (process.write(data) != data.length()) {
403 qCDebug(lcQtPass) <<
"Not all input written:" << app;
407 process.closeWriteChannel();
408 if (!waitOrCancel(process, cancel, deadline)) {
413 if (process_out !=
nullptr) {
414 *process_out = decodeAssumingUtf8(process.readAllStandardOutput());
416 if (process_err !=
nullptr) {
417 *process_err = decodeAssumingUtf8(process.readAllStandardError());
419 return process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
423 const QString &input, QString *process_out,
424 QString *process_err) ->
int {
426 return runBlocking(internal, app, args, input, process_out, process_err);
430 const QStringList &args,
const QString &input,
431 QString *process_out, QString *process_err,
432 const std::atomic_bool *cancel,
int timeoutMs)
434 return runBlocking(process, app, args, input, process_out, process_err,
439 QString *process_out, QString *process_err)
441 return executeBlocking(app, args, QString(), process_out, process_err);
445 const QString &app,
const QStringList &args,
446 QString *process_out, QString *process_err)
449 process.setProcessEnvironment(env);
450 return runBlocking(process, app, args, QString(), process_out, process_err);
479 QProcess::ExitStatus exitStatus) {
483 if (exitStatus == QProcess::NormalExit) {
485 qCDebug(lcQtPass) << i.
app <<
"exited with" << exitCode << err;
491 if (err.trimmed().isEmpty()) {
492 err = tr(
"%1 crashed or was killed").arg(i.
app);
497 emit
error(i.
id, -1, output, err);
511 QMetaObject::invokeMethod(
519 Qt::QueuedConnection);
523 -> std::pair<QString, QString> {
526 if (item.readStdout) {
527 output = decodeAssumingUtf8(
m_process.readAllStandardOutput());
529 if (item.readStderr || exitCode != 0) {
530 err = decodeAssumingUtf8(
m_process.readAllStandardError());
532 return {output, err};
void dequeuing(int id, bool completes)
Emitted, synchronously, when the queue's head leaves it: just before it is started,...
QQueue< ExecQueueItem > m_execQueue
Commands waiting to run, head first. The head stays queued while it runs and is dequeued in onProcess...
auto isIdle() const -> bool
Whether no command is running and nothing is queued.
static auto childUmask() -> int
The umask setChildUmask() set.
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
static auto translatePathForWsl(const QString &path, const QString &exe) -> QString
Translate a native path for a WSL-routed executable.
auto environment() const -> QProcessEnvironment
Return the environment passed to child processes.
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
Finish the running item when its process exits.
int m_pendingErrors
error()s of failed starts posted but not delivered yet.
auto collectOutput(const ExecQueueItem &item, int exitCode) -> std::pair< QString, QString >
Read what a finished process produced, according to the queue item's readStdout/readStderr flags; std...
bool m_drainCheckPosted
A drained() check is posted and not run yet.
static void startProcess(QProcess &process, const QString &app, const QStringList &args)
Start process running app, handling the "wsl " prefix once for both the queued and the blocking path.
void execute(int id, const QString &app, const QStringList &args, bool readStdout, bool readStderr=true)
Queue a command without stdin input.
static auto resolveExecutable(const QString &app) -> QString
The one rule for turning a configured executable into what runs.
void drained()
Emitted once the last queued item is done and nothing runs or waits any more (isIdle()),...
void error(int id, int exitCode, const QString &output, const QString &errout)
Emitted instead of finished() when a queued process could not be started, had no executable configure...
static auto wslExecArgs(const QString &command, const QStringList &args) -> QStringList
Build the wsl.exe argument list that runs command directly.
static void setChildUmask(int mask)
The umask every child QtPass starts gets, as pass sets its own.
Executor(QObject *parent=nullptr)
Construct an Executor with an optional parent QObject.
bool running
Whether the head of the queue has been started (or is being dequeued, see dequeuing()) and has not fi...
void finished(int id, int exitCode, const QString &output, const QString &errout)
Emitted when a queued process exits normally, whatever its exit code; a non-zero code is the receiver...
void starting()
Emitted just before a queued process is started; the main window shows the busy indicator on it.
void setEnvironment(const QProcessEnvironment &env)
Set the environment passed to all child processes.
static auto runBlocking(QProcess &process, const QString &app, const QStringList &args, const QString &input, QString *process_out, QString *process_err, const std::atomic_bool *cancel=nullptr, int timeoutMs=-1) -> int
Shared blocking run: start process, optionally feed input on stdin, wait, and capture stdout/stderr....
static void detachFromTerminal(QProcess &process)
Make process start its child without a controlling terminal.
void executeNext()
Start the head of the queue unless something runs or the queue is empty.
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
auto cancelNext() -> int
Cancel the next queued command without executing it.
QProcess m_process
The one process object; every queued command reuses it.
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
The "qtpass" logging category.
Execution queue item for non-interactive ordered execution.
QString workingDir
Working directory to start in; empty for the current one.
QString input
Data written to the process's stdin, then closed.
int id
Identifier the caller gave; echoed in finished()/error().
QStringList args
Arguments for the executable.
QString app
Executable as configured; resolved when started.
A configured executable that runs a Linux program through WSL.
auto argv(const QStringList &args) const -> QStringList
Arguments for launcher that run command with args.
QString command
The Linux program to run, looked up on the WSL PATH.
QString launcher
What to start.
QStringList options
wsl.exe options that go before the program (-d Debian, -u me), without any -e/--exec: argv() always a...
auto with(const QString &program) const -> WslCommand
The same launcher and options running another program.