1 /***
2 This file is part of PulseAudio.
3
4 Copyright 2009 Lennart Poettering
5
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.
10
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.
15
16 You should have received a copy of the GNU Lesser General Public License
17 along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
18 ***/
19
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23
24 #include <sys/stat.h>
25 #include <dirent.h>
26 #include <time.h>
27
28 #include <pulse/xmalloc.h>
29
30 #include <pulsecore/module.h>
31 #include <pulsecore/core-util.h>
32 #include <pulsecore/modargs.h>
33 #include <pulsecore/log.h>
34 #include <pulsecore/client.h>
35 #include <pulsecore/conf-parser.h>
36
37 PA_MODULE_AUTHOR("Lennart Poettering");
38 PA_MODULE_DESCRIPTION("Augment the property sets of streams with additional static information");
39 PA_MODULE_VERSION(PACKAGE_VERSION);
40 PA_MODULE_LOAD_ONCE(true);
41
42 #define STAT_INTERVAL 30
43 #define MAX_CACHE_SIZE 50
44
45 static const char* const valid_modargs[] = {
46 NULL
47 };
48
49 struct rule {
50 time_t timestamp;
51 bool good;
52 time_t mtime;
53 char *process_name;
54 char *application_name;
55 char *icon_name;
56 char *role;
57 pa_proplist *proplist;
58 };
59
60 struct userdata {
61 pa_hashmap *cache;
62 pa_hook_slot *client_new_slot, *client_proplist_changed_slot;
63 };
64
rule_free(struct rule * r)65 static void rule_free(struct rule *r) {
66 pa_assert(r);
67
68 pa_xfree(r->process_name);
69 pa_xfree(r->application_name);
70 pa_xfree(r->icon_name);
71 pa_xfree(r->role);
72 if (r->proplist)
73 pa_proplist_free(r->proplist);
74 pa_xfree(r);
75 }
76
parse_properties(pa_config_parser_state * state)77 static int parse_properties(pa_config_parser_state *state) {
78 struct rule *r;
79 pa_proplist *n;
80
81 pa_assert(state);
82
83 r = state->userdata;
84
85 if (!(n = pa_proplist_from_string(state->rvalue)))
86 return -1;
87
88 if (r->proplist) {
89 pa_proplist_update(r->proplist, PA_UPDATE_MERGE, n);
90 pa_proplist_free(n);
91 } else
92 r->proplist = n;
93
94 return 0;
95 }
96
parse_categories(pa_config_parser_state * state)97 static int parse_categories(pa_config_parser_state *state) {
98 struct rule *r;
99 const char *split_state = NULL;
100 char *c;
101
102 pa_assert(state);
103
104 r = state->userdata;
105
106 while ((c = pa_split(state->rvalue, ";", &split_state))) {
107
108 if (pa_streq(c, "Game")) {
109 pa_xfree(r->role);
110 r->role = pa_xstrdup("game");
111 } else if (pa_streq(c, "Telephony")) {
112 pa_xfree(r->role);
113 r->role = pa_xstrdup("phone");
114 }
115
116 pa_xfree(c);
117 }
118
119 return 0;
120 }
121
check_type(pa_config_parser_state * state)122 static int check_type(pa_config_parser_state *state) {
123 pa_assert(state);
124
125 return pa_streq(state->rvalue, "Application") ? 0 : -1;
126 }
127
catch_all(pa_config_parser_state * state)128 static int catch_all(pa_config_parser_state *state) {
129 return 0;
130 }
131
find_desktop_file_in_dir(struct rule * r,const char * desktop_file_dir,struct stat * st)132 static char * find_desktop_file_in_dir(struct rule *r, const char *desktop_file_dir, struct stat *st) {
133 char *fn = NULL;
134
135 pa_assert(st);
136
137 fn = pa_sprintf_malloc("%s" PA_PATH_SEP "%s.desktop", desktop_file_dir, r->process_name);
138 if (stat(fn, st) == 0)
139 return fn;
140
141 pa_xfree(fn);
142
143 #ifdef DT_DIR
144 {
145 DIR *desktopfiles_dir;
146 struct dirent *dir;
147
148 /* Let's try a more aggressive search, but only one level */
149 if ((desktopfiles_dir = opendir(desktop_file_dir))) {
150 while ((dir = readdir(desktopfiles_dir))) {
151 if (dir->d_type != DT_DIR
152 || pa_streq(dir->d_name, ".")
153 || pa_streq(dir->d_name, ".."))
154 continue;
155
156 fn = pa_sprintf_malloc("%s" PA_PATH_SEP "%s" PA_PATH_SEP "%s.desktop", desktop_file_dir, dir->d_name, r->process_name);
157
158 if (stat(fn, st) == 0) {
159 closedir(desktopfiles_dir);
160 return fn;
161 }
162
163 pa_xfree(fn);
164 }
165 closedir(desktopfiles_dir);
166 }
167 }
168 #endif
169
170 return NULL;
171 }
172
update_rule(struct rule * r)173 static void update_rule(struct rule *r) {
174 char *fn = NULL;
175 struct stat st;
176 static pa_config_item table[] = {
177 { "Name", pa_config_parse_string, NULL, "Desktop Entry" },
178 { "Icon", pa_config_parse_string, NULL, "Desktop Entry" },
179 { "Type", check_type, NULL, "Desktop Entry" },
180 { "X-PulseAudio-Properties", parse_properties, NULL, "Desktop Entry" },
181 { "Categories", parse_categories, NULL, "Desktop Entry" },
182 { NULL, catch_all, NULL, NULL },
183 { NULL, NULL, NULL, NULL },
184 };
185 const char *state = NULL;
186 const char *xdg_data_dirs = NULL;
187 char *data_dir = NULL;
188 char *desktop_file_dir = NULL;
189
190 pa_assert(r);
191
192 if ((xdg_data_dirs = getenv("XDG_DATA_DIRS"))) {
193 while ((data_dir = pa_split(xdg_data_dirs, ":", &state))) {
194 desktop_file_dir = pa_sprintf_malloc("%s" PA_PATH_SEP "applications", data_dir);
195
196 pa_xfree(fn);
197 fn = find_desktop_file_in_dir(r, desktop_file_dir, &st);
198
199 pa_xfree(desktop_file_dir);
200 pa_xfree(data_dir);
201
202 if (fn) {
203 break;
204 }
205 }
206 } else {
207 fn = find_desktop_file_in_dir(r, DESKTOPFILEDIR, &st);
208 }
209
210 if (!fn) {
211 r->good = false;
212 return;
213 }
214
215 if (r->good) {
216 if (st.st_mtime == r->mtime) {
217 /* Theoretically the filename could have changed, but if so
218 having the same mtime is very unlikely so not worth tracking it in r */
219 pa_xfree(fn);
220 return;
221 }
222 pa_log_debug("Found %s (which has been updated since we last checked).", fn);
223 } else
224 pa_log_debug("Found %s.", fn);
225
226 r->good = true;
227 r->mtime = st.st_mtime;
228 pa_xfree(r->application_name);
229 pa_xfree(r->icon_name);
230 pa_xfree(r->role);
231 r->application_name = r->icon_name = r->role = NULL;
232 if (r->proplist)
233 pa_proplist_clear(r->proplist);
234
235 table[0].data = &r->application_name;
236 table[1].data = &r->icon_name;
237
238 if (pa_config_parse(fn, NULL, table, NULL, false, r) < 0)
239 pa_log_warn("Failed to parse .desktop file %s.", fn);
240
241 pa_xfree(fn);
242 }
243
apply_rule(struct rule * r,pa_proplist * p)244 static void apply_rule(struct rule *r, pa_proplist *p) {
245 pa_assert(r);
246 pa_assert(p);
247
248 if (!r->good)
249 return;
250
251 if (r->proplist)
252 pa_proplist_update(p, PA_UPDATE_MERGE, r->proplist);
253
254 if (r->icon_name)
255 if (!pa_proplist_contains(p, PA_PROP_APPLICATION_ICON_NAME))
256 pa_proplist_sets(p, PA_PROP_APPLICATION_ICON_NAME, r->icon_name);
257
258 if (r->application_name) {
259 const char *t;
260
261 t = pa_proplist_gets(p, PA_PROP_APPLICATION_NAME);
262
263 if (!t || pa_streq(t, r->process_name))
264 pa_proplist_sets(p, PA_PROP_APPLICATION_NAME, r->application_name);
265 }
266
267 if (r->role)
268 if (!pa_proplist_contains(p, PA_PROP_MEDIA_ROLE))
269 pa_proplist_sets(p, PA_PROP_MEDIA_ROLE, r->role);
270 }
271
make_room(pa_hashmap * cache)272 static void make_room(pa_hashmap *cache) {
273 pa_assert(cache);
274
275 while (pa_hashmap_size(cache) >= MAX_CACHE_SIZE) {
276 struct rule *r;
277
278 pa_assert_se(r = pa_hashmap_steal_first(cache));
279 rule_free(r);
280 }
281 }
282
process(struct userdata * u,pa_proplist * p)283 static pa_hook_result_t process(struct userdata *u, pa_proplist *p) {
284 struct rule *r;
285 time_t now;
286 const char *pn;
287
288 pa_assert(u);
289 pa_assert(p);
290
291 if (!(pn = pa_proplist_gets(p, PA_PROP_APPLICATION_PROCESS_BINARY)))
292 return PA_HOOK_OK;
293
294 if (*pn == '.' || strchr(pn, '/'))
295 return PA_HOOK_OK;
296
297 time(&now);
298
299 pa_log_debug("Looking for .desktop file for %s", pn);
300
301 if ((r = pa_hashmap_get(u->cache, pn))) {
302 if (now-r->timestamp > STAT_INTERVAL) {
303 r->timestamp = now;
304 update_rule(r);
305 }
306 } else {
307 make_room(u->cache);
308
309 r = pa_xnew0(struct rule, 1);
310 r->process_name = pa_xstrdup(pn);
311 r->timestamp = now;
312 pa_hashmap_put(u->cache, r->process_name, r);
313 update_rule(r);
314 }
315
316 apply_rule(r, p);
317 return PA_HOOK_OK;
318 }
319
client_new_cb(pa_core * core,pa_client_new_data * data,struct userdata * u)320 static pa_hook_result_t client_new_cb(pa_core *core, pa_client_new_data *data, struct userdata *u) {
321 pa_core_assert_ref(core);
322 pa_assert(data);
323 pa_assert(u);
324
325 return process(u, data->proplist);
326 }
327
client_proplist_changed_cb(pa_core * core,pa_client * client,struct userdata * u)328 static pa_hook_result_t client_proplist_changed_cb(pa_core *core, pa_client *client, struct userdata *u) {
329 pa_core_assert_ref(core);
330 pa_assert(client);
331 pa_assert(u);
332
333 return process(u, client->proplist);
334 }
335
pa__init(pa_module * m)336 int pa__init(pa_module *m) {
337 pa_modargs *ma = NULL;
338 struct userdata *u;
339
340 pa_assert(m);
341
342 if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
343 pa_log("Failed to parse module arguments");
344 goto fail;
345 }
346
347 m->userdata = u = pa_xnew(struct userdata, 1);
348
349 u->cache = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) rule_free);
350 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);
351 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);
352
353 pa_modargs_free(ma);
354
355 return 0;
356
357 fail:
358 pa__done(m);
359
360 if (ma)
361 pa_modargs_free(ma);
362
363 return -1;
364 }
365
pa__done(pa_module * m)366 void pa__done(pa_module *m) {
367 struct userdata* u;
368
369 pa_assert(m);
370
371 if (!(u = m->userdata))
372 return;
373
374 if (u->client_new_slot)
375 pa_hook_slot_free(u->client_new_slot);
376 if (u->client_proplist_changed_slot)
377 pa_hook_slot_free(u->client_proplist_changed_slot);
378
379 if (u->cache)
380 pa_hashmap_free(u->cache);
381
382 pa_xfree(u);
383 }
384