2 This file is part of PulseAudio.
4 Copyright 2009 Lennart Poettering
6 PulseAudio is free software; you can redistribute it and/or modify
7 it under the terms of the GNU Lesser General Public License as published
8 by the Free Software Foundation; either version 2.1 of the License,
9 or (at your option) any later version.
11 PulseAudio is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU Lesser General Public License
17 along with PulseAudio; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
22 #include <pulsecore/pulsecore-config.h>
28 #if defined(HAVE_REGEX_H)
30 #elif defined(HAVE_PCREPOSIX_H)
31 #include <pcreposix.h>
34 #include <pulse/xmalloc.h>
36 #include <pulsecore/module.h>
37 #include <pulsecore/core-util.h>
38 #include <pulsecore/modargs.h>
39 #include <pulsecore/log.h>
40 #include <pulsecore/client.h>
41 #include <pulsecore/conf-parser.h>
42 #include <pulsecore/hashmap.h>
44 #include "module-augment-properties-symdef.h"
46 PA_MODULE_AUTHOR("Lennart Poettering");
47 PA_MODULE_DESCRIPTION("Augment the property sets of streams with additional static information");
48 PA_MODULE_VERSION(PACKAGE_VERSION);
49 PA_MODULE_LOAD_ONCE(true);
51 #ifndef CONFIG_FILE_DIR
52 #define CONFIG_FILE_DIR "/etc/pulse/augment_property_client_rules"
55 #ifndef SINK_INPUT_RULE_DIR
56 #define SINK_INPUT_RULE_DIR "/etc/pulse/augment_property_sink_input_rules"
59 #define STAT_INTERVAL 30
60 #define MAX_CACHE_SIZE 50
68 static const char* const valid_modargs[] = {
78 char *application_name;
81 pa_proplist *proplist;
84 struct sink_input_rule_file {
89 char *fn; /* for hashmap memory management */
92 struct sink_input_rule_section {
96 char *section_name; /* for hashmap memory management */
101 pa_hook_slot *client_new_slot, *client_proplist_changed_slot, *sink_input_new_slot;
102 pa_hashmap *sink_input_rules;
103 pa_client *directory_watch_client;
106 static void rule_free(struct rule *r) {
109 pa_xfree(r->process_name);
110 pa_xfree(r->application_name);
111 pa_xfree(r->icon_name);
114 pa_proplist_free(r->proplist);
118 static void sink_input_rule_free(struct sink_input_rule_section *s, struct userdata *u) {
121 pa_xfree(s->stream_key);
123 regfree(&s->stream_value);
124 pa_xfree(s->section_name);
129 static void sink_input_rule_file_free(struct sink_input_rule_file *rf, struct userdata *u) {
131 struct sink_input_rule_section *section;
135 PA_HASHMAP_FOREACH(section, rf->rules, state) {
136 pa_xfree(section->stream_key);
138 regfree(§ion->stream_value);
139 pa_xfree(section->section_name);
142 pa_hashmap_free(rf->rules);
144 pa_xfree(rf->client_name);
145 pa_xfree(rf->target_key);
146 pa_xfree(rf->target_value);
152 static int parse_properties(pa_config_parser_state *state) {
156 struct rule *r = state->userdata;
159 if (!(n = pa_proplist_from_string(state->rvalue)))
163 pa_proplist_update(r->proplist, PA_UPDATE_MERGE, n);
171 static int parse_categories(pa_config_parser_state *state) {
174 struct rule *r = state->userdata;
175 const char *s = NULL;
178 while ((c = pa_split(state->rvalue, ";", &s))) {
180 if (pa_streq(c, "Game")) {
182 r->role = pa_xstrdup("game");
183 } else if (pa_streq(c, "Telephony")) {
185 r->role = pa_xstrdup("phone");
194 static int check_type(pa_config_parser_state *state) {
197 return pa_streq(state->rvalue, "Application") ? 0 : -1;
200 static int catch_all(pa_config_parser_state *state) {
205 static void parse_file(struct rule *r, const char *fn, pa_config_item *table, bool first) {
206 char *application_name = NULL;
207 char *icon_name = NULL;
209 pa_proplist *p = NULL;
212 /* clean up before update */
213 pa_xfree(r->application_name);
214 pa_xfree(r->icon_name);
218 pa_proplist_clear(r->proplist);
221 /* keep the old data safe */
222 application_name = r->application_name;
223 icon_name = r->icon_name;
228 r->application_name = r->icon_name = r->role = NULL;
230 table[0].data = &r->application_name;
231 table[1].data = &r->icon_name;
233 if (pa_config_parse(fn, NULL, table, NULL, r) < 0)
234 pa_log_warn("Failed to parse file %s.", fn);
237 /* copy the old data in place if there was no new data, merge the proplist */
239 if (r->application_name)
240 pa_xfree(application_name);
242 r->application_name = application_name;
247 r->icon_name = icon_name;
256 pa_proplist_update(r->proplist, PA_UPDATE_MERGE, p);
257 pa_proplist_clear(p);
266 static void update_rule(struct rule *r) {
269 static pa_config_item table[] = {
270 { "Name", pa_config_parse_string, NULL, "Desktop Entry" },
271 { "Icon", pa_config_parse_string, NULL, "Desktop Entry" },
272 { "Type", check_type, NULL, "Desktop Entry" },
273 { "X-PulseAudio-Properties", parse_properties, NULL, "Desktop Entry" },
274 { "Categories", parse_categories, NULL, "Desktop Entry" },
275 { NULL, catch_all, NULL, NULL },
276 { NULL, NULL, NULL, NULL },
282 /* Check first the non-graphical applications configuration file. If a
283 file corresponding to the process isn't found, go and check the desktop
284 files. If a file is found, augment it with the desktop data anyway. */
286 fn = pa_sprintf_malloc(CONFIG_FILE_DIR PA_PATH_SEP "%s.conf", r->process_name);
288 pa_log_debug("Looking for file %s", fn);
290 if (stat(fn, &st) == 0)
296 if (found && !(r->good && st.st_mtime == r->conf_mtime)) {
297 /* Theoretically the filename could have changed, but if so
298 having the same mtime is very unlikely so not worth tracking it in r */
300 pa_log_debug("Found %s (which has been updated since we last checked).", fn);
302 pa_log_debug("Found %s.", fn);
304 parse_file(r, fn, table, true);
305 r->conf_mtime = st.st_mtime;
312 fn = pa_sprintf_malloc(DESKTOPFILEDIR PA_PATH_SEP "%s.desktop", r->process_name);
314 pa_log_debug("Looking for file %s", fn);
316 if (stat(fn, &st) == 0)
320 DIR *desktopfiles_dir;
323 /* Let's try a more aggressive search, but only one level */
324 if ((desktopfiles_dir = opendir(DESKTOPFILEDIR))) {
325 while ((dir = readdir(desktopfiles_dir))) {
326 if (dir->d_type != DT_DIR
327 || strcmp(dir->d_name, ".") == 0
328 || strcmp(dir->d_name, "..") == 0)
332 fn = pa_sprintf_malloc(DESKTOPFILEDIR PA_PATH_SEP "%s" PA_PATH_SEP "%s.desktop", dir->d_name, r->process_name);
334 if (stat(fn, &st) == 0) {
339 closedir(desktopfiles_dir);
350 if (st.st_mtime == r->desktop_mtime) {
351 /* Theoretically the filename could have changed, but if so
352 having the same mtime is very unlikely so not worth tracking it in r */
356 pa_log_debug("Found %s (which has been updated since we last checked).", fn);
358 pa_log_debug("Found %s.", fn);
360 parse_file(r, fn, table, false);
361 r->desktop_mtime = st.st_mtime;
368 static void apply_rule(struct rule *r, pa_proplist *p) {
376 pa_proplist_update(p, PA_UPDATE_MERGE, r->proplist);
379 if (!pa_proplist_contains(p, PA_PROP_APPLICATION_ICON_NAME))
380 pa_proplist_sets(p, PA_PROP_APPLICATION_ICON_NAME, r->icon_name);
382 if (r->application_name) {
385 t = pa_proplist_gets(p, PA_PROP_APPLICATION_NAME);
387 if (!t || pa_streq(t, r->process_name))
388 pa_proplist_sets(p, PA_PROP_APPLICATION_NAME, r->application_name);
392 if (!pa_proplist_contains(p, PA_PROP_MEDIA_ROLE))
393 pa_proplist_sets(p, PA_PROP_MEDIA_ROLE, r->role);
396 static void make_room(pa_hashmap *cache) {
399 while (pa_hashmap_size(cache) >= MAX_CACHE_SIZE) {
402 pa_assert_se(r = pa_hashmap_steal_first(cache));
407 static pa_hook_result_t process(struct userdata *u, pa_proplist *p) {
416 if (!(pn = pa_proplist_gets(p, PA_PROP_APPLICATION_PROCESS_BINARY)))
419 if (*pn == '.' || strchr(pn, '/'))
424 pa_log_debug("Looking for configuration file for %s", pn);
426 if ((r = pa_hashmap_get(u->cache, pn))) {
427 if (now-r->timestamp > STAT_INTERVAL) {
434 r = pa_xnew0(struct rule, 1);
435 r->process_name = pa_xstrdup(pn);
437 pa_hashmap_put(u->cache, (void *)r->process_name, r);
445 static pa_hook_result_t client_new_cb(pa_core *core, pa_client_new_data *data, struct userdata *u) {
446 pa_core_assert_ref(core);
450 return process(u, data->proplist);
453 static pa_hook_result_t client_proplist_changed_cb(pa_core *core, pa_client *client, struct userdata *u) {
454 pa_core_assert_ref(core);
458 return process(u, client->proplist);
461 static char **filter_by_client(pa_hashmap *h, pa_client *c) {
463 char **possible = pa_xnew0(char *, pa_hashmap_size(h)+1);
464 const char *process_binary;
467 const void *key = NULL;
469 if (!c || !c->proplist) {
474 process_binary = pa_proplist_gets(c->proplist, PA_PROP_APPLICATION_PROCESS_BINARY);
478 while (pa_hashmap_iterate(h, &state, &key)) {
479 struct sink_input_rule_file *rf = pa_hashmap_get(h, key);
483 if (!rf->client_name ||
484 (process_binary && strcmp(process_binary, rf->client_name) == 0)) {
485 *iter = pa_xstrdup((const char *) key);
493 static pa_hook_result_t process_sink_input(
499 const void *fn = NULL;
500 const char *prop = NULL;
501 struct sink_input_rule_file *rf;
502 char **possible = NULL, **iter;
505 possible = filter_by_client(u->sink_input_rules, client);
510 /* size of possible must be at least one, or otherwise we wouldn't be here */
512 if (possible[0] == NULL) {
516 valid = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
523 pa_hashmap_put(valid, (void *) *iter, (void *) RULE_UNDEFINED);
527 prop = pa_proplist_iterate(p, &state);
531 const char *value = pa_proplist_gets(p, prop);
537 enum rule_match status = (enum rule_match) pa_hashmap_get(valid, *iter);
539 if (status == RULE_MISS) {
544 rf = pa_hashmap_get(u->sink_input_rules, *iter);
547 void *section_state = NULL;
548 const void *section = NULL;
549 while (pa_hashmap_iterate(rf->rules, §ion_state, §ion)) {
550 struct sink_input_rule_section *s = pa_hashmap_get(rf->rules, section);
553 pa_log_error("no section associated with %s", (const char *) section);
556 if (strcmp(prop, s->stream_key) == 0) {
558 if (value && !regexec(&s->stream_value, value, 0, NULL, 0)) {
560 pa_log_debug("hit %s", *iter);
562 if (status == RULE_UNDEFINED) {
563 pa_hashmap_remove(valid, *iter);
564 pa_hashmap_put(valid, (void *) *iter, (void *) RULE_HIT);
568 /* miss, no more processing for this rule file*/
569 pa_hashmap_remove(valid, *iter);
570 pa_hashmap_put(valid, (void *) *iter, (void *) RULE_MISS);
571 pa_log_debug("miss %s", *iter);
579 prop = pa_proplist_iterate(p, &state);
582 /* go do the changes for the rule files that actually were matching */
585 while (pa_hashmap_iterate(valid, &state, &fn)) {
586 enum rule_match status = (enum rule_match) pa_hashmap_get(valid, fn);
587 if (status == RULE_HIT) {
588 pa_log_debug("rule hit: %s", (const char *) fn);
589 rf = pa_hashmap_get(u->sink_input_rules, (const char *) fn);
590 pa_proplist_sets(p, rf->target_key, rf->target_value);
592 else if (status == RULE_MISS) {
593 pa_log_debug("rule miss: %s", (const char *) fn);
595 else if (status == RULE_UNDEFINED) {
596 pa_log_debug("rule undefined: %s", (const char *) fn);
599 pa_log_error("memory corruption for %s (%i)", (const char *) fn, status);
603 pa_hashmap_free(valid);
617 static pa_hook_result_t sink_input_new_cb(
619 pa_sink_input_new_data *new_data,
620 struct userdata *u) {
622 pa_core_assert_ref(core);
626 if (!new_data->client)
629 if (!u->sink_input_rules)
632 return process_sink_input(u, new_data->proplist, new_data->client);
635 static int parse_rule_sections(pa_config_parser_state *state) {
637 struct sink_input_rule_file **rfp;
638 struct sink_input_rule_file *rf;
639 struct sink_input_rule_section *s;
645 s = pa_hashmap_get(rf->rules, state->section);
648 s = pa_xnew0(struct sink_input_rule_section, 1);
651 /* add key to the struct for later freeing */
652 s->section_name = pa_xstrdup(state->section);
653 pa_hashmap_put(rf->rules, (void *)s->section_name, s);
656 if (strcmp(state->lvalue, "prop_key") == 0) {
658 pa_xfree(s->stream_key);
659 s->stream_key = pa_xstrdup(state->rvalue);
661 else if (strcmp(state->lvalue, "prop_value") == 0) {
664 regfree(&s->stream_value);
666 ret = regcomp(&s->stream_value, state->rvalue, REG_EXTENDED|REG_NOSUB);
671 regerror(ret, &s->stream_value, errbuf, 256);
672 pa_log_error("Failed compiling regular expression: %s", errbuf);
679 static bool validate_sink_input_rule(struct sink_input_rule_file *rf) {
682 struct sink_input_rule_section *s;
684 if (!rf->target_key || !rf->target_value) {
685 pa_log_error("No result condition listed for rule file");
686 /* no result condition listed, so no point in using this rule file */
690 PA_HASHMAP_FOREACH(s, rf->rules, state) {
691 if (!s->stream_key || s->comp == false) {
692 pa_log_error("Incomplete rule section [%s] in rule file", s->section_name);
700 static pa_hashmap *update_sink_input_rules() {
703 DIR *sinkinputrulefiles_dir;
704 pa_hashmap *rules = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
706 sinkinputrulefiles_dir = opendir(SINK_INPUT_RULE_DIR);
708 if (!sinkinputrulefiles_dir)
711 while ((file = readdir(sinkinputrulefiles_dir))) {
713 if (file->d_type == DT_REG) {
715 struct sink_input_rule_file *rf = pa_xnew0(struct sink_input_rule_file, 1);
719 pa_config_item table[] = {
720 { "target_key", pa_config_parse_string, &rf->target_key, "result" },
721 { "target_value", pa_config_parse_string, &rf->target_value, "result" },
722 { "client_name", pa_config_parse_string, &rf->client_name, "general" },
723 { NULL, parse_rule_sections, &rf, NULL },
724 { NULL, NULL, NULL, NULL },
727 rf->rules = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
728 rf->fn = pa_sprintf_malloc(SINK_INPUT_RULE_DIR PA_PATH_SEP "%s", file->d_name);
730 if (pa_config_parse(rf->fn, NULL, table, NULL, rf) >= 0) {
732 pa_log_info("Successfully parsed sink input conf file %s", file->d_name);
734 if (!validate_sink_input_rule(rf)) {
735 pa_log_error("validating rule file failed");
736 sink_input_rule_file_free(rf, NULL);
740 pa_log_info("adding filename %s to %p", rf->fn, rf);
741 pa_hashmap_put(rules, (void *)rf->fn, rf);
747 closedir(sinkinputrulefiles_dir);
749 if (pa_hashmap_isempty(rules)) {
750 pa_hashmap_free(rules);
757 static void send_event(pa_client *c, const char *evt, pa_proplist *d) {
759 struct userdata *u = c->userdata;
761 const char *action = pa_proplist_gets(d, "action");
762 const char *dir = pa_proplist_gets(d, "directory");
763 const char *file = pa_proplist_gets(d, "file");
765 pa_log_debug("received event '%s': action: %s, dir: %s, file: %s", evt, action, dir, file);
767 /* update the rules */
768 pa_xfree(u->sink_input_rules);
769 u->sink_input_rules = update_sink_input_rules();
772 static pa_client *create_directory_watch_client(pa_module *m, const char *directory, struct userdata *u) {
773 pa_client_new_data data;
776 /* Create a virtual client for triggering the directory callbacks. The
777 protocol is a property list. */
779 pa_client_new_data_init(&data);
781 data.driver = __FILE__;
782 pa_proplist_sets(data.proplist, PA_PROP_APPLICATION_NAME, "Directory watch client");
783 pa_proplist_sets(data.proplist, "type", "directory-watch");
784 pa_proplist_sets(data.proplist, "dir-watch.directory", directory);
786 c = pa_client_new(m->core, &data);
788 pa_client_new_data_done(&data);
791 pa_log_error("Failed to create directory watch client");
795 c->send_event = send_event;
801 int pa__init(pa_module *m) {
802 pa_modargs *ma = NULL;
807 if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
808 pa_log("Failed to parse module arguments");
812 m->userdata = u = pa_xnew(struct userdata, 1);
814 u->sink_input_rules = update_sink_input_rules();
816 u->cache = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func);
818 u->directory_watch_client = create_directory_watch_client(m, SINK_INPUT_RULE_DIR, u);
819 if (!u->directory_watch_client)
822 u->client_new_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CLIENT_NEW], PA_HOOK_EARLY, (pa_hook_cb_t) client_new_cb, u);
823 u->client_proplist_changed_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_CLIENT_PROPLIST_CHANGED], PA_HOOK_EARLY, (pa_hook_cb_t) client_proplist_changed_cb, u);
824 u->sink_input_new_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_NEW], PA_HOOK_EARLY, (pa_hook_cb_t) sink_input_new_cb, u);
840 void pa__done(pa_module *m) {
842 struct sink_input_rule_file *rf;
847 if (!(u = m->userdata))
850 if (u->client_new_slot)
851 pa_hook_slot_free(u->client_new_slot);
852 if (u->client_proplist_changed_slot)
853 pa_hook_slot_free(u->client_proplist_changed_slot);
854 if (u->sink_input_new_slot)
855 pa_hook_slot_free(u->sink_input_new_slot);
860 while ((r = pa_hashmap_steal_first(u->cache)))
863 pa_hashmap_free(u->cache);
866 if (u->sink_input_rules) {
868 PA_HASHMAP_FOREACH(rf, u->sink_input_rules, state) {
869 sink_input_rule_file_free(rf, NULL);
872 pa_hashmap_free(u->sink_input_rules);
875 if (u->directory_watch_client)
876 pa_client_free(u->directory_watch_client);