diff options
Diffstat (limited to 'src/modules/module-udev-detect.c')
| -rw-r--r-- | src/modules/module-udev-detect.c | 391 | 
1 files changed, 332 insertions, 59 deletions
| diff --git a/src/modules/module-udev-detect.c b/src/modules/module-udev-detect.c index 1ad6fa2d..1b1e9c1a 100644 --- a/src/modules/module-udev-detect.c +++ b/src/modules/module-udev-detect.c @@ -25,13 +25,17 @@  #include <errno.h>  #include <limits.h> +#include <dirent.h>  #include <sys/inotify.h>  #include <libudev.h> +#include <pulse/timeval.h> +  #include <pulsecore/modargs.h>  #include <pulsecore/core-error.h>  #include <pulsecore/core-util.h>  #include <pulsecore/namereg.h> +#include <pulsecore/ratelimit.h>  #include "module-udev-detect-symdef.h" @@ -39,18 +43,25 @@ PA_MODULE_AUTHOR("Lennart Poettering");  PA_MODULE_DESCRIPTION("Detect available audio hardware and load matching drivers");  PA_MODULE_VERSION(PACKAGE_VERSION);  PA_MODULE_LOAD_ONCE(TRUE); +PA_MODULE_USAGE( +        "tsched=<enable system timer based scheduling mode?> " +        "ignore_dB=<ignore dB information from the device?>");  struct device {      char *path; -    pa_bool_t accessible; +    pa_bool_t need_verify;      char *card_name; +    char *args;      uint32_t module; +    pa_ratelimit ratelimit;  };  struct userdata {      pa_core *core;      pa_hashmap *devices; -    pa_bool_t use_tsched; + +    pa_bool_t use_tsched:1; +    pa_bool_t ignore_dB:1;      struct udev* udev;      struct udev_monitor *monitor; @@ -62,14 +73,18 @@ struct userdata {  static const char* const valid_modargs[] = {      "tsched", +    "ignore_dB",      NULL  }; +static int setup_inotify(struct userdata *u); +  static void device_free(struct device *d) {      pa_assert(d);      pa_xfree(d->path);      pa_xfree(d->card_name); +    pa_xfree(d->args);      pa_xfree(d);  } @@ -88,35 +103,204 @@ static const char *path_get_card_id(const char *path) {      return e + 5;  } +static pa_bool_t is_card_busy(const char *id) { +    char *card_path = NULL, *pcm_path = NULL, *sub_status = NULL; +    DIR *card_dir = NULL, *pcm_dir = NULL; +    FILE *status_file = NULL; +    size_t len; +    struct dirent *space = NULL, *de; +    pa_bool_t busy = FALSE; +    int r; + +    pa_assert(id); + +    /* This simply uses /proc/asound/card.../pcm.../sub.../status to +     * check whether there is still a process using this audio device. */ + +    card_path = pa_sprintf_malloc("/proc/asound/card%s", id); + +    if (!(card_dir = opendir(card_path))) { +        pa_log_warn("Failed to open %s: %s", card_path, pa_cstrerror(errno)); +        goto fail; +    } + +    len = offsetof(struct dirent, d_name) + fpathconf(dirfd(card_dir), _PC_NAME_MAX) + 1; +    space = pa_xmalloc(len); + +    for (;;) { +        de = NULL; + +        if ((r = readdir_r(card_dir, space, &de)) != 0) { +            pa_log_warn("readdir_r() failed: %s", pa_cstrerror(r)); +            goto fail; +        } + +        if (!de) +            break; + +        if (!pa_startswith(de->d_name, "pcm")) +            continue; + +        pa_xfree(pcm_path); +        pcm_path = pa_sprintf_malloc("%s/%s", card_path, de->d_name); + +        if (pcm_dir) +            closedir(pcm_dir); + +        if (!(pcm_dir = opendir(pcm_path))) { +            pa_log_warn("Failed to open %s: %s", pcm_path, pa_cstrerror(errno)); +            continue; +        } + +        for (;;) { +            char line[32]; + +            if ((r = readdir_r(pcm_dir, space, &de)) != 0) { +                pa_log_warn("readdir_r() failed: %s", pa_cstrerror(r)); +                goto fail; +            } + +            if (!de) +                break; + +            if (!pa_startswith(de->d_name, "sub")) +                continue; + +            pa_xfree(sub_status); +            sub_status = pa_sprintf_malloc("%s/%s/status", pcm_path, de->d_name); + +            if (status_file) +                fclose(status_file); + +            if (!(status_file = fopen(sub_status, "r"))) { +                pa_log_warn("Failed to open %s: %s", sub_status, pa_cstrerror(errno)); +                continue; +            } + +            if (!(fgets(line, sizeof(line)-1, status_file))) { +                pa_log_warn("Failed to read from %s: %s", sub_status, pa_cstrerror(errno)); +                continue; +            } + +            if (!pa_streq(line, "closed\n")) { +                busy = TRUE; +                break; +            } +        } +    } + +fail: + +    pa_xfree(card_path); +    pa_xfree(pcm_path); +    pa_xfree(sub_status); +    pa_xfree(space); + +    if (card_dir) +        closedir(card_dir); + +    if (pcm_dir) +        closedir(pcm_dir); + +    if (status_file) +        fclose(status_file); + +    return busy; +} +  static void verify_access(struct userdata *u, struct device *d) {      char *cd;      pa_card *card; +    pa_bool_t accessible;      pa_assert(u);      pa_assert(d); -    if (!(card = pa_namereg_get(u->core, d->card_name, PA_NAMEREG_CARD))) -        return; -      cd = pa_sprintf_malloc("%s/snd/controlC%s", udev_get_dev_path(u->udev), path_get_card_id(d->path)); -    d->accessible = access(cd, W_OK) >= 0; -    pa_log_info("%s is accessible: %s", cd, pa_yes_no(d->accessible)); +    accessible = access(cd, R_OK|W_OK) >= 0; +    pa_log_debug("%s is accessible: %s", cd, pa_yes_no(accessible)); +      pa_xfree(cd); -    pa_card_suspend(card, !d->accessible, PA_SUSPEND_SESSION); +    if (d->module == PA_INVALID_INDEX) { + +        /* If we are not loaded, try to load */ + +        if (accessible) { +            pa_module *m; +            pa_bool_t busy; + +            /* Check if any of the PCM devices that belong to this +             * card are currently busy. If they are, don't try to load +             * right now, to make sure the probing phase can +             * successfully complete. When the current user of the +             * device closes it we will get another notification via +             * inotify and can then recheck. */ + +            busy = is_card_busy(path_get_card_id(d->path)); +            pa_log_debug("%s is busy: %s", d->path, pa_yes_no(busy)); + +            if (!busy) { + +                /* So, why do we rate limit here? It's certainly ugly, +                 * but there seems to be no other way. Problem is +                 * this: if we are unable to configure/probe an audio +                 * device after opening it we will close it again and +                 * the module initialization will fail. This will then +                 * cause an inotify event on the device node which +                 * will be forwarded to us. We then try to reopen the +                 * audio device again, practically entering a busy +                 * loop. +                 * +                 * A clean fix would be if we would be able to ignore +                 * our own inotify close events. However, inotify +                 * lacks such functionality. Also, during probing of +                 * the device we cannot really distuingish between +                 * other processes causing EBUSY or ourselves, which +                 * means we have no way to figure out if the probing +                 * during opening was canceled by a "try again" +                 * failure or a "fatal" failure. */ + +                if (pa_ratelimit_test(&d->ratelimit)) { +                    pa_log_debug("Loading module-alsa-card with arguments '%s'", d->args); +                    m = pa_module_load(u->core, "module-alsa-card", d->args); + +                    if (m) { +                        d->module = m->index; +                        pa_log_info("Card %s (%s) module loaded.", d->path, d->card_name); +                    } else +                        pa_log_info("Card %s (%s) failed to load module.", d->path, d->card_name); +                } else +                    pa_log_warn("Tried to configure %s (%s) more often than %u times in %llus", +                                d->path, +                                d->card_name, +                                d->ratelimit.burst, +                                (long long unsigned) (d->ratelimit.interval / PA_USEC_PER_SEC)); +            } +        } + +    } else { + +        /* If we are already loaded update suspend status with +         * accessible boolean */ + +        if ((card = pa_namereg_get(u->core, d->card_name, PA_NAMEREG_CARD))) +            pa_card_suspend(card, !accessible, PA_SUSPEND_SESSION); +    }  }  static void card_changed(struct userdata *u, struct udev_device *dev) {      struct device *d;      const char *path;      const char *t; -    char *card_name, *args; -    pa_module *m;      char *n;      pa_assert(u);      pa_assert(dev); +    /* Maybe /dev/snd is now available? */ +    setup_inotify(u); +      path = udev_device_get_devpath(dev);      if ((d = pa_hashmap_get(u->devices, path))) { @@ -124,42 +308,34 @@ static void card_changed(struct userdata *u, struct udev_device *dev) {          return;      } +    d = pa_xnew0(struct device, 1); +    d->path = pa_xstrdup(path); +    d->module = PA_INVALID_INDEX; +    PA_INIT_RATELIMIT(d->ratelimit, 10*PA_USEC_PER_SEC, 5); +      if (!(t = udev_device_get_property_value(dev, "PULSE_NAME")))          if (!(t = udev_device_get_property_value(dev, "ID_ID")))              if (!(t = udev_device_get_property_value(dev, "ID_PATH")))                  t = path_get_card_id(path);      n = pa_namereg_make_valid_name(t); +    d->card_name = pa_sprintf_malloc("alsa_card.%s", n); +    d->args = pa_sprintf_malloc("device_id=\"%s\" " +                                "name=\"%s\" " +                                "card_name=\"%s\" " +                                "tsched=%s " +                                "ignore_dB=%s " +                                "card_properties=\"module-udev-detect.discovered=1\"", +                                path_get_card_id(path), +                                n, +                                d->card_name, +                                pa_yes_no(u->use_tsched), +                                pa_yes_no(u->ignore_dB)); +    pa_xfree(n); -    card_name = pa_sprintf_malloc("alsa_card.%s", n); -    args = pa_sprintf_malloc("device_id=\"%s\" " -                             "name=\"%s\" " -                             "card_name=\"%s\" " -                             "tsched=%i " -                             "card_properties=\"module-udev-detect.discovered=1\"", -                             path_get_card_id(path), -                             n, -                             card_name, -                             (int) u->use_tsched); - -    pa_log_debug("Loading module-alsa-card with arguments '%s'", args); -    m = pa_module_load(u->core, "module-alsa-card", args); -    pa_xfree(args); - -    if (m) { -        pa_log_info("Card %s (%s) added.", path, n); - -        d = pa_xnew(struct device, 1); -        d->path = pa_xstrdup(path); -        d->card_name = card_name; -        d->module = m->index; -        d->accessible = TRUE; - -        pa_hashmap_put(u->devices, d->path, d); -    } else -        pa_xfree(card_name); +    pa_hashmap_put(u->devices, d->path, d); -    pa_xfree(n); +    verify_access(u, d);  }  static void remove_card(struct userdata *u, struct udev_device *dev) { @@ -172,7 +348,10 @@ static void remove_card(struct userdata *u, struct udev_device *dev) {          return;      pa_log_info("Card %s removed.", d->path); -    pa_module_unload_request_by_index(u->core, d->module, TRUE); + +    if (d->module != PA_INVALID_INDEX) +        pa_module_unload_request_by_index(u->core, d->module, TRUE); +      device_free(d);  } @@ -249,6 +428,34 @@ fail:      u->udev_io = NULL;  } +static pa_bool_t pcm_node_belongs_to_device( +        struct device *d, +        const char *node) { + +    char *cd; +    pa_bool_t b; + +    cd = pa_sprintf_malloc("pcmC%sD", path_get_card_id(d->path)); +    b = pa_startswith(node, cd); +    pa_xfree(cd); + +    return b; +} + +static pa_bool_t control_node_belongs_to_device( +        struct device *d, +        const char *node) { + +    char *cd; +    pa_bool_t b; + +    cd = pa_sprintf_malloc("controlC%s", path_get_card_id(d->path)); +    b = pa_streq(node, cd); +    pa_xfree(cd); + +    return b; +} +  static void inotify_cb(          pa_mainloop_api*a,          pa_io_event* e, @@ -262,10 +469,13 @@ static void inotify_cb(      } buf;      struct userdata *u = userdata;      static int type = 0; -    pa_bool_t verify = FALSE; +    pa_bool_t deleted = FALSE; +    struct device *d; +    void *state;      for (;;) {          ssize_t r; +        struct inotify_event *event;          pa_zero(buf);          if ((r = pa_read(fd, &buf, sizeof(buf), &type)) <= 0) { @@ -277,25 +487,60 @@ static void inotify_cb(              goto fail;          } -        if ((buf.e.mask & IN_CLOSE_WRITE) && pa_startswith(buf.e.name, "pcmC")) -            verify = TRUE; +        event = &buf.e; +        while (r > 0) { +            size_t len; + +            if ((size_t) r < sizeof(struct inotify_event)) { +                pa_log("read() too short."); +                goto fail; +            } + +            len = sizeof(struct inotify_event) + event->len; + +            if ((size_t) r < len) { +                pa_log("Payload missing."); +                goto fail; +            } + +            /* From udev we get the guarantee that the control +             * device's ACL is changed last. To avoid races when ACLs +             * are changed we hence watch only the control device */ +            if (((event->mask & IN_ATTRIB) && pa_startswith(event->name, "controlC"))) +                PA_HASHMAP_FOREACH(d, u->devices, state) +                    if (control_node_belongs_to_device(d, event->name)) +                        d->need_verify = TRUE; + +            /* ALSA doesn't really give us any guarantee on the closing +             * order, so let's simply hope */ +            if (((event->mask & IN_CLOSE_WRITE) && pa_startswith(event->name, "pcmC"))) +                PA_HASHMAP_FOREACH(d, u->devices, state) +                    if (pcm_node_belongs_to_device(d, event->name)) +                        d->need_verify = TRUE; + +            /* /dev/snd/ might have been removed */ +            if ((event->mask & (IN_DELETE_SELF|IN_MOVE_SELF))) +                deleted = TRUE; + +            event = (struct inotify_event*) ((uint8_t*) event + len); +            r -= len; +        }      } -    if (verify) { -        struct device *d; -        void *state; - -        pa_log_debug("Verifying access."); - -        PA_HASHMAP_FOREACH(d, u->devices, state) +    PA_HASHMAP_FOREACH(d, u->devices, state) +        if (d->need_verify) { +            d->need_verify = FALSE;              verify_access(u, d); -    } +        } -    return; +    if (!deleted) +        return;  fail: -    a->io_free(u->inotify_io); -    u->inotify_io = NULL; +    if (u->inotify_io) { +        a->io_free(u->inotify_io); +        u->inotify_io = NULL; +    }      if (u->inotify_fd >= 0) {          pa_close(u->inotify_fd); @@ -307,17 +552,38 @@ static int setup_inotify(struct userdata *u) {      char *dev_snd;      int r; +    if (u->inotify_fd >= 0) +        return 0; +      if ((u->inotify_fd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK)) < 0) {          pa_log("inotify_init1() failed: %s", pa_cstrerror(errno));          return -1;      }      dev_snd = pa_sprintf_malloc("%s/snd", udev_get_dev_path(u->udev)); -    r = inotify_add_watch(u->inotify_fd, dev_snd, IN_CLOSE_WRITE); +    r = inotify_add_watch(u->inotify_fd, dev_snd, IN_ATTRIB|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF);      pa_xfree(dev_snd);      if (r < 0) { -        pa_log("inotify_add_watch() failed: %s", pa_cstrerror(errno)); +        int saved_errno = errno; + +        pa_close(u->inotify_fd); +        u->inotify_fd = -1; + +        if (saved_errno == ENOENT) { +            pa_log_debug("/dev/snd/ is apparently not existing yet, retrying to create inotify watch later."); +            return 0; +        } + +        if (saved_errno == ENOSPC) { +            pa_log("You apparently ran out of inotify watches, probably because Tracker/Beagle took them all away. " +                   "I wished people would do their homework first and fix inotify before using it for watching whole " +                   "directory trees which is something the current inotify is certainly not useful for. " +                   "Please make sure to drop the Tracker/Beagle guys a line complaining about their broken use of inotify."); +            return 0; +        } + +        pa_log("inotify_add_watch() failed: %s", pa_cstrerror(saved_errno));          return -1;      } @@ -332,6 +598,7 @@ int pa__init(pa_module *m) {      struct udev_enumerate *enumerate = NULL;      struct udev_list_entry *item = NULL, *first = NULL;      int fd; +    pa_bool_t use_tsched = TRUE, ignore_dB = FALSE;      pa_assert(m); @@ -343,13 +610,19 @@ int pa__init(pa_module *m) {      m->userdata = u = pa_xnew0(struct userdata, 1);      u->core = m->core;      u->devices = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); -    u->use_tsched = TRUE;      u->inotify_fd = -1; -    if (pa_modargs_get_value_boolean(ma, "tsched", &u->use_tsched) < 0) { -        pa_log("Failed to parse tsched argument."); +    if (pa_modargs_get_value_boolean(ma, "tsched", &use_tsched) < 0) { +        pa_log("Failed to parse tsched= argument."); +        goto fail; +    } +    u->use_tsched = use_tsched; + +    if (pa_modargs_get_value_boolean(ma, "ignore_dB", &ignore_dB) < 0) { +        pa_log("Failed to parse ignore_dB= argument.");          goto fail;      } +    u->ignore_dB = ignore_dB;      if (!(u->udev = udev_new())) {          pa_log("Failed to initialize udev library."); @@ -402,7 +675,7 @@ int pa__init(pa_module *m) {      udev_enumerate_unref(enumerate); -    pa_log_info("Loaded %u modules.", pa_hashmap_size(u->devices)); +    pa_log_info("Found %u cards.", pa_hashmap_size(u->devices));      pa_modargs_free(ma); | 
