1 | /* |
2 | This file is part of the KDE libraries |
3 | |
4 | SPDX-FileCopyrightText: 2007 Oswald Buddenhagen <ossi@kde.org> |
5 | SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org> |
6 | |
7 | SPDX-License-Identifier: LGPL-2.0-or-later |
8 | */ |
9 | |
10 | #include "kcoreaddons_debug.h" |
11 | #include "kprocess_p.h" |
12 | |
13 | #include <QStandardPaths> |
14 | #include <kshell.h> |
15 | #include <qplatformdefs.h> |
16 | #ifdef Q_OS_WIN |
17 | #include <kshell_p.h> |
18 | #include <qt_windows.h> |
19 | #endif |
20 | |
21 | #include <QFile> |
22 | |
23 | ///////////////////////////// |
24 | // public member functions // |
25 | ///////////////////////////// |
26 | |
27 | KProcess::KProcess(QObject *parent) |
28 | : QProcess(parent) |
29 | , d_ptr(new KProcessPrivate(this)) |
30 | { |
31 | setOutputChannelMode(ForwardedChannels); |
32 | } |
33 | |
34 | KProcess::KProcess(KProcessPrivate *d, QObject *parent) |
35 | : QProcess(parent) |
36 | , d_ptr(d) |
37 | { |
38 | d_ptr->q_ptr = this; |
39 | setOutputChannelMode(ForwardedChannels); |
40 | } |
41 | |
42 | KProcess::~KProcess() = default; |
43 | |
44 | void KProcess::setOutputChannelMode(OutputChannelMode mode) |
45 | { |
46 | QProcess::setProcessChannelMode(static_cast<ProcessChannelMode>(mode)); |
47 | } |
48 | |
49 | KProcess::OutputChannelMode KProcess::outputChannelMode() const |
50 | { |
51 | return static_cast<OutputChannelMode>(QProcess::processChannelMode()); |
52 | } |
53 | |
54 | void KProcess::setNextOpenMode(QIODevice::OpenMode mode) |
55 | { |
56 | Q_D(KProcess); |
57 | |
58 | d->openMode = mode; |
59 | } |
60 | |
61 | #define DUMMYENV "_KPROCESS_DUMMY_=" |
62 | |
63 | void KProcess::clearEnvironment() |
64 | { |
65 | setEnvironment(QStringList{QStringLiteral(DUMMYENV)}); |
66 | } |
67 | |
68 | void KProcess::setEnv(const QString &name, const QString &value, bool overwrite) |
69 | { |
70 | QStringList env = environment(); |
71 | if (env.isEmpty()) { |
72 | env = systemEnvironment(); |
73 | env.removeAll(QStringLiteral(DUMMYENV)); |
74 | } |
75 | QString fname(name); |
76 | fname.append(c: QLatin1Char('=')); |
77 | auto it = std::find_if(first: env.begin(), last: env.end(), pred: [&fname](const QString &s) { |
78 | return s.startsWith(s: fname); |
79 | }); |
80 | if (it != env.end()) { |
81 | if (overwrite) { |
82 | *it = fname.append(s: value); |
83 | setEnvironment(env); |
84 | } |
85 | return; |
86 | } |
87 | |
88 | env.append(t: fname.append(s: value)); |
89 | setEnvironment(env); |
90 | } |
91 | |
92 | void KProcess::unsetEnv(const QString &name) |
93 | { |
94 | QStringList env = environment(); |
95 | if (env.isEmpty()) { |
96 | env = systemEnvironment(); |
97 | env.removeAll(QStringLiteral(DUMMYENV)); |
98 | } |
99 | QString fname(name); |
100 | fname.append(c: QLatin1Char('=')); |
101 | |
102 | auto it = std::find_if(first: env.begin(), last: env.end(), pred: [&fname](const QString &s) { |
103 | return s.startsWith(s: fname); |
104 | }); |
105 | if (it != env.end()) { |
106 | env.erase(pos: it); |
107 | if (env.isEmpty()) { |
108 | env.append(QStringLiteral(DUMMYENV)); |
109 | } |
110 | setEnvironment(env); |
111 | } |
112 | } |
113 | |
114 | void KProcess::setProgram(const QString &exe, const QStringList &args) |
115 | { |
116 | QProcess::setProgram(exe); |
117 | QProcess::setArguments(args); |
118 | #ifdef Q_OS_WIN |
119 | setNativeArguments(QString()); |
120 | #endif |
121 | } |
122 | |
123 | void KProcess::setProgram(const QStringList &argv) |
124 | { |
125 | if (argv.isEmpty()) { |
126 | qCWarning(KCOREADDONS_DEBUG) << "KProcess::setProgram(const QStringList &argv) called on an empty string list, no process will be started." ; |
127 | clearProgram(); |
128 | return; |
129 | } |
130 | |
131 | QStringList args = argv; |
132 | QProcess::setProgram(args.takeFirst()); |
133 | QProcess::setArguments(args); |
134 | #ifdef Q_OS_WIN |
135 | setNativeArguments(QString()); |
136 | #endif |
137 | } |
138 | |
139 | KProcess &KProcess::operator<<(const QString &arg) |
140 | { |
141 | if (QProcess::program().isEmpty()) { |
142 | QProcess::setProgram(arg); |
143 | } else { |
144 | setArguments(arguments() << arg); |
145 | } |
146 | return *this; |
147 | } |
148 | |
149 | KProcess &KProcess::operator<<(const QStringList &args) |
150 | { |
151 | if (QProcess::program().isEmpty()) { |
152 | setProgram(args); |
153 | } else { |
154 | setArguments(arguments() << args); |
155 | } |
156 | return *this; |
157 | } |
158 | |
159 | void KProcess::clearProgram() |
160 | { |
161 | QProcess::setProgram({}); |
162 | QProcess::setArguments({}); |
163 | #ifdef Q_OS_WIN |
164 | setNativeArguments(QString()); |
165 | #endif |
166 | } |
167 | |
168 | // #ifdef NON_FREE // ... as they ship non-POSIX /bin/sh |
169 | #if !defined(__linux__) && !defined(__FreeBSD__) && !defined(__NetBSD__) && !defined(__OpenBSD__) && !defined(__DragonFly__) && !defined(__GNU__) \ |
170 | && !defined(__APPLE__) |
171 | const static bool s_nonFreeUnix = true; |
172 | #else |
173 | const static bool s_nonFreeUnix = false; |
174 | #endif |
175 | |
176 | void KProcess::setShellCommand(const QString &cmd) |
177 | { |
178 | KShell::Errors err = KShell::NoError; |
179 | auto args = KShell::splitArgs(cmd, flags: KShell::AbortOnMeta | KShell::TildeExpand, err: &err); |
180 | if (err == KShell::NoError && !args.isEmpty()) { |
181 | QProcess::setProgram(QStandardPaths::findExecutable(executableName: args.takeFirst())); |
182 | if (!QProcess::program().isEmpty()) { |
183 | setArguments(args); |
184 | #ifdef Q_OS_WIN |
185 | setNativeArguments(QString()); |
186 | #endif |
187 | return; |
188 | } |
189 | } |
190 | |
191 | setArguments({}); |
192 | |
193 | #ifdef Q_OS_UNIX |
194 | if (s_nonFreeUnix) { |
195 | // If /bin/sh is a symlink, we can be pretty sure that it points to a |
196 | // POSIX shell - the original bourne shell is about the only non-POSIX |
197 | // shell still in use and it is always installed natively as /bin/sh. |
198 | QString shell = QFile::symLinkTarget(QStringLiteral("/bin/sh" )); |
199 | if (shell.isEmpty()) { |
200 | // Try some known POSIX shells. |
201 | static const char *s_knownShells[] = {"ksh" , "ash" , "bash" , "zsh" }; |
202 | for (const auto str : s_knownShells) { |
203 | shell = QStandardPaths::findExecutable(executableName: QLatin1String(str)); |
204 | if (!shell.isEmpty()) { |
205 | break; |
206 | } |
207 | } |
208 | } |
209 | if (shell.isEmpty()) { // We're pretty much screwed, to be honest ... |
210 | shell = QStringLiteral("/bin/sh" ); |
211 | } |
212 | QProcess::setProgram(shell); |
213 | } else { |
214 | QProcess::setProgram((QStringLiteral("/bin/sh" ))); |
215 | } |
216 | |
217 | setArguments(arguments() << QStringLiteral("-c" ) << cmd); |
218 | #else // Q_OS_UNIX |
219 | // KMacroExpander::expandMacrosShellQuote(), KShell::quoteArg() and |
220 | // KShell::joinArgs() may generate these for security reasons. |
221 | setEnv(PERCENT_VARIABLE, QStringLiteral("%" )); |
222 | |
223 | #ifndef _WIN32_WCE |
224 | WCHAR sysdir[MAX_PATH + 1]; |
225 | UINT size = GetSystemDirectoryW(sysdir, MAX_PATH + 1); |
226 | QProcess::setProgram(QString::fromUtf16((const char16_t *)sysdir, size) + QLatin1String("\\cmd.exe" )); |
227 | setNativeArguments(QLatin1String("/V:OFF /S /C \"" ) + cmd + QLatin1Char('"')); |
228 | #else |
229 | QProcess::setProgram(QStringLiteral("\\windows\\cmd.exe" )); |
230 | setNativeArguments(QStringLiteral("/S /C \"" ) + cmd + QLatin1Char('"')); |
231 | #endif |
232 | #endif |
233 | } |
234 | |
235 | QStringList KProcess::program() const |
236 | { |
237 | QStringList argv = arguments(); |
238 | argv.prepend(t: QProcess::program()); |
239 | return argv; |
240 | } |
241 | |
242 | void KProcess::start() |
243 | { |
244 | Q_D(KProcess); |
245 | |
246 | QProcess::start(mode: d->openMode); |
247 | } |
248 | |
249 | int KProcess::execute(int msecs) |
250 | { |
251 | start(); |
252 | if (!waitForFinished(msecs)) { |
253 | kill(); |
254 | waitForFinished(msecs: -1); |
255 | return -2; |
256 | } |
257 | return (exitStatus() == QProcess::NormalExit) ? exitCode() : -1; |
258 | } |
259 | |
260 | // static |
261 | int KProcess::execute(const QString &exe, const QStringList &args, int msecs) |
262 | { |
263 | KProcess p; |
264 | p.setProgram(exe, args); |
265 | return p.execute(msecs); |
266 | } |
267 | |
268 | // static |
269 | int KProcess::execute(const QStringList &argv, int msecs) |
270 | { |
271 | KProcess p; |
272 | p.setProgram(argv); |
273 | return p.execute(msecs); |
274 | } |
275 | |
276 | int KProcess::startDetached() |
277 | { |
278 | qint64 pid; |
279 | if (!QProcess::startDetached(program: QProcess::program(), arguments: QProcess::arguments(), workingDirectory: workingDirectory(), pid: &pid)) { |
280 | return 0; |
281 | } |
282 | return static_cast<int>(pid); |
283 | } |
284 | |
285 | // static |
286 | int KProcess::startDetached(const QString &exe, const QStringList &args) |
287 | { |
288 | qint64 pid; |
289 | if (!QProcess::startDetached(program: exe, arguments: args, workingDirectory: QString(), pid: &pid)) { |
290 | return 0; |
291 | } |
292 | return static_cast<int>(pid); |
293 | } |
294 | |
295 | // static |
296 | int KProcess::startDetached(const QStringList &argv) |
297 | { |
298 | if (argv.isEmpty()) { |
299 | qCWarning(KCOREADDONS_DEBUG) << "KProcess::startDetached(const QStringList &argv) called on an empty string list, no process will be started." ; |
300 | return 0; |
301 | } |
302 | |
303 | QStringList args = argv; |
304 | QString prog = args.takeFirst(); |
305 | return startDetached(exe: prog, args); |
306 | } |
307 | |
308 | #include "moc_kprocess.cpp" |
309 | |