Skip to content

fix: let /status, /screenshot, and DELETE /session API methods to bypass the dispatch queue - #1222

Open
mykola-mokhnach wants to merge 10 commits into
masterfrom
order
Open

fix: let /status, /screenshot, and DELETE /session API methods to bypass the dispatch queue#1222
mykola-mokhnach wants to merge 10 commits into
masterfrom
order

Conversation

@mykola-mokhnach

@mykola-mokhnach mykola-mokhnach commented Aug 22, 2026

Copy link
Copy Markdown

Summary

Fixes #1210: when the app under test freezes, WDA's whole HTTP server stops answering — including routes like GET /status and DELETE /session/:id that don't touch the frozen app at all — because every route handler is funneled through the same main-queue dispatch before it can run.

  • Add a standalone route flag that bypasses the shared route queue entirely. Concurrent requests to the same endpoint are coalesced into a single in-flight execution (its response is fanned out to all callers); everything else gets its own queue, so distinct standalone endpoints always run in parallel with whatever else is stuck. Mark GET /status, GET /screenshot, and DELETE /session standalone.
  • When a session is killed, immediately fail every other request still pending for that session (queued or already executing) with a proper W3C "invalid session id" error instead of leaving their HTTP clients waiting on a session that no longer exists. A request that's already executing keeps running to completion in the background — GCD gives no way to abort it — but its result is discarded rather than ever reaching a client.
  • -kill now clears the active-session pointer up front, before its own teardown, so a request that arrives mid-teardown correctly resolves to "no such session" instead of racing in against a half-torn-down one.
  • Bound several previously-unbounded waits found while auditing these routes for hangs of their own:
    • FBTestmanagerdVersion() and stopScreenRecordingWithUUID:error: used an unbounded wait on a daemon RPC reply.
    • -kill's check for whether the tested app is the system app goes through a shared accessibility client (FBXCAXClientProxy) that can itself be stuck behind another in-flight request against a frozen app — now capped at 5s, defaulting to "assume it might be the system app" (skip termination) on timeout to stay on the safe side. That check runs on a background queue now, guarded with @try/@catch since a sibling XCUIApplication method (-terminate) is confirmed to hard-assert off the main thread.

@eglitise

Copy link
Copy Markdown

Since both this and #1214 seem to fix the same issue, are the changes complementary, or does this PR replace the other one?

@mykola-mokhnach

Copy link
Copy Markdown
Author

Since both this and #1214 seem to fix the same issue, are the changes complementary, or does this PR replace the other one?

They are complementary. This PR allows to take a screenshot, check status or delete the session without waiting for other requests to finish. Previously all requests were put into a single synchronous dispatch queue.

The other PR tries to help all requests to not be stuck, so they don't deadlock the queue.

Comment thread WebDriverAgentLib/Routing/FBSession.m Outdated
Comment thread WebDriverAgentLib/Routing/FBHTTPServer.m Outdated
Comment thread WebDriverAgentLib/Routing/FBHTTPServer.m Outdated
Comment thread WebDriverAgentLib/Routing/FBHTTPServer.m Outdated
mykola-mokhnach added a commit that referenced this pull request Aug 23, 2026
- FBSession -kill: restore the self==_activeSession guard, but as an
  atomic check-and-clear under a lock this time. DELETE /session and
  session creation now genuinely run concurrently (both bypass the
  frozen route queue), so a session already superseded by a newer one
  can still reach -kill via a stale reference; without atomicity a
  belated call could win the race and null out the new session's
  pointer instead of its own.
- FBHTTPServer: track session-scoped standalone requests (e.g.
  GET /session/:id/screenshot) in pendingSessionRequests too, not just
  non-standalone ones, so DELETE can abandon them the same way. DELETE
  /session's own request is excluded from tracking under its own
  session, since it's the one that performs the abandonment.
- FBHTTPServer: track pending requests by a per-request identity
  object instead of by raw nw_connection_t, so two pipelined requests
  for the same session sharing one connection no longer collapse into
  a single tracked entry (which suppressed the second one's response
  permanently).
- FBHTTPServer: allow only one in-flight request per connection at a
  time. Standalone routes now run on independent queues that can
  finish in any order, so without this, pipelined requests on one
  connection (e.g. /screenshot then /status) could have their
  responses written to the wire out of order.
