| /* SPDX-License-Identifier: LGPL-2.1+ */ |
| |
| #include <linux/rfkill.h> |
| #include <poll.h> |
| |
| #include "libudev.h" |
| #include "sd-daemon.h" |
| |
| #include "alloc-util.h" |
| #include "escape.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "io-util.h" |
| #include "mkdir.h" |
| #include "parse-util.h" |
| #include "proc-cmdline.h" |
| #include "string-table.h" |
| #include "string-util.h" |
| #include "udev-util.h" |
| #include "util.h" |
| #include "list.h" |
| |
| /* Note that any write is delayed until exit and the rfkill state will not be |
| * stored for rfkill indices that disappear after a change. */ |
| #define EXIT_USEC (5 * USEC_PER_SEC) |
| |
| typedef struct write_queue_item { |
| LIST_FIELDS(struct write_queue_item, queue); |
| int rfkill_idx; |
| char *file; |
| int state; |
| } write_queue_item; |
| |
| static struct write_queue_item* write_queue_item_free(struct write_queue_item *item) { |
| if (!item) |
| return NULL; |
| |
| free(item->file); |
| return mfree(item); |
| } |
| |
| static const char* const rfkill_type_table[NUM_RFKILL_TYPES] = { |
| [RFKILL_TYPE_ALL] = "all", |
| [RFKILL_TYPE_WLAN] = "wlan", |
| [RFKILL_TYPE_BLUETOOTH] = "bluetooth", |
| [RFKILL_TYPE_UWB] = "uwb", |
| [RFKILL_TYPE_WIMAX] = "wimax", |
| [RFKILL_TYPE_WWAN] = "wwan", |
| [RFKILL_TYPE_GPS] = "gps", |
| [RFKILL_TYPE_FM] = "fm", |
| [RFKILL_TYPE_NFC] = "nfc", |
| }; |
| |
| DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(rfkill_type, int); |
| |
| static int find_device( |
| struct udev *udev, |
| const struct rfkill_event *event, |
| struct udev_device **ret) { |
| |
| _cleanup_free_ char *sysname = NULL; |
| struct udev_device *device; |
| const char *name; |
| |
| assert(udev); |
| assert(event); |
| assert(ret); |
| |
| if (asprintf(&sysname, "rfkill%i", event->idx) < 0) |
| return log_oom(); |
| |
| device = udev_device_new_from_subsystem_sysname(udev, "rfkill", sysname); |
| if (!device) |
| return log_full_errno(IN_SET(errno, ENOENT, ENXIO, ENODEV) ? LOG_DEBUG : LOG_ERR, errno, |
| "Failed to open device '%s': %m", sysname); |
| |
| name = udev_device_get_sysattr_value(device, "name"); |
| if (!name) { |
| log_debug("Device has no name, ignoring."); |
| udev_device_unref(device); |
| return -ENOENT; |
| } |
| |
| log_debug("Operating on rfkill device '%s'.", name); |
| |
| *ret = device; |
| return 0; |
| } |
| |
| static int wait_for_initialized( |
| struct udev *udev, |
| struct udev_device *device, |
| struct udev_device **ret) { |
| |
| _cleanup_(udev_monitor_unrefp) struct udev_monitor *monitor = NULL; |
| struct udev_device *d; |
| const char *sysname; |
| int watch_fd, r; |
| |
| assert(udev); |
| assert(device); |
| assert(ret); |
| |
| if (udev_device_get_is_initialized(device) != 0) { |
| *ret = udev_device_ref(device); |
| return 0; |
| } |
| |
| assert_se(sysname = udev_device_get_sysname(device)); |
| |
| /* Wait until the device is initialized, so that we can get |
| * access to the ID_PATH property */ |
| |
| monitor = udev_monitor_new_from_netlink(udev, "udev"); |
| if (!monitor) |
| return log_error_errno(errno, "Failed to acquire monitor: %m"); |
| |
| r = udev_monitor_filter_add_match_subsystem_devtype(monitor, "rfkill", NULL); |
| if (r < 0) |
| return log_error_errno(r, "Failed to add rfkill udev match to monitor: %m"); |
| |
| r = udev_monitor_enable_receiving(monitor); |
| if (r < 0) |
| return log_error_errno(r, "Failed to enable udev receiving: %m"); |
| |
| watch_fd = udev_monitor_get_fd(monitor); |
| if (watch_fd < 0) |
| return log_error_errno(watch_fd, "Failed to get watch fd: %m"); |
| |
| /* Check again, maybe things changed */ |
| d = udev_device_new_from_subsystem_sysname(udev, "rfkill", sysname); |
| if (!d) |
| return log_full_errno(IN_SET(errno, ENOENT, ENXIO, ENODEV) ? LOG_DEBUG : LOG_ERR, errno, |
| "Failed to open device '%s': %m", sysname); |
| |
| if (udev_device_get_is_initialized(d) != 0) { |
| *ret = d; |
| return 0; |
| } |
| |
| for (;;) { |
| _cleanup_(udev_device_unrefp) struct udev_device *t = NULL; |
| |
| r = fd_wait_for_event(watch_fd, POLLIN, EXIT_USEC); |
| if (r == -EINTR) |
| continue; |
| if (r < 0) |
| return log_error_errno(r, "Failed to watch udev monitor: %m"); |
| if (r == 0) { |
| log_error("Timed out waiting for udev monitor."); |
| return -ETIMEDOUT; |
| } |
| |
| t = udev_monitor_receive_device(monitor); |
| if (!t) |
| continue; |
| |
| if (streq_ptr(udev_device_get_sysname(t), sysname)) { |
| *ret = udev_device_ref(t); |
| return 0; |
| } |
| } |
| } |
| |
| static int determine_state_file( |
| struct udev *udev, |
| const struct rfkill_event *event, |
| char **ret) { |
| |
| _cleanup_(udev_device_unrefp) struct udev_device *d = NULL; |
| _cleanup_(udev_device_unrefp) struct udev_device *device = NULL; |
| const char *path_id, *type; |
| char *state_file; |
| int r; |
| |
| assert(event); |
| assert(ret); |
| |
| r = find_device(udev, event, &d); |
| if (r < 0) |
| return r; |
| |
| r = wait_for_initialized(udev, d, &device); |
| if (r < 0) |
| return r; |
| |
| assert_se(type = rfkill_type_to_string(event->type)); |
| |
| path_id = udev_device_get_property_value(device, "ID_PATH"); |
| if (path_id) { |
| _cleanup_free_ char *escaped_path_id = NULL; |
| |
| escaped_path_id = cescape(path_id); |
| if (!escaped_path_id) |
| return log_oom(); |
| |
| state_file = strjoin("/var/lib/systemd/rfkill/", escaped_path_id, ":", type); |
| } else |
| state_file = strjoin("/var/lib/systemd/rfkill/", type); |
| |
| if (!state_file) |
| return log_oom(); |
| |
| *ret = state_file; |
| return 0; |
| } |
| |
| static int load_state( |
| int rfkill_fd, |
| struct udev *udev, |
| const struct rfkill_event *event) { |
| |
| _cleanup_free_ char *state_file = NULL, *value = NULL; |
| struct rfkill_event we; |
| ssize_t l; |
| int b, r; |
| |
| assert(rfkill_fd >= 0); |
| assert(udev); |
| assert(event); |
| |
| if (shall_restore_state() == 0) |
| return 0; |
| |
| r = determine_state_file(udev, event, &state_file); |
| if (r < 0) |
| return r; |
| |
| r = read_one_line_file(state_file, &value); |
| if (r == -ENOENT) { |
| /* No state file? Then save the current state */ |
| |
| r = write_string_file(state_file, one_zero(event->soft), WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); |
| if (r < 0) |
| return log_error_errno(r, "Failed to write state file %s: %m", state_file); |
| |
| log_debug("Saved state '%s' to %s.", one_zero(event->soft), state_file); |
| return 0; |
| } |
| if (r < 0) |
| return log_error_errno(r, "Failed to read state file %s: %m", state_file); |
| |
| b = parse_boolean(value); |
| if (b < 0) |
| return log_error_errno(b, "Failed to parse state file %s: %m", state_file); |
| |
| we = (struct rfkill_event) { |
| .op = RFKILL_OP_CHANGE, |
| .idx = event->idx, |
| .soft = b, |
| }; |
| |
| l = write(rfkill_fd, &we, sizeof(we)); |
| if (l < 0) |
| return log_error_errno(errno, "Failed to restore rfkill state for %i: %m", event->idx); |
| if (l != sizeof(we)) { |
| log_error("Couldn't write rfkill event structure, too short."); |
| return -EIO; |
| } |
| |
| log_debug("Loaded state '%s' from %s.", one_zero(b), state_file); |
| return 0; |
| } |
| |
| static void save_state_queue_remove( |
| struct write_queue_item **write_queue, |
| int idx, |
| char *state_file) { |
| |
| struct write_queue_item *item, *tmp; |
| |
| LIST_FOREACH_SAFE(queue, item, tmp, *write_queue) { |
| if ((state_file && streq(item->file, state_file)) || idx == item->rfkill_idx) { |
| log_debug("Canceled previous save state of '%s' to %s.", one_zero(item->state), item->file); |
| LIST_REMOVE(queue, *write_queue, item); |
| write_queue_item_free(item); |
| } |
| } |
| } |
| |
| static int save_state_queue( |
| struct write_queue_item **write_queue, |
| int rfkill_fd, |
| struct udev *udev, |
| const struct rfkill_event *event) { |
| |
| _cleanup_free_ char *state_file = NULL; |
| struct write_queue_item *item; |
| int r; |
| |
| assert(rfkill_fd >= 0); |
| assert(udev); |
| assert(event); |
| |
| r = determine_state_file(udev, event, &state_file); |
| if (r < 0) |
| return r; |
| |
| save_state_queue_remove(write_queue, event->idx, state_file); |
| |
| item = new0(struct write_queue_item, 1); |
| if (!item) |
| return -ENOMEM; |
| |
| item->file = TAKE_PTR(state_file); |
| item->rfkill_idx = event->idx; |
| item->state = event->soft; |
| |
| LIST_APPEND(queue, *write_queue, item); |
| |
| return 0; |
| } |
| |
| static int save_state_cancel( |
| struct write_queue_item **write_queue, |
| int rfkill_fd, |
| struct udev *udev, |
| const struct rfkill_event *event) { |
| |
| _cleanup_free_ char *state_file = NULL; |
| int r; |
| |
| assert(rfkill_fd >= 0); |
| assert(udev); |
| assert(event); |
| |
| r = determine_state_file(udev, event, &state_file); |
| save_state_queue_remove(write_queue, event->idx, state_file); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| static int save_state_write(struct write_queue_item **write_queue) { |
| struct write_queue_item *item, *tmp; |
| int result = 0; |
| bool error_logged = false; |
| int r; |
| |
| LIST_FOREACH_SAFE(queue, item, tmp, *write_queue) { |
| r = write_string_file(item->file, one_zero(item->state), WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); |
| if (r < 0) { |
| result = r; |
| if (!error_logged) { |
| log_error_errno(r, "Failed to write state file %s: %m", item->file); |
| error_logged = true; |
| } else |
| log_warning_errno(r, "Failed to write state file %s: %m", item->file); |
| } else |
| log_debug("Saved state '%s' to %s.", one_zero(item->state), item->file); |
| |
| LIST_REMOVE(queue, *write_queue, item); |
| write_queue_item_free(item); |
| } |
| return result; |
| } |
| |
| int main(int argc, char *argv[]) { |
| LIST_HEAD(write_queue_item, write_queue); |
| _cleanup_(udev_unrefp) struct udev *udev = NULL; |
| _cleanup_close_ int rfkill_fd = -1; |
| bool ready = false; |
| int r, n; |
| |
| if (argc > 1) { |
| log_error("This program requires no arguments."); |
| return EXIT_FAILURE; |
| } |
| |
| LIST_HEAD_INIT(write_queue); |
| |
| log_set_target(LOG_TARGET_AUTO); |
| log_parse_environment(); |
| log_open(); |
| |
| umask(0022); |
| |
| udev = udev_new(); |
| if (!udev) { |
| r = log_oom(); |
| goto finish; |
| } |
| |
| r = mkdir_p("/var/lib/systemd/rfkill", 0755); |
| if (r < 0) { |
| log_error_errno(r, "Failed to create rfkill directory: %m"); |
| goto finish; |
| } |
| |
| n = sd_listen_fds(false); |
| if (n < 0) { |
| r = log_error_errno(n, "Failed to determine whether we got any file descriptors passed: %m"); |
| goto finish; |
| } |
| if (n > 1) { |
| log_error("Got too many file descriptors."); |
| r = -EINVAL; |
| goto finish; |
| } |
| |
| if (n == 0) { |
| rfkill_fd = open("/dev/rfkill", O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); |
| if (rfkill_fd < 0) { |
| if (errno == ENOENT) { |
| log_debug_errno(errno, "Missing rfkill subsystem, or no device present, exiting."); |
| r = 0; |
| goto finish; |
| } |
| |
| r = log_error_errno(errno, "Failed to open /dev/rfkill: %m"); |
| goto finish; |
| } |
| } else { |
| rfkill_fd = SD_LISTEN_FDS_START; |
| |
| r = fd_nonblock(rfkill_fd, 1); |
| if (r < 0) { |
| log_error_errno(r, "Failed to make /dev/rfkill socket non-blocking: %m"); |
| goto finish; |
| } |
| } |
| |
| for (;;) { |
| struct rfkill_event event; |
| const char *type; |
| ssize_t l; |
| |
| l = read(rfkill_fd, &event, sizeof(event)); |
| if (l < 0) { |
| if (errno == EAGAIN) { |
| |
| if (!ready) { |
| /* Notify manager that we are |
| * now finished with |
| * processing whatever was |
| * queued */ |
| (void) sd_notify(false, "READY=1"); |
| ready = true; |
| } |
| |
| /* Hang around for a bit, maybe there's more coming */ |
| |
| r = fd_wait_for_event(rfkill_fd, POLLIN, EXIT_USEC); |
| if (r == -EINTR) |
| continue; |
| if (r < 0) { |
| log_error_errno(r, "Failed to poll() on device: %m"); |
| goto finish; |
| } |
| if (r > 0) |
| continue; |
| |
| log_debug("All events read and idle, exiting."); |
| break; |
| } |
| |
| log_error_errno(errno, "Failed to read from /dev/rfkill: %m"); |
| } |
| |
| if (l != RFKILL_EVENT_SIZE_V1) { |
| log_error("Read event structure of invalid size."); |
| r = -EIO; |
| goto finish; |
| } |
| |
| type = rfkill_type_to_string(event.type); |
| if (!type) { |
| log_debug("An rfkill device of unknown type %i discovered, ignoring.", event.type); |
| continue; |
| } |
| |
| switch (event.op) { |
| |
| case RFKILL_OP_ADD: |
| log_debug("A new rfkill device has been added with index %i and type %s.", event.idx, type); |
| (void) load_state(rfkill_fd, udev, &event); |
| break; |
| |
| case RFKILL_OP_DEL: |
| log_debug("An rfkill device has been removed with index %i and type %s", event.idx, type); |
| (void) save_state_cancel(&write_queue, rfkill_fd, udev, &event); |
| break; |
| |
| case RFKILL_OP_CHANGE: |
| log_debug("An rfkill device has changed state with index %i and type %s", event.idx, type); |
| (void) save_state_queue(&write_queue, rfkill_fd, udev, &event); |
| break; |
| |
| default: |
| log_debug("Unknown event %i from /dev/rfkill for index %i and type %s, ignoring.", event.op, event.idx, type); |
| break; |
| } |
| } |
| |
| r = 0; |
| |
| finish: |
| (void) save_state_write(&write_queue); |
| |
| return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; |
| } |