diff options
author | Matthias Clasen <mclasen@redhat.com> | 2010-06-07 13:25:39 -0400 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2010-06-07 13:48:42 -0400 |
commit | 6427e93757dbc38cc53ffeb87ed814cdb9df0ed4 (patch) | |
tree | 6ca2795a4c2872ad40795a03991093665d6e802f | |
parent | af78f6d418788fa76a2c78298896f9c656d8eb85 (diff) |
Merge the wip/gapplication branch
This adds a GApplication object to GIO, which is the core of
an application support class, supporting
- uniqueness
- exporting actions (simple scripting)
- standard actions (quit, activate)
The implementation for Linux uses D-Bus, takes a name on the
session bus, and exports a org.gtk.Application interface.
Implementations for Win32 and OS X are still missing.
-rw-r--r-- | docs/reference/gio/gio-docs.xml | 4 | ||||
-rw-r--r-- | docs/reference/gio/gio-sections.txt | 32 | ||||
-rw-r--r-- | docs/reference/gio/gio.types | 1 | ||||
-rw-r--r-- | gio/Makefile.am | 5 | ||||
-rw-r--r-- | gio/gappinfo.c | 42 | ||||
-rw-r--r-- | gio/gapplication.c | 1058 | ||||
-rw-r--r-- | gio/gapplication.h | 151 | ||||
-rw-r--r-- | gio/gdbusapplication.c | 426 | ||||
-rw-r--r-- | gio/gdbusconnection.h | 2 | ||||
-rw-r--r-- | gio/gdesktopappinfo.c | 12 | ||||
-rw-r--r-- | gio/gio-marshal.list | 3 | ||||
-rw-r--r-- | gio/gio.h | 1 | ||||
-rw-r--r-- | gio/gio.symbols | 21 | ||||
-rw-r--r-- | gio/gnullapplication.c | 70 | ||||
-rw-r--r-- | gio/tests/Makefile.am | 20 | ||||
-rw-r--r-- | gio/tests/appinfo-test.c | 20 | ||||
-rw-r--r-- | gio/tests/appinfo-test.desktop | 4 | ||||
-rw-r--r-- | gio/tests/appinfo.c | 25 | ||||
-rw-r--r-- | gio/tests/application.c | 134 | ||||
-rw-r--r-- | gio/tests/testapp.c | 79 | ||||
-rw-r--r-- | gio/tests/testapps.c | 533 |
21 files changed, 2626 insertions, 17 deletions
diff --git a/docs/reference/gio/gio-docs.xml b/docs/reference/gio/gio-docs.xml index 8fe0d987b..0e092d169 100644 --- a/docs/reference/gio/gio-docs.xml +++ b/docs/reference/gio/gio-docs.xml @@ -160,6 +160,10 @@ <xi:include href="xml/gpermission.xml"/> <xi:include href="xml/gsimplepermission.xml"/> </chapter> + <chapter id="application"> + <title>Application support</title> + <xi:include href="xml/gapplication.xml"/> + </chapter> <chapter id="extending"> <title>Extending GIO</title> <xi:include href="xml/gvfs.xml"/> diff --git a/docs/reference/gio/gio-sections.txt b/docs/reference/gio/gio-sections.txt index f7e68d3df..665a31ea4 100644 --- a/docs/reference/gio/gio-sections.txt +++ b/docs/reference/gio/gio-sections.txt @@ -2619,3 +2619,35 @@ g_simple_permission_new <SUBSECTION Standard> g_simple_permission_get_type </SECTION> + +<FILE>gapplication</FILE> +GApplication +GApplicationClass + +g_application_new +g_application_new_and_register +g_application_register_with_data +g_application_format_activation_data +g_application_get_instance +g_application_get_id +g_application_add_action +g_application_remove_action +g_application_list_actions +g_application_set_action_enabled +g_application_get_action_enabled +g_application_get_action_description +g_application_invoke_action +g_application_run +g_application_quit +g_application_is_remote +<SUBSECTION Standard> +G_TYPE_APPLICATION +G_APPLICATION +G_APPLICATION_CLASS +G_IS_APPLICATION +G_IS_APPLICATION_CLASS +G_APPLICATION_GET_CLASS +<SUBSECTION Private> +GApplicationPrivate +g_application_get_type +</SECTION> diff --git a/docs/reference/gio/gio.types b/docs/reference/gio/gio.types index 5a4ff84a7..5073fc36f 100644 --- a/docs/reference/gio/gio.types +++ b/docs/reference/gio/gio.types @@ -1,6 +1,7 @@ g_app_info_create_flags_get_type g_app_info_get_type g_app_launch_context_get_type +g_application_get_type g_ask_password_flags_get_type g_async_initable_get_type g_async_result_get_type diff --git a/gio/Makefile.am b/gio/Makefile.am index a2efeea13..fe4053e19 100644 --- a/gio/Makefile.am +++ b/gio/Makefile.am @@ -268,6 +268,8 @@ SUBDIRS += tests libgio_2_0_la_SOURCES = \ gappinfo.c \ + gapplication.c \ + gapplication.h \ gasynchelper.c \ gasynchelper.h \ gasyncinitable.c \ @@ -374,6 +376,8 @@ libgio_2_0_la_SOURCES = \ $(marshal_sources) \ $(NULL) +EXTRA_DIST += gnullapplication.c gdbusapplication.c + $(libgio_2_0_la_OBJECTS): $(marshal_sources) libgio_2_0_la_LIBADD = \ @@ -427,6 +431,7 @@ gio-win32-res.o: gio.rc gio_headers = \ gappinfo.h \ + gapplication.h \ gasyncinitable.h \ gasyncresult.h \ gbufferedinputstream.h \ diff --git a/gio/gappinfo.c b/gio/gappinfo.c index 6feec823c..38e3063ae 100644 --- a/gio/gappinfo.c +++ b/gio/gappinfo.c @@ -33,7 +33,7 @@ * @short_description: Application information and launch contexts * @include: gio/gio.h * - * #GAppInfo and #GAppLaunchContext are used for describing and launching + * #GAppInfo and #GAppLaunchContext are used for describing and launching * applications installed on the system. * * As of GLib 2.20, URIs will always be converted to POSIX paths @@ -117,9 +117,9 @@ g_app_info_dup (GAppInfo *appinfo) /** * g_app_info_equal: - * @appinfo1: the first #GAppInfo. + * @appinfo1: the first #GAppInfo. * @appinfo2: the second #GAppInfo. - * + * * Checks if two #GAppInfo<!-- -->s are equal. * * Returns: %TRUE if @appinfo1 is equal to @appinfo2. %FALSE otherwise. @@ -461,7 +461,7 @@ g_app_info_get_icon (GAppInfo *appinfo) * @launch_context: a #GAppLaunchContext or %NULL * @error: a #GError * - * Launches the application. Passes @files to the launched application + * Launches the application. Passes @files to the launched application * as arguments, using the optional @launch_context to get information * about the details of the launcher (like what screen it is on). * On error, @error will be set accordingly. @@ -476,8 +476,17 @@ g_app_info_get_icon (GAppInfo *appinfo) * unsupported uris with strange formats like mailto:), so if you have * a textual uri you want to pass in as argument, consider using * g_app_info_launch_uris() instead. - * - * Returns: %TRUE on successful launch, %FALSE otherwise. + * + * On UNIX, this function sets the <envvar>GIO_LAUNCHED_DESKTOP_FILE</envvar> + * environment variable with the path of the launched desktop file and + * <envvar>GIO_LAUNCHED_DESKTOP_FILE_PID</envvar> to the process + * id of the launched process. This can be used to ignore + * <envvar>GIO_LAUNCHED_DESKTOP_FILE</envvar>, should it be inherited + * by further processes. The <envvar>DISPLAY</envvar> and + * <envvar>DESKTOP_STARTUP_ID</envvar> environment variables are also + * set, based on information provided in @launch_context. + * + * Returns: %TRUE on successful launch, %FALSE otherwise. **/ gboolean g_app_info_launch (GAppInfo *appinfo, @@ -540,11 +549,11 @@ g_app_info_supports_files (GAppInfo *appinfo) /** * g_app_info_launch_uris: * @appinfo: a #GAppInfo - * @uris: a #GList containing URIs to launch. + * @uris: a #GList containing URIs to launch. * @launch_context: a #GAppLaunchContext or %NULL * @error: a #GError * - * Launches the application. Passes @uris to the launched application + * Launches the application. Passes @uris to the launched application * as arguments, using the optional @launch_context to get information * about the details of the launcher (like what screen it is on). * On error, @error will be set accordingly. @@ -555,7 +564,7 @@ g_app_info_supports_files (GAppInfo *appinfo) * can fail to start if it runs into problems during startup. There is * no way to detect this. * - * Returns: %TRUE on successful launch, %FALSE otherwise. + * Returns: %TRUE on successful launch, %FALSE otherwise. **/ gboolean g_app_info_launch_uris (GAppInfo *appinfo, @@ -600,7 +609,7 @@ g_app_info_should_show (GAppInfo *appinfo) * @launch_context: an optional #GAppLaunchContext. * @error: a #GError. * - * Utility function that launches the default application + * Utility function that launches the default application * registered to handle the specified uri. Synchronous I/O * is done on the uri to detect the type of the file if * required. @@ -726,10 +735,10 @@ g_app_launch_context_init (GAppLaunchContext *launch_context) * @info: a #GAppInfo * @files: a #GList of #GFile objects * - * Gets the display string for the display. This is used to ensure new - * applications are started on the same display as the launching - * application. - * + * Gets the display string for the @context. This is used to ensure new + * applications are started on the same display as the launching + * application, by setting the <envvar>DISPLAY</envvar> environment variable. + * * Returns: a display string for the display. **/ char * @@ -757,13 +766,14 @@ g_app_launch_context_get_display (GAppLaunchContext *context, * @files: a #GList of of #GFile objects * * Initiates startup notification for the application and returns the - * DESKTOP_STARTUP_ID for the launched operation, if supported. + * <envvar>DESKTOP_STARTUP_ID</envvar> for the launched operation, + * if supported. * * Startup notification IDs are defined in the <ulink * url="http://standards.freedesktop.org/startup-notification-spec/startup-notification-latest.txt"> * FreeDesktop.Org Startup Notifications standard</ulink>. * - * Returns: a startup notification ID for the application, or %NULL if + * Returns: a startup notification ID for the application, or %NULL if * not supported. **/ char * diff --git a/gio/gapplication.c b/gio/gapplication.c new file mode 100644 index 000000000..d974ed9fa --- /dev/null +++ b/gio/gapplication.c @@ -0,0 +1,1058 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright © 2010 Red Hat, Inc + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Colin Walters <walters@verbum.org> + * Emmanuele Bassi <ebassi@linux.intel.com> + */ + +#include "config.h" + +#include <string.h> +#include <stdlib.h> + +#include <gobject/gvaluecollector.h> + +#include "gapplication.h" +#include "gio-marshal.h" +#include "glibintl.h" + +#include "gioerror.h" + +#include "gdbusconnection.h" +#include "gdbusintrospection.h" +#include "gdbusmethodinvocation.h" + +#include "gioalias.h" + +/** + * SECTION: gapplication + * @title: GApplication + * @short_description: Core application class + * + * A #GApplication is the foundation of an application, unique for + * a given application identifier. The #GApplication wraps some + * low-level platform-specific services; it's expected that most + * software will use a higher-level application class such as + * #GtkApplication or #MxApplication. + * + * In addition to single-instance-ness, #GApplication provides support + * for 'actions', which can be presented to the user in a platform-specific + * way (e.g. Windows 7 jump lists). Note that these are just simple actions + * without parameters. For more flexible scriptability, implementing a + * a separate D-Bus interface is recommended, see e.g. + * <xref linkend="gdbus-convenience"/>. + * + * Before using #GApplication, you must choose an "application identifier". + * The expected form of an application identifier is very close to that of + * of a <ulink url="http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface">DBus bus name</ulink>. + * Examples include: "com.example.MyApp" "org.example.internal-apps.Calculator" + * For convenience, the restrictions on application identifiers are reproduced + * here: + * <itemizedlist> + * <listitem>Application identifiers must contain only the ASCII characters "[A-Z][a-z][0-9]_-" and must not begin with a digit.</listitem> + * <listitem>Application identifiers must contain at least one '.' (period) character (and thus at least two elements).</listitem> + * <listitem>Application identifiers must not begin with a '.' (period) character.</listitem> + * <listitem>Application identifiers must not exceed 255 characters.</listitem> + * </itemizedlist> + * + * <refsect2><title>D-Bus implementation</title> + * <para> + * On UNIX systems using D-Bus, #GApplication is implemented by claiming the + * application identifier as a bus name on the session bus. The implementation + * exports an object at the object path that is created by replacing '.' with + * '/' in the application identifier (e.g. the object path for the + * application id 'org.gtk.TestApp' is '/org/gtk/TestApp'). The object + * implements the org.gtk.Application interface. + * </para> + * <classsynopsis class="interface"> + * <ooclass><interfacename>org.gtk.Application</interfacename></ooclass> + * <methodsynopsis> + * <void/> + * <methodname>Activate</methodname> + * <methodparam><modifier>in</modifier><type>aay</type><parameter>arguments</parameter></methodparam> + * <methodparam><modifier>in</modifier><type>a{sv}</type><parameter>data</parameter></methodparam> + * </methodsynopsis> + * <methodsynopsis> + * <void/> + * <methodname>InvokeAction</methodname> + * <methodparam><modifier>in</modifier><type>s</type><parameter>action</parameter></methodparam> + * <methodparam><modifier>in</modifier><type>u</type><parameter>timestamp</parameter></methodparam> + * </methodsynopsis> + * <methodsynopsis> + * <type>a{s(sb)}</type> + * <methodname>ListActions</methodname> + * <void/> + * </methodsynopsis> + * <methodsynopsis> + * <void/> + * <methodname>Quit</methodname> + * <methodparam><modifier>in</modifier><type>u</type><parameter>timestamp</parameter></methodparam> + * </methodsynopsis> + * <methodsynopsis> + * <modifier>Signal</modifier> + * <void/> + * <methodname>ActionsChanged</methodname> + * <void/> + * </methodsynopsis> + * </classsynopsis> + * <para> + * The <methodname>Activate</methodname> function is called on the existing + * application instance when a second instance fails to take the bus name. + * @arguments contains the commandline arguments given to the second instance + * and @data contains platform-specific additional data, see + * g_application_format_activation_data(). + * </para> + * <para> + * The <methodname>InvokeAction</methodname> function can be called to invoke + * one of the actions exported by the application. The @timestamp parameter + * should be taken from the user event that triggered the method call (e.g. + * a button press event). + * </para> + * <para> + * The <methodname>ListActions</methodname> function returns a dictionary + * with the exported actions of the application. The keys of the dictionary + * are the action names, and the values are structs containing the description + * for the action and a boolean that represents if the action is enabled or not. + * </para> + * <para> + * The <methodname>Quit</methodname> function can be called to terminate + * the application. The @timestamp parameter should be taken from the user + * event that triggered the method call (e.g. a button press event). + * </para> + * <para> + * The <methodname>ActionsChanged</methodname> signal is emitted when the + * exported actions change (i.e. an action is added, removed, enabled, + * disabled, or otherwise changed). + * </para> + * </refsect2> + * + * Since: 2.24 + */ + +G_DEFINE_TYPE (GApplication, g_application, G_TYPE_OBJECT); + +enum +{ + PROP_0, + + PROP_APPID, + PROP_DEFAULT_QUIT, + PROP_IS_REMOTE +}; + +enum +{ + QUIT, + ACTION, + PREPARE_ACTIVATION, + + LAST_SIGNAL +}; + +static guint application_signals[LAST_SIGNAL] = { 0 }; + +typedef struct { + char *name; + char *description; + guint enabled : 1; +} GApplicationAction; + +struct _GApplicationPrivate +{ + char *appid; + GHashTable *actions; /* name -> GApplicationAction */ + GMainLoop *mainloop; + + guint default_quit : 1; + guint is_remote : 1; + + guint actions_changed_id; + +#ifdef G_OS_UNIX + char *dbus_path; + GDBusConnection *session_bus; +#endif +}; + +static GApplication *primary_application = NULL; +static GHashTable *instances_for_appid = NULL; + +static void _g_application_platform_init (GApplication *app); +static gboolean _g_application_platform_acquire_single_instance (GApplication *app, + GError **error); +static void _g_application_platform_remote_invoke_action (GApplication *app, + const char *action, + guint timestamp); +static void _g_application_platform_remote_quit (GApplication *app, + guint timestamp); +static void _g_application_platform_activate (GApplication *app, + GVariant *data) G_GNUC_NORETURN; +static void _g_application_platform_on_actions_changed (GApplication *app); + +#ifdef G_OS_UNIX +#include "gdbusapplication.c" +#else +#include "gnullapplication.c" +#endif + +static gboolean +_g_application_validate_id (const char *id) +{ + gboolean allow_dot; + + if (strlen (id) > 255) + return FALSE; + + if (!g_ascii_isalpha (*id)) + return FALSE; + + id++; + allow_dot = FALSE; + for (; *id; id++) + { + if (g_ascii_isalnum (*id) || (*id == '-') || (*id == '_')) + allow_dot = TRUE; + else if (allow_dot && *id == '.') + allow_dot = FALSE; + else + return FALSE; + } + return TRUE; +} + +static gpointer +init_appid_statics (gpointer data) +{ + instances_for_appid = g_hash_table_new (g_str_hash, g_str_equal); + return NULL; +} + +static GApplication * +application_for_appid (const char *appid) +{ + static GOnce appid_once = G_ONCE_INIT; + + g_once (&appid_once, init_appid_statics, NULL); + + return g_hash_table_lookup (instances_for_appid, appid); +} + +static gboolean +g_application_default_quit (GApplication *application, + guint timestamp) +{ + g_return_val_if_fail (application->priv->mainloop != NULL, FALSE); + g_main_loop_quit (application->priv->mainloop); + + return TRUE; +} + +static void +g_application_default_run (GApplication *application) +{ + if (application->priv->mainloop == NULL) + application->priv->mainloop = g_main_loop_new (NULL, TRUE); + + g_main_loop_run (application->priv->mainloop); +} + +static void +_g_application_handle_activation (GApplication *app, + int argc, + char **argv, + GVariant *platform_data) +{ + GVariantBuilder builder; + GVariant *message; + int i; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("(aaya{sv})")); + g_variant_builder_open (&builder, G_VARIANT_TYPE ("aay")); + + for (i = 1; i < argc; i++) + { + int j; + guint8 *argv_bytes; + + g_variant_builder_open (&builder, G_VARIANT_TYPE ("ay")); + + argv_bytes = (guint8*) argv[i]; + for (j = 0; argv_bytes[j]; j++) + g_variant_builder_add_value (&builder, + g_variant_new_byte (argv_bytes[j])); + g_variant_builder_close (&builder); + } + g_variant_builder_close (&builder); + + if (platform_data) + g_variant_builder_add (&builder, "@a{sv}", platform_data); + else + { + g_variant_builder_open (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_close (&builder); + } + + message = g_variant_builder_end (&builder); + _g_application_platform_activate (app, message); + g_variant_unref (message); +} + +static gboolean +timeout_handle_actions_changed (gpointer user_data) +{ + GApplication *application = user_data; + + application->priv->actions_changed_id = 0; + + _g_application_platform_on_actions_changed (application); + + return FALSE; +} + +static inline void +queue_actions_change_notification (GApplication *application) +{ + GApplicationPrivate *priv = application->priv; + + if (priv->actions_changed_id == 0) + priv->actions_changed_id = g_timeout_add (0, timeout_handle_actions_changed, application); +} + +static void +g_application_action_free (gpointer data) +{ + if (G_LIKELY (data != NULL)) + { + GApplicationAction *action = data; + + g_free (action->name); + g_free (action->description); + + g_slice_free (GApplicationAction, action); + } +} + + +/** + * g_application_new: + * @appid: System-dependent application identifier + * + * Create a new #GApplication. The application is initially in + * "remote" mode. Almost certainly, you want to call + * g_application_register() immediately after this function, which + * will finish initialization. + * + * As a convenience, this function is defined to call g_type_init() as + * its very first action. + * + * Returns: (transfer full): An application instance + * + * Since: 2.26 + */ +GApplication * +g_application_new (const char *appid) +{ + g_type_init (); + + return G_APPLICATION (g_object_new (G_TYPE_APPLICATION, "appid", appid, NULL)); +} + +/** + * g_application_register: + * @application: A #GApplication + * @argc: System argument count + * @argv: (array length=argc): System argument vector + * @platform_data: (allow-none): Arbitrary platform-specific data, must have signature "a{sv}" + * + * Ensure the current process is the unique owner of the application. + * If successful, the #GApplication:is-remote property will be changed + * to %FALSE, and it is safe to continue creating other resources + * such as graphics windows. + * + * If the given @appid is already running in another process, the + * #GApplication:default-exit property will be evaluated. If it's + * %TRUE, then a platform-specific action such as bringing any + * graphics windows associated with the application to the foreground + * may be initiated. After that, the current process will terminate. + * If %FALSE, then the application remains in the #GApplication:is-remote + * state, and you can e.g. call g_application_invoke_action(). + */ +void +g_application_register_with_data (GApplication *application, + int argc, + char **argv, + GVariant *platform_data) +{ + g_return_if_fail (application->priv->appid != NULL); + g_return_if_fail (application->priv->is_remote); + g_return_if_fail (platform_data == NULL + || strcmp (g_variant_get_type_string (platform_data), "a{sv}") == 0); + + if (!_g_application_platform_acquire_single_instance (application, NULL)) + { + if (application->priv->default_quit) + _g_application_handle_activation (application, argc, argv, platform_data); + else + return; + } + + application->priv->is_remote = FALSE; + + _g_application_platform_init (application); +} + +/** + * g_application_new_and_register: + * @appid: An application identifier + * @argc: System argument count + * @argv: (array length=argc): System argument vector + * + * This is a convenience function which combines g_application_new() + * with g_application_register_with_data(). + */ +GApplication * +g_application_new_and_register (const char *appid, + int argc, + char **argv) +{ + GApplication *app = g_application_new (appid); + g_application_register_with_data (app, argc, argv, NULL); + return app; +} + +/** + * g_application_add_action: + * @application: a #GApplication + * @name: the action name + * @description: the action description; can be a translatable + * string + * + * Adds an action @name to the list of exported actions of @application. + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * You can invoke an action using g_application_invoke_action(). + * + * The newly added action is enabled by default; you can call + * g_application_set_action_enabled() to disable it. + * + * Since: 2.26 + */ +void +g_application_add_action (GApplication *application, + const gchar *name, + const gchar *description) +{ + GApplicationPrivate *priv; + GApplicationAction *action; + + g_return_if_fail (G_IS_APPLICATION (application)); + g_return_if_fail (name != NULL && *name != '\0'); + g_return_if_fail (!application->priv->is_remote); + + priv = application->priv; + + g_return_if_fail (g_hash_table_lookup (priv->actions, name) == NULL); + + action = g_slice_new (GApplicationAction); + action->name = g_strdup (name); + action->description = g_strdup (description); + action->enabled = TRUE; + + g_hash_table_insert (priv->actions, action->name, action); + queue_actions_change_notification (application); +} + +/** + * g_application_remove_action: + * @application: a #GApplication + * @name: the name of the action to remove + * + * Removes the action @name from the list of exported actions of @application. + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * Since: 2.26 + */ +void +g_application_remove_action (GApplication *application, + const gchar *name) +{ + GApplicationPrivate *priv; + + g_return_if_fail (G_IS_APPLICATION (application)); + g_return_if_fail (name != NULL && *name != '\0'); + g_return_if_fail (!application->priv->is_remote); + + priv = application->priv; + + g_return_if_fail (g_hash_table_lookup (priv->actions, name) != NULL); + + g_hash_table_remove (priv->actions, name); + queue_actions_change_notification (application); +} + +/** + * g_application_invoke_action: + * @application: a #GApplication + * @name: the name of the action to invoke + * @timestamp: the timestamp that is going to be passed to + * the #GApplication::action signal + * + * Invokes the action @name of the passed #GApplication. + * + * This function has different behavior depending on whether @application + * is acting as a proxy for another process. In the normal case where + * the current process is hosting the application, and the specified + * action exists and is enabled, the #GApplication::action signal will + * be emitted. + * + * If @application is a proxy, then the specified action will be invoked + * in the remote process. It is not necessary to call + * g_application_add_action() in the current process in order to invoke + * one remotely. + * + * Since: 2.26 + */ +void +g_application_invoke_action (GApplication *application, + const char *name, + guint timestamp) +{ + GApplicationPrivate *priv; + GApplicationAction *action; + + g_return_if_fail (G_IS_APPLICATION (application)); + g_return_if_fail (name != NULL); + + priv = application->priv; + + if (priv->is_remote) + { + _g_application_platform_remote_invoke_action (application, name, timestamp); + return; + } + + action = g_hash_table_lookup (priv->actions, name); + g_return_if_fail (action != NULL); + if (!action->enabled) + return; + + g_signal_emit (application, application_signals[ACTION], + g_quark_from_string (name), + name, + timestamp); +} + +/** + * g_application_list_actions: + * @application: a #GApplication + * + * Retrieves the list of action names currently exported by @application. + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * Return value: (transfer full): a newly allocation, %NULL-terminated array + * of strings containing action names; use g_strfreev() to free the + * resources used by the returned array + * + * Since: 2.26 + */ +gchar ** +g_application_list_actions (GApplication *application) +{ + GApplicationPrivate *priv; + GHashTableIter iter; + gpointer key; + gchar **retval; + gint i; + + g_return_val_if_fail (G_IS_APPLICATION (application), NULL); + g_return_val_if_fail (!application->priv->is_remote, NULL); + + priv = application->priv; + + retval = g_new (gchar*, g_hash_table_size (priv->actions)); + + i = 0; + g_hash_table_iter_init (&iter, priv->actions); + while (g_hash_table_iter_next (&iter, &key, NULL)) + retval[i++] = g_strdup (key); + + retval[i] = NULL; + + return retval; +} + +/** + * g_application_set_action_enabled: + * @application: a #GApplication + * @name: the name of the application + * @enabled: whether to enable or disable the action @name + * + * Sets whether the action @name inside @application should be enabled + * or disabled. + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * Invoking a disabled action will not result in the #GApplication::action + * signal being emitted. + * + * Since: 2.26 + */ +void +g_application_set_action_enabled (GApplication *application, + const gchar *name, + gboolean enabled) +{ + GApplicationAction *action; + + g_return_if_fail (G_IS_APPLICATION (application)); + g_return_if_fail (name != NULL); + g_return_if_fail (!application->priv->is_remote); + + enabled = !!enabled; + + action = g_hash_table_lookup (application->priv->actions, name); + g_return_if_fail (action != NULL); + if (action->enabled == enabled) + return; + + action->enabled = enabled; + + queue_actions_change_notification (application); +} + + +/** + * g_application_get_action_description: + * @application: a #GApplication + * @name: Action name + * + * Gets the description of the action @name. + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * Returns: Description for the given action named @name + * + * Since: 2.26 + */ +G_CONST_RETURN gchar * +g_application_get_action_description (GApplication *application, + const gchar *name) +{ + GApplicationAction *action; + + g_return_val_if_fail (G_IS_APPLICATION (application), NULL); + g_return_val_if_fail (name != NULL, NULL); + g_return_val_if_fail (!application->priv->is_remote, NULL); + + action = g_hash_table_lookup (application->priv->actions, name); + g_return_val_if_fail (action != NULL, NULL); + + return action->description; +} + + +/** + * g_application_get_action_enabled: + * @application: a #GApplication + * @name: the name of the action + * + * Retrieves whether the action @name is enabled or not. + * + * See g_application_set_action_enabled(). + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * Return value: %TRUE if the action was enabled, and %FALSE otherwise + * + * Since: 2.26 + */ +gboolean +g_application_get_action_enabled (GApplication *application, + const gchar *name) +{ + GApplicationAction *action; + + g_return_val_if_fail (G_IS_APPLICATION (application), FALSE); + g_return_val_if_fail (name != NULL, FALSE); + g_return_val_if_fail (!application->priv->is_remote, FALSE); + + action = g_hash_table_lookup (application->priv->actions, name); + g_return_val_if_fail (action != NULL, FALSE); + + return action->enabled; +} + +/** + * g_application_run: + * @application: a #GApplication + * + * Starts the application. + * + * The default implementation of this virtual function will simply run + * a main loop. + * + * It is an error to call this function if @application is a proxy for + * a remote application. + * + * Since: 2.26 + */ +void +g_application_run (GApplication *application) +{ + g_return_if_fail (G_IS_APPLICATION (application)); + g_return_if_fail (!application->priv->is_remote); + + G_APPLICATION_GET_CLASS (application)->run (application); +} + +/** + * g_application_quit: + * @application: a #GApplication + * @timestamp: Platform-specific event timestamp, may be 0 for default + * + * Request that the application quits. + * + * This function has different behavior depending on whether @application + * is acting as a proxy for another process. In the normal case where + * the current process is hosting the application, the default + * implementation will quit the main loop created by g_application_run(). + * + * If @application is a proxy, then the remote process will be asked + * to quit. + * + * Returns: %TRUE if the application accepted the request, %FALSE otherwise + * + * Since: 2.26 + */ +gboolean +g_application_quit (GApplication *application, + guint timestamp) +{ + gboolean retval = FALSE; + + g_return_val_if_fail (G_IS_APPLICATION (application), FALSE); + + if (application->priv->is_remote) + { + _g_application_platform_remote_quit (application, timestamp); + retval = TRUE; + } + else + g_signal_emit (application, application_signals[QUIT], 0, timestamp, &retval); + + return retval; +} + +/** + * g_application_get_instance: + * + * In the normal case where there is exactly one #GApplication instance + * in this process, return that instance. If there are multiple, the + * first one created will be returned. Otherwise, return %NULL. + * + * Returns: (transfer none): The primary instance of #GApplication, + * or %NULL if none is set + * + * Since: 2.26 + */ +GApplication * +g_application_get_instance (void) +{ + return primary_application; +} + +/** + * g_application_get_id: + * + * Retrieves the platform-specific identifier for the #GApplication. + * + * Return value: The platform-specific identifier. The returned string + * is owned by the #GApplication instance and it should never be + * modified or freed + * + * Since: 2.26 + */ +G_CONST_RETURN char * +g_application_get_id (GApplication *application) +{ + g_return_val_if_fail (G_IS_APPLICATION (application), NULL); + + return application->priv->appid; +} + +/** + * g_application_is_remote: + * @application: a #GApplication + * + * Returns: %TRUE if this object represents a proxy for a remote application. + */ +gboolean +g_application_is_remote (GApplication *application) +{ + g_return_val_if_fail (G_IS_APPLICATION (application), FALSE); + + return application->priv->is_remote; +} + +static void +g_application_init (GApplication *app) +{ + app->priv = G_TYPE_INSTANCE_GET_PRIVATE (app, + G_TYPE_APPLICATION, + GApplicationPrivate); + + app->priv->actions = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + g_application_action_free); + app->priv->default_quit = TRUE; + app->priv->is_remote = TRUE; +} + +static void +g_application_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GApplication *app = G_APPLICATION (object); + + switch (prop_id) + { + case PROP_APPID: + g_value_set_string (value, g_application_get_id (app)); + break; + + case PROP_DEFAULT_QUIT: + g_value_set_boolean (value, app->priv->default_quit); + break; + + case PROP_IS_REMOTE: + g_value_set_boolean (value, g_application_is_remote (app)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +g_application_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GApplication *app = G_APPLICATION (object); + + switch (prop_id) + { + case PROP_APPID: + g_return_if_fail (_g_application_validate_id (g_value_get_string (value))); + app->priv->appid = g_value_dup_string (value); + break; + + case PROP_DEFAULT_QUIT: + app->priv->default_quit = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static GObject* +g_application_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GApplication *app; + GObject *object; + const char *appid = NULL; + guint i; + + for (i = 0; i < n_construct_properties; i++) + { + GObjectConstructParam *param = &construct_params[i]; + if (strcmp (param->pspec->name, "appid") == 0) + appid = g_value_get_string (param->value); + } + + g_return_val_if_fail (appid != NULL, NULL); + + app = application_for_appid (appid); + if (app != NULL) + return g_object_ref (app); + + object = (* G_OBJECT_CLASS (g_application_parent_class)->constructor) (type, + n_construct_properties, + construct_params); + app = G_APPLICATION (object); + + if (primary_application == NULL) + primary_application = app; + g_hash_table_insert (instances_for_appid, g_strdup (appid), app); + + return object; +} + +static void +g_application_finalize (GObject *object) +{ + GApplication *app = G_APPLICATION (object); + + g_free (app->priv->appid); + if (app->priv->actions) + g_hash_table_unref (app->priv->actions); + if (app->priv->actions_changed_id) + g_source_remove (app->priv->actions_changed_id); + if (app->priv->mainloop) + g_main_loop_unref (app->priv->mainloop); + +#ifdef G_OS_UNIX + g_free (app->priv->dbus_path); + if (app->priv->session_bus) + g_object_unref (app->priv->session_bus); +#endif + + G_OBJECT_CLASS (g_application_parent_class)->finalize (object); +} + +static void +g_application_class_init (GApplicationClass *klass) +{ + GObjectClass *gobject_class G_GNUC_UNUSED = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (GApplicationPrivate)); + + gobject_class->constructor = g_application_constructor; + gobject_class->set_property = g_application_set_property; + gobject_class->get_property = g_application_get_property; + + gobject_class->finalize = g_application_finalize; + + klass->run = g_application_default_run; + klass->quit = g_application_default_quit; + + application_signals[QUIT] = + g_signal_new (g_intern_static_string ("quit"), + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GApplicationClass, quit), + g_signal_accumulator_true_handled, NULL, + _gio_marshal_BOOLEAN__UINT, + G_TYPE_BOOLEAN, 1, + G_TYPE_UINT); + + application_signals[ACTION] = + g_signal_new (g_intern_static_string ("action"), + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE | G_SIGNAL_DETAILED, + G_STRUCT_OFFSET (GApplicationClass, action), + NULL, NULL, + _gio_marshal_VOID__STRING_INT, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_INT); + + /** + * GApplication::prepare-activation: + * @arguments: A #GVariant with the signature "aay" + * @platform_data: A #GVariant with the signature "a{sv}" + * + * This signal is emitted when a non-primary process for a given + * application is invoked while your application is running; for + * example, when a file browser launches your program to open a + * file. The raw operating system arguments are passed in the + * @arguments variant. Additional platform-dependent data is + * stored in @platform_data. + */ + + application_signals[PREPARE_ACTIVATION] = + g_signal_new (g_intern_static_string ("prepare-activation"), + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GApplicationClass, prepare_activation), + NULL, NULL, + _gio_marshal_VOID__BOXED_BOXED, + G_TYPE_NONE, 2, + G_TYPE_VARIANT, + G_TYPE_VARIANT); + + /** + * GApplication:appid: + * + * The unique identifier for this application. See the documentation for + * #GApplication for more information about this property. + * + */ + g_object_class_install_property (gobject_class, + PROP_APPID, + g_param_spec_string ("appid", + P_("Application ID"), + P_("Identifier for this application"), + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GApplication:default-quit: + * + * By default, if a different process is running this application, the + * process will be exited. Set this property to %FALSE to allow custom + * interaction with the remote process. + * + */ + g_object_class_install_property (gobject_class, + PROP_DEFAULT_QUIT, + g_param_spec_boolean ("default-quit", + P_("Default Quit"), + P_("Exit the process by default"), + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + + /** + * GApplication:is-remote: + * + * This property is %TRUE if this application instance represents a proxy + * to the instance of this application in another process. + * + */ + g_object_class_install_property (gobject_class, + PROP_IS_REMOTE, + g_param_spec_boolean ("is-remote", + P_("Is Remote"), + P_("Whether this application is a proxy for another process"), + TRUE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); +} + +#define __G_APPLICATION_C__ +#include "gioaliasdef.c" diff --git a/gio/gapplication.h b/gio/gapplication.h new file mode 100644 index 000000000..bb70fe1fa --- /dev/null +++ b/gio/gapplication.h @@ -0,0 +1,151 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright © 2010 Red Hat, Inc + * + * This program 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 of the licence 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, write to the + * Free Software Foundation, Inc., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Colin Walters <walters@verbum.org> + * Emmanuele Bassi <ebassi@linux.intel.com> + */ + +#if !defined (__GIO_GIO_H_INSIDE__) && !defined (GIO_COMPILATION) +#error "Only <gio/gio.h> can be included directly." +#endif + +#ifndef __G_APPLICATION_H__ +#define __G_APPLICATION_H__ + +#include <glib-object.h> +#include <gio/giotypes.h> + +G_BEGIN_DECLS + +#define G_TYPE_APPLICATION (g_application_get_type ()) +#define G_APPLICATION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), G_TYPE_APPLICATION, GApplication)) +#define G_APPLICATION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), G_TYPE_APPLICATION, GApplicationClass)) +#define G_IS_APPLICATION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), G_TYPE_APPLICATION)) +#define G_IS_APPLICATION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), G_TYPE_APPLICATION)) +#define G_APPLICATION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), G_TYPE_APPLICATION, GApplicationClass)) + +typedef struct _GApplication GApplication; +typedef struct _GApplicationPrivate GApplicationPrivate; +typedef struct _GApplicationClass GApplicationClass; + +/** + * GApplication: + * + * The <structname>GApplication</structname> structure contains private + * data and should only be accessed using the provided API + * + * Since: 2.26 + */ +struct _GApplication +{ + /*< private >*/ + GObject parent_instance; + + GApplicationPrivate *priv; +}; + +/** + * GApplicationClass: + * @action: class handler for the #GApplication::action signal + * @quit: class handler for the #GApplication::quit signal + * @prepare_activation: class handler for the #GApplication::prepare-activation signal + * @run: virtual function, called by g_application_run() + * @format_activation_data: virtual function, called by g_application_format_activation_data() + * + * The <structname>GApplicationClass</structname> structure contains + * private data only + * + * Since: 2.26 + */ +struct _GApplicationClass +{ + /*< private >*/ + GObjectClass parent_class; + + /*< public >*/ + /* signals */ + void (* action) (GApplication *application, + const gchar *action_name, + guint timestamp); + gboolean (* quit) (GApplication *application, + guint timestamp); + void (* prepare_activation) (GApplication *application, + GVariant *arguments, + GVariant *platform_data); + + /* vfuncs */ + void (* run) (GApplication *application); + void (*format_activation_data) (GApplication *application, + GVariantBuilder *builder); + + /*< private >*/ + /* Padding for future expansion */ + void (*_g_reserved1) (void); + void (*_g_reserved2) (void); + void (*_g_reserved3) (void); + void (*_g_reserved4) (void); + void (*_g_reserved5) (void); + void (*_g_reserved6) (void); +}; +GType g_application_get_type (void) G_GNUC_CONST; + +GApplication * g_application_new (const char *appid); + +void g_application_register_with_data (GApplication *application, + int argc, + char **argv, + GVariant *platform_data); + +GApplication * g_application_new_and_register (const char *appid, + int argc, + char **argv); + +GApplication * g_application_get_instance (void); +G_CONST_RETURN gchar * g_application_get_id (GApplication *application); + +void g_application_add_action (GApplication *application, + const char *name, + const char *description); +void g_application_remove_action (GApplication *application, + const char *name); +gchar ** g_application_list_actions (GApplication *application); +void g_application_set_action_enabled (GApplication *application, + const char *name, + gboolean enabled); +gboolean g_application_get_action_enabled (GApplication *application, + const char *name); +G_CONST_RETURN gchar * g_application_get_action_description (GApplication *application, + const char *name); +void g_application_invoke_action (GApplication *application, + const char *name, + guint timestamp); + +void g_application_run (GApplication *application); +gboolean g_application_quit (GApplication *app, + guint timestamp); + +gboolean g_application_is_remote (GApplication *application); + +void g_application_format_activation_data (GApplication *app, + GVariantBuilder *builder); + + +G_END_DECLS + +#endif /* __G_APPLICATION_H__ */ diff --git a/gio/gdbusapplication.c b/gio/gdbusapplication.c new file mode 100644 index 000000000..012845db7 --- /dev/null +++ b/gio/gdbusapplication.c @@ -0,0 +1,426 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright © 2010 Red Hat, Inc + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Colin Walters <walters@verbum.org> + */ + +#define G_APPLICATION_IFACE "org.gtk.Application" + +static void +application_dbus_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GApplication *app = G_APPLICATION (user_data); + + if (method_name == NULL && *method_name == '\0') + return; + + if (strcmp (method_name, "Quit") == 0) + { + guint32 timestamp; + g_variant_get (parameters, "(u)", ×tamp); + + g_dbus_method_invocation_return_value (invocation, NULL); + + g_application_quit (app, timestamp); + } + else if (strcmp (method_name, "ListActions") == 0) + { + GHashTableIter iter; + GApplicationAction *value; + GVariantBuilder builder; + GVariant *return_args; + GVariant *result; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + g_hash_table_iter_init (&iter, app->priv->actions); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&value)) + g_variant_builder_add (&builder, "{s(sb)}", + value->name, + value->description ? value->description : "", + value->enabled); + + result = g_variant_builder_end (&builder); + return_args = g_variant_new_tuple (&result, 1); + g_dbus_method_invocation_return_value (invocation, return_args); + g_variant_unref (return_args); + g_variant_unref (result); + } + else if (strcmp (method_name, "InvokeAction") == 0) + { + const char *action_name; + guint32 timestamp; + GApplicationAction *action; + + g_variant_get (parameters, "(su)", &action_name, ×tamp); + + action = g_hash_table_lookup (app->priv->actions, action_name); + + if (!action) + { + char *errmsg = g_strdup_printf ("Invalid action: %s", action_name); + g_dbus_method_invocation_return_dbus_error (invocation, G_APPLICATION_IFACE ".InvalidAction", errmsg); + g_free (errmsg); + return; + } + + g_signal_emit (app, application_signals[ACTION], g_quark_from_string (action_name), action_name, (guint)timestamp); + + g_dbus_method_invocation_return_value (invocation, NULL); + } + else if (strcmp (method_name, "Activate") == 0) + { + GVariant *args; + GVariant *platform_data; + + g_variant_get (parameters, "(@aay@a{sv})", &args, &platform_data); + + g_signal_emit (app, application_signals[PREPARE_ACTIVATION], 0, args, platform_data); + + g_variant_unref (args); + g_variant_unref (platform_data); + + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusArgInfo application_quit_in_args[] = +{ + { + -1, + "timestamp", + "u", + NULL + } +}; + +static const GDBusArgInfo * const application_quit_in_args_p[] = { + &application_quit_in_args[0], + NULL +}; + +static const GDBusArgInfo application_list_actions_out_args[] = +{ + { + -1, + "actions", + "a{s(sb)}", + NULL + } +}; + +static const GDBusArgInfo * const application_list_actions_out_args_p[] = { + &application_list_actions_out_args[0], + NULL +}; + +static const GDBusArgInfo application_invoke_action_in_args[] = +{ + { + -1, + "action", + "s", + NULL + }, + { + -1, + "timestamp", + "u", + NULL + } +}; + +static const GDBusArgInfo * const application_invoke_action_in_args_p[] = { + &application_invoke_action_in_args[0], + &application_invoke_action_in_args[1], + NULL +}; + +static const GDBusMethodInfo application_quit_method_info = +{ + -1, + "Quit", + (GDBusArgInfo **) &application_quit_in_args_p, + NULL, + NULL +}; + +static const GDBusMethodInfo application_list_actions_method_info = +{ + -1, + "ListActions", + NULL, + (GDBusArgInfo **) &application_list_actions_out_args_p, + NULL +}; + +static const GDBusMethodInfo application_invoke_action_method_info = +{ + -1, + "InvokeAction", + (GDBusArgInfo **) &application_invoke_action_in_args_p, + NULL, + NULL +}; + +static const GDBusArgInfo application_activate_in_args[] = +{ + { + -1, + "arguments", + "aay", + NULL + }, + { + -1, + "data", + "a{sv}", + NULL + } +}; + +static const GDBusArgInfo * const application_activate_in_args_p[] = { + &application_activate_in_args[0], + &application_activate_in_args[1], + NULL +}; + +static const GDBusMethodInfo application_activate_method_info = +{ + -1, + "Activate", + (GDBusArgInfo **) &application_activate_in_args_p, + NULL, + NULL +}; + +static const GDBusMethodInfo * const application_dbus_method_info_p[] = +{ + &application_quit_method_info, + &application_list_actions_method_info, + &application_invoke_action_method_info, + &application_activate_method_info, + NULL +}; + +static const GDBusSignalInfo application_dbus_signal_info[] = +{ + { + -1, + "ActionsChanged", + NULL, + NULL + } +}; + +static const GDBusSignalInfo * const application_dbus_signal_info_p[] = { + &application_dbus_signal_info[0], + NULL +}; + +static const GDBusInterfaceInfo application_dbus_interface_info = +{ + -1, + G_APPLICATION_IFACE, + (GDBusMethodInfo **) application_dbus_method_info_p, + (GDBusSignalInfo **) application_dbus_signal_info_p, + NULL, +}; + +static GDBusInterfaceVTable application_dbus_vtable = +{ + application_dbus_method_call, + NULL, + NULL +}; + +static char * +application_path_from_appid (const char *appid) +{ + char *appid_path, *iter; + + + appid_path = g_strconcat ("/", appid, NULL); + for (iter = appid_path; *iter; iter++) + { + if (*iter == '.') + *iter = '/'; + } + + return appid_path; +} + +static void +ensure_bus (GApplication *app) +{ + GError *error = NULL; + + if (app->priv->session_bus == NULL) + app->priv->session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (app->priv->session_bus == NULL) + { + g_error ("%s", error->message); + g_error_free (error); + } + + if (app->priv->dbus_path == NULL) + app->priv->dbus_path = application_path_from_appid (app->priv->appid); +} + +static void +_g_application_platform_init (GApplication *app) +{ + GError *error = NULL; + guint registration_id; + + ensure_bus (app); + + registration_id = g_dbus_connection_register_object (app->priv->session_bus, + app->priv->dbus_path, + &application_dbus_interface_info, + &application_dbus_vtable, + app, NULL, + &error); + if (registration_id == 0) + { + g_error ("%s", error->message); + g_error_free (error); + } +} + +static gboolean +_g_application_platform_acquire_single_instance (GApplication *app, + GError **error) +{ + GVariant *request_result; + guint32 request_status; + + ensure_bus (app); + if (app->priv->session_bus == NULL) + return FALSE; + + request_result = g_dbus_connection_call_sync (app->priv->session_bus, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "RequestName", + g_variant_new ("(su)", app->priv->appid, 0x4), + NULL, 0, -1, NULL, error); + + if (request_result == NULL) + return FALSE; + + if (strcmp (g_variant_get_type_string (request_result), "(u)") == 0) + g_variant_get (request_result, "(u)", &request_status); + else + request_status = 0; + + g_variant_unref (request_result); + + if (request_status != 1 && request_status != 4) + { + if (request_status == 3) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Another process has name \"%s\"", app->priv->appid); + else + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown error"); + + return FALSE; + } + + return TRUE; +} + +static void +_g_application_platform_on_actions_changed (GApplication *app) +{ + g_dbus_connection_emit_signal (app->priv->session_bus, NULL, + app->priv->dbus_path, + G_APPLICATION_IFACE, + "ActionsChanged", NULL, NULL); +} + +static void +_g_application_platform_remote_invoke_action (GApplication *app, + const char *action, + guint timestamp) +{ + GVariant *result; + + ensure_bus (app); + + result = g_dbus_connection_call_sync (app->priv->session_bus, + app->priv->appid, + app->priv->dbus_path, + G_APPLICATION_IFACE, + "InvokeAction", + g_variant_new ("(su)", + action, + timestamp), + NULL, 0, -1, NULL, NULL); + if (result) + g_variant_unref (result); +} + +static void +_g_application_platform_remote_quit (GApplication *app, + guint timestamp) +{ + GVariant *result; + + ensure_bus (app); + + result = g_dbus_connection_call_sync (app->priv->session_bus, + app->priv->appid, + app->priv->dbus_path, + G_APPLICATION_IFACE, + "Quit", + g_variant_new ("(u)", + timestamp), + NULL, 0, -1, NULL, NULL); + if (result) + g_variant_unref (result); +} + +static void +_g_application_platform_activate (GApplication *app, + GVariant *data) +{ + GVariant *result; + + ensure_bus (app); + + result = g_dbus_connection_call_sync (app->priv->session_bus, + app->priv->appid, + app->priv->dbus_path, + G_APPLICATION_IFACE, + "Activate", + data, + NULL, 0, -1, NULL, NULL); + + if (result) + g_variant_unref (result); + + exit (0); +} diff --git a/gio/gdbusconnection.h b/gio/gdbusconnection.h index 1a9597c13..b203196a2 100644 --- a/gio/gdbusconnection.h +++ b/gio/gdbusconnection.h @@ -434,7 +434,7 @@ gboolean g_dbus_connection_unregister_subtree (GDBusConnection * @connection: A #GDBusConnection. * @sender_name: The unique bus name of the sender of the signal. * @object_path: The object path that the signal was emitted on. - * @interface_name: The name of the signal. + * @interface_name: The name of the interface. * @signal_name: The name of the signal. * @parameters: A #GVariant tuple with parameters for the signal. * @user_data: User data passed when subscribing to the signal. diff --git a/gio/gdesktopappinfo.c b/gio/gdesktopappinfo.c index ddccf3497..493a3cdc3 100644 --- a/gio/gdesktopappinfo.c +++ b/gio/gdesktopappinfo.c @@ -879,6 +879,7 @@ typedef struct { char *display; char *sn_id; + char *desktop_file; } ChildSetupData; static void @@ -891,6 +892,16 @@ child_setup (gpointer user_data) if (data->sn_id) g_setenv ("DESKTOP_STARTUP_ID", data->sn_id, TRUE); + + if (data->desktop_file) + { + gchar pid[20]; + + g_setenv ("GIO_LAUNCHED_DESKTOP_FILE", data->desktop_file, TRUE); + + g_snprintf (pid, 20, "%d", getpid ()); + g_setenv ("GIO_LAUNCHED_DESKTOP_FILE_PID", pid, TRUE); + } } static gboolean @@ -927,6 +938,7 @@ g_desktop_app_info_launch_uris (GAppInfo *appinfo, data.display = NULL; data.sn_id = NULL; + data.desktop_file = info->filename; if (launch_context) { diff --git a/gio/gio-marshal.list b/gio/gio-marshal.list index 7899b9d69..217017393 100644 --- a/gio/gio-marshal.list +++ b/gio/gio-marshal.list @@ -9,3 +9,6 @@ BOOL:UINT VOID:STRING,STRING,BOXED VOID:BOOL,BOXED VOID:BOXED,BOXED +VOID:INT +VOID:STRING,INT +VOID:BOXED,BOXED @@ -28,6 +28,7 @@ #include <gio/giotypes.h> #include <gio/gappinfo.h> +#include <gio/gapplication.h> #include <gio/gasyncinitable.h> #include <gio/gasyncresult.h> #include <gio/gbufferedinputstream.h> diff --git a/gio/gio.symbols b/gio/gio.symbols index 55ff916ac..a366e226c 100644 --- a/gio/gio.symbols +++ b/gio/gio.symbols @@ -26,6 +26,27 @@ g_vfs_get_local #endif #endif +#if IN_HEADER(__G_APPLICATION_H__) +#if IN_FILE(__G_APPLICATION_C__) +g_application_get_type G_GNUC_CONST +g_application_new +g_application_new_and_register +g_application_get_instance +g_application_get_id +g_application_set_action_enabled +g_application_get_action_enabled +g_application_get_action_description +g_application_add_action +g_application_remove_action +g_application_register_with_data +g_application_invoke_action +g_application_list_actions +g_application_run +g_application_quit +g_application_is_remote +#endif +#endif + #if IN_HEADER(__G_APP_INFO_H__) #if IN_FILE(__G_APP_INFO_C__) g_app_info_get_type G_GNUC_CONST diff --git a/gio/gnullapplication.c b/gio/gnullapplication.c new file mode 100644 index 000000000..6d255e642 --- /dev/null +++ b/gio/gnullapplication.c @@ -0,0 +1,70 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright © 2010 Red Hat, Inc + * + * 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 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, write to the + * Free Software Foundation, Inc., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Authors: Colin Walters <walters@verbum.org> + */ + +#include <string.h> +#include <stdlib.h> + +#include "gioerror.h" + +static void +_g_application_platform_init (GApplication *app) +{ +} + +static gboolean +_g_application_platform_acquire_single_instance (GApplication *app, + GError **error) +{ + return TRUE; +} + +static void +_g_application_platform_on_actions_changed (GApplication *app) +{ +} + +static void +_g_application_platform_remote_invoke_action (GApplication *app, + const char *action, + guint timestamp) +{ +} + +static void +_g_application_platform_remote_quit (GApplication *app, + guint timestamp) +{ +} + +static void +_g_application_platform_default_quit (void) +{ + exit (0); +} + +static void +_g_application_platform_activate (GApplication *app, + GVariant *data) +{ + exit (0); +} + diff --git a/gio/tests/Makefile.am b/gio/tests/Makefile.am index caab30912..8fcdd9cb7 100644 --- a/gio/tests/Makefile.am +++ b/gio/tests/Makefile.am @@ -54,6 +54,9 @@ TEST_PROGS += \ gdbus-error \ gdbus-peer \ gdbus-exit-on-close \ + application \ + testapps \ + appinfo \ $(NULL) endif @@ -73,6 +76,8 @@ SAMPLE_PROGS = \ gdbus-example-subtree \ gdbus-example-peer \ gdbus-example-proxy-subclass \ + testapp \ + appinfo-test \ $(NULL) @@ -248,6 +253,21 @@ gdbus_example_proxy_subclass_LDADD = $(progs_ldadd) gdbus_example_export_SOURCES = gdbus-example-export.c gdbus_example_export_LDADD = $(progs_ldadd) +application_SOURCES = application.c +application_LDADD = $(progs_ldadd) + +appinfo_SOURCES = appinfo.c +appinfo_LDADD = $(progs_ldadd) + +appinfo_test_SOURCES = appinfo-test.c +appinfo_test_LDADD = $(progs_ldadd) + +testapp_SOURCES = testapp.c +testapp_LDADD = $(progs_ldadd) + +testapps_SOURCES = testapps.c +testapps_LDADD = $(progs_ldadd) + EXTRA_DIST += \ socket-common.c \ org.gtk.test.gschema \ diff --git a/gio/tests/appinfo-test.c b/gio/tests/appinfo-test.c new file mode 100644 index 000000000..9d6a5ff3c --- /dev/null +++ b/gio/tests/appinfo-test.c @@ -0,0 +1,20 @@ +#include <stdlib.h> +#include <gio/gio.h> + +int +main (int argc, char *argv[]) +{ + const gchar *envvar; + gint pid_from_env; + + envvar = g_getenv ("GIO_LAUNCHED_DESKTOP_FILE_PID"); + g_assert (envvar != NULL); + pid_from_env = atoi (envvar); + g_assert_cmpint (pid_from_env, ==, getpid ()); + + envvar = g_getenv ("GIO_LAUNCHED_DESKTOP_FILE"); + g_assert_cmpstr (envvar, ==, SRCDIR "/appinfo-test.desktop"); + + return 0; +} + diff --git a/gio/tests/appinfo-test.desktop b/gio/tests/appinfo-test.desktop new file mode 100644 index 000000000..becc1c4a6 --- /dev/null +++ b/gio/tests/appinfo-test.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Type=Application +Name=appinfo-test +Exec=./appinfo-test diff --git a/gio/tests/appinfo.c b/gio/tests/appinfo.c new file mode 100644 index 000000000..099068a1b --- /dev/null +++ b/gio/tests/appinfo.c @@ -0,0 +1,25 @@ +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> + +static void +test_launch (void) +{ + GAppInfo *appinfo; + + appinfo = (GAppInfo*)g_desktop_app_info_new_from_filename (SRCDIR "/appinfo-test.desktop"); + g_assert (appinfo != NULL); + + g_assert (g_app_info_launch (appinfo, NULL, NULL, NULL)); +} + +int +main (int argc, char *argv[]) +{ + g_type_init (); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/appinfo/launch", test_launch); + + return g_test_run (); +} + diff --git a/gio/tests/application.c b/gio/tests/application.c new file mode 100644 index 000000000..5dba027d7 --- /dev/null +++ b/gio/tests/application.c @@ -0,0 +1,134 @@ +#include <stdlib.h> +#include <gio.h> +#include <gstdio.h> + +enum +{ + INVOKE_ACTION, + CHECK_ACTION, + DISABLE_ACTION, + INVOKE_DISABLED_ACTION, + CHECK_DISABLED_ACTION, + END +}; + +static guint timestamp = 0; +static gint state = -1; +static gboolean action_invoked = FALSE; + +static void +on_app_action (GApplication *application, + const gchar *action_name, + guint action_timestamp) +{ + if (g_test_verbose ()) + g_print ("Action '%s' invoked (timestamp: %u, expected: %u)\n", + action_name, + action_timestamp, + timestamp); + + g_assert_cmpstr (action_name, ==, "About"); + g_assert_cmpint (action_timestamp, ==, timestamp); + + action_invoked = TRUE; +} + +static gboolean +check_invoke_action (gpointer data) +{ + GApplication *application = data; + + if (state == INVOKE_ACTION) + { + timestamp = (guint) time (NULL); + + if (g_test_verbose ()) + g_print ("Invoking About...\n"); + + g_application_invoke_action (application, "About", timestamp); + state = CHECK_ACTION; + return TRUE; + } + + if (state == CHECK_ACTION) + { + if (g_test_verbose ()) + g_print ("Verifying About invocation...\n"); + + g_assert (action_invoked); + state = DISABLE_ACTION; + return TRUE; + } + + if (state == DISABLE_ACTION) + { + if (g_test_verbose ()) + g_print ("Disabling About...\n"); + + g_application_set_action_enabled (application, "About", FALSE); + action_invoked = FALSE; + state = INVOKE_DISABLED_ACTION; + return TRUE; + } + + if (state == INVOKE_DISABLED_ACTION) + { + if (g_test_verbose ()) + g_print ("Invoking disabled About action...\n"); + + g_application_invoke_action (application, "About", (guint) time (NULL)); + state = CHECK_DISABLED_ACTION; + return TRUE; + } + + if (state == CHECK_DISABLED_ACTION) + { + if (g_test_verbose ()) + g_print ("Verifying lack of About invocation...\n"); + + g_assert (!action_invoked); + state = END; + return TRUE; + } + + if (state == END) + { + if (g_test_verbose ()) + g_print ("Test complete\n"); + + g_application_quit (application, (guint) time (NULL)); + return FALSE; + } + + g_assert_not_reached (); +} + +static void +test_basic (void) +{ + GApplication *app; + + app = g_application_new_and_register ("org.gtk.TestApplication", 0, NULL); + g_application_add_action (app, "About", "Print an about message"); + + g_signal_connect (app, "action::About", G_CALLBACK (on_app_action), NULL); + + state = INVOKE_ACTION; + g_timeout_add (100, check_invoke_action, app); + + g_application_run (app); + + g_assert (state == END); + g_object_unref (app); +} + +int +main (int argc, char *argv[]) +{ + g_type_init (); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/application/basic", test_basic); + + return g_test_run (); +} diff --git a/gio/tests/testapp.c b/gio/tests/testapp.c new file mode 100644 index 000000000..8ed309185 --- /dev/null +++ b/gio/tests/testapp.c @@ -0,0 +1,79 @@ +#include <stdlib.h> +#include <gio.h> +#include <gstdio.h> +#include <string.h> + +static gboolean action3_added = FALSE; + +static void +on_app_action (GApplication *application, + const gchar *action_name, + guint action_timestamp) +{ + if (strcmp (action_name, "action1") == 0) + exit (1); + else if (strcmp (action_name, "action2") == 0) + { + if (action3_added) + g_application_remove_action (application, "action3"); + else + g_application_add_action (application, "action3", "An extra action"); + action3_added = !action3_added; + } +} + +static gboolean +invoke_action1 (gpointer data) +{ + GApplication *app = data; + + g_application_invoke_action (app, "action1", 0); + + return FALSE; +} + +static void +on_app_activated (GApplication *application, + GVariant *args, + GVariant *platform_data) +{ + char *str; + + g_print ("got args: "); + str = g_variant_print (args, TRUE); + g_print ("%s ", str); + g_free (str); + str = g_variant_print (platform_data, TRUE); + g_print ("%s\n", str); + g_free (str); +} + +int +main (int argc, char *argv[]) +{ + GApplication *app; + GMainLoop *loop; + + app = g_application_new ("org.gtk.test.app"); + if (!(argc > 1 && strcmp (argv[1], "--non-unique") == 0)) + g_application_register_with_data (app, argc, argv, NULL); + + if (g_application_is_remote (app)) + { + g_timeout_add (1000, invoke_action1, app); + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + } + else + { + g_application_add_action (app, "action1", "Action1"); + g_application_add_action (app, "action2", "Action2"); + g_signal_connect (app, "action", + G_CALLBACK (on_app_action), NULL); + g_signal_connect (app, "prepare-activation", + G_CALLBACK (on_app_activated), NULL); + g_application_run (app); + } + + return 0; +} diff --git a/gio/tests/testapps.c b/gio/tests/testapps.c new file mode 100644 index 000000000..51db0b879 --- /dev/null +++ b/gio/tests/testapps.c @@ -0,0 +1,533 @@ +#include <gio/gio.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <signal.h> + +static gint appeared; +static gint disappeared; +static gint changed; +static gboolean died; +static gboolean timed_out; +GPid pid; + +static void +name_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + appeared++; + + if (loop) + g_main_loop_quit (loop); +} + +static void +name_disappeared (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + disappeared++; + + if (loop) + g_main_loop_quit (loop); +} + +static gboolean +start_application (gpointer data) +{ + gchar *argv[] = { "./testapp", NULL }; + + g_assert (g_spawn_async (NULL, argv, NULL, 0, NULL, NULL, &pid, NULL)); + + return FALSE; +} + +static gboolean +run_application_sync (gpointer data) +{ + GMainLoop *loop = data; + + g_assert (g_spawn_command_line_sync ("./testapp", NULL, NULL, NULL, NULL)); + + if (loop) + g_main_loop_quit (loop); + + return FALSE; +} + +static gboolean +timeout (gpointer data) +{ + GMainLoop *loop = data; + + timed_out = TRUE; + + g_main_loop_quit (loop); + + return TRUE; +} + +/* This test starts an application, checks that its name appears + * on the bus, then starts it again and checks that the second + * instance exits right away. + */ +static void +test_unique (void) +{ + GMainLoop *loop; + gint watch; + guint id1, id2, id3; + + appeared = 0; + timed_out = FALSE; + + loop = g_main_loop_new (NULL, FALSE); + id1 = g_timeout_add (5000, timeout, loop); + + watch = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gtk.test.app", + 0, + name_appeared, + NULL, + loop, + NULL); + + id2 = g_timeout_add (0, start_application, loop); + + g_main_loop_run (loop); + + g_assert_cmpint (appeared, ==, 1); + + id3 = g_timeout_add (0, run_application_sync, loop); + + g_main_loop_run (loop); + + g_assert_cmpint (appeared, ==, 1); + g_assert_cmpint (timed_out, ==, FALSE); + + g_bus_unwatch_name (watch); + + kill (pid, SIGTERM); + + g_main_loop_unref (loop); + g_source_remove (id1); + g_source_remove (id2); + g_source_remove (id3); +} + +static gboolean +quit_app (gpointer data) +{ + GDBusConnection *connection; + GVariant *res; + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + res = g_dbus_connection_call_sync (connection, + "org.gtk.test.app", + "/org/gtk/test/app", + "org.gtk.Application", + "Quit", + g_variant_new ("(u)", 0), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL); + if (res) + g_variant_unref (res); + + return FALSE; +} + +static void +child_is_dead (GPid pid, + gint status, + gpointer data) +{ + GMainLoop *loop = data; + + died++; + + g_assert (WIFEXITED (status) && WEXITSTATUS(status) == 0); + + if (loop) + g_main_loop_quit (loop); +} + +/* This test start an application, checks that its name appears on + * the bus, then calls Quit, and verifies that the name disappears + * and the application exits. + */ +static void +test_quit (void) +{ + GMainLoop *loop; + gint watch; + guint id1, id2, id3; + gchar *argv[] = { "./testapp", NULL }; + + appeared = 0; + disappeared = 0; + died = FALSE; + timed_out = FALSE; + + loop = g_main_loop_new (NULL, FALSE); + watch = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gtk.test.app", + 0, + name_appeared, + name_disappeared, + NULL, + NULL); + + g_assert (g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid, NULL)); + + id1 = g_child_watch_add (pid, child_is_dead, loop); + + id2 = g_timeout_add (500, quit_app, NULL); + + id3 = g_timeout_add (5000, timeout, loop); + + g_main_loop_run (loop); + g_assert_cmpint (timed_out, ==, FALSE); + g_assert_cmpint (appeared, ==, 1); + g_assert_cmpint (disappeared, >=, 1); + g_assert_cmpint (died, ==, TRUE); + + g_bus_unwatch_name (watch); + + g_main_loop_unref (loop); + g_source_remove (id1); + g_source_remove (id2); + g_source_remove (id3); +} + +static gboolean +_g_strv_has_string (const gchar* const * haystack, + const gchar *needle) +{ + guint n; + + for (n = 0; haystack != NULL && haystack[n] != NULL; n++) + { + if (g_strcmp0 (haystack[n], needle) == 0) + return TRUE; + } + return FALSE; +} + +static gchar ** +list_actions (void) +{ + GDBusConnection *connection; + GVariant *res; + gchar **strv; + gchar *str; + GVariantIter *iter; + gint i; + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + res = g_dbus_connection_call_sync (connection, + "org.gtk.test.app", + "/org/gtk/test/app", + "org.gtk.Application", + "ListActions", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL); + + strv = g_new0 (gchar *, 32); + g_variant_get (res, "(a{s(sb)})", &iter); + i = 0; + while (g_variant_iter_loop (iter, "{s(sb)}", &str, NULL, NULL)) + { + strv[i] = g_strdup (str); + i++; + g_assert (i < 32); + } + g_variant_iter_free (iter); + + strv[i] = NULL; + + g_variant_unref (res); + g_object_unref (connection); + + return strv; +} + +/* This test start an application, waits for its name to appear on + * the bus, then calls ListActions, and verifies that it gets the expected + * actions back. + */ +static void +test_list_actions (void) +{ + GMainLoop *loop; + gchar *argv[] = { "./testapp", NULL }; + gchar **actions; + gint watch; + + appeared = 0; + + loop = g_main_loop_new (NULL, FALSE); + watch = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gtk.test.app", + 0, + name_appeared, + NULL, + loop, + NULL); + + g_assert (g_spawn_async (NULL, argv, NULL, 0, NULL, NULL, &pid, NULL)); + if (!appeared) + g_main_loop_run (loop); + g_main_loop_unref (loop); + + actions = list_actions (); + + g_assert (g_strv_length (actions) == 2); + g_assert (_g_strv_has_string ((const char *const *)actions, "action1")); + g_assert (_g_strv_has_string ((const char *const *)actions, "action2")); + + g_strfreev (actions); + + kill (pid, SIGTERM); + + g_bus_unwatch_name (watch); +} + +static gboolean +invoke_action (gpointer data) +{ + const gchar *action = data; + GDBusConnection *connection; + GVariant *res; + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + res = g_dbus_connection_call_sync (connection, + "org.gtk.test.app", + "/org/gtk/test/app", + "org.gtk.Application", + "InvokeAction", + g_variant_new ("(su)", + action, + 0), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL); + if (res) + g_variant_unref (res); + g_object_unref (connection); + + return FALSE; +} + +static void +exit_with_code_1 (GPid pid, + gint status, + gpointer data) +{ + GMainLoop *loop = data; + + died++; + + g_assert (WIFEXITED (status) && WEXITSTATUS(status) == 1); + + if (loop) + g_main_loop_quit (loop); +} + +/* This test starts an application, waits for it to appear, + * then invokes 'action1' and checks that it causes the application + * to exit with an exit code of 1. + */ +static void +test_invoke (void) +{ + GMainLoop *loop; + gint watch; + gchar *argv[] = { "./testapp", NULL }; + guint id1, id2, id3; + + appeared = 0; + disappeared = 0; + died = FALSE; + timed_out = FALSE; + + loop = g_main_loop_new (NULL, FALSE); + watch = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gtk.test.app", + 0, + name_appeared, + name_disappeared, + NULL, + NULL); + + g_assert (g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid, NULL)); + + id1 = g_child_watch_add (pid, exit_with_code_1, loop); + + id2 = g_timeout_add (500, invoke_action, "action1"); + + id3 = g_timeout_add (5000, timeout, loop); + + g_main_loop_run (loop); + g_assert_cmpint (timed_out, ==, FALSE); + g_assert_cmpint (appeared, >=, 1); + g_assert_cmpint (disappeared, >=, 1); + g_assert_cmpint (died, ==, TRUE); + + g_bus_unwatch_name (watch); + g_main_loop_unref (loop); + g_source_remove (id1); + g_source_remove (id2); + g_source_remove (id3); + + kill (pid, SIGTERM); +} + +static void +test_remote (void) +{ + GMainLoop *loop; + gint watch; + GPid pid1, pid2; + gchar *argv[] = { "./testapp", NULL, NULL }; + + appeared = 0; + timed_out = FALSE; + + loop = g_main_loop_new (NULL, FALSE); + g_timeout_add (5000, timeout, loop); + + watch = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gtk.test.app", + 0, + name_appeared, + NULL, + loop, + NULL); + + g_assert (g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid1, NULL)); + g_child_watch_add (pid1, exit_with_code_1, loop); + + g_main_loop_run (loop); + + g_assert_cmpint (appeared, ==, 1); + + argv[1] = "--non-unique"; + g_assert (g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid2, NULL)); + + g_main_loop_run (loop); + + g_assert_cmpint (appeared, ==, 1); + g_assert_cmpint (timed_out, ==, FALSE); + + g_main_loop_unref (loop); + g_bus_unwatch_name (watch); + + kill (pid1, SIGTERM); + kill (pid2, SIGTERM); +} + +static void +actions_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GMainLoop *loop = user_data; + + g_assert_cmpstr (interface_name, ==, "org.gtk.Application"); + g_assert_cmpstr (signal_name, ==, "ActionsChanged"); + + changed++; + + g_main_loop_quit (loop); +} + +static void +test_change_action (void) +{ + GMainLoop *loop; + gint watch; + guint id; + GPid pid1; + gchar *argv[] = { "./testapp", NULL, NULL }; + GDBusConnection *connection; + + appeared = 0; + changed = 0; + timed_out = FALSE; + + loop = g_main_loop_new (NULL, FALSE); + g_timeout_add (5000, timeout, loop); + + watch = g_bus_watch_name (G_BUS_TYPE_SESSION, + "org.gtk.test.app", + 0, + name_appeared, + NULL, + loop, + NULL); + + g_assert (g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid1, NULL)); + g_main_loop_run (loop); + + g_assert_cmpint (appeared, ==, 1); + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + id = g_dbus_connection_signal_subscribe (connection, + NULL, + "org.gtk.Application", + "ActionsChanged", + "/org/gtk/test/app", + NULL, + actions_changed, + loop, + NULL); + + g_timeout_add (1000, invoke_action, "action2"); + + g_main_loop_run (loop); + + g_assert_cmpint (changed, >, 0); + g_assert_cmpint (timed_out, ==, FALSE); + + g_dbus_connection_signal_unsubscribe (connection, id); + g_object_unref (connection); + g_main_loop_unref (loop); + g_bus_unwatch_name (watch); + + kill (pid1, SIGTERM); +} + +int +main (int argc, char *argv[]) +{ + g_type_init (); + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/application/unique", test_unique); + g_test_add_func ("/application/quit", test_quit); + g_test_add_func ("/application/list-actions", test_list_actions); + g_test_add_func ("/application/invoke", test_invoke); + g_test_add_func ("/application/remote", test_remote); + g_test_add_func ("/application/change-action", test_change_action); + + return g_test_run (); +} + |