From bfca45639c696c41e23e087ffa9d2f5a6998193f Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Fri, 2 Jan 2026 03:10:57 -0600 Subject: [PATCH 1/7] Experimental Wayland support --- Makefile | 4 +- ext-idle-notify-v1-client-protocol.h | 323 ++++++++++++++++++++ ext-idle-notify-v1-protocol.c | 80 +++++ main.c | 423 +++++++++++++++++++++++++-- 4 files changed, 807 insertions(+), 23 deletions(-) create mode 100644 ext-idle-notify-v1-client-protocol.h create mode 100644 ext-idle-notify-v1-protocol.c diff --git a/Makefile b/Makefile index a10a0f8..ed974cd 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ TARGET_EXEC := runwhenidle -LDLIBS=-lXss -lX11 +LDLIBS=-lXss -lX11 -lwayland-client -lm CC=gcc ifeq ($(PREFIX),) PREFIX := /usr endif -SOURCES = time_utils.c sleep_utils.c tty_utils.c process_handling.c arguments_parsing.c main.c +SOURCES = time_utils.c sleep_utils.c tty_utils.c process_handling.c arguments_parsing.c ext-idle-notify-v1-protocol.c main.c OBJECTS = $(SOURCES:.c=.o) CCFLAGS = -Werror=all all: executable diff --git a/ext-idle-notify-v1-client-protocol.h b/ext-idle-notify-v1-client-protocol.h new file mode 100644 index 0000000..74f5cef --- /dev/null +++ b/ext-idle-notify-v1-client-protocol.h @@ -0,0 +1,323 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef EXT_IDLE_NOTIFY_V1_CLIENT_PROTOCOL_H +#define EXT_IDLE_NOTIFY_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_ext_idle_notify_v1 The ext_idle_notify_v1 protocol + * @section page_ifaces_ext_idle_notify_v1 Interfaces + * - @subpage page_iface_ext_idle_notifier_v1 - idle notification manager + * - @subpage page_iface_ext_idle_notification_v1 - idle notification + * @section page_copyright_ext_idle_notify_v1 Copyright + *
+ *
+ * Copyright © 2015 Martin Gräßlin
+ * Copyright © 2022 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct ext_idle_notification_v1; +struct ext_idle_notifier_v1; +struct wl_seat; + +#ifndef EXT_IDLE_NOTIFIER_V1_INTERFACE +#define EXT_IDLE_NOTIFIER_V1_INTERFACE +/** + * @page page_iface_ext_idle_notifier_v1 ext_idle_notifier_v1 + * @section page_iface_ext_idle_notifier_v1_desc Description + * + * This interface allows clients to monitor user idle status. + * + * After binding to this global, clients can create ext_idle_notification_v1 + * objects to get notified when the user is idle for a given amount of time. + * @section page_iface_ext_idle_notifier_v1_api API + * See @ref iface_ext_idle_notifier_v1. + */ +/** + * @defgroup iface_ext_idle_notifier_v1 The ext_idle_notifier_v1 interface + * + * This interface allows clients to monitor user idle status. + * + * After binding to this global, clients can create ext_idle_notification_v1 + * objects to get notified when the user is idle for a given amount of time. + */ +extern const struct wl_interface ext_idle_notifier_v1_interface; +#endif +#ifndef EXT_IDLE_NOTIFICATION_V1_INTERFACE +#define EXT_IDLE_NOTIFICATION_V1_INTERFACE +/** + * @page page_iface_ext_idle_notification_v1 ext_idle_notification_v1 + * @section page_iface_ext_idle_notification_v1_desc Description + * + * This interface is used by the compositor to send idle notification events + * to clients. + * + * Initially the notification object is not idle. The notification object + * becomes idle when no user activity has happened for at least the timeout + * duration, starting from the creation of the notification object. User + * activity may include input events or a presence sensor, but is + * compositor-specific. + * + * How this notification responds to idle inhibitors depends on how + * it was constructed. If constructed from the + * get_idle_notification request, then if an idle inhibitor is + * active (e.g. another client has created a zwp_idle_inhibitor_v1 + * on a visible surface), the compositor must not make the + * notification object idle. However, if constructed from the + * get_input_idle_notification request, then idle inhibitors are + * ignored, and only input from the user, e.g. from a keyboard or + * mouse, counts as activity. + * + * When the notification object becomes idle, an idled event is sent. When + * user activity starts again, the notification object stops being idle, + * a resumed event is sent and the timeout is restarted. + * @section page_iface_ext_idle_notification_v1_api API + * See @ref iface_ext_idle_notification_v1. + */ +/** + * @defgroup iface_ext_idle_notification_v1 The ext_idle_notification_v1 interface + * + * This interface is used by the compositor to send idle notification events + * to clients. + * + * Initially the notification object is not idle. The notification object + * becomes idle when no user activity has happened for at least the timeout + * duration, starting from the creation of the notification object. User + * activity may include input events or a presence sensor, but is + * compositor-specific. + * + * How this notification responds to idle inhibitors depends on how + * it was constructed. If constructed from the + * get_idle_notification request, then if an idle inhibitor is + * active (e.g. another client has created a zwp_idle_inhibitor_v1 + * on a visible surface), the compositor must not make the + * notification object idle. However, if constructed from the + * get_input_idle_notification request, then idle inhibitors are + * ignored, and only input from the user, e.g. from a keyboard or + * mouse, counts as activity. + * + * When the notification object becomes idle, an idled event is sent. When + * user activity starts again, the notification object stops being idle, + * a resumed event is sent and the timeout is restarted. + */ +extern const struct wl_interface ext_idle_notification_v1_interface; +#endif + +#define EXT_IDLE_NOTIFIER_V1_DESTROY 0 +#define EXT_IDLE_NOTIFIER_V1_GET_IDLE_NOTIFICATION 1 +#define EXT_IDLE_NOTIFIER_V1_GET_INPUT_IDLE_NOTIFICATION 2 + + +/** + * @ingroup iface_ext_idle_notifier_v1 + */ +#define EXT_IDLE_NOTIFIER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_ext_idle_notifier_v1 + */ +#define EXT_IDLE_NOTIFIER_V1_GET_IDLE_NOTIFICATION_SINCE_VERSION 1 +/** + * @ingroup iface_ext_idle_notifier_v1 + */ +#define EXT_IDLE_NOTIFIER_V1_GET_INPUT_IDLE_NOTIFICATION_SINCE_VERSION 2 + +/** @ingroup iface_ext_idle_notifier_v1 */ +static inline void +ext_idle_notifier_v1_set_user_data(struct ext_idle_notifier_v1 *ext_idle_notifier_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) ext_idle_notifier_v1, user_data); +} + +/** @ingroup iface_ext_idle_notifier_v1 */ +static inline void * +ext_idle_notifier_v1_get_user_data(struct ext_idle_notifier_v1 *ext_idle_notifier_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) ext_idle_notifier_v1); +} + +static inline uint32_t +ext_idle_notifier_v1_get_version(struct ext_idle_notifier_v1 *ext_idle_notifier_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) ext_idle_notifier_v1); +} + +/** + * @ingroup iface_ext_idle_notifier_v1 + * + * Destroy the manager object. All objects created via this interface + * remain valid. + */ +static inline void +ext_idle_notifier_v1_destroy(struct ext_idle_notifier_v1 *ext_idle_notifier_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) ext_idle_notifier_v1, + EXT_IDLE_NOTIFIER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) ext_idle_notifier_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_ext_idle_notifier_v1 + * + * Create a new idle notification object. + * + * The notification object has a minimum timeout duration and is tied to a + * seat. The client will be notified if the seat is inactive for at least + * the provided timeout. See ext_idle_notification_v1 for more details. + * + * A zero timeout is valid and means the client wants to be notified as + * soon as possible when the seat is inactive. + */ +static inline struct ext_idle_notification_v1 * +ext_idle_notifier_v1_get_idle_notification(struct ext_idle_notifier_v1 *ext_idle_notifier_v1, uint32_t timeout, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) ext_idle_notifier_v1, + EXT_IDLE_NOTIFIER_V1_GET_IDLE_NOTIFICATION, &ext_idle_notification_v1_interface, wl_proxy_get_version((struct wl_proxy *) ext_idle_notifier_v1), 0, NULL, timeout, seat); + + return (struct ext_idle_notification_v1 *) id; +} + +/** + * @ingroup iface_ext_idle_notifier_v1 + * + * Create a new idle notification object to track input from the + * user, such as keyboard and mouse movement. Because this object is + * meant to track user input alone, it ignores idle inhibitors. + * + * The notification object has a minimum timeout duration and is tied to a + * seat. The client will be notified if the seat is inactive for at least + * the provided timeout. See ext_idle_notification_v1 for more details. + * + * A zero timeout is valid and means the client wants to be notified as + * soon as possible when the seat is inactive. + */ +static inline struct ext_idle_notification_v1 * +ext_idle_notifier_v1_get_input_idle_notification(struct ext_idle_notifier_v1 *ext_idle_notifier_v1, uint32_t timeout, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) ext_idle_notifier_v1, + EXT_IDLE_NOTIFIER_V1_GET_INPUT_IDLE_NOTIFICATION, &ext_idle_notification_v1_interface, wl_proxy_get_version((struct wl_proxy *) ext_idle_notifier_v1), 0, NULL, timeout, seat); + + return (struct ext_idle_notification_v1 *) id; +} + +/** + * @ingroup iface_ext_idle_notification_v1 + * @struct ext_idle_notification_v1_listener + */ +struct ext_idle_notification_v1_listener { + /** + * notification object is idle + * + * This event is sent when the notification object becomes idle. + * + * It's a compositor protocol error to send this event twice + * without a resumed event in-between. + */ + void (*idled)(void *data, + struct ext_idle_notification_v1 *ext_idle_notification_v1); + /** + * notification object is no longer idle + * + * This event is sent when the notification object stops being + * idle. + * + * It's a compositor protocol error to send this event twice + * without an idled event in-between. It's a compositor protocol + * error to send this event prior to any idled event. + */ + void (*resumed)(void *data, + struct ext_idle_notification_v1 *ext_idle_notification_v1); +}; + +/** + * @ingroup iface_ext_idle_notification_v1 + */ +static inline int +ext_idle_notification_v1_add_listener(struct ext_idle_notification_v1 *ext_idle_notification_v1, + const struct ext_idle_notification_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) ext_idle_notification_v1, + (void (**)(void)) listener, data); +} + +#define EXT_IDLE_NOTIFICATION_V1_DESTROY 0 + +/** + * @ingroup iface_ext_idle_notification_v1 + */ +#define EXT_IDLE_NOTIFICATION_V1_IDLED_SINCE_VERSION 1 +/** + * @ingroup iface_ext_idle_notification_v1 + */ +#define EXT_IDLE_NOTIFICATION_V1_RESUMED_SINCE_VERSION 1 + +/** + * @ingroup iface_ext_idle_notification_v1 + */ +#define EXT_IDLE_NOTIFICATION_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_ext_idle_notification_v1 */ +static inline void +ext_idle_notification_v1_set_user_data(struct ext_idle_notification_v1 *ext_idle_notification_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) ext_idle_notification_v1, user_data); +} + +/** @ingroup iface_ext_idle_notification_v1 */ +static inline void * +ext_idle_notification_v1_get_user_data(struct ext_idle_notification_v1 *ext_idle_notification_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) ext_idle_notification_v1); +} + +static inline uint32_t +ext_idle_notification_v1_get_version(struct ext_idle_notification_v1 *ext_idle_notification_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) ext_idle_notification_v1); +} + +/** + * @ingroup iface_ext_idle_notification_v1 + * + * Destroy the notification object. + */ +static inline void +ext_idle_notification_v1_destroy(struct ext_idle_notification_v1 *ext_idle_notification_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) ext_idle_notification_v1, + EXT_IDLE_NOTIFICATION_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) ext_idle_notification_v1), WL_MARSHAL_FLAG_DESTROY); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ext-idle-notify-v1-protocol.c b/ext-idle-notify-v1-protocol.c new file mode 100644 index 0000000..39935b5 --- /dev/null +++ b/ext-idle-notify-v1-protocol.c @@ -0,0 +1,80 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2015 Martin Gräßlin + * Copyright © 2022 Simon Ser + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface ext_idle_notification_v1_interface; +extern const struct wl_interface wl_seat_interface; + +static const struct wl_interface *ext_idle_notify_v1_types[] = { + &ext_idle_notification_v1_interface, + NULL, + &wl_seat_interface, + &ext_idle_notification_v1_interface, + NULL, + &wl_seat_interface, +}; + +static const struct wl_message ext_idle_notifier_v1_requests[] = { + { "destroy", "", ext_idle_notify_v1_types + 0 }, + { "get_idle_notification", "nuo", ext_idle_notify_v1_types + 0 }, + { "get_input_idle_notification", "2nuo", ext_idle_notify_v1_types + 3 }, +}; + +WL_PRIVATE const struct wl_interface ext_idle_notifier_v1_interface = { + "ext_idle_notifier_v1", 2, + 3, ext_idle_notifier_v1_requests, + 0, NULL, +}; + +static const struct wl_message ext_idle_notification_v1_requests[] = { + { "destroy", "", ext_idle_notify_v1_types + 0 }, +}; + +static const struct wl_message ext_idle_notification_v1_events[] = { + { "idled", "", ext_idle_notify_v1_types + 0 }, + { "resumed", "", ext_idle_notify_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface ext_idle_notification_v1_interface = { + "ext_idle_notification_v1", 2, + 1, ext_idle_notification_v1_requests, + 2, ext_idle_notification_v1_events, +}; + diff --git a/main.c b/main.c index 9ce8dbe..fefb4db 100644 --- a/main.c +++ b/main.c @@ -2,8 +2,21 @@ #include #include #include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ext-idle-notify-v1-client-protocol.h" + +#include #include "sleep_utils.h" #include "time_utils.h" @@ -40,8 +53,80 @@ const long unsigned IDLE_TIME_NOT_AVAILABLE_VALUE = ULONG_MAX; volatile sig_atomic_t interruption_received = 0; volatile sig_atomic_t command_paused = 0; +volatile sig_atomic_t sigchld_received = 0; pid_t pid; +static struct wl_display *wayland_display = NULL; +static struct wl_registry *wayland_registry = NULL; +static struct wl_seat *wayland_seat = NULL; +static struct ext_idle_notifier_v1 *wayland_idle_notifier = NULL; +static uint32_t wayland_idle_notifier_version = 0; +static struct ext_idle_notification_v1 *wayland_idle_notification = NULL; +static int wayland_idle_notify_available = 0; + +static int open_pid_file_descriptor_for_process(pid_t process_id) { +#if defined(SYS_pidfd_open) + return (int)syscall(SYS_pidfd_open, process_id, 0); +#elif defined(__NR_pidfd_open) + return (int)syscall(__NR_pidfd_open, process_id, 0); +#else + (void)process_id; + errno = ENOSYS; + return -1; +#endif +} + +static int create_one_shot_timer_file_descriptor_after_ms(long delay_ms) { + int timer_file_descriptor = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timer_file_descriptor < 0) { + return -1; + } + + if (delay_ms < 0) { + delay_ms = 0; + } + + struct itimerspec timer_spec; + memset(&timer_spec, 0, sizeof(timer_spec)); + timer_spec.it_value.tv_sec = delay_ms / 1000; + timer_spec.it_value.tv_nsec = (delay_ms % 1000) * 1000000L; + + if (timerfd_settime(timer_file_descriptor, 0, &timer_spec, NULL) < 0) { + close(timer_file_descriptor); + return -1; + } + + return timer_file_descriptor; +} + +static int create_periodic_timer_file_descriptor_every_ms(long interval_ms) { + int timer_file_descriptor = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timer_file_descriptor < 0) { + return -1; + } + + if (interval_ms <= 0) { + interval_ms = 1000; + } + + struct itimerspec timer_spec; + memset(&timer_spec, 0, sizeof(timer_spec)); + timer_spec.it_value.tv_sec = interval_ms / 1000; + timer_spec.it_value.tv_nsec = (interval_ms % 1000) * 1000000L; + timer_spec.it_interval = timer_spec.it_value; + + if (timerfd_settime(timer_file_descriptor, 0, &timer_spec, NULL) < 0) { + close(timer_file_descriptor); + return -1; + } + + return timer_file_descriptor; +} + +static void consume_timer_file_descriptor(int timer_file_descriptor) { + uint64_t expirations = 0; + (void)read(timer_file_descriptor, &expirations, sizeof(expirations)); +} long unsigned query_user_idle_time() { if (xscreensaver_is_available) { @@ -100,6 +185,11 @@ void sigterm_handler(int signum) { interruption_received = 1; } +void sigchld_handler(int signum) { + (void)signum; + sigchld_received = 1; +} + long long pause_or_resume_command_depending_on_user_activity( long long sleep_time_ms, unsigned long user_idle_time_ms) { @@ -168,27 +258,268 @@ long long pause_or_resume_command_depending_on_user_activity( return sleep_time_ms; } -int main(int argc, char *argv[]) { - parse_command_line_arguments(argc, argv); +static void wayland_idle_notification_idled(void *data, struct ext_idle_notification_v1 *notification) { + (void)data; + (void)notification; - //Open display and initialize XScreensaverInfo for querying idle time - x_display = XOpenDisplay(NULL); - if (!x_display) { - xscreensaver_is_available = 0; - fprintf_error("Couldn't open an X11 display!\n"); + if (!monitoring_started) { + return; + } + + if (debug) { + fprintf(stderr, "Wayland idle: idled()\n"); + } + + if (command_paused) { + if (verbose) { + fprintf(stderr, "Wayland idle: resuming command\n"); + } + if (!quiet) { + printf("Lack of user activity detected. "); + } + resume_command_recursively(pid); + command_paused = 0; + } +} + +static void wayland_idle_notification_resumed(void *data, struct ext_idle_notification_v1 *notification) { + (void)data; + (void)notification; + + if (!monitoring_started) { + return; + } + + if (debug) { + fprintf(stderr, "Wayland idle: resumed()\n"); + } + + if (!command_paused) { + if (verbose) { + fprintf(stderr, "Wayland idle: pausing command\n"); + } + pause_command_recursively(pid); + command_paused = 1; + } +} + +static const struct ext_idle_notification_v1_listener wayland_idle_notification_listener = { + .idled = wayland_idle_notification_idled, + .resumed = wayland_idle_notification_resumed +}; + +static void wayland_registry_global(void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) { + (void)data; + + if (strcmp(interface, "wl_seat") == 0 && wayland_seat == NULL) { + wayland_seat = wl_registry_bind(registry, name, &wl_seat_interface, 1); + return; + } + + if (strcmp(interface, "ext_idle_notifier_v1") == 0 && wayland_idle_notifier == NULL) { + uint32_t bind_version = version < 2 ? version : 2; + wayland_idle_notifier_version = bind_version; + wayland_idle_notifier = wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version); + return; + } +} + +static void wayland_registry_global_remove(void *data, + struct wl_registry *registry, + uint32_t name) { + (void)data; + (void)registry; + (void)name; +} + +static const struct wl_registry_listener wayland_registry_listener = { + .global = wayland_registry_global, + .global_remove = wayland_registry_global_remove +}; + +static int try_initialize_wayland_idle_backend(void) { + wayland_display = wl_display_connect(NULL); + if (!wayland_display) { + return 0; + } + + wayland_registry = wl_display_get_registry(wayland_display); + if (!wayland_registry) { + wl_display_disconnect(wayland_display); + wayland_display = NULL; + return 0; + } + + wl_registry_add_listener(wayland_registry, &wayland_registry_listener, NULL); + wl_display_roundtrip(wayland_display); + + if (wayland_seat == NULL || wayland_idle_notifier == NULL) { + if (wayland_registry) { + wl_registry_destroy(wayland_registry); + wayland_registry = NULL; + } + wl_display_disconnect(wayland_display); + wayland_display = NULL; + wayland_seat = NULL; + wayland_idle_notifier = NULL; + wayland_idle_notify_available = 0; + return 0; + } + + wayland_idle_notify_available = 1; + return 1; +} + +static int start_wayland_idle_notification_object(void) { + if (!wayland_idle_notify_available || wayland_idle_notification != NULL) { + return 0; + } + + uint32_t timeout_ms_for_protocol = (user_idle_timeout_ms > UINT32_MAX) ? UINT32_MAX : (uint32_t)user_idle_timeout_ms; + + if (wayland_idle_notifier_version >= 2) { + wayland_idle_notification = ext_idle_notifier_v1_get_input_idle_notification( + wayland_idle_notifier, timeout_ms_for_protocol, wayland_seat); } else { - int xscreensaver_event_base, xscreensaver_error_base; //not sure why these are needed - xscreensaver_is_available = XScreenSaverQueryExtension(x_display, &xscreensaver_event_base, - &xscreensaver_error_base); - if (xscreensaver_is_available) { - xscreensaver_info = XScreenSaverAllocInfo(); + wayland_idle_notification = ext_idle_notifier_v1_get_idle_notification( + wayland_idle_notifier, timeout_ms_for_protocol, wayland_seat); + } + + if (!wayland_idle_notification) { + return -1; + } + + ext_idle_notification_v1_add_listener(wayland_idle_notification, &wayland_idle_notification_listener, NULL); + wl_display_flush(wayland_display); + return 1; +} + +static int run_wayland_idle_event_loop(void) { + int start_monitor_timer_file_descriptor = create_one_shot_timer_file_descriptor_after_ms(start_monitor_after_ms); + int wayland_file_descriptor = wl_display_get_fd(wayland_display); + + int process_exit_wait_file_descriptor = open_pid_file_descriptor_for_process(pid); + int external_pid_fallback_check_timer_file_descriptor = -1; + if (process_exit_wait_file_descriptor < 0 && external_pid != 0) { + external_pid_fallback_check_timer_file_descriptor = create_periodic_timer_file_descriptor_every_ms(1000); + } + + struct pollfd poll_file_descriptors[4]; + int poll_file_descriptor_count = 0; + + int wayland_poll_index = poll_file_descriptor_count++; + poll_file_descriptors[wayland_poll_index] = (struct pollfd){ .fd = wayland_file_descriptor, .events = POLLIN, .revents = 0 }; + + int start_monitor_poll_index = poll_file_descriptor_count++; + poll_file_descriptors[start_monitor_poll_index] = (struct pollfd){ .fd = start_monitor_timer_file_descriptor, .events = POLLIN, .revents = 0 }; + + int process_exit_poll_index = -1; + if (process_exit_wait_file_descriptor >= 0) { + process_exit_poll_index = poll_file_descriptor_count++; + poll_file_descriptors[process_exit_poll_index] = (struct pollfd){ .fd = process_exit_wait_file_descriptor, .events = POLLIN, .revents = 0 }; + } + + int external_pid_fallback_poll_index = -1; + if (external_pid_fallback_check_timer_file_descriptor >= 0) { + external_pid_fallback_poll_index = poll_file_descriptor_count++; + poll_file_descriptors[external_pid_fallback_poll_index] = (struct pollfd){ + .fd = external_pid_fallback_check_timer_file_descriptor, .events = POLLIN, .revents = 0 + }; + } + + if (verbose) { + fprintf(stderr, "Wayland backend: waiting for idle notifications\n"); + } + + while (1) { + if (interruption_received) { + if (start_monitor_timer_file_descriptor >= 0) close(start_monitor_timer_file_descriptor); + if (process_exit_wait_file_descriptor >= 0) close(process_exit_wait_file_descriptor); + if (external_pid_fallback_check_timer_file_descriptor >= 0) close(external_pid_fallback_check_timer_file_descriptor); + return handle_interruption(); + } + + if (sigchld_received) { + sigchld_received = 0; + exit_if_pid_has_finished(pid); + } + + wl_display_dispatch_pending(wayland_display); + wl_display_flush(wayland_display); + + int poll_result = poll(poll_file_descriptors, poll_file_descriptor_count, -1); + if (poll_result < 0) { + if (errno == EINTR) { + continue; + } + fprintf_error("poll() failed: %s\n", strerror(errno)); + return 1; + } + + if (!monitoring_started && (poll_file_descriptors[start_monitor_poll_index].revents & POLLIN)) { + consume_timer_file_descriptor(start_monitor_timer_file_descriptor); + monitoring_started = 1; + + if (verbose) { + fprintf(stderr, "Starting to monitor user activity (Wayland ext-idle-notify-v1)\n"); + } + + if (start_wayland_idle_notification_object() < 0) { + fprintf_error("Failed to create Wayland idle notification object, user will be considered idle.\n"); + } else { + if (!command_paused) { + pause_command_recursively(pid); + command_paused = 1; + } + } + } + + if (process_exit_poll_index >= 0 && (poll_file_descriptors[process_exit_poll_index].revents & POLLIN)) { + exit_if_pid_has_finished(pid); + } + + if (external_pid_fallback_poll_index >= 0 && (poll_file_descriptors[external_pid_fallback_poll_index].revents & POLLIN)) { + consume_timer_file_descriptor(external_pid_fallback_check_timer_file_descriptor); + exit_if_pid_has_finished(pid); + } + + if (poll_file_descriptors[wayland_poll_index].revents & POLLIN) { + int dispatch_result = wl_display_dispatch(wayland_display); + if (dispatch_result < 0 && errno != EINTR) { + fprintf_error("Wayland display dispatch failed: %s\n", strerror(errno)); + fprintf_error("User will be considered idle to allow the command to finish.\n"); + break; + } + } else if (poll_file_descriptors[wayland_poll_index].revents & (POLLHUP | POLLERR)) { + fprintf_error("Wayland connection closed, user will be considered idle to allow the command to finish.\n"); + break; } } - if (!xscreensaver_is_available) { - fprintf_error( - "No available method for detecting user idle time on the system, user will be considered idle to allow the command to finish.\n"); + if (start_monitor_timer_file_descriptor >= 0) close(start_monitor_timer_file_descriptor); + if (process_exit_wait_file_descriptor >= 0) close(process_exit_wait_file_descriptor); + if (external_pid_fallback_check_timer_file_descriptor >= 0) close(external_pid_fallback_check_timer_file_descriptor); + + while (1) { + if (interruption_received) { + return handle_interruption(); + } + if (sigchld_received) { + sigchld_received = 0; + exit_if_pid_has_finished(pid); + } + exit_if_pid_has_finished(pid); + sleep_for_milliseconds(250); } +} + +int main(int argc, char *argv[]) { + parse_command_line_arguments(argc, argv); + if (external_pid == 0) { pid = run_shell_command(shell_command_to_run); } else { @@ -199,23 +530,73 @@ int main(int argc, char *argv[]) { } } free(shell_command_to_run); + + signal(SIGINT, sigint_handler); + signal(SIGTERM, sigterm_handler); + signal(SIGCHLD, sigchld_handler); + + if (try_initialize_wayland_idle_backend()) { + int wayland_loop_result = run_wayland_idle_event_loop(); + + if (wayland_idle_notification) { + ext_idle_notification_v1_destroy(wayland_idle_notification); + wayland_idle_notification = NULL; + } + if (wayland_idle_notifier) { + ext_idle_notifier_v1_destroy(wayland_idle_notifier); + wayland_idle_notifier = NULL; + } + if (wayland_seat) { + wl_seat_destroy(wayland_seat); + wayland_seat = NULL; + } + if (wayland_registry) { + wl_registry_destroy(wayland_registry); + wayland_registry = NULL; + } + if (wayland_display) { + wl_display_disconnect(wayland_display); + wayland_display = NULL; + } + + return wayland_loop_result; + } + + x_display = XOpenDisplay(NULL); + if (!x_display) { + xscreensaver_is_available = 0; + fprintf_error("Couldn't open an X11 display!\n"); + } else { + int xscreensaver_event_base, xscreensaver_error_base; + xscreensaver_is_available = XScreenSaverQueryExtension( + x_display, &xscreensaver_event_base, &xscreensaver_error_base); + if (xscreensaver_is_available) { + xscreensaver_info = XScreenSaverAllocInfo(); + } + } + + if (!xscreensaver_is_available) { + fprintf_error("No available method for detecting user idle time on the system, user will be considered idle to allow the command to finish.\n"); + } + struct timespec time_when_command_started; clock_gettime(CLOCK_MONOTONIC, &time_when_command_started); - long long sleep_time_ms = POLLING_INTERVAL_BEFORE_STARTING_MONITORING_MS; unsigned long user_idle_time_ms = 0; - signal(SIGINT, sigint_handler); - signal(SIGTERM, sigterm_handler); if (verbose) { - fprintf(stderr, "Starting to monitor user activity\n"); + fprintf(stderr, "Starting to monitor user activity (X11 polling)\n"); } - // Monitor user activity + while (1) { if (interruption_received) { return handle_interruption(); } + if (sigchld_received) { + sigchld_received = 0; + exit_if_pid_has_finished(pid); + } if (!monitoring_started) { struct timespec current_time; clock_gettime(CLOCK_MONOTONIC, ¤t_time); From 1ebbf94dd87ed0a3e7023cf6cac308c425b64092 Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Mon, 5 Jan 2026 02:05:54 -0600 Subject: [PATCH 2/7] Cron support --- Makefile | 2 +- main.c | 499 +++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 414 insertions(+), 87 deletions(-) diff --git a/Makefile b/Makefile index ed974cd..fb936d4 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ TARGET_EXEC := runwhenidle -LDLIBS=-lXss -lX11 -lwayland-client -lm +LDLIBS=-lXss -lX11 -lwayland-client CC=gcc ifeq ($(PREFIX),) PREFIX := /usr diff --git a/main.c b/main.c index fefb4db..145f4ff 100644 --- a/main.c +++ b/main.c @@ -11,9 +11,11 @@ #include #include #include +#include +#include +#include #include - #include "ext-idle-notify-v1-client-protocol.h" #include @@ -56,6 +58,8 @@ volatile sig_atomic_t command_paused = 0; volatile sig_atomic_t sigchld_received = 0; pid_t pid; +static int invoked_from_cron = 0; + static struct wl_display *wayland_display = NULL; static struct wl_registry *wayland_registry = NULL; static struct wl_seat *wayland_seat = NULL; @@ -64,6 +68,335 @@ static uint32_t wayland_idle_notifier_version = 0; static struct ext_idle_notification_v1 *wayland_idle_notification = NULL; static int wayland_idle_notify_available = 0; +static int is_string_null_or_empty(const char *value) { + return value == NULL || value[0] == '\0'; +} + +static int file_is_socket(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + return S_ISSOCK(st.st_mode) ? 1 : 0; +} + +static int file_is_readable_regular_file(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + if (!S_ISREG(st.st_mode)) { + return 0; + } + return access(path, R_OK) == 0 ? 1 : 0; +} + +static int directory_exists_and_accessible(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + if (!S_ISDIR(st.st_mode)) { + return 0; + } + return access(path, R_OK | X_OK) == 0 ? 1 : 0; +} + +static int read_process_status_ppid(pid_t target_pid, pid_t *out_ppid) { + char status_path[64]; + snprintf(status_path, sizeof(status_path), "/proc/%d/status", target_pid); + + FILE *fp = fopen(status_path, "r"); + if (!fp) { + return 0; + } + + char line[256]; + while (fgets(line, sizeof(line), fp)) { + if (strncmp(line, "PPid:", 5) == 0) { + long parsed = strtol(line + 5, NULL, 10); + fclose(fp); + if (parsed <= 0) { + return 0; + } + *out_ppid = (pid_t)parsed; + return 1; + } + } + + fclose(fp); + return 0; +} + +static int read_process_comm(pid_t target_pid, char *out_comm, size_t out_comm_size) { + char comm_path[64]; + snprintf(comm_path, sizeof(comm_path), "/proc/%d/comm", target_pid); + + FILE *fp = fopen(comm_path, "r"); + if (!fp) { + return 0; + } + + if (!fgets(out_comm, (int)out_comm_size, fp)) { + fclose(fp); + return 0; + } + fclose(fp); + + out_comm[strcspn(out_comm, "\n")] = '\0'; + return 1; +} + +static int comm_matches_any_cron_name(const char *comm) { + const char *cron_names[] = { "cron", "crond", "anacron", "cronie", "fcron", NULL }; + for (int i = 0; cron_names[i] != NULL; i++) { + if (strcmp(comm, cron_names[i]) == 0) { + return 1; + } + } + return 0; +} + +static int detect_invoked_from_cron_via_process_tree(void) { + pid_t current_pid = getpid(); + + for (int depth = 0; depth < 12; depth++) { + pid_t parent_pid = 0; + if (!read_process_status_ppid(current_pid, &parent_pid)) { + break; + } + if (parent_pid <= 1) { + break; + } + + char comm[128]; + if (read_process_comm(parent_pid, comm, sizeof(comm))) { + if (comm_matches_any_cron_name(comm)) { + return 1; + } + } + + current_pid = parent_pid; + } + + return 0; +} + +static int get_home_directory_for_current_user(char *out_home, size_t out_home_size) { + const char *home_env = getenv("HOME"); + if (!is_string_null_or_empty(home_env)) { + snprintf(out_home, out_home_size, "%s", home_env); + return 1; + } + + struct passwd *pw = getpwuid(getuid()); + if (!pw || !pw->pw_dir) { + return 0; + } + + snprintf(out_home, out_home_size, "%s", pw->pw_dir); + return 1; +} + +static int build_xauthority_path_from_home_dir(char *xauthority_path, + size_t xauthority_path_size, + const char *home_dir) { + static const char xauthority_suffix[] = "/.Xauthority"; + size_t home_dir_length = strnlen(home_dir, xauthority_path_size); + size_t suffix_length = sizeof(xauthority_suffix) - 1; + + if (home_dir_length == 0 || home_dir_length >= xauthority_path_size) { + return 0; + } + + if (home_dir_length + suffix_length + 1 > xauthority_path_size) { + return 0; + } + + memcpy(xauthority_path, home_dir, home_dir_length); + memcpy(xauthority_path + home_dir_length, xauthority_suffix, suffix_length + 1); + return 1; +} + +static void ensure_xauthority_is_set_if_possible(void) { + if (!is_string_null_or_empty(getenv("XAUTHORITY"))) { + return; + } + + char home_dir[PATH_MAX]; + if (!get_home_directory_for_current_user(home_dir, sizeof(home_dir))) { + return; + } + + char xauthority_path[PATH_MAX]; + if (!build_xauthority_path_from_home_dir(xauthority_path, sizeof(xauthority_path), home_dir)) { + return; + } + + if (file_is_readable_regular_file(xauthority_path)) { + setenv("XAUTHORITY", xauthority_path, 0); + } +} + + +static int build_default_xdg_runtime_dir_for_current_user(char *out_runtime_dir, size_t out_runtime_dir_size) { + uid_t uid = getuid(); + snprintf(out_runtime_dir, out_runtime_dir_size, "/run/user/%u", (unsigned)uid); + if (!directory_exists_and_accessible(out_runtime_dir)) { + return 0; + } + return 1; +} + +static int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, + char *out_socket_path, + size_t out_socket_path_size, + char *out_socket_name, + size_t out_socket_name_size) { + DIR *dir = opendir(runtime_dir); + if (!dir) { + return 0; + } + + int found = 0; + int best_numeric_suffix = INT_MAX; + char best_name[NAME_MAX + 1]; + best_name[0] = '\0'; + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, "wayland-", 8) != 0) { + continue; + } + + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s/%s", runtime_dir, entry->d_name); + + if (!file_is_socket(full_path)) { + continue; + } + + int numeric_suffix = -1; + const char *suffix = entry->d_name + 8; + if (*suffix >= '0' && *suffix <= '9') { + numeric_suffix = (int)strtol(suffix, NULL, 10); + } + + if (!found) { + found = 1; + best_numeric_suffix = (numeric_suffix >= 0) ? numeric_suffix : INT_MAX; + snprintf(best_name, sizeof(best_name), "%s", entry->d_name); + continue; + } + + if (numeric_suffix >= 0 && numeric_suffix < best_numeric_suffix) { + best_numeric_suffix = numeric_suffix; + snprintf(best_name, sizeof(best_name), "%s", entry->d_name); + continue; + } + + if (best_numeric_suffix == INT_MAX && numeric_suffix == -1) { + snprintf(best_name, sizeof(best_name), "%s", entry->d_name); + } + } + + closedir(dir); + + if (!found) { + return 0; + } + + snprintf(out_socket_path, out_socket_path_size, "%s/%s", runtime_dir, best_name); + if (out_socket_name && out_socket_name_size > 0) { + snprintf(out_socket_name, out_socket_name_size, "%s", best_name); + } + return 1; +} + +static int find_best_x11_display_from_socket_dir(char *out_display, size_t out_display_size) { + const char *x11_socket_dir = "/tmp/.X11-unix"; + DIR *dir = opendir(x11_socket_dir); + if (!dir) { + return 0; + } + + int found = 0; + int best_display_number = INT_MAX; + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] != 'X') { + continue; + } + const char *digits = entry->d_name + 1; + if (*digits < '0' || *digits > '9') { + continue; + } + + int display_number = (int)strtol(digits, NULL, 10); + + char socket_path[PATH_MAX]; + snprintf(socket_path, sizeof(socket_path), "%s/%s", x11_socket_dir, entry->d_name); + + if (!file_is_socket(socket_path)) { + continue; + } + + if (!found || display_number < best_display_number) { + found = 1; + best_display_number = display_number; + } + } + + closedir(dir); + + if (!found) { + return 0; + } + + snprintf(out_display, out_display_size, ":%d", best_display_number); + return 1; +} + +static void best_effort_infer_graphical_session_environment_if_missing(void) { + if (!is_string_null_or_empty(getenv("WAYLAND_DISPLAY")) || !is_string_null_or_empty(getenv("DISPLAY"))) { + return; + } + + char inferred_runtime_dir[PATH_MAX]; + if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { + if (build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { + setenv("XDG_RUNTIME_DIR", inferred_runtime_dir, 0); + } + } + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (!is_string_null_or_empty(runtime_dir) && is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { + char wayland_socket_path[PATH_MAX]; + char wayland_socket_name[NAME_MAX + 1]; + if (find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), + wayland_socket_name, sizeof(wayland_socket_name))) { + setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); + } + } + + if (is_string_null_or_empty(getenv("DISPLAY"))) { + char inferred_display[32]; + if (find_best_x11_display_from_socket_dir(inferred_display, sizeof(inferred_display))) { + setenv("DISPLAY", inferred_display, 0); + ensure_xauthority_is_set_if_possible(); + } + } + + if (verbose && invoked_from_cron) { + const char *wd = getenv("WAYLAND_DISPLAY"); + const char *xdg = getenv("XDG_RUNTIME_DIR"); + const char *dpy = getenv("DISPLAY"); + fprintf(stderr, "Detected cron context; inferred session vars: XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s DISPLAY=%s\n", + xdg ? xdg : "(unset)", wd ? wd : "(unset)", dpy ? dpy : "(unset)"); + } +} + static int open_pid_file_descriptor_for_process(pid_t process_id) { #if defined(SYS_pidfd_open) return (int)syscall(SYS_pidfd_open, process_id, 0); @@ -190,74 +523,6 @@ void sigchld_handler(int signum) { sigchld_received = 1; } -long long pause_or_resume_command_depending_on_user_activity( - long long sleep_time_ms, - unsigned long user_idle_time_ms) { - if (user_idle_time_ms >= user_idle_timeout_ms) { - if (debug) - fprintf(stderr, "Idle time: %lums, idle timeout: %lums, user is inactive\n", user_idle_time_ms, - user_idle_timeout_ms); - if (command_paused) { - sleep_time_ms = POLLING_INTERVAL_MS; //reset to default value - if (verbose) { - fprintf(stderr, "Idle time: %lums, idle timeout: %lums, resuming command\n", user_idle_time_ms, - user_idle_timeout_ms); - } - if (!quiet) { - printf("Lack of user activity detected. "); - //intentionally no new line here, resume_command will print the rest of the message. - } - resume_command_recursively(pid); - command_paused = 0; - } - } else { - struct timespec time_when_starting_to_pause; - int command_was_paused_this_iteration = 0; - // User is active - if (!command_paused) { - clock_gettime(CLOCK_MONOTONIC, &time_when_starting_to_pause); - if (verbose) { - fprintf(stderr, "Idle time: %lums.\n", user_idle_time_ms); - } - pause_command_recursively(pid); - if (debug) fprintf(stderr, "Command paused\n"); - command_paused = 1; - command_was_paused_this_iteration = 1; - } - sleep_time_ms = user_idle_timeout_ms - user_idle_time_ms; - if (debug) fprintf(stderr, "Target sleep time: %llums\n", sleep_time_ms); - if (command_was_paused_this_iteration) { - if (debug) fprintf(stderr, "Command was paused this iteration\n"); - struct timespec time_before_sleep; - clock_gettime(CLOCK_MONOTONIC, &time_before_sleep); - long long pausing_time_ms = get_elapsed_time_ms(time_when_starting_to_pause, time_before_sleep); - if (debug) - fprintf(stderr, - "Target sleep time before taking into account time it took to pause: %lldms, time it took to pause: %lldms\n", - sleep_time_ms, pausing_time_ms); - sleep_time_ms = sleep_time_ms - pausing_time_ms; - - } - - if (sleep_time_ms < POLLING_INTERVAL_MS) { - if (debug) - fprintf(stderr, - "Target sleep time %lldms is less than polling interval %lldms, resetting it to polling interval\n", - sleep_time_ms, POLLING_INTERVAL_MS); - sleep_time_ms = POLLING_INTERVAL_MS; - } - if (verbose) { - fprintf( - stderr, - "Polling every second is temporarily disabled due to user activity, idle time: %lums, next activity check scheduled in %lldms\n", - user_idle_time_ms, - sleep_time_ms - ); - } - } - return sleep_time_ms; -} - static void wayland_idle_notification_idled(void *data, struct ext_idle_notification_v1 *notification) { (void)data; (void)notification; @@ -341,8 +606,45 @@ static const struct wl_registry_listener wayland_registry_listener = { .global_remove = wayland_registry_global_remove }; +static struct wl_display *connect_to_wayland_best_effort(void) { + struct wl_display *display = wl_display_connect(NULL); + if (display) { + return display; + } + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + char inferred_runtime_dir[PATH_MAX]; + if (is_string_null_or_empty(runtime_dir)) { + if (!build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { + return NULL; + } + runtime_dir = inferred_runtime_dir; + } + + char wayland_socket_path[PATH_MAX]; + char wayland_socket_name[NAME_MAX + 1]; + if (!find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), + wayland_socket_name, sizeof(wayland_socket_name))) { + return NULL; + } + + display = wl_display_connect(wayland_socket_path); + if (!display) { + return NULL; + } + + if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { + setenv("XDG_RUNTIME_DIR", runtime_dir, 0); + } + if (is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { + setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); + } + + return display; +} + static int try_initialize_wayland_idle_backend(void) { - wayland_display = wl_display_connect(NULL); + wayland_display = connect_to_wayland_best_effort(); if (!wayland_display) { return 0; } @@ -358,10 +660,8 @@ static int try_initialize_wayland_idle_backend(void) { wl_display_roundtrip(wayland_display); if (wayland_seat == NULL || wayland_idle_notifier == NULL) { - if (wayland_registry) { - wl_registry_destroy(wayland_registry); - wayland_registry = NULL; - } + wl_registry_destroy(wayland_registry); + wayland_registry = NULL; wl_display_disconnect(wayland_display); wayland_display = NULL; wayland_seat = NULL; @@ -431,10 +731,6 @@ static int run_wayland_idle_event_loop(void) { }; } - if (verbose) { - fprintf(stderr, "Wayland backend: waiting for idle notifications\n"); - } - while (1) { if (interruption_received) { if (start_monitor_timer_file_descriptor >= 0) close(start_monitor_timer_file_descriptor); @@ -464,10 +760,6 @@ static int run_wayland_idle_event_loop(void) { consume_timer_file_descriptor(start_monitor_timer_file_descriptor); monitoring_started = 1; - if (verbose) { - fprintf(stderr, "Starting to monitor user activity (Wayland ext-idle-notify-v1)\n"); - } - if (start_wayland_idle_notification_object() < 0) { fprintf_error("Failed to create Wayland idle notification object, user will be considered idle.\n"); } else { @@ -517,9 +809,31 @@ static int run_wayland_idle_event_loop(void) { } } +static Display *open_x11_display_best_effort(void) { + Display *display = XOpenDisplay(NULL); + if (display) { + return display; + } + + if (!is_string_null_or_empty(getenv("DISPLAY"))) { + return NULL; + } + + char inferred_display[32]; + if (!find_best_x11_display_from_socket_dir(inferred_display, sizeof(inferred_display))) { + return NULL; + } + + setenv("DISPLAY", inferred_display, 0); + ensure_xauthority_is_set_if_possible(); + return XOpenDisplay(NULL); +} + int main(int argc, char *argv[]) { parse_command_line_arguments(argc, argv); + invoked_from_cron = detect_invoked_from_cron_via_process_tree(); + if (external_pid == 0) { pid = run_shell_command(shell_command_to_run); } else { @@ -535,6 +849,8 @@ int main(int argc, char *argv[]) { signal(SIGTERM, sigterm_handler); signal(SIGCHLD, sigchld_handler); + best_effort_infer_graphical_session_environment_if_missing(); + if (try_initialize_wayland_idle_backend()) { int wayland_loop_result = run_wayland_idle_event_loop(); @@ -562,7 +878,7 @@ int main(int argc, char *argv[]) { return wayland_loop_result; } - x_display = XOpenDisplay(NULL); + x_display = open_x11_display_best_effort(); if (!x_display) { xscreensaver_is_available = 0; fprintf_error("Couldn't open an X11 display!\n"); @@ -615,9 +931,20 @@ int main(int argc, char *argv[]) { exit_if_pid_has_finished(pid); if (monitoring_started) { - sleep_time_ms = pause_or_resume_command_depending_on_user_activity( - sleep_time_ms, - user_idle_time_ms); + if (user_idle_time_ms >= user_idle_timeout_ms) { + if (command_paused) { + if (!quiet) { + printf("Lack of user activity detected. "); + } + resume_command_recursively(pid); + command_paused = 0; + } + } else { + if (!command_paused) { + pause_command_recursively(pid); + command_paused = 1; + } + } } if (debug) fprintf(stderr, "Sleeping for %lldms\n", sleep_time_ms); sleep_for_milliseconds(sleep_time_ms); From ff32f2b26253cea4e1a4567a235b7d72b57b6b89 Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Sat, 14 Mar 2026 22:24:44 +0300 Subject: [PATCH 3/7] Update README with Wayland Clarified Wayland support status and updated known issues section. --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ce99ce..c0cfd66 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,12 @@ always handled by the OS. ## Under the Hood: How runwhenidle Controls Processes Based on User Activity -runwhenidle uses XScreenSaverQueryInfo() to check when last user activity happened. -Therefore, running X server is required. Wayland is not currently supported. + +### Wayland +On Wayland ext_idle_notification_v1 is used. + +### X11 +On X11 runwhenidle uses XScreenSaverQueryInfo() to check when last user activity happened. When user is active, these checks happen as infrequently as possible to satisfy the desired inactivity timeout (default - every 5 minutes). When user is inactive, these checks happen once per second, to allow to restore the system responsiveness quickly. @@ -83,8 +87,7 @@ If you want to install it system-wide, run `sudo make install` or simply `sudo c ### Known issues -1. Wayland support. runwhenidle currently doesn't work without XScreenSaver, but Wayland support should be possible and - is planned (at least for the DEs supporting ext-idle-notify, which now both Gnome and KDE support). +1. Wayland support is currently only present in the main branch. It has worked well for me for 2 months, but some clean up is needed for a release. 2. When monitoring an existing pid, once it gets paused, it gets detached from the terminal it was in. Running "fg" command could be a workaround to get it reattached, but it is required after every pause. From c95648fbfaaf7736184b1cab884db9e9bb4c7f1a Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Sat, 14 Mar 2026 22:32:12 -0500 Subject: [PATCH 4/7] Move helper functions to separate files and remove cron process guess logic as it is not really needed --- Makefile | 2 +- environment_guessing.c | 263 +++++++++++++++++++++++++++ environment_guessing.h | 9 + file_utils.c | 53 ++++++ file_utils.h | 10 ++ main.c | 393 +---------------------------------------- string_utils.c | 7 + string_utils.h | 4 + 8 files changed, 349 insertions(+), 392 deletions(-) create mode 100644 environment_guessing.c create mode 100644 environment_guessing.h create mode 100644 file_utils.c create mode 100644 file_utils.h create mode 100644 string_utils.c create mode 100644 string_utils.h diff --git a/Makefile b/Makefile index fb936d4..7f4a7a2 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ CC=gcc ifeq ($(PREFIX),) PREFIX := /usr endif -SOURCES = time_utils.c sleep_utils.c tty_utils.c process_handling.c arguments_parsing.c ext-idle-notify-v1-protocol.c main.c +SOURCES = time_utils.c sleep_utils.c tty_utils.c file_utils.c string_utils.c process_handling.c arguments_parsing.c ext-idle-notify-v1-protocol.c environment_guessing.c main.c OBJECTS = $(SOURCES:.c=.o) CCFLAGS = -Werror=all all: executable diff --git a/environment_guessing.c b/environment_guessing.c new file mode 100644 index 0000000..1d10a65 --- /dev/null +++ b/environment_guessing.c @@ -0,0 +1,263 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "file_utils.h" +#include "string_utils.h" + +static int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, + char *out_socket_path, + size_t out_socket_path_size, + char *out_socket_name, + size_t out_socket_name_size) { + DIR *dir = opendir(runtime_dir); + if (!dir) { + return 0; + } + + int found = 0; + int best_numeric_suffix = INT_MAX; + char best_name[NAME_MAX + 1]; + best_name[0] = '\0'; + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, "wayland-", 8) != 0) { + continue; + } + + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s/%s", runtime_dir, entry->d_name); + + if (!file_is_socket(full_path)) { + continue; + } + + int numeric_suffix = -1; + const char *suffix = entry->d_name + 8; + if (*suffix >= '0' && *suffix <= '9') { + numeric_suffix = (int)strtol(suffix, NULL, 10); + } + + if (!found) { + found = 1; + best_numeric_suffix = (numeric_suffix >= 0) ? numeric_suffix : INT_MAX; + snprintf(best_name, sizeof(best_name), "%s", entry->d_name); + continue; + } + + if (numeric_suffix >= 0 && numeric_suffix < best_numeric_suffix) { + best_numeric_suffix = numeric_suffix; + snprintf(best_name, sizeof(best_name), "%s", entry->d_name); + continue; + } + + if (best_numeric_suffix == INT_MAX && numeric_suffix == -1) { + snprintf(best_name, sizeof(best_name), "%s", entry->d_name); + } + } + + closedir(dir); + + if (!found) { + return 0; + } + + snprintf(out_socket_path, out_socket_path_size, "%s/%s", runtime_dir, best_name); + if (out_socket_name && out_socket_name_size > 0) { + snprintf(out_socket_name, out_socket_name_size, "%s", best_name); + } + return 1; +} + +static int build_default_xdg_runtime_dir_for_current_user(char *out_runtime_dir, size_t out_runtime_dir_size) { + uid_t uid = getuid(); + snprintf(out_runtime_dir, out_runtime_dir_size, "/run/user/%u", (unsigned)uid); + if (!directory_exists_and_accessible(out_runtime_dir)) { + return 0; + } + return 1; +} +static int build_xauthority_path_from_home_dir(char *xauthority_path, + size_t xauthority_path_size, + const char *home_dir) { + static const char xauthority_suffix[] = "/.Xauthority"; + size_t home_dir_length = strnlen(home_dir, xauthority_path_size); + size_t suffix_length = sizeof(xauthority_suffix) - 1; + + if (home_dir_length == 0 || home_dir_length >= xauthority_path_size) { + return 0; + } + + if (home_dir_length + suffix_length + 1 > xauthority_path_size) { + return 0; + } + + memcpy(xauthority_path, home_dir, home_dir_length); + memcpy(xauthority_path + home_dir_length, xauthority_suffix, suffix_length + 1); + return 1; +} +static void ensure_xauthority_is_set_if_possible(void) { + if (!is_string_null_or_empty(getenv("XAUTHORITY"))) { + return; + } + + char home_dir[PATH_MAX]; + if (!get_home_directory_for_current_user(home_dir, sizeof(home_dir))) { + return; + } + + char xauthority_path[PATH_MAX]; + if (!build_xauthority_path_from_home_dir(xauthority_path, sizeof(xauthority_path), home_dir)) { + return; + } + + if (file_is_readable_regular_file(xauthority_path)) { + setenv("XAUTHORITY", xauthority_path, 0); + } +} +static int find_best_x11_display_from_socket_dir(char *out_display, size_t out_display_size) { + const char *x11_socket_dir = "/tmp/.X11-unix"; + DIR *dir = opendir(x11_socket_dir); + if (!dir) { + return 0; + } + + int found = 0; + int best_display_number = INT_MAX; + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] != 'X') { + continue; + } + const char *digits = entry->d_name + 1; + if (*digits < '0' || *digits > '9') { + continue; + } + + int display_number = (int)strtol(digits, NULL, 10); + + char socket_path[PATH_MAX]; + snprintf(socket_path, sizeof(socket_path), "%s/%s", x11_socket_dir, entry->d_name); + + if (!file_is_socket(socket_path)) { + continue; + } + + if (!found || display_number < best_display_number) { + found = 1; + best_display_number = display_number; + } + } + + closedir(dir); + + if (!found) { + return 0; + } + + snprintf(out_display, out_display_size, ":%d", best_display_number); + return 1; +} + +void best_effort_infer_graphical_session_environment_if_missing(bool log_when_inferred) { + if (!is_string_null_or_empty(getenv("WAYLAND_DISPLAY")) || !is_string_null_or_empty(getenv("DISPLAY"))) { + return; + } + + char inferred_runtime_dir[PATH_MAX]; + if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { + if (build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { + setenv("XDG_RUNTIME_DIR", inferred_runtime_dir, 0); + } + } + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (!is_string_null_or_empty(runtime_dir) && is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { + char wayland_socket_path[PATH_MAX]; + char wayland_socket_name[NAME_MAX + 1]; + if (find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), + wayland_socket_name, sizeof(wayland_socket_name))) { + setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); + } + } + + if (is_string_null_or_empty(getenv("DISPLAY"))) { + char inferred_display[32]; + if (find_best_x11_display_from_socket_dir(inferred_display, sizeof(inferred_display))) { + setenv("DISPLAY", inferred_display, 0); + ensure_xauthority_is_set_if_possible(); + } + } + + if (log_when_inferred) { + const char *wd = getenv("WAYLAND_DISPLAY"); + const char *xdg = getenv("XDG_RUNTIME_DIR"); + const char *dpy = getenv("DISPLAY"); + fprintf(stderr, "inferred session vars: XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s DISPLAY=%s\n", + xdg ? xdg : "(unset)", wd ? wd : "(unset)", dpy ? dpy : "(unset)"); + } + } + +struct wl_display *connect_to_wayland_best_effort(void) { + struct wl_display *display = wl_display_connect(NULL); + if (display) { + return display; + } + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + char inferred_runtime_dir[PATH_MAX]; + if (is_string_null_or_empty(runtime_dir)) { + if (!build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { + return NULL; + } + runtime_dir = inferred_runtime_dir; + } + + char wayland_socket_path[PATH_MAX]; + char wayland_socket_name[NAME_MAX + 1]; + if (!find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), + wayland_socket_name, sizeof(wayland_socket_name))) { + return NULL; + } + + display = wl_display_connect(wayland_socket_path); + if (!display) { + return NULL; + } + + if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { + setenv("XDG_RUNTIME_DIR", runtime_dir, 0); + } + if (is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { + setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); + } + + return display; + } + +Display *open_x11_display_best_effort(void) { + Display *display = XOpenDisplay(NULL); + if (display) { + return display; + } + + if (!is_string_null_or_empty(getenv("DISPLAY"))) { + return NULL; + } + + char inferred_display[32]; + if (!find_best_x11_display_from_socket_dir(inferred_display, sizeof(inferred_display))) { + return NULL; + } + + setenv("DISPLAY", inferred_display, 0); + ensure_xauthority_is_set_if_possible(); + return XOpenDisplay(NULL); + } \ No newline at end of file diff --git a/environment_guessing.h b/environment_guessing.h new file mode 100644 index 0000000..efe8013 --- /dev/null +++ b/environment_guessing.h @@ -0,0 +1,9 @@ +#ifndef RUNWHENIDLE_ENVIRONMENT_GUESSING_H +#define RUNWHENIDLE_ENVIRONMENT_GUESSING_H + +void best_effort_infer_graphical_session_environment_if_missing(bool log_when_inferred); +Display *open_x11_display_best_effort(void); +struct wl_display *connect_to_wayland_best_effort(void); + + +#endif //RUNWHENIDLE_ENVIRONMENT_GUESSING_H diff --git a/file_utils.c b/file_utils.c new file mode 100644 index 0000000..d9d4d7f --- /dev/null +++ b/file_utils.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +#include "string_utils.h" + +int file_is_socket(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + return S_ISSOCK(st.st_mode) ? 1 : 0; +} + +int file_is_readable_regular_file(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + if (!S_ISREG(st.st_mode)) { + return 0; + } + return access(path, R_OK) == 0 ? 1 : 0; +} + +int directory_exists_and_accessible(const char *path) { + struct stat st; + if (stat(path, &st) != 0) { + return 0; + } + if (!S_ISDIR(st.st_mode)) { + return 0; + } + return access(path, R_OK | X_OK) == 0 ? 1 : 0; +} + +int get_home_directory_for_current_user(char *out_home, size_t out_home_size) { + const char *home_env = getenv("HOME"); + if (!is_string_null_or_empty(home_env)) { + snprintf(out_home, out_home_size, "%s", home_env); + return 1; + } + + struct passwd *pw = getpwuid(getuid()); + if (!pw || !pw->pw_dir) { + return 0; + } + + snprintf(out_home, out_home_size, "%s", pw->pw_dir); + return 1; +} \ No newline at end of file diff --git a/file_utils.h b/file_utils.h new file mode 100644 index 0000000..59a5510 --- /dev/null +++ b/file_utils.h @@ -0,0 +1,10 @@ +#ifndef RUNWHENIDLE_FILE_UTILS_H +#define RUNWHENIDLE_FILE_UTILS_H + +#include +int file_is_socket(const char *path); +int file_is_readable_regular_file(const char *path); +int directory_exists_and_accessible(const char *path); +int get_home_directory_for_current_user(char *out_home, size_t out_home_size); + +#endif \ No newline at end of file diff --git a/main.c b/main.c index 145f4ff..7d865d6 100644 --- a/main.c +++ b/main.c @@ -11,8 +11,6 @@ #include #include #include -#include -#include #include #include @@ -20,6 +18,7 @@ #include +#include "environment_guessing.h" #include "sleep_utils.h" #include "time_utils.h" #include "tty_utils.h" @@ -58,8 +57,6 @@ volatile sig_atomic_t command_paused = 0; volatile sig_atomic_t sigchld_received = 0; pid_t pid; -static int invoked_from_cron = 0; - static struct wl_display *wayland_display = NULL; static struct wl_registry *wayland_registry = NULL; static struct wl_seat *wayland_seat = NULL; @@ -68,335 +65,6 @@ static uint32_t wayland_idle_notifier_version = 0; static struct ext_idle_notification_v1 *wayland_idle_notification = NULL; static int wayland_idle_notify_available = 0; -static int is_string_null_or_empty(const char *value) { - return value == NULL || value[0] == '\0'; -} - -static int file_is_socket(const char *path) { - struct stat st; - if (stat(path, &st) != 0) { - return 0; - } - return S_ISSOCK(st.st_mode) ? 1 : 0; -} - -static int file_is_readable_regular_file(const char *path) { - struct stat st; - if (stat(path, &st) != 0) { - return 0; - } - if (!S_ISREG(st.st_mode)) { - return 0; - } - return access(path, R_OK) == 0 ? 1 : 0; -} - -static int directory_exists_and_accessible(const char *path) { - struct stat st; - if (stat(path, &st) != 0) { - return 0; - } - if (!S_ISDIR(st.st_mode)) { - return 0; - } - return access(path, R_OK | X_OK) == 0 ? 1 : 0; -} - -static int read_process_status_ppid(pid_t target_pid, pid_t *out_ppid) { - char status_path[64]; - snprintf(status_path, sizeof(status_path), "/proc/%d/status", target_pid); - - FILE *fp = fopen(status_path, "r"); - if (!fp) { - return 0; - } - - char line[256]; - while (fgets(line, sizeof(line), fp)) { - if (strncmp(line, "PPid:", 5) == 0) { - long parsed = strtol(line + 5, NULL, 10); - fclose(fp); - if (parsed <= 0) { - return 0; - } - *out_ppid = (pid_t)parsed; - return 1; - } - } - - fclose(fp); - return 0; -} - -static int read_process_comm(pid_t target_pid, char *out_comm, size_t out_comm_size) { - char comm_path[64]; - snprintf(comm_path, sizeof(comm_path), "/proc/%d/comm", target_pid); - - FILE *fp = fopen(comm_path, "r"); - if (!fp) { - return 0; - } - - if (!fgets(out_comm, (int)out_comm_size, fp)) { - fclose(fp); - return 0; - } - fclose(fp); - - out_comm[strcspn(out_comm, "\n")] = '\0'; - return 1; -} - -static int comm_matches_any_cron_name(const char *comm) { - const char *cron_names[] = { "cron", "crond", "anacron", "cronie", "fcron", NULL }; - for (int i = 0; cron_names[i] != NULL; i++) { - if (strcmp(comm, cron_names[i]) == 0) { - return 1; - } - } - return 0; -} - -static int detect_invoked_from_cron_via_process_tree(void) { - pid_t current_pid = getpid(); - - for (int depth = 0; depth < 12; depth++) { - pid_t parent_pid = 0; - if (!read_process_status_ppid(current_pid, &parent_pid)) { - break; - } - if (parent_pid <= 1) { - break; - } - - char comm[128]; - if (read_process_comm(parent_pid, comm, sizeof(comm))) { - if (comm_matches_any_cron_name(comm)) { - return 1; - } - } - - current_pid = parent_pid; - } - - return 0; -} - -static int get_home_directory_for_current_user(char *out_home, size_t out_home_size) { - const char *home_env = getenv("HOME"); - if (!is_string_null_or_empty(home_env)) { - snprintf(out_home, out_home_size, "%s", home_env); - return 1; - } - - struct passwd *pw = getpwuid(getuid()); - if (!pw || !pw->pw_dir) { - return 0; - } - - snprintf(out_home, out_home_size, "%s", pw->pw_dir); - return 1; -} - -static int build_xauthority_path_from_home_dir(char *xauthority_path, - size_t xauthority_path_size, - const char *home_dir) { - static const char xauthority_suffix[] = "/.Xauthority"; - size_t home_dir_length = strnlen(home_dir, xauthority_path_size); - size_t suffix_length = sizeof(xauthority_suffix) - 1; - - if (home_dir_length == 0 || home_dir_length >= xauthority_path_size) { - return 0; - } - - if (home_dir_length + suffix_length + 1 > xauthority_path_size) { - return 0; - } - - memcpy(xauthority_path, home_dir, home_dir_length); - memcpy(xauthority_path + home_dir_length, xauthority_suffix, suffix_length + 1); - return 1; -} - -static void ensure_xauthority_is_set_if_possible(void) { - if (!is_string_null_or_empty(getenv("XAUTHORITY"))) { - return; - } - - char home_dir[PATH_MAX]; - if (!get_home_directory_for_current_user(home_dir, sizeof(home_dir))) { - return; - } - - char xauthority_path[PATH_MAX]; - if (!build_xauthority_path_from_home_dir(xauthority_path, sizeof(xauthority_path), home_dir)) { - return; - } - - if (file_is_readable_regular_file(xauthority_path)) { - setenv("XAUTHORITY", xauthority_path, 0); - } -} - - -static int build_default_xdg_runtime_dir_for_current_user(char *out_runtime_dir, size_t out_runtime_dir_size) { - uid_t uid = getuid(); - snprintf(out_runtime_dir, out_runtime_dir_size, "/run/user/%u", (unsigned)uid); - if (!directory_exists_and_accessible(out_runtime_dir)) { - return 0; - } - return 1; -} - -static int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, - char *out_socket_path, - size_t out_socket_path_size, - char *out_socket_name, - size_t out_socket_name_size) { - DIR *dir = opendir(runtime_dir); - if (!dir) { - return 0; - } - - int found = 0; - int best_numeric_suffix = INT_MAX; - char best_name[NAME_MAX + 1]; - best_name[0] = '\0'; - - struct dirent *entry; - while ((entry = readdir(dir)) != NULL) { - if (strncmp(entry->d_name, "wayland-", 8) != 0) { - continue; - } - - char full_path[PATH_MAX]; - snprintf(full_path, sizeof(full_path), "%s/%s", runtime_dir, entry->d_name); - - if (!file_is_socket(full_path)) { - continue; - } - - int numeric_suffix = -1; - const char *suffix = entry->d_name + 8; - if (*suffix >= '0' && *suffix <= '9') { - numeric_suffix = (int)strtol(suffix, NULL, 10); - } - - if (!found) { - found = 1; - best_numeric_suffix = (numeric_suffix >= 0) ? numeric_suffix : INT_MAX; - snprintf(best_name, sizeof(best_name), "%s", entry->d_name); - continue; - } - - if (numeric_suffix >= 0 && numeric_suffix < best_numeric_suffix) { - best_numeric_suffix = numeric_suffix; - snprintf(best_name, sizeof(best_name), "%s", entry->d_name); - continue; - } - - if (best_numeric_suffix == INT_MAX && numeric_suffix == -1) { - snprintf(best_name, sizeof(best_name), "%s", entry->d_name); - } - } - - closedir(dir); - - if (!found) { - return 0; - } - - snprintf(out_socket_path, out_socket_path_size, "%s/%s", runtime_dir, best_name); - if (out_socket_name && out_socket_name_size > 0) { - snprintf(out_socket_name, out_socket_name_size, "%s", best_name); - } - return 1; -} - -static int find_best_x11_display_from_socket_dir(char *out_display, size_t out_display_size) { - const char *x11_socket_dir = "/tmp/.X11-unix"; - DIR *dir = opendir(x11_socket_dir); - if (!dir) { - return 0; - } - - int found = 0; - int best_display_number = INT_MAX; - - struct dirent *entry; - while ((entry = readdir(dir)) != NULL) { - if (entry->d_name[0] != 'X') { - continue; - } - const char *digits = entry->d_name + 1; - if (*digits < '0' || *digits > '9') { - continue; - } - - int display_number = (int)strtol(digits, NULL, 10); - - char socket_path[PATH_MAX]; - snprintf(socket_path, sizeof(socket_path), "%s/%s", x11_socket_dir, entry->d_name); - - if (!file_is_socket(socket_path)) { - continue; - } - - if (!found || display_number < best_display_number) { - found = 1; - best_display_number = display_number; - } - } - - closedir(dir); - - if (!found) { - return 0; - } - - snprintf(out_display, out_display_size, ":%d", best_display_number); - return 1; -} - -static void best_effort_infer_graphical_session_environment_if_missing(void) { - if (!is_string_null_or_empty(getenv("WAYLAND_DISPLAY")) || !is_string_null_or_empty(getenv("DISPLAY"))) { - return; - } - - char inferred_runtime_dir[PATH_MAX]; - if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { - if (build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { - setenv("XDG_RUNTIME_DIR", inferred_runtime_dir, 0); - } - } - - const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); - if (!is_string_null_or_empty(runtime_dir) && is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { - char wayland_socket_path[PATH_MAX]; - char wayland_socket_name[NAME_MAX + 1]; - if (find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), - wayland_socket_name, sizeof(wayland_socket_name))) { - setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); - } - } - - if (is_string_null_or_empty(getenv("DISPLAY"))) { - char inferred_display[32]; - if (find_best_x11_display_from_socket_dir(inferred_display, sizeof(inferred_display))) { - setenv("DISPLAY", inferred_display, 0); - ensure_xauthority_is_set_if_possible(); - } - } - - if (verbose && invoked_from_cron) { - const char *wd = getenv("WAYLAND_DISPLAY"); - const char *xdg = getenv("XDG_RUNTIME_DIR"); - const char *dpy = getenv("DISPLAY"); - fprintf(stderr, "Detected cron context; inferred session vars: XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s DISPLAY=%s\n", - xdg ? xdg : "(unset)", wd ? wd : "(unset)", dpy ? dpy : "(unset)"); - } -} - static int open_pid_file_descriptor_for_process(pid_t process_id) { #if defined(SYS_pidfd_open) return (int)syscall(SYS_pidfd_open, process_id, 0); @@ -606,42 +274,6 @@ static const struct wl_registry_listener wayland_registry_listener = { .global_remove = wayland_registry_global_remove }; -static struct wl_display *connect_to_wayland_best_effort(void) { - struct wl_display *display = wl_display_connect(NULL); - if (display) { - return display; - } - - const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); - char inferred_runtime_dir[PATH_MAX]; - if (is_string_null_or_empty(runtime_dir)) { - if (!build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { - return NULL; - } - runtime_dir = inferred_runtime_dir; - } - - char wayland_socket_path[PATH_MAX]; - char wayland_socket_name[NAME_MAX + 1]; - if (!find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), - wayland_socket_name, sizeof(wayland_socket_name))) { - return NULL; - } - - display = wl_display_connect(wayland_socket_path); - if (!display) { - return NULL; - } - - if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { - setenv("XDG_RUNTIME_DIR", runtime_dir, 0); - } - if (is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { - setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); - } - - return display; -} static int try_initialize_wayland_idle_backend(void) { wayland_display = connect_to_wayland_best_effort(); @@ -809,30 +441,9 @@ static int run_wayland_idle_event_loop(void) { } } -static Display *open_x11_display_best_effort(void) { - Display *display = XOpenDisplay(NULL); - if (display) { - return display; - } - - if (!is_string_null_or_empty(getenv("DISPLAY"))) { - return NULL; - } - - char inferred_display[32]; - if (!find_best_x11_display_from_socket_dir(inferred_display, sizeof(inferred_display))) { - return NULL; - } - - setenv("DISPLAY", inferred_display, 0); - ensure_xauthority_is_set_if_possible(); - return XOpenDisplay(NULL); -} - int main(int argc, char *argv[]) { parse_command_line_arguments(argc, argv); - invoked_from_cron = detect_invoked_from_cron_via_process_tree(); if (external_pid == 0) { pid = run_shell_command(shell_command_to_run); @@ -849,7 +460,7 @@ int main(int argc, char *argv[]) { signal(SIGTERM, sigterm_handler); signal(SIGCHLD, sigchld_handler); - best_effort_infer_graphical_session_environment_if_missing(); + best_effort_infer_graphical_session_environment_if_missing(verbose); if (try_initialize_wayland_idle_backend()) { int wayland_loop_result = run_wayland_idle_event_loop(); diff --git a/string_utils.c b/string_utils.c new file mode 100644 index 0000000..28c6ad6 --- /dev/null +++ b/string_utils.c @@ -0,0 +1,7 @@ +#include "string_utils.h" + +#include + +int is_string_null_or_empty(const char *value) { + return value == NULL || value[0] == '\0'; +} diff --git a/string_utils.h b/string_utils.h new file mode 100644 index 0000000..66b4f88 --- /dev/null +++ b/string_utils.h @@ -0,0 +1,4 @@ +#ifndef STRING_UTILS_H +#define STRING_UTILS_H +int is_string_null_or_empty(const char *value); +#endif //STRING_UTILS_H From ff251c42ac07e95ac5dc5a6bbb82382c430c7e67 Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Sun, 15 Mar 2026 14:12:24 -0500 Subject: [PATCH 5/7] More cleanup --- Makefile | 2 +- environment_guessing.c | 40 +------ environment_guessing.h | 10 +- main.c | 263 +++++++++++------------------------------ process_handling.c | 13 ++ process_handling.h | 2 + wayland.c | 185 +++++++++++++++++++++++++++++ wayland.h | 7 ++ 8 files changed, 285 insertions(+), 237 deletions(-) create mode 100644 wayland.c create mode 100644 wayland.h diff --git a/Makefile b/Makefile index 7f4a7a2..6f03799 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ CC=gcc ifeq ($(PREFIX),) PREFIX := /usr endif -SOURCES = time_utils.c sleep_utils.c tty_utils.c file_utils.c string_utils.c process_handling.c arguments_parsing.c ext-idle-notify-v1-protocol.c environment_guessing.c main.c +SOURCES = time_utils.c sleep_utils.c tty_utils.c file_utils.c string_utils.c process_handling.c arguments_parsing.c ext-idle-notify-v1-protocol.c environment_guessing.c wayland.c main.c OBJECTS = $(SOURCES:.c=.o) CCFLAGS = -Werror=all all: executable diff --git a/environment_guessing.c b/environment_guessing.c index 1d10a65..c5a7ca5 100644 --- a/environment_guessing.c +++ b/environment_guessing.c @@ -10,7 +10,7 @@ #include "file_utils.h" #include "string_utils.h" -static int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, +int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, char *out_socket_path, size_t out_socket_path_size, char *out_socket_name, @@ -75,7 +75,7 @@ static int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, return 1; } -static int build_default_xdg_runtime_dir_for_current_user(char *out_runtime_dir, size_t out_runtime_dir_size) { +int build_default_xdg_runtime_dir_for_current_user(char *out_runtime_dir, size_t out_runtime_dir_size) { uid_t uid = getuid(); snprintf(out_runtime_dir, out_runtime_dir_size, "/run/user/%u", (unsigned)uid); if (!directory_exists_and_accessible(out_runtime_dir)) { @@ -205,42 +205,6 @@ void best_effort_infer_graphical_session_environment_if_missing(bool log_when_in } } -struct wl_display *connect_to_wayland_best_effort(void) { - struct wl_display *display = wl_display_connect(NULL); - if (display) { - return display; - } - - const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); - char inferred_runtime_dir[PATH_MAX]; - if (is_string_null_or_empty(runtime_dir)) { - if (!build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { - return NULL; - } - runtime_dir = inferred_runtime_dir; - } - - char wayland_socket_path[PATH_MAX]; - char wayland_socket_name[NAME_MAX + 1]; - if (!find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), - wayland_socket_name, sizeof(wayland_socket_name))) { - return NULL; - } - - display = wl_display_connect(wayland_socket_path); - if (!display) { - return NULL; - } - - if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { - setenv("XDG_RUNTIME_DIR", runtime_dir, 0); - } - if (is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { - setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); - } - - return display; - } Display *open_x11_display_best_effort(void) { Display *display = XOpenDisplay(NULL); diff --git a/environment_guessing.h b/environment_guessing.h index efe8013..575b01c 100644 --- a/environment_guessing.h +++ b/environment_guessing.h @@ -1,9 +1,13 @@ #ifndef RUNWHENIDLE_ENVIRONMENT_GUESSING_H #define RUNWHENIDLE_ENVIRONMENT_GUESSING_H +#include void best_effort_infer_graphical_session_environment_if_missing(bool log_when_inferred); Display *open_x11_display_best_effort(void); -struct wl_display *connect_to_wayland_best_effort(void); - - +int build_default_xdg_runtime_dir_for_current_user(char *out_runtime_dir, size_t out_runtime_dir_size); +int find_best_wayland_socket_in_runtime_dir(const char *runtime_dir, + char *out_socket_path, + size_t out_socket_path_size, + char *out_socket_name, + size_t out_socket_name_size); #endif //RUNWHENIDLE_ENVIRONMENT_GUESSING_H diff --git a/main.c b/main.c index 7d865d6..30ecbb0 100644 --- a/main.c +++ b/main.c @@ -14,7 +14,6 @@ #include #include -#include "ext-idle-notify-v1-client-protocol.h" #include @@ -24,7 +23,9 @@ #include "tty_utils.h" #include "process_handling.h" #include "arguments_parsing.h" +#include "ext-idle-notify-v1-client-protocol.h" #include "pause_methods.h" +#include "wayland.h" #ifndef VERSION #define VERSION 'unkown' @@ -41,6 +42,7 @@ long start_monitor_after_ms = 300; long unsigned user_idle_timeout_ms = 300000; const long long POLLING_INTERVAL_MS = 1000; const long long POLLING_INTERVAL_BEFORE_STARTING_MONITORING_MS = 100; +constexpr long long POLLING_INTERVAL_WHEN_NOT_MONITORING_MS = 100; const char *pause_method_string[] = { //order must match order in pause_method enum [PAUSE_METHOD_SIGTSTP] = "SIGTSTP", @@ -57,78 +59,6 @@ volatile sig_atomic_t command_paused = 0; volatile sig_atomic_t sigchld_received = 0; pid_t pid; -static struct wl_display *wayland_display = NULL; -static struct wl_registry *wayland_registry = NULL; -static struct wl_seat *wayland_seat = NULL; -static struct ext_idle_notifier_v1 *wayland_idle_notifier = NULL; -static uint32_t wayland_idle_notifier_version = 0; -static struct ext_idle_notification_v1 *wayland_idle_notification = NULL; -static int wayland_idle_notify_available = 0; - -static int open_pid_file_descriptor_for_process(pid_t process_id) { -#if defined(SYS_pidfd_open) - return (int)syscall(SYS_pidfd_open, process_id, 0); -#elif defined(__NR_pidfd_open) - return (int)syscall(__NR_pidfd_open, process_id, 0); -#else - (void)process_id; - errno = ENOSYS; - return -1; -#endif -} - -static int create_one_shot_timer_file_descriptor_after_ms(long delay_ms) { - int timer_file_descriptor = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (timer_file_descriptor < 0) { - return -1; - } - - if (delay_ms < 0) { - delay_ms = 0; - } - - struct itimerspec timer_spec; - memset(&timer_spec, 0, sizeof(timer_spec)); - timer_spec.it_value.tv_sec = delay_ms / 1000; - timer_spec.it_value.tv_nsec = (delay_ms % 1000) * 1000000L; - - if (timerfd_settime(timer_file_descriptor, 0, &timer_spec, NULL) < 0) { - close(timer_file_descriptor); - return -1; - } - - return timer_file_descriptor; -} - -static int create_periodic_timer_file_descriptor_every_ms(long interval_ms) { - int timer_file_descriptor = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (timer_file_descriptor < 0) { - return -1; - } - - if (interval_ms <= 0) { - interval_ms = 1000; - } - - struct itimerspec timer_spec; - memset(&timer_spec, 0, sizeof(timer_spec)); - timer_spec.it_value.tv_sec = interval_ms / 1000; - timer_spec.it_value.tv_nsec = (interval_ms % 1000) * 1000000L; - timer_spec.it_interval = timer_spec.it_value; - - if (timerfd_settime(timer_file_descriptor, 0, &timer_spec, NULL) < 0) { - close(timer_file_descriptor); - return -1; - } - - return timer_file_descriptor; -} - -static void consume_timer_file_descriptor(int timer_file_descriptor) { - uint64_t expirations = 0; - (void)read(timer_file_descriptor, &expirations, sizeof(expirations)); -} - long unsigned query_user_idle_time() { if (xscreensaver_is_available) { XScreenSaverQueryInfo(x_display, DefaultRootWindow(x_display), xscreensaver_info); @@ -236,105 +166,80 @@ static void wayland_idle_notification_resumed(void *data, struct ext_idle_notifi } } -static const struct ext_idle_notification_v1_listener wayland_idle_notification_listener = { - .idled = wayland_idle_notification_idled, - .resumed = wayland_idle_notification_resumed -}; - -static void wayland_registry_global(void *data, - struct wl_registry *registry, - uint32_t name, - const char *interface, - uint32_t version) { - (void)data; - - if (strcmp(interface, "wl_seat") == 0 && wayland_seat == NULL) { - wayland_seat = wl_registry_bind(registry, name, &wl_seat_interface, 1); - return; +static int create_one_shot_timer_file_descriptor_after_ms(long delay_ms) { + int timer_file_descriptor = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timer_file_descriptor < 0) { + return -1; } - if (strcmp(interface, "ext_idle_notifier_v1") == 0 && wayland_idle_notifier == NULL) { - uint32_t bind_version = version < 2 ? version : 2; - wayland_idle_notifier_version = bind_version; - wayland_idle_notifier = wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version); - return; + struct itimerspec timer_spec = {0}; + timer_spec.it_value.tv_sec = delay_ms / 1000; + timer_spec.it_value.tv_nsec = (delay_ms % 1000) * 1000000L; + + if (timerfd_settime(timer_file_descriptor, 0, &timer_spec, NULL) < 0) { + close(timer_file_descriptor); + return -1; } -} -static void wayland_registry_global_remove(void *data, - struct wl_registry *registry, - uint32_t name) { - (void)data; - (void)registry; - (void)name; + return timer_file_descriptor; } - -static const struct wl_registry_listener wayland_registry_listener = { - .global = wayland_registry_global, - .global_remove = wayland_registry_global_remove -}; - - -static int try_initialize_wayland_idle_backend(void) { - wayland_display = connect_to_wayland_best_effort(); - if (!wayland_display) { - return 0; +static int create_periodic_timer_file_descriptor_every_ms(long interval_ms) { + int timer_file_descriptor = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timer_file_descriptor < 0) { + return -1; } - wayland_registry = wl_display_get_registry(wayland_display); - if (!wayland_registry) { - wl_display_disconnect(wayland_display); - wayland_display = NULL; - return 0; - } + struct itimerspec timer_spec = {0}; + timer_spec.it_value.tv_sec = interval_ms / 1000; + timer_spec.it_value.tv_nsec = (interval_ms % 1000) * 1000000L; + timer_spec.it_interval = timer_spec.it_value; - wl_registry_add_listener(wayland_registry, &wayland_registry_listener, NULL); - wl_display_roundtrip(wayland_display); - - if (wayland_seat == NULL || wayland_idle_notifier == NULL) { - wl_registry_destroy(wayland_registry); - wayland_registry = NULL; - wl_display_disconnect(wayland_display); - wayland_display = NULL; - wayland_seat = NULL; - wayland_idle_notifier = NULL; - wayland_idle_notify_available = 0; - return 0; + if (timerfd_settime(timer_file_descriptor, 0, &timer_spec, NULL) < 0) { + close(timer_file_descriptor); + return -1; } - wayland_idle_notify_available = 1; - return 1; + return timer_file_descriptor; } -static int start_wayland_idle_notification_object(void) { - if (!wayland_idle_notify_available || wayland_idle_notification != NULL) { - return 0; - } - - uint32_t timeout_ms_for_protocol = (user_idle_timeout_ms > UINT32_MAX) ? UINT32_MAX : (uint32_t)user_idle_timeout_ms; +static void consume_timer_file_descriptor(int timer_file_descriptor) { + uint64_t expirations = 0; + (void)read(timer_file_descriptor, &expirations, sizeof(expirations)); +} - if (wayland_idle_notifier_version >= 2) { - wayland_idle_notification = ext_idle_notifier_v1_get_input_idle_notification( - wayland_idle_notifier, timeout_ms_for_protocol, wayland_seat); - } else { - wayland_idle_notification = ext_idle_notifier_v1_get_idle_notification( - wayland_idle_notifier, timeout_ms_for_protocol, wayland_seat); +int wait_for_pid_to_exit_checking_for_signals(void) { + while (1) { + if (interruption_received) { + return handle_interruption(); + } + if (sigchld_received) { + sigchld_received = 0; + exit_if_pid_has_finished(pid); + } + exit_if_pid_has_finished(pid); + sleep_for_milliseconds(POLLING_INTERVAL_WHEN_NOT_MONITORING_MS); } - - if (!wayland_idle_notification) { +} +const struct ext_idle_notification_v1_listener wayland_idle_notification_listener = { + .idled = wayland_idle_notification_idled, + .resumed = wayland_idle_notification_resumed +}; +int run_wayland_idle_event_loop(struct wl_display *wayland_display) { + const int start_monitor_timer_file_descriptor = create_one_shot_timer_file_descriptor_after_ms(start_monitor_after_ms); + if (start_monitor_timer_file_descriptor == -1) { + fprintf_error("Wayland idle event loop aborted: failed to create a timer file descriptor\n"); + return -1; + } + const int wayland_display_file_descriptor = wl_display_get_fd(wayland_display); + if (wayland_display_file_descriptor == -1) { + fprintf_error("Wayland idle event loop aborted: failed to get wayland display file descriptor\n"); return -1; } - ext_idle_notification_v1_add_listener(wayland_idle_notification, &wayland_idle_notification_listener, NULL); - wl_display_flush(wayland_display); - return 1; -} - -static int run_wayland_idle_event_loop(void) { - int start_monitor_timer_file_descriptor = create_one_shot_timer_file_descriptor_after_ms(start_monitor_after_ms); - int wayland_file_descriptor = wl_display_get_fd(wayland_display); - - int process_exit_wait_file_descriptor = open_pid_file_descriptor_for_process(pid); + const int process_exit_wait_file_descriptor = open_pid_file_descriptor_for_process(pid); + if (process_exit_wait_file_descriptor == -1) { + fprintf_error("Failed to open file descriptor for pid %d: %s \n", pid, strerror(errno)); + } int external_pid_fallback_check_timer_file_descriptor = -1; if (process_exit_wait_file_descriptor < 0 && external_pid != 0) { external_pid_fallback_check_timer_file_descriptor = create_periodic_timer_file_descriptor_every_ms(1000); @@ -344,7 +249,7 @@ static int run_wayland_idle_event_loop(void) { int poll_file_descriptor_count = 0; int wayland_poll_index = poll_file_descriptor_count++; - poll_file_descriptors[wayland_poll_index] = (struct pollfd){ .fd = wayland_file_descriptor, .events = POLLIN, .revents = 0 }; + poll_file_descriptors[wayland_poll_index] = (struct pollfd){ .fd = wayland_display_file_descriptor, .events = POLLIN, .revents = 0 }; int start_monitor_poll_index = poll_file_descriptor_count++; poll_file_descriptors[start_monitor_poll_index] = (struct pollfd){ .fd = start_monitor_timer_file_descriptor, .events = POLLIN, .revents = 0 }; @@ -392,7 +297,7 @@ static int run_wayland_idle_event_loop(void) { consume_timer_file_descriptor(start_monitor_timer_file_descriptor); monitoring_started = 1; - if (start_wayland_idle_notification_object() < 0) { + if (start_wayland_idle_notification_object(&wayland_idle_notification_listener) < 0) { fprintf_error("Failed to create Wayland idle notification object, user will be considered idle.\n"); } else { if (!command_paused) { @@ -427,24 +332,14 @@ static int run_wayland_idle_event_loop(void) { if (start_monitor_timer_file_descriptor >= 0) close(start_monitor_timer_file_descriptor); if (process_exit_wait_file_descriptor >= 0) close(process_exit_wait_file_descriptor); if (external_pid_fallback_check_timer_file_descriptor >= 0) close(external_pid_fallback_check_timer_file_descriptor); - - while (1) { - if (interruption_received) { - return handle_interruption(); - } - if (sigchld_received) { - sigchld_received = 0; - exit_if_pid_has_finished(pid); - } - exit_if_pid_has_finished(pid); - sleep_for_milliseconds(250); - } + return wait_for_pid_to_exit_checking_for_signals(); } + + int main(int argc, char *argv[]) { parse_command_line_arguments(argc, argv); - if (external_pid == 0) { pid = run_shell_command(shell_command_to_run); } else { @@ -462,31 +357,9 @@ int main(int argc, char *argv[]) { best_effort_infer_graphical_session_environment_if_missing(verbose); - if (try_initialize_wayland_idle_backend()) { - int wayland_loop_result = run_wayland_idle_event_loop(); - - if (wayland_idle_notification) { - ext_idle_notification_v1_destroy(wayland_idle_notification); - wayland_idle_notification = NULL; - } - if (wayland_idle_notifier) { - ext_idle_notifier_v1_destroy(wayland_idle_notifier); - wayland_idle_notifier = NULL; - } - if (wayland_seat) { - wl_seat_destroy(wayland_seat); - wayland_seat = NULL; - } - if (wayland_registry) { - wl_registry_destroy(wayland_registry); - wayland_registry = NULL; - } - if (wayland_display) { - wl_display_disconnect(wayland_display); - wayland_display = NULL; - } - - return wayland_loop_result; + const int wayland_loop_result = try_monitor_wayland_idle_notify(run_wayland_idle_event_loop); + if (wayland_loop_result == 0) { + return 0; } x_display = open_x11_display_best_effort(); diff --git a/process_handling.c b/process_handling.c index 47f05f2..40c58a6 100644 --- a/process_handling.c +++ b/process_handling.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -291,3 +292,15 @@ void exit_if_pid_has_finished(pid_t pid) { exit(exit_code); } } + +int open_pid_file_descriptor_for_process(pid_t process_id) { +#if defined(SYS_pidfd_open) + return (int)syscall(SYS_pidfd_open, process_id, 0); +#elif defined(__NR_pidfd_open) + return (int)syscall(__NR_pidfd_open, process_id, 0); +#else + (void)process_id; + errno = ENOSYS; + return -1; +#endif +} \ No newline at end of file diff --git a/process_handling.h b/process_handling.h index aed9bc0..504baa8 100644 --- a/process_handling.h +++ b/process_handling.h @@ -65,4 +65,6 @@ int wait_for_pid_to_exit_synchronously(int pid); */ void exit_if_pid_has_finished(pid_t pid); +int open_pid_file_descriptor_for_process(pid_t process_id); + #endif //RUNWHENIDLE_PROCESS_HANDLING_H diff --git a/wayland.c b/wayland.c new file mode 100644 index 0000000..6e17e8e --- /dev/null +++ b/wayland.c @@ -0,0 +1,185 @@ +#include "ext-idle-notify-v1-client-protocol.h" + +#include "wayland.h" + +#include +#include +#include +#include +#include + +#include "arguments_parsing.h" +#include "environment_guessing.h" +#include "process_handling.h" +#include "sleep_utils.h" +#include "string_utils.h" +#include "tty_utils.h" + +static struct wl_display *wayland_display = NULL; +static struct wl_registry *wayland_registry = NULL; + +static struct wl_seat *wayland_seat = NULL; + +static struct ext_idle_notifier_v1 *wayland_idle_notifier = NULL; + +static uint32_t wayland_idle_notifier_version = 0; + +static struct ext_idle_notification_v1 *wayland_idle_notification = NULL; + +static int wayland_idle_notify_available = 0; + + +static struct wl_display *connect_to_wayland_best_effort(void) { + struct wl_display *display = wl_display_connect(NULL); + if (display) { + return display; + } + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + char inferred_runtime_dir[PATH_MAX]; + if (is_string_null_or_empty(runtime_dir)) { + if (!build_default_xdg_runtime_dir_for_current_user(inferred_runtime_dir, sizeof(inferred_runtime_dir))) { + return NULL; + } + runtime_dir = inferred_runtime_dir; + } + + char wayland_socket_path[PATH_MAX]; + char wayland_socket_name[NAME_MAX + 1]; + if (!find_best_wayland_socket_in_runtime_dir(runtime_dir, wayland_socket_path, sizeof(wayland_socket_path), + wayland_socket_name, sizeof(wayland_socket_name))) { + return NULL; + } + + display = wl_display_connect(wayland_socket_path); + if (!display) { + return NULL; + } + + if (is_string_null_or_empty(getenv("XDG_RUNTIME_DIR"))) { + setenv("XDG_RUNTIME_DIR", runtime_dir, 0); + } + if (is_string_null_or_empty(getenv("WAYLAND_DISPLAY"))) { + setenv("WAYLAND_DISPLAY", wayland_socket_name, 0); + } + + return display; +} + +static int try_initialize_wayland_idle_backend(const struct wl_registry_listener *wayland_registry_listener) { + wayland_display = connect_to_wayland_best_effort(); + if (!wayland_display) { + return 0; + } + + wayland_registry = wl_display_get_registry(wayland_display); + if (!wayland_registry) { + wl_display_disconnect(wayland_display); + wayland_display = NULL; + return 0; + } + + wl_registry_add_listener(wayland_registry, wayland_registry_listener, NULL); + wl_display_roundtrip(wayland_display); + + if (wayland_seat == NULL || wayland_idle_notifier == NULL) { + wl_registry_destroy(wayland_registry); + wayland_registry = NULL; + wl_display_disconnect(wayland_display); + wayland_display = NULL; + wayland_seat = NULL; + wayland_idle_notifier = NULL; + wayland_idle_notify_available = 0; + return 0; + } + + wayland_idle_notify_available = 1; + return 1; +} + +int start_wayland_idle_notification_object(const struct ext_idle_notification_v1_listener *wayland_idle_notification_listener) { + if (!wayland_idle_notify_available || wayland_idle_notification != NULL) { + return 0; + } + + uint32_t timeout_ms_for_protocol = (user_idle_timeout_ms > UINT32_MAX) ? UINT32_MAX : (uint32_t)user_idle_timeout_ms; + + if (wayland_idle_notifier_version >= 2) { + wayland_idle_notification = ext_idle_notifier_v1_get_input_idle_notification( + wayland_idle_notifier, timeout_ms_for_protocol, wayland_seat); + } else { + wayland_idle_notification = ext_idle_notifier_v1_get_idle_notification( + wayland_idle_notifier, timeout_ms_for_protocol, wayland_seat); + } + + if (!wayland_idle_notification) { + return -1; + } + + ext_idle_notification_v1_add_listener(wayland_idle_notification, wayland_idle_notification_listener, NULL); + wl_display_flush(wayland_display); + return 1; +} +static void wayland_registry_global(void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) { + (void)data; + + if (strcmp(interface, "wl_seat") == 0 && wayland_seat == NULL) { + wayland_seat = wl_registry_bind(registry, name, &wl_seat_interface, 1); + return; + } + + if (strcmp(interface, "ext_idle_notifier_v1") == 0 && wayland_idle_notifier == NULL) { + uint32_t bind_version = version < 2 ? version : 2; + wayland_idle_notifier_version = bind_version; + wayland_idle_notifier = wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version); + } +} + +static void wayland_registry_global_remove(void *data, + struct wl_registry *registry, + uint32_t name) { + (void)data; + (void)registry; + (void)name; +} + +static const struct wl_registry_listener wayland_registry_listener = { + .global = wayland_registry_global, + .global_remove = wayland_registry_global_remove +}; + +int try_monitor_wayland_idle_notify(WaylandLoopFunction wayland_loop_function) { + if (!try_initialize_wayland_idle_backend(&wayland_registry_listener)) { + return -1; + } + int wayland_loop_result = wayland_loop_function(wayland_display); + + if (wayland_idle_notification) { + ext_idle_notification_v1_destroy(wayland_idle_notification); + wayland_idle_notification = NULL; + } + if (wayland_idle_notifier) { + ext_idle_notifier_v1_destroy(wayland_idle_notifier); + wayland_idle_notifier = NULL; + } + if (wayland_seat) { + wl_seat_destroy(wayland_seat); + wayland_seat = NULL; + } + if (wayland_registry) { + wl_registry_destroy(wayland_registry); + wayland_registry = NULL; + } + if (wayland_display) { + wl_display_disconnect(wayland_display); + wayland_display = NULL; + } + + return wayland_loop_result; +} + + diff --git a/wayland.h b/wayland.h new file mode 100644 index 0000000..0414c7e --- /dev/null +++ b/wayland.h @@ -0,0 +1,7 @@ +#ifndef RUNWHENIDLE_WAYLAND_H +#define RUNWHENIDLE_WAYLAND_H +typedef int (*WaylandLoopFunction)(struct wl_display*); +int try_monitor_wayland_idle_notify(WaylandLoopFunction wayland_loop_function); +int start_wayland_idle_notification_object(const struct ext_idle_notification_v1_listener *wayland_idle_notification_listener); + +#endif //RUNWHENIDLE_WAYLAND_H From 06a505d82ada03d6ff52a760eb5caaf026b43ab7 Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Sun, 15 Mar 2026 14:15:34 -0500 Subject: [PATCH 6/7] switch to constexpr --- main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.c b/main.c index 30ecbb0..c49f411 100644 --- a/main.c +++ b/main.c @@ -40,8 +40,8 @@ int monitoring_started = 0; enum pause_method pause_method = PAUSE_METHOD_SIGSTOP; long start_monitor_after_ms = 300; long unsigned user_idle_timeout_ms = 300000; -const long long POLLING_INTERVAL_MS = 1000; -const long long POLLING_INTERVAL_BEFORE_STARTING_MONITORING_MS = 100; +constexpr long long POLLING_INTERVAL_MS = 1000; +constexpr long long POLLING_INTERVAL_BEFORE_STARTING_MONITORING_MS = 100; constexpr long long POLLING_INTERVAL_WHEN_NOT_MONITORING_MS = 100; const char *pause_method_string[] = { //order must match order in pause_method enum From ee8053dd3857c675d52adccd7b3927c64ecf447f Mon Sep 17 00:00:00 2001 From: Konstantin Pereiaslov Date: Sun, 15 Mar 2026 16:36:20 -0500 Subject: [PATCH 7/7] Bring back deleted code --- main.c | 115 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 30 deletions(-) diff --git a/main.c b/main.c index c49f411..288a951 100644 --- a/main.c +++ b/main.c @@ -120,7 +120,20 @@ void sigchld_handler(int signum) { (void)signum; sigchld_received = 1; } +static void resume_paused_command_on_user_idle(void) { + if (!quiet) { + printf("Lack of user activity detected. "); + //intentionally no new line here, resume_command will print the rest of the message. + } + resume_command_recursively(pid); + command_paused = 0; +} +static void pause_running_command_on_user_activity(void) { + pause_command_recursively(pid); + if (debug) fprintf(stderr, "Command paused\n"); + command_paused = 1; +} static void wayland_idle_notification_idled(void *data, struct ext_idle_notification_v1 *notification) { (void)data; (void)notification; @@ -133,16 +146,7 @@ static void wayland_idle_notification_idled(void *data, struct ext_idle_notifica fprintf(stderr, "Wayland idle: idled()\n"); } - if (command_paused) { - if (verbose) { - fprintf(stderr, "Wayland idle: resuming command\n"); - } - if (!quiet) { - printf("Lack of user activity detected. "); - } - resume_command_recursively(pid); - command_paused = 0; - } + resume_paused_command_on_user_idle(); } static void wayland_idle_notification_resumed(void *data, struct ext_idle_notification_v1 *notification) { @@ -156,13 +160,8 @@ static void wayland_idle_notification_resumed(void *data, struct ext_idle_notifi if (debug) { fprintf(stderr, "Wayland idle: resumed()\n"); } - if (!command_paused) { - if (verbose) { - fprintf(stderr, "Wayland idle: pausing command\n"); - } - pause_command_recursively(pid); - command_paused = 1; + pause_running_command_on_user_activity(); } } @@ -220,10 +219,12 @@ int wait_for_pid_to_exit_checking_for_signals(void) { sleep_for_milliseconds(POLLING_INTERVAL_WHEN_NOT_MONITORING_MS); } } + const struct ext_idle_notification_v1_listener wayland_idle_notification_listener = { .idled = wayland_idle_notification_idled, .resumed = wayland_idle_notification_resumed }; + int run_wayland_idle_event_loop(struct wl_display *wayland_display) { const int start_monitor_timer_file_descriptor = create_one_shot_timer_file_descriptor_after_ms(start_monitor_after_ms); if (start_monitor_timer_file_descriptor == -1) { @@ -336,7 +337,72 @@ int run_wayland_idle_event_loop(struct wl_display *wayland_display) { } +static long long pause_or_resume_command_depending_on_user_activity( + long long sleep_time_ms, + unsigned long user_idle_time_ms) { + if (user_idle_time_ms >= user_idle_timeout_ms) { + if (debug) + fprintf(stderr, "Idle time: %lums, idle timeout: %lums, user is inactive\n", user_idle_time_ms, + user_idle_timeout_ms); + if (command_paused) { + sleep_time_ms = POLLING_INTERVAL_MS; //reset to default value + if (verbose) { + fprintf(stderr, "Idle time: %lums, idle timeout: %lums, resuming command\n", user_idle_time_ms, + user_idle_timeout_ms); + } + resume_paused_command_on_user_idle(); + if (!quiet) { + printf("Lack of user activity detected. "); + //intentionally no new line here, resume_command will print the rest of the message. + } + resume_command_recursively(pid); + command_paused = 0; + } + } else { + struct timespec time_when_starting_to_pause; + int command_was_paused_this_iteration = 0; + // User is active + if (!command_paused) { + clock_gettime(CLOCK_MONOTONIC, &time_when_starting_to_pause); + if (verbose) { + fprintf(stderr, "Idle time: %lums.\n", user_idle_time_ms); + } + pause_running_command_on_user_activity(); + command_was_paused_this_iteration = 1; + } + sleep_time_ms = user_idle_timeout_ms - user_idle_time_ms; + if (debug) fprintf(stderr, "Target sleep time: %llums\n", sleep_time_ms); + if (command_was_paused_this_iteration) { + if (debug) fprintf(stderr, "Command was paused this iteration\n"); + struct timespec time_before_sleep; + clock_gettime(CLOCK_MONOTONIC, &time_before_sleep); + long long pausing_time_ms = get_elapsed_time_ms(time_when_starting_to_pause, time_before_sleep); + if (debug) + fprintf(stderr, + "Target sleep time before taking into account time it took to pause: %lldms, time it took to pause: %lldms\n", + sleep_time_ms, pausing_time_ms); + sleep_time_ms = sleep_time_ms - pausing_time_ms; + + } + if (sleep_time_ms < POLLING_INTERVAL_MS) { + if (debug) + fprintf(stderr, + "Target sleep time %lldms is less than polling interval %lldms, resetting it to polling interval\n", + sleep_time_ms, POLLING_INTERVAL_MS); + sleep_time_ms = POLLING_INTERVAL_MS; + } + if (verbose) { + fprintf( + stderr, + "Polling every second is temporarily disabled due to user activity, idle time: %lums, next activity check scheduled in %lldms\n", + user_idle_time_ms, + sleep_time_ms + ); + } + } + return sleep_time_ms; +} int main(int argc, char *argv[]) { parse_command_line_arguments(argc, argv); @@ -415,20 +481,9 @@ int main(int argc, char *argv[]) { exit_if_pid_has_finished(pid); if (monitoring_started) { - if (user_idle_time_ms >= user_idle_timeout_ms) { - if (command_paused) { - if (!quiet) { - printf("Lack of user activity detected. "); - } - resume_command_recursively(pid); - command_paused = 0; - } - } else { - if (!command_paused) { - pause_command_recursively(pid); - command_paused = 1; - } - } + sleep_time_ms = pause_or_resume_command_depending_on_user_activity( + sleep_time_ms, + user_idle_time_ms); } if (debug) fprintf(stderr, "Sleeping for %lldms\n", sleep_time_ms); sleep_for_milliseconds(sleep_time_ms);