1462 lines
43 KiB
C
1462 lines
43 KiB
C
/* 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;
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
escaped = cescape(s->service);
|
|
if (!escaped) {
|
|
r = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
fprintf(f, "SERVICE=%s\n", escaped);
|
|
}
|
|
|
|
if (s->desktop) {
|
|
_cleanup_free_ char *escaped;
|
|
|
|
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) {
|
|
_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);
|