mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2025-11-07 13:30:09 -05:00
access: rework access checks
Remove the access struct. Allow for the access module to override any method of a resource to do additional checks.
This commit is contained in:
parent
2c1245f8ef
commit
611ce2151e
14 changed files with 192 additions and 300 deletions
|
|
@ -528,11 +528,18 @@ struct pw_client_node_events {
|
||||||
* The transport area is used to exchange real-time commands between
|
* The transport area is used to exchange real-time commands between
|
||||||
* the client and the server.
|
* the client and the server.
|
||||||
*
|
*
|
||||||
|
* \param readfd fd for signal data can be read
|
||||||
|
* \param writefd fd for signal data can be written
|
||||||
* \param memfd the memory fd of the area
|
* \param memfd the memory fd of the area
|
||||||
* \param offset the offset to map
|
* \param offset the offset to map
|
||||||
* \param size the size to map
|
* \param size the size to map
|
||||||
*/
|
*/
|
||||||
void (*transport) (void *object, int readfd, int writefd, int memfd, uint32_t offset, uint32_t size);
|
void (*transport) (void *object,
|
||||||
|
int readfd,
|
||||||
|
int writefd,
|
||||||
|
int memfd,
|
||||||
|
uint32_t offset,
|
||||||
|
uint32_t size);
|
||||||
};
|
};
|
||||||
|
|
||||||
#define pw_client_node_notify_set_props(r,...) ((struct pw_client_node_events*)r->iface->events)->props(r,__VA_ARGS__)
|
#define pw_client_node_notify_set_props(r,...) ((struct pw_client_node_events*)r->iface->events)->props(r,__VA_ARGS__)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@
|
||||||
|
|
||||||
#include <dbus/dbus.h>
|
#include <dbus/dbus.h>
|
||||||
|
|
||||||
|
#include "pipewire/client/interfaces.h"
|
||||||
#include "pipewire/client/utils.h"
|
#include "pipewire/client/utils.h"
|
||||||
|
|
||||||
#include "pipewire/server/core.h"
|
#include "pipewire/server/core.h"
|
||||||
#include "pipewire/server/module.h"
|
#include "pipewire/server/module.h"
|
||||||
|
|
||||||
|
|
@ -43,7 +45,6 @@ struct impl {
|
||||||
struct pw_listener global_removed;
|
struct pw_listener global_removed;
|
||||||
|
|
||||||
struct spa_list client_list;
|
struct spa_list client_list;
|
||||||
struct pw_access access;
|
|
||||||
|
|
||||||
struct spa_source *dispatch_event;
|
struct spa_source *dispatch_event;
|
||||||
};
|
};
|
||||||
|
|
@ -53,15 +54,23 @@ struct client_info {
|
||||||
struct spa_list link;
|
struct spa_list link;
|
||||||
struct pw_client *client;
|
struct pw_client *client;
|
||||||
bool is_sandboxed;
|
bool is_sandboxed;
|
||||||
|
const struct pw_core_methods *old_methods;
|
||||||
|
struct pw_core_methods core_methods;
|
||||||
struct spa_list async_pending;
|
struct spa_list async_pending;
|
||||||
|
struct pw_listener resource_added;
|
||||||
|
struct pw_listener resource_removed;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct async_pending {
|
struct async_pending {
|
||||||
struct spa_list link;
|
struct spa_list link;
|
||||||
bool handled;
|
|
||||||
struct client_info *info;
|
struct client_info *info;
|
||||||
|
bool handled;
|
||||||
char *handle;
|
char *handle;
|
||||||
struct pw_access_data *access_data;
|
struct pw_resource *resource;
|
||||||
|
char *factory_name;
|
||||||
|
char *name;
|
||||||
|
struct pw_properties *properties;
|
||||||
|
uint32_t new_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct client_info *find_client_info(struct impl *impl, struct pw_client *client)
|
static struct client_info *find_client_info(struct impl *impl, struct pw_client *client)
|
||||||
|
|
@ -106,44 +115,28 @@ static struct async_pending *find_pending(struct client_info *cinfo, const char
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void free_pending(struct pw_access_data *d)
|
static void free_pending(struct async_pending *p)
|
||||||
{
|
{
|
||||||
struct async_pending *p = d->user_data;
|
|
||||||
|
|
||||||
if (!p->handled)
|
if (!p->handled)
|
||||||
close_request(p);
|
close_request(p);
|
||||||
|
|
||||||
pw_log_debug("pending %p: handle %s", p, p->handle);
|
pw_log_debug("pending %p: handle %s", p, p->handle);
|
||||||
spa_list_remove(&p->link);
|
spa_list_remove(&p->link);
|
||||||
free(p->handle);
|
free(p->handle);
|
||||||
}
|
free(p->factory_name);
|
||||||
|
free(p->name);
|
||||||
static void
|
if (p->properties)
|
||||||
add_pending(struct client_info *cinfo, const char *handle, struct pw_access_data *access_data)
|
pw_properties_free(p->properties);
|
||||||
{
|
free(p);
|
||||||
struct async_pending *p;
|
|
||||||
struct pw_access_data *ad;
|
|
||||||
|
|
||||||
ad = access_data->async_start(access_data, sizeof(struct async_pending));
|
|
||||||
ad->free = free_pending;
|
|
||||||
|
|
||||||
p = ad->user_data;
|
|
||||||
p->info = cinfo;
|
|
||||||
p->handle = strdup(handle);
|
|
||||||
p->access_data = ad;
|
|
||||||
p->handled = false;
|
|
||||||
pw_log_debug("pending %p: handle %s", p, handle);
|
|
||||||
|
|
||||||
spa_list_insert(cinfo->async_pending.prev, &p->link);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void client_info_free(struct client_info *cinfo)
|
static void client_info_free(struct client_info *cinfo)
|
||||||
{
|
{
|
||||||
struct async_pending *p, *tmp;
|
struct async_pending *p, *tmp;
|
||||||
|
|
||||||
spa_list_for_each_safe(p, tmp, &cinfo->async_pending, link) {
|
spa_list_for_each_safe(p, tmp, &cinfo->async_pending, link)
|
||||||
p->access_data->complete(p->access_data, SPA_RESULT_NO_PERMISSION);
|
free_pending(p);
|
||||||
}
|
|
||||||
spa_list_remove(&cinfo->link);
|
spa_list_remove(&cinfo->link);
|
||||||
free(cinfo);
|
free(cinfo);
|
||||||
}
|
}
|
||||||
|
|
@ -221,8 +214,8 @@ check_global_owner(struct pw_core *core, struct pw_client *client, struct pw_glo
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static bool
|
||||||
do_view_global(struct pw_access *access, struct pw_client *client, struct pw_global *global)
|
do_global_filter(struct pw_global *global, struct pw_client *client, void *data)
|
||||||
{
|
{
|
||||||
if (global->type == client->core->type.link) {
|
if (global->type == client->core->type.link) {
|
||||||
struct pw_link *link = global->object;
|
struct pw_link *link = global->object;
|
||||||
|
|
@ -230,15 +223,15 @@ do_view_global(struct pw_access *access, struct pw_client *client, struct pw_glo
|
||||||
/* we must be able to see both nodes */
|
/* we must be able to see both nodes */
|
||||||
if (link->output
|
if (link->output
|
||||||
&& !check_global_owner(client->core, client, link->output->node->global))
|
&& !check_global_owner(client->core, client, link->output->node->global))
|
||||||
return SPA_RESULT_ERROR;
|
return false;
|
||||||
|
|
||||||
if (link->input
|
if (link->input
|
||||||
&& !check_global_owner(client->core, client, link->input->node->global))
|
&& !check_global_owner(client->core, client, link->input->node->global))
|
||||||
return SPA_RESULT_ERROR;
|
return false;
|
||||||
} else if (!check_global_owner(client->core, client, global))
|
} else if (!check_global_owner(client->core, client, global))
|
||||||
return SPA_RESULT_ERROR;
|
return false;
|
||||||
|
|
||||||
return SPA_RESULT_OK;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static DBusHandlerResult
|
static DBusHandlerResult
|
||||||
|
|
@ -250,7 +243,6 @@ portal_response(DBusConnection *connection, DBusMessage *msg, void *user_data)
|
||||||
uint32_t response = 2;
|
uint32_t response = 2;
|
||||||
DBusError error;
|
DBusError error;
|
||||||
struct async_pending *p;
|
struct async_pending *p;
|
||||||
struct pw_access_data *d;
|
|
||||||
|
|
||||||
dbus_error_init(&error);
|
dbus_error_init(&error);
|
||||||
|
|
||||||
|
|
@ -267,26 +259,39 @@ portal_response(DBusConnection *connection, DBusMessage *msg, void *user_data)
|
||||||
return DBUS_HANDLER_RESULT_HANDLED;
|
return DBUS_HANDLER_RESULT_HANDLED;
|
||||||
|
|
||||||
p->handled = true;
|
p->handled = true;
|
||||||
d = p->access_data;
|
|
||||||
|
|
||||||
pw_log_debug("portal check result: %d", response);
|
pw_log_debug("portal check result: %d", response);
|
||||||
|
|
||||||
d->complete(d, response == 0 ? SPA_RESULT_OK : SPA_RESULT_NO_PERMISSION);
|
if (response == 0) {
|
||||||
|
cinfo->old_methods->create_node (p->resource,
|
||||||
|
p->factory_name,
|
||||||
|
p->name,
|
||||||
|
&p->properties->dict,
|
||||||
|
p->new_id);
|
||||||
|
} else {
|
||||||
|
pw_core_notify_error(cinfo->client->core_resource,
|
||||||
|
p->resource->id, SPA_RESULT_NO_PERMISSION, "not allowed");
|
||||||
|
|
||||||
|
}
|
||||||
|
free_pending(p);
|
||||||
|
pw_client_set_busy(cinfo->client, false);
|
||||||
|
|
||||||
return DBUS_HANDLER_RESULT_HANDLED;
|
return DBUS_HANDLER_RESULT_HANDLED;
|
||||||
}
|
}
|
||||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
do_create_node(struct pw_access *access,
|
static void do_create_node(void *object,
|
||||||
struct pw_access_data *data,
|
const char *factory_name,
|
||||||
const char *factory_name,
|
const char *name,
|
||||||
const char *name,
|
const struct spa_dict *props,
|
||||||
struct pw_properties *properties)
|
uint32_t new_id)
|
||||||
{
|
{
|
||||||
struct impl *impl = SPA_CONTAINER_OF(access, struct impl, access);
|
struct pw_resource *resource = object;
|
||||||
struct client_info *cinfo = find_client_info(impl, data->resource->client);
|
struct client_info *cinfo = resource->access_private;
|
||||||
|
struct impl *impl = cinfo->impl;
|
||||||
|
struct pw_client *client = resource->client;
|
||||||
DBusMessage *m = NULL, *r = NULL;
|
DBusMessage *m = NULL, *r = NULL;
|
||||||
DBusError error;
|
DBusError error;
|
||||||
pid_t pid;
|
pid_t pid;
|
||||||
|
|
@ -294,14 +299,15 @@ do_create_node(struct pw_access *access,
|
||||||
DBusMessageIter dict_iter;
|
DBusMessageIter dict_iter;
|
||||||
const char *handle;
|
const char *handle;
|
||||||
const char *device;
|
const char *device;
|
||||||
|
struct async_pending *p;
|
||||||
|
|
||||||
if (!cinfo->is_sandboxed) {
|
if (!cinfo->is_sandboxed) {
|
||||||
data->complete(data, SPA_RESULT_OK);
|
cinfo->old_methods->create_node (object, factory_name, name, props, new_id);
|
||||||
return SPA_RESULT_OK;
|
return;
|
||||||
}
|
}
|
||||||
if (strcmp(factory_name, "client-node") != 0) {
|
if (strcmp(factory_name, "client-node") != 0) {
|
||||||
data->complete(data, SPA_RESULT_NO_PERMISSION);
|
pw_log_error("can only allow client-node");
|
||||||
return SPA_RESULT_NO_PERMISSION;
|
goto not_allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
pw_log_info("ask portal for client %p", cinfo->client);
|
pw_log_info("ask portal for client %p", cinfo->client);
|
||||||
|
|
@ -345,61 +351,93 @@ do_create_node(struct pw_access *access,
|
||||||
|
|
||||||
dbus_connection_add_filter(impl->bus, portal_response, cinfo, NULL);
|
dbus_connection_add_filter(impl->bus, portal_response, cinfo, NULL);
|
||||||
|
|
||||||
add_pending(cinfo, handle, data);
|
p = calloc(1, sizeof(struct async_pending));
|
||||||
|
p->info = cinfo;
|
||||||
|
p->handle = strdup(handle);
|
||||||
|
p->handled = false;
|
||||||
|
p->resource = resource;
|
||||||
|
p->factory_name = strdup(factory_name);
|
||||||
|
p->name = strdup(name);
|
||||||
|
p->properties = props ? pw_properties_new_dict(props) : NULL;
|
||||||
|
p->new_id = new_id;
|
||||||
|
pw_client_set_busy(client, true);
|
||||||
|
|
||||||
return SPA_RESULT_RETURN_ASYNC(0);
|
pw_log_debug("pending %p: handle %s", p, handle);
|
||||||
|
spa_list_insert(cinfo->async_pending.prev, &p->link);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
no_method_call:
|
no_method_call:
|
||||||
pw_log_error("Failed to create message");
|
pw_log_error("Failed to create message");
|
||||||
return SPA_RESULT_NO_PERMISSION;
|
goto not_allowed;
|
||||||
message_failed:
|
message_failed:
|
||||||
dbus_message_unref(m);
|
dbus_message_unref(m);
|
||||||
return SPA_RESULT_NO_PERMISSION;
|
goto not_allowed;
|
||||||
send_failed:
|
send_failed:
|
||||||
pw_log_error("Failed to call portal: %s", error.message);
|
pw_log_error("Failed to call portal: %s", error.message);
|
||||||
dbus_error_free(&error);
|
dbus_error_free(&error);
|
||||||
dbus_message_unref(m);
|
dbus_message_unref(m);
|
||||||
return SPA_RESULT_NO_PERMISSION;
|
goto not_allowed;
|
||||||
parse_failed:
|
parse_failed:
|
||||||
pw_log_error("Failed to parse AccessDevice result: %s", error.message);
|
pw_log_error("Failed to parse AccessDevice result: %s", error.message);
|
||||||
dbus_error_free(&error);
|
dbus_error_free(&error);
|
||||||
dbus_message_unref(r);
|
dbus_message_unref(r);
|
||||||
return SPA_RESULT_NO_PERMISSION;
|
goto not_allowed;
|
||||||
subscribe_failed:
|
subscribe_failed:
|
||||||
pw_log_error("Failed to subscribe to Request signal: %s", error.message);
|
pw_log_error("Failed to subscribe to Request signal: %s", error.message);
|
||||||
dbus_error_free(&error);
|
dbus_error_free(&error);
|
||||||
return SPA_RESULT_NO_PERMISSION;
|
goto not_allowed;
|
||||||
|
not_allowed:
|
||||||
|
pw_core_notify_error(client->core_resource,
|
||||||
|
resource->id, SPA_RESULT_NO_PERMISSION, "not allowed");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static void
|
||||||
do_create_link(struct pw_access *access,
|
do_create_link(void *object,
|
||||||
struct pw_access_data *data,
|
|
||||||
uint32_t output_node_id,
|
uint32_t output_node_id,
|
||||||
uint32_t output_port_id,
|
uint32_t output_port_id,
|
||||||
uint32_t input_node_id,
|
uint32_t input_node_id,
|
||||||
uint32_t input_port_id,
|
uint32_t input_port_id,
|
||||||
const struct spa_format *filter,
|
const struct spa_format *filter,
|
||||||
const struct pw_properties *props)
|
const struct spa_dict *props,
|
||||||
|
uint32_t new_id)
|
||||||
{
|
{
|
||||||
struct impl *impl = SPA_CONTAINER_OF(access, struct impl, access);
|
struct pw_resource *resource = object;
|
||||||
struct client_info *cinfo = find_client_info(impl, data->resource->client);
|
struct client_info *cinfo = resource->access_private;
|
||||||
int res;
|
struct pw_client *client = resource->client;
|
||||||
|
|
||||||
if (cinfo->is_sandboxed)
|
if (cinfo->is_sandboxed) {
|
||||||
res = SPA_RESULT_NO_PERMISSION;
|
pw_core_notify_error(client->core_resource,
|
||||||
else
|
resource->id, SPA_RESULT_NO_PERMISSION, "not allowed");
|
||||||
res = SPA_RESULT_OK;
|
return;
|
||||||
|
}
|
||||||
data->complete(data, res);
|
cinfo->old_methods->create_link (object,
|
||||||
return SPA_RESULT_OK;
|
output_node_id,
|
||||||
|
output_port_id,
|
||||||
|
input_node_id,
|
||||||
|
input_port_id,
|
||||||
|
filter,
|
||||||
|
props,
|
||||||
|
new_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void on_resource_added(struct pw_listener *listener,
|
||||||
|
struct pw_client *client,
|
||||||
|
struct pw_resource *resource)
|
||||||
|
{
|
||||||
|
struct client_info *cinfo = SPA_CONTAINER_OF(listener, struct client_info, resource_added);
|
||||||
|
struct impl *impl = cinfo->impl;
|
||||||
|
|
||||||
static struct pw_access access_checks = {
|
if (resource->type == impl->core->type.core) {
|
||||||
do_view_global,
|
cinfo->old_methods = resource->implementation;
|
||||||
do_create_node,
|
cinfo->core_methods = *cinfo->old_methods;
|
||||||
do_create_link,
|
resource->implementation = &cinfo->core_methods;
|
||||||
};
|
resource->access_private = cinfo;
|
||||||
|
cinfo->core_methods.create_node = do_create_node;
|
||||||
|
cinfo->core_methods.create_link = do_create_link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
on_global_added(struct pw_listener *listener, struct pw_core *core, struct pw_global *global)
|
on_global_added(struct pw_listener *listener, struct pw_core *core, struct pw_global *global)
|
||||||
|
|
@ -417,6 +455,8 @@ on_global_added(struct pw_listener *listener, struct pw_core *core, struct pw_gl
|
||||||
cinfo->is_sandboxed = true;
|
cinfo->is_sandboxed = true;
|
||||||
spa_list_init(&cinfo->async_pending);
|
spa_list_init(&cinfo->async_pending);
|
||||||
|
|
||||||
|
pw_signal_add(&client->resource_added, &cinfo->resource_added, on_resource_added);
|
||||||
|
|
||||||
spa_list_insert(impl->client_list.prev, &cinfo->link);
|
spa_list_insert(impl->client_list.prev, &cinfo->link);
|
||||||
|
|
||||||
pw_log_debug("module %p: client %p added", impl, client);
|
pw_log_debug("module %p: client %p added", impl, client);
|
||||||
|
|
@ -624,7 +664,6 @@ static struct impl *module_new(struct pw_core *core, struct pw_properties *prope
|
||||||
|
|
||||||
impl->core = core;
|
impl->core = core;
|
||||||
impl->properties = properties;
|
impl->properties = properties;
|
||||||
impl->access = access_checks;
|
|
||||||
|
|
||||||
impl->bus = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
|
impl->bus = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
|
||||||
if (impl->bus == NULL)
|
if (impl->bus == NULL)
|
||||||
|
|
@ -640,12 +679,12 @@ static struct impl *module_new(struct pw_core *core, struct pw_properties *prope
|
||||||
toggle_timeout, impl, NULL);
|
toggle_timeout, impl, NULL);
|
||||||
dbus_connection_set_wakeup_main_function(impl->bus, wakeup_main, impl, NULL);
|
dbus_connection_set_wakeup_main_function(impl->bus, wakeup_main, impl, NULL);
|
||||||
|
|
||||||
core->access = &impl->access;
|
|
||||||
|
|
||||||
spa_list_init(&impl->client_list);
|
spa_list_init(&impl->client_list);
|
||||||
|
|
||||||
pw_signal_add(&core->global_added, &impl->global_added, on_global_added);
|
pw_signal_add(&core->global_added, &impl->global_added, on_global_added);
|
||||||
pw_signal_add(&core->global_removed, &impl->global_removed, on_global_removed);
|
pw_signal_add(&core->global_removed, &impl->global_removed, on_global_removed);
|
||||||
|
core->global_filter = do_global_filter;
|
||||||
|
core->global_filter_data = impl;
|
||||||
|
|
||||||
return impl;
|
return impl;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
/* PipeWire
|
|
||||||
* Copyright (C) 2015 Wim Taymans <wim.taymans@gmail.com>
|
|
||||||
*
|
|
||||||
* This library is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Library General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This library is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Library General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Library General Public
|
|
||||||
* License along with this library; if not, write to the
|
|
||||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301, USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "pipewire/server/core.h"
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
/* PipeWire
|
|
||||||
* Copyright (C) 2015 Wim Taymans <wim.taymans@gmail.com>
|
|
||||||
*
|
|
||||||
* This library is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Library General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This library is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Library General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Library General Public
|
|
||||||
* License along with this library; if not, write to the
|
|
||||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
||||||
* Boston, MA 02110-1301, USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __PIPEWIRE_ACCESS_H__
|
|
||||||
#define __PIPEWIRE_ACCESS_H__
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define PIPEWIRE_TYPE__Access "PipeWire:Object:Access"
|
|
||||||
#define PIPEWIRE_TYPE_ACCESS_BASE PIPEWIRE_TYPE__Access ":"
|
|
||||||
|
|
||||||
#include <pipewire/client/sig.h>
|
|
||||||
#include <pipewire/server/client.h>
|
|
||||||
#include <pipewire/server/resource.h>
|
|
||||||
|
|
||||||
struct pw_access_data {
|
|
||||||
struct pw_resource *resource;
|
|
||||||
|
|
||||||
void *(*async_start) (struct pw_access_data *data, size_t size);
|
|
||||||
void (*complete) (struct pw_access_data *data, int res);
|
|
||||||
void (*free) (struct pw_access_data *data);
|
|
||||||
void *user_data;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* \struct pw_access
|
|
||||||
*
|
|
||||||
* PipeWire Access support struct.
|
|
||||||
*/
|
|
||||||
struct pw_access {
|
|
||||||
int (*view_global) (struct pw_access *access,
|
|
||||||
struct pw_client *client, struct pw_global *global);
|
|
||||||
int (*create_node) (struct pw_access *access,
|
|
||||||
struct pw_access_data *data,
|
|
||||||
const char *factory_name,
|
|
||||||
const char *name, struct pw_properties *properties);
|
|
||||||
int (*create_link) (struct pw_access *access,
|
|
||||||
struct pw_access_data *data,
|
|
||||||
uint32_t output_node_id,
|
|
||||||
uint32_t output_port_id,
|
|
||||||
uint32_t input_node_id,
|
|
||||||
uint32_t input_port_id,
|
|
||||||
const struct spa_format *filter,
|
|
||||||
const struct pw_properties *props);
|
|
||||||
};
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* __PIPEWIRE_ACCESS_H__ */
|
|
||||||
|
|
@ -1173,7 +1173,9 @@ struct pw_client_node *pw_client_node_new(struct pw_client *client,
|
||||||
this->resource = pw_resource_new(client,
|
this->resource = pw_resource_new(client,
|
||||||
id,
|
id,
|
||||||
client->core->type.client_node,
|
client->core->type.client_node,
|
||||||
this, (pw_destroy_t) client_node_resource_destroy);
|
this,
|
||||||
|
&client_node_methods,
|
||||||
|
(pw_destroy_t) client_node_resource_destroy);
|
||||||
if (this->resource == NULL)
|
if (this->resource == NULL)
|
||||||
goto error_no_resource;
|
goto error_no_resource;
|
||||||
|
|
||||||
|
|
@ -1184,8 +1186,6 @@ struct pw_client_node *pw_client_node_new(struct pw_client *client,
|
||||||
pw_signal_add(&this->node->loop_changed, &impl->loop_changed, on_loop_changed);
|
pw_signal_add(&this->node->loop_changed, &impl->loop_changed, on_loop_changed);
|
||||||
pw_signal_add(&impl->core->global_added, &impl->global_added, on_global_added);
|
pw_signal_add(&impl->core->global_added, &impl->global_added, on_global_added);
|
||||||
|
|
||||||
this->resource->implementation = &client_node_methods;
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
|
||||||
error_no_resource:
|
error_no_resource:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ client_bind_func(struct pw_global *global, struct pw_client *client, uint32_t ve
|
||||||
struct pw_client *this = global->object;
|
struct pw_client *this = global->object;
|
||||||
struct pw_resource *resource;
|
struct pw_resource *resource;
|
||||||
|
|
||||||
resource = pw_resource_new(client, id, global->type, global->object, client_unbind_func);
|
resource = pw_resource_new(client, id, global->type, global->object, NULL, client_unbind_func);
|
||||||
if (resource == NULL)
|
if (resource == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,19 +38,16 @@ struct impl {
|
||||||
struct spa_support support[4];
|
struct spa_support support[4];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct access_create_node {
|
|
||||||
struct pw_access_data data;
|
|
||||||
char *factory_name;
|
|
||||||
char *name;
|
|
||||||
struct pw_properties *properties;
|
|
||||||
uint32_t new_id;
|
|
||||||
bool async;
|
|
||||||
};
|
|
||||||
/** \endcond */
|
/** \endcond */
|
||||||
|
|
||||||
#define ACCESS_VIEW_GLOBAL(client,global) (client->core->access == NULL || \
|
static bool pw_global_is_visible(struct pw_global *global,
|
||||||
client->core->access->view_global (client->core->access, \
|
struct pw_client *client)
|
||||||
client, global) == SPA_RESULT_OK)
|
{
|
||||||
|
struct pw_core *core = client->core;
|
||||||
|
|
||||||
|
return (core->global_filter == NULL ||
|
||||||
|
core->global_filter(global, client, core->global_filter_data));
|
||||||
|
}
|
||||||
|
|
||||||
static void registry_bind(void *object, uint32_t id, uint32_t version, uint32_t new_id)
|
static void registry_bind(void *object, uint32_t id, uint32_t version, uint32_t new_id)
|
||||||
{
|
{
|
||||||
|
|
@ -66,7 +63,7 @@ static void registry_bind(void *object, uint32_t id, uint32_t version, uint32_t
|
||||||
if (&global->link == &core->global_list)
|
if (&global->link == &core->global_list)
|
||||||
goto no_id;
|
goto no_id;
|
||||||
|
|
||||||
if (!ACCESS_VIEW_GLOBAL(client, global))
|
if (!pw_global_is_visible(global, client))
|
||||||
goto no_id;
|
goto no_id;
|
||||||
|
|
||||||
pw_log_debug("global %p: bind object id %d to %d", global, id, new_id);
|
pw_log_debug("global %p: bind object id %d to %d", global, id, new_id);
|
||||||
|
|
@ -118,16 +115,17 @@ static void core_get_registry(void *object, uint32_t new_id)
|
||||||
|
|
||||||
registry_resource = pw_resource_new(client,
|
registry_resource = pw_resource_new(client,
|
||||||
new_id,
|
new_id,
|
||||||
this->type.registry, this, destroy_registry_resource);
|
this->type.registry,
|
||||||
|
this,
|
||||||
|
®istry_methods,
|
||||||
|
destroy_registry_resource);
|
||||||
if (registry_resource == NULL)
|
if (registry_resource == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
registry_resource->implementation = ®istry_methods;
|
|
||||||
|
|
||||||
spa_list_insert(this->registry_resource_list.prev, ®istry_resource->link);
|
spa_list_insert(this->registry_resource_list.prev, ®istry_resource->link);
|
||||||
|
|
||||||
spa_list_for_each(global, &this->global_list, link) {
|
spa_list_for_each(global, &this->global_list, link) {
|
||||||
if (ACCESS_VIEW_GLOBAL(client, global))
|
if (pw_global_is_visible(global, client))
|
||||||
pw_registry_notify_global(registry_resource,
|
pw_registry_notify_global(registry_resource,
|
||||||
global->id,
|
global->id,
|
||||||
spa_type_map_get_type(this->type.map,
|
spa_type_map_get_type(this->type.map,
|
||||||
|
|
@ -143,73 +141,6 @@ static void core_get_registry(void *object, uint32_t new_id)
|
||||||
resource->id, SPA_RESULT_NO_MEMORY, "no memory");
|
resource->id, SPA_RESULT_NO_MEMORY, "no memory");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *async_create_node_start(struct pw_access_data *data, size_t size)
|
|
||||||
{
|
|
||||||
struct access_create_node *d;
|
|
||||||
struct pw_client *client = data->resource->client;
|
|
||||||
|
|
||||||
d = calloc(1, sizeof(struct access_create_node) + size);
|
|
||||||
memcpy(d, data, sizeof(struct access_create_node));
|
|
||||||
d->factory_name = strdup(d->factory_name);
|
|
||||||
d->name = strdup(d->name);
|
|
||||||
d->async = true;
|
|
||||||
d->data.user_data = SPA_MEMBER(d, sizeof(struct access_create_node), void);
|
|
||||||
|
|
||||||
pw_client_set_busy(client, true);
|
|
||||||
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void async_create_node_free(struct pw_access_data *data)
|
|
||||||
{
|
|
||||||
struct access_create_node *d = (struct access_create_node *) data;
|
|
||||||
|
|
||||||
if (d->properties)
|
|
||||||
pw_properties_free(d->properties);
|
|
||||||
if (d->async) {
|
|
||||||
if (d->data.free)
|
|
||||||
d->data.free(&d->data);
|
|
||||||
free(d->factory_name);
|
|
||||||
free(d->name);
|
|
||||||
free(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void async_create_node_complete(struct pw_access_data *data, int res)
|
|
||||||
{
|
|
||||||
struct access_create_node *d = (struct access_create_node *) data;
|
|
||||||
struct pw_resource *resource = d->data.resource;
|
|
||||||
struct pw_client *client = resource->client;
|
|
||||||
struct pw_node_factory *factory;
|
|
||||||
|
|
||||||
if (res != SPA_RESULT_OK)
|
|
||||||
goto denied;
|
|
||||||
|
|
||||||
factory = pw_core_find_node_factory(client->core, d->factory_name);
|
|
||||||
if (factory == NULL)
|
|
||||||
goto no_factory;
|
|
||||||
|
|
||||||
/* error will be posted */
|
|
||||||
pw_node_factory_create_node(factory, client, d->name, d->properties, d->new_id);
|
|
||||||
d->properties = NULL;
|
|
||||||
|
|
||||||
goto done;
|
|
||||||
|
|
||||||
no_factory:
|
|
||||||
pw_log_error("can't find node factory");
|
|
||||||
pw_core_notify_error(client->core_resource,
|
|
||||||
resource->id, SPA_RESULT_INVALID_ARGUMENTS, "unknown factory name");
|
|
||||||
goto done;
|
|
||||||
denied:
|
|
||||||
pw_log_error("create node refused %d", res);
|
|
||||||
pw_core_notify_error(client->core_resource,
|
|
||||||
resource->id, SPA_RESULT_NO_PERMISSION, "operation not allowed");
|
|
||||||
done:
|
|
||||||
async_create_node_free(&d->data);
|
|
||||||
pw_client_set_busy(client, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
core_create_node(void *object,
|
core_create_node(void *object,
|
||||||
const char *factory_name,
|
const char *factory_name,
|
||||||
|
|
@ -219,47 +150,39 @@ core_create_node(void *object,
|
||||||
{
|
{
|
||||||
struct pw_resource *resource = object;
|
struct pw_resource *resource = object;
|
||||||
struct pw_client *client = resource->client;
|
struct pw_client *client = resource->client;
|
||||||
int i;
|
struct pw_node_factory *factory;
|
||||||
struct pw_properties *properties;
|
struct pw_properties *properties;
|
||||||
struct access_create_node access_data;
|
int i;
|
||||||
int res;
|
|
||||||
|
factory = pw_core_find_node_factory(client->core, factory_name);
|
||||||
|
if (factory == NULL)
|
||||||
|
goto no_factory;
|
||||||
|
|
||||||
properties = pw_properties_new(NULL, NULL);
|
properties = pw_properties_new(NULL, NULL);
|
||||||
if (properties == NULL)
|
if (properties == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
for (i = 0; i < props->n_items; i++) {
|
for (i = 0; i < props->n_items; i++)
|
||||||
pw_properties_set(properties, props->items[i].key, props->items[i].value);
|
pw_properties_set(properties, props->items[i].key, props->items[i].value);
|
||||||
}
|
|
||||||
|
|
||||||
access_data.data.resource = resource;
|
/* error will be posted */
|
||||||
access_data.data.async_start = async_create_node_start;
|
pw_node_factory_create_node(factory, client, name, properties, new_id);
|
||||||
access_data.data.complete = async_create_node_complete;
|
properties = NULL;
|
||||||
access_data.data.free = NULL;
|
|
||||||
access_data.factory_name = (char *) factory_name;
|
|
||||||
access_data.name = (char *) name;
|
|
||||||
access_data.properties = properties;
|
|
||||||
access_data.new_id = new_id;
|
|
||||||
access_data.async = false;
|
|
||||||
|
|
||||||
if (client->core->access) {
|
done:
|
||||||
res = client->core->access->create_node(client->core->access,
|
|
||||||
&access_data.data,
|
|
||||||
factory_name,
|
|
||||||
name,
|
|
||||||
properties);
|
|
||||||
} else {
|
|
||||||
res = SPA_RESULT_OK;
|
|
||||||
}
|
|
||||||
if (!SPA_RESULT_IS_ASYNC(res))
|
|
||||||
async_create_node_complete(&access_data.data, res);
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
no_factory:
|
||||||
|
pw_log_error("can't find node factory");
|
||||||
|
pw_core_notify_error(client->core_resource,
|
||||||
|
resource->id, SPA_RESULT_INVALID_ARGUMENTS, "unknown factory name");
|
||||||
|
goto done;
|
||||||
|
|
||||||
no_mem:
|
no_mem:
|
||||||
pw_log_error("can't create client node");
|
pw_log_error("can't create properties");
|
||||||
pw_core_notify_error(client->core_resource,
|
pw_core_notify_error(client->core_resource,
|
||||||
resource->id, SPA_RESULT_NO_MEMORY, "no memory");
|
resource->id, SPA_RESULT_NO_MEMORY, "no memory");
|
||||||
return;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -294,7 +217,7 @@ static void core_update_types(void *object, uint32_t first_id, uint32_t n_types,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct pw_core_methods core_methods = {
|
static const struct pw_core_methods core_methods = {
|
||||||
&core_update_types,
|
&core_update_types,
|
||||||
&core_sync,
|
&core_sync,
|
||||||
&core_get_registry,
|
&core_get_registry,
|
||||||
|
|
@ -316,12 +239,10 @@ core_bind_func(struct pw_global *global, struct pw_client *client, uint32_t vers
|
||||||
struct pw_core *this = global->object;
|
struct pw_core *this = global->object;
|
||||||
struct pw_resource *resource;
|
struct pw_resource *resource;
|
||||||
|
|
||||||
resource = pw_resource_new(client, id, global->type, global->object, core_unbind_func);
|
resource = pw_resource_new(client, id, global->type, global->object, &core_methods, core_unbind_func);
|
||||||
if (resource == NULL)
|
if (resource == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
resource->implementation = &core_methods;
|
|
||||||
|
|
||||||
spa_list_insert(this->resource_list.prev, &resource->link);
|
spa_list_insert(this->resource_list.prev, &resource->link);
|
||||||
client->core_resource = resource;
|
client->core_resource = resource;
|
||||||
|
|
||||||
|
|
@ -483,8 +404,8 @@ pw_core_add_global(struct pw_core *core,
|
||||||
pw_log_debug("global %p: new %u %s, owner %p", this, this->id, type_name, owner);
|
pw_log_debug("global %p: new %u %s, owner %p", this, this->id, type_name, owner);
|
||||||
|
|
||||||
spa_list_for_each(registry, &core->registry_resource_list, link)
|
spa_list_for_each(registry, &core->registry_resource_list, link)
|
||||||
if (ACCESS_VIEW_GLOBAL(registry->client, this))
|
if (pw_global_is_visible(this, registry->client))
|
||||||
pw_registry_notify_global(registry, this->id, type_name, this->version);
|
pw_registry_notify_global(registry, this->id, type_name, this->version);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -533,8 +454,8 @@ void pw_global_destroy(struct pw_global *global)
|
||||||
pw_signal_emit(&global->destroy_signal, global);
|
pw_signal_emit(&global->destroy_signal, global);
|
||||||
|
|
||||||
spa_list_for_each(registry, &core->registry_resource_list, link)
|
spa_list_for_each(registry, &core->registry_resource_list, link)
|
||||||
if (ACCESS_VIEW_GLOBAL(registry->client, global))
|
if (pw_global_is_visible(global, registry->client))
|
||||||
pw_registry_notify_global_remove(registry, global->id);
|
pw_registry_notify_global_remove(registry, global->id);
|
||||||
|
|
||||||
pw_map_remove(&core->objects, global->id);
|
pw_map_remove(&core->objects, global->id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ extern "C" {
|
||||||
struct pw_global;
|
struct pw_global;
|
||||||
|
|
||||||
#include <pipewire/client/type.h>
|
#include <pipewire/client/type.h>
|
||||||
#include <pipewire/server/access.h>
|
#include <pipewire/client/interfaces.h>
|
||||||
|
|
||||||
#include <pipewire/server/main-loop.h>
|
#include <pipewire/server/main-loop.h>
|
||||||
#include <pipewire/server/data-loop.h>
|
#include <pipewire/server/data-loop.h>
|
||||||
#include <pipewire/server/node.h>
|
#include <pipewire/server/node.h>
|
||||||
|
|
@ -87,6 +88,9 @@ struct pw_global;
|
||||||
typedef int (*pw_bind_func_t) (struct pw_global *global,
|
typedef int (*pw_bind_func_t) (struct pw_global *global,
|
||||||
struct pw_client *client, uint32_t version, uint32_t id);
|
struct pw_client *client, uint32_t version, uint32_t id);
|
||||||
|
|
||||||
|
typedef bool (*pw_global_filter_func_t) (struct pw_global *global,
|
||||||
|
struct pw_client *client, void *data);
|
||||||
|
|
||||||
/** \page page_global Global
|
/** \page page_global Global
|
||||||
*
|
*
|
||||||
* Global objects represent resources that are available on the server and
|
* Global objects represent resources that are available on the server and
|
||||||
|
|
@ -141,7 +145,9 @@ struct pw_core {
|
||||||
struct pw_properties *properties; /**< properties of the core */
|
struct pw_properties *properties; /**< properties of the core */
|
||||||
|
|
||||||
struct pw_type type; /**< type map and common types */
|
struct pw_type type; /**< type map and common types */
|
||||||
struct pw_access *access; /**< access control checks */
|
|
||||||
|
pw_global_filter_func_t global_filter;
|
||||||
|
void *global_filter_data;
|
||||||
|
|
||||||
struct pw_map objects; /**< map of known objects */
|
struct pw_map objects; /**< map of known objects */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -834,7 +834,7 @@ link_bind_func(struct pw_global *global, struct pw_client *client, uint32_t vers
|
||||||
struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
|
struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this);
|
||||||
struct pw_resource *resource;
|
struct pw_resource *resource;
|
||||||
|
|
||||||
resource = pw_resource_new(client, id, global->type, global->object, link_unbind_func);
|
resource = pw_resource_new(client, id, global->type, global->object, NULL, link_unbind_func);
|
||||||
if (resource == NULL)
|
if (resource == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
pipewirecore_headers = [
|
pipewirecore_headers = [
|
||||||
'access.h',
|
|
||||||
'client.h',
|
'client.h',
|
||||||
'client-node.h',
|
'client-node.h',
|
||||||
'command.h',
|
'command.h',
|
||||||
|
|
@ -17,7 +16,6 @@ pipewirecore_headers = [
|
||||||
]
|
]
|
||||||
|
|
||||||
pipewirecore_sources = [
|
pipewirecore_sources = [
|
||||||
'access.c',
|
|
||||||
'client.c',
|
'client.c',
|
||||||
'client-node.c',
|
'client-node.c',
|
||||||
'command.c',
|
'command.c',
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ module_bind_func(struct pw_global *global, struct pw_client *client, uint32_t ve
|
||||||
struct pw_module *this = global->object;
|
struct pw_module *this = global->object;
|
||||||
struct pw_resource *resource;
|
struct pw_resource *resource;
|
||||||
|
|
||||||
resource = pw_resource_new(client, id, global->type, global->object, NULL);
|
resource = pw_resource_new(client, id, global->type, global->object, NULL, NULL);
|
||||||
if (resource == NULL)
|
if (resource == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -465,7 +465,7 @@ node_bind_func(struct pw_global *global, struct pw_client *client, uint32_t vers
|
||||||
struct pw_node *this = global->object;
|
struct pw_node *this = global->object;
|
||||||
struct pw_resource *resource;
|
struct pw_resource *resource;
|
||||||
|
|
||||||
resource = pw_resource_new(client, id, global->type, global->object, node_unbind_func);
|
resource = pw_resource_new(client, id, global->type, global->object, NULL, node_unbind_func);
|
||||||
if (resource == NULL)
|
if (resource == NULL)
|
||||||
goto no_mem;
|
goto no_mem;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,11 @@ struct impl {
|
||||||
/** \endcond */
|
/** \endcond */
|
||||||
|
|
||||||
struct pw_resource *pw_resource_new(struct pw_client *client,
|
struct pw_resource *pw_resource_new(struct pw_client *client,
|
||||||
uint32_t id, uint32_t type, void *object, pw_destroy_t destroy)
|
uint32_t id,
|
||||||
|
uint32_t type,
|
||||||
|
void *object,
|
||||||
|
const void *implementation,
|
||||||
|
pw_destroy_t destroy)
|
||||||
{
|
{
|
||||||
struct impl *impl;
|
struct impl *impl;
|
||||||
struct pw_resource *this;
|
struct pw_resource *this;
|
||||||
|
|
@ -43,6 +47,7 @@ struct pw_resource *pw_resource_new(struct pw_client *client,
|
||||||
this->client = client;
|
this->client = client;
|
||||||
this->type = type;
|
this->type = type;
|
||||||
this->object = object;
|
this->object = object;
|
||||||
|
this->implementation = implementation;
|
||||||
this->destroy = destroy;
|
this->destroy = destroy;
|
||||||
|
|
||||||
pw_signal_init(&this->destroy_signal);
|
pw_signal_init(&this->destroy_signal);
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,19 @@ struct pw_resource {
|
||||||
const struct pw_interface *iface; /**< protocol specific interface functions */
|
const struct pw_interface *iface; /**< protocol specific interface functions */
|
||||||
const void *implementation; /**< implementation */
|
const void *implementation; /**< implementation */
|
||||||
|
|
||||||
|
void *access_private; /**< private data for access control */
|
||||||
|
|
||||||
/** Emited when the resource is destroyed */
|
/** Emited when the resource is destroyed */
|
||||||
PW_SIGNAL(destroy_signal, (struct pw_listener *listener, struct pw_resource *resource));
|
PW_SIGNAL(destroy_signal, (struct pw_listener *listener, struct pw_resource *resource));
|
||||||
};
|
};
|
||||||
|
|
||||||
struct pw_resource *
|
struct pw_resource *
|
||||||
pw_resource_new(struct pw_client *client,
|
pw_resource_new(struct pw_client *client,
|
||||||
uint32_t id, uint32_t type, void *object, pw_destroy_t destroy);
|
uint32_t id,
|
||||||
|
uint32_t type,
|
||||||
|
void *object,
|
||||||
|
const void *implementation,
|
||||||
|
pw_destroy_t destroy);
|
||||||
|
|
||||||
void
|
void
|
||||||
pw_resource_destroy(struct pw_resource *resource);
|
pw_resource_destroy(struct pw_resource *resource);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue