feat(ios): add dartVmServicePort capability for deterministic Dart VM binding + log-filter fallback#870
Merged
KazuCocoa merged 4 commits intoMay 28, 2026
Conversation
Adds an `appium:dartVmServicePort` capability (iOS only) that pins the Dart VM service to a known port at app launch and provides a discovery fallback when the syslog scan exhausts. Background Flutter renamed the engine flag `--observatory-port` to `--vm-service-port` in Flutter 3.10 (engine commit 396c7fd0bd, Jan 2023). Customer app binaries built against Flutter >=3.10 no longer recognise the legacy alias — passing `--observatory-port` via processArguments is silently ignored and the Dart VM binds to an OS-assigned ephemeral port instead of the requested one. Conversely, Flutter <3.10 only recognises the legacy name. Without knowing the customer's bundled Flutter version at session creation time, there is no single flag name that works across the field. Separately, iOS unified-logging privacy filters can silently drop the `flutter: The Dart VM service is listening on http://127.0.0.1:<port>/` syslog line on a significant fraction of launches, causing the existing LogMonitor-based discovery to time out even on apps that bind correctly. What this adds `appium:dartVmServicePort: <number>` (iOS only). When set, the driver: 1. In `startIOSSession`, injects both `--vm-service-port=<port>` and `--observatory-port=<port>` (plus `--disable-service-auth-codes` if absent) into `caps.processArguments.args`. Any pre-existing entries for either flag are stripped first so the cap is authoritative. The Flutter engine silently ignores unknown flags, so sending both names is safe across all Flutter versions — each engine picks up whichever it knows. 2. In `getObservatoryWsUri`, when the syslog scan exhausts without finding the Dart VM service URL, fall back to `ws://127.0.0.1:<dartVmServicePort>/ws` instead of throwing. The engine was instructed to bind to this port at launch, so this is the correct connect target even when the discovery line was filtered. Pure opt-in. When the cap is not set, behaviour is identical to before. Honours the existing `observatoryWsUri` escape hatch (that takes precedence for connection). Scope iOS only. Android Flutter uses `optionalIntentArguments` with a different injection shape, and the flag-rename surface there has not been verified — left as a follow-up. Files * `lib/desired-caps.ts` — register the cap with `isNumber: true`. * `lib/sessions/ios.ts` — add `injectDartVmServicePortFlags` helper, call from `startIOSSession`, add fallback branch in `getObservatoryWsUri`. * `README.md` — document the new cap in the capabilities table. References * Flutter engine rename commit: https://chromium.googlesource.com/external/github.com/flutter/engine/+/396c7fd0bd324c74f1027b3a961e9269ed5ab63c%5E!/ * Dart 3.0 Observatory removal: dart-lang/sdk#50233 * Flutter 3.10 release notes: https://docs.flutter.dev/release/release-notes/release-notes-3.10.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
Simplify the injection to only the flags strictly required for the cap's self-contained behaviour on modern Flutter: * `--vm-service-port=<port>` — engine binding (Flutter >=3.10) * `--disable-service-auth-codes` — required so the fallback URL has no random auth-code path component `--observatory-port=<port>` (the legacy alias for Flutter <3.10) was dropped. Customer apps on Flutter <3.10 are an increasingly rare cohort, and users targeting them can continue to use `processArguments.args` directly or `observatoryWsUri`. Scope the cap as "Flutter >=3.10" in the JSDoc and README. The dedup filter now only strips pre-existing `--vm-service-port=*` entries (the cap is authoritative for the port). Pre-existing `--observatory-port=*` entries are left untouched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KazuCocoa
reviewed
May 27, 2026
| if (typeof port !== 'number') { | ||
| return; | ||
| } | ||
| caps.processArguments = caps.processArguments ?? {}; |
Member
There was a problem hiding this comment.
Suggested change
| caps.processArguments = caps.processArguments ?? {}; | |
| caps.processArguments ??= {}; |
Comment on lines
+175
to
+177
| urlObject = new URL(`http://${LOCALHOST}:${caps.dartVmServicePort}/`); | ||
| urlObject.protocol = `ws`; | ||
| urlObject.pathname += `ws`; |
Member
There was a problem hiding this comment.
Any reason to not set:
new URL(`ws://${LOCALHOST}:${caps.dartVmServicePort}/ws`);
?
Contributor
Author
There was a problem hiding this comment.
There’s no particular reason for that. The three-line http to ws conversion was reused from the surrounding implementation, where extractObservatoryUrl parses an http:// URL from the syslog and then converts it afterward. In this fallback flow, we’re constructing the URL directly, so using ws:// from the start is simpler and cleaner.
* Use logical-nullish-assignment for caps.processArguments default
(`??=` instead of `... = ... ?? {}`).
* Construct fallback URL directly as `ws://...` instead of building an
http URL and mutating protocol/pathname.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KazuCocoa
approved these changes
May 28, 2026
Member
|
|
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.
Summary
Adds an
appium:dartVmServicePortcapability (iOS only, Flutter ≥3.10) that pins the Dart VM service to a known port at app launch and provides a discovery fallback for the case where the syslog scan exhausts without finding the URL.Problem
The current driver discovers the Dart VM port by tailing iOS syslog for:
If that line doesn't deliver within ~30 s, the session dies with
No observatory URL matching .... Two real-world conditions make this fail:1. iOS unified-logging filters the discovery line non-deterministically
Apple's unified-log privacy/redaction filters silently drop the
flutter: …line on a significant fraction of launches. Empirical data on iPhone 15 Pro / iOS 17.1.1 across one day of production traffic on a single device: 25Runner(Flutter)PIDs launched, only 10flutter: …lines reached syslog — a ~60% filter rate. Same pattern observed on iOS 18.5 and iOS 26.4.1. Per-launch non-deterministic, OS-level, outside the driver's control.2. The port is non-deterministic when the engine doesn't honor the legacy flag
Engine commit
396c7fd0bd(Jan 2023, first in Flutter 3.10) renamed--observatory-port→--vm-service-port. Apps built against Flutter ≥3.10 silently ignore--observatory-portand bind to an OS-assigned ephemeral port. Without the syslog line, the driver has no way to discover what that random port is.What this PR adds
A single new capability:
appium:dartVmServicePort: <number>(iOS only).When set, the driver does two things — the user doesn't need to touch
processArgumentsdirectly:1. At session start (
startIOSSession): inject required engine flagsA new
injectDartVmServicePortFlagshelper mutatescaps.processArguments.argsto inject:--vm-service-port=<port>— tells the Flutter engine which port to bind. Requires Flutter ≥3.10.--disable-service-auth-codes— drops the random auth-code path component from the service URL so the fallback URL (constructed without observing the actual URL) is correct.Any pre-existing
--vm-service-port=*entries inprocessArguments.argsare stripped first so this cap is the authoritative source for the port. Other entries are left untouched.2. In discovery (
getObservatoryWsUri): fall back when scan exhaustsWhen
LogMonitor.waitForLastMatchExistreturns no match ANDdartVmServicePortis set, the driver dialsws://127.0.0.1:<dartVmServicePort>/wsinstead of throwing. The engine was instructed to bind to this port with no auth codes — so this is the correct URL even when the discovery line was filtered.When
dartVmServicePortis NOT set, behaviour is identical to upstream — driver throws the sameNo observatory URL matchingerror.How it works — scenarios
✅ Passing scenarios
LogMonitor, extracts URL with whatever port the engine bound to (which is the cap value since--vm-service-portwas honored), dials it.LogMonitorexhausts, fallback constructsws://127.0.0.1:<port>/wsfrom cap value, dials it. Engine is reachable there because we sent both--vm-service-port=<port>and--disable-service-auth-codes.dartVmServicePortandobservatoryWsUrisetobservatoryWsUritakes precedence for the connection target;dartVmServicePortstill injects flags so the engine binds where requested.observatoryWsUriis preserved unchanged.❌ Failing scenarios + how to avoid
--observatory-port, not--vm-service-port)--vm-service-port=N, binds to a random ephemeral port. Syslog leak path: driver reads the random port and connects (✅ — cap was effectively a no-op but session succeeds). Syslog filter path: fallback dials cap value, engine is on a random port,socket hang up(❌).--observatory-port=NviaprocessArguments.argsdirectly and rely on syslog scan only. Future Android-extension or backward-compat addition could mitigate this.FlutterDartProject(arguments: ["--vm-service-port=X"])with a hardcoded X different from the cap valuesocket hang up(❌).dartVmServicePort: X), or remove the hardcodedargumentsfrom the AppDelegate so the CLI flag wins.NSProcessInfo.argumentsto the engine (flutter#34731)socket hang up.NSProcessInfo.argumentstoFlutterDartProject. Not addressable from this cap.--vm-service-port=YinprocessArgumentsAND also sets the cap to a different value Z--vm-service-port=Ybefore injecting--vm-service-port=Z. Cap wins; user's value is silently overridden.Properties
processArguments, auth codes, or the Flutter flag rename.observatoryWsUriescape hatch (that takes precedence for connection).processArguments.argscontinue to work. Cap is independent.Scope
iOS only. Android Flutter uses
optionalIntentArgumentswith--ei observatory-port N(a different injection shape), and the flag-rename surface there has not been verified equivalently. Happy to follow up with an Android counterpart in a separate PR if there's interest.Flutter ≥3.10 only (engine commit
396c7fd0bdis when--vm-service-portwas added; older engines don't recognise it). Earlier scope iteration injected--observatory-portas a legacy alias but was dropped in5df06c3based on review feedback — cap is now narrower and cleaner.Files
driver/lib/desired-caps.tsdartVmServicePort: { isNumber: true }driver/lib/sessions/ios.tsinjectDartVmServicePortFlagshelper; call fromstartIOSSession; add fallback branch ingetObservatoryWsUriREADME.mdDiff: small (~
+73 / -6).Commit history
6070ab2--vm-service-port+--observatory-port+--disable-service-auth-codes.5df06c3← HEAD--observatory-port(Flutter <3.10 cohort is out-of-scope). Inject only--vm-service-port+--disable-service-auth-codes. Dedup filter only strips pre-existing--vm-service-port=*.Validation
npm run format:check— clean on modified files.npx eslint lib/— 0 errors. 5 warnings reported onlib/sessions/ios.tsare all pre-existing onmain(identical line numbers shifted by the new function).npm install(inIsolateSocket-related files) are pre-existing onmainfrom anrpc-websocketsv10 type drift — out of scope for this PR.Example usage
vs. without the cap, where users have to know both flag-name mechanics and disable-service-auth-codes implications:
References
396c7fd0bd— Jan 30, 2023Happy to iterate on naming, scope, or implementation based on review feedback.