From 3f3e9791ce5368972d2739ea49744631f2e55040 Mon Sep 17 00:00:00 2001 From: Bharath Mohan <2254476+bharathm03@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:51:25 +0530 Subject: [PATCH 1/2] test(windows): harden the renderer-process probe against WMI stalls The renderer lifecycle test shells out to PowerShell to count WebView2 renderer processes, and capped each call at 10s. That probe is also polled in a loop while WebView2 processes are starting and exiting, so on a contended CI runner a single slow WMI query aborts the whole suite -- observed as `TimeoutException after 0:00:10.000000` on the very first call, before any WebView is created. A Win32_Process query costs ~0.7s per call on an idle Windows 11 machine, almost all of it PowerShell and WMI start-up rather than the enumeration itself (a full enumeration of 398 processes is 265ms warm, a filtered one 216ms), so the ceiling is what needs headroom, not the script. Raise it to 30s and retry once, so a transient stall costs a second probe instead of the run. --- .../webview_flutter_test.dart | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/examples/platform/integration_test/webview_flutter_test.dart b/examples/platform/integration_test/webview_flutter_test.dart index 849fce0..2da5563 100644 --- a/examples/platform/integration_test/webview_flutter_test.dart +++ b/examples/platform/integration_test/webview_flutter_test.dart @@ -1589,6 +1589,31 @@ bool _isWKWebView() { defaultTargetPlatform == TargetPlatform.macOS; } +/// Runs [script] in PowerShell, retrying once if the probe stalls. +/// +/// A single `Win32_Process` query costs roughly 0.7s even on an idle machine, +/// nearly all of it PowerShell and WMI start-up rather than the enumeration +/// itself, and this probe is polled in a loop while WebView2 processes are +/// starting and exiting. A contended CI runner can push one call well past a +/// short deadline, so give it real headroom and absorb a single stall instead +/// of failing the whole suite. +Future _runWindowsProcessProbe(String script) async { + Future run() { + return Process.run('powershell.exe', [ + '-NoProfile', + '-NonInteractive', + '-Command', + script, + ]).timeout(const Duration(seconds: 30)); + } + + try { + return await run(); + } on TimeoutException { + return await run(); + } +} + Future _countWindowsRendererProcesses() async { final String script = r''' @@ -1613,12 +1638,7 @@ while ($pendingIds.Count -gt 0) { }).Count ''' .replaceFirst('__ROOT_PROCESS_ID__', '$pid'); - final ProcessResult result = await Process.run('powershell.exe', [ - '-NoProfile', - '-NonInteractive', - '-Command', - script, - ]).timeout(const Duration(seconds: 10)); + final ProcessResult result = await _runWindowsProcessProbe(script); if (result.exitCode != 0) { throw TestFailure( 'Failed to inspect Windows WebView2 renderer processes: ' From e15c37d238bac596120721bcc3726b36129f046d Mon Sep 17 00:00:00 2001 From: moluopro Date: Tue, 1 Sep 2026 17:08:18 +0800 Subject: [PATCH 2/2] test(windows): bound renderer process probes --- .../webview_flutter_test.dart | 135 ++++++++++++++---- 1 file changed, 107 insertions(+), 28 deletions(-) diff --git a/examples/platform/integration_test/webview_flutter_test.dart b/examples/platform/integration_test/webview_flutter_test.dart index 2da5563..e49b80b 100644 --- a/examples/platform/integration_test/webview_flutter_test.dart +++ b/examples/platform/integration_test/webview_flutter_test.dart @@ -264,25 +264,24 @@ return { await tester.pumpWidget(WebViewWidget(controller: controller)); await pageFinished.future.timeout(const Duration(seconds: 15)); - await _waitForCondition( - () async => await _countWindowsRendererProcesses() > rendererCountBefore, - reason: 'Creating a Windows WebView did not start a renderer process.', - timeout: const Duration(seconds: 15), - ); final int rendererCountWhileMounted = await _countWindowsRendererProcesses(); + expect( + rendererCountWhileMounted, + greaterThan(rendererCountBefore), + reason: 'Creating a Windows WebView did not start a renderer process.', + ); await tester.pumpWidget(const SizedBox.shrink()); await windowsController.dispose(); disposed = true; - await _waitForCondition( - () async => await _countWindowsRendererProcesses() <= rendererCountBefore, + await _waitForWindowsRendererProcessCount( + (int count) => count <= rendererCountBefore, reason: 'Disposing the Windows controller did not restore the renderer ' 'process count from $rendererCountWhileMounted to ' '$rendererCountBefore.', - timeout: const Duration(seconds: 15), ); await expectLater(controller.currentUrl(), throwsStateError); }); @@ -1589,32 +1588,78 @@ bool _isWKWebView() { defaultTargetPlatform == TargetPlatform.macOS; } -/// Runs [script] in PowerShell, retrying once if the probe stalls. -/// -/// A single `Win32_Process` query costs roughly 0.7s even on an idle machine, -/// nearly all of it PowerShell and WMI start-up rather than the enumeration -/// itself, and this probe is polled in a loop while WebView2 processes are -/// starting and exiting. A contended CI runner can push one call well past a -/// short deadline, so give it real headroom and absorb a single stall instead -/// of failing the whole suite. -Future _runWindowsProcessProbe(String script) async { - Future run() { - return Process.run('powershell.exe', [ - '-NoProfile', - '-NonInteractive', - '-Command', - script, - ]).timeout(const Duration(seconds: 30)); +const Duration _windowsProcessProbeTimeout = Duration(seconds: 30); +const Duration _windowsProcessTerminationTimeout = Duration(seconds: 2); +const Duration _windowsProcessPollInterval = Duration(milliseconds: 500); +const int _windowsProcessProbeAttempts = 2; + +/// Runs [script] with a shared timeout and terminates stalled attempts. +Future _runWindowsProcessProbe( + String script, { + Duration timeout = _windowsProcessProbeTimeout, +}) async { + final Stopwatch stopwatch = Stopwatch()..start(); + TimeoutException? lastTimeout; + + for (var attempt = 0; attempt < _windowsProcessProbeAttempts; attempt++) { + final Duration remaining = timeout - stopwatch.elapsed; + final int remainingAttempts = _windowsProcessProbeAttempts - attempt; + if (remaining <= Duration.zero) { + break; + } + final Duration attemptTimeout = Duration( + microseconds: remaining.inMicroseconds ~/ remainingAttempts, + ); + try { + return await _runWindowsProcessProbeAttempt(script, attemptTimeout); + } on TimeoutException catch (error) { + lastTimeout = error; + } } + throw TestFailure( + 'Windows process inspection did not complete after ' + '$_windowsProcessProbeAttempts attempts within ' + '${timeout.inSeconds} seconds. Last error: $lastTimeout', + ); +} + +Future _runWindowsProcessProbeAttempt( + String script, + Duration timeout, +) async { + final Process process = await Process.start('powershell.exe', [ + '-NoProfile', + '-NonInteractive', + '-Command', + script, + ]); + final Future stdout = systemEncoding.decodeStream(process.stdout); + final Future stderr = systemEncoding.decodeStream(process.stderr); + + late final int exitCode; try { - return await run(); + exitCode = await process.exitCode.timeout(timeout); } on TimeoutException { - return await run(); + process.kill(); + try { + await process.exitCode.timeout(_windowsProcessTerminationTimeout); + } on TimeoutException { + throw TestFailure( + 'Timed-out Windows process probe ${process.pid} could not be ' + 'terminated.', + ); + } + await Future.wait(>[stdout, stderr]); + throw TimeoutException('Windows process inspection timed out.', timeout); } + + return ProcessResult(process.pid, exitCode, await stdout, await stderr); } -Future _countWindowsRendererProcesses() async { +Future _countWindowsRendererProcesses({ + Duration timeout = _windowsProcessProbeTimeout, +}) async { final String script = r''' $rootProcessId = __ROOT_PROCESS_ID__ @@ -1638,7 +1683,10 @@ while ($pendingIds.Count -gt 0) { }).Count ''' .replaceFirst('__ROOT_PROCESS_ID__', '$pid'); - final ProcessResult result = await _runWindowsProcessProbe(script); + final ProcessResult result = await _runWindowsProcessProbe( + script, + timeout: timeout, + ); if (result.exitCode != 0) { throw TestFailure( 'Failed to inspect Windows WebView2 renderer processes: ' @@ -1652,6 +1700,37 @@ while ($pendingIds.Count -gt 0) { return count; } +Future _waitForWindowsRendererProcessCount( + bool Function(int count) condition, { + required String reason, + Duration timeout = _windowsProcessProbeTimeout, +}) async { + final Stopwatch stopwatch = Stopwatch()..start(); + int? lastCount; + + while (stopwatch.elapsed < timeout) { + final Duration remaining = timeout - stopwatch.elapsed; + lastCount = await _countWindowsRendererProcesses(timeout: remaining); + if (condition(lastCount)) { + return lastCount; + } + + final Duration delayBudget = timeout - stopwatch.elapsed; + if (delayBudget <= Duration.zero) { + break; + } + await Future.delayed( + delayBudget < _windowsProcessPollInterval + ? delayBudget + : _windowsProcessPollInterval, + ); + } + + throw TestFailure( + '$reason Last observed renderer process count: $lastCount.', + ); +} + Future _waitForCondition( FutureOr Function() condition, { required String reason,