Merge branch 'g-win32-app-info-launch-uris-async' into 'main'

GWin32AppInfo: Implement launch_uris_async

Closes gtk#4400

See merge request GNOME/glib!2760
This commit is contained in:
Luca Bacci 2022-08-27 14:11:10 +00:00
commit e1e9bc9429
3 changed files with 461 additions and 80 deletions

View File

@ -651,7 +651,9 @@ g_app_info_supports_files (GAppInfo *appinfo)
* Launches the application. This passes the @uris to the launched application
* as arguments, using the optional @context to get information
* about the details of the launcher (like what screen it is on).
* On error, @error will be set accordingly.
* On error, @error will be set accordingly. If the application only supports
* one URI per invocation as part of their command-line, multiple instances
* of the application will be spawned.
*
* To launch the application without arguments pass a %NULL @uris list.
*
@ -1379,6 +1381,10 @@ g_app_launch_context_class_init (GAppLaunchContextClass *klass)
* fails. The startup notification id is provided, so that the launcher
* can cancel the startup notification.
*
* Because a launch operation may involve spawning multiple instances of the
* target application, you should expect this signal to be emitted multiple
* times, one for each spawned instance.
*
* Since: 2.36
*/
signals[LAUNCH_FAILED] = g_signal_new (I_("launch-failed"),
@ -1409,6 +1415,10 @@ g_app_launch_context_class_init (GAppLaunchContextClass *klass)
* It is guaranteed that this signal is followed by either a #GAppLaunchContext::launched or
* #GAppLaunchContext::launch-failed signal.
*
* Because a launch operation may involve spawning multiple instances of the
* target application, you should expect this signal to be emitted multiple
* times, one for each spawned instance.
*
* Since: 2.72
*/
signals[LAUNCH_STARTED] = g_signal_new (I_("launch-started"),
@ -1430,7 +1440,13 @@ g_app_launch_context_class_init (GAppLaunchContextClass *klass)
* @platform_data: additional platform-specific data for this launch
*
* The #GAppLaunchContext::launched signal is emitted when a #GAppInfo is successfully
* launched. The @platform_data is an GVariant dictionary mapping
* launched.
*
* Because a launch operation may involve spawning multiple instances of the
* target application, you should expect this signal to be emitted multiple
* times, one time for each spawned instance.
*
* The @platform_data is an GVariant dictionary mapping
* strings to variants (ie `a{sv}`), which contains additional,
* platform-specific data about this launch. On UNIX, at least the
* `pid` and `startup-notification-id` keys will be present.

View File

@ -3261,9 +3261,8 @@ g_desktop_app_info_launch_uris (GAppInfo *appinfo,
typedef struct
{
GAppInfo *appinfo;
GList *uris;
GAppLaunchContext *context;
GList *uris; /* (element-type utf8) (owned) (nullable) */
GAppLaunchContext *context; /* (owned) (nullable) */
} LaunchUrisData;
static void
@ -3280,13 +3279,13 @@ launch_uris_with_dbus_cb (GObject *object,
gpointer user_data)
{
GTask *task = G_TASK (user_data);
GError *error = NULL;
GError *local_error = NULL;
g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
if (error != NULL)
g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &local_error);
if (local_error != NULL)
{
g_dbus_error_strip_remote_error (error);
g_task_return_error (task, g_steal_pointer (&error));
g_dbus_error_strip_remote_error (local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
}
else
g_task_return_boolean (task, TRUE);
@ -3316,7 +3315,7 @@ launch_uris_bus_get_cb (GObject *object,
LaunchUrisData *data = g_task_get_task_data (task);
GCancellable *cancellable = g_task_get_cancellable (task);
GDBusConnection *session_bus;
GError *error = NULL;
GError *local_error = NULL;
session_bus = g_bus_get_finish (result, NULL);
@ -3342,10 +3341,10 @@ launch_uris_bus_get_cb (GObject *object,
data->uris, data->context,
_SPAWN_FLAGS_DEFAULT, NULL,
NULL, NULL, NULL, -1, -1, -1,
&error);
if (error != NULL)
&local_error);
if (local_error != NULL)
{
g_task_return_error (task, g_steal_pointer (&error));
g_task_return_error (task, g_steal_pointer (&local_error));
g_object_unref (task);
}
else if (session_bus)
@ -3379,7 +3378,7 @@ g_desktop_app_info_launch_uris_async (GAppInfo *appinfo,
data = g_new0 (LaunchUrisData, 1);
data->uris = g_list_copy_deep (uris, (GCopyFunc) g_strdup, NULL);
data->context = (context != NULL) ? g_object_ref (context) : NULL;
g_set_object (&data->context, context);
g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) launch_uris_data_free);
g_bus_get (G_BUS_TYPE_SESSION, cancellable, launch_uris_bus_get_cb, task);
@ -3929,7 +3928,7 @@ run_update_command (char *command,
NULL,
};
GPid pid = 0;
GError *error = NULL;
GError *local_error = NULL;
argv[0] = command;
argv[1] = g_build_filename (g_get_user_data_dir (), subdir, NULL);
@ -3942,7 +3941,7 @@ run_update_command (char *command,
G_SPAWN_DO_NOT_REAP_CHILD,
NULL, NULL, /* No setup function */
&pid,
&error))
&local_error))
g_child_watch_add (pid, update_program_done, NULL);
else
{
@ -3952,8 +3951,8 @@ run_update_command (char *command,
* dialog at this point, so we just do a g_warning to give the user a
* chance of debugging it.
*/
g_warning ("%s", error->message);
g_error_free (error);
g_warning ("%s", local_error->message);
g_error_free (local_error);
}
g_free (argv[1]);

View File

@ -4685,43 +4685,334 @@ get_appath_for_exe (const gchar *exe_basename)
return appath;
}
/* GDesktopAppInfo::launch_uris_async emits all GAppLaunchContext's signals
* on the main thread.
*
* We do the same: when g_win32_app_info_launch_uris_impl has a non-NULL
* from_task argument we schedule the signal emissions on the main loop,
* taking care not to emit signals after the task itself is completed
* (see g_task_get_completed).
*/
typedef struct {
GAppLaunchContext *context; /* (owned) */
GWin32AppInfo *info; /* (owned) */
} EmitLaunchStartedData;
static void
emit_launch_started_data_free (EmitLaunchStartedData *data)
{
g_clear_object (&data->context);
g_clear_object (&data->info);
g_free (data);
}
static gboolean
emit_launch_started_cb (EmitLaunchStartedData *data)
{
g_signal_emit_by_name (data->context, "launch-started", data->info, NULL);
return G_SOURCE_REMOVE;
}
static void
emit_launch_started (GAppLaunchContext *context,
GWin32AppInfo *info,
GTask *from_task)
{
if (!context || !info)
return;
if (!from_task)
g_signal_emit_by_name (context, "launch-started", info, NULL);
else
{
EmitLaunchStartedData *data;
data = g_new (EmitLaunchStartedData, 1);
data->context = g_object_ref (context);
data->info = g_object_ref (info);
g_main_context_invoke_full (g_task_get_context (from_task),
g_task_get_priority (from_task),
G_SOURCE_FUNC (emit_launch_started_cb),
g_steal_pointer (&data),
(GDestroyNotify) emit_launch_started_data_free);
}
}
typedef struct {
GAppLaunchContext *context; /* (owned) */
GWin32AppInfo *info; /* (owned) */
GPid pid; /* (owned) */
} EmitLaunchedData;
static void
emit_launched_data_free (EmitLaunchedData *data)
{
g_clear_object (&data->context);
g_clear_object (&data->info);
g_spawn_close_pid (data->pid);
g_free (data);
}
static GVariant*
make_platform_data (GPid pid)
{
GVariantBuilder builder;
g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
/* pid handles are never bigger than 2^24 as per
* https://docs.microsoft.com/en-us/windows/win32/sysinfo/kernel-objects,
* so truncating to `int32` is valid.
* The gsize cast is to silence a compiler warning
* about conversion from pointer to integer of
* different size. */
g_variant_builder_add (&builder, "{sv}", "pid", g_variant_new_int32 ((gsize) pid));
return g_variant_ref_sink (g_variant_builder_end (&builder));
}
static gboolean
emit_launched_cb (EmitLaunchedData *data)
{
GVariant *platform_data = make_platform_data (data->pid);
g_signal_emit_by_name (data->context, "launched", data->info, platform_data);
g_variant_unref (platform_data);
return G_SOURCE_REMOVE;
}
static void
emit_launched (GAppLaunchContext *context,
GWin32AppInfo *info,
GPid *pid,
GTask *from_task)
{
if (!context || !info)
return;
if (!from_task)
{
GVariant *platform_data = make_platform_data (*pid);
g_signal_emit_by_name (context, "launched", info, platform_data);
g_variant_unref (platform_data);
}
else
{
EmitLaunchedData *data;
data = g_new (EmitLaunchedData, 1);
data->context = g_object_ref (context);
data->info = g_object_ref (info);
data->pid = *pid;
g_main_context_invoke_full (g_task_get_context (from_task),
g_task_get_priority (from_task),
G_SOURCE_FUNC (emit_launched_cb),
g_steal_pointer (&data),
(GDestroyNotify) emit_launched_data_free);
}
*pid = 0;
}
typedef struct {
GAppLaunchContext *context; /* (owned) */
GWin32AppInfo *info; /* (owned) */
} EmitLaunchFailedData;
static void
emit_launch_failed_data_free (EmitLaunchFailedData *data)
{
g_clear_object (&data->context);
g_clear_object (&data->info);
g_free (data);
}
static gboolean
emit_launch_failed_cb (EmitLaunchFailedData *data)
{
g_signal_emit_by_name (data->context, "launch-failed", data->info, NULL);
return G_SOURCE_REMOVE;
}
static void
emit_launch_failed (GAppLaunchContext *context,
GWin32AppInfo *info,
GTask *from_task)
{
if (!context || !info)
return;
if (!from_task)
g_signal_emit_by_name (context, "launch-failed", info, NULL);
else
{
EmitLaunchFailedData *data;
data = g_new (EmitLaunchFailedData, 1);
data->context = g_object_ref (context);
data->info = g_object_ref (info);
g_main_context_invoke_full (g_task_get_context (from_task),
g_task_get_priority (from_task),
G_SOURCE_FUNC (emit_launch_failed_cb),
g_steal_pointer (&data),
(GDestroyNotify) emit_launch_failed_data_free);
}
}
static gboolean
g_win32_app_info_launch_uwp_internal (GWin32AppInfo *info,
gboolean for_files,
IShellItemArray *items,
GWin32AppInfoShellVerb *shverb,
GAppLaunchContext *launch_context,
GTask *from_task,
GError **error)
{
DWORD pid;
IApplicationActivationManager* paam = NULL;
gboolean result = TRUE;
gboolean com_initialized = FALSE;
gboolean result = FALSE;
DWORD process_id = 0;
HRESULT hr;
const wchar_t *app_canonical_name = (const wchar_t *) info->app->canonical_name;
hr = CoCreateInstance (&CLSID_ApplicationActivationManager, NULL, CLSCTX_INPROC_SERVER, &IID_IApplicationActivationManager, (void **) &paam);
/* ApplicationActivationManager threading model is both,
* prefer the multithreaded apartment type, as we don't
* need anything of the STA here. */
hr = CoInitializeEx (NULL, COINIT_MULTITHREADED);
if (SUCCEEDED (hr))
com_initialized = TRUE;
else if (hr != RPC_E_CHANGED_MODE)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Failed to initialize the COM support library for the thread: 0x%lx", hr);
goto cleanup;
}
/* It's best to instantiate ApplicationActivationManager out-of-proc,
* as documented on MSDN:
*
* An IApplicationActivationManager object creates a thread in its
* host process to serve any activated event arguments objects
* (LaunchActivatedEventArgs, FileActivatedEventArgs, and Protocol-
* ActivatedEventArgs) that are passed to the app. If the calling
* process is long-lived, you can create this object in-proc,
* based on the assumption that the event arguments will exist long
* enough for the target app to use them.
* However, if the calling process is spawned only to launch the
* target app, it should create the IApplicationActivationManager
* object out-of-process, by using CLSCTX_LOCAL_SERVER. This causes
* the object to be created in a Dllhost instance that automatically
* manages the object's lifetime based on outstanding references to
* the activated event argument objects.
*/
hr = CoCreateInstance (&CLSID_ApplicationActivationManager, NULL,
CLSCTX_LOCAL_SERVER,
&IID_IApplicationActivationManager, (void **) &paam);
if (FAILED (hr))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Failed to create ApplicationActivationManager: 0x%lx", hr);
return FALSE;
goto cleanup;
}
emit_launch_started (launch_context, info, from_task);
/* The Activate methods return a process identifier (PID), so we should consider
* those methods as potentially blocking */
if (items == NULL)
hr = IApplicationActivationManager_ActivateApplication (paam, (const wchar_t *) info->app->canonical_name, NULL, AO_NONE, &pid);
hr = IApplicationActivationManager_ActivateApplication (paam,
app_canonical_name,
NULL, AO_NONE,
&process_id);
else if (for_files)
hr = IApplicationActivationManager_ActivateForFile (paam, (const wchar_t *) info->app->canonical_name, items, shverb->verb_name, &pid);
hr = IApplicationActivationManager_ActivateForFile (paam,
app_canonical_name,
items, shverb->verb_name,
&process_id);
else
hr = IApplicationActivationManager_ActivateForProtocol (paam, (const wchar_t *) info->app->canonical_name, items, &pid);
hr = IApplicationActivationManager_ActivateForProtocol (paam,
app_canonical_name,
items,
&process_id);
if (FAILED (hr))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"The app %s failed to launch: 0x%lx",
g_win32_appinfo_application_get_some_name (info->app), hr);
result = FALSE;
emit_launch_failed (launch_context, info, from_task);
goto cleanup;
}
else if (launch_context)
{
DWORD access_rights = 0;
HANDLE process_handle = NULL;
/* Unfortunately, there's a race condition here.
* ApplicationActivationManager methods return a process ID, but it
* keeps no open HANDLE to the spawned process internally (tested
* on Windows 10 21H2). So we cannot guarantee that by the time
* OpenProcess is called, process ID still referes to the spawned
* process. Anyway hitting such case is extremely unlikely.
*
* https://docs.microsoft.com/en-us/answers/questions/942879/
* iapplicationactivationmanager-race-condition.html
*
* Maybe we could make use of the WinRT APIs to activate UWP apps,
* instead? */
/* As documented on MSDN, the handle returned by CreateProcess has
* PROCESS_ALL_ACCESS rights. First try passing PROCESS_ALL_ACCESS
* to have the same access rights as the non-UWP code-path; should
* that fail with ERROR_ACCESS_DENIED error code, retry using safe
* access rights */
access_rights = PROCESS_ALL_ACCESS;
process_handle = OpenProcess (access_rights, FALSE, process_id);
if (!process_handle && GetLastError () == ERROR_ACCESS_DENIED)
{
DWORD access_rights = PROCESS_QUERY_LIMITED_INFORMATION |
SYNCHRONIZE;
process_handle = OpenProcess (access_rights, FALSE, process_id);
}
if (!process_handle)
{
g_warning ("OpenProcess failed with error code %" G_GUINT32_FORMAT,
(guint32) GetLastError ());
}
/* Emit the launched signal regardless if we have the process
* HANDLE or NULL */
emit_launched (launch_context, info, (GPid*) &process_handle, from_task);
g_spawn_close_pid ((GPid) process_handle);
}
result = TRUE;
cleanup:
if (paam)
{
IApplicationActivationManager_Release (paam);
paam = NULL;
}
if (com_initialized)
{
CoUninitialize ();
com_initialized = FALSE;
}
return result;
}
@ -4734,6 +5025,7 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
IShellItemArray *items, /* UWP only */
GAppLaunchContext *launch_context,
GSpawnFlags spawn_flags,
GTask *from_task,
GError **error)
{
gboolean completed = FALSE;
@ -4742,6 +5034,7 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
const gchar *command;
gchar *apppath;
GWin32AppInfoShellVerb *shverb;
GPid pid = NULL;
g_return_val_if_fail (info != NULL, FALSE);
g_return_val_if_fail (info->app != NULL, FALSE);
@ -4776,6 +5069,8 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
for_files,
items,
shverb,
launch_context,
from_task,
error);
if (launch_context)
@ -4832,7 +5127,8 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
do
{
GPid pid;
if (from_task && g_task_return_error_if_cancelled (from_task))
goto out;
if (!expand_application_parameters (info,
command,
@ -4842,6 +5138,8 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
error))
goto out;
emit_launch_started (launch_context, info, from_task);
if (!g_spawn_async (NULL,
argv,
envp,
@ -4851,28 +5149,16 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
NULL,
&pid,
error))
goto out;
if (launch_context != NULL)
{
GVariantBuilder builder;
GVariant *platform_data;
emit_launch_failed (launch_context, info, from_task);
g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
/* pid handles are never bigger than 2^24 as per
* https://docs.microsoft.com/en-us/windows/win32/sysinfo/kernel-objects,
* so truncating to `int32` is valid.
* The gsize cast is to silence a compiler warning
* about conversion from pointer to integer of
* different size. */
g_variant_builder_add (&builder, "{sv}", "pid", g_variant_new_int32 ((gsize) pid));
platform_data = g_variant_ref_sink (g_variant_builder_end (&builder));
g_signal_emit_by_name (launch_context, "launched", info, platform_data);
g_variant_unref (platform_data);
goto out;
}
else if (launch_context)
emit_launched (launch_context, info, &pid, from_task);
g_spawn_close_pid (pid);
pid = NULL;
g_strfreev (argv);
argv = NULL;
}
@ -4880,7 +5166,8 @@ g_win32_app_info_launch_internal (GWin32AppInfo *info,
completed = TRUE;
out:
out:
g_spawn_close_pid (pid);
g_strfreev (argv);
g_strfreev (envp);
@ -5028,9 +5315,10 @@ make_item_array (gboolean for_files,
static gboolean
g_win32_app_info_launch_uris (GAppInfo *appinfo,
g_win32_app_info_launch_uris_impl (GAppInfo *appinfo,
GList *uris,
GAppLaunchContext *launch_context,
GTask *from_task,
GError **error)
{
gboolean res = FALSE;
@ -5049,7 +5337,7 @@ g_win32_app_info_launch_uris (GAppInfo *appinfo,
return res;
}
res = g_win32_app_info_launch_internal (info, NULL, FALSE, items, launch_context, 0, error);
res = g_win32_app_info_launch_internal (info, NULL, FALSE, items, launch_context, 0, from_task, error);
if (items != NULL)
IShellItemArray_Release (items);
@ -5090,6 +5378,7 @@ g_win32_app_info_launch_uris (GAppInfo *appinfo,
NULL,
launch_context,
G_SPAWN_SEARCH_PATH,
from_task,
error);
g_list_free_full (objs, free_file_or_uri);
@ -5097,6 +5386,80 @@ g_win32_app_info_launch_uris (GAppInfo *appinfo,
return res;
}
static gboolean
g_win32_app_info_launch_uris (GAppInfo *appinfo,
GList *uris,
GAppLaunchContext *launch_context,
GError **error)
{
return g_win32_app_info_launch_uris_impl (appinfo, uris, launch_context, NULL, error);
}
typedef struct
{
GList *uris; /* (element-type utf8) (owned) (nullable) */
GAppLaunchContext *context; /* (owned) (nullable) */
} LaunchUrisData;
static void
launch_uris_data_free (LaunchUrisData *data)
{
g_clear_object (&data->context);
g_list_free_full (data->uris, g_free);
g_free (data);
}
static void
launch_uris_async_thread (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GAppInfo *appinfo = G_APP_INFO (source_object);
LaunchUrisData *data = task_data;
GError *local_error = NULL;
gboolean succeeded;
succeeded = g_win32_app_info_launch_uris_impl (appinfo, data->uris, data->context, task, &local_error);
if (succeeded)
g_task_return_boolean (task, TRUE);
else if (!g_task_had_error (task))
g_task_return_error (task, g_steal_pointer (&local_error));
}
static void
g_win32_app_info_launch_uris_async (GAppInfo *appinfo,
GList *uris,
GAppLaunchContext *context,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
LaunchUrisData *data;
task = g_task_new (appinfo, cancellable, callback, user_data);
g_task_set_source_tag (task, g_win32_app_info_launch_uris_async);
data = g_new0 (LaunchUrisData, 1);
data->uris = g_list_copy_deep (uris, (GCopyFunc) g_strdup, NULL);
g_set_object (&data->context, context);
g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) launch_uris_data_free);
g_task_run_in_thread (task, launch_uris_async_thread);
g_object_unref (task);
}
static gboolean
g_win32_app_info_launch_uris_finish (GAppInfo *appinfo,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (g_task_is_valid (result, appinfo), FALSE);
return g_task_propagate_boolean (G_TASK (result), error);
}
static gboolean
g_win32_app_info_should_show (GAppInfo *appinfo)
{
@ -5128,7 +5491,7 @@ g_win32_app_info_launch (GAppInfo *appinfo,
return res;
}
res = g_win32_app_info_launch_internal (info, NULL, TRUE, items, launch_context, 0, error);
res = g_win32_app_info_launch_internal (info, NULL, TRUE, items, launch_context, 0, NULL, error);
if (items != NULL)
IShellItemArray_Release (items);
@ -5160,6 +5523,7 @@ g_win32_app_info_launch (GAppInfo *appinfo,
NULL,
launch_context,
G_SPAWN_SEARCH_PATH,
NULL,
error);
g_list_free_full (objs, free_file_or_uri);
@ -5242,6 +5606,8 @@ g_win32_app_info_iface_init (GAppInfoIface *iface)
iface->supports_uris = g_win32_app_info_supports_uris;
iface->supports_files = g_win32_app_info_supports_files;
iface->launch_uris = g_win32_app_info_launch_uris;
iface->launch_uris_async = g_win32_app_info_launch_uris_async;
iface->launch_uris_finish = g_win32_app_info_launch_uris_finish;
iface->should_show = g_win32_app_info_should_show;
/* iface->set_as_default_for_type = g_win32_app_info_set_as_default_for_type;*/
/* iface->set_as_default_for_extension = g_win32_app_info_set_as_default_for_extension;*/