obs-studio/libobs/util/platform-nix-portal.c
Georges Basile Stavracas Neto 62a2513369 libobs: Make portal inhibitor asynchronous
Currently, libobs contains two inhibitors: the portal-based one, and the
regular one based on each desktop environment's session manager. The portal
inhibitor takes precedence over the session manager one, when available.

Like the session manager inhibitor, the portal inhibitor performs all D-Bus
calls synchronously in the calling thread, which can lead to stalls. This is
not a good practice.

Make the portal inhibitor asynchronous, by using g_dbus_connection_call()
instead of g_dbus_connection_call_sync(). If an uninhibit call is made
before the previous inhibit call finishes, cancel the inhibit call instead.

Fixes obsproject/obs-studio#5314
2021-09-23 07:35:47 -07:00

244 lines
6.5 KiB
C

/*
* Copyright (c) 2015 Hugh Bailey <obs.jim@gmail.com>
* 2021 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <assert.h>
#include <gio/gio.h>
#include "bmem.h"
#include "dstr.h"
#define PORTAL_NAME "org.freedesktop.portal.Desktop"
struct portal_inhibit_info {
GDBusConnection *c;
GCancellable *cancellable;
unsigned int signal_id;
char *sender_name;
char *request_path;
bool active;
};
static void new_request(struct portal_inhibit_info *info, char **out_token,
char **out_path)
{
struct dstr token;
struct dstr path;
uint32_t id;
id = rand();
dstr_init(&token);
dstr_printf(&token, "obs_inhibit_portal%u", id);
*out_token = token.array;
dstr_init(&path);
dstr_printf(&path, "/org/freedesktop/portal/desktop/request/%s/%s",
info->sender_name, token.array);
*out_path = path.array;
}
static inline void unsubscribe_from_request(struct portal_inhibit_info *info)
{
if (info->signal_id > 0) {
g_dbus_connection_signal_unsubscribe(info->c, info->signal_id);
info->signal_id = 0;
}
}
static inline void remove_inhibit_data(struct portal_inhibit_info *info)
{
g_clear_pointer(&info->request_path, bfree);
info->active = false;
}
static void response_received(GDBusConnection *bus, const char *sender_name,
const char *object_path,
const char *interface_name,
const char *signal_name, GVariant *parameters,
gpointer data)
{
UNUSED_PARAMETER(bus);
UNUSED_PARAMETER(sender_name);
UNUSED_PARAMETER(object_path);
UNUSED_PARAMETER(interface_name);
UNUSED_PARAMETER(signal_name);
struct portal_inhibit_info *info = data;
g_autoptr(GVariant) ret = NULL;
uint32_t response;
g_variant_get(parameters, "(u@a{sv})", &response, &ret);
if (response != 0) {
if (response == 1)
blog(LOG_WARNING, "Inhibit denied by user");
remove_inhibit_data(info);
}
unsubscribe_from_request(info);
}
static void inhibited_cb(GObject *source_object, GAsyncResult *result,
gpointer user_data)
{
UNUSED_PARAMETER(source_object);
struct portal_inhibit_info *info = user_data;
g_autoptr(GVariant) reply = NULL;
g_autoptr(GError) error = NULL;
reply = g_dbus_connection_call_finish(info->c, result, &error);
if (error != NULL) {
if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
blog(LOG_ERROR, "Failed to inhibit: %s",
error->message);
unsubscribe_from_request(info);
remove_inhibit_data(info);
}
g_clear_object(&info->cancellable);
}
static void do_inhibit(struct portal_inhibit_info *info, const char *reason)
{
GVariantBuilder options;
uint32_t flags = 0xC;
char *token;
info->active = true;
new_request(info, &token, &info->request_path);
info->signal_id = g_dbus_connection_signal_subscribe(
info->c, PORTAL_NAME, "org.freedesktop.portal.Request",
"Response", info->request_path, NULL,
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, response_received, info,
NULL);
g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&options, "{sv}", "handle_token",
g_variant_new_string(token));
g_variant_builder_add(&options, "{sv}", "reason",
g_variant_new_string(reason));
bfree(token);
info->cancellable = g_cancellable_new();
g_dbus_connection_call(info->c, PORTAL_NAME,
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Inhibit", "Inhibit",
g_variant_new("(sua{sv})", "", flags, &options),
NULL, G_DBUS_CALL_FLAGS_NONE, -1,
info->cancellable, inhibited_cb, info);
}
static void uninhibited_cb(GObject *source_object, GAsyncResult *result,
gpointer user_data)
{
UNUSED_PARAMETER(source_object);
struct portal_inhibit_info *info = user_data;
g_autoptr(GVariant) reply = NULL;
g_autoptr(GError) error = NULL;
reply = g_dbus_connection_call_finish(info->c, result, &error);
if (error)
blog(LOG_WARNING, "Error uninhibiting: %s", error->message);
}
static void do_uninhibit(struct portal_inhibit_info *info)
{
if (info->cancellable) {
/* If uninhibit is called before the inhibit call is finished,
* cancel it instead.
*/
g_cancellable_cancel(info->cancellable);
g_clear_object(&info->cancellable);
} else {
g_dbus_connection_call(info->c, PORTAL_NAME, info->request_path,
"org.freedesktop.portal.Request",
"Close", g_variant_new("()"),
G_VARIANT_TYPE_UNIT,
G_DBUS_CALL_FLAGS_NONE, -1, NULL,
uninhibited_cb, info);
}
remove_inhibit_data(info);
}
void portal_inhibit_info_destroy(struct portal_inhibit_info *info)
{
if (info) {
g_cancellable_cancel(info->cancellable);
unsubscribe_from_request(info);
remove_inhibit_data(info);
g_clear_pointer(&info->sender_name, bfree);
g_clear_object(&info->cancellable);
g_clear_object(&info->c);
bfree(info);
}
}
struct portal_inhibit_info *portal_inhibit_info_create(void)
{
struct portal_inhibit_info *info = bzalloc(sizeof(*info));
g_autoptr(GVariant) reply = NULL;
g_autoptr(GError) error = NULL;
char *aux;
info->c = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (!info->c) {
blog(LOG_ERROR, "Could not create dbus connection: %s",
error->message);
bfree(info);
return NULL;
}
info->sender_name =
bstrdup(g_dbus_connection_get_unique_name(info->c) + 1);
while ((aux = strstr(info->sender_name, ".")) != NULL)
*aux = '_';
reply = g_dbus_connection_call_sync(
info->c, "org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", "GetNameOwner",
g_variant_new("(s)", PORTAL_NAME), NULL,
G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, NULL);
if (reply != NULL) {
blog(LOG_DEBUG, "Found portal inhibitor");
return info;
}
portal_inhibit_info_destroy(info);
return NULL;
}
void portal_inhibit(struct portal_inhibit_info *info, const char *reason,
bool active)
{
if (active == info->active)
return;
if (active)
do_inhibit(info, reason);
else
do_uninhibit(info);
}