Newer
Older
/* WirePlumber
*
* Copyright © 2019 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
#include <wp/wp.h>
#include <gio/gio.h>
#include <glib-unix.h>

George Kiagiadakis
committed
#include <pipewire/pipewire.h>
static GOptionEntry entries[] =
{
{ NULL }
};
#define WP_DOMAIN_DAEMON (wp_domain_daemon_quark ())
static G_DEFINE_QUARK (wireplumber-daemon, wp_domain_daemon);
enum WpExitCode
{
WP_CODE_DISCONNECTED = 0,
WP_CODE_INTERRUPTED,
WP_CODE_OPERATION_FAILED,
WP_CODE_INVALID_ARGUMENT,
};
struct WpDaemonData
{
WpCore *core;
GMainLoop *loop;
gint exit_code;
gchar *exit_message;
GDestroyNotify free_message;

Julian Bouzas
committed
GPtrArray *sessions;
};
static void
daemon_exit (struct WpDaemonData * d, gint code, const gchar *format, ...)
{
va_list args;
va_start (args, format);
d->exit_code = code;
d->exit_message = g_strdup_vprintf (format, args);
d->free_message = g_free;
va_end (args);
g_main_loop_quit (d->loop);
}
static void
daemon_exit_static_str (struct WpDaemonData * d, gint code, const gchar *str)
{
d->exit_code = code;
d->exit_message = (gchar *) str;
d->free_message = NULL;
g_main_loop_quit (d->loop);
}
static gboolean
signal_handler (gpointer data)
{
struct WpDaemonData *d = data;
daemon_exit_static_str (d, WP_CODE_INTERRUPTED, "interrupted by signal");
return G_SOURCE_CONTINUE;
}
static void
on_plugin_added (WpObjectManager * om, WpPlugin * p, struct WpDaemonData *d)
{
wp_info ("Activating plugin " WP_OBJECT_FORMAT, WP_OBJECT_ARGS (p));
wp_plugin_activate (p);
}
static gboolean
activate_plugins (struct WpDaemonData *d)
{
g_autoptr (WpObjectManager) om = NULL;
om = wp_object_manager_new ();
wp_object_manager_add_interest (om, WP_TYPE_PLUGIN, NULL);
g_signal_connect (om, "object-added", G_CALLBACK (on_plugin_added), d);
wp_core_install_object_manager (d->core, om);
/* object-added will be emitted for all plugins synchronously during the
install call above and we don't expect anyone to load plugins later,
so we don't need to keep a reference to this object manager.
This optimization is based on the knowledge of the implementation of
WpObjectManager and in other circumstances it should not be relied upon. */
return G_SOURCE_REMOVE;
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
}
static void
on_session_exported (WpProxy * session, GAsyncResult * res,
struct WpDaemonData *d)
{
g_autoptr (GError) error = NULL;
if (!wp_proxy_augment_finish (session, res, &error)) {
wp_warning_object (session, "session could not be exported: %s",
error->message);
}
if (wp_log_level_is_enabled (G_LOG_LEVEL_DEBUG)) {
g_autoptr (WpProperties) props = wp_proxy_get_properties (session);
wp_debug_object (session, "session '%s' exported",
wp_properties_get (props, "session.name"));
}
for (guint i = 0; i < d->sessions->len; i++) {
WpProxy *session = g_ptr_array_index (d->sessions, i);
if ((wp_proxy_get_features (session) & WP_SESSION_FEATURES_STANDARD)
!= WP_SESSION_FEATURES_STANDARD) {
/* not ready yet */
return;
}
}
wp_debug ("All sessions exported");
wp_core_idle_add (d->core, NULL, G_SOURCE_FUNC (activate_plugins), d, NULL);
}
static void
on_connected (WpCore *core, struct WpDaemonData *d)
{
for (guint i = 0; i < d->sessions->len; i++) {
WpProxy *session = g_ptr_array_index (d->sessions, i);
wp_proxy_augment (session,
WP_SESSION_FEATURES_STANDARD, NULL,
(GAsyncReadyCallback) on_session_exported, d);
}
}

George Kiagiadakis
committed
static void
on_disconnected (WpCore *core, struct WpDaemonData * d)

