diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index 6e5fe00e06b..f1b0bfaae2d 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -56,6 +56,7 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_OPERATION_IN_PROGRESS, EINPROGRESS), SD_BUS_ERROR_MAP(BUS_ERROR_SLEEP_VERB_NOT_SUPPORTED, EOPNOTSUPP), SD_BUS_ERROR_MAP(BUS_ERROR_SESSION_BUSY, EBUSY), + SD_BUS_ERROR_MAP(BUS_ERROR_NOT_YOUR_DEVICE, EPERM), SD_BUS_ERROR_MAP(BUS_ERROR_AUTOMATIC_TIME_SYNC_ENABLED, EALREADY), SD_BUS_ERROR_MAP(BUS_ERROR_NO_NTP_SUPPORT, EOPNOTSUPP), diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index 8339feb7686..6120fed2cd3 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -51,6 +51,7 @@ #define BUS_ERROR_OPERATION_IN_PROGRESS "org.freedesktop.login1.OperationInProgress" #define BUS_ERROR_SLEEP_VERB_NOT_SUPPORTED "org.freedesktop.login1.SleepVerbNotSupported" #define BUS_ERROR_SESSION_BUSY "org.freedesktop.login1.SessionBusy" +#define BUS_ERROR_NOT_YOUR_DEVICE "org.freedesktop.login1.NotYourDevice" #define BUS_ERROR_AUTOMATIC_TIME_SYNC_ENABLED "org.freedesktop.timedate1.AutomaticTimeSyncEnabled" #define BUS_ERROR_NO_NTP_SUPPORT "org.freedesktop.timedate1.NoNTPSupport" diff --git a/src/login/logind-brightness.c b/src/login/logind-brightness.c new file mode 100644 index 00000000000..8dfa97d7aed --- /dev/null +++ b/src/login/logind-brightness.c @@ -0,0 +1,256 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "bus-util.h" +#include "device-util.h" +#include "hash-funcs.h" +#include "logind-brightness.h" +#include "logind.h" +#include "process-util.h" +#include "stdio-util.h" + +/* Brightness and LED devices tend to be very slow to write to (often being I2C and such). Writes to the + * sysfs attributes are synchronous, and hence will freeze our process on access. We can't really have that, + * hence we add some complexity: whenever we need to write to the brightness attribute, we do so in a forked + * off process, which terminates when it is done. Watching that process allows us to watch completion of the + * write operation. + * + * To make this even more complex: clients are likely to send us many write requests in a short time-frame + * (because they implement reactive brightness sliders on screen). Let's coalesce writes to make this + * efficient: whenever we get requests to change brightness while we are still writing to the brightness + * attribute, let's remember the request and restart a new one when the initial operation finished. When we + * get another request while one is ongoing and one is pending we'll replace the pending one with the new + * one. + * + * The bus messages are answered when the first write operation finishes that started either due to the + * request or due to a later request that overrode the requested one. + * + * Yes, this is complex, but I don't see an easier way if we want to be both efficient and still support + * completion notification. */ + +typedef struct BrightnessWriter { + Manager *manager; + + sd_device *device; + char *path; + + pid_t child; + + uint32_t brightness; + bool again; + + Set *current_messages; + Set *pending_messages; + + sd_event_source* child_event_source; +} BrightnessWriter; + +static void brightness_writer_free(BrightnessWriter *w) { + if (!w) + return; + + if (w->manager && w->path) + (void) hashmap_remove_value(w->manager->brightness_writers, w->path, w); + + sd_device_unref(w->device); + free(w->path); + + set_free(w->current_messages); + set_free(w->pending_messages); + + w->child_event_source = sd_event_source_unref(w->child_event_source); + + free(w); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(BrightnessWriter*, brightness_writer_free); + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + brightness_writer_hash_ops, + char, + string_hash_func, + string_compare_func, + BrightnessWriter, + brightness_writer_free); + +static void brightness_writer_reply(BrightnessWriter *w, int error) { + int r; + + assert(w); + + for (;;) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + m = set_steal_first(w->current_messages); + if (!m) + break; + + if (error == 0) + r = sd_bus_reply_method_return(m, NULL); + else + r = sd_bus_reply_method_errnof(m, error, "Failed to write to brightness device: %m"); + if (r < 0) + log_warning_errno(r, "Failed to send method reply, ignoring: %m"); + } +} + +static int brightness_writer_fork(BrightnessWriter *w); + +static int on_brightness_writer_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + BrightnessWriter *w = userdata; + int r; + + assert(s); + assert(si); + assert(w); + + assert(si->si_pid == w->child); + w->child = 0; + w->child_event_source = sd_event_source_unref(w->child_event_source); + + brightness_writer_reply(w, + si->si_code == CLD_EXITED && + si->si_status == EXIT_SUCCESS ? 0 : -EPROTO); + + if (w->again) { + /* Another request to change the brightness has been queued. Act on it, but make the pending + * messages the current ones. */ + w->again = false; + set_free(w->current_messages); + w->current_messages = TAKE_PTR(w->pending_messages); + + r = brightness_writer_fork(w); + if (r >= 0) + return 0; + + brightness_writer_reply(w, r); + } + + brightness_writer_free(w); + return 0; +} + +static int brightness_writer_fork(BrightnessWriter *w) { + int r; + + assert(w); + assert(w->manager); + assert(w->child == 0); + assert(!w->child_event_source); + + r = safe_fork("(sd-bright)", FORK_DEATHSIG|FORK_NULL_STDIO|FORK_CLOSE_ALL_FDS|FORK_LOG, &w->child); + if (r < 0) + return r; + if (r == 0) { + char brs[DECIMAL_STR_MAX(uint32_t)+1]; + + /* Child */ + xsprintf(brs, "%" PRIu32, w->brightness); + + r = sd_device_set_sysattr_value(w->device, "brightness", brs); + if (r < 0) { + log_device_error_errno(w->device, r, "Failed to write brightness to device: %m"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + r = sd_event_add_child(w->manager->event, &w->child_event_source, w->child, WEXITED, on_brightness_writer_exit, w); + if (r < 0) + return log_error_errno(r, "Failed to watch brightness writer child " PID_FMT ": %m", w->child); + + return 0; +} + +static int set_add_message(Set **set, sd_bus_message *message) { + int r; + + assert(set); + + if (!message) + return 0; + + r = sd_bus_message_get_expect_reply(message); + if (r <= 0) + return r; + + r = set_ensure_allocated(set, &bus_message_hash_ops); + if (r < 0) + return r; + + r = set_put(*set, message); + if (r < 0) + return r; + + sd_bus_message_ref(message); + return 1; +} + +int manager_write_brightness( + Manager *m, + sd_device *device, + uint32_t brightness, + sd_bus_message *message) { + + _cleanup_(brightness_writer_freep) BrightnessWriter *w = NULL; + BrightnessWriter *existing; + const char *path; + int r; + + assert(m); + assert(device); + + r = sd_device_get_syspath(device, &path); + if (r < 0) + return log_device_error_errno(device, r, "Failed to get sysfs path for brightness device: %m"); + + existing = hashmap_get(m->brightness_writers, path); + if (existing) { + /* There's already a writer for this device. Let's update it with the new brightness, and add + * our message to the set of message to reply when done. */ + + r = set_add_message(&existing->pending_messages, message); + if (r < 0) + return log_error_errno(r, "Failed to add message to set: %m"); + + /* We overide any previously requested brightness here: we coalesce writes, and the newest + * requested brightness is the one we'll put into effect. */ + existing->brightness = brightness; + existing->again = true; /* request another iteration of the writer when the current one is + * complete */ + return 0; + } + + r = hashmap_ensure_allocated(&m->brightness_writers, &brightness_writer_hash_ops); + if (r < 0) + return log_oom(); + + w = new(BrightnessWriter, 1); + if (!w) + return log_oom(); + + *w = (BrightnessWriter) { + .device = sd_device_ref(device), + .path = strdup(path), + .brightness = brightness, + }; + + if (!w->path) + return log_oom(); + + r = hashmap_put(m->brightness_writers, w->path, w); + if (r < 0) + return log_error_errno(r, "Failed to add brightness writer to hashmap: %m"); + w->manager = m; + + r = set_add_message(&w->current_messages, message); + if (r < 0) + return log_error_errno(r, "Failed to add message to set: %m"); + + r = brightness_writer_fork(w); + if (r < 0) + return r; + + TAKE_PTR(w); + return 0; +} diff --git a/src/login/logind-brightness.h b/src/login/logind-brightness.h new file mode 100644 index 00000000000..b22ee37ba77 --- /dev/null +++ b/src/login/logind-brightness.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" +#include "sd-device.h" + +#include "logind.h" + +int manager_write_brightness(Manager *m, sd_device *device, uint32_t brightness, sd_bus_message *message); diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c index df5bfba9821..c6b8794096a 100644 --- a/src/login/logind-session-dbus.c +++ b/src/login/logind-session-dbus.c @@ -8,10 +8,12 @@ #include "bus-label.h" #include "bus-util.h" #include "fd-util.h" +#include "logind-brightness.h" #include "logind-session-device.h" #include "logind-session.h" #include "logind.h" #include "missing_capability.h" +#include "path-util.h" #include "signal-util.h" #include "stat-util.h" #include "strv.h" @@ -479,6 +481,57 @@ static int method_pause_device_complete(sd_bus_message *message, void *userdata, return sd_bus_reply_method_return(message, NULL); } +static int method_set_brightness(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + const char *subsystem, *name, *seat; + Session *s = userdata; + uint32_t brightness; + uid_t uid; + int r; + + assert(message); + assert(s); + + r = sd_bus_message_read(message, "ssu", &subsystem, &name, &brightness); + if (r < 0) + return r; + + if (!STR_IN_SET(subsystem, "backlight", "leds")) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Subsystem type %s not supported, must be one of 'backlight' or 'leds'.", subsystem); + if (!filename_is_valid(name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not a valid device name %s, refusing.", name); + + if (!s->seat) + return sd_bus_error_setf(error, BUS_ERROR_NOT_YOUR_DEVICE, "Your session has no seat, refusing."); + if (s->seat->active != s) + return sd_bus_error_setf(error, BUS_ERROR_NOT_YOUR_DEVICE, "Session is not in foreground, refusing."); + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &uid); + if (r < 0) + return r; + + if (uid != 0 && uid != s->user->uid) + return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change brightness."); + + r = sd_device_new_from_subsystem_sysname(&d, subsystem, name); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to open device %s:%s: %m", subsystem, name); + + if (sd_device_get_property_value(d, "ID_SEAT", &seat) >= 0 && !streq_ptr(seat, s->seat->id)) + return sd_bus_error_setf(error, BUS_ERROR_NOT_YOUR_DEVICE, "Device %s:%s does not belong to your seat %s, refusing.", subsystem, name, s->seat->id); + + r = manager_write_brightness(s->manager, d, brightness, message); + if (r < 0) + return r; + + return 1; +} + const sd_bus_vtable session_vtable[] = { SD_BUS_VTABLE_START(0), @@ -519,6 +572,7 @@ const sd_bus_vtable session_vtable[] = { SD_BUS_METHOD("TakeDevice", "uu", "hb", method_take_device, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ReleaseDevice", "uu", NULL, method_release_device, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("PauseDeviceComplete", "uu", NULL, method_pause_device_complete, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("SetBrightness", "ssu", NULL, method_set_brightness, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_SIGNAL("PauseDevice", "uus", 0), SD_BUS_SIGNAL("ResumeDevice", "uuh", 0), diff --git a/src/login/logind.c b/src/login/logind.c index f8a01d156e9..89807dc917d 100644 --- a/src/login/logind.c +++ b/src/login/logind.c @@ -117,6 +117,7 @@ static Manager* manager_unref(Manager *m) { hashmap_free(m->users); hashmap_free(m->inhibitors); hashmap_free(m->buttons); + hashmap_free(m->brightness_writers); hashmap_free(m->user_units); hashmap_free(m->session_units); @@ -1214,7 +1215,7 @@ static int run(int argc, char *argv[]) { (void) mkdir_label("/run/systemd/users", 0755); (void) mkdir_label("/run/systemd/sessions", 0755); - assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGHUP, SIGTERM, SIGINT, -1) >= 0); + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGHUP, SIGTERM, SIGINT, SIGCHLD, -1) >= 0); r = manager_new(&m); if (r < 0) diff --git a/src/login/logind.h b/src/login/logind.h index 7b6f73c6ec6..0089fff87a7 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -31,6 +31,7 @@ struct Manager { Hashmap *users; Hashmap *inhibitors; Hashmap *buttons; + Hashmap *brightness_writers; LIST_HEAD(Seat, seat_gc_queue); LIST_HEAD(Session, session_gc_queue); diff --git a/src/login/meson.build b/src/login/meson.build index 1cc75fd1cfe..89ce81f767f 100644 --- a/src/login/meson.build +++ b/src/login/meson.build @@ -12,29 +12,31 @@ logind_gperf_c = custom_target( command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) liblogind_core_sources = files(''' - logind-core.c - logind-device.c - logind-device.h - logind-button.c - logind-button.h + logind-acl.h logind-action.c logind-action.h - logind-seat.c - logind-seat.h - logind-session.c - logind-session.h - logind-session-device.c - logind-session-device.h - logind-user.c - logind-user.h + logind-brightness.c + logind-brightness.h + logind-button.c + logind-button.h + logind-core.c + logind-dbus.c + logind-device.c + logind-device.h logind-inhibit.c logind-inhibit.h - logind-dbus.c - logind-session-dbus.c logind-seat-dbus.c + logind-seat.c + logind-seat.h + logind-session-dbus.c + logind-session-device.c + logind-session-device.h + logind-session.c + logind-session.h logind-user-dbus.c + logind-user.c + logind-user.h logind-utmp.c - logind-acl.h '''.split()) liblogind_core_sources += [logind_gperf_c] diff --git a/src/login/org.freedesktop.login1.conf b/src/login/org.freedesktop.login1.conf index f3c13ad89a0..124a25810e3 100644 --- a/src/login/org.freedesktop.login1.conf +++ b/src/login/org.freedesktop.login1.conf @@ -302,6 +302,10 @@ send_interface="org.freedesktop.login1.Session" send_member="PauseDeviceComplete"/> + + diff --git a/src/shared/bus-util.c b/src/shared/bus-util.c index c2440271feb..81acff4602b 100644 --- a/src/shared/bus-util.c +++ b/src/shared/bus-util.c @@ -1767,3 +1767,13 @@ int bus_reply_pair_array(sd_bus_message *m, char **l) { return sd_bus_send(NULL, reply, NULL); } + +static void bus_message_unref_wrapper(void *m) { + sd_bus_message_unref(m); +} + +const struct hash_ops bus_message_hash_ops = { + .hash = trivial_hash_func, + .compare = trivial_compare_func, + .free_value = bus_message_unref_wrapper, +}; diff --git a/src/shared/bus-util.h b/src/shared/bus-util.h index 59bfdb23981..3216b0c37a1 100644 --- a/src/shared/bus-util.h +++ b/src/shared/bus-util.h @@ -179,3 +179,5 @@ static inline int bus_open_system_watch_bind(sd_bus **ret) { } int bus_reply_pair_array(sd_bus_message *m, char **l); + +extern const struct hash_ops bus_message_hash_ops;