Comment thread WebDriverAgentLib/Routing/FBHTTPServer.m
Comment thread WebDriverAgentLib/Routing/FBSession.m
Comment thread WebDriverAgentLib/Routing/FBSession.m Outdated
mykola-mokhnach added a commit that referenced this pull request Aug 23, 2026
- FBHTTPServer: move the buffer append itself onto bufferProcessingQueue,
  not just the parse. -client:didReceiveData: previously appended under
  a separate lock, so a receive callback could still mutate a
  connection's buffer while -processBufferForClient: was reading it
  unlocked on the processing queue.
- FBSession: replace the per-instance kill-wait with class-level
  teardown-in-progress state, and add +killActiveSessionAndWaitForTeardown.
  The per-instance wait only helped a caller that still held a
  reference to the outgoing session; handleCreateSession: instead reads
  FBSession.activeSession fresh, so once a concurrent -kill had already
  cleared the pointer, it saw nil and proceeded to launch the
  replacement app without waiting for that -kill's teardown - including
  its app termination - to actually finish.
- FBSession -fb_terminateTestedApplicationWithTimeout:: make the
  deferred main-thread -terminate call cancelable. If the bounded wait
  times out, the call is marked "given up on" under the same lock the
  deferred block checks before actually calling -terminate, so it can
  no longer fire later against a replacement session's app once -kill
  has reported its teardown finished.

All FBSessionTests pass; WebDriverAgentLib builds clean.
mykola-mokhnach and others added 10 commits August 24, 2026 07:09
…route queue

