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/native1

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;
    }
    ...
}

results matching ""

    No results matching ""