George Kiagiadakis
committed
{
/* something else triggered the exit; we will certainly get a state
* change while destroying the remote, but let's not change the message */
if (d->exit_message)
return;
daemon_exit_static_str (d, WP_CODE_DISCONNECTED,
"disconnected from pipewire");

George Kiagiadakis
committed
}
static gboolean
parse_commands_file (struct WpDaemonData *d, GInputStream * stream,
GError ** error)
{
gchar buffer[4096];
gssize bytes_read;
gchar *cur, *linestart, *saveptr;

George Kiagiadakis
committed
gchar *cmd;
gint lineno = 1, block_lines = 1, in_block = 0;
gboolean eof = FALSE, in_comment = FALSE;
GVariant *properties;
bytes_read = g_input_stream_read (stream, cur,
sizeof (buffer) - (cur - linestart), NULL, error);
if (bytes_read < 0)
return FALSE;
else if (bytes_read == 0) {
eof = TRUE;
/* terminate the remaining data, so that we consume it all */
if (cur != linestart) {
*cur = '\n';
}
}
bytes_read += (cur - linestart);
while (cur - buffer < bytes_read) {
/* advance cur to the end of the line that is at the end of the block */
while (cur - buffer < bytes_read && (in_block || *cur != '\n')) {
switch (*cur) {
case '{':
if (!in_comment)
in_block++;
break;
case '}':
if (!in_comment)
in_block--;
case '#':
in_comment = TRUE;
break;
case '\n': // found a newline inside a block
block_lines++;
in_comment = FALSE;
break;
default:
break;
}
/* replace comments with spaces to make the parser ignore them */
if (in_comment)
*cur = ' ';
if (!in_block && *cur == '\n') {
/* found the end of a line */
*cur = '\0';
/* tokenize and execute */
cmd = strtok_r (linestart, " ", &saveptr);
if (!cmd || cmd[0] == '#') {
/* empty line or comment, skip */
} else if (!g_strcmp0 (cmd, "load-module")) {

George Kiagiadakis
committed
gchar *abi, *module, *props;
abi = strtok_r (NULL, " ", &saveptr);
module = strtok_r (NULL, " ", &saveptr);

George Kiagiadakis
committed
if (!abi || !module ||
(abi && abi[0] == '{') || (module && module[0] == '{'))
{
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"expected ABI and MODULE at line %i", lineno);
return FALSE;
}
/* if there are remaining characters after the module name,
treat it as a serialized GVariant for the properties */
props = module + strlen(module) + 1;
if (cur - props > 0 && !in_comment) {
g_autoptr (GError) tmperr = NULL;
g_autofree gchar *context = NULL;
properties = g_variant_parse (G_VARIANT_TYPE_VARDICT, props, cur,
NULL, &tmperr);
if (!properties) {
context = g_variant_parse_error_print_context (tmperr, props);
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"GVariant parse error after line %i:\n%s", lineno, context);
return FALSE;
}
} else {
properties = g_variant_new_parsed ("@a{sv} {}");
}
if (!wp_module_load (d->core, abi, module, properties, error)) {
} else if (!g_strcmp0 (cmd, "load-pipewire-module")) {
gchar *module, *props;
module = strtok_r (NULL, " ", &saveptr);
props = module + strlen(module) + 1;
if (!pw_context_load_module (wp_core_get_pw_context (d->core), module,
props, NULL)) {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_OPERATION_FAILED,
"failed to load pipewire module '%s': %s", module,
g_strerror (errno));
return FALSE;
}

George Kiagiadakis
committed
} else if (!g_strcmp0 (cmd, "add-spa-lib")) {
gchar *regex, *lib;
gint ret;
regex = strtok_r (NULL, " ", &saveptr);
lib = strtok_r (NULL, " ", &saveptr);
if (!regex || !lib ||
(regex && regex[0] == '{') || (lib && lib[0] == '{'))
{
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"expected REGEX and LIB at line %i", lineno);
return FALSE;
}
ret = pw_context_add_spa_lib (wp_core_get_pw_context (d->core), regex,
lib);

George Kiagiadakis
committed
if (ret < 0) {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_OPERATION_FAILED,
"failed to add spa lib ('%s' on '%s'): %s", regex, lib,
g_strerror (-ret));
return FALSE;
}

Julian Bouzas
committed
} else if (!g_strcmp0 (cmd, "create-session")) {
g_autoptr (WpImplSession) session = NULL;
gchar *name = strtok_r (NULL, " ", &saveptr);
if (!name) {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"expected session name at line %i", lineno);
return FALSE;
}
session = wp_impl_session_new (d->core);
wp_impl_session_set_property (session, "session.name", name);
g_ptr_array_add (d->sessions, g_steal_pointer (&session));
} else {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_INVALID_ARGUMENT,
"unknown command '%s' at line %i", cmd, lineno);
return FALSE;
}
/* continue with the next line */
linestart = ++cur;
lineno += block_lines;
block_lines = 1;
in_comment = FALSE;
}
}
/* reached the end of the data that was read */
if (cur - linestart >= sizeof (buffer)) {
g_set_error (error, WP_DOMAIN_DAEMON, WP_CODE_OPERATION_FAILED,
"line %i exceeds the maximum allowed line size (%d bytes)",
lineno, (gint) sizeof (buffer));
return FALSE;
} else if (cur - linestart > 0) {
/* we have unparsed data, move it to the
* beginning of the buffer and continue */
strncpy (buffer, linestart, cur - linestart);
linestart = buffer;
cur = buffer + (cur - linestart);

George Kiagiadakis
committed
} else {
/* reset for the next g_input_stream_read() call */
linestart = cur = buffer;
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
}
} while (!eof);
return TRUE;
}
static gboolean
load_commands_file (struct WpDaemonData *d)
{
g_autoptr (GFile) file = NULL;
g_autoptr (GError) error = NULL;
g_autoptr (GFileInputStream) istream = NULL;
const gchar *filename;
filename = g_getenv ("WIREPLUMBER_CONFIG_FILE");
if (!filename)
filename = WIREPLUMBER_DEFAULT_CONFIG_FILE;
file = g_file_new_for_path (filename);
istream = g_file_read (file, NULL, &error);
if (!istream) {
daemon_exit (d, WP_CODE_INVALID_ARGUMENT, "%s", error->message);
return G_SOURCE_REMOVE;
}
if (!parse_commands_file (d, G_INPUT_STREAM (istream), &error)) {
daemon_exit (d, error->code, "Failed to read '%s': %s", filename,
error->message);
return G_SOURCE_REMOVE;
}

George Kiagiadakis
committed
/* connect to pipewire */
if (!wp_core_connect (d->core))
daemon_exit_static_str (d, WP_CODE_DISCONNECTED, "failed to connect");

George Kiagiadakis
committed
g_autoptr (GOptionContext) context = NULL;
g_autoptr (GError) error = NULL;
g_autoptr (WpConfiguration) config = NULL;

Julian Bouzas
committed
g_autoptr (GPtrArray) sessions = NULL;
const gchar *configuration_path;
context = g_option_context_new ("- PipeWire Session/Policy Manager");
g_option_context_add_main_entries (context, entries, NULL);
if (!g_option_context_parse (context, &argc, &argv, &error)) {
data.exit_message = error->message;
data.exit_code = WP_CODE_INVALID_ARGUMENT;

George Kiagiadakis
committed
data.core = core = wp_core_new (NULL, wp_properties_new (
PW_KEY_APP_NAME, "WirePlumber",
NULL));
g_signal_connect (core, "connected", G_CALLBACK (on_connected), &data);
g_signal_connect (core, "disconnected", (GCallback) on_disconnected, &data);
/* init configuration */
configuration_path = g_getenv ("WIREPLUMBER_CONFIG_DIR");
if (!configuration_path)
configuration_path = WIREPLUMBER_DEFAULT_CONFIG_DIR;
config = wp_configuration_get_instance (core);
wp_configuration_add_path (config, configuration_path);

George Kiagiadakis
committed
data.loop = loop = g_main_loop_new (NULL, FALSE);

Julian Bouzas
committed
/* init sessions */
data.sessions = sessions = g_ptr_array_new_with_free_func (g_object_unref);
/* watch for exit signals */
g_unix_signal_add (SIGINT, signal_handler, &data);
g_unix_signal_add (SIGTERM, signal_handler, &data);
g_unix_signal_add (SIGHUP, signal_handler, &data);
g_idle_add ((GSourceFunc) load_commands_file, &data);
g_main_loop_run (data.loop);
wp_message ("%s", data.exit_message);
if (data.free_message)
data.free_message (data.exit_message);