Last active
April 26, 2025 08:24
-
-
Save andrew-sayers/1c4a24f86a9a4c1b1e38d109f1bd1d1e to your computer and use it in GitHub Desktop.
Get notified when the user in on-seat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: CC0-1.0 | |
| /* | |
| * Linux systems can have multiple sessions logged in at once. | |
| * For example, siblings with separate accounts on a family laptop | |
| * can use the "switch user" function to share access without | |
| * closing all their programs every time. | |
| * | |
| * Systemd provides user services to handle services that run for | |
| * each user, but this can be a problem for services that should only | |
| * run for the current user. For example, audio notifications | |
| * should only be played for the user currently using the computer. | |
| * | |
| * To pause a service, you need to monitor whether the specified user | |
| * is *active*, and whether they have *sessions with seats*. | |
| * | |
| * This gist shows one way to continually monitor those values. | |
| * | |
| * To run this gist, do: | |
| * | |
| * gcc user-on-seat.c -lsystemd -o user-on-seat | |
| * ./user-on-seat | |
| * # (press e.g. ctrl+alt+F8 then ctrl+alt+F7 to switch sessions) | |
| * | |
| * See also: | |
| * * https://www.freedesktop.org/software/systemd/man/latest/sd_login_monitor.html | |
| * * https://www.freedesktop.org/software/systemd/man/latest/sd_uid_get_state.html | |
| * * https://gitlab.freedesktop.org/pipewire/wireplumber/-/blob/master/modules/module-logind.c#L52 | |
| */ | |
| #include <assert.h> | |
| #include <errno.h> | |
| #include <poll.h> | |
| #include <stddef.h> | |
| #include <string.h> | |
| #include <time.h> | |
| #include <unistd.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <systemd/sd-login.h> | |
| /** | |
| * @brief Check whether the user is currently active | |
| */ | |
| struct sd_active { | |
| sd_login_monitor *m; | |
| int uid; | |
| } sd_active; | |
| /** | |
| * @brief Destroy an sd_active struct | |
| * @param sa struct to destroy | |
| */ | |
| static inline void sd_active_cleanup(struct sd_active *sa) { | |
| sd_login_monitor_unref(sa->m); | |
| } | |
| /** | |
| * @brief Init an sd_active struct | |
| * @param sa struct to init | |
| * @return 0 on success, negative errno-style code on error | |
| */ | |
| static inline int sd_active_init(struct sd_active *sa) { | |
| sa->m = NULL; | |
| sa->uid = getuid(); | |
| return sd_login_monitor_new("uid", &sa->m); | |
| } | |
| /** | |
| * @brief Init a pollfd to listen for changes in activity | |
| * @param sa struct to listen for | |
| * @param pfd poll fd to init | |
| * @return 0 on success, negative errno-style code on error | |
| */ | |
| static inline int sd_active_pollfd_init(struct sd_active *sa, struct pollfd *pfd) { | |
| pfd->fd = sd_login_monitor_get_fd(sa->m); | |
| if (pfd->fd < 0) return pfd->fd; | |
| pfd->events = sd_login_monitor_get_events(sa->m); | |
| return (pfd->events < 0) ? pfd->events : 0; | |
| } | |
| /** | |
| * @brief Get timeout for poll() | |
| * @param sa struct to poll | |
| * @return timeout in milliseconds on success (may be -1), negative errno-style code on error (not -1) | |
| */ | |
| static inline int sd_active_get_poll_timeout(struct sd_active *sa) { | |
| uint64_t timeout_usec; | |
| int res = sd_login_monitor_get_timeout(sa->m, &timeout_usec); | |
| if (res < 0) { | |
| // sd_login_monitor_get_timeout returns -EINVAL and -ENOMEM, | |
| // neither of which should be -1: | |
| assert(res != -1); | |
| return res; | |
| } | |
| if (timeout_usec == (uint64_t)-1) { | |
| return -1; | |
| } else { | |
| struct timespec ts; | |
| res = clock_gettime(CLOCK_MONOTONIC, &ts); | |
| if (res < 0) return -errno; | |
| uint64_t n; | |
| n = (uint64_t) ts.tv_sec * 1000000 + ts.tv_nsec / 1000; | |
| return timeout_usec > n ? (int) ((timeout_usec - n + 999) / 1000) : 0; | |
| } | |
| } | |
| static inline void free_string(char **str) { | |
| free(*str); | |
| } | |
| /** | |
| * @brief Check whether the current user is active | |
| * @param sa struct to check | |
| * @return 1 if active, 0 if inactive, else negative errno-style code | |
| */ | |
| static inline int sd_active_check(struct sd_active *sa) { | |
| __attribute__((cleanup(free_string))) char *state = NULL; | |
| int res = sd_login_monitor_flush(sa->m); | |
| if (res < 0) return res; | |
| res = sd_uid_get_state(sa->uid, &state); | |
| if (res < 0) return res; | |
| if (strcmp(state,"active")) return 0; | |
| res = sd_uid_get_seats(sa->uid, 1, NULL); | |
| return (res >= 0) ? !!res : res; | |
| } | |
| int main() { | |
| int res; | |
| __attribute__((cleanup(sd_active_cleanup))) struct sd_active sa; | |
| if ((res = sd_active_init(&sa)) < 0) { | |
| fprintf(stderr, "sd_active_init(): %s\n", strerror(-res)); | |
| return 2; | |
| } | |
| struct pollfd pfd; | |
| if ((res = sd_active_pollfd_init(&sa, &pfd)) < 0) { | |
| fprintf(stderr, "sd_active_pollfd_init(): %s\n", strerror(-res)); | |
| return 2; | |
| } | |
| int was_active = 0; | |
| while (1) { | |
| int active = sd_active_check(&sa); | |
| if (active < 0) { | |
| fprintf(stderr, "sd_active_check(): %s\n", strerror(-active)); | |
| return 2; | |
| } | |
| if (active != was_active) { | |
| was_active = active; | |
| printf("Active: %d\n", active); | |
| } | |
| int timeout = sd_active_get_poll_timeout(&sa); | |
| if (timeout < -1) { | |
| fprintf(stderr, "sd_active_get_poll_timeout(): %s\n", strerror(-timeout)); | |
| return 2; | |
| } | |
| if (poll(&pfd, 1, timeout) < 0) { | |
| fprintf(stderr, "poll(): %s\n", strerror(errno)); | |
| return 2; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment