From 3a4b18f9038f316c617e229123b54ea9885ba1dc Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 18 Aug 2018 15:35:33 -0400 Subject: [PATCH 1/2] GApplication: Add a way to replace a unique instance While uniqueness is great, sometimes you want to restart a newer version of the same app. These two flags make that possible. We also add a ::name-lost signal, that is emitted when it happens. The default handler for this signal just calls g_application_quit(), but applications may want to connect and do cleanup or state-saving here. --- gio/gapplication.c | 46 ++++++++++++++++++++++++++ gio/gapplication.h | 3 +- gio/gapplicationimpl-dbus.c | 66 ++++++++++++++++++++++++++++++++++--- gio/gioenums.h | 9 ++++- 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/gio/gapplication.c b/gio/gapplication.c index 5a2f5adf7..2d2ab48e3 100644 --- a/gio/gapplication.c +++ b/gio/gapplication.c @@ -212,6 +212,7 @@ * the @dbus_register vfunc. Since: 2.34 * @handle_local_options: invoked locally after the parsing of the commandline * options has occurred. Since: 2.40 + * @name_lost: invoked when another instance is taking over the name. Since: 2.60 * * Virtual function table for #GApplication. * @@ -277,6 +278,7 @@ enum SIGNAL_ACTION, SIGNAL_COMMAND_LINE, SIGNAL_HANDLE_LOCAL_OPTIONS, + SIGNAL_NAME_LOST, NR_SIGNALS }; @@ -476,6 +478,7 @@ g_application_parse_command_line (GApplication *application, { gboolean become_service = FALSE; gchar *app_id = NULL; + gboolean replace = FALSE; GVariantDict *dict = NULL; GOptionContext *context; GOptionGroup *gapplication_group; @@ -557,6 +560,18 @@ g_application_parse_command_line (GApplication *application, g_option_group_add_entries (gapplication_group, entries); } + /* Allow replacing if the application allows it */ + if (application->priv->flags & G_APPLICATION_ALLOW_REPLACEMENT) + { + GOptionEntry entries[] = { + { "gapplication-replace", '\0', 0, G_OPTION_ARG_NONE, &replace, + N_("Replace the running instance") }, + { NULL } + }; + + g_option_group_add_entries (gapplication_group, entries); + } + /* Now we parse... */ if (!g_option_context_parse_strv (context, arguments, error)) goto out; @@ -569,6 +584,10 @@ g_application_parse_command_line (GApplication *application, if (app_id) g_application_set_application_id (application, app_id); + /* Check for --gapplication-replace */ + if (replace) + application->priv->flags |= G_APPLICATION_REPLACE; + dict = g_variant_dict_new (NULL); if (application->priv->packed_options) { @@ -1177,6 +1196,13 @@ g_application_real_dbus_unregister (GApplication *application, { } +static gboolean +g_application_real_name_lost (GApplication *application) +{ + g_application_quit (application); + return TRUE; +} + /* GObject implementation stuff {{{1 */ static void g_application_set_property (GObject *object, @@ -1434,6 +1460,7 @@ g_application_class_init (GApplicationClass *class) class->add_platform_data = g_application_real_add_platform_data; class->dbus_register = g_application_real_dbus_register; class->dbus_unregister = g_application_real_dbus_unregister; + class->name_lost = g_application_real_name_lost; g_object_class_install_property (object_class, PROP_APPLICATION_ID, g_param_spec_string ("application-id", @@ -1628,6 +1655,25 @@ g_application_class_init (GApplicationClass *class) g_application_handle_local_options_accumulator, NULL, NULL, G_TYPE_INT, 1, G_TYPE_VARIANT_DICT); + /** + * GApplication::name-lost: + * @application: the application + * + * The ::name-lost signal is emitted only on the registered primary instance + * when a new instance has taken over. This can only happen if the application + * is using the %G_APPLICATION_ALLOW_REPLACEMENT flag. + * + * The default handler for this signal calls g_application_quit(). + * + * Returns: %TRUE if the signal has been handled + * + * Since: 2.60 + */ + g_application_signals[SIGNAL_NAME_LOST] = + g_signal_new (I_("name-lost"), G_TYPE_APPLICATION, G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GApplicationClass, name_lost), + g_signal_accumulator_true_handled, NULL, NULL, + G_TYPE_BOOLEAN, 0); } /* Application ID validity {{{1 */ diff --git a/gio/gapplication.h b/gio/gapplication.h index cdb93655a..adc32ed44 100644 --- a/gio/gapplication.h +++ b/gio/gapplication.h @@ -112,9 +112,10 @@ struct _GApplicationClass const gchar *object_path); gint (* handle_local_options)(GApplication *application, GVariantDict *options); + gboolean (* name_lost) (GApplication *application); /*< private >*/ - gpointer padding[8]; + gpointer padding[7]; }; GLIB_AVAILABLE_IN_ALL diff --git a/gio/gapplicationimpl-dbus.c b/gio/gapplicationimpl-dbus.c index f9e5e710d..76d67eec8 100644 --- a/gio/gapplicationimpl-dbus.c +++ b/gio/gapplicationimpl-dbus.c @@ -111,6 +111,7 @@ struct _GApplicationImpl GDBusConnection *session_bus; GActionGroup *exported_actions; const gchar *bus_name; + guint name_lost_signal; gchar *object_path; guint object_id; @@ -327,6 +328,25 @@ application_path_from_appid (const gchar *appid) return appid_path; } +static void g_application_impl_stop_primary (GApplicationImpl *impl); + +static void +name_lost (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GApplicationImpl *impl = user_data; + gboolean handled; + + impl->primary = FALSE; + g_application_impl_stop_primary (impl); + g_signal_emit_by_name (impl->app, "name-lost", &handled); +} + /* Attempt to become the primary instance. * * Returns %TRUE if everything went OK, regardless of if we became the @@ -347,6 +367,8 @@ g_application_impl_attempt_primary (GApplicationImpl *impl, NULL /* set_property */ }; GApplicationClass *app_class = G_APPLICATION_GET_CLASS (impl->app); + GBusNameOwnerFlags name_owner_flags; + GApplicationFlags app_flags; GVariant *reply; guint32 rval; @@ -426,11 +448,33 @@ g_application_impl_attempt_primary (GApplicationImpl *impl, * the well-known name and fall back to remote mode (!is_primary) * in the case that we can't do that. */ - reply = g_dbus_connection_call_sync (impl->session_bus, "org.freedesktop.DBus", "/org/freedesktop/DBus", - "org.freedesktop.DBus", "RequestName", - g_variant_new ("(su)", - impl->bus_name, - G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE), + name_owner_flags = G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE; + app_flags = g_application_get_flags (impl->app); + + if (app_flags & G_APPLICATION_ALLOW_REPLACEMENT) + { + impl->name_lost_signal = g_dbus_connection_signal_subscribe (impl->session_bus, + "org.freedesktop.DBus", + "org.freedesktop.DBus", + "NameLost", + "/org/freedesktop/DBus", + impl->bus_name, + G_DBUS_SIGNAL_FLAGS_NONE, + name_lost, + impl, + NULL); + + name_owner_flags |= G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; + } + if (app_flags & G_APPLICATION_REPLACE) + name_owner_flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE; + + reply = g_dbus_connection_call_sync (impl->session_bus, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "RequestName", + g_variant_new ("(su)", impl->bus_name, name_owner_flags), G_VARIANT_TYPE ("(u)"), 0, -1, cancellable, error); @@ -443,6 +487,12 @@ g_application_impl_attempt_primary (GApplicationImpl *impl, /* DBUS_REQUEST_NAME_REPLY_EXISTS: 3 */ impl->primary = (rval != 3); + if (!impl->primary && impl->name_lost_signal) + { + g_dbus_connection_signal_unsubscribe (impl->session_bus, impl->name_lost_signal); + impl->name_lost_signal = 0; + } + return TRUE; } @@ -485,6 +535,12 @@ g_application_impl_stop_primary (GApplicationImpl *impl) impl->actions_id = 0; } + if (impl->name_lost_signal) + { + g_dbus_connection_signal_unsubscribe (impl->session_bus, impl->name_lost_signal); + impl->name_lost_signal = 0; + } + if (impl->primary && impl->bus_name) { g_dbus_connection_call (impl->session_bus, "org.freedesktop.DBus", diff --git a/gio/gioenums.h b/gio/gioenums.h index a01532cfd..d6d1e59f1 100644 --- a/gio/gioenums.h +++ b/gio/gioenums.h @@ -1474,6 +1474,11 @@ typedef enum * @G_APPLICATION_CAN_OVERRIDE_APP_ID: Allow users to override the * application ID from the command line with `--gapplication-app-id`. * Since: 2.48 + * @G_APPLICATION_ALLOW_REPLACEMENT: Allow another instance to take over + * the bus name. Since: 2.60 + * @G_APPLICATION_REPLACE: Take over from another instance. This flag is + * usually set by passing `--gapplication-replace` on the commandline. + * Since: 2.60 * * Flags used to define the behaviour of a #GApplication. * @@ -1491,7 +1496,9 @@ typedef enum G_APPLICATION_NON_UNIQUE = (1 << 5), - G_APPLICATION_CAN_OVERRIDE_APP_ID = (1 << 6) + G_APPLICATION_CAN_OVERRIDE_APP_ID = (1 << 6), + G_APPLICATION_ALLOW_REPLACEMENT = (1 << 7), + G_APPLICATION_REPLACE = (1 << 8) } GApplicationFlags; /** From 136f83eefdf92aec3aae596ddf402bc175d583aa Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Fri, 24 Aug 2018 20:02:34 +0000 Subject: [PATCH 2/2] Add tests for --gapplication-replace Test the GApplication replacement functionality. --- gio/tests/gapplication.c | 155 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/gio/tests/gapplication.c b/gio/tests/gapplication.c index f5491ec9e..ec57977a5 100644 --- a/gio/tests/gapplication.c +++ b/gio/tests/gapplication.c @@ -969,6 +969,156 @@ test_api (void) g_object_unref (app); } +/* Check that G_APPLICATION_ALLOW_REPLACEMENT works. To do so, we launch + * a GApplication in this process that allows replacement, and then + * launch a subprocess with --gapplication-replace. We have to do our + * own async version of g_test_trap_subprocess() here since we need + * the main process to keep spinning its mainloop. + */ + +static gboolean +name_was_lost (GApplication *app, + gboolean *called) +{ + *called = TRUE; + g_application_quit (app); + return TRUE; +} + +static void +startup_in_subprocess (GApplication *app, + gboolean *called) +{ + *called = TRUE; +} + +typedef struct +{ + gboolean allow_replacement; + GSubprocess *subprocess; +} TestReplaceData; + +static void +startup_cb (GApplication *app, + TestReplaceData *data) +{ + const char *argv[] = { NULL, "--verbose", "--quiet", "-p", NULL, "--GTestSubprocess", NULL }; + GSubprocessLauncher *launcher; + GError *local_error = NULL; + + g_application_hold (app); + + argv[0] = g_get_prgname (); + + if (data->allow_replacement) + argv[4] = "/gapplication/replace"; + else + argv[4] = "/gapplication/no-replace"; + + /* Now that we are the primary instance, launch our replacement. + * We inherit the environment to share the test session bus. + */ + g_test_message ("launching subprocess"); + + launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE); + g_subprocess_launcher_set_environ (launcher, NULL); + data->subprocess = g_subprocess_launcher_spawnv (launcher, argv, &local_error); + g_assert_no_error (local_error); + g_object_unref (launcher); + + if (!data->allow_replacement) + { + /* make sure we exit after a bit, if the subprocess is not replacing us */ + g_application_set_inactivity_timeout (app, 500); + g_application_release (app); + } +} + +static void +activate (gpointer data) +{ + /* GApplication complains if we don't connect to ::activate */ +} + +static gboolean +quit_already (gpointer data) +{ + GApplication *app = data; + + g_application_quit (app); + + return G_SOURCE_REMOVE; +} + +static void +test_replace (gconstpointer data) +{ + gboolean allow = GPOINTER_TO_INT (data); + + if (g_test_subprocess ()) + { + char *binpath = g_test_build_filename (G_TEST_BUILT, "unimportant", NULL); + char *argv[] = { binpath, "--gapplication-replace", NULL }; + GApplication *app; + gboolean startup = FALSE; + + app = g_application_new ("org.gtk.TestApplication.Replace", G_APPLICATION_ALLOW_REPLACEMENT); + g_signal_connect (app, "startup", G_CALLBACK (startup_in_subprocess), &startup); + g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); + + g_application_run (app, G_N_ELEMENTS (argv) - 1, argv); + + if (allow) + g_assert_true (startup); + else + g_assert_false (startup); + + g_object_unref (app); + g_free (binpath); + } + else + { + char *binpath = g_test_build_filename (G_TEST_BUILT, "unimportant", NULL); + gchar *argv[] = { binpath, NULL }; + GApplication *app; + gboolean name_lost = FALSE; + TestReplaceData data; + GTestDBus *bus; + + data.allow_replacement = allow; + data.subprocess = NULL; + + bus = g_test_dbus_new (0); + g_test_dbus_up (bus); + + app = g_application_new ("org.gtk.TestApplication.Replace", allow ? G_APPLICATION_ALLOW_REPLACEMENT : G_APPLICATION_FLAGS_NONE); + g_application_set_inactivity_timeout (app, 500); + g_signal_connect (app, "name-lost", G_CALLBACK (name_was_lost), &name_lost); + g_signal_connect (app, "startup", G_CALLBACK (startup_cb), &data); + g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); + + if (!allow) + g_timeout_add_seconds (1, quit_already, app); + + g_application_run (app, G_N_ELEMENTS (argv) - 1, argv); + + g_assert_nonnull (data.subprocess); + if (allow) + g_assert_true (name_lost); + else + g_assert_false (name_lost); + + g_object_unref (app); + g_free (binpath); + + g_subprocess_wait (data.subprocess, NULL, NULL); + g_clear_object (&data.subprocess); + + g_test_dbus_down (bus); + g_object_unref (bus); + } +} + int main (int argc, char **argv) { @@ -976,7 +1126,8 @@ main (int argc, char **argv) g_test_init (&argc, &argv, NULL); - g_test_dbus_unset (); + if (!g_test_subprocess ()) + g_test_dbus_unset (); g_test_add_func ("/gapplication/no-dbus", test_nodbus); /* g_test_add_func ("/gapplication/basic", basic); */ @@ -996,6 +1147,8 @@ main (int argc, char **argv) g_test_add_func ("/gapplication/test-handle-local-options2", test_handle_local_options_failure); g_test_add_func ("/gapplication/test-handle-local-options3", test_handle_local_options_passthrough); g_test_add_func ("/gapplication/api", test_api); + g_test_add_data_func ("/gapplication/replace", GINT_TO_POINTER (TRUE), test_replace); + g_test_add_data_func ("/gapplication/no-replace", GINT_TO_POINTER (FALSE), test_replace); return g_test_run (); }