1/*
2 * Copyright © 2013 Red Hat Inc.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice (including the next
12 * paragraph) shall be included in all copies or substantial portions of the
13 * Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 * Author: Hans de Goede <hdegoede@redhat.com>
24 */
25
26#ifdef HAVE_XORG_CONFIG_H
27#include <xorg-config.h>
28#endif
29
30#include <dbus/dbus.h>
31#include <string.h>
32#include <sys/types.h>
33#include <unistd.h>
34
35#include "os.h"
36#include "dbus-core.h"
37#include "linux.h"
38#include "xf86.h"
39#include "xf86platformBus.h"
40#include "xf86Xinput.h"
41#include "xf86Priv.h"
42#include "globals.h"
43
44#include "systemd-logind.h"
45
46struct systemd_logind_info {
47    DBusConnection *conn;
48    char *session;
49    Bool active;
50    Bool vt_active;
51};
52
53static struct systemd_logind_info logind_info;
54
55static InputInfoPtr
56systemd_logind_find_info_ptr_by_devnum(InputInfoPtr start,
57                                       int major, int minor)
58{
59    InputInfoPtr pInfo;
60
61    for (pInfo = start; pInfo; pInfo = pInfo->next)
62        if (pInfo->major == major && pInfo->minor == minor &&
63                (pInfo->flags & XI86_SERVER_FD))
64            return pInfo;
65
66    return NULL;
67}
68
69static void
70systemd_logind_set_input_fd_for_all_devs(int major, int minor, int fd,
71                                         Bool enable)
72{
73    InputInfoPtr pInfo;
74
75    pInfo = systemd_logind_find_info_ptr_by_devnum(xf86InputDevs, major, minor);
76    while (pInfo) {
77        pInfo->fd = fd;
78        pInfo->options = xf86ReplaceIntOption(pInfo->options, "fd", fd);
79        if (enable)
80            xf86EnableInputDeviceForVTSwitch(pInfo);
81
82        pInfo = systemd_logind_find_info_ptr_by_devnum(pInfo->next, major, minor);
83    }
84}
85
86int
87systemd_logind_take_fd(int _major, int _minor, const char *path,
88                       Bool *paused_ret)
89{
90    struct systemd_logind_info *info = &logind_info;
91    InputInfoPtr pInfo;
92    DBusError error;
93    DBusMessage *msg = NULL;
94    DBusMessage *reply = NULL;
95    dbus_int32_t major = _major;
96    dbus_int32_t minor = _minor;
97    dbus_bool_t paused;
98    int fd = -1;
99
100    if (!info->session || major == 0)
101        return -1;
102
103    /* logind does not support mouse devs (with evdev we don't need them) */
104    if (strstr(path, "mouse"))
105        return -1;
106
107    /* Check if we already have an InputInfo entry with this major, minor
108     * (shared device-nodes happen ie with Wacom tablets). */
109    pInfo = systemd_logind_find_info_ptr_by_devnum(xf86InputDevs, major, minor);
110    if (pInfo) {
111        LogMessage(X_INFO, "systemd-logind: returning pre-existing fd for %s %u:%u\n",
112               path, major, minor);
113        *paused_ret = FALSE;
114        return pInfo->fd;
115    }
116
117    dbus_error_init(&error);
118
119    msg = dbus_message_new_method_call("org.freedesktop.login1", info->session,
120            "org.freedesktop.login1.Session", "TakeDevice");
121    if (!msg) {
122        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
123        goto cleanup;
124    }
125
126    if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &major,
127                                       DBUS_TYPE_UINT32, &minor,
128                                       DBUS_TYPE_INVALID)) {
129        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
130        goto cleanup;
131    }
132
133    reply = dbus_connection_send_with_reply_and_block(info->conn, msg,
134                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
135    if (!reply) {
136        LogMessage(X_ERROR, "systemd-logind: failed to take device %s: %s\n",
137                   path, error.message);
138        goto cleanup;
139    }
140
141    if (!dbus_message_get_args(reply, &error,
142                               DBUS_TYPE_UNIX_FD, &fd,
143                               DBUS_TYPE_BOOLEAN, &paused,
144                               DBUS_TYPE_INVALID)) {
145        LogMessage(X_ERROR, "systemd-logind: TakeDevice %s: %s\n",
146                   path, error.message);
147        goto cleanup;
148    }
149
150    *paused_ret = paused;
151
152    LogMessage(X_INFO, "systemd-logind: got fd for %s %u:%u fd %d paused %d\n",
153               path, major, minor, fd, paused);
154
155cleanup:
156    if (msg)
157        dbus_message_unref(msg);
158    if (reply)
159        dbus_message_unref(reply);
160    dbus_error_free(&error);
161
162    return fd;
163}
164
165void
166systemd_logind_release_fd(int _major, int _minor, int fd)
167{
168    struct systemd_logind_info *info = &logind_info;
169    InputInfoPtr pInfo;
170    DBusError error;
171    DBusMessage *msg = NULL;
172    DBusMessage *reply = NULL;
173    dbus_int32_t major = _major;
174    dbus_int32_t minor = _minor;
175    int matches = 0;
176
177    if (!info->session || major == 0)
178        goto close;
179
180    /* Only release the fd if there is only 1 InputInfo left for this major
181     * and minor, otherwise other InputInfo's are still referencing the fd. */
182    pInfo = systemd_logind_find_info_ptr_by_devnum(xf86InputDevs, major, minor);
183    while (pInfo) {
184        matches++;
185        pInfo = systemd_logind_find_info_ptr_by_devnum(pInfo->next, major, minor);
186    }
187    if (matches > 1) {
188        LogMessage(X_INFO, "systemd-logind: not releasing fd for %u:%u, still in use\n", major, minor);
189        return;
190    }
191
192    LogMessage(X_INFO, "systemd-logind: releasing fd for %u:%u\n", major, minor);
193
194    dbus_error_init(&error);
195
196    msg = dbus_message_new_method_call("org.freedesktop.login1", info->session,
197            "org.freedesktop.login1.Session", "ReleaseDevice");
198    if (!msg) {
199        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
200        goto cleanup;
201    }
202
203    if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &major,
204                                       DBUS_TYPE_UINT32, &minor,
205                                       DBUS_TYPE_INVALID)) {
206        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
207        goto cleanup;
208    }
209
210    reply = dbus_connection_send_with_reply_and_block(info->conn, msg,
211                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
212    if (!reply)
213        LogMessage(X_ERROR, "systemd-logind: failed to release device: %s\n",
214                   error.message);
215
216cleanup:
217    if (msg)
218        dbus_message_unref(msg);
219    if (reply)
220        dbus_message_unref(reply);
221    dbus_error_free(&error);
222close:
223    if (fd != -1)
224        close(fd);
225}
226
227int
228systemd_logind_controls_session(void)
229{
230    return logind_info.session ? 1 : 0;
231}
232
233void
234systemd_logind_vtenter(void)
235{
236    struct systemd_logind_info *info = &logind_info;
237    InputInfoPtr pInfo;
238    int i;
239
240    if (!info->session)
241        return; /* Not using systemd-logind */
242
243    if (!info->active)
244        return; /* Session not active */
245
246    if (info->vt_active)
247        return; /* Already did vtenter */
248
249    for (i = 0; i < xf86_num_platform_devices; i++) {
250        if (xf86_platform_devices[i].flags & XF86_PDEV_PAUSED)
251            break;
252    }
253    if (i != xf86_num_platform_devices)
254        return; /* Some drm nodes are still paused wait for resume */
255
256    xf86VTEnter();
257    info->vt_active = TRUE;
258
259    /* Activate any input devices which were resumed before the drm nodes */
260    for (pInfo = xf86InputDevs; pInfo; pInfo = pInfo->next)
261        if ((pInfo->flags & XI86_SERVER_FD) && pInfo->fd != -1)
262            xf86EnableInputDeviceForVTSwitch(pInfo);
263
264    /* Do delayed input probing, this must be done after the above enabling */
265    xf86InputEnableVTProbe();
266}
267
268static void
269systemd_logind_ack_pause(struct systemd_logind_info *info,
270                         dbus_int32_t minor, dbus_int32_t major)
271{
272    DBusError error;
273    DBusMessage *msg = NULL;
274    DBusMessage *reply = NULL;
275
276    dbus_error_init(&error);
277
278    msg = dbus_message_new_method_call("org.freedesktop.login1", info->session,
279            "org.freedesktop.login1.Session", "PauseDeviceComplete");
280    if (!msg) {
281        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
282        goto cleanup;
283    }
284
285    if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &major,
286                                       DBUS_TYPE_UINT32, &minor,
287                                       DBUS_TYPE_INVALID)) {
288        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
289        goto cleanup;
290    }
291
292    reply = dbus_connection_send_with_reply_and_block(info->conn, msg,
293                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
294    if (!reply)
295        LogMessage(X_ERROR, "systemd-logind: failed to ack pause: %s\n",
296                   error.message);
297
298cleanup:
299    if (msg)
300        dbus_message_unref(msg);
301    if (reply)
302        dbus_message_unref(reply);
303    dbus_error_free(&error);
304}
305
306/*
307 * Send a message to logind, to pause the drm device
308 * and ensure the drm_drop_master is done before
309 * VT_RELDISP when switching VT
310 */
311void systemd_logind_drop_master(void)
312{
313    int i;
314    for (i = 0; i < xf86_num_platform_devices; i++) {
315        if (xf86_platform_devices[i].flags & XF86_PDEV_SERVER_FD) {
316            dbus_int32_t major, minor;
317            struct systemd_logind_info *info = &logind_info;
318
319            xf86_platform_devices[i].flags |= XF86_PDEV_PAUSED;
320            major = xf86_platform_odev_attributes(i)->major;
321            minor = xf86_platform_odev_attributes(i)->minor;
322            systemd_logind_ack_pause(info, minor, major);
323        }
324    }
325}
326
327static Bool are_platform_devices_resumed(void) {
328    int i;
329    for (i = 0; i < xf86_num_platform_devices; i++) {
330        if (xf86_platform_devices[i].flags & XF86_PDEV_PAUSED) {
331            return FALSE;
332        }
333    }
334    return TRUE;
335}
336
337static DBusHandlerResult
338message_filter(DBusConnection * connection, DBusMessage * message, void *data)
339{
340    struct systemd_logind_info *info = data;
341    struct xf86_platform_device *pdev = NULL;
342    InputInfoPtr pInfo = NULL;
343    int ack = 0, pause = 0, fd = -1;
344    DBusError error;
345    dbus_int32_t major, minor;
346    char *pause_str;
347
348    if (strcmp(dbus_message_get_path(message), info->session) != 0)
349        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
350
351    dbus_error_init(&error);
352
353    if (dbus_message_is_signal(message, "org.freedesktop.login1.Session",
354                               "PauseDevice")) {
355        if (!dbus_message_get_args(message, &error,
356                               DBUS_TYPE_UINT32, &major,
357                               DBUS_TYPE_UINT32, &minor,
358                               DBUS_TYPE_STRING, &pause_str,
359                               DBUS_TYPE_INVALID)) {
360            LogMessage(X_ERROR, "systemd-logind: PauseDevice: %s\n",
361                       error.message);
362            dbus_error_free(&error);
363            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
364        }
365
366        if (strcmp(pause_str, "pause") == 0) {
367            pause = 1;
368            ack = 1;
369        }
370        else if (strcmp(pause_str, "force") == 0) {
371            pause = 1;
372        }
373        else if (strcmp(pause_str, "gone") == 0) {
374            /* Device removal is handled through udev */
375            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
376        }
377        else {
378            LogMessage(X_WARNING, "systemd-logind: unknown pause type: %s\n",
379                       pause_str);
380            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
381        }
382    }
383    else if (dbus_message_is_signal(message, "org.freedesktop.login1.Session",
384                                    "ResumeDevice")) {
385        if (!dbus_message_get_args(message, &error,
386                                   DBUS_TYPE_UINT32, &major,
387                                   DBUS_TYPE_UINT32, &minor,
388                                   DBUS_TYPE_UNIX_FD, &fd,
389                                   DBUS_TYPE_INVALID)) {
390            LogMessage(X_ERROR, "systemd-logind: ResumeDevice: %s\n",
391                       error.message);
392            dbus_error_free(&error);
393            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
394        }
395    } else
396        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
397
398    LogMessage(X_INFO, "systemd-logind: got %s for %u:%u\n",
399               pause ? "pause" : "resume", major, minor);
400
401    pdev = xf86_find_platform_device_by_devnum(major, minor);
402    if (!pdev)
403        pInfo = systemd_logind_find_info_ptr_by_devnum(xf86InputDevs,
404                                                       major, minor);
405    if (!pdev && !pInfo) {
406        LogMessage(X_WARNING, "systemd-logind: could not find dev %u:%u\n",
407                   major, minor);
408        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
409    }
410
411    if (pause) {
412        /* Our VT_PROCESS usage guarantees we've already given up the vt */
413        info->active = info->vt_active = FALSE;
414        /* Note the actual vtleave has already been handled by xf86Events.c */
415        if (pdev)
416            pdev->flags |= XF86_PDEV_PAUSED;
417        else {
418            close(pInfo->fd);
419            systemd_logind_set_input_fd_for_all_devs(major, minor, -1, FALSE);
420        }
421        if (ack)
422            systemd_logind_ack_pause(info, major, minor);
423    }
424    else {
425        /* info->vt_active gets set by systemd_logind_vtenter() */
426        info->active = TRUE;
427
428        if (pdev) {
429            pdev->flags &= ~XF86_PDEV_PAUSED;
430        } else
431            systemd_logind_set_input_fd_for_all_devs(major, minor, fd,
432                                                     info->vt_active);
433        /* Call vtenter if all platform devices are resumed, or if there are no platform device */
434        if (are_platform_devices_resumed())
435            systemd_logind_vtenter();
436    }
437    return DBUS_HANDLER_RESULT_HANDLED;
438}
439
440static void
441connect_hook(DBusConnection *connection, void *data)
442{
443    struct systemd_logind_info *info = data;
444    DBusError error;
445    DBusMessage *msg = NULL;
446    DBusMessage *reply = NULL;
447    dbus_int32_t arg;
448    char *session = NULL;
449
450    dbus_error_init(&error);
451
452    msg = dbus_message_new_method_call("org.freedesktop.login1",
453            "/org/freedesktop/login1", "org.freedesktop.login1.Manager",
454            "GetSessionByPID");
455    if (!msg) {
456        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
457        goto cleanup;
458    }
459
460    arg = getpid();
461    if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, &arg,
462                                  DBUS_TYPE_INVALID)) {
463        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
464        goto cleanup;
465    }
466
467    reply = dbus_connection_send_with_reply_and_block(connection, msg,
468                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
469    if (!reply) {
470        LogMessage(X_ERROR, "systemd-logind: failed to get session: %s\n",
471                   error.message);
472        goto cleanup;
473    }
474    dbus_message_unref(msg);
475
476    if (!dbus_message_get_args(reply, &error, DBUS_TYPE_OBJECT_PATH, &session,
477                               DBUS_TYPE_INVALID)) {
478        LogMessage(X_ERROR, "systemd-logind: GetSessionByPID: %s\n",
479                   error.message);
480        goto cleanup;
481    }
482    session = XNFstrdup(session);
483
484    dbus_message_unref(reply);
485    reply = NULL;
486
487
488    msg = dbus_message_new_method_call("org.freedesktop.login1",
489            session, "org.freedesktop.login1.Session", "TakeControl");
490    if (!msg) {
491        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
492        goto cleanup;
493    }
494
495    arg = FALSE; /* Don't forcibly take over over the session */
496    if (!dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &arg,
497                                  DBUS_TYPE_INVALID)) {
498        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
499        goto cleanup;
500    }
501
502    reply = dbus_connection_send_with_reply_and_block(connection, msg,
503                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
504    if (!reply) {
505        LogMessage(X_ERROR, "systemd-logind: TakeControl failed: %s\n",
506                   error.message);
507        goto cleanup;
508    }
509
510    dbus_bus_add_match(connection,
511        "type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.login1.Session',member='PauseDevice'",
512        &error);
513    if (dbus_error_is_set(&error)) {
514        LogMessage(X_ERROR, "systemd-logind: could not add match: %s\n",
515                   error.message);
516        goto cleanup;
517    }
518
519    dbus_bus_add_match(connection,
520        "type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.login1.Session',member='ResumeDevice'",
521        &error);
522    if (dbus_error_is_set(&error)) {
523        LogMessage(X_ERROR, "systemd-logind: could not add match: %s\n",
524                   error.message);
525        goto cleanup;
526    }
527
528    /*
529     * HdG: This is not useful with systemd <= 208 since the signal only
530     * contains invalidated property names there, rather than property, val
531     * pairs as it should.  Instead we just use the first resume / pause now.
532     */
533#if 0
534    snprintf(match, sizeof(match),
535        "type='signal',sender='org.freedesktop.login1',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='%s'",
536        session);
537    dbus_bus_add_match(connection, match, &error);
538    if (dbus_error_is_set(&error)) {
539        LogMessage(X_ERROR, "systemd-logind: could not add match: %s\n",
540                   error.message);
541        goto cleanup;
542    }
543#endif
544
545    if (!dbus_connection_add_filter(connection, message_filter, info, NULL)) {
546        LogMessage(X_ERROR, "systemd-logind: could not add filter: %s\n",
547                   error.message);
548        goto cleanup;
549    }
550
551    LogMessage(X_INFO, "systemd-logind: took control of session %s\n",
552               session);
553    info->conn = connection;
554    info->session = session;
555    info->vt_active = info->active = TRUE; /* The server owns the vt during init */
556    session = NULL;
557
558cleanup:
559    free(session);
560    if (msg)
561        dbus_message_unref(msg);
562    if (reply)
563        dbus_message_unref(reply);
564    dbus_error_free(&error);
565}
566
567static void
568systemd_logind_release_control(struct systemd_logind_info *info)
569{
570    DBusError error;
571    DBusMessage *msg = NULL;
572    DBusMessage *reply = NULL;
573
574    dbus_error_init(&error);
575
576    msg = dbus_message_new_method_call("org.freedesktop.login1",
577            info->session, "org.freedesktop.login1.Session", "ReleaseControl");
578    if (!msg) {
579        LogMessage(X_ERROR, "systemd-logind: out of memory\n");
580        goto cleanup;
581    }
582
583    reply = dbus_connection_send_with_reply_and_block(info->conn, msg,
584                                                      DBUS_TIMEOUT_USE_DEFAULT, &error);
585    if (!reply) {
586        LogMessage(X_ERROR, "systemd-logind: ReleaseControl failed: %s\n",
587                   error.message);
588        goto cleanup;
589    }
590
591cleanup:
592    if (msg)
593        dbus_message_unref(msg);
594    if (reply)
595        dbus_message_unref(reply);
596    dbus_error_free(&error);
597}
598
599static void
600disconnect_hook(void *data)
601{
602    struct systemd_logind_info *info = data;
603
604    free(info->session);
605    info->session = NULL;
606    info->conn = NULL;
607}
608
609static struct dbus_core_hook core_hook = {
610    .connect = connect_hook,
611    .disconnect = disconnect_hook,
612    .data = &logind_info,
613};
614
615int
616systemd_logind_init(void)
617{
618    if (!ServerIsNotSeat0() && xf86HasTTYs() && linux_parse_vt_settings(TRUE) && !linux_get_keeptty()) {
619        LogMessage(X_INFO,
620            "systemd-logind: logind integration requires -keeptty and "
621            "-keeptty was not provided, disabling logind integration\n");
622        return 1;
623    }
624
625    return dbus_core_add_hook(&core_hook);
626}
627
628void
629systemd_logind_fini(void)
630{
631    if (logind_info.session)
632        systemd_logind_release_control(&logind_info);
633
634    dbus_core_remove_hook(&core_hook);
635}
636