| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <linux/kd.h> |
| #include <linux/vt.h> |
| #include <signal.h> |
| #include <sys/ioctl.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include "sd-messages.h" |
| |
| #include "alloc-util.h" |
| #include "audit-util.h" |
| #include "bus-error.h" |
| #include "bus-util.h" |
| #include "env-file.h" |
| #include "escape.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "format-util.h" |
| #include "io-util.h" |
| #include "logind-dbus.h" |
| #include "logind-seat-dbus.h" |
| #include "logind-session-dbus.h" |
| #include "logind-session.h" |
| #include "logind-user-dbus.h" |
| #include "mkdir.h" |
| #include "parse-util.h" |
| #include "path-util.h" |
| #include "process-util.h" |
| #include "serialize.h" |
| #include "string-table.h" |
| #include "strv.h" |
| #include "terminal-util.h" |
| #include "tmpfile-util.h" |
| #include "user-util.h" |
| #include "util.h" |
| |
| #define RELEASE_USEC (20*USEC_PER_SEC) |
| |
| static void session_remove_fifo(Session *s); |
| static void session_restore_vt(Session *s); |
| |
| int session_new(Session **ret, Manager *m, const char *id) { |
| _cleanup_(session_freep) Session *s = NULL; |
| int r; |
| |
| assert(ret); |
| assert(m); |
| assert(id); |
| |
| if (!session_id_valid(id)) |
| return -EINVAL; |
| |
| s = new(Session, 1); |
| if (!s) |
| return -ENOMEM; |
| |
| *s = (Session) { |
| .manager = m, |
| .fifo_fd = -1, |
| .vtfd = -1, |
| .audit_id = AUDIT_SESSION_INVALID, |
| .tty_validity = _TTY_VALIDITY_INVALID, |
| }; |
| |
| s->state_file = path_join("/run/systemd/sessions", id); |
| if (!s->state_file) |
| return -ENOMEM; |
| |
| s->id = basename(s->state_file); |
| |
| s->devices = hashmap_new(&devt_hash_ops); |
| if (!s->devices) |
| return -ENOMEM; |
| |
| r = hashmap_put(m->sessions, s->id, s); |
| if (r < 0) |
| return r; |
| |
| *ret = TAKE_PTR(s); |
| return 0; |
| } |
| |
| Session* session_free(Session *s) { |
| SessionDevice *sd; |
| |
| if (!s) |
| return NULL; |
| |
| if (s->in_gc_queue) |
| LIST_REMOVE(gc_queue, s->manager->session_gc_queue, s); |
| |
| s->timer_event_source = sd_event_source_unref(s->timer_event_source); |
| |
| session_drop_controller(s); |
| |
| while ((sd = hashmap_first(s->devices))) |
| session_device_free(sd); |
| |
| hashmap_free(s->devices); |
| |
| if (s->user) { |
| LIST_REMOVE(sessions_by_user, s->user->sessions, s); |
| |
| if (s->user->display == s) |
| s->user->display = NULL; |
| |
| user_update_last_session_timer(s->user); |
| } |
| |
| if (s->seat) { |
| if (s->seat->active == s) |
| s->seat->active = NULL; |
| if (s->seat->pending_switch == s) |
| s->seat->pending_switch = NULL; |
| |
| seat_evict_position(s->seat, s); |
| LIST_REMOVE(sessions_by_seat, s->seat->sessions, s); |
| } |
| |
| if (s->scope) { |
| hashmap_remove(s->manager->session_units, s->scope); |
| free(s->scope); |
| } |
| |
| if (pid_is_valid(s->leader)) |
| (void) hashmap_remove_value(s->manager->sessions_by_leader, PID_TO_PTR(s->leader), s); |
| |
| free(s->scope_job); |
| |
| sd_bus_message_unref(s->create_message); |
| |
| free(s->tty); |
| free(s->display); |
| free(s->remote_host); |
| free(s->remote_user); |
| free(s->service); |
| free(s->desktop); |
| |
| hashmap_remove(s->manager->sessions, s->id); |
| |
| sd_event_source_unref(s->fifo_event_source); |
| safe_close(s->fifo_fd); |
| |
| /* Note that we remove neither the state file nor the fifo path here, since we want both to survive |
| * daemon restarts */ |
| free(s->state_file); |
| free(s->fifo_path); |
| |
| return mfree(s); |
| } |
| |
| void session_set_user(Session *s, User *u) { |
| assert(s); |
| assert(!s->user); |
| |
| s->user = u; |
| LIST_PREPEND(sessions_by_user, u->sessions, s); |
| |
| user_update_last_session_timer(u); |
| } |
| |
| int session_set_leader(Session *s, pid_t pid) { |
| int r; |
| |
| assert(s); |
| |
| if (!pid_is_valid(pid)) |
| return -EINVAL; |
| |
| if (s->leader == pid) |
| return 0; |
| |
| r = hashmap_put(s->manager->sessions_by_leader, PID_TO_PTR(pid), s); |
| if (r < 0) |
| return r; |
| |
| if (pid_is_valid(s->leader)) |
| (void) hashmap_remove_value(s->manager->sessions_by_leader, PID_TO_PTR(s->leader), s); |
| |
| s->leader = pid; |
| (void) audit_session_from_pid(pid, &s->audit_id); |
| |
| return 1; |
| } |
| |
| static void session_save_devices(Session *s, FILE *f) { |
| SessionDevice *sd; |
| |
| if (!hashmap_isempty(s->devices)) { |
| fprintf(f, "DEVICES="); |
| HASHMAP_FOREACH(sd, s->devices) |
| fprintf(f, "%u:%u ", major(sd->dev), minor(sd->dev)); |
| fprintf(f, "\n"); |
| } |
| } |
| |
| int session_save(Session *s) { |
| _cleanup_free_ char *temp_path = NULL; |
| _cleanup_fclose_ FILE *f = NULL; |
| int r; |
| |
| assert(s); |
| |
| if (!s->user) |
| return -ESTALE; |
| |
| if (!s->started) |
| return 0; |
| |
| r = mkdir_safe_label("/run/systemd/sessions", 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" |
| "UID="UID_FMT"\n" |
| "USER=%s\n" |
| "ACTIVE=%i\n" |
| "IS_DISPLAY=%i\n" |
| "STATE=%s\n" |
| "REMOTE=%i\n", |
| s->user->user_record->uid, |
| s->user->user_record->user_name, |
| session_is_active(s), |
| s->user->display == s, |
| session_state_to_string(session_get_state(s)), |
| s->remote); |
| |
| if (s->type >= 0) |
| fprintf(f, "TYPE=%s\n", session_type_to_string(s->type)); |
| |
| if (s->original_type >= 0) |
| fprintf(f, "ORIGINAL_TYPE=%s\n", session_type_to_string(s->original_type)); |
| |
| if (s->class >= 0) |
| fprintf(f, "CLASS=%s\n", session_class_to_string(s->class)); |
| |
| if (s->scope) |
| fprintf(f, "SCOPE=%s\n", s->scope); |
| if (s->scope_job) |
| fprintf(f, "SCOPE_JOB=%s\n", s->scope_job); |
| |
| if (s->fifo_path) |
| fprintf(f, "FIFO=%s\n", s->fifo_path); |
| |
| if (s->seat) |
| fprintf(f, "SEAT=%s\n", s->seat->id); |
| |
| if (s->tty) |
| fprintf(f, "TTY=%s\n", s->tty); |
| |
| if (s->tty_validity >= 0) |
| fprintf(f, "TTY_VALIDITY=%s\n", tty_validity_to_string(s->tty_validity)); |
| |
| if (s->display) |
| fprintf(f, "DISPLAY=%s\n", s->display); |
| |
| if (s->remote_host) { |
| _cleanup_free_ char *escaped = NULL; |
| |
| escaped = cescape(s->remote_host); |
| if (!escaped) { |
| r = -ENOMEM; |
| goto fail; |
| } |
| |
| fprintf(f, "REMOTE_HOST=%s\n", escaped); |
| } |
| |
| if (s->remote_user) { |
| _cleanup_free_ char *escaped = NULL; |
| |
| escaped = cescape(s->remote_user); |
| if (!escaped) { |
| r = -ENOMEM; |
| goto fail; |
| } |
| |
| fprintf(f, "REMOTE_USER=%s\n", escaped); |
| } |
| |
| if (s->service) { |
| _cleanup_free_ char *escaped = NULL; |
| |
| escaped = cescape(s->service); |
| if (!escaped) { |
| r = -ENOMEM; |
| goto fail; |
| } |
| |
| fprintf(f, "SERVICE=%s\n", escaped); |
| } |
| |
| if (s->desktop) { |
| _cleanup_free_ char *escaped = NULL; |
| |
| escaped = cescape(s->desktop); |
| if (!escaped) { |
| r = -ENOMEM; |
| goto fail; |
| } |
| |
| fprintf(f, "DESKTOP=%s\n", escaped); |
| } |
| |
| if (s->seat && seat_has_vts(s->seat)) |
| fprintf(f, "VTNR=%u\n", s->vtnr); |
| |
| if (!s->vtnr) |
| fprintf(f, "POSITION=%u\n", s->position); |
| |
| if (pid_is_valid(s->leader)) |
| fprintf(f, "LEADER="PID_FMT"\n", s->leader); |
| |
| if (audit_session_is_valid(s->audit_id)) |
| fprintf(f, "AUDIT=%"PRIu32"\n", s->audit_id); |
| |
| if (dual_timestamp_is_set(&s->timestamp)) |
| fprintf(f, |
| "REALTIME="USEC_FMT"\n" |
| "MONOTONIC="USEC_FMT"\n", |
| s->timestamp.realtime, |
| s->timestamp.monotonic); |
| |
| if (s->controller) { |
| fprintf(f, "CONTROLLER=%s\n", s->controller); |
| session_save_devices(s, f); |
| } |
| |
| 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 session data %s: %m", s->state_file); |
| } |
| |
| static int session_load_devices(Session *s, const char *devices) { |
| const char *p; |
| int r = 0; |
| |
| assert(s); |
| |
| for (p = devices;;) { |
| _cleanup_free_ char *word = NULL; |
| SessionDevice *sd; |
| dev_t dev; |
| int k; |
| |
| k = extract_first_word(&p, &word, NULL, 0); |
| if (k == 0) |
| break; |
| if (k < 0) { |
| r = k; |
| break; |
| } |
| |
| k = parse_dev(word, &dev); |
| if (k < 0) { |
| r = k; |
| continue; |
| } |
| |
| /* The file descriptors for loaded devices will be reattached later. */ |
| k = session_device_new(s, dev, false, &sd); |
| if (k < 0) |
| r = k; |
| } |
| |
| if (r < 0) |
| log_error_errno(r, "Loading session devices for session %s failed: %m", s->id); |
| |
| return r; |
| } |
| |
| int session_load(Session *s) { |
| _cleanup_free_ char *remote = NULL, |
| *seat = NULL, |
| *tty_validity = NULL, |
| *vtnr = NULL, |
| *state = NULL, |
| *position = NULL, |
| *leader = NULL, |
| *type = NULL, |
| *original_type = NULL, |
| *class = NULL, |
| *uid = NULL, |
| *realtime = NULL, |
| *monotonic = NULL, |
| *controller = NULL, |
| *active = NULL, |
| *devices = NULL, |
| *is_display = NULL; |
| |
| int k, r; |
| |
| assert(s); |
| |
| r = parse_env_file(NULL, s->state_file, |
| "REMOTE", &remote, |
| "SCOPE", &s->scope, |
| "SCOPE_JOB", &s->scope_job, |
| "FIFO", &s->fifo_path, |
| "SEAT", &seat, |
| "TTY", &s->tty, |
| "TTY_VALIDITY", &tty_validity, |
| "DISPLAY", &s->display, |
| "REMOTE_HOST", &s->remote_host, |
| "REMOTE_USER", &s->remote_user, |
| "SERVICE", &s->service, |
| "DESKTOP", &s->desktop, |
| "VTNR", &vtnr, |
| "STATE", &state, |
| "POSITION", &position, |
| "LEADER", &leader, |
| "TYPE", &type, |
| "ORIGINAL_TYPE", &original_type, |
| "CLASS", &class, |
| "UID", &uid, |
| "REALTIME", &realtime, |
| "MONOTONIC", &monotonic, |
| "CONTROLLER", &controller, |
| "ACTIVE", &active, |
| "DEVICES", &devices, |
| "IS_DISPLAY", &is_display); |
| |
| if (r < 0) |
| return log_error_errno(r, "Failed to read %s: %m", s->state_file); |
| |
| if (!s->user) { |
| uid_t u; |
| User *user; |
| |
| if (!uid) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOENT), |
| "UID not specified for session %s", |
| s->id); |
| |
| r = parse_uid(uid, &u); |
| if (r < 0) { |
| log_error("Failed to parse UID value %s for session %s.", uid, s->id); |
| return r; |
| } |
| |
| user = hashmap_get(s->manager->users, UID_TO_PTR(u)); |
| if (!user) |
| return log_error_errno(SYNTHETIC_ERRNO(ENOENT), |
| "User of session %s not known.", |
| s->id); |
| |
| session_set_user(s, user); |
| } |
| |
| if (remote) { |
| k = parse_boolean(remote); |
| if (k >= 0) |
| s->remote = k; |
| } |
| |
| if (vtnr) |
| safe_atou(vtnr, &s->vtnr); |
| |
| if (seat && !s->seat) { |
| Seat *o; |
| |
| o = hashmap_get(s->manager->seats, seat); |
| if (o) |
| r = seat_attach_session(o, s); |
| if (!o || r < 0) |
| log_error("Cannot attach session %s to seat %s", s->id, seat); |
| } |
| |
| if (!s->seat || !seat_has_vts(s->seat)) |
| s->vtnr = 0; |
| |
| if (position && s->seat) { |
| unsigned npos; |
| |
| safe_atou(position, &npos); |
| seat_claim_position(s->seat, s, npos); |
| } |
| |
| if (tty_validity) { |
| TTYValidity v; |
| |
| v = tty_validity_from_string(tty_validity); |
| if (v < 0) |
| log_debug("Failed to parse TTY validity: %s", tty_validity); |
| else |
| s->tty_validity = v; |
| } |
| |
| if (leader) { |
| pid_t pid; |
| |
| r = parse_pid(leader, &pid); |
| if (r < 0) |
| log_debug_errno(r, "Failed to parse leader PID of session: %s", leader); |
| else { |
| r = session_set_leader(s, pid); |
| if (r < 0) |
| log_warning_errno(r, "Failed to set session leader PID, ignoring: %m"); |
| } |
| } |
| |
| if (type) { |
| SessionType t; |
| |
| t = session_type_from_string(type); |
| if (t >= 0) |
| s->type = t; |
| } |
| |
| if (original_type) { |
| SessionType ot; |
| |
| ot = session_type_from_string(original_type); |
| if (ot >= 0) |
| s->original_type = ot; |
| } else |
| /* Pre-v246 compat: initialize original_type if not set in the state file */ |
| s->original_type = s->type; |
| |
| if (class) { |
| SessionClass c; |
| |
| c = session_class_from_string(class); |
| if (c >= 0) |
| s->class = c; |
| } |
| |
| if (state && streq(state, "closing")) |
| s->stopping = true; |
| |
| if (s->fifo_path) { |
| int fd; |
| |
| /* If we open an unopened pipe for reading we will not |
| get an EOF. to trigger an EOF we hence open it for |
| writing, but close it right away which then will |
| trigger the EOF. This will happen immediately if no |
| other process has the FIFO open for writing, i. e. |
| when the session died before logind (re)started. */ |
| |
| fd = session_create_fifo(s); |
| safe_close(fd); |
| } |
| |
| if (realtime) |
| (void) deserialize_usec(realtime, &s->timestamp.realtime); |
| if (monotonic) |
| (void) deserialize_usec(monotonic, &s->timestamp.monotonic); |
| |
| if (active) { |
| k = parse_boolean(active); |
| if (k >= 0) |
| s->was_active = k; |
| } |
| |
| if (is_display) { |
| /* Note that when enumerating users are loaded before sessions, hence the display session to use is |
| * something we have to store along with the session and not the user, as in that case we couldn't |
| * apply it at the time we load the user. */ |
| |
| k = parse_boolean(is_display); |
| if (k < 0) |
| log_warning_errno(k, "Failed to parse IS_DISPLAY session property: %m"); |
| else if (k > 0) |
| s->user->display = s; |
| } |
| |
| if (controller) { |
| if (bus_name_has_owner(s->manager->bus, controller, NULL) > 0) { |
| session_set_controller(s, controller, false, false); |
| session_load_devices(s, devices); |
| } else |
| session_restore_vt(s); |
| } |
| |
| return r; |
| } |
| |
| int session_activate(Session *s) { |
| unsigned num_pending; |
| |
| assert(s); |
| assert(s->user); |
| |
| if (!s->seat) |
| return -EOPNOTSUPP; |
| |
| if (s->seat->active == s) |
| return 0; |
| |
| /* on seats with VTs, we let VTs manage session-switching */ |
| if (seat_has_vts(s->seat)) { |
| if (s->vtnr == 0) |
| return -EOPNOTSUPP; |
| |
| return chvt(s->vtnr); |
| } |
| |
| /* On seats without VTs, we implement session-switching in logind. We |
| * try to pause all session-devices and wait until the session |
| * controller acknowledged them. Once all devices are asleep, we simply |
| * switch the active session and be done. |
| * We save the session we want to switch to in seat->pending_switch and |
| * seat_complete_switch() will perform the final switch. */ |
| |
| s->seat->pending_switch = s; |
| |
| /* if no devices are running, immediately perform the session switch */ |
| num_pending = session_device_try_pause_all(s); |
| if (!num_pending) |
| seat_complete_switch(s->seat); |
| |
| return 0; |
| } |
| |
| static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_error *error) { |
| int r; |
| |
| assert(s); |
| assert(s->user); |
| |
| if (!s->scope) { |
| _cleanup_free_ char *scope = NULL; |
| const char *description; |
| |
| s->scope_job = mfree(s->scope_job); |
| |
| scope = strjoin("session-", s->id, ".scope"); |
| if (!scope) |
| return log_oom(); |
| |
| description = strjoina("Session ", s->id, " of User ", s->user->user_record->user_name); |
| |
| r = manager_start_scope( |
| s->manager, |
| scope, |
| s->leader, |
| s->user->slice, |
| description, |
| /* These two have StopWhenUnneeded= set, hence add a dep towards them */ |
| STRV_MAKE(s->user->runtime_dir_service, |
| s->user->service), |
| /* And order us after some more */ |
| STRV_MAKE("systemd-logind.service", |
| "systemd-user-sessions.service", |
| s->user->runtime_dir_service, |
| s->user->service), |
| user_record_home_directory(s->user->user_record), |
| properties, |
| error, |
| &s->scope_job); |
| if (r < 0) |
| return log_error_errno(r, "Failed to start session scope %s: %s", |
| scope, bus_error_message(error, r)); |
| |
| s->scope = TAKE_PTR(scope); |
| } |
| |
| (void) hashmap_put(s->manager->session_units, s->scope, s); |
| |
| return 0; |
| } |
| |
| int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) { |
| int r; |
| |
| assert(s); |
| |
| if (!s->user) |
| return -ESTALE; |
| |
| if (s->stopping) |
| return -EINVAL; |
| |
| if (s->started) |
| return 0; |
| |
| r = user_start(s->user); |
| if (r < 0) |
| return r; |
| |
| r = session_start_scope(s, properties, error); |
| if (r < 0) |
| return r; |
| |
| log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, |
| "MESSAGE_ID=" SD_MESSAGE_SESSION_START_STR, |
| "SESSION_ID=%s", s->id, |
| "USER_ID=%s", s->user->user_record->user_name, |
| "LEADER="PID_FMT, s->leader, |
| LOG_MESSAGE("New session %s of user %s.", s->id, s->user->user_record->user_name)); |
| |
| if (!dual_timestamp_is_set(&s->timestamp)) |
| dual_timestamp_get(&s->timestamp); |
| |
| if (s->seat) |
| seat_read_active_vt(s->seat); |
| |
| s->started = true; |
| |
| user_elect_display(s->user); |
| |
| /* Save data */ |
| session_save(s); |
| user_save(s->user); |
| if (s->seat) |
| seat_save(s->seat); |
| |
| /* Send signals */ |
| session_send_signal(s, true); |
| user_send_changed(s->user, "Display", NULL); |
| |
| if (s->seat && s->seat->active == s) |
| seat_send_changed(s->seat, "ActiveSession", NULL); |
| |
| return 0; |
| } |
| |
| static int session_stop_scope(Session *s, bool force) { |
| _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; |
| int r; |
| |
| assert(s); |
| |
| if (!s->scope) |
| return 0; |
| |
| /* Let's always abandon the scope first. This tells systemd that we are not interested anymore, and everything |
| * that is left in the scope is "left-over". Informing systemd about this has the benefit that it will log |
| * when killing any processes left after this point. */ |
| r = manager_abandon_scope(s->manager, s->scope, &error); |
| if (r < 0) { |
| log_warning_errno(r, "Failed to abandon session scope, ignoring: %s", bus_error_message(&error, r)); |
| sd_bus_error_free(&error); |
| } |
| |
| s->scope_job = mfree(s->scope_job); |
| |
| /* Optionally, let's kill everything that's left now. */ |
| if (force || |
| (s->user->user_record->kill_processes != 0 && |
| (s->user->user_record->kill_processes > 0 || |
| manager_shall_kill(s->manager, s->user->user_record->user_name)))) { |
| |
| r = manager_stop_unit(s->manager, s->scope, force ? "replace" : "fail", &error, &s->scope_job); |
| if (r < 0) { |
| if (force) |
| return log_error_errno(r, "Failed to stop session scope: %s", bus_error_message(&error, r)); |
| |
| log_warning_errno(r, "Failed to stop session scope, ignoring: %s", bus_error_message(&error, r)); |
| } |
| } else { |
| |
| /* With no killing, this session is allowed to persist in "closing" state indefinitely. |
| * Therefore session stop and session removal may be two distinct events. |
| * Session stop is quite significant on its own, let's log it. */ |
| log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, |
| "SESSION_ID=%s", s->id, |
| "USER_ID=%s", s->user->user_record->user_name, |
| "LEADER="PID_FMT, s->leader, |
| LOG_MESSAGE("Session %s logged out. Waiting for processes to exit.", s->id)); |
| } |
| |
| return 0; |
| } |
| |
| int session_stop(Session *s, bool force) { |
| int r; |
| |
| assert(s); |
| |
| /* This is called whenever we begin with tearing down a session record. It's called in four cases: explicit API |
| * request via the bus (either directly for the session object or for the seat or user object this session |
| * belongs to; 'force' is true), or due to automatic GC (i.e. scope vanished; 'force' is false), or because the |
| * session FIFO saw an EOF ('force' is false), or because the release timer hit ('force' is false). */ |
| |
| if (!s->user) |
| return -ESTALE; |
| if (!s->started) |
| return 0; |
| if (s->stopping) |
| return 0; |
| |
| s->timer_event_source = sd_event_source_unref(s->timer_event_source); |
| |
| if (s->seat) |
| seat_evict_position(s->seat, s); |
| |
| /* We are going down, don't care about FIFOs anymore */ |
| session_remove_fifo(s); |
| |
| /* Kill cgroup */ |
| r = session_stop_scope(s, force); |
| |
| s->stopping = true; |
| |
| user_elect_display(s->user); |
| |
| session_save(s); |
| user_save(s->user); |
| |
| return r; |
| } |
| |
| int session_finalize(Session *s) { |
| SessionDevice *sd; |
| |
| assert(s); |
| |
| if (!s->user) |
| return -ESTALE; |
| |
| if (s->started) |
| log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, |
| "MESSAGE_ID=" SD_MESSAGE_SESSION_STOP_STR, |
| "SESSION_ID=%s", s->id, |
| "USER_ID=%s", s->user->user_record->user_name, |
| "LEADER="PID_FMT, s->leader, |
| LOG_MESSAGE("Removed session %s.", s->id)); |
| |
| s->timer_event_source = sd_event_source_unref(s->timer_event_source); |
| |
| if (s->seat) |
| seat_evict_position(s->seat, s); |
| |
| /* Kill session devices */ |
| while ((sd = hashmap_first(s->devices))) |
| session_device_free(sd); |
| |
| (void) unlink(s->state_file); |
| session_add_to_gc_queue(s); |
| user_add_to_gc_queue(s->user); |
| |
| if (s->started) { |
| session_send_signal(s, false); |
| s->started = false; |
| } |
| |
| if (s->seat) { |
| if (s->seat->active == s) |
| seat_set_active(s->seat, NULL); |
| |
| seat_save(s->seat); |
| } |
| |
| user_save(s->user); |
| user_send_changed(s->user, "Display", NULL); |
| |
| return 0; |
| } |
| |
| static int release_timeout_callback(sd_event_source *es, uint64_t usec, void *userdata) { |
| Session *s = userdata; |
| |
| assert(es); |
| assert(s); |
| |
| session_stop(s, /* force = */ false); |
| return 0; |
| } |
| |
| int session_release(Session *s) { |
| assert(s); |
| |
| if (!s->started || s->stopping) |
| return 0; |
| |
| if (s->timer_event_source) |
| return 0; |
| |
| return sd_event_add_time_relative( |
| s->manager->event, |
| &s->timer_event_source, |
| CLOCK_MONOTONIC, |
| RELEASE_USEC, 0, |
| release_timeout_callback, s); |
| } |
| |
| bool session_is_active(Session *s) { |
| assert(s); |
| |
| if (!s->seat) |
| return true; |
| |
| return s->seat->active == s; |
| } |
| |
| static int get_tty_atime(const char *tty, usec_t *atime) { |
| _cleanup_free_ char *p = NULL; |
| struct stat st; |
| |
| assert(tty); |
| assert(atime); |
| |
| if (!path_is_absolute(tty)) { |
| p = path_join("/dev", tty); |
| if (!p) |
| return -ENOMEM; |
| |
| tty = p; |
| } else if (!path_startswith(tty, "/dev/")) |
| return -ENOENT; |
| |
| if (lstat(tty, &st) < 0) |
| return -errno; |
| |
| *atime = timespec_load(&st.st_atim); |
| return 0; |
| } |
| |
| static int get_process_ctty_atime(pid_t pid, usec_t *atime) { |
| _cleanup_free_ char *p = NULL; |
| int r; |
| |
| assert(pid > 0); |
| assert(atime); |
| |
| r = get_ctty(pid, NULL, &p); |
| if (r < 0) |
| return r; |
| |
| return get_tty_atime(p, atime); |
| } |
| |
| int session_get_idle_hint(Session *s, dual_timestamp *t) { |
| usec_t atime = 0; |
| int r; |
| |
| assert(s); |
| |
| /* Graphical sessions have an explicit idle hint */ |
| if (SESSION_TYPE_IS_GRAPHICAL(s->type)) { |
| if (t) |
| *t = s->idle_hint_timestamp; |
| |
| return s->idle_hint; |
| } |
| |
| /* For sessions with an explicitly configured tty, let's check its atime */ |
| if (s->tty) { |
| r = get_tty_atime(s->tty, &atime); |
| if (r >= 0) |
| goto found_atime; |
| } |
| |
| /* For sessions with a leader but no explicitly configured tty, let's check the controlling tty of |
| * the leader */ |
| if (pid_is_valid(s->leader)) { |
| r = get_process_ctty_atime(s->leader, &atime); |
| if (r >= 0) |
| goto found_atime; |
| } |
| |
| if (t) |
| *t = DUAL_TIMESTAMP_NULL; |
| |
| return false; |
| |
| found_atime: |
| if (t) |
| dual_timestamp_from_realtime(t, atime); |
| |
| if (s->manager->idle_action_usec <= 0) |
| return false; |
| |
| return usec_add(atime, s->manager->idle_action_usec) <= now(CLOCK_REALTIME); |
| } |
| |
| int session_set_idle_hint(Session *s, bool b) { |
| assert(s); |
| |
| if (!SESSION_TYPE_IS_GRAPHICAL(s->type)) |
| return -ENOTTY; |
| |
| if (s->idle_hint == b) |
| return 0; |
| |
| s->idle_hint = b; |
| dual_timestamp_get(&s->idle_hint_timestamp); |
| |
| session_send_changed(s, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); |
| |
| if (s->seat) |
| seat_send_changed(s->seat, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); |
| |
| user_send_changed(s->user, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); |
| manager_send_changed(s->manager, "IdleHint", "IdleSinceHint", "IdleSinceHintMonotonic", NULL); |
| |
| return 1; |
| } |
| |
| int session_get_locked_hint(Session *s) { |
| assert(s); |
| |
| return s->locked_hint; |
| } |
| |
| void session_set_locked_hint(Session *s, bool b) { |
| assert(s); |
| |
| if (s->locked_hint == b) |
| return; |
| |
| s->locked_hint = b; |
| |
| session_send_changed(s, "LockedHint", NULL); |
| } |
| |
| void session_set_type(Session *s, SessionType t) { |
| assert(s); |
| |
| if (s->type == t) |
| return; |
| |
| s->type = t; |
| session_save(s); |
| |
| session_send_changed(s, "Type", NULL); |
| } |
| |
| static int session_dispatch_fifo(sd_event_source *es, int fd, uint32_t revents, void *userdata) { |
| Session *s = userdata; |
| |
| assert(s); |
| assert(s->fifo_fd == fd); |
| |
| /* EOF on the FIFO means the session died abnormally. */ |
| |
| session_remove_fifo(s); |
| session_stop(s, /* force = */ false); |
| |
| return 1; |
| } |
| |
| int session_create_fifo(Session *s) { |
| int r; |
| |
| assert(s); |
| |
| /* Create FIFO */ |
| if (!s->fifo_path) { |
| r = mkdir_safe_label("/run/systemd/sessions", 0755, 0, 0, MKDIR_WARN_MODE); |
| if (r < 0) |
| return r; |
| |
| s->fifo_path = strjoin("/run/systemd/sessions/", s->id, ".ref"); |
| if (!s->fifo_path) |
| return -ENOMEM; |
| |
| if (mkfifo(s->fifo_path, 0600) < 0 && errno != EEXIST) |
| return -errno; |
| } |
| |
| /* Open reading side */ |
| if (s->fifo_fd < 0) { |
| s->fifo_fd = open(s->fifo_path, O_RDONLY|O_CLOEXEC|O_NONBLOCK); |
| if (s->fifo_fd < 0) |
| return -errno; |
| } |
| |
| if (!s->fifo_event_source) { |
| r = sd_event_add_io(s->manager->event, &s->fifo_event_source, s->fifo_fd, 0, session_dispatch_fifo, s); |
| if (r < 0) |
| return r; |
| |
| /* Let's make sure we noticed dead sessions before we process new bus requests (which might |
| * create new sessions). */ |
| r = sd_event_source_set_priority(s->fifo_event_source, SD_EVENT_PRIORITY_NORMAL-10); |
| if (r < 0) |
| return r; |
| } |
| |
| /* Open writing side */ |
| r = open(s->fifo_path, O_WRONLY|O_CLOEXEC|O_NONBLOCK); |
| if (r < 0) |
| return -errno; |
| |
| return r; |
| } |
| |
| static void session_remove_fifo(Session *s) { |
| assert(s); |
| |
| s->fifo_event_source = sd_event_source_unref(s->fifo_event_source); |
| s->fifo_fd = safe_close(s->fifo_fd); |
| |
| if (s->fifo_path) { |
| (void) unlink(s->fifo_path); |
| s->fifo_path = mfree(s->fifo_path); |
| } |
| } |
| |
| bool session_may_gc(Session *s, bool drop_not_started) { |
| int r; |
| |
| assert(s); |
| |
| if (drop_not_started && !s->started) |
| return true; |
| |
| if (!s->user) |
| return true; |
| |
| if (s->fifo_fd >= 0) { |
| if (pipe_eof(s->fifo_fd) <= 0) |
| return false; |
| } |
| |
| if (s->scope_job) { |
| _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; |
| |
| r = manager_job_is_active(s->manager, s->scope_job, &error); |
| if (r < 0) |
| log_debug_errno(r, "Failed to determine whether job '%s' is pending, ignoring: %s", s->scope_job, bus_error_message(&error, r)); |
| if (r != 0) |
| return false; |
| } |
| |
| if (s->scope) { |
| _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; |
| |
| r = manager_unit_is_active(s->manager, s->scope, &error); |
| if (r < 0) |
| log_debug_errno(r, "Failed to determine whether unit '%s' is active, ignoring: %s", s->scope, bus_error_message(&error, r)); |
| if (r != 0) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void session_add_to_gc_queue(Session *s) { |
| assert(s); |
| |
| if (s->in_gc_queue) |
| return; |
| |
| LIST_PREPEND(gc_queue, s->manager->session_gc_queue, s); |
| s->in_gc_queue = true; |
| } |
| |
| SessionState session_get_state(Session *s) { |
| assert(s); |
| |
| /* always check closing first */ |
| if (s->stopping || s->timer_event_source) |
| return SESSION_CLOSING; |
| |
| if (s->scope_job || s->fifo_fd < 0) |
| return SESSION_OPENING; |
| |
| if (session_is_active(s)) |
| return SESSION_ACTIVE; |
| |
| return SESSION_ONLINE; |
| } |
| |
| int session_kill(Session *s, KillWho who, int signo) { |
| assert(s); |
| |
| if (!s->scope) |
| return -ESRCH; |
| |
| return manager_kill_unit(s->manager, s->scope, who, signo, NULL); |
| } |
| |
| static int session_open_vt(Session *s) { |
| char path[sizeof("/dev/tty") + DECIMAL_STR_MAX(s->vtnr)]; |
| |
| if (s->vtnr < 1) |
| return -ENODEV; |
| |
| if (s->vtfd >= 0) |
| return s->vtfd; |
| |
| sprintf(path, "/dev/tty%u", s->vtnr); |
| s->vtfd = open_terminal(path, O_RDWR | O_CLOEXEC | O_NONBLOCK | O_NOCTTY); |
| if (s->vtfd < 0) |
| return log_error_errno(s->vtfd, "cannot open VT %s of session %s: %m", path, s->id); |
| |
| return s->vtfd; |
| } |
| |
| static int session_prepare_vt(Session *s) { |
| int vt, r; |
| struct vt_mode mode = {}; |
| |
| if (s->vtnr < 1) |
| return 0; |
| |
| vt = session_open_vt(s); |
| if (vt < 0) |
| return vt; |
| |
| r = fchown(vt, s->user->user_record->uid, -1); |
| if (r < 0) { |
| r = log_error_errno(errno, |
| "Cannot change owner of /dev/tty%u: %m", |
| s->vtnr); |
| goto error; |
| } |
| |
| r = ioctl(vt, KDSKBMODE, K_OFF); |
| if (r < 0) { |
| r = log_error_errno(errno, |
| "Cannot set K_OFF on /dev/tty%u: %m", |
| s->vtnr); |
| goto error; |
| } |
| |
| r = ioctl(vt, KDSETMODE, KD_GRAPHICS); |
| if (r < 0) { |
| r = log_error_errno(errno, |
| "Cannot set KD_GRAPHICS on /dev/tty%u: %m", |
| s->vtnr); |
| goto error; |
| } |
| |
| /* Oh, thanks to the VT layer, VT_AUTO does not work with KD_GRAPHICS. |
| * So we need a dummy handler here which just acknowledges *all* VT |
| * switch requests. */ |
| mode.mode = VT_PROCESS; |
| mode.relsig = SIGRTMIN; |
| mode.acqsig = SIGRTMIN + 1; |
| r = ioctl(vt, VT_SETMODE, &mode); |
| if (r < 0) { |
| r = log_error_errno(errno, |
| "Cannot set VT_PROCESS on /dev/tty%u: %m", |
| s->vtnr); |
| goto error; |
| } |
| |
| return 0; |
| |
| error: |
| session_restore_vt(s); |
| return r; |
| } |
| |
| static void session_restore_vt(Session *s) { |
| int r; |
| |
| r = vt_restore(s->vtfd); |
| if (r == -EIO) { |
| int vt, old_fd; |
| |
| /* It might happen if the controlling process exited before or while we were |
| * restoring the VT as it would leave the old file-descriptor in a hung-up |
| * state. In this case let's retry with a fresh handle to the virtual terminal. */ |
| |
| /* We do a little dance to avoid having the terminal be available |
| * for reuse before we've cleaned it up. */ |
| old_fd = TAKE_FD(s->vtfd); |
| |
| vt = session_open_vt(s); |
| safe_close(old_fd); |
| |
| if (vt >= 0) |
| r = vt_restore(vt); |
| } |
| |
| if (r < 0) |
| log_warning_errno(r, "Failed to restore VT, ignoring: %m"); |
| |
| s->vtfd = safe_close(s->vtfd); |
| } |
| |
| void session_leave_vt(Session *s) { |
| int r; |
| |
| assert(s); |
| |
| /* This is called whenever we get a VT-switch signal from the kernel. |
| * We acknowledge all of them unconditionally. Note that session are |
| * free to overwrite those handlers and we only register them for |
| * sessions with controllers. Legacy sessions are not affected. |
| * However, if we switch from a non-legacy to a legacy session, we must |
| * make sure to pause all device before acknowledging the switch. We |
| * process the real switch only after we are notified via sysfs, so the |
| * legacy session might have already started using the devices. If we |
| * don't pause the devices before the switch, we might confuse the |
| * session we switch to. */ |
| |
| if (s->vtfd < 0) |
| return; |
| |
| session_device_pause_all(s); |
| r = vt_release(s->vtfd, false); |
| if (r < 0) |
| log_debug_errno(r, "Cannot release VT of session %s: %m", s->id); |
| } |
| |
| bool session_is_controller(Session *s, const char *sender) { |
| assert(s); |
| |
| return streq_ptr(s->controller, sender); |
| } |
| |
| static void session_release_controller(Session *s, bool notify) { |
| _unused_ _cleanup_free_ char *name = NULL; |
| SessionDevice *sd; |
| |
| if (!s->controller) |
| return; |
| |
| name = s->controller; |
| |
| /* By resetting the controller before releasing the devices, we won't send notification signals. |
| * This avoids sending useless notifications if the controller is released on disconnects. */ |
| if (!notify) |
| s->controller = NULL; |
| |
| while ((sd = hashmap_first(s->devices))) |
| session_device_free(sd); |
| |
| s->controller = NULL; |
| s->track = sd_bus_track_unref(s->track); |
| } |
| |
| static int on_bus_track(sd_bus_track *track, void *userdata) { |
| Session *s = userdata; |
| |
| assert(track); |
| assert(s); |
| |
| session_drop_controller(s); |
| |
| return 0; |
| } |
| |
| int session_set_controller(Session *s, const char *sender, bool force, bool prepare) { |
| _cleanup_free_ char *name = NULL; |
| int r; |
| |
| assert(s); |
| assert(sender); |
| |
| if (session_is_controller(s, sender)) |
| return 0; |
| if (s->controller && !force) |
| return -EBUSY; |
| |
| name = strdup(sender); |
| if (!name) |
| return -ENOMEM; |
| |
| s->track = sd_bus_track_unref(s->track); |
| r = sd_bus_track_new(s->manager->bus, &s->track, on_bus_track, s); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_track_add_name(s->track, name); |
| if (r < 0) |
| return r; |
| |
| /* When setting a session controller, we forcibly mute the VT and set |
| * it into graphics-mode. Applications can override that by changing |
| * VT state after calling TakeControl(). However, this serves as a good |
| * default and well-behaving controllers can now ignore VTs entirely. |
| * Note that we reset the VT on ReleaseControl() and if the controller |
| * exits. |
| * If logind crashes/restarts, we restore the controller during restart |
| * (without preparing the VT since the controller has probably overridden |
| * VT state by now) or reset the VT in case it crashed/exited, too. */ |
| if (prepare) { |
| r = session_prepare_vt(s); |
| if (r < 0) { |
| s->track = sd_bus_track_unref(s->track); |
| return r; |
| } |
| } |
| |
| session_release_controller(s, true); |
| s->controller = TAKE_PTR(name); |
| session_save(s); |
| |
| return 0; |
| } |
| |
| void session_drop_controller(Session *s) { |
| assert(s); |
| |
| if (!s->controller) |
| return; |
| |
| s->track = sd_bus_track_unref(s->track); |
| session_set_type(s, s->original_type); |
| session_release_controller(s, false); |
| session_save(s); |
| session_restore_vt(s); |
| } |
| |
| static const char* const session_state_table[_SESSION_STATE_MAX] = { |
| [SESSION_OPENING] = "opening", |
| [SESSION_ONLINE] = "online", |
| [SESSION_ACTIVE] = "active", |
| [SESSION_CLOSING] = "closing", |
| }; |
| |
| DEFINE_STRING_TABLE_LOOKUP(session_state, SessionState); |
| |
| static const char* const session_type_table[_SESSION_TYPE_MAX] = { |
| [SESSION_UNSPECIFIED] = "unspecified", |
| [SESSION_TTY] = "tty", |
| [SESSION_X11] = "x11", |
| [SESSION_WAYLAND] = "wayland", |
| [SESSION_MIR] = "mir", |
| [SESSION_WEB] = "web", |
| }; |
| |
| DEFINE_STRING_TABLE_LOOKUP(session_type, SessionType); |
| |
| static const char* const session_class_table[_SESSION_CLASS_MAX] = { |
| [SESSION_USER] = "user", |
| [SESSION_GREETER] = "greeter", |
| [SESSION_LOCK_SCREEN] = "lock-screen", |
| [SESSION_BACKGROUND] = "background", |
| }; |
| |
| DEFINE_STRING_TABLE_LOOKUP(session_class, SessionClass); |
| |
| static const char* const kill_who_table[_KILL_WHO_MAX] = { |
| [KILL_LEADER] = "leader", |
| [KILL_ALL] = "all", |
| }; |
| |
| DEFINE_STRING_TABLE_LOOKUP(kill_who, KillWho); |
| |
| static const char* const tty_validity_table[_TTY_VALIDITY_MAX] = { |
| [TTY_FROM_PAM] = "from-pam", |
| [TTY_FROM_UTMP] = "from-utmp", |
| [TTY_UTMP_INCONSISTENT] = "utmp-inconsistent", |
| }; |
| |
| DEFINE_STRING_TABLE_LOOKUP(tty_validity, TTYValidity); |