--- a/src/inhibit.c +++ b/src/inhibit.c @@ -13,6 +13,7 @@ #include "inhibit.h" #include "request.h" +#include "wayfire-inhibit.h" enum { INHIBIT_LOGOUT = 1, @@ -280,6 +281,14 @@ Request *request = (Request *)object; guint cookie; + if (g_object_get_data (G_OBJECT (request), "wayfire-inhibit")) + { + g_object_set_data (G_OBJECT (request), "wayfire-inhibit", NULL); + if (request->exported) request_unexport (request); + xdp_impl_request_complete_close (object, invocation); + return TRUE; + } + /* If we get a Close call before the Inhibit call returned, * delay the uninhibit call until we have the cookie. */ @@ -358,8 +367,24 @@ g_signal_connect (request, "handle-close", G_CALLBACK (handle_close_fdo), NULL); + int bridged = wayfire_inhibit_acquire (request, invocation, arg_handle, arg_window, &error); + if (bridged < 0) + { + g_dbus_method_invocation_return_error (invocation, XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, "%s", + error ? error->message : "Wayfire inhibition unavailable"); + return TRUE; + } + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + if (bridged > 0) + { + g_debug ("Using per-window Wayfire idle inhibition"); + xdp_impl_inhibit_complete_inhibit (object, invocation); + return TRUE; + } + if (!g_variant_lookup (arg_options, "reason", "&s", &reason)) reason = ""; --- a/src/meson.build +++ b/src/meson.build @@ -99,6 +99,7 @@ ], dependencies: [ portal_deps, + dependency('json-glib-1.0'), gtkx11_dep, gtkwayland_dep, ], --- /dev/null +++ b/src/wayfire-inhibit.h @@ -0,0 +1,175 @@ +/* Optional per-window Wayfire bridge. The exported portal request owns the + * connection: Close, caller/broker loss, or backend exit releases its lease. + * Existing GNOME and non-Wayfire ScreenSaver backends remain unchanged. */ +#pragma once + +#include +#include +#include + +typedef struct +{ + GSocketConnection *connection; + guint caller_watch; + guint broker_watch; + GWeakRef request; +} WayfireInhibit; + +static void +wayfire_inhibit_free (gpointer data) +{ + WayfireInhibit *lease = data; + if (lease->caller_watch) g_bus_unwatch_name (lease->caller_watch); + if (lease->broker_watch) g_bus_unwatch_name (lease->broker_watch); + g_io_stream_close (G_IO_STREAM (lease->connection), NULL, NULL); + g_object_unref (lease->connection); + g_weak_ref_clear (&lease->request); + g_free (lease); +} + +static void +wayfire_inhibit_vanished (GDBusConnection *bus, + const gchar *name, + gpointer data) +{ + WayfireInhibit *lease = data; + g_autoptr(Request) request = g_weak_ref_get (&lease->request); + if (request) + { + /* Clearing the data frees lease and both watches, including this one. */ + g_object_set_data (G_OBJECT (request), "wayfire-inhibit", NULL); + if (request->exported) request_unexport (request); + } +} + +static GVariant * +wayfire_bus_call (GDBusConnection *bus, + const gchar *method, + const gchar *name, + const GVariantType *type, + GError **error) +{ + return g_dbus_connection_call_sync (bus, "org.freedesktop.DBus", + "/org/freedesktop/DBus", "org.freedesktop.DBus", + method, g_variant_new ("(s)", name), type, + G_DBUS_CALL_FLAGS_NO_AUTO_START, 1000, NULL, error); +} + +/* 0: no Wayfire bridge; 1: held; -1: a bridge rejected this request. */ +static int +wayfire_inhibit_acquire (Request *request, + GDBusMethodInvocation *invocation, + const gchar *handle, + const gchar *window, + GError **error) +{ + const gchar *path = g_getenv ("WAYFIRE_SOCKET"); + GDBusConnection *bus = g_dbus_method_invocation_get_connection (invocation); + const gchar *broker = g_dbus_method_invocation_get_sender (invocation); + g_autoptr(GSocketClient) client = NULL; + g_autoptr(GSocketAddress) address = NULL; + g_autoptr(GSocketConnection) connection = NULL; + g_autoptr(GCredentials) credentials = NULL; + g_autoptr(GVariant) owner = NULL; + g_autoptr(GVariant) pid_reply = NULL; + g_autofree gchar *caller = NULL; + g_autoptr(JsonBuilder) builder = NULL; + g_autoptr(JsonGenerator) generator = NULL; + g_autoptr(JsonParser) parser = NULL; + g_autoptr(JsonNode) root = NULL; + g_autofree gchar *payload = NULL; + g_autofree gchar *reply = NULL; + guint32 pid, length; + gsize size, transferred; + const gchar *broker_owner; + const gchar *prefix = "/org/freedesktop/portal/desktop/request/"; + const gchar *end; + + if (!path || !*path) return 0; + client = g_socket_client_new (); + g_socket_client_set_timeout (client, 1); + address = g_unix_socket_address_new (path); + connection = g_socket_client_connect (client, G_SOCKET_CONNECTABLE (address), NULL, NULL); + if (!connection) return 0; + credentials = g_socket_get_credentials (g_socket_connection_get_socket (connection), error); + if (!credentials || g_credentials_get_unix_user (credentials, error) != getuid ()) return -1; + + /* Only the current portal broker can supply another application's identity. + * Recover the original unique bus name, not the GTK backend/broker PID or + * an untrusted app_id string. Unique names cannot be reused by a new client. */ + owner = wayfire_bus_call (bus, "GetNameOwner", "org.freedesktop.portal.Desktop", + G_VARIANT_TYPE ("(s)"), error); + if (!owner) return -1; + g_variant_get (owner, "(&s)", &broker_owner); + if (!g_str_equal (broker_owner, broker) || !g_str_has_prefix (handle, prefix)) + goto invalid; + end = strchr (handle + strlen (prefix), '/'); + if (!end || end == handle + strlen (prefix)) goto invalid; + caller = g_strdup_printf (":%.*s", (int)(end - handle - strlen (prefix)), handle + strlen (prefix)); + for (gchar *p = caller + 1; *p; p++) + { + if (*p == '_') *p = '.'; + else if (!g_ascii_isdigit (*p)) goto invalid; + } + if (!g_dbus_is_unique_name (caller)) goto invalid; + pid_reply = wayfire_bus_call (bus, "GetConnectionUnixProcessID", caller, + G_VARIANT_TYPE ("(u)"), error); + if (!pid_reply) return -1; + g_variant_get (pid_reply, "(u)", &pid); + + builder = json_builder_new (); + json_builder_begin_object (builder); + json_builder_set_member_name (builder, "method"); + json_builder_add_string_value (builder, "screensaver/portal-inhibit"); + json_builder_set_member_name (builder, "data"); + json_builder_begin_object (builder); + json_builder_set_member_name (builder, "pid"); + json_builder_add_int_value (builder, pid); + json_builder_set_member_name (builder, "window"); + json_builder_add_string_value (builder, window); + json_builder_end_object (builder); + json_builder_end_object (builder); + root = json_builder_get_root (builder); + generator = json_generator_new (); + json_generator_set_root (generator, root); + payload = json_generator_to_data (generator, &size); + length = size; /* Wayfire's IPC protocol uses a native-endian uint32 length. */ + GOutputStream *out = g_io_stream_get_output_stream (G_IO_STREAM (connection)); + GInputStream *in = g_io_stream_get_input_stream (G_IO_STREAM (connection)); + if (!g_output_stream_write_all (out, &length, sizeof length, NULL, NULL, error) || + !g_output_stream_write_all (out, payload, size, NULL, NULL, error) || + !g_input_stream_read_all (in, &length, sizeof length, &transferred, NULL, error)) return -1; + if (transferred != sizeof length || !length || length > 4096) goto invalid; + reply = g_malloc (length + 1); + if (!g_input_stream_read_all (in, reply, length, &transferred, NULL, error)) return -1; + if (transferred != length) goto invalid; + reply[length] = '\0'; + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, reply, length, error)) return -1; + if (!JSON_NODE_HOLDS_OBJECT (json_parser_get_root (parser))) goto invalid; + JsonObject *result = json_node_get_object (json_parser_get_root (parser)); + if (json_object_has_member (result, "error")) + { + const gchar *message = json_object_get_string_member (result, "error"); + if (g_strcmp0 (message, "No such method found!") == 0) return 0; + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Wayfire inhibition: %s", message); + return -1; + } + if (!json_object_has_member (result, "result") || + g_strcmp0 (json_object_get_string_member (result, "result"), "ok") != 0) goto invalid; + + WayfireInhibit *lease = g_new0 (WayfireInhibit, 1); + lease->connection = g_steal_pointer (&connection); + g_weak_ref_init (&lease->request, request); + g_object_set_data_full (G_OBJECT (request), "wayfire-inhibit", lease, wayfire_inhibit_free); + lease->caller_watch = g_bus_watch_name_on_connection (bus, caller, G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, wayfire_inhibit_vanished, lease, NULL); + lease->broker_watch = g_bus_watch_name_on_connection (bus, broker, G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, wayfire_inhibit_vanished, lease, NULL); + return 1; + +invalid: + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Invalid Wayfire portal identity or response"); + return -1; +}