From 4ec61d79b8a7c3e85ff5b4edc783b526d7beda75 Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Tue, 7 Jan 2020 13:18:02 -0500
Subject: [PATCH] config-static-nodes: add config static nodes module

This module allows wireplumber to create static nodes that match a specific
device using a spa node factory. Matching is optional, and if there is no match,
the node will always be created.
---
 modules/meson.build                           |  13 +
 modules/module-config-static-nodes.c          |  18 ++
 modules/module-config-static-nodes/context.c  | 233 +++++++++++++++++
 modules/module-config-static-nodes/context.h  |  24 ++
 .../module-config-static-nodes/parser-node.c  | 247 ++++++++++++++++++
 .../module-config-static-nodes/parser-node.h  |  41 +++
 src/config/wireplumber.conf                   |   3 +
 7 files changed, 579 insertions(+)
 create mode 100644 modules/module-config-static-nodes.c
 create mode 100644 modules/module-config-static-nodes/context.c
 create mode 100644 modules/module-config-static-nodes/context.h
 create mode 100644 modules/module-config-static-nodes/parser-node.c
 create mode 100644 modules/module-config-static-nodes/parser-node.h

diff --git a/modules/meson.build b/modules/meson.build
index 015f1a36..11d674db 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -42,6 +42,19 @@ shared_library(
   dependencies : [gio_dep, wp_dep, pipewire_dep],
 )
 
