1// Copyright 2018 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../application_package.dart'; 10import '../artifacts.dart'; 11import '../base/common.dart'; 12import '../base/file_system.dart'; 13import '../base/io.dart'; 14import '../base/process_manager.dart'; 15import '../build_info.dart'; 16import '../bundle.dart'; 17import '../convert.dart'; 18import '../dart/package_map.dart'; 19import '../device.dart'; 20import '../globals.dart'; 21import '../project.dart'; 22import '../protocol_discovery.dart'; 23import '../version.dart'; 24 25class FlutterTesterApp extends ApplicationPackage { 26 factory FlutterTesterApp.fromCurrentDirectory() { 27 return FlutterTesterApp._(fs.currentDirectory); 28 } 29 30 FlutterTesterApp._(Directory directory) 31 : _directory = directory, 32 super(id: directory.path); 33 34 final Directory _directory; 35 36 @override 37 String get name => _directory.basename; 38 39 @override 40 File get packagesFile => _directory.childFile('.packages'); 41} 42 43// TODO(scheglov): This device does not currently work with full restarts. 44class FlutterTesterDevice extends Device { 45 FlutterTesterDevice(String deviceId) : super( 46 deviceId, 47 platformType: null, 48 category: null, 49 ephemeral: false, 50 ); 51 52 Process _process; 53 final DevicePortForwarder _portForwarder = _NoopPortForwarder(); 54 55 @override 56 Future<bool> get isLocalEmulator async => false; 57 58 @override 59 Future<String> get emulatorId async => null; 60 61 @override 62 String get name => 'Flutter test device'; 63 64 @override 65 DevicePortForwarder get portForwarder => _portForwarder; 66 67 @override 68 Future<String> get sdkNameAndVersion async { 69 final FlutterVersion flutterVersion = FlutterVersion.instance; 70 return 'Flutter ${flutterVersion.frameworkRevisionShort}'; 71 } 72 73 @override 74 Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester; 75 76 @override 77 void clearLogs() { } 78 79 final _FlutterTesterDeviceLogReader _logReader = 80 _FlutterTesterDeviceLogReader(); 81 82 @override 83 DeviceLogReader getLogReader({ ApplicationPackage app }) => _logReader; 84 85 @override 86 Future<bool> installApp(ApplicationPackage app) async => true; 87 88 @override 89 Future<bool> isAppInstalled(ApplicationPackage app) async => false; 90 91 @override 92 Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false; 93 94 @override 95 bool isSupported() => true; 96 97 bool _isRunning = false; 98 bool get isRunning => _isRunning; 99 100 @override 101 Future<LaunchResult> startApp( 102 ApplicationPackage package, { 103 @required String mainPath, 104 String route, 105 @required DebuggingOptions debuggingOptions, 106 Map<String, dynamic> platformArgs, 107 bool prebuiltApplication = false, 108 bool usesTerminalUi = true, 109 bool ipv6 = false, 110 }) async { 111 final BuildInfo buildInfo = debuggingOptions.buildInfo; 112 113 if (!buildInfo.isDebug) { 114 printError('This device only supports debug mode.'); 115 return LaunchResult.failed(); 116 } 117 118 final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester); 119 if (!fs.isFileSync(shellPath)) 120 throwToolExit('Cannot find Flutter shell at $shellPath'); 121 122 final List<String> command = <String>[ 123 shellPath, 124 '--run-forever', 125 '--non-interactive', 126 '--enable-dart-profiling', 127 '--packages=${PackageMap.globalPackagesPath}', 128 ]; 129 if (debuggingOptions.debuggingEnabled) { 130 if (debuggingOptions.startPaused) 131 command.add('--start-paused'); 132 if (debuggingOptions.disableServiceAuthCodes) 133 command.add('--disable-service-auth-codes'); 134 if (debuggingOptions.hasObservatoryPort) 135 command.add('--observatory-port=${debuggingOptions.observatoryPort}'); 136 } 137 138 // Build assets and perform initial compilation. 139 final String assetDirPath = getAssetBuildDirectory(); 140 final String applicationKernelFilePath = getKernelPathForTransformerOptions( 141 fs.path.join(getBuildDirectory(), 'flutter-tester-app.dill'), 142 trackWidgetCreation: buildInfo.trackWidgetCreation, 143 ); 144 await BundleBuilder().build( 145 mainPath: mainPath, 146 assetDirPath: assetDirPath, 147 applicationKernelFilePath: applicationKernelFilePath, 148 precompiledSnapshot: false, 149 trackWidgetCreation: buildInfo.trackWidgetCreation, 150 ); 151 command.add('--flutter-assets-dir=$assetDirPath'); 152 153 command.add(applicationKernelFilePath); 154 155 try { 156 printTrace(command.join(' ')); 157 158 _isRunning = true; 159 _process = await processManager.start(command, 160 environment: <String, String>{ 161 'FLUTTER_TEST': 'true', 162 }, 163 ); 164 // Setting a bool can't fail in the callback. 165 unawaited(_process.exitCode.then<void>((_) => _isRunning = false)); 166 _process.stdout 167 .transform<String>(utf8.decoder) 168 .transform<String>(const LineSplitter()) 169 .listen((String line) { 170 _logReader.addLine(line); 171 }); 172 _process.stderr 173 .transform<String>(utf8.decoder) 174 .transform<String>(const LineSplitter()) 175 .listen((String line) { 176 _logReader.addLine(line); 177 }); 178 179 if (!debuggingOptions.debuggingEnabled) 180 return LaunchResult.succeeded(); 181 182 final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory( 183 getLogReader(), 184 hostPort: debuggingOptions.observatoryPort, 185 ); 186 187 final Uri observatoryUri = await observatoryDiscovery.uri; 188 return LaunchResult.succeeded(observatoryUri: observatoryUri); 189 } catch (error) { 190 printError('Failed to launch $package: $error'); 191 return LaunchResult.failed(); 192 } 193 } 194 195 @override 196 Future<bool> stopApp(ApplicationPackage app) async { 197 _process?.kill(); 198 _process = null; 199 return true; 200 } 201 202 @override 203 Future<bool> uninstallApp(ApplicationPackage app) async => true; 204 205 @override 206 bool isSupportedForProject(FlutterProject flutterProject) => true; 207} 208 209class FlutterTesterDevices extends PollingDeviceDiscovery { 210 FlutterTesterDevices() : super('Flutter tester'); 211 212 static const String kTesterDeviceId = 'flutter-tester'; 213 214 static bool showFlutterTesterDevice = false; 215 216 final FlutterTesterDevice _testerDevice = 217 FlutterTesterDevice(kTesterDeviceId); 218 219 @override 220 bool get canListAnything => true; 221 222 @override 223 bool get supportsPlatform => true; 224 225 @override 226 Future<List<Device>> pollingGetDevices() async { 227 return showFlutterTesterDevice ? <Device>[_testerDevice] : <Device>[]; 228 } 229} 230 231class _FlutterTesterDeviceLogReader extends DeviceLogReader { 232 final StreamController<String> _logLinesController = 233 StreamController<String>.broadcast(); 234 235 @override 236 int get appPid => 0; 237 238 @override 239 Stream<String> get logLines => _logLinesController.stream; 240 241 @override 242 String get name => 'flutter tester log reader'; 243 244 void addLine(String line) => _logLinesController.add(line); 245} 246 247/// A fake port forwarder that doesn't do anything. Used by flutter tester 248/// where the VM is running on the same machine and does not need ports forwarding. 249class _NoopPortForwarder extends DevicePortForwarder { 250 @override 251 Future<int> forward(int devicePort, { int hostPort }) { 252 if (hostPort != null && hostPort != devicePort) 253 throw 'Forwarding to a different port is not supported by flutter tester'; 254 return Future<int>.value(devicePort); 255 } 256 257 @override 258 List<ForwardedPort> get forwardedPorts => <ForwardedPort>[]; 259 260 @override 261 Future<void> unforward(ForwardedPort forwardedPort) async { } 262} 263