| /* Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| #include <assert.h> |
| #include <libudev.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <errno.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <regex.h> |
| #include <syslog.h> |
| |
| #include "cras_system_state.h" |
| #include "cras_types.h" |
| #include "cras_util.h" |
| #include "cras_checksum.h" |
| |
| struct udev_callback_data { |
| struct udev_monitor *mon; |
| struct udev *udev; |
| int fd; |
| }; |
| |
| static unsigned is_action(const char *desired, const char *actual) |
| __attribute__((nonnull(1))); |
| |
| /* Matches Alsa sound device entries generated by udev. For |
| * example: |
| * |
| * /devices/pci0000:00/0000:00:1b.0/sound/card1/pcmC1D0p |
| * |
| * We want to be able to extract: |
| * |
| * o The card number |
| * o The device number |
| * o If it's 'playback' (p) or 'capture' (c). (It may not be both.) |
| * |
| * Given the example above, the following matches should occur: |
| * |
| * |
| * | A | |
| * BBCCCD |
| * /devices/pci0000:00/0000:00:1b.0/sound/card1/pcmC1D10p |
| * |
| * A: The whole regex will be matched. |
| * B: The card. |
| * C: The device. |
| * D: 'p' (playback) or 'c' (capture) |
| * |
| * The order of the offsets in the 'pmatch' buffer does not appear |
| * to match with the documentation: |
| * |
| * Each rm_so element that is not -1 indicates the start |
| * offset of the next largest substring match within the |
| * string. |
| * |
| * But are, instead, filled in the same order presented in the |
| * string. To alleviate possible issudes, the 'C' (card) and 'D' |
| * (device) identifying characters are included in the result. |
| */ |
| static const char pcm_regex_string[] = "^.*pcm(C[0-9]+)(D[0-9]+)([pc])"; |
| static regex_t pcm_regex; |
| |
| /* Card regex is similar to above, but only has one field -- the card. The |
| * format is the same with the exception of the leaf node being of the form: |
| * |
| * /devices/...../card0 |
| * |
| * Where 0 is the card number and the only thing we care about in |
| * this case. |
| */ |
| |
| static const char card_regex_string[] = "^.*/card([0-9]+)"; |
| static regex_t card_regex; |
| |
| static char const *const subsystem = "sound"; |
| static const unsigned int MAX_DESC_NAME_LEN = 256; |
| |
| static unsigned is_action(const char *desired, const char *actual) |
| { |
| return actual != NULL && strcmp(desired, actual) == 0; |
| } |
| |
| static unsigned is_action_change(const char *action) |
| { |
| return is_action("change", action); |
| } |
| |
| static unsigned is_action_remove(const char *action) |
| { |
| return is_action("remove", action); |
| } |
| |
| static unsigned is_internal_bus(const char *bus) |
| { |
| return (bus != NULL && |
| (strcmp(bus, "pci") == 0 || strcmp(bus, "platform") == 0)); |
| } |
| |
| static unsigned is_external_bus(const char *bus) |
| { |
| return (bus != NULL && (strcmp(bus, "usb") == 0)); |
| } |
| |
| static unsigned is_internal_device(struct udev_device *dev) |
| { |
| struct udev_device *parent = udev_device_get_parent(dev); |
| while (parent != NULL) { |
| const char *name = udev_device_get_subsystem(parent); |
| |
| if (name != NULL) { |
| if (is_external_bus(name)) |
| return 0; |
| else if (is_internal_bus(name)) |
| return 1; |
| } |
| parent = udev_device_get_parent(parent); |
| } |
| return 0; |
| } |
| |
| static unsigned is_card_device(struct udev_device *dev, unsigned *internal, |
| unsigned *card_number, const char **sysname) |
| { |
| regmatch_t m[2]; |
| const char *devpath = udev_device_get_devpath(dev); |
| |
| if (devpath != NULL && |
| regexec(&card_regex, devpath, ARRAY_SIZE(m), m, 0) == 0) { |
| *sysname = udev_device_get_sysname(dev); |
| *internal = is_internal_device(dev); |
| *card_number = (unsigned)atoi(&devpath[m[1].rm_so]); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static void set_factory_default(unsigned card_number) |
| { |
| static const char alsactl[] = "/usr/sbin/alsactl"; |
| static const char asound_state[] = "/etc/asound.state"; |
| char cmd_buf[128]; |
| struct stat stat_buf; |
| int r; |
| |
| if (stat(asound_state, &stat_buf) == 0) { |
| syslog(LOG_INFO, "%s: init card '%u' to factory default", |
| __FUNCTION__, card_number); |
| r = snprintf(cmd_buf, ARRAY_SIZE(cmd_buf), |
| "%s --file %s restore %u", alsactl, asound_state, |
| card_number); |
| cmd_buf[ARRAY_SIZE(cmd_buf) - 1] = '\0'; |
| r = system(cmd_buf); |
| if (r != 0) |
| syslog(LOG_ERR, |
| "%s: failed to init card '%d' " |
| "to factory default. Failure: %d. Command: %s", |
| __FUNCTION__, card_number, r, cmd_buf); |
| } |
| } |
| |
| static inline void udev_delay_for_alsa() |
| { |
| /* Provide a small delay so that the udev message can |
| * propogate throughout the whole system, and Alsa can set up |
| * the new device. Without a small delay, an error of the |
| * form: |
| * |
| * Fail opening control hw:? |
| * |
| * will be produced by cras_alsa_card_create(). |
| */ |
| usleep(125000); /* 0.125 second */ |
| } |
| |
| /* Reads the "descriptors" file of the usb device and returns the |
| * checksum of the contents. Returns 0 if the file can not be read */ |
| static uint32_t calculate_desc_checksum(struct udev_device *dev) |
| { |
| char path[MAX_DESC_NAME_LEN]; |
| struct stat stat_buf; |
| int fd; |
| unsigned char *buf = NULL; |
| int buf_size = 0; |
| int read_size; |
| ssize_t n; |
| uint32_t result; |
| |
| if (snprintf(path, sizeof(path), "%s/descriptors", |
| udev_device_get_syspath(dev)) >= sizeof(path)) { |
| syslog(LOG_ERR, "failed to build path"); |
| return 0; |
| } |
| |
| if (stat(path, &stat_buf) < 0) { |
| syslog(LOG_ERR, "failed to stat file %s: %s", path, |
| strerror(errno)); |
| return 0; |
| } |
| |
| fd = open(path, O_RDONLY); |
| if (fd < 0) { |
| syslog(LOG_ERR, "failed to open file %s: %s", path, |
| strerror(errno)); |
| return 0; |
| } |
| |
| read_size = 0; |
| while (read_size < stat_buf.st_size) { |
| if (read_size == buf_size) { |
| if (buf_size == 0) |
| buf_size = 256; |
| else |
| buf_size *= 2; |
| uint8_t *new_buf = realloc(buf, buf_size); |
| if (new_buf == NULL) { |
| syslog(LOG_ERR, "no memory to read file %s", |
| path); |
| goto bail; |
| } |
| buf = new_buf; |
| } |
| n = read(fd, buf + read_size, buf_size - read_size); |
| if (n == 0) |
| break; |
| if (n < 0) { |
| syslog(LOG_ERR, "failed to read file %s", path); |
| goto bail; |
| } |
| read_size += n; |
| } |
| |
| close(fd); |
| result = crc32_checksum(buf, read_size); |
| free(buf); |
| return result; |
| bail: |
| close(fd); |
| free(buf); |
| return 0; |
| } |
| |
| static void fill_usb_card_info(struct cras_alsa_card_info *card_info, |
| struct udev_device *dev) |
| { |
| const char *sysattr; |
| struct udev_device *parent_dev = |
| udev_device_get_parent_with_subsystem_devtype(dev, "usb", |
| "usb_device"); |
| if (!parent_dev) |
| return; |
| |
| sysattr = udev_device_get_sysattr_value(parent_dev, "idVendor"); |
| if (sysattr) |
| card_info->usb_vendor_id = strtol(sysattr, NULL, 16); |
| sysattr = udev_device_get_sysattr_value(parent_dev, "idProduct"); |
| if (sysattr) |
| card_info->usb_product_id = strtol(sysattr, NULL, 16); |
| sysattr = udev_device_get_sysattr_value(parent_dev, "serial"); |
| if (sysattr) { |
| strncpy(card_info->usb_serial_number, sysattr, |
| USB_SERIAL_NUMBER_BUFFER_SIZE - 1); |
| card_info->usb_serial_number[USB_SERIAL_NUMBER_BUFFER_SIZE - 1] = |
| '\0'; |
| } |
| |
| card_info->usb_desc_checksum = calculate_desc_checksum(parent_dev); |
| |
| syslog(LOG_INFO, |
| "USB card: vendor:%04x, product:%04x, serial num:%s, " |
| "checksum:%08x", |
| card_info->usb_vendor_id, card_info->usb_product_id, |
| card_info->usb_serial_number, card_info->usb_desc_checksum); |
| } |
| |
| static void device_add_alsa(struct udev_device *dev, const char *sysname, |
| unsigned card, unsigned internal) |
| { |
| struct cras_alsa_card_info card_info; |
| memset(&card_info, 0, sizeof(card_info)); |
| |
| udev_delay_for_alsa(); |
| card_info.card_index = card; |
| if (internal) { |
| card_info.card_type = ALSA_CARD_TYPE_INTERNAL; |
| } else { |
| card_info.card_type = ALSA_CARD_TYPE_USB; |
| fill_usb_card_info(&card_info, dev); |
| } |
| |
| cras_system_add_alsa_card(&card_info); |
| } |
| |
| void device_remove_alsa(const char *sysname, unsigned card) |
| { |
| udev_delay_for_alsa(); |
| cras_system_remove_alsa_card(card); |
| } |
| |
| static int udev_sound_initialized(struct udev_device *dev) |
| { |
| /* udev will set SOUND_INITALIZED=1 for the main card node when the |
| * system has already been initialized, i.e. when cras is restarted |
| * on an already running system. |
| */ |
| const char *s; |
| |
| s = udev_device_get_property_value(dev, "SOUND_INITIALIZED"); |
| if (s) |
| return 1; |
| |
| return 0; |
| } |
| |
| static void change_udev_device_if_alsa_device(struct udev_device *dev) |
| { |
| /* If the device, 'dev' is an alsa device, add it to the set of |
| * devices available for I/O. Mark it as the active device. |
| */ |
| unsigned internal; |
| unsigned card_number; |
| const char *sysname; |
| |
| if (is_card_device(dev, &internal, &card_number, &sysname) && |
| udev_sound_initialized(dev) && |
| !cras_system_alsa_card_exists(card_number)) { |
| if (internal) |
| set_factory_default(card_number); |
| device_add_alsa(dev, sysname, card_number, internal); |
| } |
| } |
| |
| static void remove_device_if_card(struct udev_device *dev) |
| { |
| unsigned internal; |
| unsigned card_number; |
| const char *sysname; |
| |
| if (is_card_device(dev, &internal, &card_number, &sysname)) |
| device_remove_alsa(sysname, card_number); |
| } |
| |
| static void enumerate_devices(struct udev_callback_data *data) |
| { |
| struct udev_enumerate *enumerate = udev_enumerate_new(data->udev); |
| struct udev_list_entry *dl; |
| struct udev_list_entry *dev_list_entry; |
| |
| udev_enumerate_add_match_subsystem(enumerate, subsystem); |
| udev_enumerate_scan_devices(enumerate); |
| dl = udev_enumerate_get_list_entry(enumerate); |
| |
| udev_list_entry_foreach(dev_list_entry, dl) |
| { |
| const char *path = udev_list_entry_get_name(dev_list_entry); |
| struct udev_device *dev = |
| udev_device_new_from_syspath(data->udev, path); |
| |
| change_udev_device_if_alsa_device(dev); |
| udev_device_unref(dev); |
| } |
| udev_enumerate_unref(enumerate); |
| } |
| |
| static void udev_sound_subsystem_callback(void *arg, int revents) |
| { |
| struct udev_callback_data *data = (struct udev_callback_data *)arg; |
| struct udev_device *dev; |
| |
| dev = udev_monitor_receive_device(data->mon); |
| if (dev) { |
| const char *action = udev_device_get_action(dev); |
| |
| if (is_action_change(action)) |
| change_udev_device_if_alsa_device(dev); |
| else if (is_action_remove(action)) |
| remove_device_if_card(dev); |
| udev_device_unref(dev); |
| } else |
| syslog(LOG_WARNING, |
| "%s (internal error): " |
| "No device obtained", |
| __FUNCTION__); |
| } |
| |
| static void compile_regex(regex_t *regex, const char *str) |
| { |
| int r = regcomp(regex, str, REG_EXTENDED); |
| assert(r == 0); |
| } |
| |
| static struct udev_callback_data udev_data; |
| void cras_udev_start_sound_subsystem_monitor() |
| { |
| int r; |
| |
| udev_data.udev = udev_new(); |
| assert(udev_data.udev != NULL); |
| udev_data.mon = udev_monitor_new_from_netlink(udev_data.udev, "udev"); |
| |
| udev_monitor_filter_add_match_subsystem_devtype(udev_data.mon, |
| subsystem, NULL); |
| udev_monitor_enable_receiving(udev_data.mon); |
| udev_data.fd = udev_monitor_get_fd(udev_data.mon); |
| |
| r = cras_system_add_select_fd(udev_data.fd, |
| udev_sound_subsystem_callback, &udev_data, |
| POLLIN); |
| assert(r == 0); |
| compile_regex(&pcm_regex, pcm_regex_string); |
| compile_regex(&card_regex, card_regex_string); |
| |
| enumerate_devices(&udev_data); |
| } |
| |
| void cras_udev_stop_sound_subsystem_monitor() |
| { |
| udev_unref(udev_data.udev); |
| regfree(&pcm_regex); |
| regfree(&card_regex); |
| } |