Changeset View
Changeset View
Standalone View
Standalone View
src/settings/services/servicemenuinstaller/servicemenuinstaller.cpp
- This file was added.
1 | /*************************************************************************** | ||||
---|---|---|---|---|---|
2 | * Copyright © 2019 Alexander Potashev <aspotashev@gmail.com> * | ||||
3 | * * | ||||
4 | * This program is free software; you can redistribute it and/or * | ||||
5 | * modify it under the terms of the GNU General Public License as * | ||||
6 | * published by the Free Software Foundation; either version 2 of * | ||||
7 | * the License or (at your option) version 3 or any later version * | ||||
8 | * accepted by the membership of KDE e.V. (or its successor approved * | ||||
9 | * by the membership of KDE e.V.), which shall act as a proxy * | ||||
10 | * defined in Section 14 of version 3 of the license. * | ||||
11 | * * | ||||
12 | * This program is distributed in the hope that it will be useful, * | ||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * | ||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | ||||
15 | * GNU General Public License for more details. * | ||||
16 | * * | ||||
17 | * You should have received a copy of the GNU General Public License * | ||||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. * | ||||
19 | ***************************************************************************/ | ||||
20 | | ||||
21 | #include <QDebug> | ||||
22 | #include <QProcess> | ||||
23 | #include <QStandardPaths> | ||||
24 | #include <QDir> | ||||
25 | #include <QDirIterator> | ||||
26 | #include <QCommandLineParser> | ||||
27 | | ||||
28 | #include <KLocalizedString> | ||||
29 | | ||||
30 | // @param msg Error that gets logged to CLI | ||||
31 | Q_NORETURN void fail(const QString &str) | ||||
32 | { | ||||
elvisangelaccio: How about `Q_NORETURN` ? | |||||
33 | qCritical() << str; | ||||
34 | | ||||
35 | QProcess process; | ||||
36 | auto args = QStringList{"--passivepopup", i18n("Dolphin service menu installation failed"), "15"}; | ||||
37 | process.start("kdialog", args, QIODevice::ReadOnly); | ||||
38 | if (!process.waitForStarted()) { | ||||
39 | qFatal("Failed to run kdialog"); | ||||
40 | } | ||||
41 | | ||||
42 | exit(1); | ||||
43 | } | ||||
Since we are in a Qt program now, we could just open a KMessageBox rather than spawn another process just to show an error dialog (and kdialog might not even be installed). elvisangelaccio: Since we are in a Qt program now, we could just open a KMessageBox rather than spawn another… | |||||
I checked the sources of kdialog and discovered that what "kdialog --passivepopup" does is not trivial, see https://cgit.kde.org/kdialog.git/tree/src/kdialog.cpp#n517 Firstly it tries to show a Plasma/shell notification over D-Bus. If unavailable, it falls back to KPassivePopup. In the Beautiful KNewStuff of the Future, we could pass the error message back to the "Get New Stuff" dialog and display it there as an inline notification. One of the ways to implement this would be a plugin system where Dolphin's servicemenuinstaller is a plugin (.so/.dll). aspotashev: I checked the sources of kdialog and discovered that what "kdialog --passivepopup" does is not… | |||||
elvisangelaccio: I see, thanks for investigating it. | |||||
44 | | ||||
45 | bool evaluateShell(const QString &program, const QStringList &arguments, QString &output, QString &errorText) | ||||
46 | { | ||||
47 | QProcess process; | ||||
48 | process.start(program, arguments, QIODevice::ReadOnly); | ||||
49 | if (!process.waitForStarted()) { | ||||
50 | fail(i18n("Failed to run process: %1 %2", program, arguments.join(" "))); | ||||
51 | } | ||||
52 | | ||||
53 | if (!process.waitForFinished()) { | ||||
54 | fail(i18n("Process did not finish in reasonable time: %1 %2", program, arguments.join(" "))); | ||||
55 | } | ||||
56 | | ||||
57 | const auto stdoutResult = QString::fromUtf8(process.readAllStandardOutput()).trimmed(); | ||||
58 | const auto stderrResult = QString::fromUtf8(process.readAllStandardError()).trimmed(); | ||||
59 | | ||||
60 | if (process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0) { | ||||
61 | output = stdoutResult; | ||||
62 | return true; | ||||
63 | } else { | ||||
64 | errorText = stderrResult + stdoutResult; | ||||
65 | return false; | ||||
66 | } | ||||
67 | } | ||||
68 | | ||||
69 | QString mimeType(const QString &path) | ||||
70 | { | ||||
71 | QString result; | ||||
72 | QString errorText; | ||||
73 | if (evaluateShell("xdg-mime", QStringList{"query", "filetype", path}, result, errorText)) { | ||||
74 | return result; | ||||
75 | } else { | ||||
76 | fail(i18n("Failed to run xdg-mime %1: %2", path, errorText)); | ||||
77 | } | ||||
78 | } | ||||
elvisangelaccio: Same here. We should use `QMimeDatabase` rather than run `xdg-mime`. | |||||
79 | | ||||
80 | QString getServiceMenusDir() | ||||
81 | { | ||||
82 | const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); | ||||
83 | return QDir(dataLocation).absoluteFilePath("kservices5/ServiceMenus"); | ||||
84 | } | ||||
85 | | ||||
86 | struct UncompressCommand | ||||
87 | { | ||||
88 | QString command; | ||||
89 | QStringList args1; | ||||
90 | QStringList args2; | ||||
91 | }; | ||||
92 | | ||||
93 | void runUncompress(const QString &inputPath, const QString &outputPath) { | ||||
94 | QList<QPair<QStringList, UncompressCommand>> mimeTypeToCommand; | ||||
95 | mimeTypeToCommand.append({QStringList{"application/x-tar", "application/tar", "application/x-gtar", | ||||
96 | "multipart/x-tar"}, | ||||
97 | UncompressCommand{"tar", QStringList() << "-xf", QStringList() << "-C"}}); | ||||
98 | mimeTypeToCommand.append({QStringList{"application/x-gzip", "application/gzip", | ||||
99 | "application/x-gzip-compressed-tar", "application/gzip-compressed-tar", | ||||
100 | "application/x-gzip-compressed", "application/gzip-compressed", | ||||
101 | "application/tgz", "application/x-compressed-tar", | ||||
102 | "application/x-compressed-gtar", "file/tgz", | ||||
103 | "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped", | ||||
104 | "gzip/document"}, | ||||
105 | UncompressCommand{"tar", QStringList{"-zxf"}, QStringList{"-C"}}}); | ||||
106 | mimeTypeToCommand.append({QStringList{"application/bzip", "application/bzip2", "application/x-bzip", | ||||
107 | "application/x-bzip2", "application/bzip-compressed", | ||||
108 | "application/bzip2-compressed", "application/x-bzip-compressed", | ||||
109 | "application/x-bzip2-compressed", "application/bzip-compressed-tar", | ||||
110 | "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar", | ||||
111 | "application/x-bzip2-compressed-tar", "application/x-bz2"}, | ||||
112 | UncompressCommand{"tar", QStringList{"-jxf"}, QStringList{"-C"}}}); | ||||
113 | mimeTypeToCommand.append({QStringList{"application/zip", "application/x-zip", "application/x-zip-compressed", | ||||
114 | "multipart/x-zip"}, | ||||
115 | UncompressCommand{"unzip", QStringList{}, QStringList{"-d"}}}); | ||||
116 | | ||||
117 | const auto mime = mimeType(inputPath); | ||||
118 | | ||||
119 | UncompressCommand command{}; | ||||
120 | for (const auto &pair : mimeTypeToCommand) { | ||||
121 | if (pair.first.contains(mime)) { | ||||
122 | command = pair.second; | ||||
123 | break; | ||||
124 | } | ||||
125 | } | ||||
126 | | ||||
127 | if (command.command.isEmpty()) { | ||||
128 | fail(i18n("Unsupported archive type %1: %2", mime, inputPath)); | ||||
129 | } | ||||
130 | | ||||
131 | QProcess process; | ||||
132 | process.start( | ||||
133 | command.command, | ||||
134 | QStringList() << command.args1 << inputPath << command.args2 << outputPath, | ||||
135 | QIODevice::NotOpen); | ||||
136 | if (!process.waitForStarted()) { | ||||
137 | fail(i18n("Failed to run uncompressor command for %1", inputPath)); | ||||
138 | } | ||||
139 | | ||||
140 | if (!process.waitForFinished()) { | ||||
141 | fail( | ||||
142 | i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" "))); | ||||
143 | } | ||||
144 | | ||||
145 | if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { | ||||
146 | fail(i18n("Failed to uncompress %1", inputPath)); | ||||
147 | } | ||||
148 | } | ||||
149 | | ||||
150 | QString findRecursive(const QString &dir, const QString &basename) | ||||
151 | { | ||||
152 | QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories); | ||||
153 | while (it.hasNext()) { | ||||
154 | return QFileInfo(it.next()).canonicalFilePath(); | ||||
155 | } | ||||
156 | | ||||
157 | return QString(); | ||||
158 | } | ||||
159 | | ||||
160 | bool runInstallerScriptOnce(const QString &path, const QStringList &args, const QString &dir) | ||||
161 | { | ||||
162 | QProcess process; | ||||
163 | process.setWorkingDirectory(dir); | ||||
164 | process.start(path, args, QIODevice::NotOpen); | ||||
165 | if (!process.waitForStarted()) { | ||||
166 | fail(i18n("Failed to run installer script %1", path)); | ||||
167 | } | ||||
168 | | ||||
169 | // Wait until installer exits, without timeout | ||||
170 | if (!process.waitForFinished(-1)) { | ||||
171 | qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" "); | ||||
172 | return false; | ||||
173 | } | ||||
174 | | ||||
175 | if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { | ||||
176 | qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" "); | ||||
177 | return false; | ||||
178 | } | ||||
179 | | ||||
180 | return true; | ||||
181 | } | ||||
182 | | ||||
183 | // If hasArgVariants is true, run "path". | ||||
184 | // If hasArgVariants is false, run "path argVariants[i]" until successful. | ||||
185 | bool runInstallerScript(const QString &path, bool hasArgVariants, const QStringList &argVariants, const QString &dir, | ||||
186 | QString &errorText) | ||||
187 | { | ||||
188 | QFile file(path); | ||||
189 | if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) { | ||||
190 | errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString()); | ||||
191 | return false; | ||||
192 | } | ||||
193 | | ||||
194 | qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path; | ||||
195 | if (hasArgVariants) { | ||||
196 | for (const auto &arg : argVariants) { | ||||
197 | if (runInstallerScriptOnce(path, QStringList{arg}, dir)) { | ||||
198 | return true; | ||||
199 | } | ||||
200 | } | ||||
201 | } else { | ||||
202 | if (runInstallerScriptOnce(path, QStringList{}, dir)) { | ||||
203 | return true; | ||||
204 | } | ||||
205 | } | ||||
206 | | ||||
207 | errorText = i18nc( | ||||
208 | "%1 = comma separated list of arguments", | ||||
209 | "Installer script %1 failed, tried arguments \"%1\".", path, argVariants.join(i18nc("Separator between arguments", "\", \""))); | ||||
210 | return false; | ||||
211 | } | ||||
212 | | ||||
213 | QString generateDirPath(const QString &archive) | ||||
214 | { | ||||
215 | return QStringLiteral("%1-dir").arg(archive); | ||||
216 | } | ||||
217 | | ||||
218 | bool cmdInstall(const QString &archive, QString &errorText) | ||||
219 | { | ||||
220 | const auto serviceDir = getServiceMenusDir(); | ||||
221 | if (!QDir().mkpath(serviceDir)) { | ||||
222 | // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483 | ||||
223 | errorText = i18n("Failed to create path %1", serviceDir); | ||||
224 | return false; | ||||
225 | } | ||||
226 | | ||||
227 | if (archive.endsWith(".desktop")) { | ||||
228 | // Append basename to destination directory | ||||
229 | const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName()); | ||||
230 | qInfo() << "Single-File Service-Menu" << archive << dest; | ||||
231 | | ||||
232 | QFile source(archive); | ||||
233 | if (!source.copy(dest)) { | ||||
234 | errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString()); | ||||
235 | return false; | ||||
236 | } | ||||
237 | } else { | ||||
238 | const QString dir = generateDirPath(archive); | ||||
239 | if (QFile::exists(dir)) { | ||||
240 | if (!QDir(dir).removeRecursively()) { | ||||
241 | errorText = i18n("Failed to remove directory %1", dir); | ||||
242 | return false; | ||||
243 | } | ||||
244 | } | ||||
245 | | ||||
246 | if (QDir().mkdir(dir)) { | ||||
247 | errorText = i18n("Failed to create directory %1", dir); | ||||
248 | } | ||||
249 | | ||||
250 | runUncompress(archive, dir); | ||||
251 | | ||||
252 | // Try "install-it" first | ||||
253 | QString installItPath; | ||||
254 | const auto basenames1 = QStringList{"install-it.sh", "install-it"}; | ||||
255 | for (const auto &basename : qAsConst(basenames1)) { | ||||
elvisangelaccio: This will detach the container, please use qAsConst | |||||
256 | const auto path = findRecursive(dir, basename); | ||||
257 | if (!path.isEmpty()) { | ||||
258 | installItPath = path; | ||||
259 | break; | ||||
260 | } | ||||
261 | } | ||||
262 | | ||||
263 | if (!installItPath.isEmpty()) { | ||||
264 | return runInstallerScript(installItPath, false, QStringList{}, dir, errorText); | ||||
265 | } | ||||
266 | | ||||
267 | // If "install-it" is missing, try "install" | ||||
268 | QString installerPath; | ||||
269 | const auto basenames2 = QStringList{"installKDE4.sh", "installKDE4", "install.sh", "install"}; | ||||
elvisangelaccio: This will detach the container, please use qAsConst | |||||
270 | for (const auto &basename : qAsConst(basenames2)) { | ||||
271 | const auto path = findRecursive(dir, basename); | ||||
272 | if (!path.isEmpty()) { | ||||
273 | installerPath = path; | ||||
274 | break; | ||||
275 | } | ||||
276 | } | ||||
277 | | ||||
278 | if (!installerPath.isEmpty()) { | ||||
279 | return runInstallerScript(installerPath, true, QStringList{"--local", "--local-install", "--install"}, dir, errorText); | ||||
280 | } | ||||
281 | | ||||
282 | fail(i18n("Failed to find an installation script in %1", dir)); | ||||
283 | } | ||||
284 | | ||||
285 | return true; | ||||
286 | } | ||||
287 | | ||||
288 | bool cmdUninstall(const QString &archive, QString &errorText) | ||||
289 | { | ||||
290 | const auto serviceDir = getServiceMenusDir(); | ||||
291 | if (archive.endsWith(".desktop")) { | ||||
292 | // Append basename to destination directory | ||||
293 | const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName()); | ||||
294 | QFile file(dest); | ||||
295 | if (!file.remove()) { | ||||
296 | errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString()); | ||||
297 | return false; | ||||
298 | } | ||||
299 | } else { | ||||
300 | const QString dir = generateDirPath(archive); | ||||
301 | | ||||
302 | // Try "deinstall" first | ||||
303 | QString deinstallPath; | ||||
elvisangelaccio: This will detach the container, please use qAsConst | |||||
304 | const auto basenames1 = QStringList{"deinstall.sh", "deinstall"}; | ||||
305 | for (const auto &basename : qAsConst(basenames1)) { | ||||
306 | const auto path = findRecursive(dir, basename); | ||||
307 | if (!path.isEmpty()) { | ||||
308 | deinstallPath = path; | ||||
309 | break; | ||||
310 | } | ||||
311 | } | ||||
312 | | ||||
313 | if (!deinstallPath.isEmpty()) { | ||||
314 | bool ok = runInstallerScript(deinstallPath, false, QStringList{}, dir, errorText); | ||||
315 | if (!ok) { | ||||
316 | return ok; | ||||
317 | } | ||||
318 | } else { | ||||
319 | // If "deinstall" is missing, try "install --uninstall" | ||||
320 | | ||||
321 | QString installerPath; | ||||
elvisangelaccio: This will detach the container, please use qAsConst | |||||
322 | const auto basenames2 = QStringList{"install-it.sh", "install-it", "installKDE4.sh", | ||||
323 | "installKDE4", "install.sh", "install"}; | ||||
324 | for (const auto &basename : qAsConst(basenames2)) { | ||||
325 | const auto path = findRecursive(dir, basename); | ||||
326 | if (!path.isEmpty()) { | ||||
327 | installerPath = path; | ||||
328 | break; | ||||
329 | } | ||||
330 | } | ||||
331 | | ||||
332 | if (!installerPath.isEmpty()) { | ||||
333 | bool ok = runInstallerScript( | ||||
334 | installerPath, true, QStringList{"--remove", "--delete", "--uninstall", "--deinstall"}, dir, errorText); | ||||
335 | if (!ok) { | ||||
336 | return ok; | ||||
337 | } | ||||
338 | } else { | ||||
339 | fail(i18n("Failed to find an uninstallation script in %1", dir)); | ||||
340 | } | ||||
341 | } | ||||
342 | | ||||
343 | QDir dirObject(dir); | ||||
344 | if (!dirObject.removeRecursively()) { | ||||
345 | errorText = i18n("Failed to remove directory %1", dir); | ||||
346 | return false; | ||||
347 | } | ||||
348 | } | ||||
349 | | ||||
350 | return true; | ||||
351 | } | ||||
352 | | ||||
353 | int main(int argc, char *argv[]) | ||||
354 | { | ||||
355 | QCoreApplication app(argc, argv); | ||||
356 | | ||||
357 | QCommandLineParser parser; | ||||
358 | parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall.")); | ||||
359 | parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive.")); | ||||
360 | parser.process(app); | ||||
361 | | ||||
362 | const QStringList args = parser.positionalArguments(); | ||||
363 | if (args.isEmpty()) { | ||||
364 | fail(i18n("Command is required.")); | ||||
365 | } | ||||
366 | if (args.size() == 1) { | ||||
367 | fail(i18n("Path to archive is required.")); | ||||
368 | } | ||||
369 | | ||||
370 | const QString cmd = args[0]; | ||||
371 | const QString archive = args[1]; | ||||
372 | | ||||
373 | QString errorText; | ||||
374 | if (cmd == "install") { | ||||
375 | if (!cmdInstall(archive, errorText)) { | ||||
376 | fail(errorText); | ||||
377 | } | ||||
378 | } else if (cmd == "uninstall") { | ||||
379 | if (!cmdUninstall(archive, errorText)) { | ||||
380 | fail(errorText); | ||||
381 | } | ||||
382 | } else { | ||||
383 | fail(i18n("Unsupported command %1", cmd)); | ||||
384 | } | ||||
385 | | ||||
386 | return 0; | ||||
387 | } |
How about Q_NORETURN ?