From ef064d625b847e05a017fcc307e67c6f31880ce7 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Wed, 25 Sep 2024 15:54:44 +0200 Subject: gopenuriportal: Fix two memory leaks --- gio/gopenuriportal.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gio/gopenuriportal.c b/gio/gopenuriportal.c index 461b8388b..27f9d343c 100644 --- a/gio/gopenuriportal.c +++ b/gio/gopenuriportal.c @@ -249,6 +249,8 @@ open_call_done (GObject *source, NULL); g_object_set_data (G_OBJECT (task), "signal-id", GINT_TO_POINTER (signal_id)); } + + g_free (path); } void @@ -334,6 +336,7 @@ g_openuri_portal_open_file_async (GFile *file, errsv = errno; if (fd == -1) { + g_clear_object (&task); g_task_report_new_error (NULL, callback, user_data, NULL, G_IO_ERROR, g_io_error_from_errno (errsv), "OpenURI portal is not available"); -- cgit v1.2.3-70-g09d2 From 89488e98e9d066d269fe3097ccfd27388cc3d729 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Wed, 25 Sep 2024 18:12:02 +0200 Subject: gopenuriportal: Use task data instead of object data The docs recommend using a small number of `keys` when setting data for an object, the OpenURI portal uses three different `keys` that can easily be replaced with `g_task_set_task_data()`. --- gio/gopenuriportal.c | 68 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/gio/gopenuriportal.c b/gio/gopenuriportal.c index 27f9d343c..707a4a929 100644 --- a/gio/gopenuriportal.c +++ b/gio/gopenuriportal.c @@ -165,6 +165,29 @@ enum { XDG_DESKTOP_PORTAL_FAILED = 2 }; +typedef struct +{ + char *response_handle; + unsigned int response_signal_id; + gboolean open_file; +} CallData; + +static CallData * +call_data_new (void) +{ + return g_new0 (CallData, 1); +} + +static void +call_data_free (gpointer data) +{ + CallData *call = data; + + g_assert (call->response_signal_id == 0); + g_clear_pointer (&call->response_handle, g_free); + g_free_sized (data, sizeof (CallData)); +} + static void response_received (GDBusConnection *connection, const char *sender_name, @@ -175,11 +198,12 @@ response_received (GDBusConnection *connection, gpointer user_data) { GTask *task = user_data; + CallData *call_data; guint32 response; - guint signal_id; - signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "signal-id")); - g_dbus_connection_signal_unsubscribe (connection, signal_id); + call_data = g_task_get_task_data (task); + g_dbus_connection_signal_unsubscribe (connection, call_data->response_signal_id); + call_data->response_signal_id = 0; g_variant_get (parameters, "(u@a{sv})", &response, NULL); @@ -208,17 +232,15 @@ open_call_done (GObject *source, GXdpOpenURI *openuri = GXDP_OPEN_URI (source); GDBusConnection *connection; GTask *task = user_data; + CallData *call_data; GError *error = NULL; - gboolean open_file; gboolean res; char *path = NULL; - const char *handle; - guint signal_id; + call_data = g_task_get_task_data (task); connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (openuri)); - open_file = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "open-file")); - if (open_file) + if (call_data->open_file) res = gxdp_open_uri_call_open_file_finish (openuri, &path, NULL, result, &error); else res = gxdp_open_uri_call_open_uri_finish (openuri, &path, result, &error); @@ -231,11 +253,12 @@ open_call_done (GObject *source, return; } - handle = (const char *)g_object_get_data (G_OBJECT (task), "handle"); - if (g_strcmp0 (handle, path) != 0) + if (g_strcmp0 (call_data->response_handle, path) != 0) { - signal_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "signal-id")); - g_dbus_connection_signal_unsubscribe (connection, signal_id); + guint signal_id; + + g_dbus_connection_signal_unsubscribe (connection, call_data->response_signal_id); + call_data->response_signal_id = 0; signal_id = g_dbus_connection_signal_subscribe (connection, "org.freedesktop.portal.Desktop", @@ -247,7 +270,9 @@ open_call_done (GObject *source, response_received, task, NULL); - g_object_set_data (G_OBJECT (task), "signal-id", GINT_TO_POINTER (signal_id)); + g_clear_pointer (&call_data->response_handle, g_free); + call_data->response_signal_id = signal_id; + call_data->response_handle = g_steal_pointer (&path); } g_free (path); @@ -261,6 +286,7 @@ g_openuri_portal_open_file_async (GFile *file, GAsyncReadyCallback callback, gpointer user_data) { + CallData *call_data; GDBusConnection *connection; GTask *task; GVariant *opts = NULL; @@ -293,7 +319,6 @@ g_openuri_portal_open_file_async (GFile *file, sender[i] = '_'; handle = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s", sender, token); - g_object_set_data_full (G_OBJECT (task), "handle", handle, g_free); g_free (sender); signal_id = g_dbus_connection_signal_subscribe (connection, @@ -306,7 +331,6 @@ g_openuri_portal_open_file_async (GFile *file, response_received, task, NULL); - g_object_set_data (G_OBJECT (task), "signal-id", GINT_TO_POINTER (signal_id)); g_variant_builder_init_static (&opt_builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&opt_builder, "{sv}", "handle_token", g_variant_new_string (token)); @@ -318,9 +342,17 @@ g_openuri_portal_open_file_async (GFile *file, g_variant_new_string (startup_id)); opts = g_variant_builder_end (&opt_builder); + + call_data = call_data_new (); + call_data->response_handle = g_steal_pointer (&handle); + call_data->response_signal_id = signal_id; + g_task_set_task_data (task, call_data, call_data_free); } else - task = NULL; + { + call_data = NULL; + task = NULL; + } if (g_file_is_native (file)) { @@ -328,8 +360,8 @@ g_openuri_portal_open_file_async (GFile *file, GUnixFDList *fd_list = NULL; int fd, fd_id, errsv; - if (task) - g_object_set_data (G_OBJECT (task), "open-file", GINT_TO_POINTER (TRUE)); + if (call_data) + call_data->open_file = TRUE; path = g_file_get_path (file); fd = g_open (path, O_RDONLY | O_CLOEXEC); -- cgit v1.2.3-70-g09d2 From 0758788a25d93c75c1e210dd18d4dda0ce79cffa Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Wed, 25 Sep 2024 16:00:48 +0200 Subject: gopenuriportal: Create GXdpOpenURI DBus proxy when needed Instead of using singleton initialized the first time when it's needed we can create the `DBusProxy` object when needed without blocking since the interface doesn't have any properties nor signals. --- gio/gopenuriportal.c | 81 ++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/gio/gopenuriportal.c b/gio/gopenuriportal.c index 707a4a929..dbd97c23b 100644 --- a/gio/gopenuriportal.c +++ b/gio/gopenuriportal.c @@ -39,59 +39,28 @@ #define HAVE_O_CLOEXEC 1 #endif - -static GXdpOpenURI *openuri; - -static gboolean -init_openuri_portal (void) -{ - static gsize openuri_inited = 0; - - if (g_once_init_enter (&openuri_inited)) - { - GError *error = NULL; - GDBusConnection *connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); - - if (connection != NULL) - { - openuri = gxdp_open_uri_proxy_new_sync (connection, 0, - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - NULL, &error); - if (openuri == NULL) - { - g_warning ("Cannot create document portal proxy: %s", error->message); - g_error_free (error); - } - - g_object_unref (connection); - } - else - { - g_warning ("Cannot connect to session bus when initializing document portal: %s", - error->message); - g_error_free (error); - } - - g_once_init_leave (&openuri_inited, 1); - } - - return openuri != NULL; -} - gboolean g_openuri_portal_open_file (GFile *file, const char *parent_window, const char *startup_id, GError **error) { + GXdpOpenURI *openuri; GVariantBuilder opt_builder; gboolean res; - if (!init_openuri_portal ()) + openuri = gxdp_open_uri_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + NULL, + error); + + if (openuri == NULL) { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, - "OpenURI portal is not available"); + g_prefix_error (error, "Failed to create OpenURI proxy: "); return FALSE; } @@ -156,6 +125,8 @@ g_openuri_portal_open_file (GFile *file, g_free (uri); } + g_clear_object (&openuri); + return res; } @@ -167,6 +138,7 @@ enum { typedef struct { + GXdpOpenURI *proxy; char *response_handle; unsigned int response_signal_id; gboolean open_file; @@ -184,6 +156,7 @@ call_data_free (gpointer data) CallData *call = data; g_assert (call->response_signal_id == 0); + g_clear_object (&call->proxy); g_clear_pointer (&call->response_handle, g_free); g_free_sized (data, sizeof (CallData)); } @@ -287,17 +260,27 @@ g_openuri_portal_open_file_async (GFile *file, gpointer user_data) { CallData *call_data; + GError *error = NULL; GDBusConnection *connection; + GXdpOpenURI *openuri; GTask *task; GVariant *opts = NULL; int i; guint signal_id; - if (!init_openuri_portal ()) + openuri = gxdp_open_uri_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + NULL, + &error); + + if (openuri == NULL) { - g_task_report_new_error (NULL, callback, user_data, NULL, - G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, - "OpenURI portal is not available"); + g_prefix_error (&error, "Failed to create OpenURI proxy: "); + g_task_report_error (NULL, callback, user_data, NULL, error); return; } @@ -344,6 +327,7 @@ g_openuri_portal_open_file_async (GFile *file, opts = g_variant_builder_end (&opt_builder); call_data = call_data_new (); + call_data->proxy = g_object_ref (openuri); call_data->response_handle = g_steal_pointer (&handle); call_data->response_signal_id = signal_id; g_task_set_task_data (task, call_data, call_data_free); @@ -372,6 +356,7 @@ g_openuri_portal_open_file_async (GFile *file, g_task_report_new_error (NULL, callback, user_data, NULL, G_IO_ERROR, g_io_error_from_errno (errsv), "OpenURI portal is not available"); + g_clear_object (&openuri); return; } @@ -408,6 +393,8 @@ g_openuri_portal_open_file_async (GFile *file, task); g_free (uri); } + + g_clear_object (&openuri); } gboolean -- cgit v1.2.3-70-g09d2 From 73970ee0b4ea6351958b01ab6d9973b9eb6b2065 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Wed, 25 Sep 2024 17:03:33 +0200 Subject: gopenuriportal: Improve error messages --- gio/gopenuriportal.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gio/gopenuriportal.c b/gio/gopenuriportal.c index dbd97c23b..308d876e4 100644 --- a/gio/gopenuriportal.c +++ b/gio/gopenuriportal.c @@ -84,7 +84,7 @@ g_openuri_portal_open_file (GFile *file, if (fd == -1) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv), - "Failed to open '%s'", path); + "Failed to open ‘%s’: %s", path, g_strerror (errsv)); g_free (path); g_variant_builder_clear (&opt_builder); return FALSE; @@ -125,6 +125,8 @@ g_openuri_portal_open_file (GFile *file, g_free (uri); } + g_prefix_error (error, "Failed to call OpenURI portal: "); + g_clear_object (&openuri); return res; @@ -355,7 +357,7 @@ g_openuri_portal_open_file_async (GFile *file, g_clear_object (&task); g_task_report_new_error (NULL, callback, user_data, NULL, G_IO_ERROR, g_io_error_from_errno (errsv), - "OpenURI portal is not available"); + "Failed to open ‘%s’: %s", path, g_strerror (errsv)); g_clear_object (&openuri); return; } -- cgit v1.2.3-70-g09d2 From 59877f5cc66d98f1f8f33455e24384bf46605ab7 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Thu, 19 Sep 2024 22:18:16 +0200 Subject: tests: Add launch context to `g_app_info_launch_uris()` tests This ensure that `g_app_info_launch_uris()` uses and set the correct activation token. --- gio/tests/dbus-appinfo.c | 46 +++++++++++++++++++--- gio/tests/org.gtk.test.dbusappinfo.flatpak.desktop | 1 + 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/gio/tests/dbus-appinfo.c b/gio/tests/dbus-appinfo.c index 80861fdd1..4ab86a547 100644 --- a/gio/tests/dbus-appinfo.c +++ b/gio/tests/dbus-appinfo.c @@ -290,6 +290,11 @@ test_dbus_appinfo (void) g_object_unref (app); } +static GType test_flatpak_application_get_type (void); +typedef GApplication TestFlatpakApplication; +typedef GApplicationClass TestFlatpakApplicationClass; +G_DEFINE_TYPE (TestFlatpakApplication, test_flatpak_application, G_TYPE_APPLICATION) + static void on_flatpak_launch_uris_finish (GObject *object, GAsyncResult *result, @@ -300,6 +305,8 @@ on_flatpak_launch_uris_finish (GObject *object, g_app_info_launch_uris_finish (G_APP_INFO (object), result, &error); g_assert_no_error (error); + g_assert_true (requested_startup_id); + g_assert_true (saw_startup_id); g_application_release (app); } @@ -308,6 +315,7 @@ static void on_flatpak_activate (GApplication *app, gpointer user_data) { + GAppLaunchContext *ctx; GDesktopAppInfo *flatpak_appinfo = user_data; char *uri; GList *uris; @@ -318,8 +326,12 @@ on_flatpak_activate (GApplication *app, uri = g_filename_to_uri (g_desktop_app_info_get_filename (flatpak_appinfo), NULL, NULL); g_assert_nonnull (uri); uris = g_list_prepend (NULL, uri); - g_app_info_launch_uris_async (G_APP_INFO (flatpak_appinfo), uris, NULL, + ctx = g_object_new (test_app_launch_context_get_type (), NULL); + requested_startup_id = FALSE; + saw_startup_id = FALSE; + g_app_info_launch_uris_async (G_APP_INFO (flatpak_appinfo), uris, ctx, NULL, on_flatpak_launch_uris_finish, app); + g_object_unref (ctx); g_list_free (uris); g_free (uri); } @@ -341,6 +353,17 @@ on_flatpak_open (GApplication *app, g_object_unref (f); } +static void +test_flatpak_application_init (TestApplication *app) +{ +} + +static void +test_flatpak_application_class_init (GApplicationClass *class) +{ + class->before_emit = test_application_before_emit; +} + static void test_flatpak_doc_export (void) { @@ -364,8 +387,10 @@ test_flatpak_doc_export (void) g_assert_nonnull (flatpak_appinfo); g_free (desktop_file); - app = g_application_new ("org.gtk.test.dbusappinfo.flatpak", - G_APPLICATION_HANDLES_OPEN); + app = g_object_new (test_flatpak_application_get_type (), + "application-id", "org.gtk.test.dbusappinfo.flatpak", + "flags", G_APPLICATION_HANDLES_OPEN, + NULL); g_signal_connect (app, "activate", G_CALLBACK (on_flatpak_activate), flatpak_appinfo); g_signal_connect (app, "open", G_CALLBACK (on_flatpak_open), NULL); @@ -389,6 +414,8 @@ on_flatpak_launch_invalid_uri_finish (GObject *object, g_app_info_launch_uris_finish (G_APP_INFO (object), result, &error); g_assert_no_error (error); + g_assert_true (requested_startup_id); + g_assert_true (saw_startup_id); g_application_release (app); } @@ -397,6 +424,7 @@ static void on_flatpak_activate_invalid_uri (GApplication *app, gpointer user_data) { + GAppLaunchContext *ctx; GDesktopAppInfo *flatpak_appinfo = user_data; GList *uris; @@ -404,8 +432,12 @@ on_flatpak_activate_invalid_uri (GApplication *app, g_application_hold (app); uris = g_list_prepend (NULL, "file:///hopefully/an/invalid/path.desktop"); - g_app_info_launch_uris_async (G_APP_INFO (flatpak_appinfo), uris, NULL, + ctx = g_object_new (test_app_launch_context_get_type (), NULL); + requested_startup_id = FALSE; + saw_startup_id = FALSE; + g_app_info_launch_uris_async (G_APP_INFO (flatpak_appinfo), uris, ctx, NULL, on_flatpak_launch_invalid_uri_finish, app); + g_object_unref (ctx); g_list_free (uris); } @@ -448,8 +480,10 @@ test_flatpak_missing_doc_export (void) flatpak_appinfo = g_desktop_app_info_new_from_filename (desktop_file); g_assert_nonnull (flatpak_appinfo); - app = g_application_new ("org.gtk.test.dbusappinfo.flatpak", - G_APPLICATION_HANDLES_OPEN); + app = g_object_new (test_flatpak_application_get_type (), + "application-id", "org.gtk.test.dbusappinfo.flatpak", + "flags", G_APPLICATION_HANDLES_OPEN, + NULL); g_signal_connect (app, "activate", G_CALLBACK (on_flatpak_activate_invalid_uri), flatpak_appinfo); g_signal_connect (app, "open", G_CALLBACK (on_flatpak_open_invalid_uri), NULL); diff --git a/gio/tests/org.gtk.test.dbusappinfo.flatpak.desktop b/gio/tests/org.gtk.test.dbusappinfo.flatpak.desktop index 9ef248a5f..6a814ae43 100644 --- a/gio/tests/org.gtk.test.dbusappinfo.flatpak.desktop +++ b/gio/tests/org.gtk.test.dbusappinfo.flatpak.desktop @@ -3,3 +3,4 @@ Type=Application Name=Test DBusActivatable=true X-Flatpak=org.gtk.test.dbusappinfo.flatpak +StartupNotify=true -- cgit v1.2.3-70-g09d2 From c8e80132d822f0c00303b64bae9c64594aef45e1 Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Tue, 24 Sep 2024 14:12:15 +0200 Subject: tests: Add tests for launching default for file/uri via XDG portal --- gio/org.freedesktop.portal.Request.xml | 93 +++++++ gio/tests/dbus-appinfo.c | 180 +++++++++++++ gio/tests/fake-desktop-portal.c | 455 +++++++++++++++++++++++++++++++++ gio/tests/fake-desktop-portal.h | 38 +++ gio/tests/meson.build | 33 ++- 5 files changed, 798 insertions(+), 1 deletion(-) create mode 100644 gio/org.freedesktop.portal.Request.xml create mode 100644 gio/tests/fake-desktop-portal.c create mode 100644 gio/tests/fake-desktop-portal.h diff --git a/gio/org.freedesktop.portal.Request.xml b/gio/org.freedesktop.portal.Request.xml new file mode 100644 index 000000000..82848af9b --- /dev/null +++ b/gio/org.freedesktop.portal.Request.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gio/tests/dbus-appinfo.c b/gio/tests/dbus-appinfo.c index 4ab86a547..d2f8e5074 100644 --- a/gio/tests/dbus-appinfo.c +++ b/gio/tests/dbus-appinfo.c @@ -1,5 +1,6 @@ /* * Copyright © 2013 Canonical Limited + * Copyright © 2024 GNOME Foundation Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later * @@ -17,12 +18,14 @@ * Public License along with this library; if not, see . * * Authors: Ryan Lortie + * Julian Sparber */ #include #include #include "gdbus-sessionbus.h" +#include "fake-desktop-portal.h" #include "fake-document-portal.h" static GDesktopAppInfo *appinfo; @@ -498,14 +501,191 @@ test_flatpak_missing_doc_export (void) g_clear_object (&thread); } +static void +check_portal_openuri_call (const char *expected_uri, + GFakeDesktopPortalThread *thread) +{ + const char *activation_token = NULL; + GFile *expected_file = NULL; + GFile *file = NULL; + const char *uri = NULL; + + activation_token = g_fake_desktop_portal_thread_get_last_request_activation_token (thread); + uri = g_fake_desktop_portal_thread_get_last_request_uri (thread); + + g_assert_cmpstr (activation_token, ==, "expected startup id"); + g_assert_nonnull (uri); + + expected_file = g_file_new_for_uri (expected_uri); + file = g_file_new_for_uri (uri); + g_assert_true (g_file_equal (expected_file, file)); + + g_object_unref (file); + g_object_unref (expected_file); +} + +static void +test_portal_open_file (void) +{ + GAppLaunchContext *ctx; + GError *error = NULL; + char *uri; + GFakeDesktopPortalThread *thread = NULL; + + /* Run a fake-desktop-portal */ + thread = g_fake_desktop_portal_thread_new (session_bus_get_address ()); + g_fake_desktop_portal_thread_run (thread); + + uri = g_filename_to_uri (g_test_get_filename (G_TEST_DIST, + "org.gtk.test.dbusappinfo.flatpak.desktop", + NULL), + NULL, + NULL); + + ctx = g_object_new (test_app_launch_context_get_type (), NULL); + + requested_startup_id = FALSE; + + g_app_info_launch_default_for_uri (uri, ctx, &error); + + g_assert_no_error (error); + g_assert_true (requested_startup_id); + + g_fake_desktop_portal_thread_stop (thread); + check_portal_openuri_call (uri, thread); + + g_clear_object (&ctx); + g_clear_object (&thread); + g_clear_pointer (&uri, g_free); +} + +static void +test_portal_open_uri (void) +{ + GAppLaunchContext *ctx; + GError *error = NULL; + const char *uri = "http://example.com"; + GFakeDesktopPortalThread *thread = NULL; + + /* Run a fake-desktop-portal */ + thread = g_fake_desktop_portal_thread_new (session_bus_get_address ()); + g_fake_desktop_portal_thread_run (thread); + + ctx = g_object_new (test_app_launch_context_get_type (), NULL); + + requested_startup_id = FALSE; + + g_app_info_launch_default_for_uri (uri, ctx, &error); + + g_assert_no_error (error); + g_assert_true (requested_startup_id); + + g_fake_desktop_portal_thread_stop (thread); + check_portal_openuri_call (uri, thread); + + g_clear_object (&ctx); + g_clear_object (&thread); +} + +static void +on_launch_default_for_uri_finished (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GError *error = NULL; + gboolean *called = user_data; + + g_app_info_launch_default_for_uri_finish (result, &error); + g_assert_no_error (error); + + *called = TRUE; + + g_main_context_wakeup (NULL); +} + +static void +test_portal_open_file_async (void) +{ + GAppLaunchContext *ctx; + gboolean called = FALSE; + char *uri; + GFakeDesktopPortalThread *thread = NULL; + + /* Run a fake-desktop-portal */ + thread = g_fake_desktop_portal_thread_new (session_bus_get_address ()); + g_fake_desktop_portal_thread_run (thread); + + uri = g_filename_to_uri (g_test_get_filename (G_TEST_DIST, + "org.gtk.test.dbusappinfo.flatpak.desktop", + NULL), + NULL, + NULL); + + ctx = g_object_new (test_app_launch_context_get_type (), NULL); + + requested_startup_id = FALSE; + + g_app_info_launch_default_for_uri_async (uri, ctx, + NULL, on_launch_default_for_uri_finished, &called); + + while (!called) + g_main_context_iteration (NULL, TRUE); + + g_assert_true (requested_startup_id); + + g_fake_desktop_portal_thread_stop (thread); + check_portal_openuri_call (uri, thread); + + g_clear_pointer (&uri, g_free); + g_clear_object (&ctx); + g_clear_object (&thread); +} + +static void +test_portal_open_uri_async (void) +{ + GAppLaunchContext *ctx; + gboolean called = FALSE; + const char *uri = "http://example.com"; + GFakeDesktopPortalThread *thread = NULL; + + /* Run a fake-desktop-portal */ + thread = g_fake_desktop_portal_thread_new (session_bus_get_address ()); + g_fake_desktop_portal_thread_run (thread); + + ctx = g_object_new (test_app_launch_context_get_type (), NULL); + + requested_startup_id = FALSE; + + g_app_info_launch_default_for_uri_async (uri, ctx, + NULL, on_launch_default_for_uri_finished, &called); + + while (!called) + g_main_context_iteration (NULL, TRUE); + + g_assert_true (requested_startup_id); + + g_fake_desktop_portal_thread_stop (thread); + check_portal_openuri_call (uri, thread); + + g_clear_object (&ctx); + g_clear_object (&thread); +} + int main (int argc, char **argv) { g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); + g_setenv ("GIO_USE_PORTALS", "1", TRUE); + g_test_add_func ("/appinfo/dbusappinfo", test_dbus_appinfo); g_test_add_func ("/appinfo/flatpak-doc-export", test_flatpak_doc_export); g_test_add_func ("/appinfo/flatpak-missing-doc-export", test_flatpak_missing_doc_export); + g_test_add_func ("/appinfo/portal-open-file", test_portal_open_file); + g_test_add_func ("/appinfo/portal-open-uri", test_portal_open_uri); + g_test_add_func ("/appinfo/portal-open-file-async", test_portal_open_file_async); + g_test_add_func ("/appinfo/portal-open-uri-async", test_portal_open_uri_async); return session_bus_run (); } diff --git a/gio/tests/fake-desktop-portal.c b/gio/tests/fake-desktop-portal.c new file mode 100644 index 000000000..50b4555b0 --- /dev/null +++ b/gio/tests/fake-desktop-portal.c @@ -0,0 +1,455 @@ +/* + * Copyright © 2024 GNOME Foundation Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + * + * Authors: Julian Sparber + * Philip Withnall + */ + +/* A stub implementation of xdg-desktop-portal */ + +#include +#include +#include + + +#include "fake-desktop-portal.h" +#include "fake-openuri-portal-generated.h" +#include "fake-request-portal-generated.h" + +struct _GFakeDesktopPortalThread +{ + GObject parent_instance; + + char *address; /* (not nullable) */ + GCancellable *cancellable; /* (not nullable) (owned) */ + GThread *thread; /* (not nullable) (owned) */ + GCond cond; /* (mutex mutex) */ + GMutex mutex; + gboolean ready; /* (mutex mutex) */ + + char *request_activation_token; /* (mutex mutex) */ + char *request_uri; /* (mutex mutex) */ +} FakeDesktopPortalThread; + +G_DEFINE_FINAL_TYPE (GFakeDesktopPortalThread, g_fake_desktop_portal_thread, G_TYPE_OBJECT) + +static void g_fake_desktop_portal_thread_finalize (GObject *object); + +static void +g_fake_desktop_portal_thread_class_init (GFakeDesktopPortalThreadClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = g_fake_desktop_portal_thread_finalize; +} + +static void +g_fake_desktop_portal_thread_init (GFakeDesktopPortalThread *self) +{ + self->cancellable = g_cancellable_new (); + g_cond_init (&self->cond); + g_mutex_init (&self->mutex); +} + +static void +g_fake_desktop_portal_thread_finalize (GObject *object) +{ + GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (object); + + g_assert (self->thread == NULL); /* should already have been joined */ + + g_mutex_clear (&self->mutex); + g_cond_clear (&self->cond); + g_clear_object (&self->cancellable); + g_clear_pointer (&self->address, g_free); + + g_clear_pointer (&self->request_activation_token, g_free); + g_clear_pointer (&self->request_uri, g_free); + + G_OBJECT_CLASS (g_fake_desktop_portal_thread_parent_class)->finalize (object); +} + +static gboolean +on_handle_close (FakeRequest *object, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + g_test_message ("Got close request"); + fake_request_complete_close (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static char* +get_request_path (GDBusMethodInvocation *invocation, + const char *token) +{ + char *request_obj_path; + char *sender; + + sender = g_strdup (g_dbus_method_invocation_get_sender (invocation) + 1); + + /* The object path needs to be the specific format. + * See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request */ + for (size_t i = 0; sender[i]; i++) + if (sender[i] == '.') + sender[i] = '_'; + + request_obj_path = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s", sender, token); + g_free (sender); + + return request_obj_path; +} + +static gboolean +handle_request (GFakeDesktopPortalThread *self, + FakeOpenURI *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_uri, + gboolean open_file, + GVariant *arg_options) +{ + const char *activation_token = NULL; + GError *error = NULL; + FakeRequest *interface_request; + GVariantBuilder opt_builder; + char *request_obj_path; + const char *token = NULL; + + if (arg_options) + { + g_variant_lookup (arg_options, "activation_token", "&s", &activation_token); + g_variant_lookup (arg_options, "handle_token", "&s", &token); + } + + g_set_str (&self->request_activation_token, activation_token); + g_set_str (&self->request_uri, arg_uri); + + request_obj_path = get_request_path (invocation, token ? token : "t"); + + if (open_file) + { + g_test_message ("Got open file request for %s", arg_uri); + + fake_open_uri_complete_open_file (object, + invocation, + NULL, + request_obj_path); + + } + else + { + g_test_message ("Got open URI request for %s", arg_uri); + + fake_open_uri_complete_open_uri (object, + invocation, + request_obj_path); + + } + + interface_request = fake_request_skeleton_new (); + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + + g_signal_connect (interface_request, + "handle-close", + G_CALLBACK (on_handle_close), + NULL); + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface_request), + g_dbus_method_invocation_get_connection (invocation), + request_obj_path, + &error); + g_assert_no_error (error); + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (interface_request), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + g_test_message ("Request skeleton exported at %s", request_obj_path); + + /* We can't use `fake_request_emit_response()` because it doesn't set the sender */ + g_dbus_connection_emit_signal (g_dbus_method_invocation_get_connection (invocation), + g_dbus_method_invocation_get_sender (invocation), + request_obj_path, + "org.freedesktop.portal.Request", + "Response", + g_variant_new ("(u@a{sv})", + 0, /* Success */ + g_variant_builder_end (&opt_builder)), + NULL); + + g_test_message ("Response emitted"); + + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface_request)); + g_free (request_obj_path); + g_object_unref (interface_request); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static char * +handle_to_uri (GVariant *handle, + GUnixFDList *fd_list) +{ + int fd = -1; + int fd_id; + char *proc_path; + char *path; + char *uri; + + fd_id = g_variant_get_handle (handle); + fd = g_unix_fd_list_get (fd_list, fd_id, NULL); + + if (fd == -1) + return NULL; + + proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); + path = g_file_read_link (proc_path, NULL); + g_assert_nonnull (path); + + uri = g_filename_to_uri (path, NULL, NULL); + g_free (proc_path); + g_free (path); + close (fd); + + return uri; +} + +static gboolean +on_handle_open_file (FakeOpenURI *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_parent_window, + GVariant *arg_fd, + GVariant *arg_options, + gpointer user_data) +{ + GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data); + char *uri = NULL; + + uri = handle_to_uri (arg_fd, fd_list); + handle_request (self, + object, + invocation, + arg_parent_window, + uri, + TRUE, + arg_options); + g_free (uri); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +on_handle_open_uri (FakeOpenURI *object, + GDBusMethodInvocation *invocation, + const gchar *arg_parent_window, + const gchar *arg_uri, + GVariant *arg_options, + gpointer user_data) +{ + GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data); + + handle_request (self, + object, + invocation, + arg_parent_window, + arg_uri, + TRUE, + arg_options); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data); + + g_test_message ("Acquired the name %s", name); + + g_mutex_lock (&self->mutex); + self->ready = TRUE; + g_cond_signal (&self->cond); + g_mutex_unlock (&self->mutex); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_test_message ("Lost the name %s", name); +} + +static gboolean +cancelled_cb (GCancellable *cancellable, + gpointer user_data) +{ + g_test_message ("fake-desktop-portal cancelled"); + return G_SOURCE_CONTINUE; +} + +static gpointer +fake_desktop_portal_thread_cb (gpointer user_data) +{ + GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data); + GMainContext *context = NULL; + GDBusConnection *connection = NULL; + GSource *source = NULL; + guint id; + FakeOpenURI *interface_open_uri; + GError *local_error = NULL; + + context = g_main_context_new (); + g_main_context_push_thread_default (context); + + connection = g_dbus_connection_new_for_address_sync (self->address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, + NULL, + self->cancellable, + &local_error); + g_assert_no_error (local_error); + + /* Listen for cancellation. The source will wake up the context iteration + * which can then re-check its exit condition below. */ + source = g_cancellable_source_new (self->cancellable); + g_source_set_callback (source, G_SOURCE_FUNC (cancelled_cb), NULL, NULL); + g_source_attach (source, context); + g_source_unref (source); + + /* Set up the interface */ + g_test_message ("Acquired a message bus connection"); + + interface_open_uri = fake_open_uri_skeleton_new (); + + g_signal_connect (interface_open_uri, + "handle-open-file", + G_CALLBACK (on_handle_open_file), + self); + g_signal_connect (interface_open_uri, + "handle-open-uri", + G_CALLBACK (on_handle_open_uri), + self); + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface_open_uri), + connection, + "/org/freedesktop/portal/desktop", + &local_error); + g_assert_no_error (local_error); + + /* Own the portal name */ + id = g_bus_own_name_on_connection (connection, + "org.freedesktop.portal.Desktop", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_name_acquired, + on_name_lost, + self, + NULL); + + while (!g_cancellable_is_cancelled (self->cancellable)) + g_main_context_iteration (context, TRUE); + + g_bus_unown_name (id); + g_clear_object (&connection); + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface_open_uri)); + g_object_unref (interface_open_uri); + g_main_context_pop_thread_default (context); + g_clear_pointer (&context, g_main_context_unref); + + return NULL; +} + +/* Get the activation token given to the most recent OpenURI request + * + * Returns: (transfer none) (nullable: an activation token + */ +const gchar * +g_fake_desktop_portal_thread_get_last_request_activation_token (GFakeDesktopPortalThread *self) +{ + g_return_val_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self), NULL); + + return self->request_activation_token; +} + +/* Get the file or URI given to the most recent OpenURI request + * + * Returns: (transfer none) (nullable): an URI + */ +const gchar * +g_fake_desktop_portal_thread_get_last_request_uri (GFakeDesktopPortalThread *self) +{ + g_return_val_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self), NULL); + + return self->request_uri; +} + +/* + * Create a new #GFakeDesktopPortalThread. The thread isn’t started until + * g_fake_desktop_portal_thread_run() is called on it. + * + * Returns: (transfer full): the new fake desktop portal wrapper + */ +GFakeDesktopPortalThread * +g_fake_desktop_portal_thread_new (const char *address) +{ + GFakeDesktopPortalThread *self = g_object_new (G_TYPE_FAKE_DESKTOP_PORTAL_THREAD, NULL); + self->address = g_strdup (address); + return g_steal_pointer (&self); +} + +/* + * Start a worker thread which will run a fake + * `org.freedesktop.portal.Desktops` portal on the bus at @address. This is + * intended to be used with #GTestDBus to mock up a portal from within a unit + * test process, rather than relying on D-Bus activation of a mock portal + * subprocess. + * + * It blocks until the thread has owned its D-Bus name and is ready to handle + * requests. + */ +void +g_fake_desktop_portal_thread_run (GFakeDesktopPortalThread *self) +{ + g_return_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self)); + g_return_if_fail (self->thread == NULL); + + self->thread = g_thread_new ("fake-desktop-portal", fake_desktop_portal_thread_cb, self); + + /* Block until the thread is ready. */ + g_mutex_lock (&self->mutex); + while (!self->ready) + g_cond_wait (&self->cond, &self->mutex); + g_mutex_unlock (&self->mutex); +} + +/* Stop and join a worker thread started with fake_desktop_portal_thread_run(). + * Blocks until the thread has stopped and joined. + * + * Once this has been called, it’s safe to drop the final reference on @self. */ +void +g_fake_desktop_portal_thread_stop (GFakeDesktopPortalThread *self) +{ + g_return_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self)); + g_return_if_fail (self->thread != NULL); + + g_cancellable_cancel (self->cancellable); + g_thread_join (g_steal_pointer (&self->thread)); +} diff --git a/gio/tests/fake-desktop-portal.h b/gio/tests/fake-desktop-portal.h new file mode 100644 index 000000000..72f118058 --- /dev/null +++ b/gio/tests/fake-desktop-portal.h @@ -0,0 +1,38 @@ +/* + * Copyright 2024 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + */ + +#ifndef __FAKE_DESKTOP_PORTAL_H__ +#define __FAKE_DESKTOP_PORTAL_H__ + +#include + +G_BEGIN_DECLS + +#define G_TYPE_FAKE_DESKTOP_PORTAL_THREAD (g_fake_desktop_portal_thread_get_type ()) +G_DECLARE_FINAL_TYPE (GFakeDesktopPortalThread, g_fake_desktop_portal_thread, G, FAKE_DESKTOP_PORTAL_THREAD, GObject) + +GFakeDesktopPortalThread *g_fake_desktop_portal_thread_new (const char *address); +const gchar *g_fake_desktop_portal_thread_get_last_request_uri (GFakeDesktopPortalThread *self); +const gchar *g_fake_desktop_portal_thread_get_last_request_activation_token (GFakeDesktopPortalThread *self); +void g_fake_desktop_portal_thread_run (GFakeDesktopPortalThread *self); +void g_fake_desktop_portal_thread_stop (GFakeDesktopPortalThread *self); + +G_END_DECLS + +#endif /* __FAKE_DESKTOP_PORTAL_H__ */ diff --git a/gio/tests/meson.build b/gio/tests/meson.build index 3e4e1e847..273ff01a9 100644 --- a/gio/tests/meson.build +++ b/gio/tests/meson.build @@ -557,10 +557,41 @@ if host_machine.system() != 'windows' '--c-namespace', 'Fake', '@INPUT@']) + fake_openuri_portal_generated = custom_target('fake-openuri-portal-generated', + input : ['../org.freedesktop.portal.OpenURI.xml'], + output : ['fake-openuri-portal-generated.h', + 'fake-openuri-portal-generated.c'], + depend_files : gdbus_codegen_built_files, + depends : gdbus_codegen_built_targets, + command : [python, gdbus_codegen, + '--interface-prefix', 'org.freedesktop.portal.', + '--output-directory', '@OUTDIR@', + '--generate-c-code', 'fake-openuri-portal-generated', + '--c-namespace', 'Fake', + '@INPUT@']) + + fake_request_portal_generated = custom_target('fake-request-portal-generated', + input : ['../org.freedesktop.portal.Request.xml'], + output : ['fake-request-portal-generated.h', + 'fake-request-portal-generated.c'], + depend_files : gdbus_codegen_built_files, + depends : gdbus_codegen_built_targets, + command : [python, gdbus_codegen, + '--interface-prefix', 'org.freedesktop.portal.', + '--output-directory', '@OUTDIR@', + '--generate-c-code', 'fake-request-portal-generated', + '--c-namespace', 'Fake', + '@INPUT@']) + if not glib_have_cocoa gio_tests += { 'dbus-appinfo' : { - 'extra_sources' : [extra_sources, 'fake-document-portal.c', fake_document_portal_generated], + 'extra_sources' : [extra_sources, + 'fake-document-portal.c', + fake_document_portal_generated, + 'fake-desktop-portal.c', + fake_openuri_portal_generated, + fake_request_portal_generated], }, } endif -- cgit v1.2.3-70-g09d2