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:
1 parent
a4d427099e
commit
4da61465b0
11 files changed
+282
-132
No files matched your search
+49
-42
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user