]>
Commit | Line | Data |
---|---|---|
118ecf32 DH |
1 | /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ |
2 | ||
3 | /*** | |
4 | This file is part of systemd. | |
5 | ||
6 | Copyright 2013 David Herrmann | |
7 | ||
8 | systemd is free software; you can redistribute it and/or modify it | |
9 | under the terms of the GNU Lesser General Public License as published by | |
10 | the Free Software Foundation; either version 2.1 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | systemd is distributed in the hope that it will be useful, but | |
14 | WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
16 | Lesser General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU Lesser General Public License | |
19 | along with systemd; If not, see <http://www.gnu.org/licenses/>. | |
20 | ***/ | |
21 | ||
22 | #include <assert.h> | |
23 | #include <fcntl.h> | |
24 | #include <libudev.h> | |
25 | #include <linux/input.h> | |
26 | #include <linux/ioctl.h> | |
27 | #include <string.h> | |
28 | #include <sys/ioctl.h> | |
29 | #include <sys/stat.h> | |
30 | #include <sys/types.h> | |
31 | #include <unistd.h> | |
32 | ||
33 | #include "dbus-common.h" | |
34 | #include "logind-session-device.h" | |
35 | #include "util.h" | |
36 | #include "missing.h" | |
37 | ||
38 | enum SessionDeviceNotifications { | |
39 | SESSION_DEVICE_RESUME, | |
40 | SESSION_DEVICE_TRY_PAUSE, | |
41 | SESSION_DEVICE_PAUSE, | |
42 | SESSION_DEVICE_RELEASE, | |
43 | }; | |
44 | ||
45 | static void session_device_notify(SessionDevice *sd, enum SessionDeviceNotifications type) { | |
46 | _cleanup_dbus_message_unref_ DBusMessage *m = NULL; | |
47 | _cleanup_free_ char *path = NULL; | |
48 | const char *t = NULL; | |
49 | ||
50 | assert(sd); | |
51 | ||
52 | if (!sd->session->controller) | |
53 | return; | |
54 | ||
55 | path = session_bus_path(sd->session); | |
56 | if (!path) | |
57 | return; | |
58 | ||
59 | m = dbus_message_new_signal(path, | |
60 | "org.freedesktop.login1.Session", | |
61 | (type == SESSION_DEVICE_RESUME) ? "ResumeDevice" : "PauseDevice"); | |
62 | if (!m) | |
63 | return; | |
64 | ||
65 | if (!dbus_message_set_destination(m, sd->session->controller)) | |
66 | return; | |
67 | ||
68 | switch (type) { | |
69 | case SESSION_DEVICE_RESUME: | |
70 | if (!dbus_message_append_args(m, | |
71 | DBUS_TYPE_UINT32, major(sd->dev), | |
72 | DBUS_TYPE_UINT32, minor(sd->dev), | |
73 | DBUS_TYPE_UNIX_FD, &sd->fd, | |
74 | DBUS_TYPE_INVALID)) | |
75 | return; | |
76 | break; | |
77 | case SESSION_DEVICE_TRY_PAUSE: | |
78 | t = "pause"; | |
79 | break; | |
80 | case SESSION_DEVICE_PAUSE: | |
81 | t = "force"; | |
82 | break; | |
83 | case SESSION_DEVICE_RELEASE: | |
84 | t = "gone"; | |
85 | break; | |
86 | default: | |
87 | return; | |
88 | } | |
89 | ||
90 | if (t && !dbus_message_append_args(m, | |
91 | DBUS_TYPE_UINT32, major(sd->dev), | |
92 | DBUS_TYPE_UINT32, minor(sd->dev), | |
93 | DBUS_TYPE_STRING, &t, | |
94 | DBUS_TYPE_INVALID)) | |
95 | return; | |
96 | ||
97 | dbus_connection_send(sd->session->manager->bus, m, NULL); | |
98 | } | |
99 | ||
100 | static int sd_eviocrevoke(int fd) { | |
101 | static bool warned; | |
102 | int r; | |
103 | ||
104 | assert(fd >= 0); | |
105 | ||
106 | r = ioctl(fd, EVIOCREVOKE, 1); | |
107 | if (r < 0) { | |
108 | r = -errno; | |
109 | if (r == -EINVAL && !warned) { | |
110 | warned = true; | |
111 | log_warning("kernel does not support evdev-revocation"); | |
112 | } | |
113 | } | |
114 | ||
115 | return 0; | |
116 | } | |
117 | ||
118 | static int sd_drmsetmaster(int fd) { | |
119 | int r; | |
120 | ||
121 | assert(fd >= 0); | |
122 | ||
123 | r = ioctl(fd, DRM_IOCTL_SET_MASTER, 0); | |
124 | if (r < 0) | |
125 | return -errno; | |
126 | ||
127 | return 0; | |
128 | } | |
129 | ||
130 | static int sd_drmdropmaster(int fd) { | |
131 | int r; | |
132 | ||
133 | assert(fd >= 0); | |
134 | ||
135 | r = ioctl(fd, DRM_IOCTL_DROP_MASTER, 0); | |
136 | if (r < 0) | |
137 | return -errno; | |
138 | ||
139 | return 0; | |
140 | } | |
141 | ||
142 | static int session_device_open(SessionDevice *sd, bool active) { | |
143 | int fd; | |
144 | ||
145 | assert(sd->type != DEVICE_TYPE_UNKNOWN); | |
146 | ||
147 | /* open device and try to get an udev_device from it */ | |
148 | fd = open(sd->node, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); | |
149 | if (fd < 0) | |
150 | return -errno; | |
151 | ||
152 | switch (sd->type) { | |
153 | case DEVICE_TYPE_DRM: | |
154 | if (active) | |
155 | sd_drmsetmaster(fd); | |
156 | else { | |
157 | /* DRM-Master is granted to the first user who opens a | |
158 | * device automatically (ughh, racy!). Hence, we just | |
159 | * drop DRM-Master in case we were the first. */ | |
160 | sd_drmdropmaster(fd); | |
161 | } | |
162 | break; | |
163 | case DEVICE_TYPE_EVDEV: | |
164 | if (!active) | |
165 | sd_eviocrevoke(fd); | |
166 | break; | |
167 | case DEVICE_TYPE_FBDEV: | |
168 | case DEVICE_TYPE_UNKNOWN: | |
169 | default: | |
170 | /* fallback for devices wihout synchronizations */ | |
171 | break; | |
172 | } | |
173 | ||
174 | return fd; | |
175 | } | |
176 | ||
177 | static int session_device_start(SessionDevice *sd) { | |
178 | int r; | |
179 | ||
180 | assert(sd); | |
181 | assert(session_is_active(sd->session)); | |
182 | ||
183 | if (sd->active) | |
184 | return 0; | |
185 | ||
186 | switch (sd->type) { | |
187 | case DEVICE_TYPE_DRM: | |
188 | /* Device is kept open. Simply call drmSetMaster() and hope | |
189 | * there is no-one else. In case it fails, we keep the device | |
190 | * paused. Maybe at some point we have a drmStealMaster(). */ | |
191 | r = sd_drmsetmaster(sd->fd); | |
192 | if (r < 0) | |
193 | return r; | |
194 | break; | |
195 | case DEVICE_TYPE_EVDEV: | |
196 | /* Evdev devices are revoked while inactive. Reopen it and we | |
197 | * are fine. */ | |
198 | r = session_device_open(sd, true); | |
199 | if (r < 0) | |
200 | return r; | |
201 | close_nointr_nofail(sd->fd); | |
202 | sd->fd = r; | |
203 | break; | |
204 | case DEVICE_TYPE_FBDEV: | |
205 | /* fbdev devices have no way to synchronize access. Moreover, | |
206 | * they mostly operate through mmaps() without any pageflips | |
207 | * and modesetting, so there is no way for us to prevent access | |
208 | * but tear down mmaps. | |
209 | * That would be quite expensive to do on a per-fd context. So | |
210 | * ignore legcy fbdev and let its users feel the pain they asked | |
211 | * for when deciding for fbdev. */ | |
212 | case DEVICE_TYPE_UNKNOWN: | |
213 | default: | |
214 | /* fallback for devices wihout synchronizations */ | |
215 | break; | |
216 | } | |
217 | ||
218 | sd->active = true; | |
219 | return 0; | |
220 | } | |
221 | ||
222 | static void session_device_stop(SessionDevice *sd) { | |
223 | assert(sd); | |
224 | ||
225 | if (!sd->active) | |
226 | return; | |
227 | ||
228 | switch (sd->type) { | |
229 | case DEVICE_TYPE_DRM: | |
230 | /* On DRM devices we simply drop DRM-Master but keep it open. | |
231 | * This allows the user to keep resources allocated. The | |
232 | * CAP_SYS_ADMIN restriction to DRM-Master prevents users from | |
233 | * circumventing this. */ | |
234 | sd_drmdropmaster(sd->fd); | |
235 | break; | |
236 | case DEVICE_TYPE_EVDEV: | |
237 | /* Revoke access on evdev file-descriptors during deactivation. | |
238 | * This will basically prevent any operations on the fd and | |
239 | * cannot be undone. Good side is: it needs no CAP_SYS_ADMIN | |
240 | * protection this way. */ | |
241 | sd_eviocrevoke(sd->fd); | |
242 | break; | |
243 | case DEVICE_TYPE_FBDEV: | |
244 | case DEVICE_TYPE_UNKNOWN: | |
245 | default: | |
246 | /* fallback for devices without synchronization */ | |
247 | break; | |
248 | } | |
249 | ||
250 | sd->active = false; | |
251 | } | |
252 | ||
253 | static DeviceType detect_device_type(struct udev_device *dev) { | |
254 | const char *sysname, *subsystem; | |
255 | DeviceType type; | |
256 | ||
257 | sysname = udev_device_get_sysname(dev); | |
258 | subsystem = udev_device_get_subsystem(dev); | |
259 | type = DEVICE_TYPE_UNKNOWN; | |
260 | ||
261 | if (streq_ptr(subsystem, "graphics")) { | |
262 | if (!streq(sysname, "fbcon") && startswith(sysname, "fb")) | |
263 | type = DEVICE_TYPE_FBDEV; | |
264 | } else if (streq_ptr(subsystem, "drm")) { | |
265 | if (startswith(sysname, "card")) | |
266 | type = DEVICE_TYPE_DRM; | |
267 | } else if (streq_ptr(subsystem, "input")) { | |
268 | if (startswith(sysname, "event")) | |
269 | type = DEVICE_TYPE_EVDEV; | |
270 | } | |
271 | ||
272 | return type; | |
273 | } | |
274 | ||
275 | static int session_device_verify(SessionDevice *sd) { | |
276 | struct udev_device *dev, *p = NULL; | |
277 | const char *sp, *node; | |
278 | int r; | |
279 | ||
280 | dev = udev_device_new_from_devnum(sd->session->manager->udev, 'c', sd->dev); | |
281 | if (!dev) | |
282 | return -ENODEV; | |
283 | ||
284 | sp = udev_device_get_syspath(dev); | |
285 | node = udev_device_get_devnode(dev); | |
286 | if (!node) { | |
287 | r = -EINVAL; | |
288 | goto err_dev; | |
289 | } | |
290 | ||
291 | /* detect device type so we can find the correct sysfs parent */ | |
292 | sd->type = detect_device_type(dev); | |
293 | if (sd->type == DEVICE_TYPE_UNKNOWN) { | |
294 | r = -ENODEV; | |
295 | goto err_dev; | |
296 | } else if (sd->type == DEVICE_TYPE_EVDEV) { | |
297 | /* for evdev devices we need the parent node as device */ | |
298 | p = dev; | |
299 | dev = udev_device_get_parent_with_subsystem_devtype(p, "input", NULL); | |
300 | if (!dev) { | |
301 | r = -ENODEV; | |
302 | goto err_dev; | |
303 | } | |
304 | sp = udev_device_get_syspath(dev); | |
305 | } else if (sd->type != DEVICE_TYPE_FBDEV && | |
306 | sd->type != DEVICE_TYPE_DRM) { | |
307 | /* Prevent opening unsupported devices. Especially devices of | |
308 | * subsystem "input" must be opened via the evdev node as | |
309 | * we require EVIOCREVOKE. */ | |
310 | r = -ENODEV; | |
311 | goto err_dev; | |
312 | } | |
313 | ||
314 | /* search for an existing seat device and return it if available */ | |
315 | sd->device = hashmap_get(sd->session->manager->devices, sp); | |
316 | if (!sd->device) { | |
317 | /* The caller might have gotten the udev event before we were | |
318 | * able to process it. Hence, fake the "add" event and let the | |
319 | * logind-manager handle the new device. */ | |
320 | r = manager_process_seat_device(sd->session->manager, dev); | |
321 | if (r < 0) | |
322 | goto err_dev; | |
323 | ||
324 | /* if it's still not available, then the device is invalid */ | |
325 | sd->device = hashmap_get(sd->session->manager->devices, sp); | |
326 | if (!sd->device) { | |
327 | r = -ENODEV; | |
328 | goto err_dev; | |
329 | } | |
330 | } | |
331 | ||
332 | if (sd->device->seat != sd->session->seat) { | |
333 | r = -EPERM; | |
334 | goto err_dev; | |
335 | } | |
336 | ||
337 | sd->node = strdup(node); | |
338 | if (!sd->node) { | |
339 | r = -ENOMEM; | |
340 | goto err_dev; | |
341 | } | |
342 | ||
343 | r = 0; | |
344 | err_dev: | |
345 | udev_device_unref(p ? : dev); | |
346 | return r; | |
347 | } | |
348 | ||
349 | int session_device_new(Session *s, dev_t dev, SessionDevice **out) { | |
350 | SessionDevice *sd; | |
351 | int r; | |
352 | ||
353 | assert(s); | |
354 | assert(out); | |
355 | ||
356 | if (!s->seat) | |
357 | return -EPERM; | |
358 | ||
359 | sd = new0(SessionDevice, 1); | |
360 | if (!sd) | |
361 | return -ENOMEM; | |
362 | ||
363 | sd->session = s; | |
364 | sd->dev = dev; | |
365 | sd->fd = -1; | |
366 | sd->type = DEVICE_TYPE_UNKNOWN; | |
367 | ||
368 | r = session_device_verify(sd); | |
369 | if (r < 0) | |
370 | goto error; | |
371 | ||
372 | assert_cc(sizeof(unsigned long) >= sizeof(dev_t)); | |
373 | ||
374 | r = hashmap_put(s->devices, ULONG_TO_PTR((unsigned long)sd->dev), sd); | |
375 | if (r < 0) { | |
376 | r = -ENOMEM; | |
377 | goto error; | |
378 | } | |
379 | ||
380 | /* Open the device for the first time. We need a valid fd to pass back | |
381 | * to the caller. If the session is not active, this _might_ immediately | |
382 | * revoke access and thus invalidate the fd. But this is still needed | |
383 | * to pass a valid fd back. */ | |
384 | sd->active = session_is_active(s); | |
385 | sd->fd = session_device_open(sd, sd->active); | |
386 | if (sd->fd < 0) | |
387 | goto error; | |
388 | ||
389 | LIST_PREPEND(SessionDevice, sd_by_device, sd->device->session_devices, sd); | |
390 | ||
391 | *out = sd; | |
392 | return 0; | |
393 | ||
394 | error: | |
395 | hashmap_remove(s->devices, ULONG_TO_PTR((unsigned long)sd->dev)); | |
396 | free(sd->node); | |
397 | free(sd); | |
398 | return r; | |
399 | } | |
400 | ||
401 | void session_device_free(SessionDevice *sd) { | |
402 | assert(sd); | |
403 | ||
404 | session_device_stop(sd); | |
405 | session_device_notify(sd, SESSION_DEVICE_RELEASE); | |
406 | close_nointr_nofail(sd->fd); | |
407 | ||
408 | LIST_REMOVE(SessionDevice, sd_by_device, sd->device->session_devices, sd); | |
409 | ||
410 | hashmap_remove(sd->session->devices, ULONG_TO_PTR((unsigned long)sd->dev)); | |
411 | ||
412 | free(sd->node); | |
413 | free(sd); | |
414 | } | |
415 | ||
416 | void session_device_complete_pause(SessionDevice *sd) { | |
d7bd01b5 DH |
417 | SessionDevice *iter; |
418 | Iterator i; | |
419 | ||
118ecf32 DH |
420 | if (!sd->active) |
421 | return; | |
422 | ||
423 | session_device_stop(sd); | |
d7bd01b5 DH |
424 | |
425 | /* if not all devices are paused, wait for further completion events */ | |
426 | HASHMAP_FOREACH(iter, sd->session->devices, i) | |
427 | if (iter->active) | |
428 | return; | |
429 | ||
430 | /* complete any pending session switch */ | |
431 | seat_complete_switch(sd->session->seat); | |
118ecf32 DH |
432 | } |
433 | ||
434 | void session_device_resume_all(Session *s) { | |
435 | SessionDevice *sd; | |
436 | Iterator i; | |
437 | int r; | |
438 | ||
439 | assert(s); | |
440 | ||
441 | HASHMAP_FOREACH(sd, s->devices, i) { | |
442 | if (!sd->active) { | |
443 | r = session_device_start(sd); | |
444 | if (!r) | |
445 | session_device_notify(sd, SESSION_DEVICE_RESUME); | |
446 | } | |
447 | } | |
448 | } | |
449 | ||
450 | void session_device_pause_all(Session *s) { | |
451 | SessionDevice *sd; | |
452 | Iterator i; | |
453 | ||
454 | assert(s); | |
455 | ||
456 | HASHMAP_FOREACH(sd, s->devices, i) { | |
457 | if (sd->active) { | |
458 | session_device_stop(sd); | |
459 | session_device_notify(sd, SESSION_DEVICE_PAUSE); | |
460 | } | |
461 | } | |
462 | } | |
d7bd01b5 DH |
463 | |
464 | unsigned int session_device_try_pause_all(Session *s) { | |
465 | SessionDevice *sd; | |
466 | Iterator i; | |
467 | unsigned int num_pending = 0; | |
468 | ||
469 | assert(s); | |
470 | ||
471 | HASHMAP_FOREACH(sd, s->devices, i) { | |
472 | if (sd->active) { | |
473 | session_device_notify(sd, SESSION_DEVICE_TRY_PAUSE); | |
474 | ++num_pending; | |
475 | } | |
476 | } | |
477 | ||
478 | return num_pending; | |
479 | } |