Files
wireplumber/lib/wp/core.c
George Kiagiadakis 60382df63f conf: refactor configuration loading
Changes:

- Configuration files are no longer located by libpipewire,
  which allows us to control the paths that are being looked up.
  This is a requirement for installations where pipewire and
  wireplumber are built using different prefixes, in which case
  the configuration files of wireplumber end up being installed in
  a place that libpipewire doesn't look into...

- The location of conf files is now again $prefix/share/wireplumber,
  /etc/wireplumber and $XDG_CONFIG_HOME/wireplumber, instead of using
  the pipewire directories. Also, since the previous commits, we now
  also support $XDG_CONFIG_DIRS/wireplumber (typically /etc/xdg/wireplumber)
  and $XDG_DATA_DIRS/wireplumber for system-wide configuration.

- Since libpipewire doesn't expose the parser, we now also do the
  parsing of sections ourselves. This has the advantage that we can
  optimize it a bit for our use case.

- The WpConf API has changed to not be a singleton and it is a
  property of WpCore instead. The configuration is now expected
  to be opened before the core is created, which allows the caller
  to identify configuration errors in advance. By not being a singleton,
  we can also reuse the WpConf API to open other SPA-JSON files.

- WpConf also now has a lazy loading mechanism. The configuration
  files are mmap'ed and the various sections are located in advance,
  but not parsed until they are actually requested. Also, the sections
  are not copied in memory, unlike what happens in libpipewire. They
  are only copied when merging is needed.

- WpCore now disables loading of a configuration file in pw_context,
  if a WpConf is provided. This is to have complete control here.
  The 'context.spa-libs' and 'context.modules' sections are still
  loaded, but we load them in WpConf and pass them down to pw_context
  for parsing. If a WpConf is not provided, pw_context is left to load
  the default configuration file (client.conf normally).
2024-03-04 07:07:56 +00:00

1425 lines
40 KiB
C

