diff --git a/lib/web_ui/dev/safari_macos.dart b/lib/web_ui/dev/safari_macos.dart index bb0727b8ec865..5e5329a934bb3 100644 --- a/lib/web_ui/dev/safari_macos.dart +++ b/lib/web_ui/dev/safari_macos.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:test_api/src/backend/runtime.dart'; @@ -23,6 +24,58 @@ class SafariMacOsEnvironment extends WebDriverBrowserEnvironment { @override Uri get driverUri => Uri(scheme: 'http', host: 'localhost', port: portNumber); + late Process _driverProcess; + int _retryCount = 0; + static const int _waitBetweenRetryInSeconds = 1; + static const int _maxRetryCount = 10; + @override Future spawnDriverProcess() => Process.start('safaridriver', ['-p', portNumber.toString()]); + + @override + Future prepare() async { + await _startDriverProcess(); + } + + /// Pick an unused port and start `safaridriver` using that port. + /// + /// On macOS 13, starting `safaridriver` can be flaky so if it returns an + /// "Operation not permitted" error, kill the `safaridriver` process and try + /// again with a different port. Wait [_waitBetweenRetryInSeconds] seconds + /// between retries. Try up to [_maxRetryCount] times. + Future _startDriverProcess() async { + _retryCount += 1; + if (_retryCount > 1) { + await Future.delayed(const Duration(seconds: _waitBetweenRetryInSeconds)); + } + portNumber = await pickUnusedPort(); + + print('Attempt $_retryCount to start safaridriver on port $portNumber'); + + _driverProcess = await spawnDriverProcess(); + + _driverProcess.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String error) { + print('[Webdriver][Error] $error'); + if (_retryCount > _maxRetryCount) { + print('[Webdriver][Error] Failed to start after $_maxRetryCount tries.'); + } else if (error.contains('Operation not permitted')) { + _driverProcess.kill(); + _startDriverProcess(); + } + }); + _driverProcess.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String log) { + print('[Webdriver] $log'); + }); + } + + @override + Future cleanup() async { + _driverProcess.kill(); + } } diff --git a/lib/web_ui/dev/webdriver_browser.dart b/lib/web_ui/dev/webdriver_browser.dart index ef27394127fa7..024a04580a40b 100644 --- a/lib/web_ui/dev/webdriver_browser.dart +++ b/lib/web_ui/dev/webdriver_browser.dart @@ -13,14 +13,14 @@ import 'package:webdriver/async_io.dart' show WebDriver, createDriver; import 'browser.dart'; abstract class WebDriverBrowserEnvironment extends BrowserEnvironment { - late final int portNumber; + late int portNumber; late final Process _driverProcess; Future spawnDriverProcess(); Uri get driverUri; /// Finds and returns an unused port on the test host in the local port range. - Future _pickUnusedPort() async { + Future pickUnusedPort() async { // Use bind to allocate an unused port, then unbind from that port to // make it available for use. final ServerSocket socket = await ServerSocket.bind('localhost', 0); @@ -33,7 +33,7 @@ abstract class WebDriverBrowserEnvironment extends BrowserEnvironment { @override Future prepare() async { - portNumber = await _pickUnusedPort(); + portNumber = await pickUnusedPort(); _driverProcess = await spawnDriverProcess();