diff --git a/data/dbus-interfaces/org.gnome.Mutter.ServiceChannel.xml b/data/dbus-interfaces/org.gnome.Mutter.ServiceChannel.xml index 97c3b872b1fb0f0221554c3ac4edd2b2c7cba3fa..12329cb0999484f62eaf07c5a36a0e0be960c7a7 100644 --- a/data/dbus-interfaces/org.gnome.Mutter.ServiceChannel.xml +++ b/data/dbus-interfaces/org.gnome.Mutter.ServiceChannel.xml @@ -17,6 +17,28 @@ + + + + + + + diff --git a/doc/man/gnome-service-client.rst b/doc/man/gnome-service-client.rst new file mode 100644 index 0000000000000000000000000000000000000000..60311b083c349c3e458c0c6c2ec2e9eef5df0675 --- /dev/null +++ b/doc/man/gnome-service-client.rst @@ -0,0 +1,61 @@ +=================== +gnome-service-client +=================== + +------------------- +GNOME Service Client +------------------- + +:Manual section: 1 +:Manual group: User Commands + +SYNOPSIS +-------- + +gnome-service-client [option ...] -- COMMAND + +DESCRIPTION +----------- +gnome-service-client provides a way to spawn a Wayland client and optionally set +a default window tag for all its windows. + +It requires a compositor that supports the ``org.gnome.Mutter.ServiceChannel`` +D-Bus API, such as GNOME Shell or GNOME Kiosk. + +OPTIONS +------- +``--help``, ``-h`` + + Show a help message and exit. + +``--tag``, ``-t`` + + Optionally specifies the tag to set for all windows of the client. + +EXAMPLES +-------- + +Start the client "gnome-calculator" without any tag + +:: + + gnome-service-client -- gnome-calculator + +Start the client "gnome-tour" with the tag "demo" + +:: + + gnome-service-client -t demo -- gnome-tour + +BUGS +---- +The bug tracker can be reached by visiting the website +https://gitlab.gnome.org/GNOME/mutter/-/issues. +Before sending a bug report, please verify that you have the latest version +of gnome-shell. Many bugs (major and minor) are fixed at each release, and +if yours is out of date, the problem may already have been solved. + +ADDITIONAL INFORMATION +---------------------- +For further information, visit the website +https://gitlab.gnome.org/GNOME/mutter/-/blob/main/README.md. diff --git a/doc/man/meson.build b/doc/man/meson.build index 38649eeb9234fc057444639688976156caa4b0ee..df1c9e473c1f6df2f42ad6d8f9f0cd0ecf2fa7f5 100644 --- a/doc/man/meson.build +++ b/doc/man/meson.build @@ -10,3 +10,12 @@ custom_target('gdctl.1', install_dir: mandir + '/man1', install: true ) + +custom_target('gnome-service-client.1', + input: 'gnome-service-client.rst', + output: 'gnome-service-client.1', + command: [rst2man, '--syntax-highlight=none', '@INPUT@'], + capture: true, + install_dir: mandir + '/man1', + install: true +) diff --git a/src/core/meta-service-channel.c b/src/core/meta-service-channel.c index ccd4acf09143783d785b9744caa07c166043fa22..9b87d1eaa2c8b996028c0e898a30a33721a0fcef 100644 --- a/src/core/meta-service-channel.c +++ b/src/core/meta-service-channel.c @@ -18,6 +18,9 @@ #include "config.h" +#include +#include + #include "core/meta-service-channel.h" #include "wayland/meta-wayland-client-private.h" @@ -103,6 +106,27 @@ verify_service_client_type (uint32_t service_client_type) return FALSE; } +static MetaWaylandClient * +setup_wayland_client_with_fd (MetaContext *context, + GUnixFDList *fd_list, + int *fd_id, + GError **error) +{ + g_autoptr (MetaWaylandClient) wayland_client = NULL; + g_autofd int fd = -1; + + wayland_client = meta_wayland_client_new_create (context, error); + if (!wayland_client) + return NULL; + + fd = meta_wayland_client_take_client_fd (wayland_client); + *fd_id = g_unix_fd_list_append (fd_list, fd, error); + if (*fd_id == -1) + return NULL; + + return g_steal_pointer (&wayland_client); +} + static gboolean handle_open_wayland_service_connection (MetaDBusServiceChannel *object, GDBusMethodInvocation *invocation, @@ -114,7 +138,6 @@ handle_open_wayland_service_connection (MetaDBusServiceChannel *object, g_autoptr (GError) error = NULL; g_autoptr (MetaWaylandClient) wayland_client = NULL; g_autoptr (GUnixFDList) out_fd_list = NULL; - int fd; int fd_id; if (meta_context_get_compositor_type (service_channel->context) != @@ -136,8 +159,11 @@ handle_open_wayland_service_connection (MetaDBusServiceChannel *object, return G_DBUS_METHOD_INVOCATION_HANDLED; } - wayland_client = meta_wayland_client_new_create (service_channel->context, - &error); + out_fd_list = g_unix_fd_list_new (); + wayland_client = setup_wayland_client_with_fd (service_channel->context, + out_fd_list, + &fd_id, + &error); if (!wayland_client) { g_dbus_method_invocation_return_error (invocation, @@ -151,36 +177,80 @@ handle_open_wayland_service_connection (MetaDBusServiceChannel *object, meta_wayland_client_set_caps (wayland_client, META_WAYLAND_CLIENT_CAPS_X11_INTEROP); - fd = meta_wayland_client_take_client_fd (wayland_client); - out_fd_list = g_unix_fd_list_new (); - fd_id = g_unix_fd_list_append (out_fd_list, fd, &error); - close (fd); + g_hash_table_replace (service_channel->service_clients, + GUINT_TO_POINTER (service_client_type), + meta_service_client_new (service_channel, + wayland_client, + service_client_type)); - if (fd_id == -1) + meta_dbus_service_channel_complete_open_wayland_service_connection ( + object, invocation, out_fd_list, g_variant_new_handle (fd_id)); + return G_DBUS_METHOD_INVOCATION_HANDLED; +#else /* HAVE_WAYLAND */ + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_NOT_SUPPORTED, + "Wayland not supported"); + return G_DBUS_METHOD_INVOCATION_HANDLED; +#endif /* HAVE_WAYLAND */ +} + +static gboolean +handle_open_wayland_connection (MetaDBusServiceChannel *object, + GDBusMethodInvocation *invocation, + GUnixFDList *in_fd_list, + GVariant *arg_options) +{ +#ifdef HAVE_WAYLAND + MetaServiceChannel *service_channel = META_SERVICE_CHANNEL (object); + g_autoptr (GError) error = NULL; + g_autoptr (MetaWaylandClient) wayland_client = NULL; + g_autoptr (GUnixFDList) out_fd_list = NULL; + g_autoptr (GVariant) window_tag_variant = NULL; + int fd_id; + + if (meta_context_get_compositor_type (service_channel->context) != + META_COMPOSITOR_TYPE_WAYLAND) + { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_NOT_SUPPORTED, + "Not a Wayland compositor"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + out_fd_list = g_unix_fd_list_new (); + wayland_client = setup_wayland_client_with_fd (service_channel->context, + out_fd_list, + &fd_id, + &error); + if (!wayland_client) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, - G_DBUS_ERROR_ACCESS_DENIED, - "Failed to append fd: %s", + G_DBUS_ERROR_NOT_SUPPORTED, + "Failed to create Wayland client: %s", error->message); return G_DBUS_METHOD_INVOCATION_HANDLED; } - g_hash_table_replace (service_channel->service_clients, - GUINT_TO_POINTER (service_client_type), - meta_service_client_new (service_channel, - wayland_client, - service_client_type)); + window_tag_variant = g_variant_lookup_value (arg_options, + "window-tag", + G_VARIANT_TYPE_STRING); + if (window_tag_variant) + { + const char *window_tag = g_variant_get_string (window_tag_variant, NULL); + meta_wayland_client_set_window_tag (wayland_client, window_tag); + } - meta_dbus_service_channel_complete_open_wayland_service_connection ( + meta_dbus_service_channel_complete_open_wayland_connection ( object, invocation, out_fd_list, g_variant_new_handle (fd_id)); return G_DBUS_METHOD_INVOCATION_HANDLED; #else /* HAVE_WAYLAND */ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, - "Wayland not supported", - error->message); + "Wayland not supported"); return G_DBUS_METHOD_INVOCATION_HANDLED; #endif /* HAVE_WAYLAND */ } @@ -190,6 +260,8 @@ meta_service_channel_init_iface (MetaDBusServiceChannelIface *iface) { iface->handle_open_wayland_service_connection = handle_open_wayland_service_connection; + iface->handle_open_wayland_connection = + handle_open_wayland_connection; } static void diff --git a/src/tests/meson.build b/src/tests/meson.build index 2393434ee01c8ba69983f9c0bb6865a80ebffeaf..0e7c6b91aa81758cde3e7d7cdb5e513895199fc6 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -1032,6 +1032,7 @@ test_cases += [ 'suite': 'wayland', 'sources': [ 'service-channel-tests.c', + wayland_test_client_utils, wayland_test_utils, ], 'depends': [ diff --git a/src/tests/service-channel-tests.c b/src/tests/service-channel-tests.c index 6c3a899e0bcfacc5992cde86c768ef11f8559ca6..e861876ecd41d4b0bebf8d58d4029197112b4ade 100644 --- a/src/tests/service-channel-tests.c +++ b/src/tests/service-channel-tests.c @@ -27,6 +27,12 @@ #include "wayland/meta-wayland.h" #include "wayland/meta-wayland-client-private.h" #include "wayland/meta-wayland-surface-private.h" +#include "tests/wayland-test-clients/wayland-test-client-utils.h" + +#include +#include +#include +#include static MetaContext *test_context; static MetaWaylandTestDriver *test_driver; @@ -63,6 +69,135 @@ meta_test_service_channel_wayland (void) meta_wayland_test_client_finish (wayland_test_client); } +typedef struct +{ + const char *test_tag; + gboolean client_terminated; + GDBusConnection *connection; +} ServiceClientTestdata; + +static gpointer +service_client_thread_func (gpointer user_data) +{ + ServiceClientTestdata *testdata = user_data; + g_autoptr (GMainContext) thread_main_context = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (GDBusProxy) service_channel_proxy = NULL; + g_autoptr (GVariant) result = NULL; + g_autoptr (GVariant) fd_variant = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + g_autoptr (WaylandDisplay) display = NULL; + g_autoptr (WaylandSurface) surface = NULL; + g_auto(GVariantBuilder) options_builder = + G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); + g_autofd int fd = -1; + struct wl_display *wayland_display; + + thread_main_context = g_main_context_new (); + g_main_context_push_thread_default (thread_main_context); + + service_channel_proxy = + g_dbus_proxy_new_sync (testdata->connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.gnome.Mutter.ServiceChannel", + "/org/gnome/Mutter/ServiceChannel", + "org.gnome.Mutter.ServiceChannel", + NULL, + &error); + g_assert_no_error (error); + g_assert_nonnull (service_channel_proxy); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&options_builder, "{sv}", + "window-tag", g_variant_new_string (testdata->test_tag)); + + result = + g_dbus_proxy_call_with_unix_fd_list_sync (service_channel_proxy, + "OpenWaylandConnection", + g_variant_new ("(a{sv})", + &options_builder), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + &fd_list, + NULL, + &error); + g_assert_no_error (error); + g_assert_nonnull (result); + g_assert_nonnull (fd_list); + + /* Extract the file descriptor */ + g_variant_get (result, "(@h)", &fd_variant); + fd = g_unix_fd_list_get (fd_list, g_variant_get_handle (fd_variant), &error); + g_assert_no_error (error); + g_assert_cmpint (fd, >=, 0); + + /* Test that we can connect to the Wayland display */ + wayland_display = wl_display_connect_to_fd (fd); + g_assert_nonnull (wayland_display); + + display = wayland_display_new_full (WAYLAND_DISPLAY_CAPABILITY_TEST_DRIVER, + wayland_display); + g_assert_nonnull (display); + + surface = wayland_surface_new (display, "test-tagged-window", + 100, 100, 0xffabcdff); + g_assert_nonnull (surface); + + wl_surface_commit (surface->wl_surface); + wait_for_sync_event (display, 0); + g_object_unref (display); + + g_atomic_int_set (&testdata->client_terminated, TRUE); + + return NULL; +} + +static void +meta_test_service_channel_open_wayland_connection (void) +{ + ServiceClientTestdata testdata = {}; + g_autoptr (GError) error = NULL; + g_autoptr (GDBusConnection) connection = NULL; + g_autoptr (GThread) thread = NULL; + MetaWindow *window; + const char *applied_tag; + + /* Connect to the session bus to call the service channel */ + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (connection); + + testdata = (ServiceClientTestdata) { + .test_tag = "test-window-tag", + .client_terminated = FALSE, + .connection = connection, + }; + + thread = g_thread_new ("service-client-thread", + service_client_thread_func, + &testdata); + + /* Wait for the window to be created by mutter */ + window = meta_wait_for_client_window (test_context, "test-tagged-window"); + g_assert_nonnull (window); + + /* Check that the window tag was correctly applied */ + applied_tag = meta_window_get_tag (window); + g_assert_nonnull (applied_tag); + g_assert_cmpstr (applied_tag, ==, testdata.test_tag); + + meta_wayland_test_driver_emit_sync_event (test_driver, 0); + + g_debug ("Waiting for client to disconnect"); + while (!g_atomic_int_get (&testdata.client_terminated)) + g_main_context_iteration (NULL, TRUE); + + g_debug ("Waiting for thread to terminate"); + g_thread_join (thread); +} + static void on_before_tests (void) { @@ -86,6 +221,8 @@ init_tests (void) { g_test_add_func ("/service-channel/wayland", meta_test_service_channel_wayland); + g_test_add_func ("/service-channel/open-wayland-connection", + meta_test_service_channel_open_wayland_connection); } int diff --git a/src/wayland/meta-wayland-client-private.h b/src/wayland/meta-wayland-client-private.h index 01fe0597cd432de6c4e4b6fbfa748dab14971b6f..d30beda09f8e1aee83e7d0db66d1da199854a281 100644 --- a/src/wayland/meta-wayland-client-private.h +++ b/src/wayland/meta-wayland-client-private.h @@ -76,3 +76,8 @@ int meta_wayland_client_take_client_fd (MetaWaylandClient *client); META_EXPORT_TEST MetaWaylandClient * meta_get_wayland_client (const struct wl_client *wl_client); + +void meta_wayland_client_set_window_tag (MetaWaylandClient *client, + const char *window_tag); + +const char * meta_wayland_client_get_window_tag (MetaWaylandClient *client); \ No newline at end of file diff --git a/src/wayland/meta-wayland-client.c b/src/wayland/meta-wayland-client.c index 6e32eef205ebc59e4aa3c8a2add99e3fd8047f57..7e875512c60733baf131c17266a21b5f7f4ace60 100644 --- a/src/wayland/meta-wayland-client.c +++ b/src/wayland/meta-wayland-client.c @@ -71,6 +71,8 @@ struct _MetaWaylandClient struct { GSubprocess *subprocess; } subprocess; + + char *window_tag; }; G_DEFINE_TYPE (MetaWaylandClient, meta_wayland_client, G_TYPE_OBJECT) @@ -82,6 +84,7 @@ meta_wayland_client_finalize (GObject *object) g_clear_fd (&client->created.client_fd, NULL); g_clear_object (&client->subprocess.subprocess); + g_clear_pointer (&client->window_tag, g_free); G_OBJECT_CLASS (meta_wayland_client_parent_class)->finalize (object); } @@ -331,7 +334,7 @@ meta_wayland_client_get_subprocess (MetaWaylandClient *client) } /** - * meta_wayland_client_owns_wayland_window + * meta_wayland_client_owns_window * @client: a #MetaWaylandClient * @window: (not nullable): a MetaWindow * @@ -361,3 +364,16 @@ meta_get_wayland_client (const struct wl_client *wl_client) { return wl_client_get_user_data ((struct wl_client *) wl_client); } + +void +meta_wayland_client_set_window_tag (MetaWaylandClient *client, + const char *window_tag) +{ + g_set_str (&client->window_tag, window_tag); +} + +const char * +meta_wayland_client_get_window_tag (MetaWaylandClient *client) +{ + return client->window_tag; +} diff --git a/src/wayland/meta-window-wayland.c b/src/wayland/meta-window-wayland.c index 5d47d4a986b1c200c8211792764187305d5b51dc..15200fab9a42235ad358b739695d64cddfe44e0a 100644 --- a/src/wayland/meta-window-wayland.c +++ b/src/wayland/meta-window-wayland.c @@ -37,6 +37,7 @@ #include "core/stack-tracker.h" #include "core/window-private.h" #include "wayland/meta-wayland-actor-surface.h" +#include "wayland/meta-wayland-client-private.h" #include "wayland/meta-wayland-private.h" #include "wayland/meta-wayland-surface-private.h" #include "wayland/meta-wayland-toplevel-drag.h" @@ -1059,6 +1060,23 @@ meta_window_wayland_class_init (MetaWindowWaylandClass *klass) g_object_class_install_properties (object_class, PROP_LAST, obj_props); } +static void +meta_window_wayland_maybe_apply_custom_tag (MetaWindow *window) +{ + MetaWindowWayland *wl_window = META_WINDOW_WAYLAND (window); + MetaWaylandSurface *surface = wl_window->surface; + MetaWaylandClient *wayland_client; + struct wl_client *wl_client; + const char *window_tag; + + wl_client = wl_resource_get_client (surface->resource); + wayland_client = meta_get_wayland_client (wl_client); + + window_tag = meta_wayland_client_get_window_tag (wayland_client); + if (window_tag) + meta_window_set_tag (window, window_tag); +} + MetaWindow * meta_window_wayland_new (MetaDisplay *display, MetaWaylandSurface *surface) @@ -1074,6 +1092,7 @@ meta_window_wayland_new (MetaDisplay *display, NULL); wl_window = META_WINDOW_WAYLAND (window); set_geometry_scale_for_window (wl_window, wl_window->geometry_scale); + meta_window_wayland_maybe_apply_custom_tag (window); return window; } diff --git a/tools/gnome-service-client b/tools/gnome-service-client new file mode 100755 index 0000000000000000000000000000000000000000..037ddbc9788111a4fe619bc78993f1eb5e78bd57 --- /dev/null +++ b/tools/gnome-service-client @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 + +""" +Launch a Wayland client with an optional tag using Mutter's ServiceChannel. + +This script connects to the Mutter ServiceChannel D-Bus service to create +a Wayland connection and runs the specified command on it. +""" + +import argparse +import sys +import re +import subprocess +from contextlib import contextmanager +from os import environ +from typing import List, Iterator, Optional + +import dbus +import dbus.exceptions + +# Service channel constants +SERVICE_NAME = "org.gnome.Mutter.ServiceChannel" +SERVICE_INTERFACE = "org.gnome.Mutter.ServiceChannel" +SERVICE_OBJECT_PATH = "/org/gnome/Mutter/ServiceChannel" + +# Exit codes +EXIT_SUCCESS = 0 +EXIT_FAILURE = 1 +EXIT_DBUS_ERROR = 2 +EXIT_MISSING_ARGS = 3 + + +def get_service_channel() -> tuple[dbus.Interface, dbus.Bus]: + """ + Get the Mutter ServiceChannel D-Bus interface and own the sender name. + + Returns: + Tuple of (D-Bus interface for the service channel, D-Bus bus) + + Raises: + SystemExit: If the service is not available + """ + try: + bus = dbus.SessionBus() + service_channel = bus.get_object(SERVICE_NAME, SERVICE_OBJECT_PATH) + return dbus.Interface(service_channel, dbus_interface=SERVICE_INTERFACE), bus + except dbus.exceptions.DBusException as error: + print(f"Error: Unable to connect to Mutter ServiceChannel: {error}", + file=sys.stderr) + print("Make sure you're running under a compatible Wayland compositor " + "(GNOME Shell, GNOME Kiosk, etc.)", file=sys.stderr) + sys.exit(EXIT_DBUS_ERROR) + + +def get_wayland_connection_fd(service_channel: dbus.Interface, tag: Optional[str]) -> dbus.types.UnixFd: + """ + Request a new Wayland connection file descriptor with an optional window tag. + + Args: + service_channel: The D-Bus service channel interface + tag: The optional tag to associate with the Wayland client + + Returns: + File descriptor for the Wayland connection + + Raises: + SystemExit: If the service call fails + """ + + try: + options = {} + if tag: + options['window-tag'] = tag + fd = service_channel.OpenWaylandConnection(options) + return fd + except dbus.exceptions.DBusException as error: + print(f"Error: Failed to create Wayland connection: {error}", file=sys.stderr) + sys.exit(EXIT_DBUS_ERROR) + + +@contextmanager +def wayland_socket_fd(fd: dbus.types.UnixFd) -> Iterator[int]: + """ + Context manager for handling the Wayland socket file descriptor. + + Args: + fd: The Unix file descriptor from D-Bus + + Yields: + The file descriptor number as an integer + """ + fd_num = fd.take() + try: + yield fd_num + finally: + try: + import os + os.close(fd_num) + except OSError: + # fd might already be closed by subprocess + pass + + +def run_command_with_wayland_socket(command: List[str], fd_num: int) -> int: + """ + Run the specified command with the Wayland socket. + + Args: + command: Command and arguments to execute + fd_num: File descriptor number for the Wayland socket + + Returns: + The exit code from the executed command + """ + # Set up environment for the subprocess + env = environ.copy() + env["WAYLAND_SOCKET"] = str(fd_num) + + try: + with subprocess.Popen(command, env=env, pass_fds=[fd_num]) as proc: + return proc.wait() + except FileNotFoundError: + print(f"Error: Command not found: {command[0]}", file=sys.stderr) + return EXIT_FAILURE + except PermissionError: + print(f"Error: Permission denied executing: {command[0]}", file=sys.stderr) + return EXIT_FAILURE + except KeyboardInterrupt: + # Handle Ctrl+C gracefully + print("\nInterrupted by user", file=sys.stderr) + return EXIT_SUCCESS + except Exception as error: + print(f"Error: Failed to execute command: {error}", file=sys.stderr) + return EXIT_FAILURE + + +def parse_arguments() -> argparse.Namespace: + """ + Parse command line arguments. + + Returns: + Parsed arguments namespace + """ + parser = argparse.ArgumentParser( + description='Launch a Wayland client with an optional tag', + epilog='Example: %(prog)s -t demo -- gnome-tour\n' + ' %(prog)s -- gnome-calculator' + ) + + parser.add_argument( + '-t', '--tag', + required=False, + help='Optional tag to associate with the Wayland client' + ) + + parser.add_argument( + 'command', + nargs='+', + help='Command to run and its arguments' + ) + + return parser.parse_args() + + +def main() -> None: + """Main entry point.""" + try: + args = parse_arguments() + except SystemExit as e: + # argparse handles --help and invalid args + sys.exit(e.code) + + # Validate arguments + if args.tag is not None and not args.tag.strip(): + print("Error: Tag cannot be empty", file=sys.stderr) + sys.exit(EXIT_MISSING_ARGS) + + if not args.command: + print("Error: No command specified", file=sys.stderr) + sys.exit(EXIT_MISSING_ARGS) + + # Connect to service and run command + service_channel, bus = get_service_channel() + + try: + wayland_fd = get_wayland_connection_fd(service_channel, args.tag) + + with wayland_socket_fd(wayland_fd) as fd_num: + exit_code = run_command_with_wayland_socket(args.command, fd_num) + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(EXIT_DBUS_ERROR) + + sys.exit(exit_code) + + +if __name__ == '__main__': + main() diff --git a/tools/meson.build b/tools/meson.build index 94b3e3ac19885f93fcdc734ddf1bcd6ec4b1723a..639e0cf26e2b312b6f9a0dd3f416f8cf8e33b504 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -3,6 +3,11 @@ install_data( install_dir: bindir, ) +install_data( + 'gnome-service-client', + install_dir: bindir, +) + if have_bash_completion bash_completion = dependency('bash-completion', required: false) if bash_completion.found()