Repository navigation
headless session: the AT-SPI registry never starts, so ui_* tools fail with 'no application named ...' (self-test 16/18) #3
Description
desktop_health output
From a headless session in the bug state, after a clean install, logged out and back in (extension ACTIVE, so this is not the usual "you didn't log out" case):
{
"verdict": "USABLE on the default desktop with limits: the accessibility tree is empty -- toolkit-accessibility may have been off when these apps started. Screenshots, window control and pointer input all work.",
"implementation": {
"source": "/home/tim/projects/deskwright",
"revision": "9624a33",
"session": "headless:default"
},
"extension": "ACTIVE",
"windows": 0,
"atspi_apps": 0,
"pointer_control": "absolute, via org.gnome.Mutter.RemoteDesktop; desktop is 1280x720 at (0, 0)"
}Note the verdict blames toolkit-accessibility. That misdirection is part of why this is easy to miss.
Environment
- GNOME Shell version: 50.1
- Distribution: Ubuntu 26.04 (at-spi2-core 2.60.4, dbus-daemon 1.16.2, dbus-broker 37-5, systemd 259)
- How is it installed?: git clone (reproduced identically with the 0.1.1 pipx install)
- Which desktop was it driving?: a headless session (DESKWRIGHT_SESSION=headless)
- Logged out and back in since installing: yes, extension reports ACTIVE.
What happened
DESKWRIGHT_SESSION=headless deskwright --self-test fails 2/18, always the same two:
FAIL ui_tree(gnome-shell) ToolError: no application named 'gnome-shell' on the AT-SPI bus. Present: .
FAIL ui_find(actionable) ToolError: no application named 'gnome-shell' on the AT-SPI bus. Present: .
16/18 passed
The cause is in the session's own log, and it is not about apps or trees: the headless session's a11y bus never had a registry at all. ~/.local/state/deskwright/headless-shell.log shows the a11y broker asking the private session bus to activate org.freedesktop.systemd1 (to start at-spi2-registryd), once per retry:
dbus-daemon[...]: Activating service name='org.freedesktop.systemd1' requested by ':1.7' (... comm="/usr/bin/dbus-broker-launch ...")
dbus-daemon[...]: Activated service 'org.freedesktop.systemd1' failed: Process org.freedesktop.systemd1 exited with status 1
The chain:
- The headless session runs a private
dbus-daemon --session, which has no systemd behind it (that is the point of it). - Since at-spi2-core 2.59.0 the a11y launcher is dbus-broker by default, and it routes service activation through systemd.
- On a bus with no systemd,
org.freedesktop.systemd1resolves to the stub systemd ships for systemd-less buses,Exec=/bin/false, which exits 1. Soat-spi2-registrydis never started,org.a11y.atspi.Registryhas no owner, and everyui_*call fails.
The user's real session never sees this because its bus has the real systemd behind the name. That asymmetry is presumably why this reads as "stunted trees" (the toolkit-accessibility explanation) rather than "no registry".
This looks version-triggered rather than Ubuntu-specific: the dbus-broker-by-default switch is at-spi2-core 2.59.0 (GNOME 49.1 era), so any current GNOME 50 stack should hit it. I have only measured Ubuntu 26.04; if anyone can run the repro below on Fedora/Arch it would confirm the range.
Reproduce it (scripted, no guessing)
On any GNOME Wayland login with the repo's dependencies present, from a fresh clone of main:
git clone https://github.com/tristanmuzzu/deskwright && cd deskwright
python3 - <<'PY'
import os, subprocess, sys
sys.path.insert(0, ".")
from deskwright import headless
st = headless.ensure(name="bugrepro") # ~20 s first time
print("session running:", st["running"])
headless.pin_env(st, os.environ) # ask FROM INSIDE the session
def gdbus(*args, addr=None):
cmd = ["gdbus", "call", "--address", addr] if addr else ["gdbus", "call", "--session"]
return subprocess.run(cmd + list(args), capture_output=True, text=True,
env=dict(os.environ, **({} if addr else {"DBUS_SESSION_BUS_ADDRESS": st["bus_address"]})))
# 1. the a11y bus itself is fine -- its launcher does not need systemd
out = gdbus("--dest", "org.a11y.Bus", "--object-path", "/org/a11y/bus",
"--method", "org.a11y.Bus.GetAddress")
sock = out.stdout.strip().strip("()").strip("'\"").removeprefix("unix:path=")
print("a11y socket answers:", bool(sock), sock)
# 2. the registry name on that bus -- THIS is the bug: no owner
name = gdbus("--dest", "org.freedesktop.DBus", "--object-path", "/org/freedesktop/DBus",
"--method", "org.freedesktop.DBus.GetNameOwner",
"org.a11y.atspi.Registry", addr=sock)
print("registry owned:", name.returncode == 0, name.stderr.strip()[:80])
headless.stop("bugrepro")
sys.exit(0 if name.returncode == 0 else 1)
PY
echo "exit=$?" # main: 'registry owned: False', exit=1The same probe doubles as the acceptance check for a fix: exit 0 with registry owned: True. A second thing a fix must get right, measured while working around this by hand: the registry has to be up before gnome-shell starts. A registry that appears after the shell answers still leaves the self-test at 16/18, because the shell's atk-bridge stops retrying registration (apps started later were on the bus while gnome-shell was not, same session). A cold DESKWRIGHT_SESSION=headless deskwright --self-test should go 18/18.
Workaround for anyone blocked today: start registryd against the private bus by hand (it resolves the a11y socket itself, only DBUS_SESSION_BUS_ADDRESS is needed) and restart the headless session so the shell registers:
DBUS_SESSION_BUS_ADDRESS="$(python3 -c 'import json;print(json.load(open(".../.local/state/deskwright/headless-default.json"))["bus_address"])')" \
/usr/libexec/at-spi2-registryd --use-gnome-session & # keep it running
Activity