Repository navigation
[flutter_tools] Wait for iOS simulator log stream before launch - #193142
Conversation
There was a problem hiding this comment.
Code Review
This pull request modifies the iOS simulator to wait for the log stream to be ready before launching an application, addressing potential race conditions where startup logs are missed. The review feedback highlights that because the log reader is cached and reused, the _ready completer must not be final and needs to be reset on subsequent launches. Additionally, the completer should be completed immediately if the log process fails to start or falls back to syslog parsing to prevent hangs. Finally, the feedback suggests catching the TimeoutException and throwing a descriptive ToolExit with an updated test expectation, aligning with the Flutter Style Guide's rule on useful error messages.
| /// | ||
| /// `log stream` is ready once it prints its first line (a "Filtering the log | ||
| /// data using ..." header); events logged before that are dropped. | ||
| final _ready = Completer<void>(); |
There was a problem hiding this comment.
_ready is created once per _IOSSimulatorLogReader, but a log stream process is created once per listen cycle, and the reader is cached per app in _logReaders (L406 / L644). After the first launch completes, a second startApp on the same device + app skips the wait entirely and we are back to the race from #181771.
That second launch does happen in flutter_tools: flutter test integration_test/ with more than one test file. Each file gets its own IntegrationTestTestDevice with the same device (flutter_platform.dart L481-L489), nothing else listens to the log reader on that path ("No need to set up the log reader…", integration_test_device.dart L77-L78), so startApp's finally { await vmServiceDiscovery?.cancel(); } (L561) drops the last listener → onCancel: _stop kills log stream. The next file's startApp → ProtocolDiscovery.vmService(...) listens again → onListen: _start spawns a new log stream ("If a listener is added again later, after the onCancel was called, the onListen will be called again" — https://api.dart.dev/stable/dart-async/StreamController/StreamController.broadcast.html) — but _ready is already complete, so simctl launch runs immediately.
| final _ready = Completer<void>(); | |
| var _ready = Completer<void>(); |
plus, as the first statement of _start():
// Every (re-)listen starts a new `log stream`, so wait for it again.
_ready = Completer<void>();Also capture the completer in a local inside _start() and use that local in the stdout listener and in exitCode.whenComplete (L917), otherwise a late exit of the previous process can complete the new completer; and a regression test for the second launch
There was a problem hiding this comment.
Done: _ready is now recreated on every listen and captured in a local, and the regression test launches the same package twice. 2656461
One thing I couldn't reproduce: IntegrationTestTestDevice creates a new ApplicationPackage for each file, and _logReaders is keyed by that instance. In a unit test, reusing the same ApplicationPackage for the second startApp skipped the wait (before this change), but creating a new one each time, as IntegrationTestTestDevice does, didn't (test below). The same instance comes back when a daemon client reuses a package id, e.g. ProxiedDevice. Is that the setup you had in mind?
Test I used
testUsingContext(
'startApp waits for a new log stream when called again with a new package',
() {
fakeAsync((FakeAsync async) {
final firstLogProcess = FakeStreamingProcess();
final secondLogProcess = FakeStreamingProcess();
processManager.addCommand(FakeCommand(command: logStreamCommand, process: firstLogProcess));
processManager.addCommand(FakeCommand(command: logStreamCommand, process: secondLogProcess));
final IOSSimulator device = buildDevice();
// First test file.
device.startApp(
buildPackage(),
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
);
firstLogProcess.write('Filtering the log data using "type == 1024"\n');
async.elapse(const Duration(seconds: 1));
firstLogProcess.write(
'"eventMessage" : "The Dart VM service is listening on http://127.0.0.1:1234/abcdef/"\n',
);
async.elapse(const Duration(seconds: 1));
expect(simControl.requests, hasLength(1));
// Second test file: a new package, as IntegrationTestTestDevice.start creates.
device.startApp(
buildPackage(),
prebuiltApplication: true,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
);
async.elapse(const Duration(seconds: 1));
expect(simControl.requests, hasLength(1));
secondLogProcess.write('Filtering the log data using "type == 1024"\n');
async.flushMicrotasks();
expect(simControl.requests, hasLength(2));
expect(processManager, hasNoRemainingExpectations);
});
},
overrides: <Type, Generator>{
PlistParser: () => testPlistParser,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Logger: () => logger,
Xcode: () => xcode,
},
);Before this change, passing the same package to both calls made the second expect(simControl.requests, hasLength(1)) fail.
| await logReader._ready.future.timeout( | ||
| timeout, | ||
| onTimeout: () => | ||
| throw TimeoutException('The iOS simulator log stream did not start.', timeout), | ||
| ); |
There was a problem hiding this comment.
on timeout, printError and return LaunchResult.failed() like every other failure in this method, instead of letting the TimeoutException escape.
| await logReader._ready.future.timeout( | |
| timeout, | |
| onTimeout: () => | |
| throw TimeoutException('The iOS simulator log stream did not start.', timeout), | |
| ); | |
| try { | |
| await logReader._ready.future.timeout(timeout); | |
| } on TimeoutException { | |
| globals.printError( | |
| 'The iOS simulator log stream did not start within ${timeout.inSeconds} seconds.', | |
| ); | |
| await vmServiceDiscovery.cancel(); | |
| return LaunchResult.failed(); | |
| } |
There was a problem hiding this comment.
Done: on timeout, startApp now prints the error and returns LaunchResult.failed(), like the other failures in this method.
| ); | ||
| // Launch the app only after the log stream is ready; otherwise the | ||
| // Dart VM Service URL it logs on startup can be missed. | ||
| const timeout = Duration(seconds: 30); |
There was a problem hiding this comment.
was 30 s measured or picked?
There was a problem hiding this comment.
Picked, with a measurement as a guide: locally, log stream prints its first line in under 1 s, so 30 s leaves plenty of margin.
The main point is to have a timeout at all, so that a log stream that never becomes ready fails the launch instead of hanging forever. Would 60 s or 300 s be better, e.g. for slower CI machines?
There was a problem hiding this comment.
30s SGTM. Going to 60 s or 300 s only lengthens the hang before a user sees "did not start" when something could be genuinely broken
6996a5b to
2656461
Compare
| ); | ||
| // Launch the app only after the log stream is ready; otherwise the | ||
| // Dart VM Service URL it logs on startup can be missed. | ||
| const timeout = Duration(seconds: 30); |
There was a problem hiding this comment.
30s SGTM. Going to 60 s or 300 s only lengthens the hang before a user sees "did not start" when something could be genuinely broken
|
autosubmit label was removed for flutter/flutter/193142, because This PR has not met approval requirements for merging. The PR author is not a member of flutter-hackers and needs 1 more review(s) in order to merge this PR.
|
…ter#181771) The -v run 37799511552 showed where the first attempt waits: the tool printed "Waiting for VM Service port to be available..." and never "VM Service URL on device", while the app and the tool's log stream stayed alive for 9.5 minutes. The tool starts "simctl launch" and "log stream" in the same instant, and log stream drops everything logged before its first line, so it can miss the URL the app prints at startup. The Dart entrypoint is fine: whenever the tool got the URL it found the isolate at once and the three tests passed. Upstream PR flutter/flutter#193142 (approved, not released yet) launches the app only after log stream has printed its first line, with a 30 s limit. The ios job now applies its simulators.dart part to the 3.44.6 SDK: - tools/ci/flutter-181771.patch is rebuilt against the 3.44.6 file, because the upstream hunk context has a cpuArch getter that 3.44.6 lacks. It applies and reverses cleanly, and the patched file passes dart analyze; - the new step applies it (idempotent) and removes the tool stamp so the flutter script rebuilds the tool. A Flutter upgrade makes the apply fail loudly; - attempt 1 runs without -v. Attempts 2 and 3 keep -v and first kill the earlier attempt's flutter tool and log stream, which survive the step timeout. The warning of attempt 2 now says "trying again"; - the design doc says that CI patches the tool. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01G3n8VQCkYmLnMTjmfu5PUV
| // Dart VM Service URL it logs on startup can be missed. | ||
| const timeout = Duration(seconds: 30); | ||
| try { | ||
| await logReader._ready.future.timeout(timeout); |
There was a problem hiding this comment.
You can use a timer to print a warning that it's taking longer than expected, but it shouldn't cancel/fail. See example:
flutter/packages/flutter_tools/lib/src/ios/devices.dart
Lines 688 to 709 in edacdd1
There was a problem hiding this comment.
Switched to a timer that only prints a warning. 6d3d865
I'd used a timeout because I thought that if this log output takes abnormally long in flutter drive, something must have gone wrong.
| /// Completes once the log stream is ready to deliver events, or has exited. | ||
| /// | ||
| /// `log stream` is ready once it prints its first line (a "Filtering the log | ||
| /// data using ..." header); events logged before that are dropped. |
There was a problem hiding this comment.
| /// data using ..." header); events logged before that are dropped. | |
| /// data using ..." header). |
I don't see where events are being dropped?
flutter/flutter@426be96...5c7e45a 2026-10-15 [email protected] [web_paragraph] Honor TextHeightBehavior.leadingDistribution and default to proportional (flutter/flutter#193736) 2026-10-15 [email protected] Roll Skia from 79a00d7c4898 to d131b7789482 (2 revisions) (flutter/flutter#194446) 2026-10-15 [email protected] Roll Dart SDK from b8ffa9233096 to 84686ea04815 (1 revision) (flutter/flutter#194442) 2026-10-15 [email protected] Roll Dart SDK from e81af2dac9d1 to b8ffa9233096 (1 revision) (flutter/flutter#194432) 2026-10-15 [email protected] Roll Skia from ef24b8f87a2a to 79a00d7c4898 (4 revisions) (flutter/flutter#194431) 2026-10-15 [email protected] Roll Skia from 48c51004ee4e to ef24b8f87a2a (1 revision) (flutter/flutter#194427) 2026-10-15 [email protected] Roll Dart SDK from 6b50116630d0 to e81af2dac9d1 (5 revisions) (flutter/flutter#194422) 2026-10-15 [email protected] Roll Skia from b1be97266d3a to 48c51004ee4e (10 revisions) (flutter/flutter#194421) 2026-10-15 [email protected] RawAutocomplete: Fix focusNode listener mismatch in didUpdateWidget/dispose (flutter/flutter#190686) 2026-10-14 [email protected] [flutter_tools] Wait for iOS simulator log stream before launch (flutter/flutter#193142) 2026-10-14 [email protected] Move Material/Cupertino PRs from design triage to framework triage (flutter/flutter#194244) 2026-10-14 [email protected] refactor(flutter_tools): share FlutterCommand options and OptionBundles (flutter/flutter#194356) 2026-10-14 [email protected] Roll pub packages (flutter/flutter#194413) 2026-10-14 [email protected] Add ContextMenuButtonKind (flutter/flutter#193528) 2026-10-14 [email protected] Split SwiftPM plugin into multiple products (flutter/flutter#193521) 2026-10-14 [email protected] Roll Skia from 43d707a3be0e to b1be97266d3a (8 revisions) (flutter/flutter#194392) 2026-10-14 [email protected] Roll pub packages (flutter/flutter#194398) 2026-10-14 [email protected] [flutter_tools] Migrate AndroidSdk and AndroidStudio away from globals (flutter/flutter#193650) 2026-10-14 [email protected] [flutter_tools] Reload widget-preview when data assets change (flutter/flutter#193437) 2026-10-14 [email protected] [flutter tool] suppress sub-tool analytics during first run (flutter/flutter#194320) 2026-10-14 [email protected] Roll Skia from acde3755d2e2 to 43d707a3be0e (1 revision) (flutter/flutter#194385) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
* [CI] iOS lane에 Xcode 빌드 캐시와 시뮬레이터 로그 스트림 대기 수정 적용 - iOS job: DerivedData와 SwiftPM SourcePackages를 Xcode·Flutter toolchain, pubspec.lock, ios 소스로 만든 키로 캐시한다. - Flutter 3.44.0 tool이 log stream 준비 전에 앱을 실행해 VM service 주소를 놓치는 경합(flutter/flutter#181771)을 상류 수정(flutter/flutter#193142)으로 막는다. 적용할 수 없거나 스냅샷에 반영되지 않으면 실패한다. Refs AquilaXk/easysubway#3036 * [CI] 임시 진단: iOS 빌드 단계별 시간 기록 (병합 전 제거) * [CI] 임시 진단: 연속 두 번 빌드로 증분 빌드 효과 측정 (병합 전 제거) * [CI] 복원한 캐시의 inode 변경을 Xcode와 git이 무시하도록 설정 * [CI] 임시 진단: 복원한 캐시로 실제 drive 빌드가 재사용되는지 측정 (병합 전 제거) * [CI] 임시 진단: drive 단계별 시간 기록 (병합 전 제거) * [CI] 임시 진단: inode/git 설정 없이 복원 빌드 재사용 여부 확인 (병합 전 제거) * [CI] 임시 진단 단계 제거: 캐시된 빌드는 inode 설정 없이도 증분 빌드됨을 확인 * [CI] 드라이버가 앱 시작 뒤에 붙이는 log stream도 준비될 때까지 기다리도록 Flutter 패치 확장 테스트 첫 줄(NATIVE_SYSTEM_TEXT_SCALE=)이 새로 뜬 log stream 준비 전에 나가 사라지는 경합(CI 2회 font_scale_mismatch)을 막는다. * [CI] 임시 프로브: 증분 빌드가 네이티브·Dart 변경을 반영하는지 확인 v1 (revert 예정) * [CI] 임시 프로브 v2: v1 캐시에서 복원해 증분 빌드해도 v1이 남지 않는지 확인 (revert 예정) * [CI] 임시 프로브 제거, 캐시 사용 결과 보고를 exact/partial/miss로 정리 프로브 확인: v1 캐시에서 v2 소스를 증분 빌드하면 v1 문자열은 사라지고 v2만 남는다(네이티브 Runner.app, Dart 로그 모두). * [CI] iOS 시뮬레이터 부팅을 job 맨 앞에서 시작해 준비 단계와 겹친다 * [CI] 시뮬레이터 부팅 선행 시도 되돌림: simctl boot가 부팅이 끝날 때까지 막아 겹치지 않음 * [CI] 시험: iOS 시뮬레이터 부팅을 백그라운드로 먼저 시작 * [CI] 시뮬레이터 백그라운드 부팅 시도 되돌림: 3코어 러너에서 다른 준비 단계가 느려져 이득이 불확실 * [CI] 리뷰 F1~F3 반영: store 가드 복원, 캐시 저장을 main push로 한정, SDK 캐시 키에 패치 해시 - 가드 정규식을 (?<!re)store로 바꿔 keystore·datastore·appstore·app_store를 다시 막고 restore만 허용한다. - actions/cache를 restore와 save로 나누고 save는 main push에서 정확한 일치가 없을 때만 한다. - flutter-action SDK 캐시 키에 패치 파일 해시를 넣고, 적용 실패 안내에 SDK 캐시 비우기·패치 파일명 변경을 적었다.
The ios workflow's simulator step hung three times on 2026-10-24 and 2026-10-25 (on 91eced2, 2fe6d08 and c1f0e6b), each time until its 25-minute limit, the Flutter tool's last line "Waiting for VM Service port to be available..." and the app idle on the simulator. The tool learns the address of the app's Dart VM service from the one line the app logs it in, through a `log stream` it starts as it launches the app, and waits for it with no limit (startApp in packages/flutter_tools/lib/src/ios/simulators.dart). In 3.47.2 nothing waits for that stream to be ready, so when it starts late the line is missed and the wait never ends. That is flutter/flutter#181771, which Flutter fixed in flutter/flutter#193142 (c3ed85ef), in no release yet: not 3.47.8, the latest stable. third_party/flutter/simulator_log_stream.patch is that fix, fitted to 3.47.2, with its tests: one line of context differs, and the tests' simulator is built without the cpuArch argument 3.47.2 does not take. On 3.47.2 with only its tests applied, the two that wait for the log stream fail, the app launched before the stream printed anything; with the fix, all 53 of the simulator tests pass. The ios workflow applies it to the Flutter it sets up, empties the tool's stamp so that the flutter command builds its tool again from the patched source, and checks that the tool it built has the fix. Run on the 3.47.2 release archive as flutter-action installs it, with no .dart_tool, so that the rebuild's pub upgrade makes the package config: the tool builds in about 30 s, Flutter still reports 3.47.2 on stable and pub still sees 3.47.2, and a second run, on a Flutter that has the fix already, passes too. The step fails if the tool is not built again, if the fix is not applied, or if the patch does not fit: 3 of 3 mutants caught. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01W92ERJ2vuziEpghjrqPe5w
…ter#193142) Fixes flutter#181771 Fixes a race where `flutter test integration_test` on an iOS simulator could hang forever at "Waiting for VM Service port to be available...". `_IOSSimulatorLogReader` starts `xcrun simctl spawn … log stream` from `onListen`, and nothing waited for the process to start, so `startApp` could launch the app before `log stream` was ready. `log stream` drops events logged before it prints its first line (`Filtering the log data using ...`), so the Dart VM Service URL could be missed. The verbose log attached to flutter#181771 shows `simctl launch` running before `simctl spawn … log stream`. This PR makes `startApp` wait, right before `simctl launch`, until `log stream` prints its first line or exits. If that does not happen within 30 seconds, `startApp` prints a warning and keeps waiting. A log reader starts a new `log stream` each time it is listened to again, so the wait applies to each of them. The process is still started from `onListen`, so `flutter run`, which listens to the log reader before building the app, still starts `log stream` during the build. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [x] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Elijah Okoroh <[email protected]> Co-authored-by: Victoria Ashworth <[email protected]>
…hang In run 42325860453 the iOS Dart suite hung again, verbose this time: flutter test launched the app and logged "Waiting for VM Service port to be available..." for 6 minutes. That's flutter/flutter#181771: flutter test finds the app's VM service in the simulator's log through `log stream`, which drops events logged before it prints its first line, so the URL is sometimes missed and flutter waits forever. Its fix, flutter/flutter#193142, is on master only. The iOS job now applies its lib/ change, fitted to 3.47.7, to the SDK and rebuilds the tool. The step fails once the patch stops applying, i.e. when .fvmrc moves, which is when to check whether the new release has the fix and drop it. The Android Dart suite passed in that run, with logcat recorded; its hang in run 42324403392 is still unexplained. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
* Add CI workflow One workflow, ci.yaml, with a job per check: Dart format/analyze/unit tests, the Go test server, the Swift core tests on macOS, the Android example build, and the iOS simulator and Android emulator integration tests, with the Dart and native bridge suites as separate steps. ci-ok depends on all of them, for use as the single required check. run_integration_tests.sh takes FLUTTER to run without fvm, and writes one log per platform and suite. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Drop the separate Android build job; always cancel in-progress runs android-integration already builds the example and the plugin. Cancel superseded runs on main too, not just on PRs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Cancel in-progress runs only on PRs Runs on main always finish, so every commit there gets a result. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Add a timeout to every CI step A hung step now fails on its own name instead of using up the job's budget. The emulator gets a 5 minute boot timeout. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Give the Android emulator enough disk to start The emulator exited at startup on a runner with 6.6 GB free (it needed 7.4 GB for userdata). Free preinstalled toolchains first and shrink the emulator disk to 4 GB. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Build while the devices boot; fix the script on bash 3.2 Android: start the emulator ourselves at the top of the job and build the example and the plugin's instrumented tests while it boots, then run the Dart and native suites as separate steps. Cache the system image and Gradle. iOS: boot the simulator before setting up the toolchains. run_integration_tests.sh: macOS's bash 3.2 treats an empty EXTRA_ARGS as unbound under set -u, and with the EXIT trap set the script then exited 0, so the iOS Dart suite never ran but passed. DEBUG: a watchdog in the iOS native step reports what keeps the step running after its tests pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Pin the AVD home; skip native suites when the device never came up avdmanager put the AVD outside ~/.android/avd on the runner, so the config edit failed. Set ANDROID_AVD_HOME for every tool. The native steps ran after the emulator setup failed, since !cancelled() ignores earlier failures. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Run the setup steps in parallel iOS: set up Go and Flutter and boot the simulator in one parallel group. Android: free disk space, enable KVM and restore the system image cache in parallel, then start the emulator and set up Java, Go and Flutter in parallel. The test suites stay sequential. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Boot the iOS simulator after setup, not alongside it Its first-boot data migration saturates the macOS runner's disk: in parallel, the Flutter SDK download fell from 36 MB/s to under 1 MB/s and setup-go timed out. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Stop the test server reliably, so the native step exits On the macOS runner the iOS native step hung after its tests passed: the test server outlived the script, re-parented to launchd, and held the log's tee open. The EXIT trap that should have stopped it was set inside `run | tee`'s subshell. Run in the main shell, logging through exec > >(tee), stop the server explicitly at the end of run as well as from the trap, and drop the debug watchdog that found this. Co-Authored-By: Claude Opus 5.5 <[email protected]> * DEBUG: rerun the iOS Dart suite and record the app's network logs The first few iOS Dart cases time out before the TLS ClientHello on every run. A second run shows whether that comes from the freshly booted simulator; the network log shows what the connections wait on. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Warm up the iOS simulator with the native suite; tolerate HTTP/2 fallback Right after boot, the first minute of Dart-suite connections stalled before the TLS handshake, and a second run passed. Run the native XCTest suite first, which also exercises URLSession in the simulator. URLSession races QUIC against TCP for each new connection and keeps whichever connects first; on the CI runner TCP sometimes wins, so the HTTP/3-only cases landed on HTTP/2. They now find a client whose connection negotiated HTTP/3 (checked with the test server's X-Proto), trying fresh clients for up to 8 s. Keep recording the simulator's network log as a CI artifact; drop the debug rerun. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fail when an HTTP/3 origin falls back to TCP locally; trace the server's QUIC On the CI runner, URLSession sometimes connected to the local HTTP/3 origin over TCP: the server hadn't answered the QUIC Initial 230 ms after it was sent, while it answered TLS over TCP in 125 ms. Fallback is expected on real networks, but not on this path, so a new case fails when a fresh client's first call isn't HTTP/3. test_server: let quic-go size the UDP socket's buffers (its 'Not a *net.UDPConn' warning), without exposing the socket in a way that bypasses Freeze, and write qlog traces to $QLOGDIR. CI uploads them with the simulator's network log. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Document FLUTTER, the log files, QLOGDIR and the strict HTTP/3 case Co-Authored-By: Claude Opus 5.5 <[email protected]> * Cache iOS and Gradle builds; prebuild the native tests iOS rebuilt everything on every run: cache Xcode's derived data (its simulator SDK module cache above all), the Flutter app's build directory and the core tests' build. The core tests' build moves to ios/connect_native_http/build_core_tests.sh, for a generic simulator destination, which CI runs alongside the toolchain setup and run_integration_tests.sh then finds up to date; it took 62-192 s of the native step, against 1.4 s of tests. Android cached Gradle's dependencies but not its outputs: enable the build cache and keep ~/.gradle/caches/build-cache-1 between runs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Time out every integration-test command; tighten test and step timeouts flutter test once built and launched the app on CI, then waited 13 minutes without connecting to it, until the step timed out. run_integration_tests.sh now runs each command under a watchdog (macOS has no timeout(1)) that kills its whole process tree, so a hang fails in minutes and names the command. Test cases time out after 10 s (20 s for the HTTP/3 ones), and CI steps and jobs are sized to their observed durations. Commit the project.pbxproj references Flutter's Swift Package Manager integration adds for the plugin and FlutterFramework, which it otherwise rewrites on every build. DEBUG: run the iOS Dart suite with -v. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Make build_core_tests.sh executable; list the booted simulators The iOS job's native build failed with Permission denied: the script was committed as 644. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Create the native tests' build directory's parent before resolving it On a fresh CI checkout build/ doesn't exist (the iOS build cache has never been saved, since every iOS job so far failed), so build_core_tests.sh's cd into it failed. set -e didn't stop it, because an assignment's status is its last command substitution's (basename), so the derived data path became /ios_core_tests and xcodebuild failed on the read-only root. run_integration_tests.sh creates build/ first, which is why it only failed when CI called the script directly. Create the parent, and resolve it on a line of its own so a failed cd stops the script. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s * test_server: warm up both protocols before reporting ready In run 42319973108's qlog, the server's first two QUIC connections (one per address family, from URLSession's Happy Eyeballs) took 86-159 ms from the client's Initial to the server's reply, against 2 ms for every later one. URLSession races QUIC against TCP and had connected over TCP by then, so the suite's first call went over HTTP/2; the client never sent another QUIC packet. Run 42316536743 shows the same: the first request was HTTP/2 and every later one HTTP/3. The server now sends itself one request over HTTP/3 and one over HTTP/2, through loopback and verified against its CA, before it starts the admin endpoint that callers poll for readiness. It logs how long each took, which shows the cold-start cost on CI. The TCP listener is opened before serving, so the warm-up can't race it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s * Revert "test_server: warm up both protocols before reporting ready" It didn't help, and it ruled out the cause it was written for. In run 42324403392 the warm-up took 7 ms, and the first call of each suite still went over HTTP/2. The simulator's network log shows why: when the app makes its first call, right after launch, the whole runner is CPU-starved. URLSession only started the QUIC connection 142 ms after its path was ready, TLS over TCP on loopback took 155 ms, and the server handled the QUIC Initial after the client had already picked TCP and cancelled QUIC. That is URLSession racing the two as designed, under load, not a slow QUIC path in the server or the library. The strict "first call to an HTTP/3 origin goes over HTTP/3" case, a later call, passed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s * Record logcat during the Android suites; run the Android Dart suite verbose In run 42324403392, flutter test built and installed the example app on the emulator, then printed nothing for 8 minutes until the script's watchdog killed it; the server saw no request. It's the same symptom as the iOS launch hang. On iOS that's a known flutter_tools race (flutter/flutter#181771: it can miss the VM service URI in the simulator's log, fixed on master only), but the Android reader replays logcat from before it attaches, so that race doesn't apply. logcat and flutter's verbose output show whether the app crashed, never started, or announced a VM service flutter didn't see. Raise the logcat buffer to 16M, which the emulator tries to do itself but fails while the device is still offline: it logs everything verbosely, and the default buffer wraps within seconds. DEBUG: -v on the Android Dart suite. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s * Patch flutter_tools for the iOS simulator log race behind the launch hang In run 42325860453 the iOS Dart suite hung again, verbose this time: flutter test launched the app and logged "Waiting for VM Service port to be available..." for 6 minutes. That's flutter/flutter#181771: flutter test finds the app's VM service in the simulator's log through `log stream`, which drops events logged before it prints its first line, so the URL is sometimes missed and flutter waits forever. Its fix, flutter/flutter#193142, is on master only. The iOS job now applies its lib/ change, fitted to 3.47.7, to the SDK and rebuilds the tool. The step fails once the patch stops applying, i.e. when .fvmrc moves, which is when to check whether the new release has the fix and drop it. The Android Dart suite passed in that run, with logcat recorded; its hang in run 42324403392 is still unexplained. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s * Detect the flutter_tools patch by its content, not by reversing it Run 42327069006 reported "already applied" on its first run with the patch, and flutter --version didn't rebuild the tool: the SDK went unpatched, and the iOS suite passed without the fix. macOS's patch takes a reverse of a patch that isn't applied as a forward one, so `patch -R --dry-run` succeeds either way. Check for a line only the patch adds instead, and fail if it's still missing after patching. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
…hang flutter test hangs before the tests start on most first attempts in CI: it launches the app without waiting for its Simulator log stream to attach, and can miss the line that gives the app's VM service address (flutter/flutter#181771, fixed by flutter/flutter#193142 after 3.50 branched). Diagnosed on a throwaway branch: the app was alive and had logged the address, which a stream attached earlier caught. The script treats 2 minutes of silence after "Xcode build done." as the hang (a healthy run starts testing within 30 seconds), and also retries "Unable to start the app on the device" (xcodebuild -showBuildSettings timing out). Up to 3 attempts, each retry flagged as a warning; test failures aren't retried. In 8 runs of it, 6 first attempts hung and every retry passed, at a cost of about 3 minutes rather than a 20-minute timeout. It replaces the timeout-and-reboot retry steps on GitHub and Azure, and Azure's iOS job goes back to 60 minutes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…or launching (#451) The iOS runtime-material job intermittently builds and launches successfully, then waits indefinitely for Flutter to discover the Dart VM-service address. Increasing the job timeout would prolong that wait. A verbose repeat captured successful installation and `simctl launch`, followed by `Waiting for VM Service port to be available...` with no test-runner connection. Its trace starts `simctl launch` before starting the simulator log stream. The pinned Flutter 3.44.8 SDK has the race described in [flutter/flutter#181771](flutter/flutter#181771): the app can print its VM-service address before the log reader is ready, so the tool misses that address. This matches the captured failure; it is earlier than Thermion's material tests. Backport [Flutter's merged fix #193142](flutter/flutter#193142), commit `c3ed85ef987759ee54a6bce275490cde2a238dc1`, to the disposable SDK used by this iOS CI job. The fix waits for the log stream's readiness output before launching the app. The patch includes the upstream regression tests, adapted to 3.44.8's older test imports/constructor/logger types. TODO: remove the backport when the pinned Flutter SDK incorporates the upstream fix.
Fixes #181771
Fixes a race where
flutter test integration_teston an iOS simulator could hang forever at "Waiting for VM Service port to be available..."._IOSSimulatorLogReaderstartsxcrun simctl spawn … log streamfromonListen, and nothing waited for the process to start, sostartAppcould launch the app beforelog streamwas ready.log streamdrops events logged before it prints its first line (Filtering the log data using ...), so the Dart VM Service URL could be missed. The verbose log attached to #181771 showssimctl launchrunning beforesimctl spawn … log stream.This PR makes
startAppwait, right beforesimctl launch, untillog streamprints its first line or exits. If that does not happen within 30 seconds,startAppprints a warning and keeps waiting. A log reader starts a newlog streameach time it is listened to again, so the wait applies to each of them. The process is still started fromonListen, soflutter run, which listens to the log reader before building the app, still startslog streamduring the build.Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.