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 | |
162 | void attemptRegistration() |
163 | { |
164 | Q_ASSERT(!d->registered); |
165 | |
166 | auto queueOption = QDBusConnectionInterface::DontQueueService; |
167 | |
168 | if (options & KDBusService::Unique) { |
169 | // When a process crashes and gets auto-restarted by KCrash we may |
170 | // be in this code path "too early". There is a bit of a delay |
171 | // between the restart and the previous process dropping off of the |
172 | // bus and thus releasing its registered names. As a result there |
173 | // is a good chance that if we wait a bit the name will shortly |
174 | // become registered. |
175 | |
176 | queueOption = QDBusConnectionInterface::QueueService; |
177 | |
178 | connect(sender: bus, signal: &QDBusConnectionInterface::serviceRegistered, context: this, slot: [this](const QString &service) { |
179 | if (service != d->serviceName) { |
180 | return; |
181 | } |
182 | |
183 | d->registered = true; |
184 | registrationLoop.quit(); |
185 | }); |
186 | } |
187 | |
188 | d->registered = (bus->registerService(serviceName: d->serviceName, qoption: queueOption) == QDBusConnectionInterface::ServiceRegistered); |
189 | |
190 | if (d->registered) { |
191 | return; |
192 | } |
193 | |
194 | if (options & KDBusService::Replace) { |
195 | auto message = QDBusMessage::createMethodCall(destination: d->serviceName, |
196 | QStringLiteral("/MainApplication" ), |
197 | QStringLiteral("org.qtproject.Qt.QCoreApplication" ), |
198 | QStringLiteral("quit" )); |
199 | QDBusConnection::sessionBus().asyncCall(message); |
200 | waitForRegistration(); |
201 | } else if (options & KDBusService::Unique) { |
202 | // Already running so it's ok! |
203 | QVariantMap platform_data; |
204 | #if HAVE_X11 |
205 | if (QX11Info::isPlatformX11()) { |
206 | QString startupId = QString::fromUtf8(ba: qgetenv(varName: "DESKTOP_STARTUP_ID" )); |
207 | if (startupId.isEmpty()) { |
208 | startupId = QString::fromUtf8(ba: QX11Info::nextStartupId()); |
209 | } |
210 | if (!startupId.isEmpty()) { |
211 | platform_data.insert(QStringLiteral("desktop-startup-id" ), value: startupId); |
212 | } |
213 | } |
214 | #endif |
215 | |
216 | if (qEnvironmentVariableIsSet(varName: "XDG_ACTIVATION_TOKEN" )) { |
217 | platform_data.insert(QStringLiteral("activation-token" ), value: qgetenv(varName: "XDG_ACTIVATION_TOKEN" )); |
218 | } |
219 | |
220 | if (QCoreApplication::arguments().count() > 1) { |
221 | OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus()); |
222 | iface.setTimeout(5 * 60 * 1000); // Application can take time to answer |
223 | QDBusReply<int> reply = iface.CommandLine(arguments: QCoreApplication::arguments(), working_dir: QDir::currentPath(), platform_data); |
224 | if (reply.isValid()) { |
225 | exit(status: reply.value()); |
226 | } else { |
227 | d->errorMessage = reply.error().message(); |
228 | } |
229 | } else { |
230 | OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus()); |
231 | iface.setTimeout(5 * 60 * 1000); // Application can take time to answer |
232 | QDBusReply<void> reply = iface.Activate(platform_data); |
233 | if (reply.isValid()) { |
234 | exit(status: 0); |
235 | } else { |
236 | d->errorMessage = reply.error().message(); |
237 | } |
238 | } |
239 | |
240 | // service did not respond in a valid way.... |
241 | // let's wait to see if our queued registration finishes perhaps. |
242 | waitForRegistration(); |
243 | } |
244 | |
245 | if (!d->registered) { // either multi service or failed to reclaim name |
246 | 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?" ); |
247 | } |
248 | } |
249 | |
250 | void waitForRegistration() |
251 | { |
252 | QTimer quitTimer; |
253 | // We have to wait for the other application to quit completely which could take a while |
254 | quitTimer.start(msec: 8000); |
255 | connect(sender: &quitTimer, signal: &QTimer::timeout, context: ®istrationLoop, slot: &QEventLoop::quit); |
256 | registrationLoop.exec(); |
257 | } |
258 | |
259 | QDBusConnectionInterface *bus = nullptr; |
260 | KDBusService *s = nullptr; |
261 | KDBusServicePrivate *d = nullptr; |
262 | KDBusService::StartupOptions options; |
263 | QEventLoop registrationLoop; |
264 | QString objectPath; |
265 | }; |
266 | |
267 | KDBusService::KDBusService(StartupOptions options, QObject *parent) |
268 | : QObject(parent) |
269 | , d(new KDBusServicePrivate) |
270 | { |
271 | new KDBusServiceAdaptor(this); |
272 | new KDBusServiceExtensionsAdaptor(this); |
273 | |
274 | Registration registration(this, d.get(), options); |
275 | registration.run(); |
276 | } |
277 | |
278 | KDBusService::~KDBusService() = default; |
279 | |
280 | bool KDBusService::isRegistered() const |
281 | { |
282 | return d->registered; |
283 | } |
284 | |
285 | QString KDBusService::errorMessage() const |
286 | { |
287 | return d->errorMessage; |
288 | } |
289 | |
290 | void KDBusService::setExitValue(int value) |
291 | { |
292 | d->exitValue = value; |
293 | } |
294 | |
295 | QString KDBusService::serviceName() const |
296 | { |
297 | return d->serviceName; |
298 | } |
299 | |
300 | void KDBusService::unregister() |
301 | { |
302 | QDBusConnectionInterface *bus = nullptr; |
303 | if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) { |
304 | return; |
305 | } |
306 | bus->unregisterService(serviceName: d->serviceName); |
307 | } |
308 | |
309 | void KDBusService::Activate(const QVariantMap &platform_data) |
310 | { |
311 | d->handlePlatformData(platformData: platform_data); |
312 | Q_EMIT activateRequested(arguments: QStringList(QCoreApplication::arguments()[0]), workingDirectory: QDir::currentPath()); |
313 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
314 | } |
315 | |
316 | void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data) |
317 | { |
318 | d->handlePlatformData(platformData: platform_data); |
319 | Q_EMIT openRequested(uris: QUrl::fromStringList(uris)); |
320 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
321 | } |
322 | |
323 | void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data) |
324 | { |
325 | d->handlePlatformData(platformData: platform_data); |
326 | |
327 | // This is a workaround for D-Bus not supporting null variants. |
328 | const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant(); |
329 | |
330 | Q_EMIT activateActionRequested(actionName: action_name, parameter: param); |
331 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
332 | } |
333 | |
334 | int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data) |
335 | { |
336 | d->exitValue = 0; |
337 | d->handlePlatformData(platformData: platform_data); |
338 | // The TODOs here only make sense if this method can be called from the GUI. |
339 | // If it's for pure "usage in the terminal" then no startup notification got started. |
340 | // But maybe one day the workspace wants to call this for the Exec key of a .desktop file? |
341 | Q_EMIT activateRequested(arguments, workingDirectory); |
342 | qunsetenv(varName: "XDG_ACTIVATION_TOKEN" ); |
343 | return d->exitValue; |
344 | } |
345 | |
346 | #include "kdbusservice.moc" |
347 | #include "moc_kdbusservice.cpp" |
348 | |