Fix document retention and helper retry regressions

Keep failed cursor trials and native recapture anchored to the original document, and restore the requested cached root before LRU eviction. Base braille retry resets on established connection time rather than stalled startup. Poll private AT-SPI daemon ownership without triggering service activation and clean up partial startup failures. Add focused regressions and update browser testing notes.
This commit is contained in:
Storm Dragon committed 2026-11-03 11:37:36 -05:00
1 parent a4d427099e
commit 4da61465b0
11 files changed
+282 -132

No files matched your search

+49 -42
View File
@@ -59,6 +59,34 @@ def selected_proofs():
return selected
def stop_atspi_bus(processes):
for process in reversed(processes):
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
def wait_for_bus_name(process, busArgs, name):
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
if process.poll() is not None:
raise RuntimeError(name + " daemon exited before the probe")
# Query the bus itself: contacting the service can activate another
# daemon (or a user-manager unit) before our child owns its name.
result = subprocess.run(
["gdbus", "call", *busArgs, "--dest", "org.freedesktop.DBus",
"--object-path", "/org/freedesktop/DBus", "--method",
"org.freedesktop.DBus.NameHasOwner", name],
capture_output=True, text=True, timeout=2)
if result.returncode == 0 and result.stdout.strip() == "(true,)":
return
time.sleep(0.1)
raise RuntimeError(name + " did not become ready")
def start_atspi_bus():
# D-Bus activation of org.a11y.Bus and of the AT-SPI registry may
# delegate to systemd user units, which cannot work in this private
@@ -77,44 +105,29 @@ def start_atspi_bus():
launcher = locate("at-spi-bus-launcher",
("/usr/lib/at-spi-bus-launcher", "/usr/libexec/at-spi-bus-launcher"))
process = subprocess.Popen([launcher, "--launch-immediately"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
address = None
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
if process.poll() is not None:
raise RuntimeError("at-spi-bus-launcher exited before the probe")
registry = locate("at-spi2-registryd",
("/usr/lib/at-spi2-registryd", "/usr/libexec/at-spi2-registryd"))
processes = []
try:
process = subprocess.Popen([launcher, "--launch-immediately"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
processes.append(process)
wait_for_bus_name(process, ["--session"], "org.a11y.Bus")
result = subprocess.run(
["gdbus", "call", "--session", "--dest", "org.a11y.Bus",
"--object-path", "/org/a11y/bus", "--method", "org.a11y.Bus.GetAddress"],
capture_output=True, text=True)
if result.returncode == 0 and "unix:" in result.stdout:
address = result.stdout.split("'", 2)[1]
break
time.sleep(0.1)
if address is None:
process.terminate()
raise RuntimeError("AT-SPI bus did not become ready")
registry = locate("at-spi2-registryd",
("/usr/lib/at-spi2-registryd", "/usr/libexec/at-spi2-registryd"))
registryd = subprocess.Popen([registry],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
if registryd.poll() is not None:
process.terminate()
raise RuntimeError("at-spi2-registryd exited before the probe")
result = subprocess.run(
["gdbus", "call", "--address", address, "--dest", "org.a11y.atspi.Registry",
"--object-path", "/org/a11y/atspi/registry",
"--method", "org.freedesktop.DBus.Peer.Ping"],
capture_output=True, text=True)
if result.returncode == 0:
return [process, registryd]
time.sleep(0.1)
registryd.terminate()
process.terminate()
raise RuntimeError("AT-SPI registry did not become ready")
capture_output=True, text=True, timeout=2, check=True)
address = result.stdout.split("'", 2)[1]
registryEnv = os.environ.copy()
registryEnv["AT_SPI_BUS_ADDRESS"] = address
registryd = subprocess.Popen([registry], env=registryEnv,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
processes.append(registryd)
wait_for_bus_name(registryd, ["--address", address], "org.a11y.atspi.Registry")
return processes
except BaseException:
stop_atspi_bus(processes)
raise
def run_inside():
@@ -131,13 +144,7 @@ def run_inside():
# the same browser state whether run alone or after editing.
run_browser_proof(browser, proof)
finally:
for process in reversed(atspi):
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
stop_atspi_bus(atspi)
def run_browser_proof(browser, proof):
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Check private browser daemon startup without contacting a desktop bus."""
import subprocess
import unittest
from unittest.mock import Mock, patch
import native_browser
class AtspiStartupTests(unittest.TestCase):
def test_readiness_queries_bus_without_activating_service(self):
process = Mock()
process.poll.return_value = None
replies = [Mock(returncode=0, stdout="(false,)"),
Mock(returncode=0, stdout="(true,)")]
with patch("native_browser.subprocess.run", side_effect=replies) as run, \
patch("native_browser.time.sleep"):
native_browser.wait_for_bus_name(process, ["--session"], "org.a11y.Bus")
self.assertEqual(run.call_count, 2)
for call in run.call_args_list:
args = call.args[0]
self.assertEqual(args[args.index("--dest") + 1], "org.freedesktop.DBus")
self.assertEqual(args[-2:], ["org.freedesktop.DBus.NameHasOwner", "org.a11y.Bus"])
self.assertEqual(call.kwargs["timeout"], 2)
def test_registry_failure_joins_both_daemons(self):
launcher, registry = Mock(), Mock()
with patch("native_browser.shutil.which", side_effect=lambda name: "/private/" + name), \
patch("native_browser.subprocess.Popen", side_effect=[launcher, registry]) as popen, \
patch("native_browser.wait_for_bus_name", side_effect=[None, RuntimeError("registry failed")]), \
patch("native_browser.subprocess.run", return_value=Mock(stdout="('unix:path=/private/bus',)")):
with self.assertRaisesRegex(RuntimeError, "registry failed"):
native_browser.start_atspi_bus()
self.assertEqual(popen.call_args.kwargs["env"]["AT_SPI_BUS_ADDRESS"], "unix:path=/private/bus")
for process in (launcher, registry):
process.terminate.assert_called_once_with()
process.wait.assert_called_once_with(timeout=5)
def test_probe_timeout_joins_launcher(self):
launcher = Mock()
with patch("native_browser.shutil.which", return_value="/private/daemon"), \
patch("native_browser.subprocess.Popen", return_value=launcher), \
patch("native_browser.wait_for_bus_name", side_effect=subprocess.TimeoutExpired("gdbus", 2)):
with self.assertRaises(subprocess.TimeoutExpired):
native_browser.start_atspi_bus()
launcher.terminate.assert_called_once_with()
launcher.wait.assert_called_once_with(timeout=5)
if __name__ == "__main__":
unittest.main()