File: gnome-service-client

package info (click to toggle)
mutter 49.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 51,784 kB
  • sloc: ansic: 398,618; xml: 3,384; python: 3,270; sh: 389; ruby: 167; makefile: 61; javascript: 26
file content (199 lines) | stat: -rwxr-xr-x 5,645 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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()