Branch data Line data Source code
1 : : // Copyright (C) 2021 The Qt Company Ltd.
2 : : // Copyright (C) 2019 Luxoft Sweden AB
3 : : // Copyright (C) 2018 Pelagicore AG
4 : : // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
5 : :
6 : : #include <QCoreApplication>
7 : : #include <QCommandLineParser>
8 : : #include <QCommandLineOption>
9 : : #include <QStringList>
10 : : #include <QDir>
11 : : #include <QTemporaryFile>
12 : : #include <QFileInfo>
13 : : #include <QDBusConnection>
14 : : #include <QDBusPendingReply>
15 : : #include <QDBusServiceWatcher>
16 : : #include <QDBusError>
17 : : #include <QMetaObject>
18 : : #include <QStringBuilder>
19 : : #include <QRegularExpression>
20 : : #include <QJsonDocument>
21 : : #include <QLockFile>
22 : :
23 : : #include <functional>
24 : :
25 : : #include <QtAppManCommon/global.h>
26 : : #include <QtAppManCommon/error.h>
27 : : #include <QtAppManCommon/exception.h>
28 : : #include <QtAppManCommon/unixsignalhandler.h>
29 : : #include <QtAppManCommon/utilities.h>
30 : : #include <QtAppManCommon/qtyaml.h>
31 : : #include <QtAppManCommon/dbus-utilities.h>
32 : :
33 : : #include "applicationmanager_interface.h"
34 : : #include "packagemanager_interface.h"
35 : :
36 : : #include "interrupthandler.h"
37 : :
38 : : using namespace Qt::StringLiterals;
39 : :
40 : : QT_USE_NAMESPACE_AM
41 : :
42 : : class DBus : public QObject
43 : : {
44 : : Q_OBJECT
45 : :
46 : : public:
47 : 19 : DBus()
48 [ + - ]: 19 : : m_dbusService(u"io.qt.ApplicationManager"_s)
49 : :
50 : : {
51 [ + - ]: 19 : registerDBusTypes();
52 : 19 : }
53 : :
54 : 19 : void setInstanceInfo(const QVariantMap &instanceInfo)
55 : : {
56 [ + - + - ]: 19 : m_dbusAddresses = instanceInfo[u"dbus"_s].toMap();
57 : 19 : }
58 : :
59 : 12 : void connectToManager() noexcept(false)
60 : : {
61 [ - + ]: 12 : if (m_manager)
62 : 0 : return;
63 : :
64 [ + - ]: 12 : auto conn = connectTo(u"io.qt.ApplicationManager"_s);
65 : 12 : m_manager = tryConnectToDBusInterface<IoQtApplicationManagerInterface>(m_dbusService,
66 [ + - ]: 24 : u"/ApplicationManager"_s,
67 [ + - ]: 12 : conn.name(), this);
68 [ - + ]: 12 : if (!m_manager) {
69 : 0 : throw Exception("Could not connect to the io.qt.ApplicationManager D-Bus interface on %1")
70 : 0 : .arg(m_dbusName);
71 : : }
72 : 12 : }
73 : :
74 : 10 : void connectToPackager() noexcept(false)
75 : : {
76 [ - + ]: 10 : if (m_packager)
77 : 0 : return;
78 : :
79 [ + - ]: 10 : auto conn = connectTo(u"io.qt.PackageManager"_s);
80 : 10 : m_packager = tryConnectToDBusInterface<IoQtPackageManagerInterface>(m_dbusService,
81 [ + - ]: 20 : u"/PackageManager"_s,
82 [ + - ]: 10 : conn.name(), this);
83 [ - + ]: 10 : if (!m_packager) {
84 : 0 : throw Exception("Could not connect to the io.qt.PackageManager D-Bus interface on %1")
85 : 0 : .arg(m_dbusName);
86 : : }
87 : 10 : }
88 : :
89 : : Q_SIGNAL void disconnected(QString reason);
90 : :
91 : : private:
92 : : template<typename T>
93 : 22 : static T *tryConnectToDBusInterface(const QString &service, const QString &path,
94 : : const QString &connectionName, QObject *parent)
95 : : {
96 : : // we are working with very small delays in the milli-second range here, so a linear factor
97 : : // to support valgrind would have to be very large and probably conflict with usage elsewhere
98 : : // in the codebase, where the ranges are normally in the seconds.
99 [ + - + - : 22 : static const int timeout = timeoutFactor() * timeoutFactor();
+ - + - ]
100 : :
101 : 22 : QDBusConnection conn(connectionName);
102 : :
103 [ + - + - ]: 22 : if (!conn.isConnected())
104 : : return nullptr;
105 [ + - + - : 22 : if (!service.isEmpty() && conn.interface()) {
+ - ]
106 : : // the 'T' constructor can block up to 25sec (!), if the service is not registered!
107 [ + - + - : 44 : if (!conn.interface()->isServiceRegistered(service))
+ - ]
108 : : return nullptr;
109 : : }
110 : :
111 : 22 : QElapsedTimer timer;
112 : 22 : timer.start();
113 : :
114 : : do {
115 [ + - + - ]: 22 : T *iface = new T(service, path, conn, parent);
116 [ + - + - : 44 : if (!iface->lastError().isValid())
+ - ]
117 : 22 : return iface;
118 : 0 : delete iface;
119 [ # # ]: 0 : QThread::msleep(static_cast<unsigned long>(timeout));
120 [ # # ]: 0 : } while (timer.elapsed() < (100 * timeout)); // 100msec base line
121 : :
122 : : return nullptr;
123 : 22 : }
124 : :
125 : 22 : QDBusConnection connectTo(const QString &iface) noexcept(false)
126 : : {
127 : 22 : QDBusConnection conn(iface);
128 : :
129 [ + - + - ]: 44 : QString dbus = m_dbusAddresses.value(iface).toString();
130 [ - + ]: 22 : if (dbus == u"system") {
131 [ # # ]: 0 : conn = QDBusConnection::systemBus();
132 : 0 : m_dbusName = u"[system-bus]"_s;
133 [ - + ]: 22 : } else if (dbus.isEmpty()) {
134 [ # # ]: 0 : conn = QDBusConnection::sessionBus();
135 : 0 : m_dbusName = u"[session-bus]"_s;
136 [ - + ]: 22 : } else if (dbus.startsWith(u"p2p:")) {
137 [ # # ]: 0 : const auto address = dbus.mid(4);
138 [ # # ]: 0 : conn = QDBusConnection::connectToPeer(address, u"p2p"_s);
139 [ # # ]: 0 : m_dbusName = u"[p2p] "_s + address;
140 : 0 : m_dbusService.clear(); // no service names allowed on p2p busses
141 : 0 : } else {
142 [ + - ]: 22 : conn = QDBusConnection::connectToBus(dbus, u"custom"_s);
143 : 22 : m_dbusName = dbus;
144 : : }
145 : :
146 [ + - - + ]: 22 : if (!conn.isConnected()) {
147 : 0 : throw Exception(Error::IO, "Could not connect to the application manager D-Bus interface %1 at %2: %3")
148 [ # # # # ]: 0 : .arg(iface, m_dbusName, conn.lastError().message());
149 : : }
150 : :
151 [ + - ]: 22 : installDisconnectWatcher(conn, u"io.qt.ApplicationManager"_s);
152 : 22 : return conn;
153 : 22 : }
154 : :
155 : 22 : void installDisconnectWatcher(const QDBusConnection &conn, const QString &serviceName)
156 : : {
157 [ + - ]: 22 : if (m_disconnectedEmitted)
158 : : return;
159 : :
160 [ + + ]: 22 : if (!m_connections.contains(conn.name())) {
161 [ + - ]: 19 : auto *watcher = new QDBusServiceWatcher(serviceName, conn, QDBusServiceWatcher::WatchForOwnerChange, this);
162 : 19 : connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged,
163 : 0 : this, [this](const QString &, const QString &, const QString &) {
164 [ # # ]: 0 : disconnectDetected(u"owner changed"_s);
165 : 0 : });
166 [ + - ]: 38 : m_connections.append(conn.name());
167 : : }
168 : :
169 : : // serviceOwnerChanged does not work if the bus-daemon process dies (as is the case when
170 : : // the AM starts its own session bus in --dbus=auto mode and then later crashes, killing
171 : : // the bus-daemon with it).
172 : : // QDBusConnection::isConnected() does not have a change signal, so we have to poll.
173 [ + + ]: 22 : if (!m_disconnectTimer) {
174 [ + - ]: 19 : m_disconnectTimer = new QTimer(this);
175 : 19 : connect(m_disconnectTimer, &QTimer::timeout, this, [this]() {
176 [ # # ]: 0 : for (const auto &name : std::as_const(m_connections)) {
177 [ # # # # ]: 0 : if (!QDBusConnection(name).isConnected()) {
178 [ # # ]: 0 : disconnectDetected(u"bus died"_s);
179 : 0 : break;
180 : : }
181 : : }
182 : 0 : });
183 : 19 : m_disconnectTimer->start(500);
184 : : }
185 : : }
186 : :
187 : 0 : void disconnectDetected(const QString &reason)
188 : : {
189 [ # # ]: 0 : if (!m_disconnectedEmitted) {
190 [ # # ]: 0 : emit disconnected(reason);
191 : 0 : m_disconnectedEmitted = true;
192 [ # # ]: 0 : if (m_disconnectTimer)
193 : 0 : m_disconnectTimer->stop();
194 : : }
195 : 0 : }
196 : :
197 : : public:
198 : 20 : IoQtPackageManagerInterface *packager() const
199 : : {
200 : 20 : return m_packager;
201 : : }
202 : :
203 : 9 : IoQtApplicationManagerInterface *manager() const
204 : : {
205 : 9 : return m_manager;
206 : : }
207 : :
208 : : private:
209 : : IoQtPackageManagerInterface *m_packager = nullptr;
210 : : IoQtApplicationManagerInterface *m_manager = nullptr;
211 : : QVariantMap m_dbusAddresses;
212 : : QString m_dbusName;
213 : : QString m_dbusService;
214 : : QStringList m_connections;
215 : : QTimer *m_disconnectTimer = nullptr;
216 : : bool m_disconnectedEmitted = false;
217 : : };
218 : :
219 : 19 : Q_GLOBAL_STATIC(DBus, dbus)
220 : :
221 : :
222 : : enum Command {
223 : : NoCommand,
224 : : StartApplication,
225 : : DebugApplication,
226 : : StopApplication,
227 : : StopAllApplications,
228 : : ListApplications,
229 : : ShowApplication,
230 : : ListPackages,
231 : : ShowPackage,
232 : : InstallPackage,
233 : : RemovePackage,
234 : : ListInstallationTasks,
235 : : CancelInstallationTask,
236 : : ListInstallationLocations,
237 : : ShowInstallationLocation,
238 : : ListInstances,
239 : : InjectIntentRequest,
240 : : };
241 : :
242 : : // REMEMBER to update the completion file util/bash/appman-prompt, if you apply changes below!
243 : : static struct {
244 : : Command command;
245 : : const char *name;
246 : : const char *description;
247 : : } commandTable[] = {
248 : : { StartApplication, "start-application", "Start an application." },
249 : : { DebugApplication, "debug-application", "Debug an application." },
250 : : { StopApplication, "stop-application", "Stop an application." },
251 : : { StopAllApplications, "stop-all-applications", "Stop all applications." },
252 : : { ListApplications, "list-applications", "List all installed applications." },
253 : : { ShowApplication, "show-application", "Show application meta-data." },
254 : : { ListPackages, "list-packages", "List all installed packages." },
255 : : { ShowPackage, "show-package", "Show package meta-data." },
256 : : { InstallPackage, "install-package", "Install a package." },
257 : : { RemovePackage, "remove-package", "Remove a package." },
258 : : { ListInstallationTasks, "list-installation-tasks", "List all active installation tasks." },
259 : : { CancelInstallationTask, "cancel-installation-task", "Cancel an active installation task." },
260 : : { ListInstallationLocations, "list-installation-locations", "List all installaton locations." },
261 : : { ShowInstallationLocation, "show-installation-location", "Show details for installation location." },
262 : : { ListInstances, "list-instances", "List all named application manager instances." },
263 : : { InjectIntentRequest, "inject-intent-request", "Inject an intent request for testing." },
264 : : };
265 : :
266 : 21 : static Command command(QCommandLineParser &clp)
267 : : {
268 [ + + ]: 21 : if (!clp.positionalArguments().isEmpty()) {
269 [ + - ]: 20 : QByteArray cmd = clp.positionalArguments().at(0).toLatin1();
270 : :
271 [ + - ]: 188 : for (uint i = 0; i < sizeof(commandTable) / sizeof(commandTable[0]); ++i) {
272 [ + + ]: 188 : if (cmd == commandTable[i].name) {
273 [ + - ]: 20 : clp.clearPositionalArguments();
274 [ + - + - ]: 40 : clp.addPositionalArgument(QString::fromLatin1(cmd),
275 [ + - ]: 40 : QString::fromLatin1(commandTable[i].description),
276 [ + - ]: 20 : QString::fromLatin1(cmd));
277 : 20 : return commandTable[i].command;
278 : : }
279 : : }
280 : 20 : }
281 : : return NoCommand;
282 : : }
283 : :
284 : : static std::pair<QString, QMultiHash<QString, int>> runningInstanceIds();
285 : : static QVariantMap resolveInstanceInfo(const QString &instanceId);
286 : :
287 : : static void startOrDebugApplication(const QString &debugWrapper, const QString &appId,
288 : : const QMap<QString, int> &stdRedirections, bool restart,
289 : : const QString &documentUrl) noexcept(false);
290 : : static void stopApplication(const QString &appId, bool forceKill = false) noexcept(false);
291 : : static void stopAllApplications() noexcept(false);
292 : : static void listApplications() noexcept(false);
293 : : static void showApplication(const QString &appId, bool asJson = false) noexcept(false);
294 : : static void listPackages() noexcept(false);
295 : : static void showPackage(const QString &packageId, bool asJson = false) noexcept(false);
296 : : static void installPackage(const QString &packageUrl, bool acknowledge) noexcept(false);
297 : : static void removePackage(const QString &packageId, bool keepDocuments, bool force) noexcept(false);
298 : : static void listInstallationTasks() noexcept(false);
299 : : static void cancelInstallationTask(bool all, const QString &singleTaskId) noexcept(false);
300 : : static void listInstallationLocations() noexcept(false);
301 : : static void showInstallationLocation(bool asJson = false) noexcept(false);
302 : : static void listInstances() noexcept(false);
303 : : static void injectIntentRequest(const QString &intentId, bool isBroadcast,
304 : : const QString &applicationId, const QString &requestingApplicationId,
305 : : const QString &jsonParameters) noexcept(false);
306 : :
307 : :
308 : 0 : class ThrowingApplication : public QCoreApplication // clazy:exclude=missing-qobject-macro
309 : : {
310 : : public:
311 : 21 : ThrowingApplication(int &argc, char **argv)
312 : 42 : : QCoreApplication(argc, argv)
313 : : { }
314 : :
315 : 21 : Exception *exception() const
316 : : {
317 : 21 : return m_exception;
318 : : }
319 : :
320 : 20 : template <typename T> void runLater(T slot)
321 : : {
322 : : // run the specified function as soon as the event loop is up and running
323 : 20 : QMetaObject::invokeMethod(this, slot, Qt::QueuedConnection);
324 : 20 : }
325 : :
326 : : protected:
327 : 146 : bool notify(QObject *o, QEvent *e) override
328 : : {
329 : 146 : try {
330 [ + + ]: 146 : return QCoreApplication::notify(o, e);
331 [ - + ]: 1 : } catch (const Exception &e) {
332 [ + - ]: 1 : m_exception = new Exception(e);
333 [ + - ]: 1 : exit(3);
334 : 1 : return true;
335 : 1 : }
336 : : }
337 : : private:
338 : : Exception *m_exception = nullptr;
339 : : };
340 : :
341 : 21 : int main(int argc, char *argv[])
342 : : {
343 [ + - ]: 21 : QCoreApplication::setApplicationName(u"Qt Application Manager Controller"_s);
344 [ + - ]: 21 : QCoreApplication::setOrganizationName(u"QtProject"_s);
345 [ + - ]: 21 : QCoreApplication::setOrganizationDomain(u"qt-project.org"_s);
346 [ + - ]: 21 : QCoreApplication::setApplicationVersion(QString::fromLatin1(QT_AM_VERSION_STR));
347 : :
348 : 21 : ensureLibDBusIsAvailable(); // this needs to happen before the QCoreApplication constructor
349 : :
350 : 21 : ThrowingApplication a(argc, argv);
351 : :
352 [ + - ]: 21 : QByteArray desc = "\n\nAvailable commands are:\n";
353 : 21 : size_t longestName = 0;
354 [ + + ]: 357 : for (uint i = 0; i < sizeof(commandTable) / sizeof(commandTable[0]); ++i)
355 [ + - + + ]: 903 : longestName = qMax(longestName, qstrlen(commandTable[i].name));
356 [ + + ]: 357 : for (uint i = 0; i < sizeof(commandTable) / sizeof(commandTable[0]); ++i) {
357 [ + - ]: 336 : desc += " ";
358 [ + - ]: 336 : desc += commandTable[i].name;
359 [ + - + - ]: 1008 : desc += QByteArray(1 + int(longestName - qstrlen(commandTable[i].name)), ' ');
360 [ + - ]: 336 : desc += commandTable[i].description;
361 [ + - ]: 336 : desc += '\n';
362 : : }
363 : :
364 [ + - ]: 21 : desc += "\nMore information about each command can be obtained by running\n" \
365 : 21 : " appman-controller <command> --help";
366 : :
367 [ + - ]: 21 : QCommandLineParser clp;
368 : :
369 [ + - + - ]: 21 : clp.addOption({ { u"instance-id"_s }, u"Connect to the named instance."_s, u"instance-id"_s });
370 [ + - ]: 21 : clp.addHelpOption();
371 [ + - ]: 21 : clp.addVersionOption();
372 : :
373 [ + - ]: 42 : clp.addPositionalArgument(u"command"_s, u"The command to execute."_s);
374 : :
375 : : // ignore unknown options for now -- the sub-commands may need them later
376 [ + - ]: 21 : clp.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments);
377 : :
378 : : // ignore the return value here, as we also accept options we don't know about yet.
379 : : // If an option is really not accepted by a command, the command specific parsing should report
380 : : // this.
381 [ + - ]: 21 : clp.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions);
382 [ + - + - ]: 21 : clp.parse(QCoreApplication::arguments());
383 : :
384 : :
385 : : // REMEMBER to update the completion file util/bash/appman-prompt, if you apply changes below!
386 : 21 : try {
387 [ + - ]: 21 : auto cmd = command(clp);
388 [ + + ]: 21 : if ((cmd != NoCommand) && (cmd != ListInstances))
389 [ + - + - : 38 : dbus()->setInstanceInfo(resolveInstanceInfo(clp.value(u"instance-id"_s)));
+ - + - ]
390 : :
391 [ + + + + : 21 : switch (cmd) {
+ + + + +
+ + + + +
+ + + - ]
392 : 1 : case NoCommand:
393 [ + - - + ]: 1 : if (clp.isSet(u"version"_s))
394 : 0 : clp.showVersion();
395 : :
396 [ + - + - : 3 : clp.setApplicationDescription(u"\n"_s + QCoreApplication::applicationName() + QString::fromLatin1(desc));
+ - + - +
- ]
397 [ + - - + ]: 1 : if (clp.isSet(u"help"_s))
398 : 0 : clp.showHelp(0);
399 : 1 : clp.showHelp(1);
400 : 1 : break;
401 : :
402 : 1 : case StartApplication: {
403 [ + - + - : 3 : clp.addOption({ { u"i"_s, u"attach-stdin"_s }, u"Attach the app's stdin to the controller's stdin"_s });
+ + - - ]
404 [ + - + - : 3 : clp.addOption({ { u"o"_s, u"attach-stdout"_s }, u"Attach the app's stdout to the controller's stdout"_s });
+ + - - ]
405 [ + - + - : 3 : clp.addOption({ { u"e"_s, u"attach-stderr"_s }, u"Attach the app's stderr to the controller's stderr"_s });
+ + - - ]
406 [ + - + - : 3 : clp.addOption({ { u"r"_s, u"restart"_s }, u"Before starting, stop the application if it is already running"_s });
+ + - - ]
407 [ + - ]: 2 : clp.addPositionalArgument(u"application-id"_s, u"The id of an installed application."_s);
408 [ + - ]: 2 : clp.addPositionalArgument(u"document-url"_s, u"The optional document-url."_s, u"[document-url]"_s);
409 [ + - ]: 1 : clp.process(a);
410 : :
411 [ + - ]: 1 : int args = int(clp.positionalArguments().size());
412 [ - + ]: 1 : if ((args < 2) || (args > 3))
413 : 0 : clp.showHelp(1);
414 : :
415 : 1 : QMap<QString, int> stdRedirections;
416 [ + - - + ]: 1 : if (clp.isSet(u"attach-stdin"_s))
417 [ # # ]: 0 : stdRedirections[u"in"_s] = 0;
418 [ + - - + ]: 1 : if (clp.isSet(u"attach-stdout"_s))
419 [ # # ]: 0 : stdRedirections[u"out"_s] = 1;
420 [ + - - + ]: 1 : if (clp.isSet(u"attach-stderr"_s))
421 [ # # ]: 0 : stdRedirections[u"err"_s] = 2;
422 [ + - ]: 1 : bool restart = clp.isSet(u"restart"_s);
423 : :
424 [ + - ]: 3 : a.runLater(std::bind(startOrDebugApplication,
425 : 2 : QString(),
426 : 2 : clp.positionalArguments().at(1),
427 : : stdRedirections,
428 : : restart,
429 [ - + - - : 3 : args == 3 ? clp.positionalArguments().at(2) : QString()));
+ - - + -
- ]
430 : 1 : break;
431 : 1 : }
432 : 1 : case DebugApplication: {
433 [ + - + - : 3 : clp.addOption({ { u"i"_s, u"attach-stdin"_s }, u"Attach the app's stdin to the controller's stdin"_s });
+ + - - ]
434 [ + - + - : 3 : clp.addOption({ { u"o"_s, u"attach-stdout"_s }, u"Attach the app's stdout to the controller's stdout"_s });
+ + - - ]
435 [ + - + - : 3 : clp.addOption({ { u"e"_s, u"attach-stderr"_s }, u"Attach the app's stderr to the controller's stderr"_s });
+ + - - ]
436 [ + - + - : 3 : clp.addOption({ { u"r"_s, u"restart"_s }, u"Before starting, stop the application if it is already running"_s });
+ + - - ]
437 [ + - ]: 2 : clp.addPositionalArgument(u"debug-wrapper"_s, u"The debug-wrapper specification."_s);
438 [ + - ]: 2 : clp.addPositionalArgument(u"application-id"_s, u"The id of an installed application."_s);
439 [ + - ]: 2 : clp.addPositionalArgument(u"document-url"_s, u"The optional document-url."_s, u"[document-url]"_s);
440 [ + - ]: 1 : clp.process(a);
441 : :
442 [ + - ]: 1 : int args = int(clp.positionalArguments().size());
443 [ - + ]: 1 : if ((args < 3) || (args > 4))
444 : 0 : clp.showHelp(1);
445 : :
446 : 1 : QMap<QString, int> stdRedirections;
447 [ + - - + ]: 1 : if (clp.isSet(u"attach-stdin"_s))
448 [ # # ]: 0 : stdRedirections[u"in"_s] = 0;
449 [ + - - + ]: 1 : if (clp.isSet(u"attach-stdout"_s))
450 [ # # ]: 0 : stdRedirections[u"out"_s] = 1;
451 [ + - - + ]: 1 : if (clp.isSet(u"attach-stderr"_s))
452 [ # # ]: 0 : stdRedirections[u"err"_s] = 2;
453 [ + - ]: 1 : bool restart = clp.isSet(u"restart"_s);
454 : :
455 [ + - ]: 3 : a.runLater(std::bind(startOrDebugApplication,
456 : 2 : clp.positionalArguments().at(1),
457 [ + - ]: 2 : clp.positionalArguments().at(2),
458 : : stdRedirections,
459 : : restart,
460 [ + - + - : 3 : args == 3 ? clp.positionalArguments().at(2) : QString()));
+ - + - -
- ]
461 : 1 : break;
462 : 1 : }
463 : 1 : case StopAllApplications:
464 [ + - ]: 1 : clp.process(a);
465 [ + - - + ]: 1 : if (clp.positionalArguments().size() != 1)
466 : 0 : clp.showHelp(1);
467 : :
468 [ + - ]: 2 : a.runLater(stopAllApplications);
469 : : break;
470 : :
471 : 1 : case StopApplication:
472 [ + - + - : 3 : clp.addOption({ { u"f"_s, u"force"_s }, u"Force kill the application."_s });
+ + - - ]
473 [ + - ]: 2 : clp.addPositionalArgument(u"application-id"_s, u"The id of an installed application."_s);
474 [ + - ]: 1 : clp.process(a);
475 : :
476 [ + - - + ]: 1 : if (clp.positionalArguments().size() != 2)
477 : 0 : clp.showHelp(1);
478 : :
479 [ + - ]: 3 : a.runLater(std::bind(stopApplication,
480 [ + - ]: 1 : clp.positionalArguments().at(1),
481 [ + - ]: 2 : clp.isSet(u"f"_s)));
482 : 1 : break;
483 : :
484 : 1 : case ListApplications:
485 [ + - ]: 1 : clp.process(a);
486 [ + - ]: 2 : a.runLater(listApplications);
487 : : break;
488 : :
489 : 1 : case ShowApplication:
490 [ + - + - ]: 1 : clp.addOption({ u"json"_s, u"Output in JSON format instead of YAML."_s });
491 [ + - ]: 2 : clp.addPositionalArgument(u"application-id"_s, u"The id of an installed application."_s);
492 [ + - ]: 1 : clp.process(a);
493 : :
494 [ + - - + ]: 1 : if (clp.positionalArguments().size() != 2)
495 : 0 : clp.showHelp(1);
496 : :
497 [ + - ]: 3 : a.runLater(std::bind(showApplication,
498 [ + - ]: 1 : clp.positionalArguments().at(1),
499 [ + - ]: 2 : clp.isSet(u"json"_s)));
500 : 1 : break;
501 : :
502 : 1 : case ListPackages:
503 [ + - ]: 1 : clp.process(a);
504 [ + - ]: 2 : a.runLater(listPackages);
505 : : break;
506 : :
507 : 1 : case ShowPackage:
508 [ + - + - ]: 1 : clp.addOption({ u"json"_s, u"Output in JSON format instead of YAML."_s });
509 [ + - ]: 2 : clp.addPositionalArgument(u"package-id"_s, u"The id of an installed package."_s);
510 [ + - ]: 1 : clp.process(a);
511 : :
512 [ + - - + ]: 1 : if (clp.positionalArguments().size() != 2)
513 : 0 : clp.showHelp(1);
514 : :
515 [ + - ]: 3 : a.runLater(std::bind(showPackage,
516 [ + - ]: 1 : clp.positionalArguments().at(1),
517 [ + - ]: 2 : clp.isSet(u"json"_s)));
518 : 1 : break;
519 : :
520 : 2 : case InstallPackage:
521 [ + - + - : 6 : clp.addOption({ { u"l"_s, u"location"_s }, u"Set a custom installation location (deprecated and ignored)."_s, u"installation-location"_s, u"internal-0"_s });
+ + - - ]
522 [ + - + - : 6 : clp.addOption({ { u"a"_s, u"acknowledge"_s }, u"Automatically acknowledge the installation (unattended mode)."_s });
+ + - - ]
523 [ + - ]: 4 : clp.addPositionalArgument(u"package"_s, u"The file name of the package; can be - for stdin."_s);
524 [ + - ]: 2 : clp.process(a);
525 : :
526 [ + - - + ]: 2 : if (clp.positionalArguments().size() != 2)
527 : 0 : clp.showHelp(1);
528 [ + - - + ]: 2 : if (clp.isSet(u"l"_s))
529 [ # # ]: 0 : fprintf(stderr, "Ignoring the deprecated -l option.\n");
530 : :
531 [ + - ]: 6 : a.runLater(std::bind(installPackage,
532 [ + - ]: 2 : clp.positionalArguments().at(1),
533 [ + - ]: 4 : clp.isSet(u"a"_s)));
534 : 2 : break;
535 : :
536 : 1 : case RemovePackage:
537 [ + - + - : 3 : clp.addOption({ { u"f"_s, u"force"_s }, u"Force removal of package."_s });
+ + - - ]
538 [ + - + - : 3 : clp.addOption({ { u"k"_s, u"keep-documents"_s }, u"Keep the document folder of the application."_s });
+ + - - ]
539 [ + - ]: 2 : clp.addPositionalArgument(u"package-id"_s, u"The id of an installed package."_s);
540 [ + - ]: 1 : clp.process(a);
541 : :
542 [ + - - + ]: 1 : if (clp.positionalArguments().size() != 2)
543 : 0 : clp.showHelp(1);
544 : :
545 [ + - ]: 3 : a.runLater(std::bind(removePackage,
546 [ + - ]: 1 : clp.positionalArguments().at(1),
547 [ + - ]: 2 : clp.isSet(u"k"_s),
548 [ + - ]: 2 : clp.isSet(u"f"_s)));
549 : 1 : break;
550 : :
551 : 2 : case ListInstallationTasks:
552 [ + - ]: 2 : clp.process(a);
553 [ + - ]: 4 : a.runLater(listInstallationTasks);
554 : : break;
555 : :
556 : 1 : case CancelInstallationTask: {
557 [ + - ]: 2 : clp.addPositionalArgument(u"task-id"_s, u"The id of an active installation task."_s);
558 [ + - + - : 3 : clp.addOption({ { u"a"_s, u"all"_s }, u"Cancel all active installation tasks."_s });
+ + - - ]
559 [ + - ]: 1 : clp.process(a);
560 : :
561 [ + - ]: 1 : qsizetype args = clp.positionalArguments().size();
562 [ + - ]: 1 : bool all = clp.isSet(u"a"_s);
563 [ - + - - : 1 : if (!(((args == 1) && all) || ((args == 2) && !all)))
+ - - + ]
564 : 0 : clp.showHelp(1);
565 : :
566 [ + - ]: 2 : a.runLater(std::bind(cancelInstallationTask,
567 : : all,
568 [ + - + - : 3 : args == 2 ? clp.positionalArguments().at(1) : QString()));
+ - - - ]
569 : 1 : break;
570 : : }
571 : 1 : case ListInstallationLocations:
572 [ + - ]: 1 : clp.process(a);
573 [ + - ]: 2 : a.runLater(listInstallationLocations);
574 : : break;
575 : :
576 : 1 : case ShowInstallationLocation:
577 [ + - ]: 2 : clp.addPositionalArgument(u"installation-location"_s, u"The id of an installation location (deprecated and ignored)."_s);
578 [ + - + - ]: 1 : clp.addOption({ u"json"_s, u"Output in JSON format instead of YAML."_s });
579 [ + - ]: 1 : clp.process(a);
580 : :
581 [ + - - + ]: 1 : if (clp.positionalArguments().size() > 2)
582 : 0 : clp.showHelp(1);
583 [ + - + - ]: 1 : if (clp.positionalArguments().size() == 2)
584 [ + - ]: 1 : fprintf(stderr, "Ignoring the deprecated installation-location.\n");
585 : :
586 [ + - ]: 2 : a.runLater(std::bind(showInstallationLocation,
587 [ + - + - ]: 2 : clp.isSet(u"json"_s)));
588 : 1 : break;
589 : :
590 : 1 : case ListInstances:
591 [ + - ]: 1 : clp.process(a);
592 [ + - ]: 2 : a.runLater(listInstances);
593 : : break;
594 : :
595 : 3 : case InjectIntentRequest:
596 [ + - ]: 6 : clp.addPositionalArgument(u"intent-id"_s, u"The id of the intent."_s);
597 [ + - ]: 6 : clp.addPositionalArgument(u"parameters"_s, u"The optional parameters for this request."_s, u"[json-parameters]"_s);
598 [ + - + - ]: 3 : clp.addOption({ u"requesting-application-id"_s, u"Fake the requesting application id."_s, u"id"_s, u":sysui:"_s });
599 [ + - + - ]: 3 : clp.addOption({ u"application-id"_s, u"Specify the handling application id."_s, u"id"_s });
600 [ + - + - ]: 3 : clp.addOption({ u"broadcast"_s, u"Create a broadcast request."_s });
601 [ + - ]: 3 : clp.process(a);
602 : :
603 [ + - ]: 3 : bool isBroadcast = clp.isSet(u"broadcast"_s);
604 [ + - ]: 23 : QString appId = clp.value(u"application-id"_s);
605 [ + - ]: 6 : QString requestingAppId = clp.value(u"requesting-application-id"_s);
606 : :
607 [ + + - + ]: 3 : if (!appId.isEmpty() && isBroadcast)
608 : 0 : throw Exception("You cannot use --application-id and --broadcast at the same time.");
609 : :
610 [ + - - + ]: 3 : if (clp.positionalArguments().size() > 3)
611 : 0 : clp.showHelp(1);
612 : :
613 : 3 : QString jsonParams;
614 [ + - + - ]: 3 : if (clp.positionalArguments().size() == 3)
615 [ + - ]: 3 : jsonParams = clp.positionalArguments().at(2);
616 : :
617 [ + - + - ]: 12 : a.runLater(std::bind(injectIntentRequest, clp.positionalArguments().at(1),
618 : : isBroadcast, requestingAppId, appId, jsonParams));
619 : 3 : break;
620 : : }
621 : :
622 [ + - ]: 20 : int result = a.exec();
623 [ + + ]: 20 : if (a.exception())
624 : 1 : throw *a.exception();
625 : : return result;
626 : :
627 [ - + ]: 1 : } catch (const Exception &e) {
628 [ + - + - ]: 3 : fprintf(stderr, "ERROR: %s\n", qPrintable(e.errorString()));
629 : 1 : return int(e.errorCode());
630 : 1 : }
631 : 20 : }
632 : :
633 : 2 : void startOrDebugApplication(const QString &debugWrapper, const QString &appId,
634 : : const QMap<QString, int> &stdRedirections, bool restart,
635 : : const QString &documentUrl = QString()) noexcept(false)
636 : : {
637 : 2 : dbus()->connectToManager();
638 : :
639 [ - + ]: 2 : if (restart) {
640 : : bool isStopped = false;
641 : :
642 : : // pass 0: normal stop / pass 1: force kill
643 [ # # ]: 0 : for (int pass = 0; !isStopped && (pass < 2); ++pass) {
644 : 0 : stopApplication(appId, pass == 0 ? false : true);
645 : :
646 : : static const int checksPerSecond = 10;
647 : :
648 : : // check if application has quit for max. 3sec
649 [ # # ]: 0 : for (int i = 0; !isStopped && (i < (3 * checksPerSecond)); ++i) {
650 : 0 : auto stateReply = dbus()->manager()->applicationRunState(appId);
651 [ # # ]: 0 : stateReply.waitForFinished();
652 [ # # # # ]: 0 : if (stateReply.isError())
653 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to get the current run-state from application manager: %1").arg(stateReply.error().message());
654 : :
655 [ # # # # ]: 0 : if (stateReply.value() == 0 /* NotRunning */)
656 : : isStopped = true;
657 : : else
658 [ # # # # ]: 0 : QThread::currentThread()->msleep(1000 / checksPerSecond);
659 : 0 : }
660 : : }
661 : :
662 [ # # ]: 0 : if (!isStopped)
663 : 0 : throw Exception("failed to stop application %1 before restarting it").arg(appId);
664 : : }
665 : :
666 : : // the async lambda below needs to share this variable
667 : 2 : static bool isStarted = false;
668 : :
669 [ - + - - ]: 2 : if (!stdRedirections.isEmpty()) {
670 : : // just bail out, if the AM or bus dies
671 : 0 : QObject::connect(dbus(), &DBus::disconnected,
672 : 0 : qApp, [](const QString &reason) {
673 : 0 : throw Exception(Error::IO, "application might not be running: lost connection to the D-Bus service (%1)").arg(reason);
674 : : });
675 : :
676 : : // in case application quits -> quit the controller
677 [ # # # # ]: 0 : QObject::connect(dbus()->manager(), &IoQtApplicationManagerInterface::applicationRunStateChanged,
678 : 0 : qApp, [appId](const QString &id, uint runState) {
679 [ # # # # : 0 : if (isStarted && id == appId && runState == 0 /* NotRunning */) {
# # ]
680 : 0 : auto getReply = dbus()->manager()->get(id);
681 [ # # ]: 0 : getReply.waitForFinished();
682 [ # # # # ]: 0 : if (getReply.isError())
683 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to get exit code from application manager: %1").arg(getReply.error().message());
684 [ # # ]: 0 : fprintf(stdout, "\n --- application has quit ---\n\n");
685 [ # # ]: 0 : auto app = getReply.value();
686 [ # # # # : 0 : qApp->exit(app.value(u"lastExitCode"_s, 1).toInt());
# # ]
687 : 0 : }
688 : 0 : });
689 : : }
690 : :
691 [ - + ]: 2 : bool isDebug = !debugWrapper.isEmpty();
692 [ - + ]: 2 : bool hasRedirections = !stdRedirections.isEmpty();
693 : : #if defined(Q_OS_WINDOWS)
694 : : if (hasRedirections) {
695 : : fprintf(stderr, "WARNING: Ignoring std-in/out/err redirections, as these are not supported on Windows.");
696 : : hasRedirections = false;
697 : : }
698 : : #endif
699 : 2 : QDBusPendingReply<bool> reply;
700 : :
701 [ + - ]: 2 : if (!hasRedirections) {
702 [ + + + - : 4 : reply = isDebug ? dbus()->manager()->debugApplication(appId, debugWrapper, documentUrl)
+ - + - +
- ]
703 : 2 : : dbus()->manager()->startApplication(appId, documentUrl);
704 : : } else {
705 : 0 : UnixFdMap fdMap;
706 [ # # # # : 0 : for (auto it = stdRedirections.cbegin(); it != stdRedirections.cend(); ++it)
# # ]
707 [ # # # # ]: 0 : fdMap.insert(it.key(), QDBusUnixFileDescriptor(it.value()));
708 : :
709 [ # # # # : 0 : reply = isDebug ? dbus()->manager()->debugApplication(appId, debugWrapper, fdMap, documentUrl)
# # # # #
# ]
710 : 0 : : dbus()->manager()->startApplication(appId, fdMap, documentUrl);
711 : 0 : }
712 : :
713 [ + - ]: 2 : reply.waitForFinished();
714 [ + - - + ]: 2 : if (reply.isError()) {
715 : 0 : throw Exception(Error::IO, "failed to call %2Application via DBus: %1")
716 [ # # # # : 0 : .arg(reply.error().message()).arg(isDebug ? "debug" : "start");
# # ]
717 : : }
718 : :
719 [ + - ]: 2 : isStarted = reply.value();
720 [ - + - - : 2 : if (stdRedirections.isEmpty() || !isStarted) {
- - ]
721 [ - + + - ]: 2 : qApp->exit(isStarted ? 0 : 2);
722 : : } else {
723 [ # # # # ]: 0 : InterruptHandler::install([appId](int) {
724 : 0 : auto stopReply = dbus()->manager()->stopApplication(appId, true);
725 [ # # ]: 0 : stopReply.waitForFinished();
726 [ # # ]: 0 : qApp->exit(1);
727 : 0 : });
728 : : }
729 : 2 : }
730 : :
731 : 1 : void stopApplication(const QString &appId, bool forceKill) noexcept(false)
732 : : {
733 : 1 : dbus()->connectToManager();
734 : :
735 : 1 : auto reply = dbus()->manager()->stopApplication(appId, forceKill);
736 [ + - ]: 1 : reply.waitForFinished();
737 [ + - - + ]: 1 : if (reply.isError())
738 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call stopApplication via DBus: %1").arg(reply.error().message());
739 [ + - ]: 1 : qApp->quit();
740 : 1 : }
741 : :
742 : 1 : void stopAllApplications() noexcept(false)
743 : : {
744 : 1 : dbus()->connectToManager();
745 : :
746 : 1 : auto reply = dbus()->manager()->stopAllApplications();
747 [ + - ]: 1 : reply.waitForFinished();
748 [ + - - + ]: 1 : if (reply.isError())
749 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call stopAllApplications via DBus: %1").arg(reply.error().message());
750 [ + - ]: 1 : qApp->quit();
751 : 1 : }
752 : :
753 : 1 : void listApplications() noexcept(false)
754 : : {
755 : 1 : dbus()->connectToManager();
756 : :
757 : 1 : auto reply = dbus()->manager()->applicationIds();
758 [ + - ]: 1 : reply.waitForFinished();
759 [ + - - + ]: 1 : if (reply.isError())
760 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call applicationIds via DBus: %1").arg(reply.error().message());
761 : :
762 [ + - ]: 1 : const auto applicationIds = reply.value();
763 [ + + ]: 3 : for (const auto &applicationId : applicationIds)
764 [ + - + - ]: 6 : fprintf(stdout, "%s\n", qPrintable(applicationId));
765 [ + - ]: 1 : qApp->quit();
766 : 1 : }
767 : :
768 : 1 : void showApplication(const QString &appId, bool asJson) noexcept(false)
769 : : {
770 : 1 : dbus()->connectToManager();
771 : :
772 : 1 : auto reply = dbus()->manager()->get(appId);
773 [ + - ]: 1 : reply.waitForFinished();
774 [ + - - + ]: 1 : if (reply.isError())
775 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to get application via DBus: %1").arg(reply.error().message());
776 : :
777 [ + - + - ]: 2 : QVariant app = convertFromDBusVariant(reply.value());
778 [ - + - - : 3 : fprintf(stdout, "%s\n", asJson ? QJsonDocument::fromVariant(app).toJson().constData()
- - - - +
- - + -
- ]
779 [ + - + - : 7 : : QtYaml::yamlFromVariantDocuments({ app }).constData());
+ - + - +
- + - + -
+ + - + -
- - - - -
- - - - ]
780 [ + - ]: 1 : qApp->quit();
781 : 1 : }
782 : :
783 : 1 : void listPackages() noexcept(false)
784 : : {
785 : 1 : dbus()->connectToPackager();
786 : :
787 : 1 : auto reply = dbus()->packager()->packageIds();
788 [ + - ]: 1 : reply.waitForFinished();
789 [ + - - + ]: 1 : if (reply.isError())
790 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call packageIds via DBus: %1").arg(reply.error().message());
791 : :
792 [ + - ]: 1 : const auto packageIds = reply.value();
793 [ + + ]: 2 : for (const auto &packageId : packageIds)
794 [ + - + - ]: 3 : fprintf(stdout, "%s\n", qPrintable(packageId));
795 [ + - ]: 1 : qApp->quit();
796 : 1 : }
797 : :
798 : 1 : void showPackage(const QString &packageId, bool asJson) noexcept(false)
799 : : {
800 : 1 : dbus()->connectToPackager();
801 : :
802 : 1 : auto reply = dbus()->packager()->get(packageId);
803 [ + - ]: 1 : reply.waitForFinished();
804 [ + - - + ]: 1 : if (reply.isError())
805 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to get package via DBus: %1").arg(reply.error().message());
806 : :
807 [ + - + - ]: 2 : QVariant package = convertFromDBusVariant(reply.value());
808 [ - + - - : 3 : fprintf(stdout, "%s\n", asJson ? QJsonDocument::fromVariant(package).toJson().constData()
- - - - +
- - + -
- ]
809 [ + - + - : 7 : : QtYaml::yamlFromVariantDocuments({ package }).constData());
+ - + - +
- + - + -
+ + - + -
- - - - -
- - - - ]
810 [ + - ]: 1 : qApp->quit();
811 : 1 : }
812 : :
813 : 2 : void installPackage(const QString &package, bool acknowledge) noexcept(false)
814 : : {
815 : 2 : QString packageFile = package;
816 : :
817 [ - + ]: 2 : if (package == u"-") { // sent via stdin
818 : 0 : bool success = false;
819 : :
820 [ # # # # ]: 0 : QTemporaryFile *tf = new QTemporaryFile(qApp);
821 [ # # ]: 0 : QFile in;
822 : :
823 [ # # # # : 0 : if (tf->open() && in.open(stdin, QIODevice::ReadOnly)) {
# # # # ]
824 [ # # ]: 0 : packageFile = tf->fileName();
825 : :
826 [ # # # # : 0 : while (!in.atEnd() && !tf->error())
# # # # ]
827 [ # # # # ]: 0 : tf->write(in.read(1024 * 1024 * 8));
828 : :
829 [ # # # # : 0 : success = in.atEnd() && !tf->error();
# # # # ]
830 [ # # ]: 0 : tf->flush();
831 : : }
832 : :
833 [ # # ]: 0 : if (!success)
834 : 0 : throw Exception(Error::IO, "Could not copy from stdin to temporary file %1").arg(package);
835 : 0 : }
836 : :
837 [ + - ]: 2 : QFileInfo fi(packageFile);
838 [ + - + - : 2 : if (!fi.exists() || !fi.isReadable() || !fi.isFile())
+ - + - +
- + - ]
839 : 0 : throw Exception(Error::IO, "Package file is not readable: %1").arg(packageFile);
840 : :
841 [ + - + - ]: 6 : fprintf(stdout, "Starting installation of package %s ...\n", qPrintable(packageFile));
842 : :
843 [ + - + - ]: 2 : dbus()->connectToManager();
844 [ + - + - ]: 2 : dbus()->connectToPackager();
845 : :
846 : : // just bail out, if the AM or bus dies
847 [ + - + - ]: 2 : QObject::connect(dbus(), &DBus::disconnected,
848 : 0 : qApp, [](const QString &reason) {
849 : 0 : throw Exception(Error::IO, "package might not be installed: lost connection to the D-Bus service (%1)").arg(reason);
850 : : });
851 : :
852 : : // all the async lambdas below need to share this variable
853 [ + - + - ]: 2 : static QString installationId;
854 : :
855 : : // as soon as we have the manifest available: get the app id and acknowledge the installation
856 [ + + ]: 2 : if (acknowledge) {
857 [ + - + - ]: 1 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskRequestingInstallationAcknowledge,
858 : 1 : qApp, [](const QString &taskId, const QVariantMap &metadata) {
859 [ - + ]: 1 : if (taskId != installationId)
860 : 0 : return;
861 [ + - + - ]: 2 : QString packageId = metadata.value(u"packageId"_s).toString();
862 [ - + ]: 1 : if (packageId.isEmpty())
863 : 0 : throw Exception(Error::IO, "could not find a valid package id in the package");
864 [ + - + - ]: 3 : fprintf(stdout, "Acknowledging package installation for '%s'...\n", qPrintable(packageId));
865 [ + - + - ]: 2 : dbus()->packager()->acknowledgePackageInstallation(taskId);
866 : 1 : });
867 : : }
868 : :
869 : : // on failure: quit
870 [ + - + - ]: 2 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskFailed,
871 : 1 : qApp, [](const QString &taskId, int errorCode, const QString &errorString) {
872 [ - + ]: 1 : if (taskId != installationId)
873 : 0 : return;
874 : 1 : throw Exception(Error::IO, "failed to install package: %1 (code: %2)").arg(errorString).arg(errorCode);
875 : : });
876 : :
877 : : // on success
878 [ + - + - ]: 2 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskFinished,
879 : 1 : qApp, [](const QString &taskId) {
880 [ + - ]: 1 : if (taskId != installationId)
881 : : return;
882 : 1 : fprintf(stdout, "Package installation finished successfully.\n");
883 : 1 : qApp->quit();
884 : : });
885 : :
886 : : // start the package installation
887 : :
888 [ + - + - : 2 : auto reply = dbus()->packager()->startPackageInstallation(fi.absoluteFilePath());
+ - ]
889 [ + - ]: 2 : reply.waitForFinished();
890 [ + - - + ]: 2 : if (reply.isError())
891 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call startPackageInstallation via DBus: %1").arg(reply.error().message());
892 : :
893 [ + - ]: 2 : installationId = reply.value();
894 [ - + ]: 2 : if (installationId.isEmpty())
895 : 0 : throw Exception(Error::IO, "startPackageInstallation returned an empty taskId");
896 : :
897 : : // cancel the job on Ctrl+C
898 : :
899 [ + - ]: 4 : InterruptHandler::install([](int) {
900 : 0 : fprintf(stdout, "Cancelling package installation.\n");
901 : 0 : auto cancelReply = dbus()->packager()->cancelTask(installationId);
902 [ # # ]: 0 : cancelReply.waitForFinished();
903 [ # # ]: 0 : qApp->exit(1);
904 : 0 : });
905 : 2 : }
906 : :
907 : 1 : void removePackage(const QString &packageId, bool keepDocuments, bool force) noexcept(false)
908 : : {
909 [ + - ]: 3 : fprintf(stdout, "Starting removal of package %s...\n", qPrintable(packageId));
910 : :
911 : 1 : dbus()->connectToManager();
912 : 1 : dbus()->connectToPackager();
913 : :
914 : : // just bail out, if the AM or bus dies
915 : 1 : QObject::connect(dbus(), &DBus::disconnected,
916 : 0 : qApp, [](const QString &reason) {
917 : 0 : throw Exception(Error::IO, "package might not be removed: lost connection to the D-Bus service (%1)").arg(reason);
918 : : });
919 : :
920 : : // both the async lambdas below need to share this variables
921 [ + - + - ]: 1 : static QString installationId;
922 : :
923 : : // on failure: quit
924 : 1 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskFailed,
925 : 0 : qApp, [](const QString &taskId, int errorCode, const QString &errorString) {
926 [ # # ]: 0 : if (taskId != installationId)
927 : 0 : return;
928 : 0 : throw Exception(Error::IO, "failed to remove package: %1 (code: %2)").arg(errorString).arg(errorCode);
929 : : });
930 : :
931 : : // on success
932 : 1 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskFinished,
933 : 1 : qApp, [](const QString &taskId) {
934 [ + - ]: 1 : if (taskId != installationId)
935 : : return;
936 : 1 : fprintf(stdout, "Package removal finished successfully.\n");
937 : 1 : qApp->quit();
938 : : });
939 : :
940 : : // start the package removal
941 : 1 : auto reply = dbus()->packager()->removePackage(packageId, keepDocuments, force);
942 [ + - ]: 1 : reply.waitForFinished();
943 [ + - - + ]: 1 : if (reply.isError())
944 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call removePackage via DBus: %1").arg(reply.error().message());
945 : :
946 [ + - ]: 1 : installationId = reply.value();
947 [ - + ]: 1 : if (installationId.isEmpty())
948 : 0 : throw Exception(Error::IO, "removePackage returned an empty taskId");
949 : 1 : }
950 : :
951 : 2 : void listInstallationTasks() noexcept(false)
952 : : {
953 : 2 : dbus()->connectToPackager();
954 : :
955 : 2 : auto reply = dbus()->packager()->activeTaskIds();
956 [ + - ]: 2 : reply.waitForFinished();
957 [ + - - + ]: 2 : if (reply.isError())
958 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call activeTaskIds via DBus: %1").arg(reply.error().message());
959 : :
960 [ + - ]: 2 : const auto taskIds = reply.value();
961 [ + + ]: 3 : for (const auto &taskId : taskIds)
962 [ + - + - ]: 3 : fprintf(stdout, "%s\n", qPrintable(taskId));
963 [ + - ]: 2 : qApp->quit();
964 : 2 : }
965 : :
966 : :
967 : 1 : void cancelInstallationTask(bool all, const QString &singleTaskId) noexcept(false)
968 : : {
969 : 1 : dbus()->connectToPackager();
970 : :
971 : : // just bail out, if the AM or bus dies
972 : 1 : QObject::connect(dbus(), &DBus::disconnected,
973 : 0 : qApp, [](const QString &reason) {
974 : 0 : throw Exception(Error::IO, "installation task(s) might not be canceled: lost connection to the D-Bus service (%1)").arg(reason);
975 : : });
976 : :
977 : : // both the async lambdas below need to share these variables
978 [ + - + - ]: 1 : static QStringList cancelTaskIds;
979 : 1 : static int result = 0;
980 : :
981 [ - + ]: 1 : if (all) {
982 : 0 : dbus()->connectToPackager();
983 : :
984 : 0 : auto reply = dbus()->packager()->activeTaskIds();
985 [ # # ]: 0 : reply.waitForFinished();
986 [ # # # # ]: 0 : if (reply.isError())
987 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call activeTaskIds via DBus: %1").arg(reply.error().message());
988 : :
989 [ # # ]: 0 : const auto taskIds = reply.value();
990 : 0 : cancelTaskIds.reserve(taskIds.size());
991 [ # # ]: 0 : for (const auto &taskId : taskIds)
992 [ # # ]: 0 : cancelTaskIds << taskId;
993 : 0 : } else {
994 : 1 : cancelTaskIds << singleTaskId;
995 : : }
996 : :
997 [ - + ]: 1 : if (cancelTaskIds.isEmpty())
998 : 0 : qApp->quit();
999 : :
1000 : : // on task failure
1001 : 1 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskFailed,
1002 : 1 : qApp, [](const QString &taskId, int errorCode, const QString &errorString) {
1003 [ + - ]: 1 : if (cancelTaskIds.removeOne(taskId)) {
1004 [ - + ]: 1 : if (errorCode != int(Error::Canceled)) {
1005 [ # # ]: 0 : fprintf(stdout, "Could not cancel task %s anymore - the installation task already failed (%s).\n",
1006 [ # # # # ]: 0 : qPrintable(taskId), qPrintable(errorString));
1007 : 0 : result |= 2;
1008 : : } else {
1009 : 1 : fprintf(stdout, "Installation task was canceled successfully.\n");
1010 : : }
1011 [ + - ]: 1 : if (cancelTaskIds.isEmpty())
1012 : 1 : qApp->exit(result);
1013 : : }
1014 : 1 : });
1015 : :
1016 : : // on success
1017 : 1 : QObject::connect(dbus()->packager(), &IoQtPackageManagerInterface::taskFinished,
1018 : 0 : qApp, [](const QString &taskId) {
1019 [ # # ]: 0 : if (cancelTaskIds.removeOne(taskId)) {
1020 [ # # ]: 0 : fprintf(stdout, "Could not cancel task %s anymore - the installation task already finished successfully.\n",
1021 [ # # ]: 0 : qPrintable(taskId));
1022 : 0 : result |= 1;
1023 [ # # ]: 0 : if (cancelTaskIds.isEmpty())
1024 : 0 : qApp->exit(result);
1025 : : }
1026 : 0 : });
1027 : :
1028 [ + + ]: 2 : for (const auto &cancelTaskId : std::as_const(cancelTaskIds)) {
1029 [ + - ]: 3 : fprintf(stdout, "Canceling installation task %s...\n", qPrintable(cancelTaskId));
1030 : :
1031 : : // cancel the task
1032 : :
1033 : 1 : auto reply = dbus()->packager()->cancelTask(cancelTaskId);
1034 [ + - ]: 1 : reply.waitForFinished();
1035 [ + - - + ]: 1 : if (reply.isError())
1036 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call cancelTask via DBus: %1").arg(reply.error().message());
1037 : :
1038 [ + - - + ]: 1 : if (!reply.value())
1039 : 0 : throw Exception(Error::IO, "failed to cancel the installation task.");
1040 : 1 : }
1041 : 1 : }
1042 : :
1043 : 1 : void listInstallationLocations() noexcept(false)
1044 : : {
1045 : 1 : dbus()->connectToPackager();
1046 : :
1047 : 1 : auto installationLocation = dbus()->packager()->installationLocation();
1048 [ + - + - ]: 1 : if (!installationLocation.isEmpty())
1049 [ + - ]: 1 : fputs("internal-0\n", stdout);
1050 [ + - ]: 1 : qApp->quit();
1051 : 1 : }
1052 : :
1053 : 1 : void showInstallationLocation(bool asJson) noexcept(false)
1054 : : {
1055 : 1 : dbus()->connectToPackager();
1056 : :
1057 : 1 : auto installationLocation = dbus()->packager()->installationLocation();
1058 [ - + - - : 3 : fprintf(stdout, "%s\n", asJson ? QJsonDocument::fromVariant(installationLocation).toJson().constData()
- - - - +
- - + - +
- - - - ]
1059 [ + - + - : 7 : : QtYaml::yamlFromVariantDocuments({ installationLocation }).constData());
+ - + - +
- + - + +
- + - - -
- - - - -
- - ]
1060 [ + - ]: 1 : qApp->quit();
1061 : 1 : }
1062 : :
1063 : 20 : static std::pair<QString, QMultiHash<QString, int>> runningInstanceIds()
1064 : : {
1065 : 20 : QMultiHash<QString, int> result;
1066 : :
1067 [ + - ]: 20 : QString rtPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
1068 [ - + ]: 20 : if (rtPath.isEmpty())
1069 [ # # ]: 0 : rtPath = QDir::tempPath();
1070 [ + - ]: 20 : QDir rtDir(rtPath);
1071 [ + - - + ]: 20 : if (!rtDir.cd(u"qtapplicationmanager"_s))
1072 [ # # ]: 0 : return { rtDir.path(), result };
1073 : :
1074 : 20 : const QString suffix = u".lock"_s;
1075 [ + - + - : 60 : QDirIterator dit(rtDir.path(), { u'*' + suffix });
+ - + + -
- ]
1076 [ + - + + ]: 40 : while (dit.hasNext()) {
1077 [ + - ]: 20 : QString path = dit.next();
1078 [ + - ]: 20 : QString name = dit.fileName();
1079 [ + - ]: 20 : name.chop(suffix.length());
1080 [ + - ]: 20 : if (auto dashPos = name.lastIndexOf(u'-'); dashPos > 0) {
1081 : 20 : bool counterOk = false;
1082 [ + - + - ]: 40 : int counter = QStringView { name }.sliced(dashPos + 1).toInt(&counterOk);
1083 [ + - ]: 20 : if (counterOk) {
1084 [ + - ]: 20 : QLockFile testLock(path);
1085 [ + - - + ]: 20 : if (testLock.tryLock(0))
1086 [ # # ]: 0 : testLock.unlock(); // stale lock
1087 [ + - - + ]: 20 : else if (testLock.error() != QLockFile::LockFailedError)
1088 [ # # # # ]: 0 : fprintf(stderr, "WARNING: unrecoverable, stale lock file: %s\n", qPrintable(path));
1089 : : else
1090 [ + - ]: 40 : result.insert(name.left(dashPos), counter);
1091 : 20 : }
1092 : : }
1093 : 20 : }
1094 [ + - ]: 20 : return { rtDir.path(), result };
1095 : 40 : }
1096 : :
1097 : 19 : static QVariantMap resolveInstanceInfo(const QString &instanceId)
1098 : : {
1099 [ + - + - ]: 19 : static const QString defaultInstanceId = u"appman"_s;
1100 [ + - + - : 19 : static QRegularExpression re(uR"(^(.+?)(?:-(\d+))?$)"_s);
+ - ]
1101 : :
1102 [ + + ]: 19 : const auto [baseDir, running] = runningInstanceIds();
1103 [ + + ]: 19 : QString iid = instanceId.isEmpty() ? defaultInstanceId : instanceId;
1104 : 19 : QString result;
1105 : :
1106 : 19 : try {
1107 : 19 : QString id;
1108 : 19 : int counter = -1;
1109 [ + - ]: 19 : auto m = re.match(iid);
1110 [ + - - + ]: 19 : if (!m.hasMatch())
1111 : 0 : throw Exception("Invalid instance-id");
1112 [ + - ]: 19 : id = m.captured(1);
1113 : 19 : bool counterOk = true;
1114 [ + - - + : 19 : counter = m.hasCaptured(2) ? int(m.captured(2).toUInt(&counterOk)) : -1;
- - - - -
+ ]
1115 [ - + ]: 19 : if (!counterOk)
1116 : 0 : throw Exception("Invalid instance-id");
1117 : :
1118 [ - + ]: 19 : if (counter >= 0) {
1119 : : // fully qualified instance id: must match exactly
1120 [ # # ]: 0 : if (running.contains(id, counter))
1121 : 0 : result = instanceId;
1122 [ + - ]: 19 : } else if (running.count(id) == 1) {
1123 : : // id only: matches if there's exactly one instance with that name
1124 [ + - + - : 19 : result = id + u'-' + QString::number(running[id]);
+ - ]
1125 [ # # # # ]: 0 : } else if (instanceId.isEmpty() && (running.count(id) == 0)
1126 [ # # # # ]: 0 : && (running.count() == 1)) {
1127 : : // no id: matches even a named instance, if that is the only instance running
1128 [ # # # # : 0 : result = running.constBegin().key() + u'-' + QString::number(running.constBegin().value());
# # ]
1129 : : }
1130 : :
1131 [ - + ]: 19 : if (result.isEmpty()) {
1132 : 0 : throw Exception("Could not resolve the given instance-id (%1) to any running appman instance.\n (did you start the appman with '--dbus none'?)")
1133 : 0 : .arg(instanceId);
1134 : : }
1135 [ - - ]: 19 : } catch (const Exception &e) {
1136 : 0 : QStringList allIds;
1137 [ - - ]: 0 : for (auto it = running.cbegin(); it != running.cend(); ++it)
1138 [ - - - - : 0 : allIds.append(it.key() + u'-' + QString::number(it.value()));
- - ]
1139 [ - - ]: 0 : throw Exception(u"%1\n\nAvailable instances:\n %2"_s.arg(e.errorString())
1140 [ - - - - ]: 0 : .arg(allIds.join(u"\n ")));
1141 : 0 : }
1142 : :
1143 [ + - + - : 38 : QFile infof(baseDir + u'/' + result + u".json"_s);
+ - + - ]
1144 [ + - - + ]: 19 : if (!infof.open(QIODevice::ReadOnly))
1145 : 0 : throw Exception(infof, "Could not open instance info file");
1146 : :
1147 : 19 : QJsonParseError jsonError;
1148 [ + - + - ]: 19 : const auto json = QJsonDocument::fromJson(infof.readAll(), &jsonError);
1149 [ + - - + ]: 19 : if (json.isNull()) {
1150 : 0 : throw Exception("Failed to parse instance info file (%1) as JSON: %2")
1151 [ # # # # ]: 0 : .arg(infof.fileName()).arg(jsonError.errorString());
1152 : : }
1153 [ + - + - ]: 19 : return json.toVariant().toMap();
1154 : 38 : }
1155 : :
1156 : 1 : void listInstances()
1157 : : {
1158 : 1 : const auto [_, running] = runningInstanceIds();
1159 [ + + ]: 2 : for (auto it = running.cbegin(); it != running.cend(); ++it) {
1160 [ + - ]: 1 : auto &name = it.key();
1161 [ + - + - ]: 3 : fprintf(stdout, "%s-%d\n", name.toLocal8Bit().constData(), it.value());
1162 : : }
1163 [ + - ]: 1 : qApp->quit();
1164 : 1 : }
1165 : :
1166 : 3 : void injectIntentRequest(const QString &intentId, bool isBroadcast,
1167 : : const QString &requestingApplicationId, const QString &applicationId,
1168 : : const QString &jsonParameters) noexcept(false)
1169 : : {
1170 : 3 : dbus()->connectToManager();
1171 : :
1172 [ + + ]: 3 : if (isBroadcast) {
1173 : 2 : auto reply = dbus()->manager()->broadcastIntentRequestAs(requestingApplicationId,
1174 : : intentId,
1175 : 2 : jsonParameters);
1176 [ + - ]: 2 : reply.waitForFinished();
1177 [ + - - + ]: 2 : if (reply.isError())
1178 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call broadcastIntentRequest via DBus: %1").arg(reply.error().message());
1179 : 2 : } else {
1180 : 1 : auto reply = dbus()->manager()->sendIntentRequestAs(requestingApplicationId,
1181 : : intentId,
1182 : : applicationId,
1183 : 1 : jsonParameters);
1184 [ + - ]: 1 : reply.waitForFinished();
1185 [ + - - + ]: 1 : if (reply.isError())
1186 [ # # # # ]: 0 : throw Exception(Error::IO, "failed to call sendIntentRequest via DBus: %1").arg(reply.error().message());
1187 [ + - ]: 1 : const auto jsonResult = reply.value();
1188 [ + - + - ]: 3 : fprintf(stdout, "%s\n", qPrintable(jsonResult));
1189 : 1 : }
1190 : :
1191 : 3 : qApp->quit();
1192 : 3 : }
1193 : :
1194 : : #include "controller.moc"
|