| // Copyright 2018 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| // ignore_for_file: avoid_print | |
| // DO NOT DELETE THIS FILE. It is imported by DDS and used in the | |
| // devtools_server tests. | |
| // TODO(https://github.com/flutter/devtools/issues/9408): Move this into | |
| // package:dds instead. | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' | |
| hide ChromeTab; | |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' | |
| as wip | |
| show ChromeTab; | |
| import 'io_utils.dart'; | |
| // Change this if you want to be able to see Chrome opening while tests run | |
| // to aid debugging. | |
| const useChromeHeadless = true; | |
| // Chrome headless currently hangs on Windows (both Travis and locally), | |
| // so we won't use it there. | |
| final headlessModeIsSupported = !Platform.isWindows; | |
| class Chrome { | |
| Chrome._(this.executable); | |
| static Chrome? from(String executable) { | |
| return FileSystemEntity.isFileSync(executable) | |
| ? Chrome._(executable) | |
| : null; | |
| } | |
| static Chrome? locate() { | |
| if (Platform.isMacOS) { | |
| const defaultPath = '/Applications/Google Chrome.app'; | |
| const bundlePath = 'Contents/MacOS/Google Chrome'; | |
| if (FileSystemEntity.isDirectorySync(defaultPath)) { | |
| return Chrome.from(path.join(defaultPath, bundlePath)); | |
| } | |
| } else if (Platform.isLinux) { | |
| const defaultPath = '/usr/bin/google-chrome'; | |
| if (FileSystemEntity.isFileSync(defaultPath)) { | |
| return Chrome.from(defaultPath); | |
| } | |
| } else if (Platform.isWindows) { | |
| final progFiles = Platform.environment['PROGRAMFILES(X86)']!; | |
| final chromeInstall = '$progFiles\\Google\\Chrome'; | |
| final defaultPath = '$chromeInstall\\Application\\chrome.exe'; | |
| if (FileSystemEntity.isFileSync(defaultPath)) { | |
| return Chrome.from(defaultPath); | |
| } | |
| } | |
| final pathFromEnv = Platform.environment['CHROME_PATH']; | |
| if (pathFromEnv != null && FileSystemEntity.isFileSync(pathFromEnv)) { | |
| return Chrome.from(pathFromEnv); | |
| } | |
| // TODO(devoncarew): check default install locations for linux | |
| // TODO(devoncarew): try `which` on mac, linux | |
| return null; | |
| } | |
| /// Return the path to a per-user Chrome data dir. | |
| /// | |
| /// This method will create the directory if it does not exist. | |
| static String getCreateChromeDataDir() { | |
| final prefsDir = getDartPrefsDirectory(); | |
| final chromeDataDir = Directory(path.join(prefsDir.path, 'chrome')); | |
| if (!chromeDataDir.existsSync()) { | |
| chromeDataDir.createSync(recursive: true); | |
| } | |
| return chromeDataDir.path; | |
| } | |
| final String executable; | |
| Future<ChromeProcess> start({String? url, int debugPort = 9222}) { | |
| final args = <String>[ | |
| '--no-default-browser-check', | |
| '--no-first-run', | |
| '--user-data-dir=${getCreateChromeDataDir()}', | |
| '--remote-debugging-port=$debugPort', | |
| ]; | |
| if (useChromeHeadless && headlessModeIsSupported) { | |
| args.addAll(<String>['--headless', '--disable-gpu']); | |
| } | |
| if (url != null) { | |
| args.add(url); | |
| } | |
| return Process.start(executable, args).then((Process process) { | |
| return ChromeProcess(process, debugPort); | |
| }); | |
| } | |
| } | |
| class ChromeProcess { | |
| ChromeProcess(this.process, this.debugPort); | |
| final Process process; | |
| final int debugPort; | |
| bool _processAlive = true; | |
| Future<ChromeTab?> connectToTab( | |
| String url, { | |
| Duration timeout = const Duration(seconds: 20), | |
| }) async { | |
| return await _connectToTab( | |
| connection: ChromeConnection(Uri.parse(url).host, debugPort), | |
| tabFound: (tab) => tab.url == url, | |
| timeout: timeout, | |
| ); | |
| } | |
| Future<ChromeTab?> connectToTabId( | |
| String host, | |
| String id, { | |
| Duration timeout = const Duration(seconds: 20), | |
| }) async { | |
| return await _connectToTab( | |
| connection: ChromeConnection(host, debugPort), | |
| tabFound: (tab) => tab.id == id, | |
| timeout: timeout, | |
| ); | |
| } | |
| Future<ChromeTab?> getFirstTab({ | |
| Duration timeout = const Duration(seconds: 20), | |
| }) async { | |
| return await _connectToTab( | |
| connection: ChromeConnection('localhost', debugPort), | |
| tabFound: (tab) => !tab.isBackgroundPage && !tab.isChromeExtension, | |
| timeout: timeout, | |
| ); | |
| } | |
| Future<ChromeTab?> _connectToTab({ | |
| required ChromeConnection connection, | |
| required bool Function(wip.ChromeTab) tabFound, | |
| required Duration timeout, | |
| }) async { | |
| final wipTab = await connection.getTab((wip.ChromeTab tab) { | |
| return tabFound(tab); | |
| }, retryFor: timeout); | |
| unawaited( | |
| process.exitCode.then((_) { | |
| _processAlive = false; | |
| }), | |
| ); | |
| return wipTab == null ? null : ChromeTab(wipTab); | |
| } | |
| bool get isAlive => _processAlive; | |
| /// Returns `true` if the signal is successfully delivered to the process. | |
| /// Otherwise the signal could not be sent, usually meaning that the process | |
| /// is already dead. | |
| bool kill() => process.kill(); | |
| Future<int> get onExit => process.exitCode; | |
| } | |
| class ChromeTab { | |
| ChromeTab(this.wipTab); | |
| final wip.ChromeTab wipTab; | |
| WipConnection? _wip; | |
| final _entryAddedController = StreamController<LogEntry>.broadcast(); | |
| final _consoleAPICalledController = | |
| StreamController<ConsoleAPIEvent>.broadcast(); | |
| final _exceptionThrownController = | |
| StreamController<ExceptionThrownEvent>.broadcast(); | |
| num? _lostConnectionTime; | |
| Future<WipConnection?> connect({bool verbose = false}) async { | |
| _wip = await wipTab.connect(); | |
| final wipConnection = _wip!; | |
| await wipConnection.log.enable(); | |
| wipConnection.log.onEntryAdded.listen((LogEntry entry) { | |
| if (_lostConnectionTime == null || | |
| entry.timestamp > _lostConnectionTime!) { | |
| _entryAddedController.add(entry); | |
| } | |
| }); | |
| await wipConnection.runtime.enable(); | |
| wipConnection.runtime.onConsoleAPICalled.listen((ConsoleAPIEvent event) { | |
| if (_lostConnectionTime == null || | |
| event.timestamp > _lostConnectionTime!) { | |
| _consoleAPICalledController.add(event); | |
| } | |
| }); | |
| unawaited( | |
| _exceptionThrownController.addStream( | |
| wipConnection.runtime.onExceptionThrown, | |
| ), | |
| ); | |
| unawaited(wipConnection.page.enable()); | |
| wipConnection.onClose.listen((_) { | |
| _wip = null; | |
| _disconnectStream.add(null); | |
| _lostConnectionTime = DateTime.now().millisecondsSinceEpoch; | |
| }); | |
| if (verbose) { | |
| onLogEntryAdded.listen((entry) { | |
| print('chrome • log:${entry.source} • ${entry.text} ${entry.url}'); | |
| }); | |
| onConsoleAPICalled.listen((entry) { | |
| print( | |
| 'chrome • console:${entry.type} • ' | |
| '${entry.args.map((a) => a.value).join(', ')}', | |
| ); | |
| }); | |
| onExceptionThrown.listen((ex) { | |
| throw 'JavaScript exception occurred: ${ex.method}\n\n' | |
| '${ex.params}\n\n' | |
| '${ex.exceptionDetails.text}\n\n' | |
| '${ex.exceptionDetails.exception}\n\n' | |
| '${ex.exceptionDetails.stackTrace}'; | |
| }); | |
| } | |
| return _wip; | |
| } | |
| Future<String> createNewTarget() { | |
| return _wip!.target.createTarget('about:blank'); | |
| } | |
| bool get isConnected => _wip != null; | |
| Stream<void> get onDisconnect => _disconnectStream.stream; | |
| final _disconnectStream = StreamController<void>.broadcast(); | |
| Stream<LogEntry> get onLogEntryAdded => _entryAddedController.stream; | |
| Stream<ConsoleAPIEvent> get onConsoleAPICalled => | |
| _consoleAPICalledController.stream; | |
| Stream<ExceptionThrownEvent> get onExceptionThrown => | |
| _exceptionThrownController.stream; | |
| Future<WipResponse> reload() => _wip!.page.reload(); | |
| Future<WipResponse> navigate(String url) => _wip!.page.navigate(url); | |
| WipConnection? get wipConnection => _wip; | |
| } |