| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #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 "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_free_ 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; |
| } |
| |
| return 0; |
| |
| fail: |
| (void) unlink(s->state_file); |
| |
| if (temp_path) |
| (void) unlink(temp_path); |
| |
| 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; |
| } |