When the app under test freezes, every route handler funnels through the
shared main-queue dispatch before it can run, so even side-effect-free routes
like /status wait behind a stuck handler indefinitely (#1210). Add a
standalone route flag that skips the shared queue: concurrent requests to the
same endpoint are coalesced into one in-flight execution, everything else
gets its own queue. Mark /status, /screenshot, and DELETE /session standalone.

Also fixes two related hang sources found while auditing these routes for
their own risk of blocking: FBTestmanagerdVersion() and
stopScreenRecordingWithUUID:error: used an unbounded wait on a daemon RPC,
and -kill unconditionally cleared _activeSession even if a newer session had
since become active.
…em stuck

DELETE /session already bypasses the frozen route queue, but other in-flight
requests for that same session didn't: a request queued or executing when the
session dies would either hang until it finally got a turn, or (worse) run
against a session mid-teardown. -kill now clears the active-session pointer
and posts a notification up front, before its own teardown; FBWebServer uses
it to immediately fail every pending request for that session with a proper
W3C "invalid session id" error instead of leaving their clients waiting. A
request already executing keeps running in the background - GCD can't abort
it - but its result is discarded rather than ever reaching a client.

Also bounds a related unbounded wait found while testing this: -kill's check
for whether the tested app is the system app goes through a shared
accessibility client that can itself be stuck behind another in-flight
request against a frozen app. It's now capped at 5s, defaulting to "assume it
might be the system app" (skip termination) on timeout to stay on the safe
side.
+[XCUIApplication fb_systemApplication] is undocumented private API; its
sibling -terminate is confirmed (this session, live) to hard-assert when
called off the main thread, so the background-queue call added to bound
-kill's system-app check needs the same @try/@catch already used around
-terminate - an uncaught exception from inside a bare dispatch_async block
has no handler and would crash the whole process, which is worse than the
timeout this code already guards against.
Now that the clear happens up front instead of after -kill's teardown, the
scenario the guard protected against - a slow -kill finishing after a newer
session had already taken over - can't happen: every call site resolves
self/_activeSession and calls kill() in the same uninterrupted synchronous
chain (+sessionWithIdentifier: literally hands back _activeSession itself).
The only way self != _activeSession at that point is an unsynchronized data
race on the static, already an accepted, out-of-scope risk elsewhere in this
design, and one the guard couldn't protect against anyway.
- FBSession -kill: restore the self==_activeSession guard, but as an
  atomic check-and-clear under a lock this time. DELETE /session and
  session creation now genuinely run concurrently (both bypass the
  frozen route queue), so a session already superseded by a newer one
  can still reach -kill via a stale reference; without atomicity a
  belated call could win the race and null out the new session's
  pointer instead of its own.
- FBHTTPServer: track session-scoped standalone requests (e.g.
  GET /session/:id/screenshot) in pendingSessionRequests too, not just
  non-standalone ones, so DELETE can abandon them the same way. DELETE
  /session's own request is excluded from tracking under its own
  session, since it's the one that performs the abandonment.
- FBHTTPServer: track pending requests by a per-request identity
  object instead of by raw nw_connection_t, so two pipelined requests
  for the same session sharing one connection no longer collapse into
  a single tracked entry (which suppressed the second one's response
  permanently).
- FBHTTPServer: allow only one in-flight request per connection at a
  time. Standalone routes now run on independent queues that can
  finish in any order, so without this, pipelined requests on one
  connection (e.g. /screenshot then /status) could have their
  responses written to the wire out of order.
- FBSession -kill: dispatch [testedApplication terminate] to the main
  thread with a bounded wait instead of calling it inline. -kill can
  now run on a background queue (DELETE /session is standalone), and
  -terminate is confirmed to hard-assert off the main thread - the
  same reason the system-app check just above it is already
  background-dispatched. Waiting indefinitely for main would
  reintroduce the exact hang standalone routes exist to avoid if
  that's the queue currently stuck on another request, so give up
  after 5s and let the dispatched call finish on its own whenever
  main frees up.
- FBSession -kill: a caller that loses the atomic active-session race
  now waits (bounded, via NSCondition) for the winner's teardown to
  actually finish before returning, instead of proceeding immediately.
  Session creation's own pre-kill of the outgoing session relies on
  this to not launch the new app while the old one's -terminate may
  still be in flight on another thread.
- FBHTTPServer: route every call to -processBufferForClient: (from
  both new data arriving and a response completing) through one
  dedicated serial queue. The method parses a connection's buffer
  outside of any lock; that's only safe when no two calls for any
  connection can run concurrently, which no longer held once responses
  could complete on independent standalone-route queues and re-enter
  parsing directly on whichever thread finished the write.
- FBHTTPServer: standalone-route coalescing now keys on the full path
  and query string instead of just the path, so a future standalone
  route that branches on query parameters can't have a second
  concurrent request silently served the first request's response.
- FBXCodeCompatibility FBTestmanagerdVersion: stop caching the
  timeout fallback forever via dispatch_once. A merely-slow (not
  hung) first daemon reply would otherwise permanently lock in the
  fallback value for the rest of the process's life; only a real
  reply or the always-correct modern-testmanagerd branch is cached
  now, so a timeout is retried on the next call.
No behavior change - condenses several multi-line explanatory comments
added in the prior two commits down to one or two lines each.
- FBConfiguration -bindingPortRange: collapse to a single return
  statement so the compiler can elide the copy (-Wnrvo).
- FBTCPSocket -acceptConnection:: read weakSelf into a strong local
  before use, instead of a second direct weak read in the same block
  (-Warc-repeated-use-of-weak).
- RouteResponse: mutableHeaders was declared `copy` on a mutable
  dictionary type, which would silently store an immutable object if
  ever assigned through the property setter; declare it `strong`
  instead, matching how it's actually used (osx.ObjCProperty).
- FBHTTPServer: move the buffer append itself onto bufferProcessingQueue,
  not just the parse. -client:didReceiveData: previously appended under
  a separate lock, so a receive callback could still mutate a
  connection's buffer while -processBufferForClient: was reading it
  unlocked on the processing queue.
- FBSession: replace the per-instance kill-wait with class-level
  teardown-in-progress state, and add +killActiveSessionAndWaitForTeardown.
  The per-instance wait only helped a caller that still held a
  reference to the outgoing session; handleCreateSession: instead reads
  FBSession.activeSession fresh, so once a concurrent -kill had already
  cleared the pointer, it saw nil and proceeded to launch the
  replacement app without waiting for that -kill's teardown - including
  its app termination - to actually finish.
- FBSession -fb_terminateTestedApplicationWithTimeout:: make the
  deferred main-thread -terminate call cancelable. If the bounded wait
  times out, the call is marked "given up on" under the same lock the
  deferred block checks before actually calling -terminate, so it can
  no longer fire later against a replacement session's app once -kill
  has reported its teardown finished.

All FBSessionTests pass; WebDriverAgentLib builds clean.
Those vendored dependencies were removed when the HTTP server was
unified on Network.framework; nothing in the tree references them
anymore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An app that stops answering accessibility requests blocks the whole agent forever, with no timeout left to bound it

3 participants