pulseaudio/src/modules/module-device-manager.c
Colin Guthrie aebe4787f2 device-manager: Provide a method for prefering/defering a device.
This allows clients to edit the priroity order. What is not yet in place is the initialisation of that priority list
when new devices are detected or the cleaning (remove holes) when devices are removed.

In order to keep the storage transparent I will likely remove the write functionality and replace it with a
simple rename method.

I also still need to expose the priority itself when reading the data.
2009-10-01 09:08:29 +01:00

808 lines
22 KiB
C

/***
This file is part of PulseAudio.
Copyright 2006-2008 Lennart Poettering
Copyright 2009 Colin Guthrie
PulseAudio is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation; either version 2.1 of the License,
or (at your option) any later version.
PulseAudio is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with PulseAudio; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA.
***/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <pulse/xmalloc.h>
#include <pulse/volume.h>
#include <pulse/timeval.h>
#include <pulse/util.h>
#include <pulse/rtclock.h>
#include <pulsecore/core-error.h>
#include <pulsecore/module.h>
#include <pulsecore/core-util.h>
#include <pulsecore/modargs.h>
#include <pulsecore/log.h>
#include <pulsecore/core-subscribe.h>
#include <pulsecore/sink-input.h>
#include <pulsecore/source-output.h>
#include <pulsecore/namereg.h>
#include <pulsecore/protocol-native.h>
#include <pulsecore/pstream.h>
#include <pulsecore/pstream-util.h>
#include <pulsecore/database.h>
#include "module-device-manager-symdef.h"
PA_MODULE_AUTHOR("Colin Guthrie");
PA_MODULE_DESCRIPTION("Keep track of devices (and their descriptions) both past and present");
PA_MODULE_VERSION(PACKAGE_VERSION);
PA_MODULE_LOAD_ONCE(TRUE);
PA_MODULE_USAGE("This module does not take any arguments");
#define SAVE_INTERVAL (10 * PA_USEC_PER_SEC)
static const char* const valid_modargs[] = {
NULL
};
struct userdata {
pa_core *core;
pa_module *module;
pa_subscription *subscription;
pa_hook_slot
*sink_new_hook_slot,
*source_new_hook_slot,
*connection_unlink_hook_slot;
pa_time_event *save_time_event;
pa_database *database;
pa_native_protocol *protocol;
pa_idxset *subscribed;
pa_bool_t role_device_priority_routing;
pa_bool_t stream_restore_used;
pa_bool_t checked_stream_restore;
};
#define ENTRY_VERSION 1
#define NUM_ROLES 9
enum {
ROLE_NONE,
ROLE_VIDEO,
ROLE_MUSIC,
ROLE_GAME,
ROLE_EVENT,
ROLE_PHONE,
ROLE_ANIMATION,
ROLE_PRODUCTION,
ROLE_A11Y,
};
struct entry {
uint8_t version;
char description[PA_NAME_MAX];
uint32_t priority[NUM_ROLES];
} PA_GCC_PACKED;
enum {
SUBCOMMAND_TEST,
SUBCOMMAND_READ,
SUBCOMMAND_WRITE,
SUBCOMMAND_DELETE,
SUBCOMMAND_ROLE_DEVICE_PRIORITY_ROUTING,
SUBCOMMAND_PREFER_DEVICE,
SUBCOMMAND_DEFER_DEVICE,
SUBCOMMAND_SUBSCRIBE,
SUBCOMMAND_EVENT
};
static void save_time_callback(pa_mainloop_api*a, pa_time_event* e, const struct timeval *t, void *userdata) {
struct userdata *u = userdata;
pa_assert(a);
pa_assert(e);
pa_assert(u);
pa_assert(e == u->save_time_event);
u->core->mainloop->time_free(u->save_time_event);
u->save_time_event = NULL;
pa_database_sync(u->database);
pa_log_info("Synced.");
}
static struct entry* read_entry(struct userdata *u, const char *name) {
pa_datum key, data;
struct entry *e;
pa_assert(u);
pa_assert(name);
key.data = (char*) name;
key.size = strlen(name);
pa_zero(data);
if (!pa_database_get(u->database, &key, &data))
goto fail;
if (data.size != sizeof(struct entry)) {
pa_log_debug("Database contains entry for device %s of wrong size %lu != %lu. Probably due to upgrade, ignoring.", name, (unsigned long) data.size, (unsigned long) sizeof(struct entry));
goto fail;
}
e = (struct entry*) data.data;
if (e->version != ENTRY_VERSION) {
pa_log_debug("Version of database entry for device %s doesn't match our version. Probably due to upgrade, ignoring.", name);
goto fail;
}
if (!memchr(e->description, 0, sizeof(e->description))) {
pa_log_warn("Database contains entry for device %s with missing NUL byte in description", name);
goto fail;
}
return e;
fail:
pa_datum_free(&data);
return NULL;
}
static void trigger_save(struct userdata *u) {
if (u->save_time_event)
return;
u->save_time_event = pa_core_rttime_new(u->core, pa_rtclock_now() + SAVE_INTERVAL, save_time_callback, u);
}
static pa_bool_t entries_equal(const struct entry *a, const struct entry *b) {
if (strncmp(a->description, b->description, sizeof(a->description)))
return FALSE;
return TRUE;
}
static void subscribe_callback(pa_core *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) {
struct userdata *u = userdata;
struct entry entry, *old = NULL;
char *name = NULL;
pa_datum key, data;
pa_assert(c);
pa_assert(u);
if (t != (PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_NEW) &&
t != (PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_CHANGE) &&
t != (PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_NEW) &&
t != (PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_CHANGE))
return;
pa_zero(entry);
entry.version = ENTRY_VERSION;
if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK) {
pa_sink *sink;
if (!(sink = pa_idxset_get_by_index(c->sinks, idx)))
return;
name = pa_sprintf_malloc("sink:%s", sink->name);
if ((old = read_entry(u, name)))
entry = *old;
pa_strlcpy(entry.description, pa_strnull(pa_proplist_gets(sink->proplist, PA_PROP_DEVICE_DESCRIPTION)), sizeof(entry.description));
} else {
pa_source *source;
pa_assert((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SOURCE);
if (!(source = pa_idxset_get_by_index(c->sources, idx)))
return;
if (source->monitor_of)
return;
name = pa_sprintf_malloc("source:%s", source->name);
if ((old = read_entry(u, name)))
entry = *old;
pa_strlcpy(entry.description, pa_strnull(pa_proplist_gets(source->proplist, PA_PROP_DEVICE_DESCRIPTION)), sizeof(entry.description));
}
if (old) {
if (entries_equal(old, &entry)) {
pa_xfree(old);
pa_xfree(name);
return;
}
pa_xfree(old);
}
key.data = name;
key.size = strlen(name);
data.data = &entry;
data.size = sizeof(entry);
pa_log_info("Storing device description for %s.", name);
pa_database_set(u->database, &key, &data, TRUE);
pa_xfree(name);
trigger_save(u);
}
static pa_hook_result_t sink_new_hook_callback(pa_core *c, pa_sink_new_data *new_data, struct userdata *u) {
char *name;
struct entry *e;
pa_assert(c);
pa_assert(new_data);
pa_assert(u);
name = pa_sprintf_malloc("sink:%s", new_data->name);
if ((e = read_entry(u, name))) {
if (strncmp(e->description, pa_proplist_gets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION), sizeof(e->description)) != 0) {
pa_log_info("Restoring description for sink %s.", new_data->name);
pa_proplist_sets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION, e->description);
}
pa_xfree(e);
}
pa_xfree(name);
return PA_HOOK_OK;
}
static pa_hook_result_t source_new_hook_callback(pa_core *c, pa_source_new_data *new_data, struct userdata *u) {
char *name;
struct entry *e;
pa_assert(c);
pa_assert(new_data);
pa_assert(u);
name = pa_sprintf_malloc("source:%s", new_data->name);
if ((e = read_entry(u, name))) {
if (strncmp(e->description, pa_proplist_gets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION), sizeof(e->description)) != 0) {
/* NB, We cannot detect if we are a monitor here... this could mess things up a bit... */
pa_log_info("Restoring description for sink %s.", new_data->name);
pa_proplist_sets(new_data->proplist, PA_PROP_DEVICE_DESCRIPTION, e->description);
}
pa_xfree(e);
}
pa_xfree(name);
return PA_HOOK_OK;
}
static char *get_name(const char *key, const char *prefix) {
char *t;
if (strncmp(key, prefix, strlen(prefix)))
return NULL;
t = pa_xstrdup(key + strlen(prefix));
return t;
}
static void apply_entry(struct userdata *u, const char *name, struct entry *e) {
pa_sink *sink;
pa_source *source;
uint32_t idx;
char *n;
pa_assert(u);
pa_assert(name);
pa_assert(e);
if ((n = get_name(name, "sink:"))) {
for (sink = pa_idxset_first(u->core->sinks, &idx); sink; sink = pa_idxset_next(u->core->sinks, &idx)) {
if (!pa_streq(sink->name, n)) {
continue;
}
pa_log_info("Setting description for sink %s.", sink->name);
pa_sink_set_description(sink, e->description);
}
pa_xfree(n);
}
else if ((n = get_name(name, "source:"))) {
for (source = pa_idxset_first(u->core->sources, &idx); source; source = pa_idxset_next(u->core->sources, &idx)) {
if (!pa_streq(source->name, n)) {
continue;
}
if (source->monitor_of) {
pa_log_warn("Cowardly refusing to set the description for monitor source %s.", source->name);
continue;
}
pa_log_info("Setting description for source %s.", source->name);
pa_source_set_description(source, e->description);
}
pa_xfree(n);
}
}
static uint32_t get_role_index(const char* role) {
pa_assert(role);
if (strcmp(role, "") == 0)
return ROLE_NONE;
if (strcmp(role, "video") == 0)
return ROLE_VIDEO;
if (strcmp(role, "music") == 0)
return ROLE_MUSIC;
if (strcmp(role, "game") == 0)
return ROLE_GAME;
if (strcmp(role, "event") == 0)
return ROLE_EVENT;
if (strcmp(role, "phone") == 0)
return ROLE_PHONE;
if (strcmp(role, "animation") == 0)
return ROLE_ANIMATION;
if (strcmp(role, "production") == 0)
return ROLE_PRODUCTION;
if (strcmp(role, "a11y") == 0)
return ROLE_A11Y;
return PA_INVALID_INDEX;
}
#define EXT_VERSION 1
static int extension_cb(pa_native_protocol *p, pa_module *m, pa_native_connection *c, uint32_t tag, pa_tagstruct *t) {
struct userdata *u;
uint32_t command;
pa_tagstruct *reply = NULL;
pa_assert(p);
pa_assert(m);
pa_assert(c);
pa_assert(t);
u = m->userdata;
if (pa_tagstruct_getu32(t, &command) < 0)
goto fail;
reply = pa_tagstruct_new(NULL, 0);
pa_tagstruct_putu32(reply, PA_COMMAND_REPLY);
pa_tagstruct_putu32(reply, tag);
switch (command) {
case SUBCOMMAND_TEST: {
if (!pa_tagstruct_eof(t))
goto fail;
pa_tagstruct_putu32(reply, EXT_VERSION);
break;
}
case SUBCOMMAND_READ: {
pa_datum key;
pa_bool_t done;
if (!pa_tagstruct_eof(t))
goto fail;
done = !pa_database_first(u->database, &key, NULL);
while (!done) {
pa_datum next_key;
struct entry *e;
char *name;
done = !pa_database_next(u->database, &key, &next_key, NULL);
name = pa_xstrndup(key.data, key.size);
pa_datum_free(&key);
if ((e = read_entry(u, name))) {
pa_tagstruct_puts(reply, name);
pa_tagstruct_puts(reply, e->description);
pa_xfree(e);
}
pa_xfree(name);
key = next_key;
}
break;
}
case SUBCOMMAND_WRITE: {
uint32_t mode;
pa_bool_t apply_immediately = FALSE;
if (pa_tagstruct_getu32(t, &mode) < 0 ||
pa_tagstruct_get_boolean(t, &apply_immediately) < 0)
goto fail;
if (mode != PA_UPDATE_MERGE &&
mode != PA_UPDATE_REPLACE &&
mode != PA_UPDATE_SET)
goto fail;
if (mode == PA_UPDATE_SET)
pa_database_clear(u->database);
while (!pa_tagstruct_eof(t)) {
const char *name, *description;
struct entry entry;
pa_datum key, data;
pa_zero(entry);
entry.version = ENTRY_VERSION;
if (pa_tagstruct_gets(t, &name) < 0 ||
pa_tagstruct_gets(t, &description) < 0)
goto fail;
if (!name || !*name)
goto fail;
pa_strlcpy(entry.description, description, sizeof(entry.description));
key.data = (char*) name;
key.size = strlen(name);
data.data = &entry;
data.size = sizeof(entry);
if (pa_database_set(u->database, &key, &data, mode == PA_UPDATE_REPLACE) == 0)
if (apply_immediately)
apply_entry(u, name, &entry);
}
trigger_save(u);
break;
}
case SUBCOMMAND_DELETE:
while (!pa_tagstruct_eof(t)) {
const char *name;
pa_datum key;
if (pa_tagstruct_gets(t, &name) < 0)
goto fail;
key.data = (char*) name;
key.size = strlen(name);
pa_database_unset(u->database, &key);
}
trigger_save(u);
break;
case SUBCOMMAND_ROLE_DEVICE_PRIORITY_ROUTING: {
pa_bool_t enable;
uint32_t sridx = PA_INVALID_INDEX;
uint32_t idx;
pa_module *module;
if (pa_tagstruct_get_boolean(t, &enable) < 0)
goto fail;
/* If this is the first run, check for stream restore module */
if (!u->checked_stream_restore) {
u->checked_stream_restore = TRUE;
for (module = pa_idxset_first(u->core->modules, &idx); module; module = pa_idxset_next(u->core->modules, &idx)) {
if (strcmp(module->name, "module-stream-restore") == 0) {
pa_log_debug("Detected module-stream-restore is currently in use");
u->stream_restore_used = TRUE;
sridx = module->index;
}
}
}
u->role_device_priority_routing = enable;
if (enable) {
if (u->stream_restore_used) {
if (PA_INVALID_INDEX == sridx) {
/* As a shortcut on first load, we have sridx filled in, but otherwise we search for it. */
for (module = pa_idxset_first(u->core->modules, &idx); module; module = pa_idxset_next(u->core->modules, &idx)) {
if (strcmp(module->name, "module-stream-restore") == 0) {
sridx = module->index;
}
}
}
if (PA_INVALID_INDEX != sridx) {
pa_log_debug("Unloading module-stream-restore to enable role-based device-priority routing");
pa_module_unload_request_by_index(u->core, sridx, TRUE);
}
}
} else if (u->stream_restore_used) {
/* We want to reload module-stream-restore */
if (!pa_module_load(u->core, "module-stream-restore", ""))
pa_log_warn("Failed to load module-stream-restore while disabling role-based device-priority routing");
}
break;
}
case SUBCOMMAND_PREFER_DEVICE:
case SUBCOMMAND_DEFER_DEVICE: {
const char *role, *device;
struct entry *e;
uint32_t role_index;
if (pa_tagstruct_gets(t, &role) < 0 ||
pa_tagstruct_gets(t, &device) < 0)
goto fail;
if (!role || !device || !*device)
goto fail;
role_index = get_role_index(role);
if (PA_INVALID_INDEX == role_index)
goto fail;
if ((e = read_entry(u, device)) && ENTRY_VERSION == e->version) {
pa_datum key;
pa_datum data;
pa_bool_t done;
char* prefix;
uint32_t priority;
pa_bool_t haschanged = FALSE;
if (strncmp(device, "sink:", 5) == 0)
prefix = pa_xstrdup("sink:");
else
prefix = pa_xstrdup("source:");
priority = e->priority[role_index];
/* Now we need to load up all the other entries of this type and shuffle the priroities around */
done = !pa_database_first(u->database, &key, NULL);
while (!done && !haschanged) {
pa_datum next_key;
done = !pa_database_next(u->database, &key, &next_key, NULL);
/* Only read devices with the right prefix */
if (key.size > strlen(prefix) && strncmp(key.data, prefix, strlen(prefix)) == 0) {
char *name;
struct entry *e2;
name = pa_xstrndup(key.data, key.size);
pa_datum_free(&key);
if ((e2 = read_entry(u, name))) {
if (SUBCOMMAND_PREFER_DEVICE == command) {
/* PREFER */
if (e2->priority[role_index] == (priority - 1)) {
e2->priority[role_index]++;
haschanged = TRUE;
}
} else {
/* DEFER */
if (e2->priority[role_index] == (priority + 1)) {
e2->priority[role_index]--;
haschanged = TRUE;
}
}
if (haschanged) {
data.data = e2;
data.size = sizeof(*e2);
if (pa_database_set(u->database, &key, &data, FALSE))
pa_log_warn("Could not save device");
}
pa_xfree(e2);
}
pa_xfree(name);
}
key = next_key;
}
/* Now write out our actual entry */
if (haschanged) {
if (SUBCOMMAND_PREFER_DEVICE == command)
e->priority[role_index]--;
else
e->priority[role_index]++;
key.data = (char *) device;
key.size = strlen(device);
data.data = e;
data.size = sizeof(*e);
if (pa_database_set(u->database, &key, &data, FALSE))
pa_log_warn("Could not save device");
trigger_save(u);
}
pa_xfree(e);
pa_xfree(prefix);
}
break;
}
case SUBCOMMAND_SUBSCRIBE: {
pa_bool_t enabled;
if (pa_tagstruct_get_boolean(t, &enabled) < 0 ||
!pa_tagstruct_eof(t))
goto fail;
if (enabled)
pa_idxset_put(u->subscribed, c, NULL);
else
pa_idxset_remove_by_data(u->subscribed, c, NULL);
break;
}
default:
goto fail;
}
pa_pstream_send_tagstruct(pa_native_connection_get_pstream(c), reply);
return 0;
fail:
if (reply)
pa_tagstruct_free(reply);
return -1;
}
static pa_hook_result_t connection_unlink_hook_cb(pa_native_protocol *p, pa_native_connection *c, struct userdata *u) {
pa_assert(p);
pa_assert(c);
pa_assert(u);
pa_idxset_remove_by_data(u->subscribed, c, NULL);
return PA_HOOK_OK;
}
int pa__init(pa_module*m) {
pa_modargs *ma = NULL;
struct userdata *u;
char *fname;
pa_sink *sink;
pa_source *source;
uint32_t idx;
pa_assert(m);
if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
pa_log("Failed to parse module arguments");
goto fail;
}
m->userdata = u = pa_xnew0(struct userdata, 1);
u->core = m->core;
u->module = m;
u->subscribed = pa_idxset_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
u->protocol = pa_native_protocol_get(m->core);
pa_native_protocol_install_ext(u->protocol, m, extension_cb);
u->connection_unlink_hook_slot = pa_hook_connect(&pa_native_protocol_hooks(u->protocol)[PA_NATIVE_HOOK_CONNECTION_UNLINK], PA_HOOK_NORMAL, (pa_hook_cb_t) connection_unlink_hook_cb, u);
u->subscription = pa_subscription_new(m->core, PA_SUBSCRIPTION_MASK_SINK|PA_SUBSCRIPTION_MASK_SOURCE, subscribe_callback, u);
u->sink_new_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_NEW], PA_HOOK_EARLY, (pa_hook_cb_t) sink_new_hook_callback, u);
u->source_new_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_NEW], PA_HOOK_EARLY, (pa_hook_cb_t) source_new_hook_callback, u);
if (!(fname = pa_state_path("device-manager", TRUE)))
goto fail;
if (!(u->database = pa_database_open(fname, TRUE))) {
pa_log("Failed to open volume database '%s': %s", fname, pa_cstrerror(errno));
pa_xfree(fname);
goto fail;
}
pa_log_info("Sucessfully opened database file '%s'.", fname);
pa_xfree(fname);
for (sink = pa_idxset_first(m->core->sinks, &idx); sink; sink = pa_idxset_next(m->core->sinks, &idx))
subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SINK|PA_SUBSCRIPTION_EVENT_NEW, sink->index, u);
for (source = pa_idxset_first(m->core->sources, &idx); source; source = pa_idxset_next(m->core->sources, &idx))
subscribe_callback(m->core, PA_SUBSCRIPTION_EVENT_SOURCE|PA_SUBSCRIPTION_EVENT_NEW, source->index, u);
pa_modargs_free(ma);
return 0;
fail:
pa__done(m);
if (ma)
pa_modargs_free(ma);
return -1;
}
void pa__done(pa_module*m) {
struct userdata* u;
pa_assert(m);
if (!(u = m->userdata))
return;
if (u->subscription)
pa_subscription_free(u->subscription);
if (u->sink_new_hook_slot)
pa_hook_slot_free(u->sink_new_hook_slot);
if (u->source_new_hook_slot)
pa_hook_slot_free(u->source_new_hook_slot);
if (u->save_time_event)
u->core->mainloop->time_free(u->save_time_event);
if (u->database)
pa_database_close(u->database);
if (u->protocol) {
pa_native_protocol_remove_ext(u->protocol, m);
pa_native_protocol_unref(u->protocol);
}
if (u->subscribed)
pa_idxset_free(u->subscribed, NULL, NULL);
pa_xfree(u);
}