/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include #include #include "sd-messages.h" #include "alloc-util.h" #include "devnode-acl.h" #include "errno-util.h" #include "fd-util.h" #include "fileio.h" #include "format-util.h" #include "fs-util.h" #include "logind-seat-dbus.h" #include "logind-seat.h" #include "logind-session-dbus.h" #include "mkdir-label.h" #include "parse-util.h" #include "path-util.h" #include "stdio-util.h" #include "string-util.h" #include "terminal-util.h" #include "tmpfile-util.h" int seat_new(Seat** ret, Manager *m, const char *id) { _cleanup_(seat_freep) Seat *s = NULL; int r; assert(ret); assert(m); assert(id); if (!seat_name_is_valid(id)) return -EINVAL; s = new(Seat, 1); if (!s) return -ENOMEM; *s = (Seat) { .manager = m, }; s->state_file = path_join("/run/systemd/seats", id); if (!s->state_file) return -ENOMEM; s->id = basename(s->state_file); r = hashmap_put(m->seats, s->id, s); if (r < 0) return r; *ret = TAKE_PTR(s); return 0; } Seat* seat_free(Seat *s) { if (!s) return NULL; if (s->in_gc_queue) LIST_REMOVE(gc_queue, s->manager->seat_gc_queue, s); while (s->sessions) session_free(s->sessions); assert(!s->active); while (s->devices) device_free(s->devices); hashmap_remove(s->manager->seats, s->id); free(s->positions); free(s->state_file); return mfree(s); } int seat_save(Seat *s) { _cleanup_(unlink_and_freep) char *temp_path = NULL; _cleanup_fclose_ FILE *f = NULL; int r; assert(s); if (!s->started) return 0; r = mkdir_safe_label("/run/systemd/seats", 0755, 0, 0, MKDIR_WARN_MODE); if (r < 0) goto fail; r = fopen_temporary(s->state_file, &f, &temp_path); if (r < 0) goto fail; (void) fchmod(fileno(f), 0644); fprintf(f, "# This is private data. Do not parse.\n" "IS_SEAT0=%i\n" "CAN_MULTI_SESSION=1\n" "CAN_TTY=%i\n" "CAN_GRAPHICAL=%i\n", seat_is_seat0(s), seat_can_tty(s), seat_can_graphical(s)); if (s->active) { assert(s->active->user); fprintf(f, "ACTIVE=%s\n" "ACTIVE_UID="UID_FMT"\n", s->active->id, s->active->user->user_record->uid); } if (s->sessions) { fputs("SESSIONS=", f); LIST_FOREACH(sessions_by_seat, i, s->sessions) { fprintf(f, "%s%c", i->id, i->sessions_by_seat_next ? ' ' : '\n'); } fputs("UIDS=", f); LIST_FOREACH(sessions_by_seat, i, s->sessions) fprintf(f, UID_FMT"%c", i->user->user_record->uid, i->sessions_by_seat_next ? ' ' : '\n'); } r = fflush_and_check(f); if (r < 0) goto fail; if (rename(temp_path, s->state_file) < 0) { r = -errno; goto fail; } temp_path = mfree(temp_path); return 0; fail: (void) unlink(s->state_file); return log_error_errno(r, "Failed to save seat data %s: %m", s->state_file); } int seat_load(Seat *s) { assert(s); /* There isn't actually anything to read here ... */ return 0; } static int vt_allocate(unsigned vtnr) { char p[sizeof("/dev/tty") + DECIMAL_STR_MAX(unsigned)]; _cleanup_close_ int fd = -EBADF; assert(vtnr >= 1); xsprintf(p, "/dev/tty%u", vtnr); fd = open_terminal(p, O_RDWR|O_NOCTTY|O_CLOEXEC); if (fd < 0) return fd; return 0; } int seat_preallocate_vts(Seat *s) { int r = 0; unsigned i; assert(s); assert(s->manager); if (s->manager->n_autovts <= 0) return 0; if (!seat_has_vts(s)) return 0; log_debug("Preallocating VTs..."); for (i = 1; i <= s->manager->n_autovts; i++) { int q; q = vt_allocate(i); if (q < 0) r = log_error_errno(q, "Failed to preallocate VT %u: %m", i); } return r; } int seat_apply_acls(Seat *s, Session *old_active) { int r; assert(s); r = devnode_acl_all(s->id, false, !!old_active, old_active ? old_active->user->user_record->uid : 0, !!s->active, s->active ? s->active->user->user_record->uid : 0); if (r < 0) return log_error_errno(r, "Failed to apply ACLs: %m"); return 0; } int seat_set_active(Seat *s, Session *session) { Session *old_active; assert(s); assert(!session || session->seat == s); if (session == s->active) return 0; old_active = s->active; s->active = session; if (old_active) { session_device_pause_all(old_active); session_send_changed(old_active, "Active", NULL); } (void) seat_apply_acls(s, old_active); if (session && session->started) { session_send_changed(session, "Active", NULL); session_device_resume_all(session); } if (!session || session->started) seat_send_changed(s, "ActiveSession", NULL); seat_save(s); if (session) { session_save(session); user_save(session->user); } if (old_active) { session_save(old_active); if (!session || session->user != old_active->user) user_save(old_active->user); } return 0; } static Session* seat_get_position(Seat *s, unsigned pos) { assert(s); if (pos >= MALLOC_ELEMENTSOF(s->positions)) return NULL; return s->positions[pos]; } int seat_switch_to(Seat *s, unsigned num) { Session *session; /* Public session positions skip 0 (there is only F1-F12). Maybe it * will get reassigned in the future, so return error for now. */ if (num == 0) return -EINVAL; session = seat_get_position(s, num); if (!session) { /* allow switching to unused VTs to trigger auto-activate */ if (seat_has_vts(s) && num < 64) return chvt(num); return -EINVAL; } return session_activate(session); } int seat_switch_to_next(Seat *s) { unsigned start, i; Session *session; if (MALLOC_ELEMENTSOF(s->positions) == 0) return -EINVAL; start = 1; if (s->active && s->active->position > 0) start = s->active->position; for (i = start + 1; i < MALLOC_ELEMENTSOF(s->positions); ++i) { session = seat_get_position(s, i); if (session) return session_activate(session); } for (i = 1; i < start; ++i) { session = seat_get_position(s, i); if (session) return session_activate(session); } return -EINVAL; } int seat_switch_to_previous(Seat *s) { if (MALLOC_ELEMENTSOF(s->positions) == 0) return -EINVAL; size_t start = s->active && s->active->position > 0 ? s->active->position : 1; for (size_t i = start - 1; i > 0; i--) { Session *session = seat_get_position(s, i); if (session) return session_activate(session); } for (size_t i = MALLOC_ELEMENTSOF(s->positions) - 1; i > start; i--) { Session *session = seat_get_position(s, i); if (session) return session_activate(session); } return -EINVAL; } int seat_active_vt_changed(Seat *s, unsigned vtnr) { Session *new_active = NULL; int r; assert(s); assert(vtnr >= 1); if (!seat_has_vts(s)) return -EINVAL; log_debug("VT changed to %u", vtnr); /* we might have earlier closing sessions on the same VT, so try to * find a running one first */ LIST_FOREACH(sessions_by_seat, i, s->sessions) if (i->vtnr == vtnr && !i->stopping) { new_active = i; break; } if (!new_active) /* no running one? then we can't decide which one is the * active one, let the first one win */ LIST_FOREACH(sessions_by_seat, i, s->sessions) if (i->vtnr == vtnr) { new_active = i; break; } r = seat_set_active(s, new_active); manager_spawn_autovt(s->manager, vtnr); return r; } int seat_read_active_vt(Seat *s) { char t[64]; ssize_t k; int vtnr; assert(s); if (!seat_has_vts(s)) return 0; if (lseek(s->manager->console_active_fd, SEEK_SET, 0) < 0) return log_error_errno(errno, "lseek on console_active_fd failed: %m"); errno = 0; k = read(s->manager->console_active_fd, t, sizeof(t)-1); if (k <= 0) return log_error_errno(errno ?: EIO, "Failed to read current console: %s", STRERROR_OR_EOF(errno)); t[k] = 0; truncate_nl(t); vtnr = vtnr_from_tty(t); if (vtnr < 0) { log_error_errno(vtnr, "Hm, /sys/class/tty/tty0/active is badly formatted: %m"); return -EIO; } return seat_active_vt_changed(s, vtnr); } int seat_start(Seat *s) { assert(s); if (s->started) return 0; log_struct(LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SEAT_START_STR, "SEAT_ID=%s", s->id, LOG_MESSAGE("New seat %s.", s->id)); /* Initialize VT magic stuff */ seat_preallocate_vts(s); /* Read current VT */ seat_read_active_vt(s); s->started = true; /* Save seat data */ seat_save(s); seat_send_signal(s, true); return 0; } int seat_stop(Seat *s, bool force) { int r; assert(s); if (s->started) log_struct(LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SEAT_STOP_STR, "SEAT_ID=%s", s->id, LOG_MESSAGE("Removed seat %s.", s->id)); r = seat_stop_sessions(s, force); (void) unlink(s->state_file); seat_add_to_gc_queue(s); if (s->started) seat_send_signal(s, false); s->started = false; return r; } int seat_stop_sessions(Seat *s, bool force) { int r = 0, k; assert(s); LIST_FOREACH(sessions_by_seat, session, s->sessions) { k = session_stop(session, force); if (k < 0) r = k; } return r; } void seat_evict_position(Seat *s, Session *session) { unsigned pos = session->position; session->position = 0; if (pos == 0) return; if (pos < MALLOC_ELEMENTSOF(s->positions) && s->positions[pos] == session) { s->positions[pos] = NULL; /* There might be another session claiming the same * position (eg., during gdm->session transition), so let's look * for it and set it on the free slot. */ LIST_FOREACH(sessions_by_seat, iter, s->sessions) if (iter->position == pos && session_get_state(iter) != SESSION_CLOSING) { s->positions[pos] = iter; break; } } } void seat_claim_position(Seat *s, Session *session, unsigned pos) { /* with VTs, the position is always the same as the VTnr */ if (seat_has_vts(s)) pos = session->vtnr; if (!GREEDY_REALLOC0(s->positions, pos + 1)) return; seat_evict_position(s, session); session->position = pos; if (pos > 0) s->positions[pos] = session; } static void seat_assign_position(Seat *s, Session *session) { unsigned pos; if (session->position > 0) return; for (pos = 1; pos < MALLOC_ELEMENTSOF(s->positions); ++pos) if (!s->positions[pos]) break; seat_claim_position(s, session, pos); } int seat_attach_session(Seat *s, Session *session) { assert(s); assert(session); assert(!session->seat); if (!seat_has_vts(s) != !session->vtnr) return -EINVAL; session->seat = s; LIST_PREPEND(sessions_by_seat, s->sessions, session); seat_assign_position(s, session); /* On seats with VTs, the VT logic defines which session is active. On * seats without VTs, we automatically activate new sessions. */ if (!seat_has_vts(s)) seat_set_active(s, session); return 0; } void seat_complete_switch(Seat *s) { Session *session; assert(s); /* if no session-switch is pending or if it got canceled, do nothing */ if (!s->pending_switch) return; session = TAKE_PTR(s->pending_switch); seat_set_active(s, session); } bool seat_has_vts(Seat *s) { assert(s); return seat_is_seat0(s) && s->manager->console_active_fd >= 0; } bool seat_is_seat0(Seat *s) { assert(s); return s->manager->seat0 == s; } bool seat_can_tty(Seat *s) { assert(s); return seat_has_vts(s); } bool seat_has_master_device(Seat *s) { assert(s); /* device list is ordered by "master" flag */ return !!s->devices && s->devices->master; } bool seat_can_graphical(Seat *s) { assert(s); return seat_has_master_device(s); } int seat_get_idle_hint(Seat *s, dual_timestamp *t) { bool idle_hint = true; dual_timestamp ts = DUAL_TIMESTAMP_NULL; assert(s); LIST_FOREACH(sessions_by_seat, session, s->sessions) { dual_timestamp k; int ih; ih = session_get_idle_hint(session, &k); if (ih < 0) return ih; if (!ih) { if (!idle_hint) { if (k.monotonic > ts.monotonic) ts = k; } else { idle_hint = false; ts = k; } } else if (idle_hint) { if (k.monotonic > ts.monotonic) ts = k; } } if (t) *t = ts; return idle_hint; } bool seat_may_gc(Seat *s, bool drop_not_started) { assert(s); if (drop_not_started && !s->started) return true; if (seat_is_seat0(s)) return false; return !seat_has_master_device(s); } void seat_add_to_gc_queue(Seat *s) { assert(s); if (s->in_gc_queue) return; LIST_PREPEND(gc_queue, s->manager->seat_gc_queue, s); s->in_gc_queue = true; } static bool seat_name_valid_char(char c) { return ascii_isalpha(c) || ascii_isdigit(c) || IN_SET(c, '-', '_'); } bool seat_name_is_valid(const char *name) { const char *p; assert(name); if (!startswith(name, "seat")) return false; if (!name[4]) return false; for (p = name; *p; p++) if (!seat_name_valid_char(*p)) return false; if (strlen(name) > 255) return false; return true; }