diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c index 7d1ae9a3a..348935632 100644 --- a/spa/plugins/alsa/alsa-udev.c +++ b/spa/plugins/alsa/alsa-udev.c @@ -39,15 +39,28 @@ enum action { /* Used for unavailable devices in the card structure. */ #define ID_DEVICE_NOT_SUPPORTED 0 +#define MAX_PARENT_TRAVERSAL_DEPTH 5 + +enum wireless_status { + WIRELESS_STATUS_UNKNOWN = 0, + WIRELESS_STATUS_CONNECTED, + WIRELESS_STATUS_DISCONNECTED, +}; + +/* Forward declaration for struct impl */ +struct impl; + /* This represents an ALSA card. * One card can have up to 1 PCM and 1 Compress-Offload device. */ struct card { + struct impl *impl; unsigned int card_nr; struct udev_device *udev_device; unsigned int unavailable:1; unsigned int accessible:1; unsigned int ignored:1; unsigned int emitted:1; + unsigned int wireless_disconnected:1; /* Local SPA object IDs. (Global IDs are produced by PipeWire * out of this using its registry.) Compress-Offload or PCM @@ -59,6 +72,10 @@ struct card { * is used because 0 is a valid ALSA card number. */ uint32_t pcm_device_id; uint32_t compress_offload_device_id; + + char *wireless_status_path; + int wireless_status_fd; + struct spa_source wireless_status_source; }; static uint32_t calc_pcm_device_id(struct card *card) @@ -97,6 +114,11 @@ struct impl { int use_ucm; }; +/* Forward declarations */ +static void process_card(struct impl *this, enum action action, struct card *card); +static void stop_wireless_status_monitor(struct impl *this, struct card *card); +static void on_wireless_status_change(struct spa_source *source); + static int impl_udev_open(struct impl *this) { if (this->udev == NULL) { @@ -124,7 +146,9 @@ static struct card *add_card(struct impl *this, unsigned int card_nr, struct ude card = &this->cards[this->n_cards++]; spa_zero(*card); + card->impl = this; card->card_nr = card_nr; + card->wireless_status_fd = -1; udev_device_ref(udev_device); card->udev_device = udev_device; @@ -143,15 +167,20 @@ static struct card *find_card(struct impl *this, unsigned int card_nr) static void remove_card(struct impl *this, struct card *card) { + stop_wireless_status_monitor(this, card); udev_device_unref(card->udev_device); - *card = this->cards[--this->n_cards]; + this->n_cards--; + if (card != &this->cards[this->n_cards]) + *card = this->cards[this->n_cards]; } static void clear_cards(struct impl *this) { unsigned int i; - for (i = 0; i < this->n_cards; i++) + for (i = 0; i < this->n_cards; i++) { + stop_wireless_status_monitor(this, &this->cards[i]); udev_device_unref(this->cards[i].udev_device); + } this->n_cards = 0; } @@ -260,6 +289,240 @@ static void unescape(const char *src, char *dst) *d = 0; } +static enum wireless_status parse_wireless_status(const char *buf) +{ + if (spa_streq(buf, "connected")) + return WIRELESS_STATUS_CONNECTED; + if (spa_streq(buf, "disconnected")) + return WIRELESS_STATUS_DISCONNECTED; + return WIRELESS_STATUS_UNKNOWN; +} + +static enum wireless_status read_wireless_status(const char *sysfs_path) +{ + char buf[16]; + ssize_t sz; + int fd; + + fd = open(sysfs_path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return WIRELESS_STATUS_UNKNOWN; + + sz = read(fd, buf, sizeof(buf) - 1); + close(fd); + + if (sz <= 0) + return WIRELESS_STATUS_UNKNOWN; + + buf[sz] = '\0'; + if (buf[sz - 1] == '\n') + buf[sz - 1] = '\0'; + + return parse_wireless_status(buf); +} + +static bool is_usb_interface_dir(const char *name) +{ + const char *colon = strchr(name, ':'); + if (!colon || !strchr(name, '-')) + return false; + + for (const char *p = colon + 1; *p; p++) + if (*p != '.' && !(*p >= '0' && *p <= '9')) + return false; + + return true; +} + +static char *check_wireless_status_at_path(struct impl *this, const char *path) +{ + char wireless_path[PATH_MAX]; + int res; + + res = spa_scnprintf(wireless_path, sizeof(wireless_path), + "%s/wireless_status", path); + if (res < 0 || (size_t)res >= sizeof(wireless_path)) + return NULL; + + if (access(wireless_path, R_OK) < 0) + return NULL; + + return strdup(wireless_path); +} + +static char *search_siblings_for_wireless_status(struct impl *this, const char *parent_path) +{ + struct dirent *entry; + char *result = NULL; + int count = 0; + + spa_autoptr(DIR) parent_dir = opendir(parent_path); + if (!parent_dir) + return NULL; + + while ((entry = readdir(parent_dir)) != NULL) { + char sibling_path[PATH_MAX]; + char *path; + int res; + + if (entry->d_name[0] == '.') + continue; + + if (!is_usb_interface_dir(entry->d_name)) + continue; + + if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_DIR) + continue; + + res = spa_scnprintf(sibling_path, sizeof(sibling_path), + "%s/%s", parent_path, entry->d_name); + if (res < 0 || (size_t)res >= sizeof(sibling_path)) + continue; + + path = check_wireless_status_at_path(this, sibling_path); + if (path) { + count++; + if (!result) + result = path; + else + free(path); + } + } + + if (count > 1) + spa_log_info(this->log, "found %d wireless_status files, using first one", count); + + return result; +} + +static char *find_wireless_status_path(struct impl *this, const char *card_syspath) +{ + char usb_device_path[PATH_MAX]; + char *last_slash, *result; + int i, res; + + res = spa_scnprintf(usb_device_path, sizeof(usb_device_path), "%s", card_syspath); + if (res < 0 || (size_t)res >= sizeof(usb_device_path)) + return NULL; + + result = check_wireless_status_at_path(this, usb_device_path); + if (result) + return result; + + for (i = 0; i < MAX_PARENT_TRAVERSAL_DEPTH; i++) { + last_slash = strrchr(usb_device_path, '/'); + if (!last_slash || last_slash == usb_device_path) + break; + + *last_slash = '\0'; + + result = search_siblings_for_wireless_status(this, usb_device_path); + if (result) + return result; + } + + return NULL; +} + +static void stop_wireless_status_monitor(struct impl *this, struct card *card) +{ + if (card->wireless_status_fd >= 0) { + spa_loop_remove_source(this->main_loop, &card->wireless_status_source); + close(card->wireless_status_fd); + card->wireless_status_fd = -1; + } + if (card->wireless_status_path) { + free(card->wireless_status_path); + card->wireless_status_path = NULL; + } +} + +static int start_wireless_status_monitor(struct impl *this, struct card *card, const char *path) +{ + char buf[16]; + ssize_t sz; + int fd; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + spa_log_debug(this->log, "card %u: failed to open wireless_status: %s", + card->card_nr, spa_strerror(-errno)); + return -errno; + } + + sz = read(fd, buf, sizeof(buf) - 1); + if (sz <= 0) { + spa_log_debug(this->log, "card %u: failed to read wireless_status: %s", + card->card_nr, spa_strerror(sz < 0 ? -errno : -EIO)); + close(fd); + return sz < 0 ? -errno : -EIO; + } + + lseek(fd, 0, SEEK_SET); + + card->wireless_status_path = strdup(path); + if (!card->wireless_status_path) { + close(fd); + return -ENOMEM; + } + + card->wireless_status_fd = fd; + card->wireless_status_source.func = on_wireless_status_change; + card->wireless_status_source.data = card; + card->wireless_status_source.fd = fd; + card->wireless_status_source.mask = SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &card->wireless_status_source); + + spa_log_info(this->log, "card %u: monitoring wireless_status at %s", + card->card_nr, path); + + return 0; +} + +static void on_wireless_status_change(struct spa_source *source) +{ + struct card *card = source->data; + struct impl *this = card->impl; + enum wireless_status status; + bool disconnected; + char buf[16]; + ssize_t sz; + + lseek(card->wireless_status_fd, 0, SEEK_SET); + sz = read(card->wireless_status_fd, buf, sizeof(buf) - 1); + if (sz <= 0) { + spa_log_info(this->log, "card %u wireless_status unreadable, removing monitor", + card->card_nr); + stop_wireless_status_monitor(this, card); + card->wireless_disconnected = false; + process_card(this, ACTION_CHANGE, card); + return; + } + + buf[sz] = '\0'; + if (buf[sz - 1] == '\n') + buf[sz - 1] = '\0'; + + status = parse_wireless_status(buf); + if (status == WIRELESS_STATUS_UNKNOWN) { + spa_log_info(this->log, "card %u wireless_status unknown, removing monitor", + card->card_nr); + stop_wireless_status_monitor(this, card); + card->wireless_disconnected = false; + process_card(this, ACTION_CHANGE, card); + return; + } + + disconnected = (status == WIRELESS_STATUS_DISCONNECTED); + if (disconnected != card->wireless_disconnected) { + spa_log_info(this->log, "card %u wireless device %s", + card->card_nr, disconnected ? "disconnected" : "connected"); + card->wireless_disconnected = disconnected; + process_card(this, ACTION_CHANGE, card); + } +} + static int check_device_pcm_class(const char *devname) { char path[PATH_MAX]; @@ -485,6 +748,30 @@ static int emit_added_object_info(struct impl *this, struct card *card) snprintf(path, sizeof(path), "hw:%u", card->card_nr); + if (card->wireless_status_fd < 0) { + const char *syspath = udev_device_get_syspath(udev_device); + char *wireless_path; + + if (syspath && (wireless_path = find_wireless_status_path(this, syspath))) { + enum wireless_status status = read_wireless_status(wireless_path); + + if (status == WIRELESS_STATUS_DISCONNECTED) { + spa_log_info(this->log, "card %u: wireless device disconnected, not emitting", + card->card_nr); + free(wireless_path); + card->wireless_disconnected = true; + return -ENODEV; + } + + if (status == WIRELESS_STATUS_CONNECTED) { + if (start_wireless_status_monitor(this, card, wireless_path) < 0) + free(wireless_path); + } else { + free(wireless_path); + } + } + } + if ((res = check_pcm_device_availability(this, card, &num_pcm_devices)) < 0) return res; if ((res = check_compress_offload_device_availability(this, card, &num_compress_offload_devices)) < 0) @@ -731,7 +1018,7 @@ static void process_card(struct impl *this, enum action action, struct card *car switch (action) { case ACTION_CHANGE: { check_access(this, card); - if (card->accessible && !card->emitted) { + if (card->accessible && !card->wireless_disconnected && !card->emitted) { int res = emit_added_object_info(this, card); if (res < 0) { if (card->ignored) @@ -750,7 +1037,7 @@ static void process_card(struct impl *this, enum action action, struct card *car card->card_nr); card->unavailable = false; } - } else if (!card->accessible && card->emitted) { + } else if ((!card->accessible || card->wireless_disconnected) && card->emitted) { card->emitted = false; if (card->pcm_device_id != ID_DEVICE_NOT_SUPPORTED) @@ -900,18 +1187,17 @@ static void impl_on_fd_events(struct spa_source *source) if (udev_device == NULL) return; - if ((action = udev_device_get_action(udev_device)) == NULL) + action = udev_device_get_action(udev_device); + if (action == NULL) action = "change"; - spa_log_debug(this->log, "action %s", action); - start_inotify(this); - if (spa_streq(action, "add") || spa_streq(action, "change")) { + if (spa_streq(action, "add") || spa_streq(action, "change")) process_udev_device(this, ACTION_CHANGE, udev_device); - } else if (spa_streq(action, "remove")) { + else if (spa_streq(action, "remove")) process_udev_device(this, ACTION_REMOVE, udev_device); - } + udev_device_unref(udev_device); }