Connection Contexts
Revisiting pa_context_new()
We've discussed pa_context_new()
in our simple API page. We will disucss it again here, but focusing on PulseAudio SharedMemory mechanisms.
In pa_context_new(), src/pulse/context.c
, we find the following snippet:
if (!(c->mempool = pa_mempool_new(!c->conf->disable_shm, c->conf->shm_size))) {
if (!c->conf->disable_shm)
c->mempool = pa_mempool_new(false, c->conf->shm_size);
if (!c->mempool) {
context_free(c);
return NULL;
}
}
What the above basically does is that it creates a memory pool, shared or not shared according to configuration. If creation of memory pool failed, and shared memory was requested, a retrial is made to create the memory pool with shared memory off. If both ways failed, the initialization simply fail.
pa_context_new()
pa_context *pa_context_new_with_proplist(pa_mainloop_api *mainloop, const char *name, pa_proplist *p) {
pa_context *c;
pa_init_i18n();
c = pa_xnew0(pa_context, 1);
PA_REFCNT_INIT(c);
/* Appliaction name, as passed from pa_simple_new() */
if (name)
pa_proplist_sets(c->proplist, PA_PROP_APPLICATION_NAME, name);
c->system_bus = c->session_bus = NULL;
c->mainloop = mainloop;
c->playback_streams = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
c->record_streams = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);
c->client_index = PA_INVALID_INDEX;
c->use_rtclock = pa_mainloop_is_our_api(mainloop);
PA_LLIST_HEAD_INIT(pa_stream, c->streams);
PA_LLIST_HEAD_INIT(pa_operation, c->operations);
c->error = PA_OK;
c->state = PA_CONTEXT_UNCONNECTED;
reset_callbacks(c);
c->conf = pa_client_conf_new();
pa_client_conf_load(c->conf, true, true);
c->srb_template.readfd = -1;
c->srb_template.writefd = -1;
return c;
}
As you can see from above, what this simply does is to initialize
the different pa_context
structures. This is also where the
client libraries read and parse contents of the pulse-client.conf(5) file, which usually resides at ~/.pulse/client.conf
.
pa_context_connect()
This is the most important function, where actual connections are being made to the server
int pa_context_connect(
pa_context *c,
const char *server,
pa_context_flags_t flags,
const pa_spawn_api *api) {
if (server)
c->conf->autospawn = false;
else
server = c->conf->default_server;
pa_context_ref(c);
c->no_fail = !!(flags & PA_CONTEXT_NOFAIL);
c->server_specified = !!server;
pa_assert(!c->server_list);
if (server) {
if (!(c->server_list = pa_strlist_parse(server))) {
pa_context_fail(c, PA_ERR_INVALIDSERVER);
goto finish;
}
} else {
char *d;
/* Prepend in reverse order */
/* Follow the X display */
if (c->conf->auto_connect_display) {
...
}
/* Add TCP/IP on the localhost */
if (c->conf->auto_connect_localhost) {
c->server_list = pa_strlist_prepend(c->server_list, "tcp6:[::1]");
c->server_list = pa_strlist_prepend(c->server_list, "tcp4:127.0.0.1");
}
/* The system wide instance via PF_LOCAL */
c->server_list = pa_strlist_prepend(c->server_list, PA_SYSTEM_RUNTIME_PATH PA_PATH_SEP PA_NATIVE_DEFAULT_UNIX_SOCKET);
/* The user instance via PF_LOCAL */
c->server_list = prepend_per_user(c->server_list);
}
pa_context_set_state(c, PA_CONTEXT_CONNECTING);
r = try_next_connection(c);
What the above code does is very simple. The client followsi the following algorithm for connecting to the server:
- Build a list of possible server addresses, in least-priority-first order
- If configured, find where the X server is running, and store the address
- If configured, add local host TCPv4 and TCPv6 addresses. Libraries will automatically connect using default port 4713
- Then add the location of the system-wide instance socket (Assuming PA daemon runs in system mode, the client libraries cannot know). This translates to the following location:
$PULSE_INSTALLATION_DIR/var/run/pulse/native
- Then in the highest-order position, put the current user-instance socket. This usually resides at
/run/user/$UID/pulse/native
1
After building such linked list of server addresses, the client library tries to connect to them in reverse order, starting from the user instance socket up to X server address.
Finding user sockets
Finding the user instance socket deserves some attention. Such sockets are discovered by prepend_per_user()
defined in src/pulse/context.c
:
static pa_strlist *prepend_per_user(pa_strlist *l) {
char *ufn;
/* The per-user instance */
if ((ufn = pa_runtime_path(PA_NATIVE_DEFAULT_UNIX_SOCKET))) {
l = pa_strlist_prepend(l, ufn);
pa_xfree(ufn);
}
return l;
}
The PA_NATIVE_DEFAULT_UNIX_SOCKET
symbol translates to the pulseAudio socket name "native". So what the above does is to call pa_runtime_path("native")
.
char *pa_runtime_path(const char *fn) {
return get_path(fn, false, true);
}
/* [Book Note: I've strippped code parts where fn is Null, or
* prepend machine ID (/etc/machine-id) is true. Our code path
* actually send the string "native" here as fn and prependmid
* as false */
static char *get_path(const char *fn, bool prependmid, bool rt) {
char *rtp;
// [Book Note: pa_getruntime_dir() usually returns
// $XDG_RUNTIME_DIR/pulse
rtp = rt ? pa_get_runtime_dir() : pa_get_state_dir();
canonical_rtp = rtp;
// [Book Note: This returns $XDG_RUNTIME_DIR/pulse/native ]
r = pa_sprintf_malloc("%s" PA_PATH_SEP "%s", canonical_rtp, fn);
return r;
}
And as usual, $XDG_RUNTIME_DIR
returns /run/user/$UID
. And instead of of taking our notes at heart value, let's check the code of pa_get_runtime_dir()
:
char *pa_get_runtime_dir(void) {
char *d, *k = NULL, *p = NULL, *t = NULL, *mid;
mode_t m;
/* The runtime directory shall contain dynamic data that needs NOT
* to be kept across reboots and is usually private to the user,
* except in system mode, where it might be accessible by other
* users, too. Since we need POSIX locking and UNIX sockets in
* this directory, we try XDG_RUNTIME_DIR first, and if that isn't
* set create a directory in $HOME and link it to a random subdir
* in /tmp, if it was not explicitly configured. */
m = pa_in_system_mode() ? 0755U : 0700U;
/* Use the explicitly configured value if it is set */
d = getenv("PULSE_RUNTIME_PATH");
if (d) {
if (pa_make_secure_dir(d, m, (uid_t) -1, (gid_t) -1, true) < 0) {
pa_log_error("Failed to create secure directory (%s): %s", d, pa_cstrerror(errno));
goto fail;
}
return pa_xstrdup(d);
}
/* Use the XDG standard for the runtime directory. */
d = getenv("XDG_RUNTIME_DIR");
if (d) {
k = pa_sprintf_malloc("%s" PA_PATH_SEP "pulse", d);
if (pa_make_secure_dir(k, m, (uid_t) -1, (gid_t) -1, true) < 0) {
pa_log_error("Failed to create secure directory (%s): %s", k, pa_cstrerror(errno));
goto fail;
}
return k;
}
/* XDG_RUNTIME_DIR wasn't set, use the old legacy fallback */
...
Connecting to the actual sockets:
As we've seen in the implementation of pa_context_connect()
, after this method prepares the list of servers it needs to connect to, it calls try_next_connection(c) to connect to the sockets provided in the configuration server list build earlier. Let's inspect try_next_connection()
further:
static int try_next_connection(pa_context *c) {
char *u = NULL;
int r = -1;
for (;;) {
pa_xfree(u);
u = NULL;
c->server_list = pa_strlist_pop(c->server_list, &u);
if (!u) {
if (c->do_autospawn) {
if ((r = context_autospawn(c)) < 0)
goto finish;
/* Autospawn only once */
c->do_autospawn = false;
/* Connect only to per-user sockets this time */
c->server_list = prepend_per_user(c->server_list);
/* Retry connection */
continue;
}
}
pa_log_debug("Trying to connect to %s...", u);
pa_xfree(c->server);
c->server = pa_xstrdup(u);
if (!(c->client = pa_socket_client_new_string(c->mainloop, c->use_rtclock, u, PA_NATIVE_DEFAULT_PORT)))
continue;
c->is_local = pa_socket_client_is_local(c->client);
pa_socket_client_set_callback(c->client, on_connection, c);
break;
}
}
The above code, does a host of important stuff:
- If we tried every socket address in the server list, and the connection failed, then to the best of our understanding, no PA instance is running. In this case, if autospawn=on, the libraray code spawns a user instance of the daemon. In the next iteration, it tries connecting to this autospawned session instance
- Once we are able to connect to any of the PA socekts, we register our callback
on_connection
and exit the method happily
The careful reader, while checking the explanation above, will wonder: how does PA client code differentiate between a Unix domain socket and a classical TCP/IP socket while connecting above?
The key to answering this question is that the observation that when PA added TCP/IP localhost server addresses, it added the prefixes "tcp4" and "tcp6" with them.
Delving into the implementation of pa_socket_client_new_string()
defind at src/pulsecore/socket-client.c
:
pa_socket_client* pa_socket_client_new_string(pa_mainloop_api *m, bool use_rtclock, const char*name, uint16_t default_port) {
if (pa_parse_address(name, &a) < 0)
return NULL;
if (!a.port)
a.port = default_port;
switch (a.type) {
case PA_PARSED_ADDRESS_UNIX:
if ((c = pa_socket_client_new_unix(m, a.path_or_host)))
start_timeout(c, use_rtclock);
break;
case PA_PARSED_ADDRESS_TCP4: /* Fallthrough */
case PA_PARSED_ADDRESS_TCP6: /* Fallthrough */
case PA_PARSED_ADDRESS_TCP_AUTO: {
And pa_parse_address()
does the type classiification according to the server address prefix. This is defined at src/pulsecore/pulseaddr.c
:
int pa_parse_address(const char *name, pa_parsed_address *ret_p) {
const char *p;
...
if (*p == '/')
ret_p->type = PA_PARSED_ADDRESS_UNIX;
else if (pa_startswith(p, "unix:")) {
ret_p->type = PA_PARSED_ADDRESS_UNIX;
p += sizeof("unix:")-1;
} else if (pa_startswith(p, "tcp:")) {
ret_p->type = PA_PARSED_ADDRESS_TCP4;
p += sizeof("tcp:")-1;
} else if (pa_startswith(p, "tcp4:")) {
ret_p->type = PA_PARSED_ADDRESS_TCP4;
p += sizeof("tcp4:")-1;
} else if (pa_startswith(p, "tcp6:")) {
ret_p->type = PA_PARSED_ADDRESS_TCP6;
p += sizeof("tcp6:")-1;
}
...
}