1 /***************************************************************************
2 * Copyright © 2019 Alexander Potashev <aspotashev@gmail.com> *
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. *
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. *
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 ***************************************************************************/
23 #include <QStandardPaths>
25 #include <QDirIterator>
26 #include <QCommandLineParser>
27 #include <QMimeDatabase>
29 #include <QDesktopServices>
30 #include <QGuiApplication>
32 #include <KLocalizedString>
35 // @param msg Error that gets logged to CLI
36 Q_NORETURN
void fail(const QString
&str
)
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");
50 QString
getServiceMenusDir()
52 const QString dataLocation
= QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation
);
53 return QDir(dataLocation
).absoluteFilePath("kservices5/ServiceMenus");
56 struct UncompressCommand
68 void runUncompress(const QString
&inputPath
, const QString
&outputPath
)
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",
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",
90 UncompressCommand({"unzip", {}, {"-d"}})});
92 const auto mime
= QMimeDatabase().mimeTypeForFile(inputPath
).name();
94 UncompressCommand command
{};
95 for (const auto &pair
: qAsConst(mimeTypeToCommand
)) {
96 if (pair
.first
.contains(mime
)) {
97 command
= pair
.second
;
102 if (command
.command
.isEmpty()) {
103 fail(i18n("Unsupported archive type %1: %2", mime
, inputPath
));
109 QStringList() << command
.args1
<< inputPath
<< command
.args2
<< outputPath
,
111 if (!process
.waitForStarted()) {
112 fail(i18n("Failed to run uncompressor command for %1", inputPath
));
115 if (!process
.waitForFinished()) {
117 i18n("Process did not finish in reasonable time: %1 %2", process
.program(), process
.arguments().join(" ")));
120 if (process
.exitStatus() != QProcess::NormalExit
|| process
.exitCode() != 0) {
121 fail(i18n("Failed to uncompress %1", inputPath
));
125 QString
findRecursive(const QString
&dir
, const QString
&basename
)
127 QDirIterator
it(dir
, QStringList
{basename
}, QDir::Files
, QDirIterator::Subdirectories
);
128 while (it
.hasNext()) {
129 return QFileInfo(it
.next()).canonicalFilePath();
135 bool runScriptOnce(const QString
&path
, const QStringList
&args
, ScriptExecution execution
)
138 process
.setWorkingDirectory(QFileInfo(path
).absolutePath());
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(' '));
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
);
151 process
.start(path
, args
, QIODevice::NotOpen
);
153 if (!process
.waitForStarted()) {
154 fail(i18n("Failed to run installer script %1", path
));
157 // Wait until installer exits, without timeout
158 if (!process
.waitForFinished(-1)) {
159 qWarning() << "Failed to wait on installer:" << process
.program() << process
.arguments().join(" ");
163 if (process
.exitStatus() != QProcess::NormalExit
|| process
.exitCode() != 0) {
164 qWarning() << "Installer script exited with error:" << process
.program() << process
.arguments().join(" ");
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
)
176 if (!file
.setPermissions(QFile::ReadOwner
| QFile::WriteOwner
| QFile::ExeOwner
)) {
177 errorText
= i18n("Failed to set permissions on %1: %2", path
, file
.errorString());
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
)) {
188 } else if (runScriptOnce(path
, {}, ScriptExecution::Konsole
)) {
193 "%2 = comma separated list of arguments",
194 "Installer script %1 failed, tried arguments \"%2\".", path
, argVariants
.join(i18nc("Separator between arguments", "\", \"")));
198 QString
generateDirPath(const QString
&archive
)
200 return QStringLiteral("%1-dir").arg(archive
);
203 bool cmdInstall(const QString
&archive
, QString
&errorText
)
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
);
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
;
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());
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
));
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
);
235 if (QDir().mkdir(dir
)) {
236 errorText
= i18n("Failed to create directory %1", dir
);
239 runUncompress(archive
, dir
);
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
;
252 if (!installItPath
.isEmpty()) {
253 return runScriptVariants(installItPath
, false, QStringList
{}, errorText
);
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
;
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
);
275 fail(i18n("Failed to find an installation script in %1", dir
));
281 bool cmdUninstall(const QString
&archive
, QString
&errorText
)
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());
288 if (!file
.remove()) {
289 errorText
= i18n("Failed to remove .desktop file %1: %2", dest
, file
.errorString());
293 const QString dir
= generateDirPath(archive
);
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
;
306 if (!deinstallPath
.isEmpty()) {
307 const bool ok
= runScriptVariants(deinstallPath
, false, {}, errorText
);
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
;
324 if (!installerPath
.isEmpty()) {
325 const bool ok
= runScriptVariants(installerPath
, true,
326 {"--remove", "--delete", "--uninstall", "--deinstall"}, errorText
);
331 fail(i18n("Failed to find an uninstallation script in %1", dir
));
336 if (!dirObject
.removeRecursively()) {
337 errorText
= i18n("Failed to remove directory %1", dir
);
345 int main(int argc
, char *argv
[])
347 QGuiApplication
app(argc
, argv
);
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."));
354 const QStringList args
= parser
.positionalArguments();
355 if (args
.isEmpty()) {
356 fail(i18n("Command is required."));
358 if (args
.size() == 1) {
359 fail(i18n("Path to archive is required."));
362 const QString cmd
= args
[0];
363 const QString archive
= args
[1];
366 if (cmd
== QLatin1String("install")) {
367 if (!cmdInstall(archive
, errorText
)) {
370 } else if (cmd
== QLatin1String("uninstall")) {
371 if (!cmdUninstall(archive
, errorText
)) {
375 fail(i18n("Unsupported command %1", cmd
));