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
+29
-16
@@ -468,6 +468,22 @@ type stashedDocument struct {
|
||||
sources map[ObjectID]*C.AtspiAccessible
|
||||
}
|
||||
|
||||
// Source membership survives tree invalidation until the next capture, so it
|
||||
// can identify the owner of a removed focus without guessing from its bus.
|
||||
func (adapter *Adapter) retainedDocumentRoot(id ObjectID) ObjectID {
|
||||
adapter.treeMutex.Lock()
|
||||
defer adapter.treeMutex.Unlock()
|
||||
if _, retained := adapter.nativeDocument[id]; retained {
|
||||
return adapter.documentSlotRoot
|
||||
}
|
||||
for _, root := range adapter.stashedDocumentIDs {
|
||||
if _, retained := adapter.stashedDocuments[root].sources[id]; retained {
|
||||
return root
|
||||
}
|
||||
}
|
||||
return ObjectID{}
|
||||
}
|
||||
|
||||
// rotateDocumentSlots makes root's retained trees and native sources the
|
||||
// active document slots, stashing the previously active document. Capture
|
||||
// calls this on the GLib thread once the capture root is known, before
|
||||
@@ -481,6 +497,16 @@ func (adapter *Adapter) rotateDocumentSlots(root ObjectID) {
|
||||
if root == adapter.documentSlotRoot {
|
||||
return
|
||||
}
|
||||
// Remove the requested slot before stashing the active one: at capacity,
|
||||
// stashing first could evict the very document we are returning to.
|
||||
stashed := adapter.stashedDocuments[root]
|
||||
delete(adapter.stashedDocuments, root)
|
||||
for index, id := range adapter.stashedDocumentIDs {
|
||||
if id == root {
|
||||
adapter.stashedDocumentIDs = append(adapter.stashedDocumentIDs[:index], adapter.stashedDocumentIDs[index+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if adapter.documentSlotRoot.Valid() &&
|
||||
(adapter.documentTree != nil || adapter.documentTextTree != nil) {
|
||||
adapter.stashDocumentLocked(adapter.documentSlotRoot, stashedDocument{
|
||||
@@ -492,22 +518,9 @@ func (adapter *Adapter) rotateDocumentSlots(root ObjectID) {
|
||||
C.g_object_unref(C.gpointer(source))
|
||||
}
|
||||
}
|
||||
if stashed, ok := adapter.stashedDocuments[root]; ok {
|
||||
delete(adapter.stashedDocuments, root)
|
||||
for index, id := range adapter.stashedDocumentIDs {
|
||||
if id == root {
|
||||
adapter.stashedDocumentIDs = append(adapter.stashedDocumentIDs[:index], adapter.stashedDocumentIDs[index+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
adapter.documentTree = stashed.tree
|
||||
adapter.documentTextTree = stashed.textTree
|
||||
adapter.nativeDocument = stashed.sources
|
||||
} else {
|
||||
adapter.documentTree = nil
|
||||
adapter.documentTextTree = nil
|
||||
adapter.nativeDocument = nil
|
||||
}
|
||||
adapter.documentTree = stashed.tree
|
||||
adapter.documentTextTree = stashed.textTree
|
||||
adapter.nativeDocument = stashed.sources
|
||||
adapter.documentSlotRoot = root
|
||||
}
|
||||
|
||||
|
||||
@@ -21,52 +21,12 @@ func (adapter *Adapter) captureDesktop(ctx context.Context, reviewFocus ObjectID
|
||||
trace := capture_diagnostics(ctx)
|
||||
trace.begin_stage("root-selection", reviewFocus, "")
|
||||
root, err := adapter.nativeDocumentRoot(ctx, reviewFocus)
|
||||
adapter.treeMutex.Lock()
|
||||
retainedRoots := make(map[ObjectID]bool, 1+len(adapter.stashedDocumentIDs))
|
||||
if adapter.documentSlotRoot.Valid() && adapter.documentTree != nil {
|
||||
retainedRoots[adapter.documentSlotRoot] = true
|
||||
}
|
||||
for _, id := range adapter.stashedDocumentIDs {
|
||||
retainedRoots[id] = true
|
||||
}
|
||||
adapter.treeMutex.Unlock()
|
||||
if (errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrStaleObject)) && len(retainedRoots) != 0 && len(adapter.nativeFocus) != 0 {
|
||||
// Dynamic pages can remove the browse cursor without sending a new
|
||||
// focus event. Recover only from the newest retained native focus in
|
||||
// the same document. A chrome or other-tab focus is not a fallback.
|
||||
for i := len(adapter.nativeFocus) - 1; i >= 0; i-- {
|
||||
latest := adapter.nativeFocus[i]
|
||||
if latest.id.BusName != reviewFocus.BusName {
|
||||
break
|
||||
}
|
||||
C.atspi_accessible_clear_cache_single(latest.source)
|
||||
states := C.atspi_accessible_get_state_set(latest.source)
|
||||
if states == nil {
|
||||
continue
|
||||
}
|
||||
dead := C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) != 0
|
||||
C.g_object_unref(C.gpointer(states))
|
||||
if dead {
|
||||
continue
|
||||
}
|
||||
// A removed browse target can retain a non-defunct proxy after
|
||||
// Chromium has dropped its accessible object. Its role is then
|
||||
// invalid; try the preceding real focus instead.
|
||||
if C.atspi_accessible_get_role(latest.source, nil) == C.ATSPI_ROLE_INVALID {
|
||||
continue
|
||||
}
|
||||
candidate, candidateErr := adapter.nativeDocumentRoot(ctx, latest.id)
|
||||
if candidateErr != nil {
|
||||
continue
|
||||
}
|
||||
if retainedRoots[nativeObjectID(candidate)] {
|
||||
root, err = candidate, nil
|
||||
break
|
||||
}
|
||||
// The newest live focus can sit in browser chrome or a tab
|
||||
// whose document was never retained; older focus history in
|
||||
// the retained document may still resolve.
|
||||
C.g_object_unref(C.gpointer(candidate))
|
||||
if errors.Is(err, ErrObjectNotFound) || errors.Is(err, ErrStaleObject) {
|
||||
// A removed focus may still belong to a retained capture. Recover
|
||||
// through that document's root, never through another tab's focus
|
||||
// history merely because it shares the application's bus.
|
||||
if owner := adapter.retainedDocumentRoot(reviewFocus); owner.Valid() && owner != reviewFocus {
|
||||
root, err = adapter.nativeDocumentRoot(ctx, owner)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -583,6 +583,62 @@ func TestDocumentSlotsRetainTreesAcrossRoots(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentSlotsRestoreOldestAtCapacity(t *testing.T) {
|
||||
adapter := New()
|
||||
var first *AccessibleTree
|
||||
for index := range maxStashedDocuments + 1 {
|
||||
root := NewObjectID(":1.42", fmt.Sprintf("/doc%d", index))
|
||||
adapter.rotateDocumentSlots(root)
|
||||
snapshot := NewSnapshot(root.Path, "Document", "document web", "", "")
|
||||
snapshot.BusName = root.BusName
|
||||
tree, err := adapter.installCapturedTree(root, []TreeNode{{Snapshot: snapshot}}, true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if index == 0 {
|
||||
first = tree
|
||||
}
|
||||
}
|
||||
adapter.rotateDocumentSlots(NewObjectID(":1.42", "/doc0"))
|
||||
if adapter.documentTree != first {
|
||||
t.Fatal("returning to oldest retained document evicted it before restoration")
|
||||
}
|
||||
if len(adapter.stashedDocuments) != maxStashedDocuments {
|
||||
t.Fatalf("stashed documents = %d, want %d", len(adapter.stashedDocuments), maxStashedDocuments)
|
||||
}
|
||||
}
|
||||
|
||||
// These fixtures describe source membership without owning native handles.
|
||||
func documentSourceKeys[T any](_ map[ObjectID]T, ids ...ObjectID) map[ObjectID]T {
|
||||
sources := make(map[ObjectID]T, len(ids))
|
||||
var value T
|
||||
for _, id := range ids {
|
||||
sources[id] = value
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
func TestRetainedDocumentRootDoesNotCrossTabsOnSameBus(t *testing.T) {
|
||||
adapter := New()
|
||||
active, inactive := testID("/active"), testID("/inactive")
|
||||
adapter.documentSlotRoot = active
|
||||
adapter.nativeDocument = documentSourceKeys(adapter.nativeDocument, testID("/active_child"))
|
||||
adapter.stashedDocumentIDs = []ObjectID{inactive}
|
||||
adapter.stashedDocuments = map[ObjectID]stashedDocument{
|
||||
inactive: {sources: documentSourceKeys(adapter.nativeDocument, testID("/removed_child"))},
|
||||
}
|
||||
for _, item := range []struct{ focus, want ObjectID }{
|
||||
{testID("/active_child"), active},
|
||||
{testID("/removed_child"), inactive},
|
||||
{testID("/chrome"), ObjectID{}},
|
||||
{NewObjectID(":1.99", "/removed_child"), ObjectID{}},
|
||||
} {
|
||||
if got := adapter.retainedDocumentRoot(item.focus); got != item.want {
|
||||
t.Fatalf("owner of %v = %v, want %v", item.focus, got, item.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A provider whose capture just timed out is probably still stuck. The
|
||||
// next document capture for that bus must fail fast with ErrProviderHung
|
||||
// instead of queueing another multi-second native stall, and the mark
|
||||
|
||||
Reference in new issue
Block a user