Line data Source code
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 : #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
7 : #include <QTextCodec>
8 : #endif
9 : #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
10 : #include <QStringDecoder>
11 : #include <utility>
12 : #endif
13 :
14 : #ifdef QT_DEBUG
15 : #include "debughelper.h"
16 : #endif
17 :
18 : /**
19 : * @brief Executor::Executor executes external applications
20 : * @param parent
21 : */
22 78 : Executor::Executor(QObject *parent) : QObject(parent), running(false) {
23 78 : connect(&m_process,
24 : static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(
25 : &QProcess::finished),
26 : this,
27 78 : static_cast<void (Executor::*)(int, QProcess::ExitStatus)>(
28 : &Executor::finished));
29 78 : connect(&m_process, &QProcess::started, this, &Executor::starting);
30 78 : }
31 :
32 : /**
33 : * @brief Executor::executeNext consumes executable tasks from the queue
34 : */
35 0 : void Executor::executeNext() {
36 0 : if (!running) {
37 0 : if (!m_execQueue.isEmpty()) {
38 : const execQueueItem &i = m_execQueue.head();
39 0 : running = true;
40 0 : if (!i.workingDir.isEmpty()) {
41 0 : m_process.setWorkingDirectory(i.workingDir);
42 : }
43 0 : if (i.app.startsWith("wsl ")) {
44 : QStringList tmp = i.args;
45 : QString app = i.app;
46 0 : tmp.prepend(app.remove(0, 4));
47 0 : m_process.start("wsl", tmp);
48 : } else {
49 0 : m_process.start(i.app, i.args);
50 : }
51 0 : if (!i.input.isEmpty()) {
52 0 : m_process.waitForStarted(-1);
53 0 : QByteArray data = i.input.toUtf8();
54 0 : if (m_process.write(data) != data.length()) {
55 : #ifdef QT_DEBUG
56 : dbg() << "Not all data written to process:" << i.id << " " << i.app;
57 : #endif
58 : }
59 : }
60 0 : m_process.closeWriteChannel();
61 : }
62 : }
63 0 : }
64 :
65 : /**
66 : * @brief Executor::execute execute an app
67 : * @param id
68 : * @param app
69 : * @param args
70 : * @param readStdout
71 : * @param readStderr
72 : */
73 0 : void Executor::execute(int id, const QString &app, const QStringList &args,
74 : bool readStdout, bool readStderr) {
75 0 : execute(id, QString(), app, args, QString(), readStdout, readStderr);
76 0 : }
77 :
78 : /**
79 : * @brief Executor::execute executes an app from a workDir
80 : * @param id
81 : * @param workDir
82 : * @param app
83 : * @param args
84 : * @param readStdout
85 : * @param readStderr
86 : */
87 0 : void Executor::execute(int id, const QString &workDir, const QString &app,
88 : const QStringList &args, bool readStdout,
89 : bool readStderr) {
90 0 : execute(id, workDir, app, args, QString(), readStdout, readStderr);
91 0 : }
92 :
93 : /**
94 : * @brief Executor::execute an app, takes input and presents it as stdin
95 : * @param id
96 : * @param app
97 : * @param args
98 : * @param input
99 : * @param readStdout
100 : * @param readStderr
101 : */
102 0 : void Executor::execute(int id, const QString &app, const QStringList &args,
103 : QString input, bool readStdout, bool readStderr) {
104 0 : execute(id, QString(), app, args, std::move(input), readStdout, readStderr);
105 0 : }
106 :
107 : /**
108 : * @brief Executor::execute executes an app from a workDir, takes input and
109 : * presents it as stdin
110 : * @param id
111 : * @param workDir
112 : * @param app
113 : * @param args
114 : * @param input
115 : * @param readStdout
116 : * @param readStderr
117 : */
118 0 : void Executor::execute(int id, const QString &workDir, const QString &app,
119 : const QStringList &args, QString input, bool readStdout,
120 : bool readStderr) {
121 : // Happens a lot if e.g. git binary is not set.
122 : // This will result in bogus "QProcess::FailedToStart" messages,
123 : // also hiding legitimate errors from the gpg commands.
124 0 : if (app.isEmpty()) {
125 : #ifdef QT_DEBUG
126 : dbg() << "Trying to execute nothing...";
127 : #endif
128 0 : return;
129 : }
130 : QString appPath = app;
131 0 : if (!appPath.startsWith("wsl ")) {
132 : appPath =
133 0 : QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(app);
134 : }
135 0 : m_execQueue.push_back(
136 : {id, appPath, args, std::move(input), readStdout, readStderr, workDir});
137 0 : executeNext();
138 0 : }
139 :
140 : /**
141 : * @brief decodes the input into a string assuming UTF-8 encoding.
142 : * If this fails (which is likely if it is not actually UTF-8)
143 : * it will then fall back to Qt's decoding function, which
144 : * will try based on BOM and if that fails fall back to local encoding.
145 : * This should not be needed in Qt6
146 : *
147 : * @param in input data
148 : * @return Input bytes decoded to string
149 : */
150 182 : static auto decodeAssumingUtf8(const QByteArray &in) -> QString {
151 : #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
152 : QTextCodec *codec = QTextCodec::codecForName("UTF-8");
153 : QTextCodec::ConverterState state;
154 : QString out = codec->toUnicode(in.constData(), in.size(), &state);
155 : if (!state.invalidChars)
156 : return out;
157 : codec = QTextCodec::codecForUtfText(in);
158 : return codec->toUnicode(in);
159 : #else
160 182 : auto converter = QStringDecoder(QStringDecoder::Utf8);
161 182 : return converter(in);
162 : #endif
163 : }
164 :
165 : /**
166 : * @brief Executor::executeBlocking blocking version of the executor,
167 : * takes input and presents it as stdin
168 : * @param app
169 : * @param args
170 : * @param input
171 : * @param process_out
172 : * @param process_err
173 : * @return
174 : *
175 : * TODO(bezet): it might make sense to throw here, a lot of possible errors
176 : */
177 91 : auto Executor::executeBlocking(QString app, const QStringList &args,
178 : const QString &input, QString *process_out,
179 : QString *process_err) -> int {
180 91 : QProcess internal;
181 182 : if (app.startsWith("wsl ")) {
182 : QStringList tmp = args;
183 0 : tmp.prepend(app.remove(0, 4));
184 0 : internal.start("wsl", tmp);
185 : } else {
186 91 : internal.start(app, args);
187 : }
188 91 : if (!input.isEmpty()) {
189 : QByteArray data = input.toUtf8();
190 0 : internal.waitForStarted(-1);
191 0 : if (internal.write(data) != data.length()) {
192 : #ifdef QT_DEBUG
193 : dbg() << "Not all input written:" << app;
194 : #endif
195 : }
196 0 : internal.closeWriteChannel();
197 : }
198 91 : internal.waitForFinished(-1);
199 91 : if (internal.exitStatus() == QProcess::NormalExit) {
200 91 : QString pout = decodeAssumingUtf8(internal.readAllStandardOutput());
201 91 : QString perr = decodeAssumingUtf8(internal.readAllStandardError());
202 91 : if (process_out != nullptr) {
203 91 : *process_out = pout;
204 : }
205 91 : if (process_err != nullptr) {
206 26 : *process_err = perr;
207 : }
208 91 : return internal.exitCode();
209 : }
210 : // TODO(bezet): emit error() ?
211 : return -1; // QProcess error code + qDebug error?
212 91 : }
213 :
214 : /**
215 : * @brief Executor::executeBlocking blocking version of the executor
216 : * @param app
217 : * @param args
218 : * @param process_out
219 : * @param process_err
220 : * @return
221 : */
222 0 : auto Executor::executeBlocking(QString app, const QStringList &args,
223 : QString *process_out, QString *process_err)
224 : -> int {
225 0 : return executeBlocking(std::move(app), args, QString(), process_out,
226 0 : process_err);
227 : }
228 :
229 : /**
230 : * @brief Executor::setEnvironment set environment variables
231 : * for executor processes
232 : * @param env
233 : */
234 0 : void Executor::setEnvironment(const QStringList &env) {
235 0 : m_process.setEnvironment(env);
236 0 : }
237 :
238 : /**
239 : * @brief Executor::cancelNext cancels execution of first process in queue
240 : * if it's not already running
241 : *
242 : * @return id of the cancelled process or -1 on error
243 : */
244 0 : auto Executor::cancelNext() -> int {
245 0 : if (running || m_execQueue.isEmpty()) {
246 0 : return -1; // TODO(bezet): definitely throw here
247 : }
248 0 : return m_execQueue.dequeue().id;
249 : }
250 :
251 : /**
252 : * @brief Executor::finished called when an executed process finishes
253 : * @param exitCode
254 : * @param exitStatus
255 : */
256 0 : void Executor::finished(int exitCode, QProcess::ExitStatus exitStatus) {
257 : execQueueItem i = m_execQueue.dequeue();
258 0 : running = false;
259 0 : if (exitStatus == QProcess::NormalExit) {
260 0 : QString output;
261 0 : QString err;
262 0 : if (i.readStdout) {
263 0 : output = decodeAssumingUtf8(m_process.readAllStandardOutput());
264 : }
265 0 : if (i.readStderr || exitCode != 0) {
266 0 : err = decodeAssumingUtf8(m_process.readAllStandardError());
267 : if (exitCode != 0) {
268 : #ifdef QT_DEBUG
269 : dbg() << exitCode << err;
270 : #endif
271 : }
272 : }
273 0 : emit finished(i.id, exitCode, output, err);
274 : }
275 : // else: emit crashed with ID, which may give a chance to recover ?
276 0 : executeNext();
277 0 : }
|