| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| #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 BrightnessWriter* brightness_writer_free(BrightnessWriter *w) { |
| if (!w) |
| return NULL; |
| |
| 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); |
| |
| return mfree(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|FORK_REOPEN_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_put(set, &bus_message_hash_ops, 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 override 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; |
| } |
| |
| 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_ensure_put(&m->brightness_writers, &brightness_writer_hash_ops, w->path, w); |
| if (r == -ENOMEM) |
| return log_oom(); |
| 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; |
| } |