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()