feat!: Sync with upstream appium/WebDriverAgent master (v16.8.0) - #28
Merged
Conversation
## [16.5.0](appium/WebDriverAgent@v16.4.0...v16.5.0) (2026-08-20) ### Features * Add watchOS support to the TS driver, functional tests, and release pipeline ([appium#1217](appium#1217)) ([b53bb8f](appium@b53bb8f))
## [16.5.1](appium/WebDriverAgent@v16.5.0...v16.5.1) (2026-08-20) ### Bug Fixes * add watchOS assets to GitHub release artifacts ([appium#1219](appium#1219)) ([ce5a9e8](appium@ce5a9e8))
## [16.6.0](appium/WebDriverAgent@v16.5.1...v16.6.0) (2026-08-22) ### Features * Add MJPEG screenshot streaming support to watchOS ([appium#1220](appium#1220)) ([c6dcf03](appium@c6dcf03))
## [16.7.0](appium/WebDriverAgent@v16.6.0...v16.7.0) (2026-08-22) ### Features * unify HTTP server across iOS/tvOS/watchOS on Network.framework ([appium#1221](appium#1221)) ([cd741c5](appium@cd741c5))
## [16.7.1](appium/WebDriverAgent@v16.7.0...v16.7.1) (2026-08-23) ### Bug Fixes * return W3C-compliant JSON error for unmatched routes ([appium#1223](appium#1223)) ([c951a91](appium@c951a91))
## [16.7.2](appium/WebDriverAgent@v16.7.1...v16.7.2) (2026-08-24) ### Bug Fixes * harden FBHTTPServer/FBTCPSocket against races and protocol gaps ([appium#1224](appium#1224)) ([cf4bb2b](appium@cf4bb2b))
…ass the dispatch queue (appium#1222)
## [16.7.3](appium/WebDriverAgent@v16.7.2...v16.7.3) (2026-08-24) ### Bug Fixes * let /status, /screenshot, and DELETE /session API methods to bypass the dispatch queue ([appium#1222](appium#1222)) ([f99b011](appium@f99b011))
## [16.8.0](appium/WebDriverAgent@v16.7.3...v16.8.0) (2026-08-24) ### Features * bound accessibility snapshot requests to avoid indefinite hangs ([appium#1214](appium#1214)) ([cd829eb](appium@cd829eb))
Upstream replaced the vendored CocoaHTTPServer/RoutingHTTPServer stack with FBHTTPServer on Network.framework (appium#1221) and added standalone routes that bypass the dispatch queue (appium#1222). Resolutions: - Adopt upstream's standalone/isStandalone route API and migrate the fork's equivalent onControlQueue call sites (status, screencapture, unknown-command catch-alls) onto it. - Keep the fork's automation funnel by using it as FBHTTPServer's routeQueue and hopping to the main queue via dispatch_sync inside the handler, so concurrent automation requests still cannot nest inside a spinning run loop. FBWebServerDispatchTests ported to FBHTTPServer and passing. - Drop the fork's CocoaHTTPServer/RoutingHTTPServer hardening patches (obsolete; upstream hardened FBHTTPServer itself in 16.7.2). - Restore the CocoaAsyncSocket vendor library and its project wiring: droidrun's audio/video streaming and the broadcast appex still use GCDAsyncSocket. Fork-maintained from now on. - Keep FBSession's @synchronized reads/writes of _activeSession on top of upstream's new teardown-condition machinery. - Keep mobilerun no-quiescence defaults (waitForIdleTimeout=0, animationCoolOffTimeout=0) and add upstream's accessibilityDeadline. - Drop the fork's bounded testmanagerd version-exchange wait in favor of upstream's semaphore-based fix for the same hang. - Keep wda-package.yml deleted (fork releases are hand-published). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Terminate the tested app inline when -kill already runs on the main queue (every POST /session replacement does): the bounded dispatch to main would block main on its own semaphore, stall five seconds, and then give up without terminating the app. - Publish _isTeardownInProgress in the same critical section that clears _activeSession, so a concurrent +killActiveSessionAndWaitForTeardown can no longer observe a nil session with no teardown to wait for and launch a replacement whose app the still-running teardown then kills. - Raise the FBWebServerDispatchTests wait deadlines (5s -> 15s): the first run after a fresh test-runner install can exceed 5s and flaked once locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Parse Content-Length strictly instead of via -integerValue, which
silently maps garbage ("bogus" -> 0, "12abc" -> 12) to a wrong body
length and desyncs the connection's request framing, letting body
bytes be re-parsed as smuggled pipelined requests. An unparseable
value now gets a 400 and the connection is closed.
- Cap the buffered size of an incomplete header block (64 KiB): a
client that never sends the terminating CRLFCRLF could previously
grow the per-connection buffer without bound.
Both defenses existed in the vendored CocoaHTTPServer that upstream's
FBHTTPServer rewrite replaced (strict parseString:intoUInt64: and
MAX_HEADER_LINE_LENGTH/MAX_HEADER_LINES) and were lost in the rewrite.
Covered by the new FBHTTPServerFramingTests raw-socket unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 64 KiB cap only fired while the terminating CRLFCRLF was still missing; nw_connection_receive delivers up to UINT32_MAX bytes per callback, so a single large receive containing the terminator skipped the check and the oversized block was copied and parsed anyway. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reap connections that never deliver a complete request: a periodic sweep closes connections whose current request (first byte through declared body end) has not completed within 30 seconds, including peers that connect and send nothing. Idle keep-alive connections and requests already executing are exempt. The old CocoaHTTPServer stack bounded this with 30-second header read timeouts. - Bound MJPEG frame writes per client: nw_connection_send buffers without backpressure, so a viewer that stopped reading retained every generated frame until WDA ran out of memory. Frames for a client with MAX_PENDING_FRAMES_PER_CLIENT sends still outstanding are dropped instead of queued (the old GCDAsyncSocket path disconnected slow clients via a 1-second write timeout). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m sync Only FBRouteTests.m conflicted: master's timing tweaks to the spinning probe test (0.3 s gap, 1.0 s spin, expanded comment) are kept, with the comment's dispatch wording updated to the funnel-as-routeQueue architecture this branch introduces. FBSessionCommands' new cachedDeviceInfo pre-warm and the video-stream session hardening merged cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… body phase A valid declared body can legitimately take longer than 30 seconds to arrive (e.g. a large base64 payload over a slow USB tunnel); the reaper was closing such healthy uploads because the clock started at request arrival and was never refreshed. Body-phase progress now refreshes the deadline - the buffered size stays bounded by the already-validated Content-Length - while the header phase keeps the hard, non-refreshing deadline (drip-feeding there is additionally bounded by the 64 KiB cap). The replaced CocoaHTTPServer stack behaved the same way: 30-second header timeouts, unbounded body reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…back caching - Unblock the next pipelined request only from the response send's completion (nw_connection_send is FIFO per connection, so ordering is unchanged): a client that pipelined requests without reading responses could previously accumulate unbounded fully-rendered response buffers inside Network.framework. Also cap everything a connection may have buffered-but-unconsumed (body limit + 2x header cap) - the per-request checks don't run while a request is executing, so a client could pump data unboundedly for as long as its previous request took. - Reject whitespace between a header field name and its colon with 400, as RFC 7230 (3.2.4) requires: "Content-Length : 5" was stored under a "content-length " key, dispatching the request with a zero-length body and desyncing the connection's framing. - Cache the testmanagerd protocol version timeout fallback: retrying meant every /status against a degraded legacy daemon stalled for the full 20 seconds, making a bound WDA look permanently unavailable to health checks. The value is diagnostic-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… pipelining - Reject duplicate Content-Length/Transfer-Encoding headers with a 400 (RFC 7230 3.3.3) instead of collapsing them last-wins, and reject Transfer-Encoding on presence rather than on a non-empty value: a "chunked" header followed by an empty one used to look absent, so a chunked body was parsed as empty and its bytes re-read as smuggled requests. - Reject requests for a session that was already abandoned. The kill notification only reaches requests tracked at that moment, so one parsed afterwards queued on a possibly-wedged route queue with no abandonment ever coming; the abandoning response is now recorded (capped, keyed by the session's UUID) and returned immediately. Recorded under the same lock the abandonment takes, so nothing can slip in between. - Propagate nw_connection_send errors to the write completion. A failed response send was treated as success, unblocking the next pipelined request - so a mutating command could run for a connection that could no longer be answered. The connection and its buffered requests are now dropped instead. - Add accessibilityDeadline to the exported WDASettings/WDACapabilities TypeScript interfaces; the v16.8.0 setting was otherwise unusable from TS without a cast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Tag each session with a generation, bumped when one is marked active, and re-check it in the teardown steps that mutate process-wide state. +waitForActiveTeardownWithTimeout: is bounded at 35s while a worst-case teardown can reach ~30s (20s screen-recording stop + 5s system-app check + 5s terminate) before its own overhead, so an overrunning teardown could resume after a replacement session launched - and since the replacement usually runs the same bundle ID, "terminate the old app" would have killed the new one. The terminate re-checks inside the main-queue block (where the delay actually happens), and the screen-recording container reset is skipped for a stale generation so it can't drop a newer session's promise. - Reject non-empty header lines with no colon (400 + close) instead of skipping them: "Content-Length 5" used to dispatch with an empty body and leave its bytes to be read as another request, bypassing the surrounding framing checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ported from the review on appium#1226: - Bound the parsed Content-Length by NSUIntegerMax explicitly. The 15-digit cap alone is not enough on watchOS (arm64_32), where NSUInteger is 32-bit, and WebDriverAgentLib_watchOS is a real build target - truncation there would resurrect the framing desync this parser exists to prevent. - Clear pendingRequestHeaders in -closeClient: so a connection dropped by the reaper - precisely the state where a parsed header is cached and the body never completes - doesn't retain it. - Refresh the incomplete-request timestamp when the parser moves a request into its body phase: -client:didReceiveData: samples the phase before parsing, so the receive that completed a slow header block and carried the first body bytes left the connection on its header-phase deadline. - Tests: import <unistd.h>, send the payload in a loop, keep reading past recv timeouts unless the response is a keep-alive success so didClose is reliable, and assert didClose in both oversized-header tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Track in-progress teardowns with a count instead of a shared boolean. Because the teardown wait is bounded, a replacement session can be created - and later torn down itself - while an earlier teardown is still finishing. With a single flag the first to finish cleared it and woke waiters while the other was still running, so the next session creation could bump the generation and make that still-running teardown skip terminating its app and resetting the recording container. - Lift the reaper exemption and resume parsing in one step on bufferProcessingQueue, the same serial queue the reaper runs on. Removing the client from connectionsAwaitingResponse outside that queue exposed it to a sweep already queued ahead of the parse, which would judge an already fully-buffered pipelined request by the previous (possibly 30s+) request's timestamp and close the connection. A mid-request connection also gets its window measured from when parsing could actually resume; idle keep-alive connections stay exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… API Upstream's FBTCPSocket rewrite (appium#1221) unified the delegate on nw_connection_t, dropping the iOS/tvOS GCDAsyncSocket variant. Upstream ported its own consumer (FBMjpegServer); droidrun's video and audio capture sessions were not, so both silently stopped conforming to FBTCPSocketDelegate: they still declared GCDAsyncSocket * parameters and called connectedHost, writeData:withTimeout:tag: and readDataWithTimeout:tag: on what is now an nw_connection_t. The first selector sent to a connecting client would have raised unrecognized-selector, taking down capture entirely. - Port both delegates to nw_connection_t, replacing didClientSendData: with client:didReceiveData: (FBTCPSocket drives the receive loop itself, which is what surfaces disconnects - the client never sends). - Send through the socket wrapper, with a per-client backlog cap. nw_connection_send buffers without backpressure, so a client that stops draining is disconnected once its backlog exceeds ~1s of the configured bitrate - the same outcome the old 1s write timeout gave. Deliberately NOT the MJPEG server's drop-frames strategy: H.264 is inter-frame coded and Opus packets are not independently decodable, so dropping corrupts the stream instead of degrading it, and the client has no read timeout to notice. Disconnecting yields EOF, which the client already treats as a recoverable restart. - Move TCP_NODELAY into FBTCPSocket as an opt-in `noDelay` (Network configures it via listener parameters; an accepted connection exposes no descriptor). Only the capture sockets enable it, so the HTTP server's behaviour is unchanged. Losing it would let Nagle coalesce access units, which skews the client's wall-clock-derived RTP timeline. - Config packets are now emitted under the same lock that adds the client, so no frame can be broadcast between the two - the client requires config before the first key frame on every connection. Wire format is untouched: FBScrcpyPacketCreate and every payload are unchanged. New FBAudioStreamSocketTests covers the delivery path over a real TCP connection - config-on-connect, big-endian 12-byte framing, flag exclusivity, non-zero length, and pts round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re launch - Register /health, /calibrate and /wda/shutdown as standalone routes. Setting the automation funnel as the server-wide routeQueue captured them too, since get:withBlock: registers non-standalone - so the two endpoints whose whole purpose is to answer while automation is wedged (/health as liveness, /wda/shutdown as the way out) started queueing behind the very request they exist to diagnose. This restores the behaviour docs/request-dispatch.md already describes. /mobilerun/state stays on the funnel deliberately: it reports wedging by timing out. - Claim the session generation in +killActiveSessionAndWaitForTeardown rather than in +markSessionActive:. handleCreateSession: calls the former before preparing the application and the latter only after, so a teardown that outlived the bounded wait still counted as current while the replacement app was launching - and could terminate that freshly launched process, which usually shares its bundle identifier. The generation is now claimed at the moment a caller takes ownership of the device, before any launch side effects. FBWebServerDispatchTests gains a regression test that wedges the automation queue and requires /health to answer anyway; it fails against the previous registration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies the review feedback from the upstream PRs (appium/WebDriverAgent appium#1226, appium#1227, appium#1229) across the fork, so the shared files do not drift apart again on the next sync. RFC citations and the two facts a reader cannot get from the code - nw_connection_send has no backpressure signal, and NSUInteger is 32-bit on watchOS arm64_32 - are kept; the narration is gone. Also fixes a stale forward declaration of -fb_terminateTestedApplicationWithTimeout: that still omitted the generation: parameter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges
appium/WebDriverAgentmaster (v16.4.0 → v16.8.0) into our fork.What upstream changed
FBHTTPServerbuilt on Network.framework, unified across iOS/tvOS/watchOS (the separateFBWatchHTTPServeris gone)./status,/screenshot,DELETE /session) that bypass the dispatch queue, with per-endpoint coalescing of identical concurrent requests./calibratedeprecation.Conflict resolutions
standaloneAPI and migrated our call sites (/status, screencapture status/stop/keyframe routes, unknown-command catch-alls). Coalescing is safe for all of them (idempotent reads/stops).routeQueue; non-standalone handlers hop to main viadispatch_syncfrom there. Concurrent automation requests still cannot execute reentrantly inside a handler that spins the main run loop.FBWebServerDispatchTestsported to FBHTTPServer — all 4 pass.GCDAsyncSocket, so the vendor lib and its project wiring are kept (fork-maintained from now on). The CocoaHTTPServer/RoutingHTTPServer hardening patches we carried are dropped — obsolete with the new server.@synchronizedguards on_activeSessionreads/writes (standalone routes read it off-main; upstream assigns it unlocked).waitForIdleTimeout=0,animationCoolOffTimeout=0), added upstream'saccessibilityDeadline=0.wda-package.ymldeleted (hand-published releases); took upstream's env bumps inwda-tests.yml(watch vars unused by our matrix).Verification
WebDriverAgentLib,WebDriverAgentRunner(with broadcast appex), andWebDriverAgentLib_watchOSbuild clean for simulator.UnitTestssuite passes, including the portedFBWebServerDispatchTests(standalone-responsiveness + no-reentrant-nesting) andFBRouteTests.🤖 Generated with Claude Code