]> cloud.milkyroute.net Git - dolphin.git/blob - src/settings/services/servicemenuinstaller/servicemenuinstaller.cpp
60621921affdd80a44264cfbbf2425a32a21a32e
[dolphin.git] / src / settings / services / servicemenuinstaller / servicemenuinstaller.cpp
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 #include <QMimeDatabase>
28 #include <QUrl>
29 #include <QDesktopServices>
30 #include <QGuiApplication>
31
32 #include <KLocalizedString>
33 #include <KShell>
34
35 // @param msg Error that gets logged to CLI
36 Q_NORETURN void fail(const QString &str)
37 {
38 qCritical() << str;
39
40 QProcess process;
41 const QStringList args = {"--passivepopup", i18n("Dolphin service menu installation failed"), "15"};
42 process.start("kdialog", args, QIODevice::ReadOnly);
43 if (!process.waitForStarted()) {
44 qFatal("Failed to run kdialog");
45 }
46
47 exit(1);
48 }
49
50 QString getServiceMenusDir()
51 {
52 const QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
53 return QDir(dataLocation).absoluteFilePath("kservices5/ServiceMenus");
54 }
55
56 struct UncompressCommand
57 {
58 QString command;
59 QStringList args1;
60 QStringList args2;
61 };
62
63 enum ScriptExecution{
64 Process,
65 Konsole
66 };
67
68 void runUncompress(const QString &inputPath, const QString &outputPath)
69 {
70 QVector<QPair<QStringList, UncompressCommand>> mimeTypeToCommand;
71 mimeTypeToCommand.append({{"application/x-tar", "application/tar", "application/x-gtar", "multipart/x-tar"},
72 UncompressCommand({"tar", {"-xf"}, {"-C"}})});
73 mimeTypeToCommand.append({{"application/x-gzip", "application/gzip",
74 "application/x-gzip-compressed-tar", "application/gzip-compressed-tar",
75 "application/x-gzip-compressed", "application/gzip-compressed",
76 "application/tgz", "application/x-compressed-tar",
77 "application/x-compressed-gtar", "file/tgz",
78 "multipart/x-tar-gz", "application/x-gunzip", "application/gzipped",
79 "gzip/document"},
80 UncompressCommand({"tar", {"-zxf"}, {"-C"}})});
81 mimeTypeToCommand.append({{"application/bzip", "application/bzip2", "application/x-bzip",
82 "application/x-bzip2", "application/bzip-compressed",
83 "application/bzip2-compressed", "application/x-bzip-compressed",
84 "application/x-bzip2-compressed", "application/bzip-compressed-tar",
85 "application/bzip2-compressed-tar", "application/x-bzip-compressed-tar",
86 "application/x-bzip2-compressed-tar", "application/x-bz2"},
87 UncompressCommand({"tar", {"-jxf"}, {"-C"}})});
88 mimeTypeToCommand.append({{"application/zip", "application/x-zip", "application/x-zip-compressed",
89 "multipart/x-zip"},
90 UncompressCommand({"unzip", {}, {"-d"}})});
91
92 const auto mime = QMimeDatabase().mimeTypeForFile(inputPath).name();
93
94 UncompressCommand command{};
95 for (const auto &pair : qAsConst(mimeTypeToCommand)) {
96 if (pair.first.contains(mime)) {
97 command = pair.second;
98 break;
99 }
100 }
101
102 if (command.command.isEmpty()) {
103 fail(i18n("Unsupported archive type %1: %2", mime, inputPath));
104 }
105
106 QProcess process;
107 process.start(
108 command.command,
109 QStringList() << command.args1 << inputPath << command.args2 << outputPath,
110 QIODevice::NotOpen);
111 if (!process.waitForStarted()) {
112 fail(i18n("Failed to run uncompressor command for %1", inputPath));
113 }
114
115 if (!process.waitForFinished()) {
116 fail(
117 i18n("Process did not finish in reasonable time: %1 %2", process.program(), process.arguments().join(" ")));
118 }
119
120 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
121 fail(i18n("Failed to uncompress %1", inputPath));
122 }
123 }
124
125 QString findRecursive(const QString &dir, const QString &basename)
126 {
127 QDirIterator it(dir, QStringList{basename}, QDir::Files, QDirIterator::Subdirectories);
128 while (it.hasNext()) {
129 return QFileInfo(it.next()).canonicalFilePath();
130 }
131
132 return QString();
133 }
134
135 bool runScriptOnce(const QString &path, const QStringList &args, ScriptExecution execution)
136 {
137 QProcess process;
138 process.setWorkingDirectory(QFileInfo(path).absolutePath());
139
140 const static bool konsoleAvailable = !QStandardPaths::findExecutable("konsole").isEmpty();
141 if (konsoleAvailable && execution == ScriptExecution::Konsole) {
142 QString bashCommand = KShell::quoteArg(path) + ' ';
143 if (!args.isEmpty()) {
144 bashCommand.append(args.join(' '));
145 }
146 bashCommand.append("|| $SHELL");
147 // If the install script fails a shell opens and the user can fix the problem
148 // without an error konsole closes
149 process.start("konsole", QStringList() << "-e" << "bash" << "-c" << bashCommand, QIODevice::NotOpen);
150 } else {
151 process.start(path, args, QIODevice::NotOpen);
152 }
153 if (!process.waitForStarted()) {
154 fail(i18n("Failed to run installer script %1", path));
155 }
156
157 // Wait until installer exits, without timeout
158 if (!process.waitForFinished(-1)) {
159 qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
160 return false;
161 }
162
163 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
164 qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
165 return false;
166 }
167
168 return true;
169 }
170
171 // If hasArgVariants is true, run "path".
172 // If hasArgVariants is false, run "path argVariants[i]" until successful.
173 bool runScriptVariants(const QString &path, bool hasArgVariants, const QStringList &argVariants, QString &errorText)
174 {
175 QFile file(path);
176 if (!file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
177 errorText = i18n("Failed to set permissions on %1: %2", path, file.errorString());
178 return false;
179 }
180
181 qInfo() << "[servicemenuinstaller]: Trying to run installer/uninstaller" << path;
182 if (hasArgVariants) {
183 for (const auto &arg : argVariants) {
184 if (runScriptOnce(path, {arg}, ScriptExecution::Process)) {
185 return true;
186 }
187 }
188 } else if (runScriptOnce(path, {}, ScriptExecution::Konsole)) {
189 return true;
190 }
191
192 errorText = i18nc(
193 "%2 = comma separated list of arguments",
194 "Installer script %1 failed, tried arguments \"%2\".", path, argVariants.join(i18nc("Separator between arguments", "\", \"")));
195 return false;
196 }
197
198 QString generateDirPath(const QString &archive)
199 {
200 return QStringLiteral("%1-dir").arg(archive);
201 }
202
203 bool cmdInstall(const QString &archive, QString &errorText)
204 {
205 const auto serviceDir = getServiceMenusDir();
206 if (!QDir().mkpath(serviceDir)) {
207 // TODO Cannot get error string because of this bug: https://bugreports.qt.io/browse/QTBUG-1483
208 errorText = i18n("Failed to create path %1", serviceDir);
209 return false;
210 }
211
212 if (archive.endsWith(QLatin1String(".desktop"))) {
213 // Append basename to destination directory
214 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
215 qInfo() << "Single-File Service-Menu" << archive << dest;
216
217 QFile source(archive);
218 if (!source.copy(dest)) {
219 errorText = i18n("Failed to copy .desktop file %1 to %2: %3", archive, dest, source.errorString());
220 return false;
221 }
222 } else {
223 const QStringList binaryPackages = {"application/vnd.debian.binary-package", "application/x-rpm"};
224 if (binaryPackages.contains(QMimeDatabase().mimeTypeForFile(archive).name())) {
225 return QDesktopServices::openUrl(QUrl(archive));
226 }
227 const QString dir = generateDirPath(archive);
228 if (QFile::exists(dir)) {
229 if (!QDir(dir).removeRecursively()) {
230 errorText = i18n("Failed to remove directory %1", dir);
231 return false;
232 }
233 }
234
235 if (QDir().mkdir(dir)) {
236 errorText = i18n("Failed to create directory %1", dir);
237 }
238
239 runUncompress(archive, dir);
240
241 // Try "install-it" first
242 QString installItPath;
243 const QStringList basenames1 = {"install-it.sh", "install-it"};
244 for (const auto &basename : basenames1) {
245 const auto path = findRecursive(dir, basename);
246 if (!path.isEmpty()) {
247 installItPath = path;
248 break;
249 }
250 }
251
252 if (!installItPath.isEmpty()) {
253 return runScriptVariants(installItPath, false, QStringList{}, errorText);
254 }
255
256 // If "install-it" is missing, try "install"
257 QString installerPath;
258 const QStringList basenames2 = {"installKDE4.sh", "installKDE4", "install.sh", "install"};
259 for (const auto &basename : basenames2) {
260 const auto path = findRecursive(dir, basename);
261 if (!path.isEmpty()) {
262 installerPath = path;
263 break;
264 }
265 }
266
267 if (!installerPath.isEmpty()) {
268 // Try to run script without variants first
269 if (!runScriptVariants(installerPath, false, {}, errorText)) {
270 return runScriptVariants(installerPath, true, {"--local", "--local-install", "--install"}, errorText);
271 }
272 return true;
273 }
274
275 fail(i18n("Failed to find an installation script in %1", dir));
276 }
277
278 return true;
279 }
280
281 bool cmdUninstall(const QString &archive, QString &errorText)
282 {
283 const auto serviceDir = getServiceMenusDir();
284 if (archive.endsWith(QLatin1String(".desktop"))) {
285 // Append basename to destination directory
286 const auto dest = QDir(serviceDir).absoluteFilePath(QFileInfo(archive).fileName());
287 QFile file(dest);
288 if (!file.remove()) {
289 errorText = i18n("Failed to remove .desktop file %1: %2", dest, file.errorString());
290 return false;
291 }
292 } else {
293 const QString dir = generateDirPath(archive);
294
295 // Try "deinstall" first
296 QString deinstallPath;
297 const QStringList basenames1 = {"uninstall.sh", "uninstal", "deinstall.sh", "deinstall"};
298 for (const auto &basename : basenames1) {
299 const auto path = findRecursive(dir, basename);
300 if (!path.isEmpty()) {
301 deinstallPath = path;
302 break;
303 }
304 }
305
306 if (!deinstallPath.isEmpty()) {
307 const bool ok = runScriptVariants(deinstallPath, false, {}, errorText);
308 if (!ok) {
309 return ok;
310 }
311 } else {
312 // If "deinstall" is missing, try "install --uninstall"
313 QString installerPath;
314 const QStringList basenames2 = {"install-it.sh", "install-it", "installKDE4.sh",
315 "installKDE4", "install.sh", "install"};
316 for (const auto &basename : basenames2) {
317 const auto path = findRecursive(dir, basename);
318 if (!path.isEmpty()) {
319 installerPath = path;
320 break;
321 }
322 }
323
324 if (!installerPath.isEmpty()) {
325 const bool ok = runScriptVariants(installerPath, true,
326 {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText);
327 if (!ok) {
328 return ok;
329 }
330 } else {
331 fail(i18n("Failed to find an uninstallation script in %1", dir));
332 }
333 }
334
335 QDir dirObject(dir);
336 if (!dirObject.removeRecursively()) {
337 errorText = i18n("Failed to remove directory %1", dir);
338 return false;
339 }
340 }
341
342 return true;
343 }
344
345 int main(int argc, char *argv[])
346 {
347 QGuiApplication app(argc, argv);
348
349 QCommandLineParser parser;
350 parser.addPositionalArgument(QStringLiteral("command"), i18nc("@info:shell", "Command to execute: install or uninstall."));
351 parser.addPositionalArgument(QStringLiteral("path"), i18nc("@info:shell", "Path to archive."));
352 parser.process(app);
353
354 const QStringList args = parser.positionalArguments();
355 if (args.isEmpty()) {
356 fail(i18n("Command is required."));
357 }
358 if (args.size() == 1) {
359 fail(i18n("Path to archive is required."));
360 }
361
362 const QString cmd = args[0];
363 const QString archive = args[1];
364
365 QString errorText;
366 if (cmd == QLatin1String("install")) {
367 if (!cmdInstall(archive, errorText)) {
368 fail(errorText);
369 }
370 } else if (cmd == QLatin1String("uninstall")) {
371 if (!cmdUninstall(archive, errorText)) {
372 fail(errorText);
373 }
374 } else {
375 fail(i18n("Unsupported command %1", cmd));
376 }
377
378 return 0;
379 }