diff --git a/packages/flutter_tools/lib/src/preview_device.dart b/packages/flutter_tools/lib/src/preview_device.dart new file mode 100644 index 0000000000..42edf5e353 --- /dev/null +++ b/packages/flutter_tools/lib/src/preview_device.dart @@ -0,0 +1,190 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.8 + +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:process/process.dart'; + +import 'application_package.dart'; +import 'base/file_system.dart'; +import 'base/io.dart'; +import 'base/logger.dart'; +import 'base/platform.dart'; +import 'build_info.dart'; +import 'bundle_builder.dart'; +import 'cache.dart'; +import 'desktop_device.dart'; +import 'devfs.dart'; +import 'device.dart'; +import 'device_port_forwarder.dart'; +import 'project.dart'; +import 'protocol_discovery.dart'; + +typedef BundleBuilderFactory = BundleBuilder Function(); + +BundleBuilder _defaultBundleBuilder() { + return BundleBuilder(); +} + +/// A device type that runs a prebuilt desktop binary alongside a locally compiled kernel file. +/// +/// This could be used to support debug local development without plugins on machines that +/// have not completed the SDK setup. These features are not fully implemented and the +/// device is not currently discoverable. +class PreviewDevice extends Device { + PreviewDevice({ + @required Platform platform, + @required ProcessManager processManager, + @required Logger logger, + @required FileSystem fileSystem, + @visibleForTesting BundleBuilderFactory builderFactory = _defaultBundleBuilder, + }) : _platform = platform, + _processManager = processManager, + _logger = logger, + _fileSystem = fileSystem, + _bundleBuilderFactory = builderFactory, + super('preview', ephemeral: false, category: Category.desktop, platformType: PlatformType.custom); + + final Platform _platform; + final ProcessManager _processManager; + final Logger _logger; + final FileSystem _fileSystem; + final BundleBuilderFactory _bundleBuilderFactory; + + @override + void clearLogs() { } + + @override + Future dispose() async { } + + @override + Future get emulatorId async => null; + + final DesktopLogReader _logReader = DesktopLogReader(); + + @override + FutureOr getLogReader({covariant ApplicationPackage app, bool includePastLogs = false}) => _logReader; + + @override + Future installApp(covariant ApplicationPackage app, {String userIdentifier}) async => true; + + @override + Future isAppInstalled(covariant ApplicationPackage app, {String userIdentifier}) async => false; + + @override + Future isLatestBuildInstalled(covariant ApplicationPackage app) async => false; + + @override + Future get isLocalEmulator async => false; + + @override + bool isSupported() => true; + + @override + bool isSupportedForProject(FlutterProject flutterProject) => true; + + @override + String get name => 'preview'; + + @override + DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder(); + + @override + Future get sdkNameAndVersion async => 'preview'; + + Process _process; + Directory _assetDirectory; + + @override + Future startApp(covariant ApplicationPackage package, { + String mainPath, + String route, + @required DebuggingOptions debuggingOptions, + Map platformArgs, + bool prebuiltApplication = false, + bool ipv6 = false, + String userIdentifier, + }) async { + _assetDirectory = _fileSystem.systemTempDirectory + .createTempSync('flutter_preview.'); + + // Build assets and perform initial compilation. + Status status; + try { + status = _logger.startProgress('Compiling application for preview...'); + await _bundleBuilderFactory().build( + buildInfo: debuggingOptions.buildInfo, + mainPath: mainPath, + platform: TargetPlatform.tester, + assetDirPath: getAssetBuildDirectory(), + ); + copyDirectory(_fileSystem.directory( + getAssetBuildDirectory()), + _assetDirectory.childDirectory('data').childDirectory('flutter_assets'), + ); + } finally { + status.stop(); + } + + // Merge with precompiled executable. + final Directory precompiledDirectory = _fileSystem.directory(_fileSystem.path.join(Cache.flutterRoot, 'artifacts_temp', 'Debug')); + copyDirectory(precompiledDirectory, _assetDirectory); + + final Process process = await _processManager.start( + [ + _assetDirectory.childFile('splash').path, + ], + ); + _process = process; + _logReader.initializeProcess(process); + + final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(_logReader, + devicePort: debuggingOptions?.deviceVmServicePort, + hostPort: debuggingOptions?.hostVmServicePort, + ipv6: ipv6, + logger: _logger, + ); + try { + final Uri observatoryUri = await observatoryDiscovery.uri; + if (observatoryUri != null) { + return LaunchResult.succeeded(observatoryUri: observatoryUri); + } + _logger.printError( + 'Error waiting for a debug connection: ' + 'The log reader stopped unexpectedly.', + ); + } on Exception catch (error) { + _logger.printError('Error waiting for a debug connection: $error'); + } finally { + await observatoryDiscovery.cancel(); + } + return LaunchResult.failed(); + } + + @override + Future stopApp(covariant ApplicationPackage app, {String userIdentifier}) async { + return _process?.kill(); + } + + @override + Future get targetPlatform async { + if (_platform.isWindows) { + return TargetPlatform.windows_x64; + } + return TargetPlatform.tester; + } + + @override + Future uninstallApp(covariant ApplicationPackage app, {String userIdentifier}) async { + return true; + } + + @override + DevFSWriter createDevFSWriter(covariant ApplicationPackage app, String userIdentifier) { + return LocalDevFSWriter(fileSystem: _fileSystem); + } +} diff --git a/packages/flutter_tools/test/general.shard/preview_device_test.dart b/packages/flutter_tools/test/general.shard/preview_device_test.dart new file mode 100644 index 0000000000..8824ed7dae --- /dev/null +++ b/packages/flutter_tools/test/general.shard/preview_device_test.dart @@ -0,0 +1,113 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.8 + +import 'dart:async'; + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/application_package.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/build_system/build_system.dart'; +import 'package:flutter_tools/src/bundle.dart'; +import 'package:flutter_tools/src/bundle_builder.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/preview_device.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:meta/meta.dart'; +import 'package:test/fake.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; +import '../src/fake_process_manager.dart'; + +void main() { + testWithoutContext('PreviewDevice defaults', () async { + final PreviewDevice device = PreviewDevice( + fileSystem: MemoryFileSystem.test(), + processManager: FakeProcessManager.any(), + logger: BufferLogger.test(), + platform: FakePlatform(), + ); + + expect(await device.isLocalEmulator, false); + expect(device.name, 'preview'); + expect(await device.sdkNameAndVersion, 'preview'); + expect(await device.targetPlatform, TargetPlatform.tester); + expect(device.category, Category.desktop); + expect(device.ephemeral, false); + expect(device.id, 'preview'); + + expect(device.isSupported(), true); + expect(device.isSupportedForProject(FakeFlutterProject()), true); + expect(await device.isLatestBuildInstalled(FakeApplicationPackage()), false); + expect(await device.isAppInstalled(FakeApplicationPackage()), false); + expect(await device.uninstallApp(FakeApplicationPackage()), true); + }); + + testUsingContext('Can build a simulator app', () async { + Cache.flutterRoot = ''; + final Completer completer = Completer(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final BufferLogger logger = BufferLogger.test(); + final PreviewDevice device = PreviewDevice( + fileSystem: fileSystem, + processManager: FakeProcessManager.list([ + FakeCommand( + command: const [ + '/.tmp_rand0/flutter_preview.rand0/splash' + ], + stdout: 'Observatory listening on http://127.0.0.1:64494/fZ_B2N6JRwY=/\n', + completer: completer, + ) + ]), + logger: logger, + platform: FakePlatform(), + builderFactory: () => FakeBundleBuilder(fileSystem), + ); + fileSystem + .directory('artifacts_temp') + .childDirectory('Debug') + .createSync(recursive: true); + + final LaunchResult result = await device.startApp( + FakeApplicationPackage(), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ); + + expect(result.started, true); + expect(result.observatoryUri, Uri.parse('http://127.0.0.1:64494/fZ_B2N6JRwY=/')); + }); +} + +class FakeFlutterProject extends Fake implements FlutterProject {} +class FakeApplicationPackage extends Fake implements ApplicationPackage {} +class FakeBundleBuilder extends Fake implements BundleBuilder { + FakeBundleBuilder(this.fileSystem); + + final FileSystem fileSystem; + + @override + Future build({ + @required TargetPlatform platform, + @required BuildInfo buildInfo, + FlutterProject project, + String mainPath, + String manifestPath = defaultManifestPath, + String applicationKernelFilePath, + String depfilePath, + String assetDirPath, + @visibleForTesting BuildSystem buildSystem + }) async { + final Directory assetDirectory = fileSystem + .directory(assetDirPath) + .childDirectory('flutter_assets') + ..createSync(recursive: true); + assetDirectory.childFile('kernel_blob.bin').createSync(); + } +}