From 3422540bc43d72d499750d9fae971d6ad4cd9e7d Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 4 Nov 2019 09:09:20 -0800 Subject: [PATCH] copy chrome preferences to seeded data dir (#44032) --- .../flutter_tools/lib/src/web/chrome.dart | 45 +++++++++- .../flutter_tools/lib/src/web/web_device.dart | 5 +- .../test/general.shard/web/chrome_test.dart | 83 +++++++++++++------ 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart index 1460193f4e7..18c62556597 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; import '../base/common.dart'; @@ -65,13 +66,18 @@ String findChromeExecutable() { return null; } +@visibleForTesting +void resetChromeForTesting() { + ChromeLauncher._currentCompleter = Completer(); +} + /// Responsible for launching chrome with devtools configured. class ChromeLauncher { const ChromeLauncher(); static bool get hasChromeInstance => _currentCompleter.isCompleted; - static final Completer _currentCompleter = Completer(); + static Completer _currentCompleter = Completer(); /// Whether we can locate the chrome executable. bool canFindChrome() { @@ -89,15 +95,31 @@ class ChromeLauncher { /// a `headfull` browser. /// /// `skipCheck` does not attempt to make a devtools connection before returning. - Future launch(String url, { bool headless = false, bool skipCheck = false }) async { + Future launch(String url, { bool headless = false, bool skipCheck = false, Directory dataDir }) async { + // This is a JSON file which contains configuration from the + // browser session, such as window position. It is located + // under the Chrome data-dir folder. + final String preferencesPath = fs.path.join('Default', 'preferences'); + final String chromeExecutable = findChromeExecutable(); - final Directory dataDir = fs.systemTempDirectory.createTempSync('flutter_tool.'); + final Directory activeDataDir = fs.systemTempDirectory.createTempSync('flutter_tool.'); + // Seed data dir with previous state. + + final File savedPreferencesFile = fs.file(fs.path.join(dataDir?.path ?? '', preferencesPath)); + final File destinationFile = fs.file(fs.path.join(activeDataDir.path, preferencesPath)); + if (dataDir != null) { + if (savedPreferencesFile.existsSync()) { + destinationFile.parent.createSync(recursive: true); + savedPreferencesFile.copySync(destinationFile.path); + } + } + final int port = await os.findFreePort(); final List args = [ chromeExecutable, // Using a tmp directory ensures that a new instance of chrome launches // allowing for the remote debug port to be enabled. - '--user-data-dir=${dataDir.path}', + '--user-data-dir=${activeDataDir.path}', '--remote-debugging-port=$port', // When the DevTools has focus we don't want to slow down the application. '--disable-background-timer-throttling', @@ -117,6 +139,21 @@ class ChromeLauncher { final Process process = await processManager.start(args); + // When the process exits, copy the user settings back to the provided + // data-dir. + if (dataDir != null) { + unawaited(process.exitCode.whenComplete(() { + if (destinationFile.existsSync()) { + savedPreferencesFile.parent.createSync(recursive: true); + // If the file contains a crash string, remove it to hide + // the popup on next run. + final String contents = destinationFile.readAsStringSync(); + savedPreferencesFile.writeAsStringSync(contents + .replaceFirst('"exit_type":"Crashed"', '"exit_type":"Normal"')); + } + })); + } + // Wait until the DevTools are listening before trying to connect. await process.stderr .transform(utf8.decoder) diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart index 5dadcf1f03d..501aadb4a6f 100644 --- a/packages/flutter_tools/lib/src/web/web_device.dart +++ b/packages/flutter_tools/lib/src/web/web_device.dart @@ -132,7 +132,10 @@ class ChromeDevice extends Device { // for the web initialization and server logic. final String url = platformArgs['uri']; if (debuggingOptions.browserLaunch) { - _chrome = await chromeLauncher.launch(url); + _chrome = await chromeLauncher.launch(url, + dataDir: fs.currentDirectory + .childDirectory('.dart_tool') + .childDirectory('chrome-device')); } else { printStatus('Waiting for connection from Dart debug extension at $url', emphasis: true); logger.sendNotification(url, progressId: 'debugExtension'); diff --git a/packages/flutter_tools/test/general.shard/web/chrome_test.dart b/packages/flutter_tools/test/general.shard/web/chrome_test.dart index ba6b7bbebd6..422528469a3 100644 --- a/packages/flutter_tools/test/general.shard/web/chrome_test.dart +++ b/packages/flutter_tools/test/general.shard/web/chrome_test.dart @@ -19,33 +19,43 @@ import '../../src/testbed.dart'; void main() { Testbed testbed; + Completer exitCompleter; setUp(() { final MockPlatform platform = MockPlatform(); + exitCompleter = Completer.sync(); when(platform.isWindows).thenReturn(false); - final MockFileSystem mockFileSystem = MockFileSystem(); testbed = Testbed(overrides: { ProcessManager: () => MockProcessManager(), Platform: () => platform, OperatingSystemUtils: () => MockOperatingSystemUtils(), - FileSystem: () => mockFileSystem, + }, setup: () { + when(os.findFreePort()).thenAnswer((Invocation invocation) async { + return 1234; + }); + when(platform.environment).thenReturn({ + kChromeEnvironment: 'example_chrome', + }); + when(processManager.start(any)) + .thenAnswer((Invocation invocation) async { + return FakeProcess( + exitCode: exitCompleter.future, + stdout: const Stream>.empty(), + stderr: Stream>.fromIterable(>[ + utf8.encode('\n\nDevTools listening\n\n'), + ]), + ); + }); }); }); + tearDown(() { + resetChromeForTesting(); + }); + test('can launch chrome and connect to the devtools', () => testbed.run(() async { - when(os.findFreePort()).thenAnswer((Invocation invocation) async { - return 1234; - }); - when(platform.environment).thenReturn({ - kChromeEnvironment: 'example_chrome', - }); - final Directory mockDirectory = MockDirectory(); - when(fs.systemTempDirectory).thenReturn(mockDirectory); - when(mockDirectory.createTempSync(any)).thenReturn(mockDirectory); - when(mockDirectory.path).thenReturn('example'); - when(processManager.start([ + const List expected = [ 'example_chrome', - '--user-data-dir=example', '--remote-debugging-port=1234', '--disable-background-timer-throttling', '--disable-extensions', @@ -56,22 +66,45 @@ void main() { '--disable-default-apps', '--disable-translate', 'example_url', - ])).thenAnswer((Invocation invocation) async { - return FakeProcess( - exitCode: Completer().future, - stdout: const Stream>.empty(), - stderr: Stream>.fromIterable(>[ - utf8.encode('\n\nDevTools listening\n\n'), - ]), - ); - }); + ]; await chromeLauncher.launch('example_url', skipCheck: true); + final VerificationResult result = verify(processManager.start(captureAny)); + + expect(result.captured.single, containsAll(expected)); + })); + + test('can seed chrome temp directory with existing preferences', () => testbed.run(() async { + final Directory dataDir = fs.directory('chrome-stuff'); + final File preferencesFile = dataDir + .childDirectory('Default') + .childFile('preferences'); + preferencesFile + ..createSync(recursive: true) + ..writeAsStringSync('example'); + + await chromeLauncher.launch('example_url', skipCheck: true, dataDir: dataDir); + final VerificationResult result = verify(processManager.start(captureAny)); + final String arg = result.captured.single + .firstWhere((String arg) => arg.startsWith('--user-data-dir=')); + final Directory tempDirectory = fs.directory(arg.split('=')[1]); + final File tempFile = tempDirectory + .childDirectory('Default') + .childFile('preferences'); + + expect(tempFile.existsSync(), true); + expect(tempFile.readAsStringSync(), 'example'); + + // write crash to file: + tempFile.writeAsStringSync('"exit_type":"Crashed"'); + exitCompleter.complete(0); + + // writes non-crash back to dart_tool + expect(preferencesFile.readAsStringSync(), '"exit_type":"Normal"'); })); } class MockProcessManager extends Mock implements ProcessManager {} class MockPlatform extends Mock implements Platform {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} -class MockFileSystem extends Mock implements FileSystem {} -class MockDirectory extends Mock implements Directory {} +