conf: refactor component loading to use a dependency system

Each component can now list required and optional dependencies,
using the component feature names to match other components.
In addition, each component feature can be declared as required, optional
or disabled, making optional components easier to deal with.
The component flags (ifexists, nofail) have been removed.

Using virtual components, this system also allows easier customization
of which components should be loaded for a specific configuration,
without requiring the user to copy the list of components and edit it.

Also bump the required glib version to 2.68 for g_assert_cmpstrv()
This commit is contained in:
George Kiagiadakis 2023-06-15 21:27:13 +03:00
parent 9c2d25b985
commit e738076cb0
6 changed files with 849 additions and 323 deletions

View file

@ -14,137 +14,152 @@ WP_DEFINE_LOCAL_LOG_TOPIC ("wp-internal-comp-loader")
/*** ComponentData ***/
enum
{
NO_FAIL = 0x1,
IF_EXISTS = 0x2
};
typedef enum {
FEATURE_STATE_DISABLED,
FEATURE_STATE_OPTIONAL,
FEATURE_STATE_REQUIRED
} FeatureState;
typedef struct _ComponentData ComponentData;
struct _ComponentData
{
grefcount ref;
/* an identifier for this component that is understandable by the end user */
gchar *printable_id;
/* the provided feature name (points to same storage as the id) or NULL */
gchar *provides;
/* the original state of the feature (required / optional / disabled) */
FeatureState state;
/* other fields extracted as-is from the json description */
gchar *name;
gchar *type;
gint priority;
gint flags;
WpSpaJson *deps;
WpSpaJson *arguments;
GPtrArray *requires; /* value-type: string (owned) */
GPtrArray *wants; /* value-type: string (owned) */
/* TRUE when the component is in the final sorted list */
gboolean visited;
/* one of the components that requires this one with a strong
dependency chain (i.e. there is a required component that requires
this one, directly or indirectly) */
ComponentData *required_by;
};
typedef struct _ComponentData ComponentData;
static gint
component_cmp_func (const ComponentData *a, const ComponentData *b)
{
return b->priority - a->priority;
}
static void component_data_free (ComponentData * self);
static gint
component_equal_func (const ComponentData *a, ComponentData * b)
static ComponentData *
component_data_ref (ComponentData *self)
{
return
g_str_equal (a->name, b->name) && g_str_equal (a->type, b->type) ? 0 : 1;
g_ref_count_inc (&self->ref);
return self;
}
static void
component_data_free (ComponentData *self)
component_data_unref (ComponentData *self)
{
g_clear_pointer (&self->name, g_free);
g_clear_pointer (&self->type, g_free);
g_clear_pointer (&self->deps, wp_spa_json_unref);
g_slice_free (ComponentData, self);
if (self && g_ref_count_dec (&self->ref))
component_data_free (self);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComponentData, component_data_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComponentData, component_data_unref)
/*** components parser ***/
static gint
pick_default_component_priority (const char *type)
static FeatureState
get_feature_state (WpProperties * dict, const gchar * feature)
{
if (g_str_equal (type, "module"))
/* regular module default priority */
return 110;
else if (g_str_equal (type, "script/lua"))
/* Lua Script default priority */
return 100;
const gchar *value = wp_properties_get (dict, feature);
return 100;
if (!value || g_str_equal (value, "optional"))
return FEATURE_STATE_OPTIONAL;
else if (g_str_equal (value, "required"))
return FEATURE_STATE_REQUIRED;
else if (g_str_equal (value, "disabled"))
return FEATURE_STATE_DISABLED;
else {
wp_warning ("invalid feature state '%s' specified in configuration for '%s'",
value, feature);
wp_warning ("considering '%s' to be optional", feature);
return FEATURE_STATE_OPTIONAL;
}
}
static void
json_to_components_list (GList **list, WpSpaJson *json)
static ComponentData *
component_data_new_from_json (WpSpaJson * json, WpProperties * features,
GError ** error)
{
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
g_autoptr (ComponentData) comp = NULL;
g_autoptr (WpSpaJson) deps = NULL;
it = wp_spa_json_new_iterator (json);
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *cjson = g_value_get_boxed (&item);
g_autoptr (ComponentData) comp = g_slice_new0 (ComponentData);
g_autoptr (WpSpaJson) deps = NULL;
g_autoptr (WpSpaJson) flags = NULL;
if (!wp_spa_json_is_object (json)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"expected JSON object instead of: %.*s", (int) wp_spa_json_get_size (json),
wp_spa_json_get_data (json));
return NULL;
}
/* Parse name and type (mandatory) */
if (!wp_spa_json_is_object (cjson) ||
!wp_spa_json_object_get (cjson,
"name", "s", &comp->name,
"type", "s", &comp->type,
NULL)) {
wp_warning ("component must have both a 'name' and a 'type'");
continue;
}
comp = g_new0 (ComponentData, 1);
g_ref_count_init (&comp->ref);
comp->requires = g_ptr_array_new_with_free_func (g_free);
comp->wants = g_ptr_array_new_with_free_func (g_free);
/* Parse priority (optional) */
if (!wp_spa_json_object_get (cjson, "priority", "i", &comp->priority,
NULL))
comp->priority = pick_default_component_priority (comp->type);
if (!wp_spa_json_object_get (json, "type", "s", &comp->type, NULL)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"component 'type' is required at: %.*s", (int) wp_spa_json_get_size (json),
wp_spa_json_get_data (json));
return NULL;
}
/* Parse deps (optional) */
if (wp_spa_json_object_get (cjson, "deps", "J", &deps, NULL)) {
if (wp_spa_json_is_array (deps)) {
comp->deps = g_steal_pointer (&deps);
} else {
wp_warning ("skipping component %s as its 'deps' is not a JSON array",
comp->name);
continue;
}
}
wp_spa_json_object_get (json, "name", "s", &comp->name, NULL);
wp_spa_json_object_get (json, "arguments", "J", &comp->arguments, NULL);
/* Parse flags (optional) */
if (wp_spa_json_object_get (cjson, "flags", "J", &flags, NULL)) {
if (flags && wp_spa_json_is_array (flags)) {
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (flags);
g_auto (GValue) item = G_VALUE_INIT;
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *flag = g_value_get_boxed (&item);
g_autofree gchar *flag_str = wp_spa_json_parse_string (flag);
if (g_str_equal (flag_str, "ifexists"))
comp->flags |= IF_EXISTS;
else if (g_str_equal (flag_str, "nofail"))
comp->flags |= NO_FAIL;
else
wp_warning ("flag '%s' is not valid for component '%s'", flag_str,
comp->name);
}
} else {
wp_warning ("skipping component %s as its 'flags' is not a JSON array",
comp->name);
continue;
}
}
/* Insert component into the list if it does not exist */
if (!g_list_find_custom (*list, comp,
(GCompareFunc) component_equal_func)) {
wp_trace ("appended component '%s' of type '%s' with priority '%d'",
comp->name, comp->type, comp->priority);
*list = g_list_insert_sorted (*list, g_steal_pointer (&comp),
(GCompareFunc) component_cmp_func);
if (wp_spa_json_object_get (json, "provides", "s", &comp->provides, NULL)) {
comp->state = get_feature_state (features, comp->provides);
if (comp->name) {
comp->printable_id =
g_strdup_printf ("%s [%s: %s]", comp->provides, comp->type, comp->name);
} else {
wp_debug ("ignoring component '%s' as it is already defined previously",
comp->name);
comp->printable_id = g_strdup_printf ("%s [%s]", comp->provides, comp->type);
}
} else {
comp->provides = NULL;
comp->state = FEATURE_STATE_REQUIRED;
comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
}
if (wp_spa_json_object_get (json, "requires", "J", &deps, NULL)) {
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (deps);
g_auto (GValue) item = G_VALUE_INIT;
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *dep = g_value_get_boxed (&item);
g_ptr_array_add (comp->requires, wp_spa_json_to_string (dep));
}
}
if (wp_spa_json_object_get (json, "wants", "J", &deps, NULL)) {
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (deps);
g_auto (GValue) item = G_VALUE_INIT;
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *dep = g_value_get_boxed (&item);
g_ptr_array_add (comp->wants, wp_spa_json_to_string (dep));
}
}
return g_steal_pointer (&comp);
}
static void
component_data_free (ComponentData * self)
{
g_clear_pointer (&self->provides, g_free);
g_clear_pointer (&self->printable_id, g_free);
g_clear_pointer (&self->name, g_free);
g_clear_pointer (&self->type, g_free);
g_clear_pointer (&self->arguments, wp_spa_json_unref);
g_clear_pointer (&self->requires, g_ptr_array_unref);
g_clear_pointer (&self->wants, g_ptr_array_unref);
g_free (self);
}
/*** WpComponentArrayLoadTask ***/
@ -152,17 +167,22 @@ json_to_components_list (GList **list, WpSpaJson *json)
struct _WpComponentArrayLoadTask
{
WpTransition parent;
/* the input json object */
WpSpaJson *json;
GList *components;
GList *components_iter;
/* all components that provide a feature; key: comp->provides, value: comp */
GHashTable *feat_components;
/* the final sorted list of components to load */
GPtrArray *components;
/* iterator in the components array above */
ComponentData **components_iter;
/* the current component being loaded */
ComponentData *curr_component;
};
enum {
STEP_PARSE = WP_TRANSITION_STEP_CUSTOM_START,
STEP_LOAD_NEXT_1,
STEP_LOAD_NEXT_2,
STEP_CLEANUP,
STEP_GET_NEXT,
STEP_LOAD_NEXT,
};
G_DECLARE_FINAL_TYPE (WpComponentArrayLoadTask, wp_component_array_load_task,
@ -175,32 +195,6 @@ wp_component_array_load_task_init (WpComponentArrayLoadTask * self)
{
}
static gboolean
component_meets_dependencies (WpCore *core, ComponentData *comp)
{
g_autoptr (WpConf) conf = NULL;
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
if (!comp->deps)
return TRUE;
/* Note that we consider the dependency valid by default if it is not
* found in the settings configuration section */
conf = wp_conf_get_instance (core);
it = wp_spa_json_new_iterator (comp->deps);
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *dep = g_value_get_boxed (&item);
g_autofree gchar *dep_str = wp_spa_json_parse_string (dep);
gboolean value = wp_conf_get_value_boolean (conf,
"wireplumber.settings", dep_str, TRUE);
if (!value)
return FALSE;
}
return TRUE;
}
static guint
wp_component_array_load_task_get_next_step (WpTransition * transition, guint step)
{
@ -208,17 +202,164 @@ wp_component_array_load_task_get_next_step (WpTransition * transition, guint ste
switch (step) {
case WP_TRANSITION_STEP_NONE: return STEP_PARSE;
case STEP_PARSE: return STEP_LOAD_NEXT_1;
case STEP_LOAD_NEXT_1:
return (self->components_iter) ? STEP_LOAD_NEXT_2 : STEP_CLEANUP;
case STEP_LOAD_NEXT_2:
return (self->components_iter) ? STEP_LOAD_NEXT_1 : STEP_CLEANUP;
case STEP_CLEANUP: return WP_TRANSITION_STEP_NONE;
case STEP_PARSE: return STEP_GET_NEXT;
case STEP_GET_NEXT:
return (self->curr_component) ? STEP_LOAD_NEXT : WP_TRANSITION_STEP_NONE;
case STEP_LOAD_NEXT: return STEP_GET_NEXT;
default:
g_return_val_if_reached (WP_TRANSITION_STEP_ERROR);
}
}
static gchar *
print_dep_chain (ComponentData *comp)
{
GString *str = g_string_new (NULL);
while (comp->required_by) {
comp = comp->required_by;
g_string_prepend (str, comp->printable_id);
if (comp->required_by)
g_string_prepend (str, " -> ");
}
return g_string_free (str, FALSE);
}
static gboolean
add_component (ComponentData * comp, gboolean strongly_required,
WpComponentArrayLoadTask * self, GError ** error)
{
if (comp->visited || comp->state == FEATURE_STATE_DISABLED)
return TRUE;
comp->visited = TRUE;
/* recursively visit all the required features */
for (guint i = 0; i < comp->requires->len; i++) {
const gchar *dependency = g_ptr_array_index (comp->requires, i);
ComponentData *req_comp =
g_hash_table_lookup (self->feat_components, dependency);
if (!req_comp) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"no component provides '%s', required by '%s'", dependency,
comp->printable_id);
return FALSE;
}
/* make a note if there is a strong dependency chain */
if (strongly_required && !req_comp->required_by) {
if (req_comp->state == FEATURE_STATE_OPTIONAL) {
req_comp->required_by = comp;
}
else if (req_comp->state == FEATURE_STATE_DISABLED) {
g_autofree gchar *dep_chain = print_dep_chain (comp);
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"component '%s' is disabled, required by %s",
req_comp->printable_id, dep_chain);
return FALSE;
}
}
if (!add_component (req_comp, strongly_required, self, error))
return FALSE;
}
/* recursively visit all the optionally wanted features */
for (guint i = 0; i < comp->wants->len; i++) {
const gchar *dependency = g_ptr_array_index (comp->wants, i);
ComponentData *wanted_comp =
g_hash_table_lookup (self->feat_components, dependency);
if (!wanted_comp) {
/* in theory we could ignore this, but it's most likely a typo,
so let's be strict about it and let the user correct it */
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"no component provides '%s', wanted by '%s'", dependency,
comp->printable_id);
return FALSE;
}
if (!add_component (wanted_comp, FALSE, self, error))
return FALSE;
}
/* append component to the sorted list after all its dependencies */
g_ptr_array_add (self->components, component_data_ref (comp));
return TRUE;
}
static WpProperties *
conf_get_features_section (WpComponentArrayLoadTask * self)
{
WpProperties *props = wp_properties_new_empty ();
WpCore *core = wp_transition_get_data (WP_TRANSITION (self));
g_autoptr (WpConf) conf = wp_conf_get_instance (core);
g_autoptr (WpSpaJson) json =
wp_conf_get_section (conf, "wireplumber.features", NULL);
if (json)
wp_properties_update_from_json (props, json);
return props;
}
static gboolean
parse_components (WpComponentArrayLoadTask * self, GError ** error)
{
/* all the parsed components that are explicitly required */
g_autoptr (GPtrArray) required_components = NULL;
g_autoptr (WpProperties) conf = conf_get_features_section (self);
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
if (!wp_spa_json_is_array (self->json)) {
g_set_error (error,
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"components section is not a JSON array");
return FALSE;
}
self->feat_components = g_hash_table_new_full (g_str_hash, g_str_equal,
NULL, (GDestroyNotify) component_data_unref);
self->components = g_ptr_array_new_with_free_func (
(GDestroyNotify) component_data_unref);
required_components = g_ptr_array_new_with_free_func (
(GDestroyNotify) component_data_unref);
/* first parse each component from its json description */
it = wp_spa_json_new_iterator (self->json);
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaJson *cjson = g_value_get_boxed (&item);
GError *e = NULL;
g_autoptr (ComponentData) comp = NULL;
if (!(comp = component_data_new_from_json (cjson, conf, &e))) {
g_propagate_error (error, e);
return FALSE;
}
if (comp->state == FEATURE_STATE_REQUIRED)
g_ptr_array_add (required_components, component_data_ref (comp));
if (comp->provides)
g_hash_table_insert (self->feat_components, comp->provides,
component_data_ref (comp));
}
/* topological sorting based on depth-first search */
for (guint i = 0; i < required_components->len; i++) {
ComponentData *comp = g_ptr_array_index (required_components, i);
GError *e = NULL;
if (!add_component (comp, TRUE, self, &e)) {
g_propagate_error (error, e);
return FALSE;
}
}
/* terminate the array with NULL */
g_ptr_array_add (self->components, NULL);
/* clear feat_components, they are no longer needed */
g_clear_pointer (&self->feat_components, g_hash_table_unref);
return TRUE;
}
static void
on_component_loaded (WpCore *core, GAsyncResult *res, gpointer data)
{
@ -228,26 +369,30 @@ on_component_loaded (WpCore *core, GAsyncResult *res, gpointer data)
g_return_if_fail (self->curr_component);
if (!wp_core_load_component_finish (core, res, &error)) {
if (self->curr_component->flags & IF_EXISTS &&
error->domain == G_IO_ERROR &&
error->code == G_IO_ERROR_NOT_FOUND) {
wp_info_object (self, "skipping component '%s' with 'ifexists' flag "
"because the file does not exist", self->curr_component->name);
goto next;
} else if (self->curr_component->flags & NO_FAIL) {
wp_info_object (self, "skipping component '%s' with 'nofail' flag "
"due to error: %s", self->curr_component->name, error->message);
goto next;
// if it was required, fail
if (self->curr_component->state == FEATURE_STATE_REQUIRED) {
wp_transition_return_error (WP_TRANSITION (self), g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"failed to load required component '%s': %s",
self->curr_component->printable_id, error->message));
return;
}
// if it was optional, check if strongly_required
else if (self->curr_component->state == FEATURE_STATE_OPTIONAL &&
self->curr_component->required_by) {
g_autofree gchar *dep_chain = print_dep_chain (self->curr_component);
wp_transition_return_error (WP_TRANSITION (self), g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"failed to load component '%s' (required by %s): %s",
self->curr_component->printable_id, dep_chain, error->message));
return;
}
else {
wp_notice_object (core, "optional component '%s' failed to load: %s",
self->curr_component->printable_id, error->message);
}
wp_transition_return_error (WP_TRANSITION (self), g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
"failed to activate component '%s': %s", self->curr_component->name,
error->message));
return;
}
next:
wp_transition_advance (WP_TRANSITION (self));
}
@ -255,52 +400,62 @@ static void
wp_component_array_load_task_execute_step (WpTransition * transition, guint step)
{
WpComponentArrayLoadTask *self = WP_COMPONENT_ARRAY_LOAD_TASK (transition);
WpCore *core = wp_transition_get_source_object (transition);
WpCore *core = wp_transition_get_data(transition);
switch (step) {
case STEP_PARSE:
if (!wp_spa_json_is_array (self->json)) {
wp_transition_return_error (transition, g_error_new (
WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"components section is not a JSON array"));
return;
case STEP_PARSE: {
g_autoptr (GError) error = NULL;
if (parse_components (self, &error)) {
self->components_iter =
(ComponentData **) &g_ptr_array_index (self->components, 0);
wp_transition_advance (transition);
} else {
wp_transition_return_error (transition, error);
}
json_to_components_list (&self->components, self->json);
self->components_iter = g_list_first (self->components);
break;
}
case STEP_GET_NEXT:
/* get the next enabled component */
do {
self->curr_component = (ComponentData *) *self->components_iter;
self->components_iter++;
} while (self->curr_component &&
self->curr_component->state == FEATURE_STATE_DISABLED);
wp_transition_advance (transition);
break;
case STEP_LOAD_NEXT_1:
case STEP_LOAD_NEXT_2:
self->curr_component = (ComponentData *) self->components_iter->data;
case STEP_LOAD_NEXT: {
/* verify that dependencies have been loaded */
gboolean dependencies_ok = TRUE;
for (guint i = 0; i < self->curr_component->requires->len; i++) {
const gchar *dependency =
g_ptr_array_index (self->curr_component->requires, i);
if (!wp_core_test_feature (core, dependency)) {
dependencies_ok = FALSE;
break;
}
}
/* Advance iterator */
self->components_iter = g_list_next (self->components_iter);
/* Skip component if its dependencies are not met */
if (!component_meets_dependencies (core, self->curr_component)) {
wp_info_object (self, "... skipping component '%s' as its dependencies "
"are not met", self->curr_component->name);
if (!dependencies_ok) {
/* this component must be optional, because if it wasn't, the dependency
failing to load would have caused an error earlier */
g_assert (self->curr_component->state == FEATURE_STATE_OPTIONAL);
wp_notice_object (core, "skipping component '%s' because some of its "
"dependencies were not loaded", self->curr_component->printable_id);
wp_transition_advance (transition);
return;
}
/* Load the component */
wp_debug_object (self,
"... loading component '%s' ('%s') with priority '%d' and flags '%x'",
self->curr_component->name, self->curr_component->type,
self->curr_component->priority, self->curr_component->flags);
wp_debug_object (self, "loading component '%s'",
self->curr_component->printable_id);
wp_core_load_component (core, self->curr_component->name,
self->curr_component->type, NULL, NULL, NULL,
self->curr_component->type, self->curr_component->arguments,
self->curr_component->provides, NULL,
(GAsyncReadyCallback) on_component_loaded, self);
break;
case STEP_CLEANUP:
}
case WP_TRANSITION_STEP_ERROR:
g_list_free_full (g_steal_pointer (&self->components),
(GDestroyNotify) component_data_free);
g_clear_pointer (&self->json, wp_spa_json_unref);
break;
default:
@ -308,10 +463,26 @@ wp_component_array_load_task_execute_step (WpTransition * transition, guint step
}
}
static void
wp_component_array_load_task_finalize (GObject * object)
{
WpComponentArrayLoadTask *self = WP_COMPONENT_ARRAY_LOAD_TASK (object);
g_clear_pointer (&self->feat_components, g_hash_table_unref);
g_clear_pointer (&self->components, g_ptr_array_unref);
g_clear_pointer (&self->json, wp_spa_json_unref);
G_OBJECT_CLASS (wp_component_array_load_task_parent_class)->finalize (object);
}
static void
wp_component_array_load_task_class_init (WpComponentArrayLoadTaskClass * klass)
{
GObjectClass * object_class = (GObjectClass *) klass;
WpTransitionClass * transition_class = (WpTransitionClass *) klass;
object_class->finalize = wp_component_array_load_task_finalize;
transition_class->get_next_step = wp_component_array_load_task_get_next_step;
transition_class->execute_step = wp_component_array_load_task_execute_step;
}
@ -391,7 +562,9 @@ static gboolean
wp_internal_comp_loader_supports_type (WpComponentLoader * cl,
const gchar * type)
{
return g_str_equal (type, "module") || g_str_equal (type, "array");
return g_str_equal (type, "module") ||
g_str_equal (type, "array") ||
g_str_equal (type, "virtual");
}
static void
@ -414,11 +587,18 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
g_task_return_error (task, g_steal_pointer (&error));
}
else if (g_str_equal (type, "array")) {
WpTransition *task = wp_component_array_load_task_new (args, core,
WpTransition *task = wp_component_array_load_task_new (args, self,
cancellable, callback, data);
wp_transition_set_data (task, g_object_ref (core), g_object_unref);
wp_transition_set_source_tag (task, wp_internal_comp_loader_load);
wp_transition_advance (task);
}
else if (g_str_equal (type, "virtual")) {
/* dummy task, return immediately */
g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
g_task_set_source_tag (task, wp_internal_comp_loader_load);
g_task_return_pointer (task, NULL, NULL);
}
else {
g_assert_not_reached ();
}

View file

@ -22,10 +22,10 @@ pipewire_data_dir = get_option('prefix') / get_option('datadir') / 'pipewire'
cc = meson.get_compiler('c')
glib_req_version = '>= 2.62'
glib_req_version = '>= 2.68'
add_project_arguments([
'-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_62',
'-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_62',
'-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_68',
'-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_68',
], language: 'c'
)

View file

@ -79,178 +79,403 @@ context.modules = [
{ name = libpipewire-module-spa-node-factory }
]
wireplumber.features = {
## Syntax:
## <feature name> = [ required | optional | disabled ]
## optional is the default
support.settings = required
hardware.audio = required
hardware.bluetooth = required
hardware.video-capture = required
policy.standard = required
#policy.role-priority-system = optional
}
wireplumber.components = [
## WirePlumber components to load
## name and type are mandatory rest of the tags are optional.
## type is mandatory; rest of the tags are optional
##
## Syntax:
## {
## name = <component-name>
## type = <component-type>
## Higher the value higher the priority
## priority = <value>
## deps = [ <component-dependencies> ]
## flags = [ ifexists | nofail ]
## arguments = { <json object> }
##
## # Feature that this component provides
## provides = <feature>
##
## # List of features that must be provided before this component is loaded
## requires = [ <features> ]
##
## # List of features that would offer additional functionality if provided
## # but are not strictly required
## wants = [ <features> ]
## }
## Settings provider
{
name = libwireplumber-module-settings,
type = module
## highest priority
priority = 150
name = libwireplumber-module-settings, type = module
provides = support.settings
}
## The lua scripting engine
{
name = libwireplumber-module-lua-scripting,
type = module
priority = 140
}
## Session item factories
{
name = libwireplumber-module-si-node,
type = module
## default priority for session item modules
priority = 130
}
{
name = libwireplumber-module-si-audio-adapter,
type = module
## default priority for session item modules
priority = 130
}
{
name = libwireplumber-module-si-standard-link,
type = module
## default priority for session item modules
priority = 130
}
{
name = libwireplumber-module-si-audio-virtual,
type = module
## default priority for session item modules
priority = 130
}
## The shared D-Bus connection
{
name = libwireplumber-module-dbus-connection
type = module
priority = 120
}
## API to access default nodes from scripts
{
name = libwireplumber-module-default-nodes-api,
type = module
## These modules may be needed by other modules, so they assume higher
## priority than regular modules.
priority = 120
}
## API to access mixer controls, needed for volume ducking
{
name = libwireplumber-module-mixer-api,
type = module
## These modules may be needed by other modules, so they assume higher
## priority than regular modules.
priority = 120
name = libwireplumber-module-lua-scripting, type = module
provides = support.lua-scripting
}
## Module listening for pipewire objects to push events
{
name = libwireplumber-module-standard-event-source,
type = module
## regular module default priority
priority = 110
name = libwireplumber-module-standard-event-source, type = module
provides = support.standard-event-source
}
## The shared D-Bus connection
{
name = libwireplumber-module-dbus-connection, type = module
provides = support.dbus
}
## Module managing the portal permissions
{
name = libwireplumber-module-portal-permissionstore,
type = module,
## regular module default priority
priority = 110
deps = [ access-enable-flatpak-portal ]
name = libwireplumber-module-portal-permissionstore, type = module
provides = support.portal-permissionstore
requires = [ support.dbus ]
}
## Needed for device reservation to work
{
name = libwireplumber-module-reserve-device,
type = module,
## regular module default priority
priority = 110
deps = [ monitor.alsa.reserve ]
name = libwireplumber-module-reserve-device, type = module
provides = support.reserve-device
requires = [ support.dbus ]
}
## Needed for logind to work
## logind integration to enable certain functionality only on the active seat
{
name = libwireplumber-module-logind,
type = module,
## regular module default priority
priority = 110
deps = [ monitor.bluetooth.enable-logind ],
flags = [ ifexists ]
name = libwireplumber-module-logind, type = module
provides = support.logind
}
## Needed for MIDI monitor to work
## Session item factories
{
name = libwireplumber-module-file-monitor-api,
type = module,
## regular module default priority
priority = 110
deps = [ monitor.alsa.midi.monitoring ]
name = libwireplumber-module-si-node, type = module
provides = si.node
}
{
name = libwireplumber-module-si-audio-adapter, type = module
provides = si.audio-adapter
}
{
name = libwireplumber-module-si-standard-link, type = module
provides = si.standard-link
}
{
name = libwireplumber-module-si-audio-virtual, type = module
provides = si.audio-virtual
}
## API to access default nodes from scripts
{
name = libwireplumber-module-default-nodes-api, type = module
provides = api.default-nodes
}
## API to access mixer controls
{
name = libwireplumber-module-mixer-api, type = module
provides = api.mixer
}
## API to get notified about file changes
{
name = libwireplumber-module-file-monitor-api, type = module
provides = api.file-monitor
}
## Provide the "default" pw_metadata
{ name = metadata.lua, type = script/lua, priority = 110 }
{
name = metadata.lua, type = script/lua
provides = metadata.default
requires = [ support.lua-scripting ]
}
{ name = monitors/alsa.lua, type = script/lua, priority = 101 }
{ name = monitors/bluez.lua, type = script/lua, priority = 101 }
{ name = monitors/bluez-midi.lua, type = script/lua, priority = 101 }
{ name = monitors/alsa-midi.lua, type = script/lua, priority = 101 }
{ name = monitors/v4l2.lua, type = script/lua, priority = 101 }
{ name = monitors/libcamera.lua, type = script/lua, priority = 101 }
## Device monitors
{
name = monitors/alsa.lua, type = script/lua
provides = monitor.alsa
requires = [ support.lua-scripting ]
wants = [ support.reserve-device ]
}
{
name = monitors/bluez.lua, type = script/lua
provides = monitor.bluez
requires = [ support.lua-scripting ]
wants = [ support.logind ]
}
{
name = monitors/bluez-midi.lua, type = script/lua
provides = monitor.bluez-midi
requires = [ support.lua-scripting ]
wants = [ support.logind ]
}
{
name = monitors/alsa-midi.lua, type = script/lua
provides = monitor.alsa-midi
requires = [ support.lua-scripting ]
wants = [ api.file-monitor ]
}
{
name = monitors/v4l2.lua, type = script/lua
provides = monitor.v4l2
requires = [ support.lua-scripting ]
}
{
name = monitors/libcamera.lua, type = script/lua
provides = monitor.libcamera
requires = [ support.lua-scripting ]
}
{ name = default-nodes/apply-default-node.lua, type = script/lua, priority = 120 }
{ name = default-nodes/find-echo-cancel-default-node.lua, type = script/lua, priority = 120 }
{ name = default-nodes/state-default-nodes.lua, type = script/lua, priority = 120 }
{ name = default-nodes/find-best-default-node.lua, type = script/lua, priority = 120 }
{ name = default-nodes/find-selected-default-node.lua, type = script/lua, priority = 120 }
{ name = default-nodes/rescan.lua, type = script/lua, priority = 120 }
## Client access configuration hooks
{
name = client/access-default.lua, type = script/lua
provides = script.client.access-default
requires = [ support.lua-scripting ]
}
{
name = client/access-portal.lua, type = script/lua
provides = script.client.access-portal
requires = [ support.lua-scripting, support.portal-permissionstore ]
}
{
type = virtual, provides = policy.client.access
wants = [ script.client.access-default,
script.client.access-portal ]
}
{ name = linking/find-virtual-target.lua, type = script/lua, priority = 100 }
{ name = linking/find-defined-target.lua, type = script/lua, priority = 100 }
{ name = linking/find-default-target.lua, type = script/lua, priority = 100 }
{ name = linking/find-best-target.lua, type = script/lua, priority = 100 }
{ name = linking/link-target.lua, type = script/lua, priority = 100 }
{ name = linking/prepare-link.lua, type = script/lua, priority = 100 }
{ name = linking/filter-forward-format.lua, type = script/lua, priority = 100 }
{ name = linking/rescan.lua, type = script/lua, priority = 100 }
{ name = linking/move-follow.lua, type = script/lua, priority = 100 }
{ name = linking/rescan-virtual-links.lua, type = script/lua, priority = 100 }
## Device profile selection hooks
{
name = device/select-profile.lua, type = script/lua
provides = hooks.device.profile.select
requires = [ support.lua-scripting ]
}
{
name = device/find-best-profile.lua, type = script/lua
provides = hooks.device.profile.find-best
requires = [ support.lua-scripting ]
}
{
name = device/state-profile.lua, type = script/lua
provides = hooks.device.profile.state
requires = [ support.lua-scripting ]
}
{
name = device/apply-profile.lua, type = script/lua
provides = hooks.device.profile.apply
requires = [ support.lua-scripting ]
}
{
type = virtual, provides = policy.device.profile
requires = [ hooks.device.profile.select,
hooks.device.profile.apply ]
wants = [ hooks.device.profile.find-best,
hooks.device.profile.state ]
}
{ name = device/apply-routes.lua, type = script/lua, priority = 100 }
{ name = device/find-best-profile.lua, type = script/lua, priority = 100 }
{ name = device/select-routes.lua, type = script/lua, priority = 100 }
{ name = device/find-best-routes.lua, type = script/lua, priority = 100 }
{ name = device/state-profile.lua, type = script/lua, priority = 100 }
{ name = device/state-routes.lua, type = script/lua, priority = 100 }
{ name = device/apply-profile.lua, type = script/lua, priority = 100 }
{ name = device/select-profile.lua, type = script/lua, priority = 100 }
# Device route selection hooks
{
name = device/select-routes.lua, type = script/lua
provides = hooks.device.routes.select
requires = [ support.lua-scripting ]
}
{
name = device/find-best-routes.lua, type = script/lua
provides = hooks.device.routes.find-best
requires = [ support.lua-scripting ]
}
{
name = device/state-routes.lua, type = script/lua
provides = hooks.device.routes.state
requires = [ support.lua-scripting ]
}
{
name = device/apply-routes.lua, type = script/lua
provides = hooks.device.routes.apply
requires = [ support.lua-scripting ]
}
{
type = virtual, provides = policy.device.routes
requires = [ hooks.device.routes.select,
hooks.device.routes.apply ]
wants = [ hooks.device.routes.find-best,
hooks.device.routes.state ]
}
{ name = client/access-portal.lua, type = script/lua, priority = 100 }
{ name = client/access-default.lua, type = script/lua, priority = 100 }
## Default nodes selection hooks
{
name = default-nodes/rescan.lua, type = script/lua
provides = hooks.default-nodes.rescan
requires = [ support.lua-scripting ]
}
{
name = default-nodes/find-selected-default-node.lua, type = script/lua
provides = hooks.default-nodes.find-selected
requires = [ support.lua-scripting, metadata.default ]
}
{
name = default-nodes/find-best-default-node.lua, type = script/lua
provides = hooks.default-nodes.find-best
requires = [ support.lua-scripting ]
}
{
name = default-nodes/find-echo-cancel-default-node.lua, type = script/lua
provides = hooks.default-nodes.find-echo-cancel
requires = [ support.lua-scripting ]
}
{
name = default-nodes/state-default-nodes.lua, type = script/lua
provides = hooks.default-nodes.state
requires = [ support.lua-scripting, metadata.default ]
}
{
name = default-nodes/apply-default-node.lua, type = script/lua,
provides = hooks.default-nodes.apply
requires = [ support.lua-scripting, metadata.default ]
}
{
type = virtual, provides = policy.default-nodes
requires = [ hooks.default-nodes.rescan,
hooks.default-nodes.apply ]
wants = [ hooks.default-nodes.find-selected,
hooks.default-nodes.find-best,
hooks.default-nodes.find-echo-cancel,
hooks.default-nodes.state ]
}
{ name = node/create-item.lua, type = script/lua, priority = 100 }
{ name = node/create-virtual-item.lua, type = script/lua, priority = 100 }
{ name = node/suspend-node.lua, type = script/lua, priority = 100 }
{ name = node/state-stream.lua, type = script/lua, priority = 100 }
## Node configuration hooks
{
name = node/create-item.lua, type = script/lua
provides = hooks.node.create-session-item
requires = [ support.lua-scripting, si.audio-adapter, si.node ]
}
{
name = node/suspend-node.lua, type = script/lua
provides = hooks.node.suspend
requires = [ support.lua-scripting ]
}
{
name = node/state-stream.lua, type = script/lua
provides = hooks.stream.state
requires = [ support.lua-scripting ]
}
{
name = linking/filter-forward-format.lua, type = script/lua
provides = hooks.loopback.forward-format
requires = [ support.lua-scripting ]
}
{
name = node/create-virtual-item.lua, type = script/lua
provides = script.create-role-items
requires = [ support.lua-scripting, si.audio-virtual ]
}
## Linking hooks
{
name = linking/rescan.lua, type = script/lua
provides = hooks.linking.rescan
requires = [ support.lua-scripting ]
}
{
name = linking/move-follow.lua, type = script/lua
provides = hooks.linking.move-follow
requires = [ support.lua-scripting ]
}
{
name = linking/find-defined-target.lua, type = script/lua
provides = hooks.linking.target.find-defined
requires = [ support.lua-scripting ]
}
{
name = linking/find-default-target.lua, type = script/lua
provides = hooks.linking.target.find-default
requires = [ support.lua-scripting, api.default-nodes ]
}
{
name = linking/find-best-target.lua, type = script/lua
provides = hooks.linking.target.find-best
requires = [ support.lua-scripting ]
}
{
name = linking/prepare-link.lua, type = script/lua
provides = hooks.linking.target.prepare-link
requires = [ support.lua-scripting, api.default-nodes ]
}
{
name = linking/link-target.lua, type = script/lua
provides = hooks.linking.target.link
requires = [ support.lua-scripting, si.standard-link ]
}
{
type = virtual, provides = policy.linking.standard
requires = [ hooks.linking.rescan,
hooks.linking.target.prepare-link,
hooks.linking.target.link ]
wants = [ hooks.linking.move-follow,
hooks.linking.target.find-defined,
hooks.linking.target.find-default,
hooks.linking.target.find-best ]
}
## Linking: Role-based priority system
{
name = linking/rescan-virtual-links.lua, type = script/lua
provides = hooks.linking.role-priority-system.links.rescan
requires = [ support.lua-scripting, api.mixer ]
}
{
name = linking/find-virtual-target.lua, type = script/lua
provides = hooks.linking.role-priority-system.target.find
requires = [ support.lua-scripting ]
}
{
type = virtual, provides = policy.linking.role-priority-system
requires = [ policy.linking.standard,
hooks.linking.role-priority-system.links.rescan,
hooks.linking.role-priority-system.target.find ]
}
## Load targets
{
type = virtual, provides = hardware.audio
wants = [ monitor.alsa, monitor.alsa-midi ]
}
{
type = virtual, provides = hardware.bluetooth
wants = [ monitor.bluez, monitor.bluez-midi ]
}
{
type = virtual, provides = hardware.video-capture
wants = [ monitor.v4l2, monitor.libcamera ]
}
{
type = virtual, provides = policy.standard
requires = [ policy.client.access
policy.device.profile
policy.device.routes
policy.default-nodes
policy.linking.standard
hooks.node.create-session-item
support.standard-event-source ]
wants = [ hooks.node.suspend
hooks.stream.state
hooks.loopback.forward-format ]
}
{
type = virtual, provides = policy.role-priority-system
requires = [ policy.standard,
script.create-role-items,
policy.linking.role-priority-system ]
}
]
wireplumber.settings = {

View file

@ -139,6 +139,7 @@ wp_base_test_fixture_teardown (WpBaseTestFixture * self)
g_main_context_iteration (self->context, TRUE);
g_main_context_pop_thread_default (self->context);
g_clear_pointer (&self->conf_file, g_free);
g_clear_object (&self->client_core);
g_clear_object (&self->core);
g_clear_pointer (&self->timeout_source, g_source_unref);

View file

@ -40,6 +40,7 @@ wp_test_plugin_class_init (WpTestPluginClass * klass)
struct _WpTestCompLoader
{
GObject parent;
GPtrArray *history;
};
static void wp_test_comp_loader_iface_init (WpComponentLoaderInterface * iface);
@ -55,11 +56,21 @@ G_DEFINE_TYPE_WITH_CODE (WpTestCompLoader, wp_test_comp_loader,
static void
wp_test_comp_loader_init (WpTestCompLoader * self)
{
self->history = g_ptr_array_new_with_free_func (g_free);
}
static void
wp_test_comp_loader_finalize (GObject * self)
{
g_clear_pointer (&WP_TEST_COMP_LOADER (self)->history, g_ptr_array_unref);
G_OBJECT_CLASS (wp_test_comp_loader_parent_class)->finalize (self);
}
static void
wp_test_comp_loader_class_init (WpTestCompLoaderClass * klass)
{
GObjectClass *oclass = (GObjectClass *) klass;
oclass->finalize = wp_test_comp_loader_finalize;
}
static gboolean
@ -78,6 +89,7 @@ wp_test_comp_loader_load (WpComponentLoader * self, WpCore * core,
"name", component,
"core", core,
NULL);
g_ptr_array_add (WP_TEST_COMP_LOADER (self)->history, g_strdup (component));
g_task_return_pointer (task, plugin, g_object_unref);
}
@ -99,14 +111,15 @@ wp_test_comp_loader_iface_init (WpComponentLoaderInterface * iface)
typedef struct {
WpBaseTestFixture base;
WpTestCompLoader *loader;
} TestFixture;
static void
test_setup (TestFixture *self, gconstpointer user_data)
{
wp_base_test_fixture_setup (&self->base, 0);
wp_core_register_object (self->base.core,
g_object_new (WP_TYPE_TEST_COMP_LOADER, NULL));
self->loader = g_object_new (WP_TYPE_TEST_COMP_LOADER, NULL);
wp_core_register_object (self->base.core, self->loader);
}
static void
@ -142,6 +155,47 @@ test_load (TestFixture *f, gconstpointer data)
g_assert_true (wp_core_test_feature (f->base.core, "feature.name123"));
}
static void
test_dependencies_setup (TestFixture *f, gconstpointer data)
{
f->base.conf_file =
g_strdup_printf ("%s/component-loader.conf", g_getenv ("G_TEST_SRCDIR"));
test_setup (f, data);
}
static void
test_dependencies (TestFixture *f, gconstpointer data)
{
g_autoptr (WpConf) conf = wp_conf_get_instance (f->base.core);
g_assert_nonnull (conf);
g_autoptr (WpSpaJson) components = wp_conf_get_section (conf,
"wireplumber.components", NULL);
g_assert_nonnull (components);
wp_core_load_component (f->base.core, NULL, "array", components,
NULL, NULL, (GAsyncReadyCallback) on_component_loaded, f);
g_main_loop_run (f->base.loop);
// NULL-terminate the array
g_ptr_array_add (f->loader->history, NULL);
/* verify the order of loading the plugins was as expected */
const gchar *expected[] = {
"five", "one", "six", "two", "three", "four", "seven", NULL };
g_assert_cmpstrv (f->loader->history->pdata, expected);
g_assert_true (wp_core_test_feature (f->base.core, "support.one"));
g_assert_true (wp_core_test_feature (f->base.core, "support.two"));
g_assert_true (wp_core_test_feature (f->base.core, "support.three"));
g_assert_true (wp_core_test_feature (f->base.core, "support.four"));
g_assert_true (wp_core_test_feature (f->base.core, "virtual.four"));
g_assert_true (wp_core_test_feature (f->base.core, "support.five"));
g_assert_true (wp_core_test_feature (f->base.core, "support.six"));
g_assert_false (wp_core_test_feature (f->base.core, "support.seven"));
g_assert_false (wp_core_test_feature (f->base.core, "support.eight"));
}
gint
main (gint argc, gchar *argv[])
{
@ -150,6 +204,8 @@ main (gint argc, gchar *argv[])
g_test_add ("/wp/comploader/load", TestFixture, NULL,
test_setup, test_load, test_teardown);
g_test_add ("/wp/comploader/dependencies", TestFixture, NULL,
test_dependencies_setup, test_dependencies, test_teardown);
return g_test_run ();
}

View file

@ -0,0 +1,64 @@
context.modules = [
{ name = libpipewire-module-protocol-native }
]
wireplumber.features = {
virtual.four = required
}
wireplumber.components = [
# expected load order:
# five, one, six, two, three, four, seven
# eight is not loaded - optional feature
{
name = one
type = test
provides = support.one
}
{
name = two
type = test
provides = support.two
requires = [ support.one support.six ]
}
{
type = virtual
provides = virtual.four
requires = [ support.four ]
}
{
name = three
type = test
provides = support.three
wants = [ support.two ]
}
{
name = four
type = test
provides = support.four
requires = [ support.five ]
wants = [ support.three ]
}
{
name = five
type = test
provides = support.five
}
{
name = six
type = test
provides = support.six
requires = [ support.one ]
}
{
name = seven
type = test
requires = [ support.five ]
}
{
name = eight
type = test
provides = support.eight
requires = [ support.four ]
}
]