1 | /* |
2 | This file is part of libkdbusaddons |
3 | |
4 | SPDX-FileCopyrightText: 2011 David Faure <faure@kde.org> |
5 | SPDX-FileCopyrightText: 2011 Kevin Ottens <ervin@kde.org> |
6 | SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org> |
7 | |
8 | SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
9 | */ |
10 | |
11 | #include "kdbusservice.h" |
12 | |
13 | #include <QCoreApplication> |
14 | #include <QDebug> |
15 | |
16 | #include <QDBusConnection> |
17 | #include <QDBusConnectionInterface> |
18 | #include <QDBusReply> |
19 | |
20 | #include "FreeDesktopApplpicationIface.h" |
21 | #include "KDBusServiceIface.h" |
22 | |
23 | #include "config-kdbusaddons.h" |
24 | |
25 | #if HAVE_X11 |
26 | #include <private/qtx11extras_p.h> |
27 | #endif |
28 | |
29 | #include "kdbusaddons_debug.h" |
30 | #include "kdbusservice_adaptor.h" |
31 | #include "kdbusserviceextensions_adaptor.h" |
32 | |
33 | class KDBusServicePrivate |
34 | { |
35 | public: |
36 | KDBusServicePrivate() |
37 | : registered(false) |
38 | , exitValue(0) |
39 | { |
40 | } |
41 | |
42 | QString generateServiceName() |
43 | { |
44 | const QCoreApplication *app = QCoreApplication::instance(); |
45 | const QString domain = app->organizationDomain(); |
46 | const QStringList parts = domain.split(sep: QLatin1Char('.'), behavior: Qt::SkipEmptyParts); |
47 | |
48 | QString reversedDomain; |
49 | if (parts.isEmpty()) { |
50 | reversedDomain = QStringLiteral("local." ); |
51 | } else { |
52 | for (const QString &part : parts) { |
53 | reversedDomain.prepend(c: QLatin1Char('.')); |
54 | reversedDomain.prepend(s: part); |
55 | } |
56 | } |
57 | |
58 | return reversedDomain + app->applicationName(); |
59 | } |
60 | |
61 | static void handlePlatformData(const QVariantMap &platformData) |
62 | { |
63 | #if HAVE_X11 |
64 | if (QX11Info::isPlatformX11()) { |
65 | QByteArray desktopStartupId = platformData.value(QStringLiteral("desktop-startup-id" )).toByteArray(); |
66 | if (!desktopStartupId.isEmpty()) { |
67 | QX11Info::setNextStartupId(desktopStartupId); |
68 | } |
69 | } |
70 | #endif |
71 | |
72 | const auto xdgActivationToken = platformData.value(key: QLatin1String("activation-token" )).toByteArray(); |
73 | if (!xdgActivationToken.isEmpty()) { |
74 | qputenv(varName: "XDG_ACTIVATION_TOKEN" , value: xdgActivationToken); |
75 | } |
76 | } |
77 | |
78 | bool registered; |
79 | QString serviceName; |
80 | QString errorMessage; |
81 | int exitValue; |
82 | }; |
83 | |
84 | // Wraps a serviceName registration. |
85 | class Registration : public QObject |
86 | { |
87 | Q_OBJECT |
88 | public: |
89 | enum class Register { |
90 | RegisterWitoutQueue, |
91 | RegisterWithQueue, |
92 | }; |
93 | |
94 | Registration(KDBusService *s_, KDBusServicePrivate *d_, KDBusService::StartupOptions options_) |
95 | : s(s_) |
96 | , d(d_) |
97 | , options(options_) |
98 | { |
99 | if (!QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) { |
100 | d->errorMessage = QLatin1String( |
101 | "DBus session bus not found. To circumvent this problem try the following command (with bash):\n" |
102 | " export $(dbus-launch)" ); |
103 | } else { |
104 | generateServiceName(); |
105 | } |
106 | } |
107 | |
108 | void run() |
109 | { |
110 | if (bus) { |
111 | registerOnBus(); |
112 | } |
113 | |
114 | if (!d->registered && ((options & KDBusService::NoExitOnFailure) == 0)) { |
115 | qCCritical(KDBUSADDONS_LOG) << qPrintable(d->errorMessage); |
116 | exit(status: 1); |
117 | } |
118 | } |
119 | |
120 | private: |
121 | void generateServiceName() |
122 | { |
123 | d->serviceName = d->generateServiceName(); |
124 | objectPath = QLatin1Char('/') + d->serviceName; |
125 | objectPath.replace(before: QLatin1Char('.'), after: QLatin1Char('/')); |
126 | objectPath.replace(before: QLatin1Char('-'), after: QLatin1Char('_')); // see spec change at https://bugs.freedesktop.org/show_bug.cgi?id=95129 |
127 | |
128 | if (options & KDBusService::Multiple) { |
129 | const bool inSandbox = QFileInfo::exists(QStringLiteral("/.flatpak-info" )); |
130 | if (inSandbox) { |
131 | d->serviceName += QStringLiteral(".kdbus-" ) |
132 | + QDBusConnection::sessionBus().baseService().replace(re: QRegularExpression(QStringLiteral("[\\.:]" )), QStringLiteral("_" )); |
133 | } else { |
134 | d->serviceName += QLatin1Char('-') + QString::number(QCoreApplication::applicationPid()); |
135 | } |
136 | } |
137 | } |
138 | |
139 | void registerOnBus() |
140 | { |
141 | auto bus = QDBusConnection::sessionBus(); |
142 | bool objectRegistered = false; |
143 | objectRegistered = bus.registerObject(QStringLiteral("/MainApplication" ), |
144 | object: QCoreApplication::instance(), |
145 | options: QDBusConnection::ExportAllSlots // |
146 | | QDBusConnection::ExportScriptableProperties // |
147 | | QDBusConnection::ExportAdaptors); |
148 | if (!objectRegistered) { |
149 | qCWarning(KDBUSADDONS_LOG) << "Failed to register /MainApplication on DBus" ; |
150 | return; |
151 | } |
152 | |
153 | objectRegistered = bus.registerObject(path: objectPath, object: s, options: QDBusConnection::ExportAdaptors); |
154 | if (!objectRegistered) { |
155 | qCWarning(KDBUSADDONS_LOG) << "Failed to register" << objectPath << "on DBus" ; |
156 | return; |
157 | } |
158 | |
159 | attemptRegistration(); |
160 | |
161 | if (d->registered) { |
162 | if (QCoreApplication *app = QCoreApplication::instance()) { |
163 | connect(sender: app, signal: &QCoreApplication::aboutToQuit, context: s, slot: &KDBusService::unregister); |
164 | } |
165 | } |
166 | } |
167 | |
168 | void attemptRegistration() |
169 | { |
170 | Q_ASSERT(!d->registered); |
171 | |
172 | auto queueOption = QDBusConnectionInterface::DontQueueService; |
173 | |
174 | if (options & KDBusService::Unique) { |
175 | // When a process crashes and gets auto-restarted by KCrash we may |
176 | // be in this code path "too early". There is a bit of a delay |
177 | // between the restart and the previous process dropping off of the |
178 | // bus and thus releasing its registered names. As a result there |
179 | // is a good chance that if we wait a bit the name will shortly |
180 | // become registered. |
181 | |
182 | queueOption = QDBusConnectionInterface::QueueService; |
183 | |
184 | connect(sender: bus, signal: &QDBusConnectionInterface::serviceRegistered, context: this, slot: [this](const QString &service) { |
185 | if (service != d->serviceName) { |
186 | return; |
187 | } |
188 | |
189 | d->registered = true; |
190 | registrationLoop.quit(); |
191 | }); |
192 | } |
193 | |
194 | d->registered = (bus->registerService(serviceName: d->serviceName, qoption: queueOption) == QDBusConnectionInterface::ServiceRegistered); |
195 | |
196 | if (d->registered) { |
197 | return; |
198 | } |
199 | |
200 | if (options & KDBusService::Replace) { |
201 | auto message = QDBusMessage::createMethodCall(destination: d->serviceName, |
202 | QStringLiteral("/MainApplication" ), |
203 | QStringLiteral("org.qtproject.Qt.QCoreApplication" ), |
204 | QStringLiteral("quit" )); |
205 | QDBusConnection::sessionBus().asyncCall(message); |
206 | waitForRegistration(); |
207 | } else if (options & KDBusService::Unique) { |
208 | // Already running so it's ok! |
209 | QVariantMap platform_data; |
210 | #if HAVE_X11 |
211 | if (QX11Info::isPlatformX11()) { |
212 | QString startupId = QString::fromUtf8(ba: qgetenv(varName: "DESKTOP_STARTUP_ID" )); |
213 | if (startupId.isEmpty()) { |
214 | startupId = QString::fromUtf8(ba: QX11Info::nextStartupId()); |
215 | } |
216 | if (!startupId.isEmpty()) { |
217 | platform_data.insert(QStringLiteral("desktop-startup-id" ), value: startupId); |
218 | } |
219 | } |
220 | #endif |
221 | |
222 | if (qEnvironmentVariableIsSet(varName: "XDG_ACTIVATION_TOKEN" )) { |
223 | platform_data.insert(QStringLiteral("activation-token" ), value: qgetenv(varName: "XDG_ACTIVATION_TOKEN" )); |
224 | } |
225 | |
226 | if (QCoreApplication::arguments().count() > 1) { |
227 | OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus()); |
228 | iface.setTimeout(5 * 60 * 1000); // Application can take time to answer |
229 | QDBusReply<int> reply = iface.CommandLine(arguments: QCoreApplication::arguments(), working_dir: QDir::currentPath(), platform_data); |
230 | if (reply.isValid()) { |
231 | exit(status: reply.value()); |
232 | } else { |
233 | d->errorMessage = reply.error().message(); |
234 | } |
235 | } else { |
236 | OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus()); |
237 | iface.setTimeout(5 * 60 * 1000); // Application can take time to answer |
238 | QDBusReply<void> reply = iface.Activate(platform_data); |
239 | if (reply.isValid()) { |
240 | exit(status: 0); |
241 | } else { |
242 | d->errorMessage = reply.error().message(); |
243 | } |
244 | } |
245 | |
246 | // service did not respond in a valid way.... |
247 | // let's wait to see if our queued registration finishes perhaps. |
248 | waitForRegistration(); |
249 | } |
250 | |
251 | if (!d->registered) { // either multi service or failed to reclaim name |
252 | d->errorMessage = QLatin1String("Failed to register name '" ) + d->serviceName + QLatin1String("' with DBUS - does this process have permission to use the name, and do no other processes own it already?" ); |
253 | } |
254 | } |
255 | |
256 | void waitForRegistration() |
257 | { |
258 | QTimer quitTimer; |
259 | // Wait a bit longer when we know this instance was restarted. There's |
260 | // a very good chance we'll eventually get the name once the defunct |
261 | // process closes its sockets. |
262 | quitTimer.start(msec: qEnvironmentVariableIsSet(varName: "KCRASH_AUTO_RESTARTED" ) ? 8000 : 2000); |
263 | connect(sender: &quitTimer, signal: &QTimer::timeout, context: ®istrationLoop, slot: &QEventLoop::quit); |
264 | registrationLoop.exec(); |
265 | } |
266 | |
267 | QDBusConnectionInterface *bus = nullptr; |
268 | KDBusService *s = nullptr; |
269 | KDBusServicePrivate *d = nullptr; |
270 | KDBusService::StartupOptions options; |
271 | QEventLoop registrationLoop; |
272 | QString objectPath; |
273 | }; |
274 | |
275 | KDBusService::KDBusService(StartupOptions options, QObject *parent) |
276 | : QObject(parent) |
277 | , d(new KDBusServicePrivate) |
278 | { |
279 | new KDBusServiceAdaptor(this); |
280 | new KDBusServiceExtensionsAdaptor(this); |
281 | |
282 | Registration registration(this, d.get(), options); |
283 | registration.run(); |
284 | } |
285 | |
286 | KDBusService::~KDBusService() = default; |
287 | |
288 | bool KDBusService::isRegistered() const |
289 | { |
290 | return d->registered; |
291 | } |
292 | |
293 | QString KDBusService::errorMessage() const |
294 | { |
295 | return d->errorMessage; |
296 | } |
297 | |
298 | void KDBusService::setExitValue(int value) |
299 | { |
300 | d->exitValue = value; |
301 | } |
302 | |
303 | QString KDBusService::serviceName() const |
304 | { |
305 | return d->serviceName; |
306 | } |
307 | |
308 | void KDBusService::unregister() |
309 | { |
310 | QDBusConnectionInterface *bus = nullptr; |
311 | if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) { |
312 | return; |
313 | } |
314 | bus->unregisterService(serviceName: d->serviceName); |
315 | } |
316 | |
317 | void KDBusService::Activate(const QVariantMap &platform_data) |
318 | { |
319 | d->handlePlatformData(platformData: platform_data); |
320 | Q_EMIT activateRequested(arguments: QStringList(QCoreApplication::arguments()[0]), workingDirectory: QDir::currentPath()); |
321 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
322 | } |
323 | |
324 | void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data) |
325 | { |
326 | d->handlePlatformData(platformData: platform_data); |
327 | Q_EMIT openRequested(uris: QUrl::fromStringList(uris)); |
328 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
329 | } |
330 | |
331 | void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data) |
332 | { |
333 | d->handlePlatformData(platformData: platform_data); |
334 | |
335 | // This is a workaround for D-Bus not supporting null variants. |
336 | const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant(); |
337 | |
338 | Q_EMIT activateActionRequested(actionName: action_name, parameter: param); |
339 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
340 | } |
341 | |
342 | int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data) |
343 | { |
344 | d->exitValue = 0; |
345 | d->handlePlatformData(platformData: platform_data); |
346 | // The TODOs here only make sense if this method can be called from the GUI. |
347 | // If it's for pure "usage in the terminal" then no startup notification got started. |
348 | // But maybe one day the workspace wants to call this for the Exec key of a .desktop file? |
349 | Q_EMIT activateRequested(arguments, workingDirectory); |
350 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
351 | return d->exitValue; |
352 | } |
353 | |
354 | #include "kdbusservice.moc" |
355 | #include "moc_kdbusservice.cpp" |
356 | |