Skip to content

feat!: Sync with upstream appium/WebDriverAgent master (v16.8.0) - #28

Merged
Timo972 merged 34 commits into
masterfrom
timo/sync-appium-master-16.8
Aug 26, 2026
Merged

feat!: Sync with upstream appium/WebDriverAgent master (v16.8.0)#28
Timo972 merged 34 commits into
masterfrom
timo/sync-appium-master-16.8

Conversation

@Timo972

@Timo972 Timo972 commented Aug 25, 2026

Copy link
Copy Markdown

Merges appium/WebDriverAgent master (v16.4.0 → v16.8.0) into our fork.

What upstream changed

Conflict resolutions

  • standalone vs onControlQueue: both sides independently invented queue-bypassing routes. Adopted upstream's standalone API 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).
  • Automation funnel preserved: our serial funnel is now the server's routeQueue; non-standalone handlers hop to main via dispatch_sync from there. Concurrent automation requests still cannot execute reentrantly inside a handler that spins the main run loop. FBWebServerDispatchTests ported to FBHTTPServer — all 4 pass.
  • CocoaAsyncSocket restored: our audio/video streaming stack and the broadcast appex use 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.
  • FBSession: took upstream's new teardown-condition machinery, kept our @synchronized guards on _activeSession reads/writes (standalone routes read it off-main; upstream assigns it unlocked).
  • FBConfiguration: kept mobilerun no-quiescence defaults (waitForIdleTimeout=0, animationCoolOffTimeout=0), added upstream's accessibilityDeadline=0.
  • FBXCodeCompatibility: dropped our bounded testmanagerd version-exchange wait in favor of upstream's semaphore-based fix for the same hang.
  • CI: kept wda-package.yml deleted (hand-published releases); took upstream's env bumps in wda-tests.yml (watch vars unused by our matrix).

Verification

  • WebDriverAgentLib, WebDriverAgentRunner (with broadcast appex), and WebDriverAgentLib_watchOS build clean for simulator.
  • Full UnitTests suite passes, including the ported FBWebServerDispatchTests (standalone-responsiveness + no-reentrant-nesting) and FBRouteTests.
  • pbxproj validated: plutil parse OK, zero dangling refs, zero orphan build files.
  • tvOS lib not buildable locally (tvOS simulator platform not installed) — covered by CI.

🤖 Generated with Claude Code

mykola-mokhnach and others added 30 commits August 19, 2026 22:04
## [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))
## [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>
Timo972 and others added 4 commits August 26, 2026 12:14
- 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>
@Timo972 Timo972 changed the title Sync with upstream appium/WebDriverAgent master (v16.8.0) feat!: Sync with upstream appium/WebDriverAgent master (v16.8.0) Aug 26, 2026
@Timo972
Timo972 merged commit 34fb538 into master Aug 26, 2026
41 of 49 checks passed
@Timo972
Timo972 deleted the timo/sync-appium-master-16.8 branch August 27, 2026 09:04
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.

4 participants