Adds scripts/live-dbus-test.sh (`just live-dbus`): each scenario runs the installed Ghostty 1.3.1 and the Rust build on a private dbus-run-session bus inside a private headless weston (the user's session bus is never written to), with Python/Gio mocks for org.freedesktop.Notifications, the GlobalShortcuts/Settings portal and org.freedesktop.systemd1, plus a real xdg-desktop-portal run. Scenarios: single-instance (+detect), +new-window with arguments, notifications, global shortcuts, color-scheme settings, cgroup scope creation, URL opening. tests/live_dbus.rs compares the Rust build against goldens recorded from the original (skips when tools or a fresh production build are missing). Two Rust bugs found and fixed (no upstream deviation): - GlobalShortcuts CreateSession/BindShortcuts payloads used the wrong D-Bus signatures ((v), double-boxed values), which a real portal rejects; signature tests added. - CreateSession was sent twice at startup; the config is now bound only after the D-Bus connection is set, as in Zig. All nine scenarios now behave identically to the original (docs/rewrite/notes/live-dbus.md). Test Plan: - just live-dbus - cargo test --test live_dbus (needs target/vp/production/ghostty)
252 lines
11 KiB
Python
Executable File
252 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Mock D-Bus services for the live D-Bus comparison (scripts/live-dbus-test.sh).
|
|
|
|
Runs on the private session bus and claims:
|
|
org.freedesktop.Notifications Notify/CloseNotification/GetCapabilities/GetServerInformation
|
|
org.freedesktop.systemd1 Manager.StartTransientUnit
|
|
org.freedesktop.portal.Desktop GlobalShortcuts, Settings, OpenURI (+ Request objects)
|
|
test.ghostty.Mock control: Activate(s), SetColorScheme(u), Quit()
|
|
|
|
Every received call is appended to $MOCK_LOG as one JSON line:
|
|
{"iface": ..., "method": ..., "args": ...}
|
|
The portal pieces are mocks (the real xdg-desktop-portal needs a running
|
|
backend and a desktop session); they implement just what Ghostty calls.
|
|
"""
|
|
import json
|
|
import os
|
|
import signal
|
|
import sys
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gio", "2.0")
|
|
from gi.repository import Gio, GLib # noqa: E402
|
|
|
|
LOG = os.environ.get("MOCK_LOG", "/dev/stderr")
|
|
COLOR_SCHEME = int(os.environ.get("MOCK_COLOR_SCHEME", "0")) # 0 none, 1 dark, 2 light
|
|
BIND_REPLY = os.environ.get("MOCK_BIND", "ok")
|
|
|
|
XML = """
|
|
<node>
|
|
<interface name='org.freedesktop.Notifications'>
|
|
<method name='Notify'>
|
|
<arg type='s' direction='in'/><arg type='u' direction='in'/><arg type='s' direction='in'/>
|
|
<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='as' direction='in'/>
|
|
<arg type='a{sv}' direction='in'/><arg type='i' direction='in'/><arg type='u' direction='out'/>
|
|
</method>
|
|
<method name='CloseNotification'><arg type='u' direction='in'/></method>
|
|
<method name='GetCapabilities'><arg type='as' direction='out'/></method>
|
|
<method name='GetServerInformation'>
|
|
<arg type='s' direction='out'/><arg type='s' direction='out'/><arg type='s' direction='out'/><arg type='s' direction='out'/>
|
|
</method>
|
|
<signal name='ActionInvoked'><arg type='u'/><arg type='s'/></signal>
|
|
<signal name='NotificationClosed'><arg type='u'/><arg type='u'/></signal>
|
|
</interface>
|
|
<interface name='org.freedesktop.systemd1.Manager'>
|
|
<method name='StartTransientUnit'>
|
|
<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='a(sv)' direction='in'/>
|
|
<arg type='a(sa(sv))' direction='in'/><arg type='o' direction='out'/>
|
|
</method>
|
|
</interface>
|
|
<interface name='org.freedesktop.portal.GlobalShortcuts'>
|
|
<method name='CreateSession'><arg type='a{sv}' direction='in'/><arg type='o' direction='out'/></method>
|
|
<method name='BindShortcuts'>
|
|
<arg type='o' direction='in'/><arg type='a(sa{sv})' direction='in'/><arg type='s' direction='in'/>
|
|
<arg type='a{sv}' direction='in'/><arg type='o' direction='out'/>
|
|
</method>
|
|
<signal name='Activated'><arg type='o'/><arg type='s'/><arg type='t'/><arg type='a{sv}'/></signal>
|
|
</interface>
|
|
<interface name='org.freedesktop.portal.Session'><method name='Close'/></interface>
|
|
<interface name='org.freedesktop.portal.Settings'>
|
|
<method name='ReadAll'><arg type='as' direction='in'/><arg type='a{sa{sv}}' direction='out'/></method>
|
|
<method name='Read'><arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='v' direction='out'/></method>
|
|
<signal name='SettingChanged'><arg type='s'/><arg type='s'/><arg type='v'/></signal>
|
|
<property name='version' type='u' access='read'/>
|
|
</interface>
|
|
<interface name='org.freedesktop.portal.OpenURI'>
|
|
<method name='OpenURI'>
|
|
<arg type='s' direction='in'/><arg type='s' direction='in'/><arg type='a{sv}' direction='in'/><arg type='o' direction='out'/>
|
|
</method>
|
|
<method name='OpenFile'>
|
|
<arg type='s' direction='in'/><arg type='h' direction='in'/><arg type='a{sv}' direction='in'/><arg type='o' direction='out'/>
|
|
</method>
|
|
<property name='version' type='u' access='read'/>
|
|
</interface>
|
|
<interface name='org.freedesktop.portal.Request'><method name='Close'/>
|
|
<signal name='Response'><arg type='u'/><arg type='a{sv}'/></signal></interface>
|
|
</node>
|
|
"""
|
|
|
|
CTRL_XML = """
|
|
<node><interface name='test.ghostty.Mock'>
|
|
<method name='Activate'><arg type='s' direction='in'/></method>
|
|
<method name='SetColorScheme'><arg type='u' direction='in'/></method>
|
|
<method name='Invoke'><arg type='u' direction='in'/><arg type='s' direction='in'/></method>
|
|
<method name='SetFail'><arg type='b' direction='in'/></method>
|
|
<method name='Quit'/>
|
|
</interface></node>
|
|
"""
|
|
|
|
nid = [0]
|
|
state = {"session": None, "scheme": COLOR_SCHEME}
|
|
loop = GLib.MainLoop()
|
|
conns = {}
|
|
|
|
|
|
def log(iface, method, args):
|
|
def conv(o):
|
|
if isinstance(o, (bytes, bytearray)):
|
|
return list(o)
|
|
return str(o)
|
|
|
|
with open(LOG, "a") as f:
|
|
f.write(json.dumps({"iface": iface, "method": method, "args": args}, default=conv, sort_keys=True) + "\n")
|
|
|
|
|
|
def unpack(v):
|
|
return v.unpack() if v is not None else None
|
|
|
|
|
|
def request_path(sender, options):
|
|
tok = options.get("handle_token", "tok")
|
|
return "/org/freedesktop/portal/desktop/request/" + sender[1:].replace(".", "_") + "/" + tok
|
|
|
|
|
|
def reply_request(conn, sender, options, results):
|
|
"""Emit Request.Response after the method reply was sent."""
|
|
path = request_path(sender, options)
|
|
|
|
def emit():
|
|
conn.emit_signal(
|
|
sender, path, "org.freedesktop.portal.Request", "Response",
|
|
GLib.Variant("(ua{sv})", (0, results)),
|
|
)
|
|
return False
|
|
|
|
GLib.timeout_add(100, emit)
|
|
return path
|
|
|
|
|
|
def settings_all():
|
|
return {"org.freedesktop.appearance": {"color-scheme": GLib.Variant("u", state["scheme"])}}
|
|
|
|
|
|
def on_call(conn, sender, path, iface, method, params, inv):
|
|
args = unpack(params)
|
|
if iface == "org.freedesktop.Notifications":
|
|
if method == "Notify":
|
|
log(iface, method, args)
|
|
nid[0] += 1
|
|
inv.return_value(GLib.Variant("(u)", (nid[0],)))
|
|
elif method == "GetCapabilities":
|
|
inv.return_value(GLib.Variant("(as)", (["actions", "body", "body-markup", "icon-static"],)))
|
|
elif method == "GetServerInformation":
|
|
inv.return_value(GLib.Variant("(ssss)", ("mock", "ghostty-test", "1.0", "1.2")))
|
|
else:
|
|
log(iface, method, args)
|
|
inv.return_value(None)
|
|
elif iface == "org.freedesktop.systemd1.Manager":
|
|
log(iface, method, args)
|
|
if state.get("fail"):
|
|
inv.return_dbus_error("org.freedesktop.DBus.Error.AccessDenied", "mock failure")
|
|
return
|
|
inv.return_value(GLib.Variant("(o)", ("/org/freedesktop/systemd1/job/1",)))
|
|
elif iface == "org.freedesktop.portal.GlobalShortcuts":
|
|
log(iface, method, args)
|
|
if method == "CreateSession":
|
|
opts = args[0]
|
|
sess = "/org/freedesktop/portal/desktop/session/" + sender[1:].replace(".", "_") + "/" + opts.get("session_handle_token", "s")
|
|
state["session"] = sess
|
|
state["sender"] = sender
|
|
sess_info = Gio.DBusNodeInfo.new_for_xml(XML).lookup_interface("org.freedesktop.portal.Session")
|
|
conn.register_object(sess, sess_info, on_call, None, None)
|
|
rp = reply_request(conn, sender, opts, {"session_handle": GLib.Variant("s", sess)})
|
|
inv.return_value(GLib.Variant("(o)", (rp,)))
|
|
else:
|
|
rp = reply_request(conn, sender, args[3], {"shortcuts": GLib.Variant("a(sa{sv})", [])})
|
|
inv.return_value(GLib.Variant("(o)", (rp,)))
|
|
elif iface == "org.freedesktop.portal.Session":
|
|
log(iface, method, {"path": path})
|
|
inv.return_value(None)
|
|
elif iface == "org.freedesktop.portal.Settings":
|
|
log(iface, method, args)
|
|
if method == "ReadAll":
|
|
inv.return_value(GLib.Variant("(a{sa{sv}})", (settings_all(),)))
|
|
else:
|
|
inv.return_value(GLib.Variant("(v)", (GLib.Variant("v", GLib.Variant("u", state["scheme"])),)))
|
|
elif iface == "org.freedesktop.portal.OpenURI":
|
|
log(iface, method, args)
|
|
rp = reply_request(conn, sender, args[-1], {})
|
|
inv.return_value(GLib.Variant("(o)", (rp,)))
|
|
elif iface == "org.freedesktop.portal.Request":
|
|
inv.return_value(None)
|
|
elif iface == "test.ghostty.Mock":
|
|
if method == "Activate":
|
|
conn.emit_signal(
|
|
None, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "Activated",
|
|
GLib.Variant("(osta{sv})", (state["session"], args[0], 0, {})),
|
|
)
|
|
elif method == "SetColorScheme":
|
|
state["scheme"] = args[0]
|
|
conn.emit_signal(
|
|
None, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings", "SettingChanged",
|
|
GLib.Variant("(ssv)", ("org.freedesktop.appearance", "color-scheme", GLib.Variant("u", args[0]))),
|
|
)
|
|
elif method == "Invoke":
|
|
conn.emit_signal(
|
|
None, "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked",
|
|
GLib.Variant("(us)", (args[0], args[1])),
|
|
)
|
|
elif method == "SetFail":
|
|
state["fail"] = args[0]
|
|
elif method == "Quit":
|
|
loop.quit()
|
|
inv.return_value(None)
|
|
|
|
|
|
def on_prop(conn, sender, path, iface, name):
|
|
return GLib.Variant("u", 1 if iface == "org.freedesktop.portal.Settings" else 3)
|
|
|
|
|
|
def on_bus(conn, name):
|
|
node = Gio.DBusNodeInfo.new_for_xml(XML)
|
|
paths = {
|
|
"org.freedesktop.Notifications": ["/org/freedesktop/Notifications"],
|
|
"org.freedesktop.systemd1.Manager": ["/org/freedesktop/systemd1"],
|
|
"org.freedesktop.portal.GlobalShortcuts": ["/org/freedesktop/portal/desktop"],
|
|
"org.freedesktop.portal.Settings": ["/org/freedesktop/portal/desktop"],
|
|
"org.freedesktop.portal.OpenURI": ["/org/freedesktop/portal/desktop"],
|
|
"org.freedesktop.portal.Session": [],
|
|
}
|
|
for ifc in node.interfaces:
|
|
for p in paths.get(ifc.name, []):
|
|
conn.register_object(p, ifc, on_call, on_prop, None)
|
|
ctrl = Gio.DBusNodeInfo.new_for_xml(CTRL_XML)
|
|
conn.register_object("/test/ghostty/Mock", ctrl.interfaces[0], on_call, None, None)
|
|
conns["c"] = conn
|
|
|
|
|
|
def acquired(conn, name):
|
|
if name == "test.ghostty.Mock":
|
|
sys.stdout.write("READY\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
# Own all names on one connection; announce READY once the last is acquired.
|
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
|
on_bus(bus, None)
|
|
names = os.environ.get("MOCK_NAMES", "notifications,systemd,portal").split(",")
|
|
want = []
|
|
if "notifications" in names:
|
|
want.append("org.freedesktop.Notifications")
|
|
if "systemd" in names:
|
|
want.append("org.freedesktop.systemd1")
|
|
if "portal" in names:
|
|
want.append("org.freedesktop.portal.Desktop")
|
|
want.append("test.ghostty.Mock")
|
|
for n in want:
|
|
Gio.bus_own_name_on_connection(bus, n, Gio.BusNameOwnerFlags.NONE, acquired, None)
|
|
signal.signal(signal.SIGTERM, lambda *a: loop.quit())
|
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, lambda *a: loop.quit())
|
|
loop.run()
|