]> cloud.milkyroute.net Git - dolphin.git/blob - src/settings/services/servicemenuinstaller/servicemenuinstaller.cpp
servicemenuinstaller: Run installation scripts with cwd in their parent directories
[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
28 #include <KLocalizedString>
29
30 // @param msg Error that gets logged to CLI
31 Q_NORETURN void fail(const QString &str)
32 {
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 }
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 }
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)
161 {
162 QProcess process;
163 process.setWorkingDirectory(QFileInfo(path).absolutePath());
164
165 process.start(path, args, QIODevice::NotOpen);
166 if (!process.waitForStarted()) {
167 fail(i18n("Failed to run installer script %1", path));
168 }
169
170 // Wait until installer exits, without timeout
171 if (!process.waitForFinished(-1)) {
172 qWarning() << "Failed to wait on installer:" << process.program() << process.arguments().join(" ");
173 return false;
174 }
175
176 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
177 qWarning() << "Installer script exited with error:" << process.program() << process.arguments().join(" ");
178 return false;
179 }
180
181 return true;
182 }
183
184 // If hasArgVariants is true, run "path".
185 // If hasArgVariants is false, run "path argVariants[i]" until successful.
186 bool runInstallerScript(const QString &path, bool hasArgVariants, const QStringList &argVariants, 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})) {
198 return true;
199 }
200 }
201 } else {
202 if (runInstallerScriptOnce(path, QStringList{})) {
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)) {
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{}, 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"};
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"}, 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;
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{}, errorText);
315 if (!ok) {
316 return ok;
317 }
318 } else {
319 // If "deinstall" is missing, try "install --uninstall"
320
321 QString installerPath;
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"}, 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 }