1/***
2    This file is part of PulseAudio.
3
4    Copyright 2008 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 <stdio.h>
25#include <unistd.h>
26#include <stdlib.h>
27#include <errno.h>
28#include <stdlib.h>
29#include <sys/types.h>
30
31#include <pulse/xmalloc.h>
32
33#include <pulsecore/module.h>
34#include <pulsecore/log.h>
35#include <pulsecore/hashmap.h>
36#include <pulsecore/idxset.h>
37#include <pulsecore/modargs.h>
38#include <pulsecore/dbus-shared.h>
39
40PA_MODULE_AUTHOR("Lennart Poettering");
41PA_MODULE_DESCRIPTION("Create a client for each ConsoleKit session of this user");
42PA_MODULE_VERSION(PACKAGE_VERSION);
43PA_MODULE_LOAD_ONCE(true);
44
45static const char* const valid_modargs[] = {
46    NULL
47};
48
49struct session {
50    char *id;
51    pa_client *client;
52};
53
54struct userdata {
55    pa_module *module;
56    pa_core *core;
57    pa_dbus_connection *connection;
58    pa_hashmap *sessions;
59    bool filter_added;
60};
61
62static void add_session(struct userdata *u, const char *id) {
63    DBusError error;
64    DBusMessage *m = NULL, *reply = NULL;
65    uint32_t uid;
66    struct session *session;
67    pa_client_new_data data;
68
69    dbus_error_init(&error);
70
71    if (pa_hashmap_get(u->sessions, id)) {
72        pa_log_warn("Duplicate session %s, ignoring.", id);
73        return;
74    }
75
76    if (!(m = dbus_message_new_method_call("org.freedesktop.ConsoleKit", id, "org.freedesktop.ConsoleKit.Session", "GetUnixUser"))) {
77        pa_log("Failed to allocate GetUnixUser() method call.");
78        goto fail;
79    }
80
81    if (!(reply = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(u->connection), m, -1, &error))) {
82        pa_log("GetUnixUser() call failed: %s: %s", error.name, error.message);
83        goto fail;
84    }
85
86    /* CK 0.3 this changed from int32 to uint32 */
87    if (!dbus_message_get_args(reply, &error, DBUS_TYPE_UINT32, &uid, DBUS_TYPE_INVALID)) {
88        dbus_error_free(&error);
89
90        if (!dbus_message_get_args(reply, &error, DBUS_TYPE_INT32, &uid, DBUS_TYPE_INVALID)) {
91            pa_log("Failed to parse GetUnixUser() result: %s: %s", error.name, error.message);
92            goto fail;
93        }
94    }
95
96    /* We only care about our own sessions */
97    if ((uid_t) uid != getuid())
98        goto fail;
99
100    session = pa_xnew(struct session, 1);
101    session->id = pa_xstrdup(id);
102
103    pa_client_new_data_init(&data);
104    data.module = u->module;
105    data.driver = __FILE__;
106    pa_proplist_setf(data.proplist, PA_PROP_APPLICATION_NAME, "ConsoleKit Session %s", id);
107    pa_proplist_sets(data.proplist, "console-kit.session", id);
108    session->client = pa_client_new(u->core, &data);
109    pa_client_new_data_done(&data);
110
111    if (!session->client) {
112        pa_xfree(session->id);
113        pa_xfree(session);
114        goto fail;
115    }
116
117    pa_hashmap_put(u->sessions, session->id, session);
118
119    pa_log_debug("Added new session %s", id);
120
121    /* Positive exit_idle_time is only useful when we have no session tracking
122     * capability, so we can set it to 0 now that we have detected a session.
123     * The benefit of setting exit_idle_time to 0 is that pulseaudio will exit
124     * immediately when the session ends. That in turn is useful, because some
125     * systems (those that use pam_systemd but don't use systemd for managing
126     * pulseaudio) clean $XDG_RUNTIME_DIR on logout, but fail to terminate all
127     * services that depend on the files in $XDG_RUNTIME_DIR. The directory
128     * contains our sockets, and if the sockets are removed without terminating
129     * pulseaudio, a quick relogin will likely cause trouble, because a new
130     * instance will be spawned while the old instance is still running. */
131    if (u->core->exit_idle_time > 0)
132        pa_core_set_exit_idle_time(u->core, 0);
133
134fail:
135
136    if (m)
137        dbus_message_unref(m);
138
139    if (reply)
140        dbus_message_unref(reply);
141
142    dbus_error_free(&error);
143}
144
145static void free_session(struct session *session) {
146    pa_assert(session);
147
148    pa_log_debug("Removing session %s", session->id);
149
150    pa_client_free(session->client);
151    pa_xfree(session->id);
152    pa_xfree(session);
153}
154
155static void remove_session(struct userdata *u, const char *id) {
156    pa_assert(u);
157    pa_assert(id);
158
159    pa_hashmap_remove_and_free(u->sessions, id);
160}
161
162static DBusHandlerResult filter_cb(DBusConnection *bus, DBusMessage *message, void *userdata) {
163    struct userdata *u = userdata;
164    DBusError error;
165    const char *path;
166
167    pa_assert(bus);
168    pa_assert(message);
169    pa_assert(u);
170
171    dbus_error_init(&error);
172
173    if (dbus_message_is_signal(message, "org.freedesktop.ConsoleKit.Seat", "SessionAdded")) {
174
175        /* CK API changed to match spec in 0.3 */
176        if (!dbus_message_get_args(message, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) {
177            dbus_error_free(&error);
178
179            if (!dbus_message_get_args(message, &error, DBUS_TYPE_STRING, &path, DBUS_TYPE_INVALID)) {
180                pa_log_error("Failed to parse SessionAdded message: %s: %s", error.name, error.message);
181                goto finish;
182            }
183        }
184
185        add_session(u, path);
186
187    } else if (dbus_message_is_signal(message, "org.freedesktop.ConsoleKit.Seat", "SessionRemoved")) {
188
189        /* CK API changed to match spec in 0.3 */
190        if (!dbus_message_get_args(message, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) {
191            dbus_error_free(&error);
192
193            if (!dbus_message_get_args(message, &error, DBUS_TYPE_STRING, &path, DBUS_TYPE_INVALID)) {
194                pa_log_error("Failed to parse SessionRemoved message: %s: %s", error.name, error.message);
195                goto finish;
196            }
197        }
198
199        remove_session(u, path);
200    }
201
202finish:
203    dbus_error_free(&error);
204
205    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
206}
207
208static int get_session_list(struct userdata *u) {
209    DBusError error;
210    DBusMessage *m = NULL, *reply = NULL;
211    uint32_t uid;
212    DBusMessageIter iter, sub;
213    int ret = -1;
214
215    pa_assert(u);
216
217    dbus_error_init(&error);
218
219    if (!(m = dbus_message_new_method_call("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "GetSessionsForUnixUser"))) {
220        pa_log("Failed to allocate GetSessionsForUnixUser() method call.");
221        goto fail;
222    }
223
224    uid = (uint32_t) getuid();
225    if (!(dbus_message_append_args(m, DBUS_TYPE_UINT32, &uid, DBUS_TYPE_INVALID))) {
226        pa_log("Failed to append arguments to GetSessionsForUnixUser() method call.");
227        goto fail;
228    }
229
230    if (!(reply = dbus_connection_send_with_reply_and_block(pa_dbus_connection_get(u->connection), m, -1, &error))) {
231        pa_log("GetSessionsForUnixUser() call failed: %s: %s", error.name, error.message);
232        goto fail;
233    }
234
235    dbus_message_iter_init(reply, &iter);
236
237    if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY ||
238        dbus_message_iter_get_element_type(&iter) != DBUS_TYPE_OBJECT_PATH) {
239        pa_log("Failed to parse GetSessionsForUnixUser() result.");
240        goto fail;
241    }
242
243    dbus_message_iter_recurse(&iter, &sub);
244
245    for (;;) {
246        int at;
247        const char *id;
248
249        if ((at = dbus_message_iter_get_arg_type(&sub)) == DBUS_TYPE_INVALID)
250            break;
251
252        pa_assert(at == DBUS_TYPE_OBJECT_PATH);
253        dbus_message_iter_get_basic(&sub, &id);
254
255        add_session(u, id);
256
257        dbus_message_iter_next(&sub);
258    }
259
260    ret = 0;
261
262fail:
263
264    if (m)
265        dbus_message_unref(m);
266
267    if (reply)
268        dbus_message_unref(reply);
269
270    dbus_error_free(&error);
271
272    return ret;
273}
274
275int pa__init(pa_module*m) {
276    DBusError error;
277    pa_dbus_connection *connection;
278    struct userdata *u = NULL;
279    pa_modargs *ma;
280
281    pa_assert(m);
282
283    dbus_error_init(&error);
284
285    /* If systemd's logind service is running, we shouldn't watch ConsoleKit
286     * but login */
287    if (access("/run/systemd/seats/", F_OK) >= 0)
288        return 0;
289
290    if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
291        pa_log("Failed to parse module arguments");
292        goto fail;
293    }
294
295    if (!(connection = pa_dbus_bus_get(m->core, DBUS_BUS_SYSTEM, &error)) || dbus_error_is_set(&error)) {
296
297        if (connection)
298            pa_dbus_connection_unref(connection);
299
300        pa_log_error("Unable to contact D-Bus system bus: %s: %s", error.name, error.message);
301        goto fail;
302    }
303
304    m->userdata = u = pa_xnew0(struct userdata, 1);
305    u->core = m->core;
306    u->module = m;
307    u->connection = connection;
308    u->sessions = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) free_session);
309
310    if (!dbus_connection_add_filter(pa_dbus_connection_get(connection), filter_cb, u, NULL)) {
311        pa_log_error("Failed to add filter function");
312        goto fail;
313    }
314
315    u->filter_added = true;
316
317    if (pa_dbus_add_matches(
318                pa_dbus_connection_get(connection), &error,
319                "type='signal',sender='org.freedesktop.ConsoleKit',interface='org.freedesktop.ConsoleKit.Seat',member='SessionAdded'",
320                "type='signal',sender='org.freedesktop.ConsoleKit',interface='org.freedesktop.ConsoleKit.Seat',member='SessionRemoved'", NULL) < 0) {
321        pa_log_error("Unable to subscribe to ConsoleKit signals: %s: %s", error.name, error.message);
322        goto fail;
323    }
324
325    if (get_session_list(u) < 0)
326        goto fail;
327
328    pa_modargs_free(ma);
329
330    return 0;
331
332fail:
333    if (ma)
334        pa_modargs_free(ma);
335
336    dbus_error_free(&error);
337    pa__done(m);
338
339    return -1;
340}
341
342void pa__done(pa_module *m) {
343    struct userdata *u;
344
345    pa_assert(m);
346
347    if (!(u = m->userdata))
348        return;
349
350    if (u->sessions)
351        pa_hashmap_free(u->sessions);
352
353    if (u->connection) {
354        pa_dbus_remove_matches(
355                pa_dbus_connection_get(u->connection),
356                "type='signal',sender='org.freedesktop.ConsoleKit',interface='org.freedesktop.ConsoleKit.Seat',member='SessionAdded'",
357                "type='signal',sender='org.freedesktop.ConsoleKit',interface='org.freedesktop.ConsoleKit.Seat',member='SessionRemoved'", NULL);
358
359        if (u->filter_added)
360            dbus_connection_remove_filter(pa_dbus_connection_get(u->connection), filter_cb, u);
361
362        pa_dbus_connection_unref(u->connection);
363    }
364
365    pa_xfree(u);
366}
367