]> cloud.milkyroute.net Git - dolphin.git/blob - src/settings/services/servicemenuinstaller/servicemenuinstaller.cpp
Merge branch 'release/20.08' into master
[dolphin.git] / src / settings / services / servicemenuinstaller / servicemenuinstaller.cpp
1 /*
2 * SPDX-FileCopyrightText: 2019 Alexander Potashev <aspotashev@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6
7 #include <QDebug>
8 #include <QProcess>
9 #include <QTimer>
10 #include <QStandardPaths>
11 #include <QDir>
12 #include <QDirIterator>
13 #include <QCommandLineParser>
14 #include <QMimeDatabase>
15 #include <QUrl>
16 #include <QGuiApplication>
17 #include <KLocalizedString>
18 #include <KShell>
19
20 #include "../../../config-packagekit.h"
21
22 const static QStringList binaryPackages = {QStringLiteral("application/vnd.debian.binary-package"),
23 QStringLiteral("application/x-rpm"),
24 QStringLiteral("application/x-xz"),
25 QStringLiteral("application/zstd")};
26 enum PackageOperation {
27 Install,
28 Uninstall
29 };
30
31 #ifdef HAVE_PACKAGEKIT
32 #include <PackageKit/Daemon>
33 #include <PackageKit/Details>
34 #include <PackageKit/Transaction>
35 #else
36 #include <QDesktopServices>
37 #endif
38
39 // @param msg Error that gets logged to CLI
40 Q_NORETURN void fail(const QString &str)
41 {
42 qCritical() << str;
43 const QStringList args = {"--detailederror" ,i18n("Dolphin service menu installation failed"), str};
44 QProcess::startDetached("kdialog", args);
45
46 exit(1);
47 }
48
49 QString getServiceMenusDir()
50 {
51 const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
52 return QDir(dataLocation).absoluteFilePath("kservices5/ServiceMenus");
53 }
54
55 #ifdef HAVE_PACKAGEKIT
56 void packageKitInstall(const QString &fileName)
57 {
58 PackageKit::Transaction *transaction = PackageKit::Daemon::installFile(fileName);
59
60 const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
61 fail(details);
62 };
63
64 QObject::connect(transaction, &PackageKit::Transaction::finished,
65 [=](PackageKit::Transaction::Exit status, uint) {
66 if (status == PackageKit::Transaction::ExitSuccess) {
67 exit(0);
68 }
69 // Fallback error handling
70 QTimer::singleShot(500, [=](){
71 fail(i18n("Failed to install \"%1\", exited with status \"%2\"",
72 fileName, QVariant::fromValue(status).toString()));
73 });
74 });
75 QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
76 }
77
78 void packageKitUninstall(const QString &fileName)
79 {
80 const auto exitWithError = [=](PackageKit::Transaction::Error, const QString &details) {
81 fail(details);
82 };
83 const auto uninstallLambda = [=](PackageKit::Transaction::Exit status, uint) {
84 if (status == PackageKit::Transaction::ExitSuccess) {
85 exit(0);
86 }
87 };
88
89 PackageKit::Transaction *transaction = PackageKit::Daemon::getDetailsLocal(fileName);
90 QObject::connect(transaction, &PackageKit::Transaction::details,
91 [=](const PackageKit::Details &details) {
92 PackageKit::Transaction *transaction = PackageKit::Daemon::removePackage(details.packageId());
93 QObject::connect(transaction, &PackageKit::Transaction::finished, uninstallLambda);
94 QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
95 });
96
97 QObject::connect(transaction, &PackageKit::Transaction::errorCode, exitWithError);
98 // Fallback error handling
99 QObject::connect(transaction, &PackageKit::Transaction::finished,
100 [=](PackageKit::Transaction::Exit status, uint) {
101 if (status != PackageKit::Transaction::ExitSuccess) {
102 QTimer::singleShot(500, [=]() {
103 fail(i18n("Failed to uninstall \"%1\", exited with status \"%2\"",
104 fileName, QVariant::fromValue(status).toString()));
105 });
106 }
107 });
108 }
109 #endif
110
111 Q_NORETURN void packageKit(PackageOperation operation, const QString &fileName)
112 {
113 #ifdef HAVE_PACKAGEKIT
114 QFileInfo fileInfo(fileName);
115 if (!fileInfo.exists()) {
116 fail(i18n("The file does not exist!"));
117 }
118 const QString absPath = fileInfo.absoluteFilePath();
119 if (operation == PackageOperation::Install) {
120 packageKitInstall(absPath);
121 } else {
122 packageKitUninstall(absPath);
123 }
124 QGuiApplication::exec(); // For event handling, no return after signals finish
125 fail(i18n("Unknown error when installing package"));
126 #else
127 Q_UNUSED(operation)
128 QDesktopServices::openUrl(QUrl(fileName));
129 exit(0);
130 #endif
131 }
132
133 struct UncompressCommand
134 {
135 QString command;
136 QStringList args1;
137 QStringList args2;
138 };
139
140 enum ScriptExecution{
141 Process,
142 Konsole
143 };
144
145 void runUncompress(const QString &inputPath, const QString &outputPath)
146 {
147 QVector<QPair<QStringList, UncompressCommand>> mimeTypeToCommand;
148 mimeTypeToCommand.append({{"application/x-tar", "application/tar", "application/x-gtar", "multipart/x-tar"},
149 UncompressCommand({"tar", {"-xf"}, {"-C"}})});
150 mimeTypeToCommand.append({{"application/x-gzip", "application/gzip",
151 "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
152 "application/x-gzip-compressed", "application/gzip-compressed",
153 "application/tgz", "application/x-compressed-tar",
154 "application/x-compressed-gtar", "file/tgz",
155 "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
156 "gzip/document"},
157 UncompressCommand({"tar", {"-zxf"}, {"-C"}})});
158 mimeTypeToCommand.append({{"application/bzip", "application/bzip2", "application/x-bzip",
159 "application/x-bzip2", "application/bzip-compressed",
160 "application/bzip2-compressed", "application/x-bzip-compressed",
161 "application/x-bzip2-compressed", "application/bzip-compressed-tar",
162 "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
163 "application/x-bzip2-compressed-tar", "application/x-bz2"},
164 UncompressCommand({"tar", {"-jxf"}, {"-C"}})});
165 mimeTypeToCommand.append({{"application/zip", "application/x-zip", "application/x-zip-compressed",
166 "multipart/x-zip"},
167 UncompressCommand({"unzip", {}, {"-d"}})});
168
169 const auto mime = QMimeDatabase().mimeTypeForFile(inputPath).name();
170
171 UncompressCommand command{};
172 for (const auto &pair : qAsConst(mimeTypeToCommand)) {
173 if (pair.first.contains(mime)) {
174 command = pair.second;
175 break;
176 }
177 }
178
179 if (command.command.isEmpty()) {
180 fail(i18n("Unsupported archive type %1: %2", mime, inputPath));
181 }
182
183 QProcess process;
184 process.start(
185 command.command,
186 QStringList() << command.args1 << inputPath << command.args2 << outputPath,
187 QIODevice::NotOpen);
188 if (!process.waitForStarted()) {
189 fail(i18n("Failed to run uncompressor command for %1", inputPath));
190 }
191
192 if (!process.waitForFinished()) {
193 fail(
194 i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" ")));
195 }
196
197 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
198 fail(i18n("Failed to uncompress %1", inputPath));
199 }
200 }
201
202 QString findRecursive(const QString &dir, const QString &basename)
203 {
204 QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories);
205 while (it.hasNext()) {
206 return QFileInfo(it.next()).canonicalFilePath();
207 }
208
209 return QString();
210 }
211
212 bool runScriptOnce(const QString &path, const QStringList &args, ScriptExecution execution)
213 {
214 QProcess process;
215 process.setWorkingDirectory(QFileInfo(path).absolutePath());
216
217 const static bool konsoleAvailable = !QStandardPaths::findExecutable("konsole").isEmpty();
218 if (konsoleAvailable && execution == ScriptExecution::Konsole) {
219 QString bashCommand = KShell::quoteArg(path) + ' ';
220 if (!args.isEmpty()) {
221 bashCommand.append(args.join(' '));
222 }
223 bashCommand.append("|| $SHELL");
224 // If the install script fails a shell opens and the user can fix the problem
225 // without an error konsole closes
226 process.start("konsole", QStringList() << "-e" << "bash" << "-c" << bashCommand, QIODevice::NotOpen);
227 } else {
228 process.start(path, args, QIODevice::NotOpen);
229 }
230 if (!process.waitForStarted()) {
231 fail(i18n("Failed to run installer script %1", path));
232 }
233
234 // Wait until installer exits, without timeout
235 if (!process.waitForFinished(-1)) {
236 qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
237 return false;
238 }
239
240 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
241 qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
242 return false;
243 }
244
245 return true;
246 }
247
248 // If hasArgVariants is true, run "path".
249 // If hasArgVariants is false, run "path argVariants[i]" until successful.
250 bool runScriptVariants(const QString &path, bool hasArgVariants, const QStringList &argVariants, QString &errorText)
251 {
252 QFile file(path);
253 if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
254 errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString());
255 return false;
256 }
257
258 qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path;
259 if (hasArgVariants) {
260 for (const auto &arg : argVariants) {
261 if (runScriptOnce(path, {arg}, ScriptExecution::Process)) {
262 return true;
263 }
264 }
265 } else if (runScriptOnce(path, {}, ScriptExecution::Konsole)) {
266 return true;
267 }
268
269 errorText = i18nc(
270 "%2 = comma separated list of arguments",
271 "Installer script %1 failed, tried arguments \"%2\".", path, argVariants.join(i18nc("Separator between arguments", "\", \"")));
272 return false;
273 }
274
275 QString generateDirPath(const QString &archive)
276 {
277 return QStringLiteral("%1-dir").arg(archive);
278 }
279
280 bool cmdInstall(const QString &archive, QString &errorText)
281 {
282 const auto serviceDir = getServiceMenusDir();
283 if (!QDir().mkpath(serviceDir)) {
284 // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
285 errorText = i18n("Failed to create path %1", serviceDir);
286 return false;
287 }
288
289 if (archive.endsWith(QLatin1String(".desktop"))) {
290 // Append basename to destination directory
291 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
292 qInfo() << "Single-File Service-Menu" << archive << dest;
293
294 QFile source(archive);
295 if (!source.copy(dest)) {
296 errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString());
297 return false;
298 }
299 } else {
300 if (binaryPackages.contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
301 packageKit(PackageOperation::Install, archive);
302 }
303 const QString dir = generateDirPath(archive);
304 if (QFile::exists(dir)) {
305 if (!QDir(dir).removeRecursively()) {
306 errorText = i18n("Failed to remove directory %1", dir);
307 return false;
308 }
309 }
310
311 if (QDir().mkdir(dir)) {
312 errorText = i18n("Failed to create directory %1", dir);
313 }
314
315 runUncompress(archive, dir);
316
317 // Try "install-it" first
318 QString installItPath;
319 const QStringList basenames1 = {"install-it.sh", "install-it"};
320 for (const auto &basename : basenames1) {
321 const auto path = findRecursive(dir, basename);
322 if (!path.isEmpty()) {
323 installItPath = path;
324 break;
325 }
326 }
327
328 if (!installItPath.isEmpty()) {
329 return runScriptVariants(installItPath, false, QStringList{}, errorText);
330 }
331
332 // If "install-it" is missing, try "install"
333 QString installerPath;
334 const QStringList basenames2 = {"installKDE4.sh", "installKDE4", "install.sh", "install"};
335 for (const auto &basename : basenames2) {
336 const auto path = findRecursive(dir, basename);
337 if (!path.isEmpty()) {
338 installerPath = path;
339 break;
340 }
341 }
342
343 if (!installerPath.isEmpty()) {
344 // Try to run script without variants first
345 if (!runScriptVariants(installerPath, false, {}, errorText)) {
346 return runScriptVariants(installerPath, true, {"--local", "--local-install", "--install"}, errorText);
347 }
348 return true;
349 }
350
351 fail(i18n("Failed to find an installation script in %1", dir));
352 }
353
354 return true;
355 }
356
357 bool cmdUninstall(const QString &archive, QString &errorText)
358 {
359 const auto serviceDir = getServiceMenusDir();
360 if (archive.endsWith(QLatin1String(".desktop"))) {
361 // Append basename to destination directory
362 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
363 QFile file(dest);
364 if (!file.remove()) {
365 errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString());
366 return false;
367 }
368 } else {
369 if (binaryPackages.contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
370 packageKit(PackageOperation::Uninstall, archive);
371 }
372 const QString dir = generateDirPath(archive);
373
374 // Try "deinstall" first
375 QString deinstallPath;
376 const QStringList basenames1 = {"uninstall.sh", "uninstal", "deinstall.sh", "deinstall"};
377 for (const auto &basename : basenames1) {
378 const auto path = findRecursive(dir, basename);
379 if (!path.isEmpty()) {
380 deinstallPath = path;
381 break;
382 }
383 }
384
385 if (!deinstallPath.isEmpty()) {
386 const bool ok = runScriptVariants(deinstallPath, false, {}, errorText);
387 if (!ok) {
388 return ok;
389 }
390 } else {
391 // If "deinstall" is missing, try "install --uninstall"
392 QString installerPath;
393 const QStringList basenames2 = {"install-it.sh", "install-it", "installKDE4.sh",
394 "installKDE4", "install.sh", "install"};
395 for (const auto &basename : basenames2) {
396 const auto path = findRecursive(dir, basename);
397 if (!path.isEmpty()) {
398 installerPath = path;
399 break;
400 }
401 }
402
403 if (!installerPath.isEmpty()) {
404 const bool ok = runScriptVariants(installerPath, true,
405 {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText);
406 if (!ok) {
407 return ok;
408 }
409 } else {
410 fail(i18n("Failed to find an uninstallation script in %1", dir));
411 }
412 }
413
414 QDir dirObject(dir);
415 if (!dirObject.removeRecursively()) {
416 errorText = i18n("Failed to remove directory %1", dir);
417 return false;
418 }
419 }
420
421 return true;
422 }
423
424 int main(int argc, char *argv[])
425 {
426 QGuiApplication app(argc, argv);
427
428 QCommandLineParser parser;
429 parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
430 parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
431 parser.process(app);
432
433 const QStringList args = parser.positionalArguments();
434 if (args.isEmpty()) {
435 fail(i18n("Command is required."));
436 }
437 if (args.size() == 1) {
438 fail(i18n("Path to archive is required."));
439 }
440
441 const QString cmd = args[0];
442 const QString archive = args[1];
443
444 QString errorText;
445 if (cmd == QLatin1String("install")) {
446 if (!cmdInstall(archive, errorText)) {
447 fail(errorText);
448 }
449 } else if (cmd == QLatin1String("uninstall")) {
450 if (!cmdUninstall(archive, errorText)) {
451 fail(errorText);
452 }
453 } else {
454 fail(i18n("Unsupported command %1", cmd));
455 }
456
457 return 0;
458 }