+shared_library(
+  'wireplumber-module-config-static-nodes',
+  [
+    'module-config-static-nodes/parser-node.c',
+    'module-config-static-nodes/context.c',
+    'module-config-static-nodes.c',
+  ],
+  c_args : [common_c_args, '-DG_LOG_DOMAIN="m-config-static-nodes"'],
+  install : true,
+  install_dir : wireplumber_module_dir,
+  dependencies : [wp_dep, wptoml_dep, pipewire_dep],
+)
+
 shared_library(
   'wireplumber-module-config-endpoint',
   [
diff --git a/modules/module-config-static-nodes.c b/modules/module-config-static-nodes.c
new file mode 100644
index 00000000..68777da9
--- /dev/null
+++ b/modules/module-config-static-nodes.c
@@ -0,0 +1,18 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wp/wp.h>
+
+#include "module-config-static-nodes/context.h"
+
+void
+wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
+{
+  WpConfigStaticNodesContext *ctx = wp_config_static_nodes_context_new (core);
+  wp_module_set_destroy_callback (module, g_object_unref, ctx);
+}
diff --git a/modules/module-config-static-nodes/context.c b/modules/module-config-static-nodes/context.c
new file mode 100644
index 00000000..99d6744f
--- /dev/null
+++ b/modules/module-config-static-nodes/context.c
@@ -0,0 +1,233 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <pipewire/pipewire.h>
+
+#include <wp/wp.h>
+
+#include "parser-node.h"
+#include "context.h"
+
+struct _WpConfigStaticNodesContext
+{
+  GObject parent;
+
+  /* Props */
+  GWeakRef core;
+
+  WpObjectManager *devices_om;
+  GPtrArray *static_nodes;
+};
+
+enum {
+  PROP_0,
+  PROP_CORE,
+};
+
+enum {
+  SIGNAL_NODE_CREATED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+G_DEFINE_TYPE (WpConfigStaticNodesContext, wp_config_static_nodes_context,
+    G_TYPE_OBJECT)
+
+static void
+wp_config_static_nodes_context_create_node (WpConfigStaticNodesContext *self,
+  const struct WpParserNodeData *node_data)
+{
+  g_autoptr (WpProxy) node_proxy = NULL;
+  g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
+  g_return_if_fail (core);
+
+  /* Create the node */
+  node_proxy = node_data->n.local ?
+      wp_core_create_local_object (core, node_data->n.factory,
+          PW_TYPE_INTERFACE_Node, PW_VERSION_NODE_PROXY, node_data->n.props) :
+      wp_core_create_remote_object (core, node_data->n.factory,
+          PW_TYPE_INTERFACE_Node, PW_VERSION_NODE_PROXY, node_data->n.props);
+  if (!node_proxy) {
+    g_warning ("WpConfigStaticNodesContext:%p: failed to create node: %s", self,
+        g_strerror (errno));
+    return;
+  }
+
+  /* Add the node to the array */
+  g_ptr_array_add (self->static_nodes, g_object_ref (node_proxy));
+  g_debug ("WpConfigStaticNodesContext:%p: added static node: %s", self,
+      node_data->n.factory);
+
+  /* Emit the node-created signal */
+  g_signal_emit (self, signals[SIGNAL_NODE_CREATED], 0, node_proxy);
+}
+
+static void
+on_device_added (WpObjectManager *om, WpProxy *proxy, gpointer p)
+{
+  WpConfigStaticNodesContext *self = p;
+  g_autoptr (WpProperties) dev_props =
+      wp_proxy_device_get_properties (WP_PROXY_DEVICE (proxy));
+  g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
+  g_autoptr (WpConfiguration) config = wp_configuration_get_instance (core);
+  g_autoptr (WpConfigParser) parser = NULL;
+  const struct WpParserNodeData *node_data = NULL;
+
+  /* Get the parser node data and skip the node if not found */
+  parser = wp_configuration_get_parser (config, WP_PARSER_NODE_EXTENSION);
+  node_data = wp_config_parser_get_matched_data (parser, dev_props);
+  if (!node_data)
+    return;
+
+  /* Create the node */
+  wp_config_static_nodes_context_create_node (self, node_data);
+}
+
+static gboolean
+parser_node_foreach_func (const struct WpParserNodeData *node_data,
+    gpointer data)
+{
+  WpConfigStaticNodesContext *self = data;
+
+  /* Only create nodes that don't have match-device info */
+  if (!node_data->has_md) {
+    wp_config_static_nodes_context_create_node (self, node_data);
+    return TRUE;
+  }
+
+  return TRUE;
+}
+
+static void
+start_static_nodes (WpConfigStaticNodesContext *self)
+{
+  g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
+  g_autoptr (WpConfiguration) config = wp_configuration_get_instance (core);
+  g_autoptr (WpConfigParser) parser =
+      wp_configuration_get_parser (config, WP_PARSER_NODE_EXTENSION);
+
+  /* Create static nodes without match-device */
+  wp_parser_node_foreach (WP_PARSER_NODE (parser), parser_node_foreach_func,
+      self);
+}
+
+static void
+wp_config_static_nodes_context_constructed (GObject * object)
+{
+  WpConfigStaticNodesContext *self = WP_CONFIG_STATIC_NODES_CONTEXT (object);
+  g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
+  g_autoptr (WpConfiguration) config = wp_configuration_get_instance (core);
+
+  /* Add the node parser and parse the node files */
+  wp_configuration_add_extension (config, WP_PARSER_NODE_EXTENSION,
+      WP_TYPE_PARSER_NODE);
+  wp_configuration_reload (config, WP_PARSER_NODE_EXTENSION);
+
+  /* Install the object manager */
+  wp_core_install_object_manager (core, self->devices_om);
+
+  /* Start creating static nodes when the connected callback is triggered */
+  g_signal_connect_object (core, "remote-state-changed::connected",
+      (GCallback) start_static_nodes, self, G_CONNECT_SWAPPED);
+
+  G_OBJECT_CLASS (wp_config_static_nodes_context_parent_class)->constructed (object);
+}
+
+static void
+wp_config_static_nodes_context_set_property (GObject * object, guint property_id,
+    const GValue * value, GParamSpec * pspec)
+{
+  WpConfigStaticNodesContext *self = WP_CONFIG_STATIC_NODES_CONTEXT (object);
+
+  switch (property_id) {
+  case PROP_CORE:
+    g_weak_ref_set (&self->core, g_value_get_object (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+wp_config_static_nodes_context_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  WpConfigStaticNodesContext *self = WP_CONFIG_STATIC_NODES_CONTEXT (object);
+
+  switch (property_id) {
+  case PROP_CORE:
+    g_value_take_object (value, g_weak_ref_get (&self->core));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+wp_config_static_nodes_context_finalize (GObject *object)
+{
+  WpConfigStaticNodesContext *self = WP_CONFIG_STATIC_NODES_CONTEXT (object);
+
+  g_clear_object (&self->devices_om);
+  g_clear_pointer (&self->static_nodes, g_ptr_array_unref);
+
+  g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
+  if (core) {
+    g_autoptr (WpConfiguration) config = wp_configuration_get_instance (core);
+    wp_configuration_remove_extension (config, WP_PARSER_NODE_EXTENSION);
+  }
+  g_weak_ref_clear (&self->core);
+
+  G_OBJECT_CLASS (wp_config_static_nodes_context_parent_class)->finalize (object);
+}
+
+static void
+wp_config_static_nodes_context_init (WpConfigStaticNodesContext *self)
+{
+  self->static_nodes = g_ptr_array_new_with_free_func (g_object_unref);
+  self->devices_om = wp_object_manager_new ();
+
+  /* Only handle devices */
+  wp_object_manager_add_proxy_interest (self->devices_om,
+      PW_TYPE_INTERFACE_Device, NULL, WP_PROXY_FEATURE_INFO);
+  g_signal_connect (self->devices_om, "object-added",
+      (GCallback) on_device_added, self);
+}
+
+static void
+wp_config_static_nodes_context_class_init (WpConfigStaticNodesContextClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  object_class->constructed = wp_config_static_nodes_context_constructed;
+  object_class->finalize = wp_config_static_nodes_context_finalize;
+  object_class->set_property = wp_config_static_nodes_context_set_property;
+  object_class->get_property = wp_config_static_nodes_context_get_property;
+
+  /* Properties */
+  g_object_class_install_property (object_class, PROP_CORE,
+      g_param_spec_object ("core", "core", "The wireplumber core",
+          WP_TYPE_CORE,
+          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /* Signals */
+  signals[SIGNAL_NODE_CREATED] = g_signal_new ("node-created",
+      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+      G_TYPE_NONE, 1, WP_TYPE_PROXY);
+}
+
+WpConfigStaticNodesContext *
+wp_config_static_nodes_context_new (WpCore *core)
+{
+  return g_object_new (wp_config_static_nodes_context_get_type (),
+    "core", core,
+    NULL);
+}
diff --git a/modules/module-config-static-nodes/context.h b/modules/module-config-static-nodes/context.h
new file mode 100644
index 00000000..bdd9ba3b
--- /dev/null
+++ b/modules/module-config-static-nodes/context.h
@@ -0,0 +1,24 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef __WIREPLUMBER_CONFIG_STATIC_NODES_CONTEXT_H__
+#define __WIREPLUMBER_CONFIG_STATIC_NODES_CONTEXT_H__
+
+#include <wp/wp.h>
+
+G_BEGIN_DECLS
+
+#define WP_TYPE_CONFIG_STATIC_NODES_CONTEXT (wp_config_static_nodes_context_get_type ())
+G_DECLARE_FINAL_TYPE (WpConfigStaticNodesContext, wp_config_static_nodes_context,
+    WP, CONFIG_STATIC_NODES_CONTEXT, GObject);
+
+WpConfigStaticNodesContext * wp_config_static_nodes_context_new (WpCore *core);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/module-config-static-nodes/parser-node.c b/modules/module-config-static-nodes/parser-node.c
new file mode 100644
index 00000000..f14f82fe
--- /dev/null
+++ b/modules/module-config-static-nodes/parser-node.c
@@ -0,0 +1,247 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wptoml/wptoml.h>
+
+#include <pipewire/pipewire.h>
+
+#include "parser-node.h"
+
+struct _WpParserNode
+{
+  GObject parent;
+
+  GPtrArray *datas;
+};
+
+static void wp_parser_node_config_parser_init (gpointer iface,
+    gpointer iface_data);
+
+G_DEFINE_TYPE_WITH_CODE (WpParserNode, wp_parser_node,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_CONFIG_PARSER,
+                           wp_parser_node_config_parser_init))
+
+static void
+wp_parser_node_data_destroy (gpointer p)
+{
+  struct WpParserNodeData *data = p;
+
+  /* Free the strings */
+  g_clear_pointer (&data->md.props, wp_properties_unref);
+  g_clear_pointer (&data->n.factory, g_free);
+  g_clear_pointer (&data->n.props, wp_properties_unref);
+
+  g_slice_free (struct WpParserNodeData, data);
+}
+
+static void
+parse_properties_for_each (const WpTomlTable *table, gpointer user_data)
+{
+  WpProperties *props = user_data;
+  g_return_if_fail (props);
+
+  /* Skip unparsed tables */
+  if (!table)
+    return;
+
+  /* Parse the name and value */
+  g_autofree gchar *name = wp_toml_table_get_string (table, "name");
+  g_autofree gchar *value = wp_toml_table_get_string (table, "value");
+
+  /* Set the property */
+  if (name && value)
+    wp_properties_set (props, name, value);
+}
+
+static WpProperties *
+parse_properties (WpTomlTable *table, const char *name)
+{
+  WpProperties *props = wp_properties_new_empty ();
+
+  g_autoptr (WpTomlTableArray) properties = NULL;
+  properties = wp_toml_table_get_array_table (table, name);
+  if (properties)
+    wp_toml_table_array_for_each (properties, parse_properties_for_each, props);
+
+  return props;
+}
+
+static struct WpParserNodeData *
+wp_parser_node_data_new (const gchar *location)
+{
+  g_autoptr (WpTomlFile) file = NULL;
+  g_autoptr (WpTomlTable) table = NULL, md = NULL, n = NULL;
+  struct WpParserNodeData *res = NULL;
+
+  /* File format:
+   * ------------
+   * [match-device]
+   * priority (uint32)
+   * properties (WpProperties)
+   *
+   * [node]
+   * factory (string)
+   * local (boolean)
+   * properties (WpProperties)
+   */
+
+  /* Get the TOML file */
+  file = wp_toml_file_new (location);
+  if (!file)
+    return NULL;
+
+  /* Get the file table */
+  table = wp_toml_file_get_table (file);
+  if (!table)
+    return NULL;
+
+  /* Create the node data */
+  res = g_slice_new0(struct WpParserNodeData);
+
+  /* Get the match-device table */
+  res->has_md = FALSE;
+  md = wp_toml_table_get_table (table, "match-device");
+  if (md) {
+    res->has_md = TRUE;
+
+    /* Get the priority from the match-device table */
+    res->md.priority = 0;
+    wp_toml_table_get_uint32 (md, "priority", &res->md.priority);
+
+    /* Get the match device properties */
+    res->md.props = parse_properties (md, "properties");
+  }
+
+  /* Get the node table */
+  n = wp_toml_table_get_table (table, "node");
+  if (!n)
+    goto error;
+
+  /* Get factory from the node table */
+  res->n.factory = wp_toml_table_get_string (n, "factory");
+
+  /* Get local from the node table */
+  res->n.local = FALSE;
+  wp_toml_table_get_boolean (n, "local", &res->n.local);
+
+  /* Get the node properties */
+  res->n.props = parse_properties (n, "properties");
+
+  return res;
+
+error:
+  g_clear_pointer (&res, wp_parser_node_data_destroy);
+  return NULL;
+}
+
+static gint
+compare_datas_func (gconstpointer a, gconstpointer b)
+{
+  struct WpParserNodeData *da = *(struct WpParserNodeData *const *)a;
+  struct WpParserNodeData *db = *(struct WpParserNodeData *const *)b;
+
+  return db->md.priority - da->md.priority;
+}
+
+static gboolean
+wp_parser_node_add_file (WpConfigParser *parser,
+    const gchar *name)
+{
+  WpParserNode *self = WP_PARSER_NODE (parser);
+  struct WpParserNodeData *data;
+
+  /* Parse the file */
+  data = wp_parser_node_data_new (name);
+  if (!data) {
+    g_warning ("Failed to parse configuration file '%s'", name);
+    return FALSE;
+  }
+
+  /* Add the data to the array */
+  g_ptr_array_add(self->datas, data);
+
+  /* Sort the array by priority */
+  g_ptr_array_sort(self->datas, compare_datas_func);
+
+  return TRUE;
+}
+
+static gconstpointer
+wp_parser_node_get_matched_data (WpConfigParser *parser, gpointer data)
+{
+  WpParserNode *self = WP_PARSER_NODE (parser);
+  WpProperties *props = data;
+  const struct WpParserNodeData *d = NULL;
+
+  g_return_val_if_fail (props, NULL);
+
+  /* Find the first data that matches device properties */
+  for (guint i = 0; i < self->datas->len; i++) {
+    d = g_ptr_array_index(self->datas, i);
+    if (d->has_md && wp_properties_matches (props, d->md.props))
+      return d;
+  }
+
+  return NULL;
+}
+
+static void
+wp_parser_node_reset (WpConfigParser *parser)
+{
+  WpParserNode *self = WP_PARSER_NODE (parser);
+
+  g_ptr_array_set_size (self->datas, 0);
+}
+
+static void
+wp_parser_node_config_parser_init (gpointer iface, gpointer iface_data)
+{
+  WpConfigParserInterface *cp_iface = iface;
+
+  cp_iface->add_file = wp_parser_node_add_file;
+  cp_iface->get_matched_data = wp_parser_node_get_matched_data;
+  cp_iface->reset = wp_parser_node_reset;
+}
+
+static void
+wp_parser_node_finalize (GObject * object)
+{
+  WpParserNode *self = WP_PARSER_NODE (object);
+
+  g_clear_pointer (&self->datas, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (wp_parser_node_parent_class)->finalize (object);
+}
+
+static void
+wp_parser_node_init (WpParserNode * self)
+{
+  self->datas = g_ptr_array_new_with_free_func (wp_parser_node_data_destroy);
+}
+
+static void
+wp_parser_node_class_init (WpParserNodeClass * klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  object_class->finalize = wp_parser_node_finalize;
+}
+
+void
+wp_parser_node_foreach (WpParserNode *self, WpParserNodeForeachFunction f,
+    gpointer data)
+{
+  const struct WpParserNodeData *d;
+
+  for (guint i = 0; i < self->datas->len; i++) {
+    d = g_ptr_array_index(self->datas, i);
+    if (!f (d, data))
+      break;
+  }
+}
diff --git a/modules/module-config-static-nodes/parser-node.h b/modules/module-config-static-nodes/parser-node.h
new file mode 100644
index 00000000..ee66052d
--- /dev/null
+++ b/modules/module-config-static-nodes/parser-node.h
@@ -0,0 +1,41 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef __WIREPLUMBER_PARSER_NODE_H__
+#define __WIREPLUMBER_PARSER_NODE_H__
+
+#include <wp/wp.h>
+
+G_BEGIN_DECLS
+
+#define WP_PARSER_NODE_EXTENSION "node"
+
+struct WpParserNodeData {
+  struct MatchDevice {
+    guint priority;
+    WpProperties *props;
+  } md;
+  gboolean has_md;
+  struct Node {
+    char *factory;
+    gboolean local;
+    WpProperties *props;
+  } n;
+};
+
+#define WP_TYPE_PARSER_NODE (wp_parser_node_get_type ())
+G_DECLARE_FINAL_TYPE (WpParserNode, wp_parser_node, WP, PARSER_NODE, GObject)
+
+typedef gboolean (*WpParserNodeForeachFunction) (
+    const struct WpParserNodeData *parser_data, gpointer data);
+void wp_parser_node_foreach (WpParserNode *self, WpParserNodeForeachFunction f,
+    gpointer data);
+
+G_END_DECLS
+
+#endif
diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf
index e16d6247..76343215 100644
--- a/src/config/wireplumber.conf
+++ b/src/config/wireplumber.conf
@@ -31,6 +31,9 @@ load-module C libwireplumber-module-monitor {
   "factory": <"api.v4l2.enum.udev">
 }
 
+# Implements static nodes creation based on TOML configuration files
+load-module C libwireplumber-module-config-static-nodes
+
 # Implements endpoint creation based on TOML configuration files
 load-module C libwireplumber-module-config-endpoint
 
-- 
GitLab