diff options
-rw-r--r-- | gio/gdesktopappinfo.c | 653 | ||||
-rw-r--r-- | gio/glocalfilemonitor.c | 14 | ||||
-rw-r--r-- | gio/tests/appinfo-test-actions.desktop | 2 | ||||
-rw-r--r-- | gio/tests/desktop-app-info.c | 214 |
4 files changed, 764 insertions, 119 deletions
diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index 60d6debb2..28e9dcf21 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -2825,32 +2825,367 @@ emit_launch_started (GAppLaunchContext *context, #define _SPAWN_FLAGS_DEFAULT (G_SPAWN_SEARCH_PATH) -static gboolean -g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, - GDBusConnection *session_bus, - const gchar *exec_line, - GList *uris, - GAppLaunchContext *launch_context, - GSpawnFlags spawn_flags, - GSpawnChildSetupFunc user_setup, - gpointer user_setup_data, - GDesktopAppLaunchCallback pid_callback, - gpointer pid_callback_data, - gint stdin_fd, - gint stdout_fd, - gint stderr_fd, - GError **error) +#if defined(__linux__) && !defined(__BIONIC__) +typedef struct { - gboolean completed = FALSE; - GList *old_uris; - GList *dup_uris; + int pipe[2]; + GSpawnChildSetupFunc user_setup; + gpointer user_setup_data; +} SpawnWrapperData; + +static void +launch_uris_with_spawn_pass_delayfd (gpointer user_data) +{ + SpawnWrapperData *data = user_data; + + /* Ensure our synchronization FD is passed to the shell */ + fcntl (data->pipe[0], F_SETFD, 0); + + if (data->user_setup) + data->user_setup (data->user_setup_data); +} + +static gchar * +systemd_unit_name_escape (const gchar *in) +{ + /* Adapted from systemd's unit_name_escape found in src/basic/unit-name.c */ + GString *str = g_string_sized_new (strlen (in)); + + for (; *in; in++) + { + if (g_ascii_isalnum (*in) || *in == ':' || *in == '_' || *in == '.') + g_string_append_c (str, *in); + else + g_string_append_printf (str, "\\x%02x", *in); + } + return g_string_free (str, FALSE); +} + +typedef struct _ScopeCreateData ScopeCreateData; +typedef struct _SpawnTaskData SpawnTaskData; + +struct _ScopeCreateData +{ + SpawnTaskData *task_data; /* (unowned) */ + ScopeCreateData *next; /* (owned) */ + + char *unit_name; /* (owned) */ + char *job_object; /* (owned) */ + int fd; /* (owned) */ +}; + +struct _SpawnTaskData +{ + GTask *task; /* (owned), may be NULL */ + GDBusConnection *connection; /* (owned) */ + gboolean flushed; + guint job_watch; + + ScopeCreateData *scope_tasks; /* (owned) */ +}; + +static void +scope_create_data_free (ScopeCreateData *data) +{ + if (data->fd >= 0) + close (data->fd); + data->fd = -1; + + g_clear_pointer (&data->next, scope_create_data_free); + g_clear_pointer (&data->job_object, g_free); +} + +static void +spawn_task_data_free (SpawnTaskData *data) +{ + if (data->job_watch) + g_dbus_connection_signal_unsubscribe (data->connection, data->job_watch); + g_clear_pointer (&data->scope_tasks, scope_create_data_free); + g_clear_object (&data->connection); + g_clear_object (&data->task); + + g_free (data); +} + +static void +launch_uris_with_spawn_check_done (SpawnTaskData *task_data) +{ + ScopeCreateData *scope_task; + + if (!task_data->flushed) + return; + + for (scope_task = task_data->scope_tasks; scope_task; scope_task = scope_task->next) + { + if (scope_task->fd >= 0) + return; + } + + /* We are done, return the task (if we have one). + * Note that g_desktop_app_info_launch_uris_with_spawn may have returned + * an error already, so we need to check for that case. + */ + if (task_data->task && !g_task_get_completed (task_data->task)) + g_task_return_boolean (task_data->task, TRUE); + + spawn_task_data_free (task_data); +} + +static void +on_systemd_job_removed_cb (GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data) +{ + SpawnTaskData *task_data = user_data; + ScopeCreateData *scope_task; + guint32 id; + const char *path, *unit, *result; + + g_assert (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uoss)"))); + + g_variant_get (parameters, "(u&o&s&s)", &id, &path, &unit, &result); + for (scope_task = task_data->scope_tasks; scope_task; scope_task = scope_task->next) + { + if (g_strcmp0 (scope_task->job_object, path) != 0) + continue; + + if (g_strcmp0 (result, "done") != 0) + g_warning ("Job to create transient unit for child failed with status: %s", result); + + g_assert (scope_task->fd >= 0); + close (scope_task->fd); + scope_task->fd = -1; + + launch_uris_with_spawn_check_done (task_data); + + break; + } +} + +static void +systemd_scope_created_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ScopeCreateData *data = user_data; + GVariant *res = NULL; + GError *error = NULL; + + res = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + if (error != NULL) + { + g_debug ("Failed to create transient unit for child: %s", error->message); + g_error_free (error); + + /* No job was created, give up. */ + g_assert (data->fd >= 0); + close (data->fd); + data->fd = -1; + + launch_uris_with_spawn_check_done (data->task_data); + + return; + } + + g_assert (data->job_object == NULL); + g_variant_get (res, "(o)", &data->job_object); + + if (res) + g_variant_unref (res); + + /* The next step is that we get a JobRemoved notification. */ +} + +static void +create_systemd_scope (GDesktopAppInfo *info, + gint pid, + ScopeCreateData *scope_data) +{ + GVariantBuilder builder; + const char *source_path = NULL; + char *source_path_utf8 = NULL; + char *appid = NULL; + char *appid_escaped = NULL; + char *snid_escaped = NULL; + char *unit_name = NULL; + + /* In this order: + * 1. Actual application ID from file + * 2. Stripping the .desktop from the desktop ID + * 3. Fall back to using the binary name + * 4. Use "unknown", just to be sure, we cannot exec() in that case anyway + */ + if (info->app_id) + { + appid = g_strdup (info->app_id); + } + else if (info->desktop_id && g_str_has_suffix (info->desktop_id, ".desktop")) + { + appid = g_strndup (info->desktop_id, strlen (info->desktop_id) - strlen (".desktop")); + } + else if (info->binary && info->binary[0]) + { + gchar **components; + int i; + + /* info->binary cannot be an empty string */ + components = g_strsplit (info->binary, G_DIR_SEPARATOR_S, -1); + g_assert(components[0] != NULL); - char **argv, **envp; + for (i = 0; components[i+1] != NULL; i++); + appid = g_steal_pointer (&components[i]); + + g_strfreev (components); + } + else + { + appid = g_strdup ("unknown"); + } + + appid_escaped = systemd_unit_name_escape (appid); + + /* Generate a name conforming to + * https://systemd.io/DESKTOP_ENVIRONMENTS/ + * We use the PID to disambiguate, as that should be unique enough. + */ + unit_name = g_strdup_printf ("app-glib-%s-%d.scope", appid_escaped, pid); + + source_path = g_desktop_app_info_get_filename (info); + if (source_path) + source_path_utf8 = g_filename_to_utf8 (source_path, -1, NULL, NULL, NULL); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("(ssa(sv)a(sa(sv)))")); + g_variant_builder_add (&builder, "s", unit_name); + g_variant_builder_add (&builder, "s", "fail"); /* job-mode */ + + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(sv)")); + + /* Add a generic human readable description, can be changed at will. + * Note that this might be localized, but systemd wouldn't generally do this. + */ + if (info->name) + g_variant_builder_add (&builder, + "(sv)", + "Description", + g_variant_new_string (info->name)); + + /* If we have a .desktop file, document that the scope has been "generated" + * from it. + */ + if (source_path_utf8) + g_variant_builder_add (&builder, + "(sv)", + "SourcePath", + g_variant_new_take_string (source_path_utf8)); + + g_variant_builder_add (&builder, + "(sv)", + "PIDs", + g_variant_new_fixed_array (G_VARIANT_TYPE_UINT32, &pid, 1, sizeof (guint32))); + /* Default to let systemd garbage collect failed applications we launched. */ + g_variant_builder_add (&builder, + "(sv)", + "CollectMode", + g_variant_new_string ("inactive-or-failed")); + + g_variant_builder_close (&builder); + + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(sa(sv))")); + g_variant_builder_close (&builder); + + /* It is intentionally to not allow cancellation as cancellation could result + * in the created process to not be moved into a separate systemd unit. + */ + g_dbus_connection_call (scope_data->task_data->connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "StartTransientUnit", + g_variant_builder_end (&builder), + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + systemd_scope_created_cb, + scope_data); + + g_free (appid); + g_free (appid_escaped); + g_free (snid_escaped); + g_free (unit_name); +} + +static void +launch_uris_with_spawn_flush_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + SpawnTaskData *task_data = user_data; + + g_dbus_connection_flush_finish (G_DBUS_CONNECTION (object), result, NULL); + + task_data->flushed = TRUE; + + launch_uris_with_spawn_check_done (task_data); +} +#endif + +static gboolean +g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, + GDBusConnection *session_bus, + const gchar *exec_line, + GList *uris, + GAppLaunchContext *launch_context, + GSpawnFlags spawn_flags, + GSpawnChildSetupFunc user_setup, + gpointer user_setup_data, + GDesktopAppLaunchCallback pid_callback, + gpointer pid_callback_data, + gint stdin_fd, + gint stdout_fd, + gint stderr_fd, + GTask *task, + GError **error_out) +{ +#if defined(__linux__) && !defined(__BIONIC__) + SpawnTaskData *task_data = NULL; +#endif + GList *dup_uris; + GStrv envp = NULL; + GError *error = NULL; + char *sn_id = NULL; int argc; + /* We may get a task to report back on or an error. But never both. + * Note that g_desktop_app_info_launch_action is currently passing neither. + */ + g_assert (!(task && error_out)); g_return_val_if_fail (info != NULL, FALSE); - argv = NULL; +#if defined(__linux__) && !defined(__BIONIC__) + /* Prepare task data for systemd integration */ + if (session_bus) + { + task_data = g_new0 (SpawnTaskData, 1); + if (task) + task_data->task = g_object_ref (task); + task_data->connection = g_object_ref (session_bus); + task_data->job_watch = g_dbus_connection_signal_subscribe (session_bus, + "org.freedesktop.systemd1", + "org.freedesktop.systemd1.Manager", + "JobRemoved", + "/org/freedesktop/systemd1", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_systemd_job_removed_cb, + task_data, + NULL); + } +#endif if (launch_context) envp = g_app_launch_context_get_environment (launch_context); @@ -2864,13 +3199,50 @@ g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, dup_uris = uris; do { + GList *old_uris; + GStrv argv = NULL; + GList *launched_uris = NULL; GPid pid; - GList *launched_uris; GList *iter; - char *sn_id = NULL; - char **wrapped_argv; int i; - gsize j; + + /* Wrap the @argv in a command which will: + * 1. set the `GIO_LAUNCHED_DESKTOP_FILE_PID` environment variable, and + * 2. synchronize startup with the systemd scope creation. + * Both actions need to be done after the new process has been spawned. + * We can't use either setenv() or read()/close() between fork() and + * exec() because we’d rather use posix_spawn() for speed. + * + * For 1. we simply set the environment variable (using export). + * For 2. we pass the read side of a pipe which GLib will close + * after systemd has replied. + * + * `sh` should be available on all the platforms that `GDesktopAppInfo` + * currently supports (since they are all POSIX). If additional platforms + * need to be supported in future, it will probably have to be replaced + * with a wrapper program (grep the GLib git history for + * `gio-launch-desktop` for an example of this which could be + * resurrected). + * + * We skip compiling the systemd synchronization when on android. + */ + +#if defined(__linux__) && !defined(__BIONIC__) + SpawnWrapperData wrapper_data; + /* The command does: + * 0. Set GIO_LAUNCHED_DESKTOP_FILE_PID to its pid + * 1. Wait for $signal_fd to be closed using read (shell builtin). + * This uses procfs as we need a non-posix shell otherwise. + * 2. Check that we can close the FD. + * ( set -n; exec %1$d<&- ) + * Posix only requires FDs from 0-9. So this may fail in e.g. dash. + * 3. exec the actual command, + * closing the FD if possible, otherwise leaking it into the child. + * It replaces the usual shell script if we may have systemd available. + */ + gchar *wrapper_command = NULL; +#define WRAPPER_COMMAND "export GIO_LAUNCHED_DESKTOP_FILE_PID=$$; read TMP </proc/self/fd/%1$d || true; ( set -n; exec %1$d<&- ) 2>/dev/null && exec \"$@\" %1$d<&- || exec \"$@\"" +#endif const gchar * const wrapper_argv[] = { "/bin/sh", @@ -2879,20 +3251,22 @@ g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, "-c", "export GIO_LAUNCHED_DESKTOP_FILE_PID=$$; exec \"$@\"", "sh", /* argv[0] for sh */ }; + GStrv wrapped_argv = NULL; + GSpawnChildSetupFunc setup = user_setup; + gpointer setup_data = user_setup_data; old_uris = dup_uris; - if (!expand_application_parameters (info, exec_line, &dup_uris, &argc, &argv, error)) + if (!expand_application_parameters (info, exec_line, &dup_uris, &argc, &argv, &error)) goto out; /* Get the subset of URIs we're launching with this process */ - launched_uris = NULL; for (iter = old_uris; iter != NULL && iter != dup_uris; iter = iter->next) launched_uris = g_list_prepend (launched_uris, iter->data); launched_uris = g_list_reverse (launched_uris); if (info->terminal && !prepend_terminal_to_vector (&argc, &argv)) { - g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Unable to find terminal required for application")); goto out; } @@ -2903,7 +3277,6 @@ g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, info->filename, TRUE); - sn_id = NULL; if (launch_context) { GList *launched_files = create_files_for_uris (launched_uris); @@ -2922,50 +3295,97 @@ g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, emit_launch_started (launch_context, info, sn_id); } - /* Wrap the @argv in a command which will set the - * `GIO_LAUNCHED_DESKTOP_FILE_PID` environment variable. We can’t set this - * in @envp along with `GIO_LAUNCHED_DESKTOP_FILE` because we need to know - * the PID of the new forked process. We can’t use setenv() between fork() - * and exec() because we’d rather use posix_spawn() for speed. - * - * `sh` should be available on all the platforms that `GDesktopAppInfo` - * currently supports (since they are all POSIX). If additional platforms - * need to be supported in future, it will probably have to be replaced - * with a wrapper program (grep the GLib git history for - * `gio-launch-desktop` for an example of this which could be - * resurrected). */ wrapped_argv = g_new (char *, argc + G_N_ELEMENTS (wrapper_argv) + 1); + wrapped_argv[argc + G_N_ELEMENTS (wrapper_argv)] = NULL; - for (j = 0; j < G_N_ELEMENTS (wrapper_argv); j++) - wrapped_argv[j] = g_strdup (wrapper_argv[j]); + for (i = 0; i < (int) G_N_ELEMENTS (wrapper_argv); i++) + wrapped_argv[i] = g_strdup (wrapper_argv[i]); for (i = 0; i < argc; i++) - wrapped_argv[i + G_N_ELEMENTS (wrapper_argv)] = g_steal_pointer (&argv[i]); + wrapped_argv[i + G_N_ELEMENTS (wrapper_argv)] = (gchar *) argv[i]; - wrapped_argv[i + G_N_ELEMENTS (wrapper_argv)] = NULL; - g_free (argv); - argv = NULL; +#if defined(__linux__) && !defined(__BIONIC__) + if (task_data) + { + /* Create pipes, if we use a setup func, then set cloexec, + * otherwise our wrapper script will close both sides. */ + if (!g_unix_open_pipe (wrapper_data.pipe, FD_CLOEXEC, NULL)) + { + g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Unable to create pipe for systemd synchronization")); + goto out; + } + + /* Remove CLOEXEC on the read side, as it is passed to the child. */ + fcntl (wrapper_data.pipe[0], F_SETFD, 0); + + if (!(spawn_flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN)) + { + /* In this case, we use a setup function to clear CLOEXEC again. + * We could also wait for the FD and set the environment variable + * there. If we did that, we would need to push the job watching + * into a different thread though. + * + * Note that this does not incur an additional cost because + * %G_SPAWN_LEAVE_DESCRIPTORS_OPEN must be set in order to use + * posix_spawn. + */ + wrapper_data.user_setup = setup; + wrapper_data.user_setup_data = setup_data; + + setup = launch_uris_with_spawn_pass_delayfd; + setup_data = &wrapper_data; + } + + /* Overwrite the command with one that waits for the pipe to close. */ + wrapper_command = g_strdup_printf (WRAPPER_COMMAND, wrapper_data.pipe[0]); + wrapped_argv[G_N_ELEMENTS (wrapper_argv) - 2] = wrapper_command; + } +#endif if (!g_spawn_async_with_fds (info->path, wrapped_argv, envp, spawn_flags, - user_setup, - user_setup_data, + setup, + setup_data, &pid, stdin_fd, stdout_fd, stderr_fd, - error)) + &error)) { +#if defined(__linux__) && !defined(__BIONIC__) + close (wrapper_data.pipe[0]); + close (wrapper_data.pipe[1]); +#endif + if (sn_id) g_app_launch_context_launch_failed (launch_context, sn_id); - g_free (sn_id); - g_list_free (launched_uris); - goto out; } +#if defined(__linux__) && !defined(__BIONIC__) + if (task_data) + { + ScopeCreateData *data; + + /* We close write side asynchronously later on when the dbus call + * to systemd finishes. */ + close (wrapper_data.pipe[0]); + + data = g_new0 (ScopeCreateData, 1); + + data->fd = wrapper_data.pipe[1]; + data->task_data = task_data; + + data->next = task_data->scope_tasks; + task_data->scope_tasks = data; + + create_systemd_scope (info, pid, data); + } +#endif + if (pid_callback != NULL) pid_callback (info, pid, pid_callback_data); @@ -2990,21 +3410,68 @@ g_desktop_app_info_launch_uris_with_spawn (GDesktopAppInfo *info, sn_id, launched_uris); - g_free (sn_id); - g_list_free (launched_uris); - - g_strfreev (wrapped_argv); - wrapped_argv = NULL; + out: + g_clear_pointer (&sn_id, g_free); + g_clear_pointer (&launched_uris, g_list_free); + g_clear_pointer (&wrapped_argv, g_free); + g_clear_pointer (&argv, g_strfreev); +#if defined(__linux__) && !defined(__BIONIC__) + g_clear_pointer (&wrapper_command, g_free); +#endif } - while (dup_uris != NULL); - - completed = TRUE; + while (dup_uris != NULL && !error); - out: - g_strfreev (argv); g_strfreev (envp); - return completed; + + if (session_bus != NULL) /* !!session_bus == !!task_data */ + { + GCancellable *cancellable = task ? g_task_get_cancellable (task) : NULL; +#if defined(__linux__) && !defined(__BIONIC__) + + /* FIXME: The D-Bus message from the notify_desktop_launch() function + * can be still lost even if flush is called later. See: + * https://gitlab.freedesktop.org/dbus/dbus/issues/72 + */ + g_dbus_connection_flush (session_bus, + cancellable, + launch_uris_with_spawn_flush_cb, + task_data); +#else + g_dbus_connection_flush (session_bus, cancellable, NULL, NULL); +#endif + } + + if (task) + { + /* Return error via the task (pre-empting any return through other means). + * Otherwise, return if it will not happen through other means. + */ + if (error) + g_task_return_error (task, g_steal_pointer (&error)); +#if defined(__linux__) && !defined(__BIONIC__) + else if (task_data != NULL) +#else + else +#endif + g_task_return_boolean (task, TRUE); + + g_clear_object (&task); + /* Return value is ignored in this case. */ + return FALSE; + } + else + { + if (error) + { + g_propagate_error (error_out, g_steal_pointer (&error)); + return FALSE; + } + else + { + return TRUE; + } + } } static gchar * @@ -3229,17 +3696,9 @@ g_desktop_app_info_launch_uris_internal (GAppInfo *appinfo, success = g_desktop_app_info_launch_uris_with_spawn (info, session_bus, info->exec, uris, launch_context, spawn_flags, user_setup, user_setup_data, pid_callback, pid_callback_data, - stdin_fd, stdout_fd, stderr_fd, error); + stdin_fd, stdout_fd, stderr_fd, NULL, error); - if (session_bus != NULL) - { - /* This asynchronous flush holds a reference until it completes, - * which ensures that the following unref won't immediately kill - * the connection if we were the initial owner. - */ - g_dbus_connection_flush (session_bus, NULL, NULL, NULL); - g_object_unref (session_bus); - } + g_clear_object (&session_bus); return success; } @@ -3294,18 +3753,6 @@ launch_uris_with_dbus_cb (GObject *object, } static void -launch_uris_flush_cb (GObject *object, - GAsyncResult *result, - gpointer user_data) -{ - GTask *task = G_TASK (user_data); - - g_dbus_connection_flush_finish (G_DBUS_CONNECTION (object), result, NULL); - g_task_return_boolean (task, TRUE); - g_object_unref (task); -} - -static void launch_uris_bus_get_cb (GObject *object, GAsyncResult *result, gpointer user_data) @@ -3313,12 +3760,17 @@ launch_uris_bus_get_cb (GObject *object, GTask *task = G_TASK (user_data); GDesktopAppInfo *info = G_DESKTOP_APP_INFO (g_task_get_source_object (task)); LaunchUrisData *data = g_task_get_task_data (task); + LaunchUrisData *data_copy = NULL; GCancellable *cancellable = g_task_get_cancellable (task); GDBusConnection *session_bus; - GError *error = NULL; session_bus = g_bus_get_finish (result, NULL); + data_copy = g_new0 (LaunchUrisData, 1); + data_copy->appinfo = g_steal_pointer (&data->appinfo); + data_copy->uris = g_steal_pointer (&data->uris); + data_copy->context = g_steal_pointer (&data->context); + if (session_bus && info->app_id) { /* FIXME: The g_document_portal_add_documents() function, which is called @@ -3326,34 +3778,21 @@ launch_uris_bus_get_cb (GObject *object, * uses blocking calls. */ g_desktop_app_info_launch_uris_with_dbus (info, session_bus, - data->uris, data->context, + data_copy->uris, data_copy->context, cancellable, launch_uris_with_dbus_cb, g_steal_pointer (&task)); } else { - /* FIXME: The D-Bus message from the notify_desktop_launch() function - * can be still lost even if flush is called later. See: - * https://gitlab.freedesktop.org/dbus/dbus/issues/72 - */ g_desktop_app_info_launch_uris_with_spawn (info, session_bus, info->exec, - data->uris, data->context, + data_copy->uris, data_copy->context, _SPAWN_FLAGS_DEFAULT, NULL, NULL, NULL, NULL, -1, -1, -1, - &error); - if (error != NULL) - { - g_task_return_error (task, g_steal_pointer (&error)); - g_object_unref (task); - } - else - g_dbus_connection_flush (session_bus, - cancellable, - launch_uris_flush_cb, - g_steal_pointer (&task)); + g_steal_pointer (&task), NULL); } + launch_uris_data_free (data_copy); g_clear_object (&session_bus); } @@ -5169,16 +5608,12 @@ g_desktop_app_info_launch_action (GDesktopAppInfo *info, if (exec_line) g_desktop_app_info_launch_uris_with_spawn (info, session_bus, exec_line, NULL, launch_context, _SPAWN_FLAGS_DEFAULT, NULL, NULL, NULL, NULL, - -1, -1, -1, NULL); + -1, -1, -1, NULL, NULL); g_free (exec_line); } - if (session_bus != NULL) - { - g_dbus_connection_flush (session_bus, NULL, NULL, NULL); - g_object_unref (session_bus); - } + g_clear_object (&session_bus); } /* Epilogue {{{1 */ diff --git a/gio/glocalfilemonitor.c b/gio/glocalfilemonitor.c index 4f85fea52..f408d0707 100644 --- a/gio/glocalfilemonitor.c +++ b/gio/glocalfilemonitor.c @@ -348,7 +348,6 @@ g_file_monitor_source_handle_event (GFileMonitorSource *fms, gint64 event_time) { gboolean interesting = TRUE; - GFileMonitor *instance = NULL; g_assert (!child || is_basename (child)); g_assert (!rename_to || is_basename (rename_to)); @@ -359,13 +358,11 @@ g_file_monitor_source_handle_event (GFileMonitorSource *fms, g_mutex_lock (&fms->lock); - /* monitor is already gone -- don't bother */ - instance = g_weak_ref_get (&fms->instance_ref); - if (instance == NULL) - { - g_mutex_unlock (&fms->lock); - return TRUE; - } + /* NOTE: We process events even if the file monitor has already been disposed. + * The reason is that we must not take a reference to the instance here + * as destroying it from the event handling thread will lead to a + * deadlock when taking the lock in _ih_sub_cancel. + */ switch (event_type) { @@ -452,7 +449,6 @@ g_file_monitor_source_handle_event (GFileMonitorSource *fms, g_file_monitor_source_update_ready_time (fms); g_mutex_unlock (&fms->lock); - g_clear_object (&instance); return interesting; } diff --git a/gio/tests/appinfo-test-actions.desktop b/gio/tests/appinfo-test-actions.desktop index 86e3bcfc0..174586e2f 100644 --- a/gio/tests/appinfo-test-actions.desktop +++ b/gio/tests/appinfo-test-actions.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Type=Application Actions=frob;tweak;twiddle;broken; -Exec=true +Exec=touch %f [Desktop Action frob] Name=Frobnicate diff --git a/gio/tests/desktop-app-info.c b/gio/tests/desktop-app-info.c index 2dc1a2ccd..2bec50052 100644 --- a/gio/tests/desktop-app-info.c +++ b/gio/tests/desktop-app-info.c @@ -26,6 +26,7 @@ #include <stdlib.h> #include <string.h> #include <unistd.h> +#include <fcntl.h> static GAppInfo * create_app_info (const char *name) @@ -793,6 +794,217 @@ test_launch_as_manager (void) g_assert_finalize_object (context); } +static void +pid_callback (GDesktopAppInfo *appinfo, GPid pid, gpointer user_data) +{ + GPid *pids = user_data; + + while (*pids) + pids++; + + *pids = pid; +} + +static void +child_done_cb (GPid pid, gint wait_status, gpointer user_data) +{ + gboolean *done = user_data; + + g_assert_cmpint (wait_status, ==, 0); + + *done = TRUE; +} + +static void +check_systemd_unit (char *escaped_appid, GPid child) +{ + GDBusConnection *bus; + GError *error = NULL; + GVariant *res = NULL; + gchar *unit_name = g_strdup_printf ("app-glib-%s-%d.scope", escaped_appid, child); + gchar *cgroups_file; + gchar *contents = NULL; + gchar *cgroup; + + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + if (!bus) + return; + + /* This purely exists to check whether the environment has (a working) systemd. */ + res = g_dbus_connection_call_sync (bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "GetUnitByPid", + g_variant_new ("(u)", getpid ()), + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, &error); + if (error) + { + gchar *remote = g_dbus_error_get_remote_error (error); + g_assert_cmpstr (remote, !=, "org.freedesktop.systemd1.NoSuchUnit"); + g_free (remote); + } + + /* We have systemd; the PID is already dead (no unit anymore) but we can + * extract the systemd unit name from the cgroup that is still readable. + */ + cgroups_file = g_strdup_printf ("/proc/%d/cgroup", child); + + if (!g_file_get_contents (cgroups_file, &contents, NULL, NULL)) + goto out; + + /* Assume cgroups v2 */ + if (!g_str_has_prefix (contents, "0::")) + goto out; + + g_strchomp (contents); + if (g_str_has_suffix (contents, " (deleted)")) + contents[strlen(contents) - 10] = '\0'; + cgroup = strrchr(contents, '/'); + g_assert_nonnull (cgroup); + cgroup++; + + /* NOTE: cgroup has escaping, but that is not applicable here. */ + g_assert_cmpstr (cgroup, ==, unit_name); + +out: + g_free (cgroups_file); + g_free (contents); + g_free (unit_name); + if (res) + g_variant_unref (res); +} + +/* Test desktop file launching when the spawn code path is used. */ +static void +test_launch_with_spawn (void) +{ + GDesktopAppInfo *appinfo; + GError *error = NULL; + gboolean retval; + GPid pids[3] = { 0, 0, 0 }; + const char *files[2] = { "spawned-1", "spawned-2" }; + gboolean done[2] = { FALSE, FALSE }; + GList *uris = NULL; + gsize i; + + appinfo = g_desktop_app_info_new_from_filename (g_test_get_filename (G_TEST_DIST, "appinfo-test-actions.desktop", NULL)); + g_assert_nonnull (appinfo); + + for (i = 0; i < G_N_ELEMENTS (files); i++) + { + GFile *file = g_file_new_for_path (files[i]); + uris = g_list_append (uris, g_file_get_uri (file)); + g_object_unref (file); + } + + /* Do not reap, so we can read the cgroup after the child is dead. */ + retval = g_desktop_app_info_launch_uris_as_manager (appinfo, uris, NULL, + G_SPAWN_LEAVE_DESCRIPTORS_OPEN | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, NULL, + pid_callback, pids, + &error); + g_assert_no_error (error); + g_assert_true (retval); + g_assert_cmpint (pids[0], >=, 0); + g_assert_cmpint (pids[1], >=, 0); + g_assert_cmpint (pids[2], ==, 0); + + wait_for_file ("spawned-1", "frob", "tweak"); + wait_for_file ("spawned-2", "frob", "tweak"); + + check_systemd_unit ("appinfo\\x2dtest\\x2dactions", pids[0]); + check_systemd_unit ("appinfo\\x2dtest\\x2dactions", pids[1]); + + /* We could also do this blocking, as the child is likely dead already */ + g_child_watch_add (pids[0], child_done_cb, &done[0]); + g_child_watch_add (pids[1], child_done_cb, &done[1]); + while (!done[0] || !done[1]) + g_main_context_iteration (NULL, TRUE); + + g_list_free_full (uris, g_free); + g_object_unref (appinfo); +} + +static void +child_setup_func (gpointer user_data) +{ + GDBusConnection *session_bus = user_data; + int i, inherited_fds = 0; + + /* Double check we didn't leak an FD into the child at this point */ + for (i = 0; i < 1024; i++) + { + int flags = fcntl (i, F_GETFD); + if (flags >= 0 && (flags & FD_CLOEXEC) == 0) + inherited_fds++; + } + if (inherited_fds != 3 + !!session_bus) + { + const char *msg = "Unexpected number of open FDs (should be exactly stdin/stdout/stderr plus synchronization FD if a session bus exists)\n"; + write (2, msg, strlen(msg)); + exit (1); + } + + /* Touch "child-setup", so that we know this function was called */ + g_file_set_contents ("child-setup", "", -1, NULL); +} + +/* Test desktop file launching when the fork code path is used. */ +static void +test_launch_with_fork (void) +{ + GDesktopAppInfo *appinfo; + GError *error = NULL; + gboolean retval; + GPid pids[3] = { 0, 0, 0 }; + const char *files[2] = { "spawned-1", "spawned-2" }; + gboolean done[2] = { FALSE, FALSE }; + GList *uris = NULL; + gsize i; + + appinfo = g_desktop_app_info_new_from_filename (g_test_get_filename (G_TEST_DIST, "appinfo-test-actions.desktop", NULL)); + g_assert_nonnull (appinfo); + + for (i = 0; i < G_N_ELEMENTS (files); i++) + { + GFile *file = g_file_new_for_path (files[i]); + uris = g_list_append (uris, g_file_get_uri (file)); + g_object_unref (file); + } + + /* Do not reap, so we can read the cgroup after the child is dead. */ + retval = g_desktop_app_info_launch_uris_as_manager (appinfo, uris, NULL, + G_SPAWN_DO_NOT_REAP_CHILD, + child_setup_func, g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL), + pid_callback, pids, + &error); + g_assert_no_error (error); + g_assert_true (retval); + g_assert_cmpint (pids[0], >=, 0); + g_assert_cmpint (pids[1], >=, 0); + g_assert_cmpint (pids[2], ==, 0); + + wait_for_file ("spawned-1", "frob", "tweak"); + wait_for_file ("spawned-2", "frob", "tweak"); + wait_for_file ("child-setup", "spawned-1", "spawned-2"); + + check_systemd_unit ("appinfo\\x2dtest\\x2dactions", pids[0]); + check_systemd_unit ("appinfo\\x2dtest\\x2dactions", pids[1]); + + /* We could also do this blocking, as the child is likely dead already */ + g_child_watch_add (pids[0], child_done_cb, &done[0]); + g_child_watch_add (pids[1], child_done_cb, &done[1]); + while (!done[0] || !done[1]) + g_main_context_iteration (NULL, TRUE); + + g_list_free_full (uris, g_free); + g_object_unref (appinfo); +} + /* Test if Desktop-File Id is correctly formed */ static void test_id (void) @@ -826,6 +1038,8 @@ main (int argc, g_test_add_func ("/desktop-app-info/implements", test_implements); g_test_add_func ("/desktop-app-info/show-in", test_show_in); g_test_add_func ("/desktop-app-info/launch-as-manager", test_launch_as_manager); + g_test_add_func ("/desktop-app-info/launch-with-spawn", test_launch_with_spawn); + g_test_add_func ("/desktop-app-info/launch-with-fork", test_launch_with_fork); g_test_add_func ("/desktop-app-info/id", test_id); return g_test_run (); |