/* WirePlumber
*
* Copyright © 2019 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include "core.h"
#include "wp.h"
#include "private/registry.h"
#include "private/internal-comp-loader.h"
#include <pipewire/pipewire.h>
#include <spa/utils/result.h>
#include <spa/debug/types.h>
#include <spa/support/cpu.h>
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-core")
/*
* Integration between the PipeWire main loop and GMainLoop
*/
#define WP_LOOP_SOURCE(x) ((WpLoopSource *) x)
typedef struct _WpLoopSource WpLoopSource;
struct _WpLoopSource
{
GSource parent;
struct pw_loop *loop;
};
static gboolean
wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
{
int result;
wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");
pw_loop_enter (WP_LOOP_SOURCE(s)->loop);
result = pw_loop_iterate (WP_LOOP_SOURCE(s)->loop, 0);
pw_loop_leave (WP_LOOP_SOURCE(s)->loop);
wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");
if (G_UNLIKELY (result < 0))
wp_warning_boxed (G_TYPE_SOURCE, s,
"pw_loop_iterate failed: %s", spa_strerror (result));
return G_SOURCE_CONTINUE;
}
static void
wp_loop_source_finalize (GSource * s)
{
pw_loop_destroy (WP_LOOP_SOURCE(s)->loop);
}
static GSourceFuncs source_funcs = {
NULL,
NULL,
wp_loop_source_dispatch,
wp_loop_source_finalize
};
static GSource *
wp_loop_source_new (void)
{
GSource *s = g_source_new (&source_funcs, sizeof (WpLoopSource));
WP_LOOP_SOURCE(s)->loop = pw_loop_new (NULL);
g_source_add_unix_fd (s,
pw_loop_get_fd (WP_LOOP_SOURCE(s)->loop),
G_IO_IN | G_IO_ERR | G_IO_HUP);
return (GSource *) s;
}
/*! \defgroup wpcore WpCore */
/*!
* \struct WpCore
*
* The core is the central object around which everything operates. It is
* essential to create a WpCore before using any other WirePlumber API.
*
* The core object has the following responsibilities:
* * it initializes the PipeWire library
* * it creates a `pw_context` and allows connecting to the PipeWire server,
* creating a local `pw_core`
* * it glues the PipeWire library's event loop system with GMainLoop
* * it maintains a list of registered objects, which other classes use
* to keep objects loaded permanently into memory
* * it watches the PipeWire registry and keeps track of remote and local
* objects that appear in the registry, making them accessible through
* the WpObjectManager API.
*
* The core is also responsible for loading components, which are defined in
* the main configuration file. Components are loaded when
* WP_CORE_FEATURE_COMPONENTS is activated.
*
* \b Configuration
*
* The main configuration file needs to be created and opened before the core
* is created, using the WpConf API. It is then passed to the core as an
* argument in the constructor.
*
* If a configuration file is not provided, the core will let the underlying
* `pw_context` load its own configuration, based on the rules that apply to
* all pipewire clients (e.g. it respects the `PIPEWIRE_CONFIG_NAME` environment
* variable and loads "client.conf" as a last resort).
*
* If a configuration file is provided, the core does not let the underlying
* `pw_context` load any configuration and instead uses the provided WpConf
* object.
*
* \gproperties
*
* \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
* A GMainContext to attach to}
*
* \gproperty{properties, WpProperties *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
* The pipewire properties of the pw_core}
*
* \gproperty{pw-context, gpointer (struct pw_context *), G_PARAM_READABLE,
* The pipewire context}
*
* \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE,
* The pipewire core}
*
* \gproperty{conf, WpConf *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
* The main configuration file}
*
* \gsignals
*
* \par connected
* \parblock
* \code
* void
* connected_callback (WpCore * self,
* gpointer user_data)
* \endcode
* Emitted when the core is successfully connected to the PipeWire server
* \endparblock
*
* \par disconnected
* \parblock
* \code
* void
* disconnected_callback (WpCore * self,
* gpointer user_data)
* \endcode
* Emitted when the core is disconnected from the PipeWire server
* \endparblock
*/
struct _WpCore
{
WpObject parent;
/* main loop integration */
GMainContext *g_main_context;
/* extra properties */
WpProperties *properties;
/* pipewire main objects */
struct pw_context *pw_context;
struct pw_core *pw_core;
struct pw_core_info *info;
/* pipewire main listeners */
struct spa_hook core_listener;
struct spa_hook proxy_core_listener;
/* the main configuration file */
WpConf *conf;
WpRegistry registry;
GHashTable *async_tasks; // <int seq, GTask*>
};
enum {
PROP_0,
PROP_G_MAIN_CONTEXT,
PROP_PROPERTIES,
PROP_PW_CONTEXT,
PROP_PW_CORE,
PROP_CONF,
};
enum {
SIGNAL_CONNECTED,
SIGNAL_DISCONNECTED,
NUM_SIGNALS
};
static guint32 signals[NUM_SIGNALS];
G_DEFINE_TYPE (WpCore, wp_core, WP_TYPE_OBJECT)
static void
core_info (void *data, const struct pw_core_info * info)
{
WpCore *self = WP_CORE (data);
gboolean new_connection = (self->info == NULL);
self->info = pw_core_info_update (self->info, info);
wp_info_object (self, "connected to server: %s, cookie: %u",
self->info->name, self->info->cookie);
if (new_connection) {
g_signal_emit (self, signals[SIGNAL_CONNECTED], 0);
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_CONNECTED, 0);
}
}
static void
core_done (void *data, uint32_t id, int seq)
{
WpCore *self = WP_CORE (data);
g_autoptr (GTask) task = NULL;
g_hash_table_steal_extended (self->async_tasks, GINT_TO_POINTER (seq), NULL,
(gpointer *) &task);
wp_debug_object (self, "done, seq 0x%x, task " WP_OBJECT_FORMAT,
seq, WP_OBJECT_ARGS (task));
if (task)
g_task_return_boolean (task, TRUE);
}
static gboolean
core_disconnect_async (WpCore * self)
{
wp_core_disconnect (self);
return G_SOURCE_REMOVE;
}
static void
core_error (void *data, uint32_t id, int seq, int res, const char *message)
{
WpCore *self = WP_CORE (data);
/* protocol socket disconnected; schedule disconnecting our core */
if (id == 0 && res == -EPIPE) {
wp_core_idle_add_closure (self, NULL, g_cclosure_new_object (
G_CALLBACK (core_disconnect_async), G_OBJECT (self)));
}
}
static const struct pw_core_events core_events = {
PW_VERSION_CORE_EVENTS,
.info = core_info,
.done = core_done,
.error = core_error,
};
static gboolean
async_tasks_finish (gpointer key, gpointer value, gpointer user_data)
{
GTask *task = G_TASK (value);
g_return_val_if_fail (task, FALSE);
g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVARIANT, "core disconnected");
return TRUE;
}
static void
proxy_core_destroy (void *data)
{
WpCore *self = WP_CORE (data);
g_hash_table_foreach_remove (self->async_tasks, async_tasks_finish, NULL);
g_clear_pointer (&self->info, pw_core_info_free);
spa_hook_remove(&self->core_listener);
spa_hook_remove(&self->proxy_core_listener);
self->pw_core = NULL;
wp_debug_object (self, "emit disconnected");
g_signal_emit (self, signals[SIGNAL_DISCONNECTED], 0);
wp_object_update_features (WP_OBJECT (self), 0, WP_CORE_FEATURE_CONNECTED);
}
static const struct pw_proxy_events proxy_core_events = {
PW_VERSION_PROXY_EVENTS,
.destroy = proxy_core_destroy,
};
static void
wp_core_init (WpCore * self)
{
wp_registry_init (&self->registry);
self->async_tasks = g_hash_table_new_full (g_direct_hash, g_direct_equal,
NULL, g_object_unref);
wp_core_register_object (self,
g_object_new (WP_TYPE_INTERNAL_COMP_LOADER, NULL));
}
static void
wp_core_constructed (GObject *object)
{
WpCore *self = WP_CORE (object);
g_autoptr (GSource) source = NULL;
/* loop */
source = wp_loop_source_new ();
g_source_attach (source, self->g_main_context);
/* context */
if (!self->pw_context) {
struct pw_properties *p = NULL;
const gchar *str = NULL;
/* use our own configuration file, if specified */
if (self->conf) {
wp_info_object (self, "using configuration file: %s",
wp_conf_get_name (self->conf));
/* ensure we have our very own properties set,
since we are going to modify it */
self->properties = self->properties ?
wp_properties_ensure_unique_owner (self->properties) :
wp_properties_new_empty ();
/* load context.properties */
wp_conf_section_update_props (self->conf, "context.properties",
self->properties);
/* disable loading of a configuration file in pw_context */
wp_properties_set (self->properties, PW_KEY_CONFIG_NAME, "null");
}
/* properties are fully stored in the pw_context, no need to keep a copy */
p = self->properties ?
wp_properties_unref_and_take_pw_properties (self->properties) : NULL;
self->properties = NULL;
self->pw_context = pw_context_new (WP_LOOP_SOURCE(source)->loop, p,
sizeof (grefcount));
g_return_if_fail (self->pw_context);
/* use the same config option as pipewire to set the log level */
p = (struct pw_properties *) pw_context_get_properties (self->pw_context);
if (!g_getenv("WIREPLUMBER_DEBUG") &&
(str = pw_properties_get(p, "log.level")) != NULL) {
if (!wp_log_set_level (str))
wp_warning ("ignoring invalid log.level in config file: %s", str);
}
/* parse pw_context specific configuration sections */
if (self->conf)
wp_conf_parse_pw_context_sections (self->conf, self->pw_context);
/* Init refcount */
grefcount *rc = pw_context_get_user_data (self->pw_context);
g_return_if_fail (rc);
g_ref_count_init (rc);
} else {
/* Increase refcount */
grefcount *rc = pw_context_get_user_data (self->pw_context);
g_return_if_fail (rc);
g_ref_count_inc (rc);
}
G_OBJECT_CLASS (wp_core_parent_class)->constructed (object);
}
static void
wp_core_dispose (GObject * obj)
{
WpCore *self = WP_CORE (obj);
wp_registry_clear (&self->registry);
wp_object_update_features (WP_OBJECT (self), 0, WP_CORE_FEATURE_COMPONENTS);
G_OBJECT_CLASS (wp_core_parent_class)->dispose (obj);
}
static void
wp_core_finalize (GObject * obj)
{
WpCore *self = WP_CORE (obj);
grefcount *rc = pw_context_get_user_data (self->pw_context);
g_return_if_fail (rc);
wp_core_disconnect (self);
/* Clear pw-context if refcount reaches 0 */
if (g_ref_count_dec (rc))
g_clear_pointer (&self->pw_context, pw_context_destroy);
g_clear_pointer (&self->properties, wp_properties_unref);
g_clear_pointer (&self->g_main_context, g_main_context_unref);
g_clear_pointer (&self->async_tasks, g_hash_table_unref);
g_clear_object (&self->conf);
wp_debug_object (self, "WpCore destroyed");
G_OBJECT_CLASS (wp_core_parent_class)->finalize (obj);
}
static void
wp_core_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
WpCore *self = WP_CORE (object);
switch (property_id) {
case PROP_G_MAIN_CONTEXT:
g_value_set_boxed (value, self->g_main_context);
break;
case PROP_PROPERTIES:
g_value_take_boxed (value, wp_core_get_properties (self));
break;
case PROP_PW_CONTEXT:
g_value_set_pointer (value, self->pw_context);
break;
case PROP_PW_CORE:
g_value_set_pointer (value, self->pw_core);
break;
case PROP_CONF:
g_value_set_object (value, self->conf);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_core_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpCore *self = WP_CORE (object);
switch (property_id) {
case PROP_G_MAIN_CONTEXT:
self->g_main_context = g_value_dup_boxed (value);
break;
case PROP_PROPERTIES:
self->properties = g_value_dup_boxed (value);
break;
case PROP_PW_CONTEXT:
self->pw_context = g_value_get_pointer (value);
break;
case PROP_CONF:
self->conf = g_value_dup_object (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static WpObjectFeatures
wp_core_get_supported_features (WpObject * self)
{
return WP_CORE_FEATURE_CONNECTED |
WP_CORE_FEATURE_COMPONENTS;
}
enum {
STEP_CONNECT = WP_TRANSITION_STEP_CUSTOM_START,
STEP_LOAD_COMPONENTS,
};
static guint
wp_core_activate_get_next_step (WpObject * self,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
switch (step) {
case WP_TRANSITION_STEP_NONE:
if (missing & WP_CORE_FEATURE_CONNECTED)
return STEP_CONNECT;
G_GNUC_FALLTHROUGH;
case STEP_CONNECT:
if (missing & WP_CORE_FEATURE_COMPONENTS)
return STEP_LOAD_COMPONENTS;
G_GNUC_FALLTHROUGH;
case STEP_LOAD_COMPONENTS:
return WP_TRANSITION_STEP_NONE;
default:
return WP_TRANSITION_STEP_ERROR;
}
}
static void
on_components_loaded (WpCore * self, GAsyncResult *res,
WpTransition * transition)
{
g_autoptr (GError) error = NULL;
if (!wp_core_load_component_finish (self, res, &error)) {
wp_transition_return_error (transition, g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"failed to load components: %s", error->message));
return;
}
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
}
static void
wp_core_activate_execute_step (WpObject * object,
WpFeatureActivationTransition * transition, guint step,
WpObjectFeatures missing)
{
WpCore *self = WP_CORE (object);
switch (step) {
case STEP_CONNECT: {
wp_info_object (self, "connecting to pipewire...");
if (!wp_core_connect (self)) {
wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_SERVICE_UNAVAILABLE,
"Failed to connect to PipeWire"));
}
break;
}
case STEP_LOAD_COMPONENTS: {
g_autoptr (WpProperties) props = wp_core_get_properties (self);
if (spa_atob (wp_properties_get (props, "wireplumber.export-core"))) {
/* do not load any components on the export core */
wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
return;
}
else {
const gchar *profile = wp_properties_get (props, "wireplumber.profile");
wp_info_object (self,
"parsing & loading components for profile [%s]...", profile);
/* Load components that are defined in the configuration section */
wp_core_load_component (self, profile, "profile", NULL, NULL, NULL,
(GAsyncReadyCallback) on_components_loaded, transition);
}
break;
}
case WP_TRANSITION_STEP_ERROR:
break;
default:
g_assert_not_reached ();
}
}
static void
wp_core_deactivate (WpObject * self, WpObjectFeatures features)
{
if (features & WP_CORE_FEATURE_CONNECTED)
wp_core_disconnect (WP_CORE (self));
/* WP_CORE_FEATURE_COMPONENTS cannot be manually deactivated */
}
static void
wp_core_class_init (WpCoreClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpObjectClass *wpobject_class = (WpObjectClass *) klass;
object_class->constructed = wp_core_constructed;
object_class->dispose = wp_core_dispose;
object_class->finalize = wp_core_finalize;
object_class->get_property = wp_core_get_property;
object_class->set_property = wp_core_set_property;
wpobject_class->get_supported_features = wp_core_get_supported_features;
wpobject_class->activate_get_next_step = wp_core_activate_get_next_step;
wpobject_class->activate_execute_step = wp_core_activate_execute_step;
wpobject_class->deactivate = wp_core_deactivate;
g_object_class_install_property (object_class, PROP_G_MAIN_CONTEXT,
g_param_spec_boxed ("g-main-context", "g-main-context",
"A GMainContext to attach to", G_TYPE_MAIN_CONTEXT,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_PROPERTIES,
g_param_spec_boxed ("properties", "properties", "Extra properties",
WP_TYPE_PROPERTIES,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_PW_CONTEXT,
g_param_spec_pointer ("pw-context", "pw-context", "The pipewire context",
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_PW_CORE,
g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core",
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_CONF,
g_param_spec_object ("conf", "conf", "The main configuration file",
WP_TYPE_CONF,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
signals[SIGNAL_CONNECTED] = g_signal_new ("connected",
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
signals[SIGNAL_DISCONNECTED] = g_signal_new ("disconnected",
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
G_TYPE_NONE, 0);
}
/*!
* \brief Creates a new core object
*
* \ingroup wpcore
* \param context (transfer none) (nullable): the GMainContext to use for events
* \param conf (transfer full) (nullable): the main configuration file
* \param properties (transfer full) (nullable): additional properties, which
* are also passed to pw_context_new() and pw_context_connect()
* \returns (transfer full): a new WpCore
*/
WpCore *
wp_core_new (GMainContext * context, WpConf * conf, WpProperties * properties)
{
g_autoptr (WpConf) c = conf;
g_autoptr (WpProperties) props = properties;
return g_object_new (WP_TYPE_CORE,
"g-main-context", context,
"conf", conf,
"properties", properties,
"pw-context", NULL,
NULL);
}
/*!
* \brief Clones a core with the same context as \a self
*
* \ingroup wpcore
* \param self the core
* \returns (transfer full): the clone WpCore
*/
WpCore *
wp_core_clone (WpCore * self)
{
return g_object_new (WP_TYPE_CORE,
"core", self,
"g-main-context", self->g_main_context,
"conf", self->conf,
"properties", self->properties,
"pw-context", self->pw_context,
NULL);
}
static gboolean
find_export_core (gconstpointer a, gconstpointer b)
{
gpointer obj = (gpointer) a;
if (WP_IS_CORE ((gpointer) obj)) {
g_autoptr (WpProperties) props = wp_core_get_properties (WP_CORE (obj));
if (spa_atob (wp_properties_get (props, "wireplumber.export-core")))
return TRUE;
}
return FALSE;
}
/*!
* \brief Returns the special WpCore that is used to maintain a secondary
* connection to PipeWire, for exporting objects
*
* The export core is enabled by loading the built-in "export-core" component.
*
* \ingroup wpcore
* \param self the core
* \returns (transfer full): the export WpCore
*/
WpCore *
wp_core_get_export_core (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
return wp_core_find_object (self, find_export_core, NULL);
}
/*!
* \brief Gets the main configuration file of the core
*
* \ingroup wpcore
* \param self the core
* \returns (transfer full) (nullable): the main configuration file
*/
WpConf *
wp_core_get_conf (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
return self->conf ? g_object_ref (self->conf) : NULL;
}
/*!
* \brief Gets the GMainContext of the core
*
* \ingroup wpcore
* \param self the core
* \returns (transfer none) (nullable): the GMainContext that is in
* use by this core for events
*/
GMainContext *
wp_core_get_g_main_context (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
return self->g_main_context;
}
/*!
* \brief Gets the internal PipeWire context of the core
*
* \ingroup wpcore
* \param self the core
* \returns (transfer none): the internal pw_context object
*/
struct pw_context *
wp_core_get_pw_context (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
return self->pw_context;
}
/*!
* \brief Gets the internal PipeWire core of the core
*
* \ingroup wpcore
* \param self the core
* \returns (transfer none) (nullable): the internal pw_core object,
* or NULL if the core is not connected to PipeWire
*/
struct pw_core *
wp_core_get_pw_core (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
return self->pw_core;
}
/*!
* \brief Gets the virtual machine type of the core
*
* \ingroup wpcore
* \param self the core
* \returns (transfer full) (nullable): a comma separated string with all the
* virtual machine types that this core matches, or NULL if the core is not
* running in a virtual machine.
*
* \since 0.4.11
*/
gchar *
wp_core_get_vm_type (WpCore *self)
{
const struct spa_support *support;
uint32_t n_support;
struct spa_cpu *spa_cpu;
uint32_t vm_type;
gchar *res;
gboolean first = TRUE;
struct vm_type_info {
uint32_t type;
const gchar *name;
};
static struct vm_type_info vm_types[] = {
{SPA_CPU_VM_OTHER, "other"},
{SPA_CPU_VM_KVM, "kvm"},
{SPA_CPU_VM_QEMU, "qemu"},
{SPA_CPU_VM_BOCHS, "bochs"},
{SPA_CPU_VM_XEN, "zen"},
{SPA_CPU_VM_UML, "uml"},
{SPA_CPU_VM_VMWARE, "vmware"},
{SPA_CPU_VM_ORACLE, "oracle"},
{SPA_CPU_VM_MICROSOFT, "microsoft"},
{SPA_CPU_VM_ZVM, "zvm"},
{SPA_CPU_VM_PARALLELS, "parallels"},
{SPA_CPU_VM_BHYVE, "bhyve"},
{SPA_CPU_VM_QNX, "qnx"},
{SPA_CPU_VM_ACRN, "acrn"},
{SPA_CPU_VM_POWERVM, "powervm"},
{0, NULL},
};
g_return_val_if_fail (WP_IS_CORE (self), NULL);
g_return_val_if_fail (self->pw_context, NULL);
/* Get spa_cpu */
support = pw_context_get_support (self->pw_context, &n_support);
spa_cpu = spa_support_find (support, n_support, SPA_TYPE_INTERFACE_CPU);
g_return_val_if_fail (spa_cpu, NULL);
/* Just return NULL if CPU is not a VM */
vm_type = spa_cpu_get_vm_type (spa_cpu);
if (vm_type == SPA_CPU_VM_NONE)
return NULL;
/* Otherwise return a string with all matching VM types */
res = g_strdup ("");
for (guint i = 0; vm_types[i].name; i++) {
if (vm_type & vm_types[i].type) {
gchar *tmp = g_strdup_printf ("%s%s%s", res, first ? "": ",",
vm_types[i].name);
g_free (res);
res = tmp;
first = FALSE;
}
}
return res;
}
/*!
* \brief Connects this core to the PipeWire server.
*
* When connection succeeds, the WpCore \c "connected" signal is emitted.
*
* \ingroup wpcore
* \param self the core
* \returns TRUE if the core is effectively connected or FALSE if
* connection failed
*/
gboolean
wp_core_connect (WpCore *self)
{
struct pw_properties *p = NULL;
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
/* Don't do anything if core is already connected */
if (self->pw_core)
return TRUE;
/* Connect */
p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
self->pw_core = pw_context_connect (self->pw_context, p, 0);
if (!self->pw_core)
return FALSE;
/* Add the core listeners */
pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
&self->proxy_core_listener, &proxy_core_events, self);
/* Add the registry listener */
wp_registry_attach (&self->registry, self->pw_core);
return TRUE;
}
/*!
* \brief Disconnects this core from the PipeWire server.
*
* This also effectively destroys all WpCore objects that were created through
* the registry, destroys the pw_core and finally emits the WpCore
* \c "disconnected" signal.
*
* \ingroup wpcore
* \param self the core
*/
void
wp_core_disconnect (WpCore *self)
{
wp_registry_detach (&self->registry);
/* pw_core_disconnect destroys the core proxy
and we continue in proxy_core_destroy() */
if (self->pw_core)
pw_core_disconnect (self->pw_core);
}
/*!
* \brief Checks if the core is connected to PipeWire
*
* \ingroup wpcore
* \param self the core
* \returns TRUE if the core is connected to PipeWire, FALSE otherwise
*/
gboolean
wp_core_is_connected (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
return self->pw_core && self->info;
}
/*!
* \brief Gets the bound id of the client object that is created as a result
* of this core being connected to the PipeWire daemon
*
* \ingroup wpcore
* \since 0.4.16
* \param self the core
* \returns the bound id of this client
*/
guint32
wp_core_get_own_bound_id (WpCore * self)
{
struct pw_client *client;
g_return_val_if_fail (wp_core_is_connected (self), SPA_ID_INVALID);
client = pw_core_get_client (self->pw_core);
return pw_proxy_get_bound_id ((struct pw_proxy *) client);
}
/*!
* \brief Gets the cookie of the core's connected PipeWire instance
*
* \ingroup wpcore
* \param self the core
* \returns The cookie of the PipeWire instance that \a self is connected to.
* The cookie is a unique random number for identifying an instance of
* PipeWire
*/
guint32
wp_core_get_remote_cookie (WpCore * self)
{
g_return_val_if_fail (wp_core_is_connected (self), 0);
return self->info->cookie;
}
/*!
* \brief Gets the name of the core's connected PipeWire instance
* \ingroup wpcore
* \param self the core
* \returns The name of the PipeWire instance that \a self is connected to
*/
const gchar *
wp_core_get_remote_name (WpCore * self)
{
g_return_val_if_fail (wp_core_is_connected (self), NULL);
return self->info->name;
}
/*!
* \brief Gets the user name of the core's connected PipeWire instance
* \ingroup wpcore
* \param self the core
* \returns The name of the user that started the PipeWire instance that
* \a self is connected to
*/
const gchar *
wp_core_get_remote_user_name (WpCore * self)
{
g_return_val_if_fail (wp_core_is_connected (self), NULL);
return self->info->user_name;
}
/*!
* \brief Gets the host name of the core's connected PipeWire instance
* \ingroup wpcore
* \param self the core
* \returns The name of the host where the PipeWire instance that
* \a self is connected to is running on
*/
const gchar *
wp_core_get_remote_host_name (WpCore * self)
{
g_return_val_if_fail (wp_core_is_connected (self), NULL);
return self->info->host_name;
}
/*!
* \brief Gets the version of the core's connected PipeWire instance
* \ingroup wpcore
* \param self the core
* \returns The version of the PipeWire instance that \a self is connected to
*/
const gchar *
wp_core_get_remote_version (WpCore * self)
{
g_return_val_if_fail (wp_core_is_connected (self), NULL);
return self->info->version;
}
/*!
* \brief Gets the properties of the core's connected PipeWire instance
* \ingroup wpcore
* \param self the core
* \returns (transfer full): the properties of the PipeWire instance that
* \a self is connected to
*/
WpProperties *
wp_core_get_remote_properties (WpCore * self)
{
g_return_val_if_fail (wp_core_is_connected (self), NULL);
return wp_properties_new_wrap_dict (self->info->props);
}
/*!
* \brief Gets the properties of the core
* \ingroup wpcore
* \param self the core
* \returns (transfer full): the properties of \a self
*/
WpProperties *
wp_core_get_properties (WpCore * self)
{
g_return_val_if_fail (WP_IS_CORE (self), NULL);
/* pw_core has all the properties of the pw_context,
plus our updates, passed in pw_context_connect() */
if (self->pw_core)
return wp_properties_new_wrap (pw_core_get_properties (self->pw_core));
/* if there is no connection yet, return the properties of the context */
else if (!self->properties)
return wp_properties_new_wrap (pw_context_get_properties (self->pw_context));
/* ... plus any further updates that we got from wp_core_update_properties() */
else {
/* we need to copy here in order to augment with the updates */
WpProperties *ret =
wp_properties_new_copy (pw_context_get_properties (self->pw_context));
wp_properties_update (ret, self->properties);
return ret;
}
}
/*!
* \brief Updates the properties of \a self on the connection, making them
* appear on the client object that represents this connection.
*
* If \a self is not connected yet, these properties are stored and passed to
* pw_context_connect() when connecting.
*
* \ingroup wpcore
* \param self the core
* \param updates (transfer full): updates to apply to the properties of
* \a self; this does not need to include properties that have not changed
*/
void
wp_core_update_properties (WpCore * self, WpProperties * updates)
{
g_autoptr (WpProperties) upd = updates;
g_return_if_fail (WP_IS_CORE (self));
g_return_if_fail (updates != NULL);
/* store updates locally so that
- they persist after disconnection
- we can pass them to pw_context_connect */
if (!self->properties)
self->properties = wp_properties_new_empty ();
wp_properties_update (self->properties, upd);
if (self->pw_core)
pw_core_update_properties (self->pw_core, wp_properties_peek_dict (upd));
}
/*!
* \brief Adds an idle callback to be called in the same GMainContext as the
* one used by this core.
*
* This is essentially the same as g_idle_add_full(), but it adds the created
* GSource on the GMainContext used by this core instead of the default context.
*
* \ingroup wpcore
* \param self the core
* \param source (out) (optional): the source
* \param function (scope notified): the function to call
* \param data (closure): data to pass to \a function
* \param destroy (nullable): a function to destroy \a data
*/
void
wp_core_idle_add (WpCore * self, GSource **source, GSourceFunc function,
gpointer data, GDestroyNotify destroy)
{
g_autoptr (GSource) s = NULL;
g_return_if_fail (WP_IS_CORE (self));
s = g_idle_source_new ();
g_source_set_callback (s, function, data, destroy);
g_source_attach (s, self->g_main_context);
if (source)
*source = g_source_ref (s);
}
/*!
* \brief Adds an idle callback to be called in the same GMainContext as
* the one used by this core.
*
* This is the same as wp_core_idle_add(), but it allows you to specify a
* GClosure instead of a C callback.
*
* \ingroup wpcore
* \param self the core
* \param source (out) (optional): the source
* \param closure the closure to invoke
*/
void
wp_core_idle_add_closure (WpCore * self, GSource **source, GClosure * closure)
{
g_autoptr (GSource) s = NULL;
g_return_if_fail (WP_IS_CORE (self));
g_return_if_fail (closure != NULL);
s = g_idle_source_new ();
g_source_set_closure (s, closure);
g_source_attach (s, self->g_main_context);
if (source)
*source = g_source_ref (s);
}
/*!
* \brief Adds a timeout callback to be called at regular intervals in the same
* GMainContext as the one used by this core.
*
* The function is called repeatedly until it returns FALSE, at which point
* the timeout is automatically destroyed and the function will not be called
* again. The first call to the function will be at the end of the first
* interval.
* This is essentially the same as g_timeout_add_full(), but it adds
* the created GSource on the GMainContext used by this core instead of the
* default context.
*
* \ingroup wpcore
* \param self the core
* \param source (out) (optional): the source
* \param timeout_ms the timeout in milliseconds
* \param function (scope notified): the function to call
* \param data (closure): data to pass to \a function
* \param destroy (nullable): a function to destroy \a data
*/
void
wp_core_timeout_add (WpCore * self, GSource **source, guint timeout_ms,
GSourceFunc function, gpointer data, GDestroyNotify destroy)
{
g_autoptr (GSource) s = NULL;
g_return_if_fail (WP_IS_CORE (self));
s = g_timeout_source_new (timeout_ms);
g_source_set_callback (s, function, data, destroy);
g_source_attach (s, self->g_main_context);
if (source)
*source = g_source_ref (s);
}
/*!
* \brief Adds a timeout callback to be called at regular intervals in the same
* GMainContext as the one used by this core.
*
* This is the same as wp_core_timeout_add(), but it allows you to specify a
* GClosure instead of a C callback.
*
* \ingroup wpcore
* \param self the core
* \param source (out) (optional): the source
* \param timeout_ms the timeout in milliseconds
* \param closure the closure to invoke
*/
void
wp_core_timeout_add_closure (WpCore * self, GSource **source, guint timeout_ms,
GClosure * closure)
{
g_autoptr (GSource) s = NULL;
g_return_if_fail (WP_IS_CORE (self));
g_return_if_fail (closure != NULL);
s = g_timeout_source_new (timeout_ms);
g_source_set_closure (s, closure);
g_source_attach (s, self->g_main_context);
if (source)
*source = g_source_ref (s);
}
/*!
* \brief Asks the PipeWire server to call the \a callback via an event.
*
* Since methods are handled in-order and events are delivered
* in-order, this can be used as a barrier to ensure all previous
* methods and the resulting events have been handled.
*
* In both success and error cases, \a callback is always called.
* Use wp_core_sync_finish() from within the \a callback to determine whether
* the operation completed successfully or if an error occurred.
*
* \ingroup wpcore
* \param self the core
* \param cancellable (nullable): a GCancellable to cancel the operation
* \param callback (scope async): a function to call when the operation is done
* \param user_data (closure): data to pass to \a callback
* \returns TRUE if the sync operation was started, FALSE if an error
* occurred before returning from this function
*/
gboolean
wp_core_sync (WpCore * self, GCancellable * cancellable,
GAsyncReadyCallback callback, gpointer user_data)
{
return wp_core_sync_closure (self, cancellable,
g_cclosure_new (G_CALLBACK (callback), user_data, NULL));
}
static void
invoke_closure (GObject * obj, GAsyncResult * res, gpointer data)
{
GClosure *closure = data;
GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };
g_value_init (&values[0], G_TYPE_OBJECT);
g_value_init (&values[1], G_TYPE_OBJECT);
g_value_set_object (&values[0], obj);
g_value_set_object (&values[1], res);
g_closure_invoke (closure, NULL, 2, values, NULL);
g_value_unset (&values[0]);
g_value_unset (&values[1]);
g_closure_unref (closure);
}
/*!
* \brief Asks the PipeWire server to invoke the \a closure via an event.
*
* Since methods are handled in-order and events are delivered
* in-order, this can be used as a barrier to ensure all previous
* methods and the resulting events have been handled.
*
* In both success and error cases, \a closure is always invoked.
* Use wp_core_sync_finish() from within the \a closure to determine whether
* the operation completed successfully or if an error occurred.
*
* \ingroup wpcore
* \since 0.4.6
* \param self the core
* \param cancellable (nullable): a GCancellable to cancel the operation
* \param closure (transfer floating): a closure to invoke when the operation
* is done
* \returns TRUE if the sync operation was started, FALSE if an error
* occurred before returning from this function
*/
gboolean
wp_core_sync_closure (WpCore * self, GCancellable * cancellable,
GClosure * closure)
{
g_autoptr (GTask) task = NULL;
int seq;
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
g_return_val_if_fail (closure, FALSE);
closure = g_closure_ref (closure);
g_closure_sink (closure);
if (G_CLOSURE_NEEDS_MARSHAL (closure))
g_closure_set_marshal (closure, g_cclosure_marshal_VOID__OBJECT);
task = g_task_new (self, cancellable, invoke_closure, closure);
if (G_UNLIKELY (!self->pw_core)) {
g_warn_if_reached ();
g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVARIANT, "No pipewire core");
return FALSE;
}
seq = pw_core_sync (self->pw_core, 0, 0);
if (G_UNLIKELY (seq < 0)) {
g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_OPERATION_FAILED, "pw_core_sync failed: %s",
g_strerror (-seq));
return FALSE;
}
wp_debug_object (self, "sync, seq 0x%x, task " WP_OBJECT_FORMAT,
seq, WP_OBJECT_ARGS (task));
g_hash_table_insert (self->async_tasks, GINT_TO_POINTER (seq),
g_steal_pointer (&task));
return TRUE;
}
/*!
* \brief This function is meant to be called from within the callback of
* wp_core_sync() in order to determine the success or failure of the operation.
*
* \ingroup wpcore
* \param self the core
* \param res a GAsyncResult
* \param error (out) (optional): the error that occurred, if any
* \returns TRUE if the operation succeeded, FALSE otherwise
*/
gboolean
wp_core_sync_finish (WpCore * self, GAsyncResult * res, GError ** error)
{
g_return_val_if_fail (WP_IS_CORE (self), FALSE);
g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
return g_task_propagate_boolean (G_TASK (res), error);
}
/*!
* \brief Finds a registered object
*
* \param self the core
* \param func (scope call): a function that takes the object being searched
* as the first argument and \a data as the second. it should return TRUE if
* the object is found or FALSE otherwise
* \param data the second argument to \a func
* \returns (transfer full) (type GObject*) (nullable): the registered object
* or NULL if not found
*/
gpointer
wp_core_find_object (WpCore * self, GEqualFunc func, gconstpointer data)
{
GObject *object;
guint i;
g_return_val_if_fail (WP_IS_CORE (self), NULL);
/* prevent bad things when called from within wp_registry_clear() */
if (G_UNLIKELY (!self->registry.objects))
return NULL;
for (i = 0; i < self->registry.objects->len; i++) {
object = g_ptr_array_index (self->registry.objects, i);
if (func (object, data))
return g_object_ref (object);
}
return NULL;
}
/*!
* \brief Registers \a obj with the core, making it appear on WpObjectManager
* instances as well.
*
* The core will also maintain a ref to that object until it
* is removed.
*
* \ingroup wpcore
* \param self the core
* \param obj (transfer full) (type GObject*): the object to register
*/
void
wp_core_register_object (WpCore * self, gpointer obj)
{
g_return_if_fail (WP_IS_CORE (self));
g_return_if_fail (G_IS_OBJECT (obj));
/* prevent bad things when called from within wp_registry_clear() */
if (G_UNLIKELY (!self->registry.objects)) {
g_object_unref (obj);
return;
}
/* ensure the registered object is associated with this core */
if (WP_IS_OBJECT (obj)) {
g_autoptr (WpCore) obj_core = wp_object_get_core (WP_OBJECT (obj));
g_return_if_fail (obj_core == self);
}
g_ptr_array_add (self->registry.objects, obj);
/* notify object managers */
wp_registry_notify_add_object (&self->registry, obj);
}
/*!
* \brief Detaches and unrefs the specified object from this core.
*
* \ingroup wpcore
* \param self the core
* \param obj (transfer none) (type GObject*): a pointer to the object to remove
*/
void
wp_core_remove_object (WpCore * self, gpointer obj)
{
g_return_if_fail (WP_IS_CORE (self));
g_return_if_fail (G_IS_OBJECT (obj));
/* prevent bad things when called from within wp_registry_clear() */
if (G_UNLIKELY (!self->registry.objects))
return;
/* notify object managers */
wp_registry_notify_rm_object (&self->registry, obj);
g_ptr_array_remove_fast (self->registry.objects, obj);
}
/*!
* \brief Test if a global feature is provided
*
* \ingroup wpcore
* \param self the core
* \param feature the feature name
* \returns TRUE if the feature is provided, FALSE otherwise
*/
gboolean
wp_core_test_feature (WpCore * self, const gchar * feature)
{
return g_ptr_array_find_with_equal_func (self->registry.features, feature,
g_str_equal, NULL);
}
WpRegistry *
wp_core_get_registry (WpCore * self)
{
return &self->registry;
}
WpCore *
wp_registry_get_core (WpRegistry * self)
{
return SPA_CONTAINER_OF (self, WpCore, registry);
}