1// Copyright 2015 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:args/command_runner.dart'; 8 9import '../base/common.dart'; 10import '../base/file_system.dart'; 11import '../base/time.dart'; 12import '../base/utils.dart'; 13import '../build_info.dart'; 14import '../cache.dart'; 15import '../device.dart'; 16import '../features.dart'; 17import '../globals.dart'; 18import '../macos/xcode.dart'; 19import '../project.dart'; 20import '../reporting/reporting.dart'; 21import '../resident_runner.dart'; 22import '../run_cold.dart'; 23import '../run_hot.dart'; 24import '../runner/flutter_command.dart'; 25import '../tracing.dart'; 26import '../version.dart'; 27import '../web/web_runner.dart'; 28import 'daemon.dart'; 29 30abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts { 31 // Used by run and drive commands. 32 RunCommandBase({ bool verboseHelp = false }) { 33 addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp); 34 usesFlavorOption(); 35 argParser 36 ..addFlag('trace-startup', 37 negatable: false, 38 help: 'Trace application startup, then exit, saving the trace to a file.', 39 ) 40 ..addFlag('verbose-system-logs', 41 negatable: false, 42 help: 'Include verbose logging from the flutter engine.', 43 ) 44 ..addOption('route', 45 help: 'Which route to load when running the app.', 46 ); 47 usesTargetOption(); 48 usesPortOptions(); 49 usesIpv6Flag(); 50 usesPubOption(); 51 usesTrackWidgetCreation(verboseHelp: verboseHelp); 52 usesIsolateFilterOption(hide: !verboseHelp); 53 } 54 55 bool get traceStartup => argResults['trace-startup']; 56 57 String get route => argResults['route']; 58} 59 60class RunCommand extends RunCommandBase { 61 RunCommand({ bool verboseHelp = false }) : super(verboseHelp: verboseHelp) { 62 requiresPubspecYaml(); 63 usesFilesystemOptions(hide: !verboseHelp); 64 argParser 65 ..addFlag('start-paused', 66 negatable: false, 67 help: 'Start in a paused mode and wait for a debugger to connect.', 68 ) 69 ..addFlag('enable-software-rendering', 70 negatable: false, 71 help: 'Enable rendering using the Skia software backend. ' 72 'This is useful when testing Flutter on emulators. By default, ' 73 'Flutter will attempt to either use OpenGL or Vulkan and fall back ' 74 'to software when neither is available.', 75 ) 76 ..addFlag('skia-deterministic-rendering', 77 negatable: false, 78 help: 'When combined with --enable-software-rendering, provides 100% ' 79 'deterministic Skia rendering.', 80 ) 81 ..addFlag('trace-skia', 82 negatable: false, 83 help: 'Enable tracing of Skia code. This is useful when debugging ' 84 'the GPU thread. By default, Flutter will not log skia code.', 85 ) 86 ..addFlag('trace-systrace', 87 negatable: false, 88 help: 'Enable tracing to the system tracer. This is only useful on ' 89 'platforms where such a tracer is available (Android and Fuchsia).', 90 ) 91 ..addFlag('dump-skp-on-shader-compilation', 92 negatable: false, 93 help: 'Automatically dump the skp that triggers new shader compilations. ' 94 'This is useful for wrting custom ShaderWarmUp to reduce jank. ' 95 'By default, this is not enabled to reduce the overhead. ' 96 'This is only available in profile or debug build. ', 97 ) 98 ..addFlag('await-first-frame-when-tracing', 99 defaultsTo: true, 100 help: 'Whether to wait for the first frame when tracing startup ("--trace-startup"), ' 101 'or just dump the trace as soon as the application is running. The first frame ' 102 'is detected by looking for a Timeline event with the name ' 103 '"${Tracing.firstUsefulFrameEventName}". ' 104 'By default, the widgets library\'s binding takes care of sending this event. ', 105 ) 106 ..addFlag('use-test-fonts', 107 negatable: true, 108 help: 'Enable (and default to) the "Ahem" font. This is a special font ' 109 'used in tests to remove any dependencies on the font metrics. It ' 110 'is enabled when you use "flutter test". Set this flag when running ' 111 'a test using "flutter run" for debugging purposes. This flag is ' 112 'only available when running in debug mode.', 113 ) 114 ..addFlag('build', 115 defaultsTo: true, 116 help: 'If necessary, build the app before running.', 117 ) 118 ..addOption('dart-flags', 119 hide: !verboseHelp, 120 help: 'Pass a list of comma separated flags to the Dart instance at ' 121 'application startup. Flags passed through this option must be ' 122 'present on the whitelist defined within the Flutter engine. If ' 123 'a non-whitelisted flag is encountered, the process will be ' 124 'terminated immediately.\n\n' 125 'This flag is not available on the stable channel and is only ' 126 'applied in debug and profile modes. This option should only ' 127 'be used for experiments and should not be used by typical users.') 128 ..addOption('use-application-binary', 129 hide: !verboseHelp, 130 help: 'Specify a pre-built application binary to use when running.', 131 ) 132 ..addOption('project-root', 133 hide: !verboseHelp, 134 help: 'Specify the project root directory.', 135 ) 136 ..addFlag('machine', 137 hide: !verboseHelp, 138 negatable: false, 139 help: 'Handle machine structured JSON command input and provide output ' 140 'and progress in machine friendly format.', 141 ) 142 ..addFlag('hot', 143 negatable: true, 144 defaultsTo: kHotReloadDefault, 145 help: 'Run with support for hot reloading. Only available for debug mode. Not available with "--trace-startup".', 146 ) 147 ..addFlag('resident', 148 negatable: true, 149 defaultsTo: true, 150 hide: !verboseHelp, 151 help: 'Stay resident after launching the application. Not available with "--trace-startup".', 152 ) 153 ..addOption('pid-file', 154 help: 'Specify a file to write the process id to. ' 155 'You can send SIGUSR1 to trigger a hot reload ' 156 'and SIGUSR2 to trigger a hot restart.', 157 ) 158 ..addFlag('benchmark', 159 negatable: false, 160 hide: !verboseHelp, 161 help: 'Enable a benchmarking mode. This will run the given application, ' 162 'measure the startup time and the app restart time, write the ' 163 'results out to "refresh_benchmark.json", and exit. This flag is ' 164 'intended for use in generating automated flutter benchmarks.', 165 ) 166 ..addFlag('disable-service-auth-codes', 167 negatable: false, 168 hide: !verboseHelp, 169 help: 'No longer require an authentication code to connect to the VM ' 170 'service (not recommended).') 171 ..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true) 172 ..addOption(FlutterOptions.kExtraGenSnapshotOptions, hide: true) 173 ..addMultiOption(FlutterOptions.kEnableExperiment, 174 splitCommas: true, 175 hide: true, 176 ); 177 } 178 179 @override 180 final String name = 'run'; 181 182 @override 183 final String description = 'Run your Flutter app on an attached device.'; 184 185 List<Device> devices; 186 187 @override 188 Future<String> get usagePath async { 189 final String command = await super.usagePath; 190 191 if (devices == null) 192 return command; 193 else if (devices.length > 1) 194 return '$command/all'; 195 else 196 return '$command/${getNameForTargetPlatform(await devices[0].targetPlatform)}'; 197 } 198 199 @override 200 Future<Map<CustomDimensions, String>> get usageValues async { 201 String deviceType, deviceOsVersion; 202 bool isEmulator; 203 204 if (devices == null || devices.isEmpty) { 205 deviceType = 'none'; 206 deviceOsVersion = 'none'; 207 isEmulator = false; 208 } else if (devices.length == 1) { 209 deviceType = getNameForTargetPlatform(await devices[0].targetPlatform); 210 deviceOsVersion = await devices[0].sdkNameAndVersion; 211 isEmulator = await devices[0].isLocalEmulator; 212 } else { 213 deviceType = 'multiple'; 214 deviceOsVersion = 'multiple'; 215 isEmulator = false; 216 } 217 final String modeName = getBuildInfo().modeName; 218 final AndroidProject androidProject = FlutterProject.current().android; 219 final IosProject iosProject = FlutterProject.current().ios; 220 final List<String> hostLanguage = <String>[]; 221 222 if (androidProject != null && androidProject.existsSync()) { 223 hostLanguage.add(androidProject.isKotlin ? 'kotlin' : 'java'); 224 } 225 if (iosProject != null && iosProject.exists) { 226 hostLanguage.add(iosProject.isSwift ? 'swift' : 'objc'); 227 } 228 229 return <CustomDimensions, String>{ 230 CustomDimensions.commandRunIsEmulator: '$isEmulator', 231 CustomDimensions.commandRunTargetName: deviceType, 232 CustomDimensions.commandRunTargetOsVersion: deviceOsVersion, 233 CustomDimensions.commandRunModeName: modeName, 234 CustomDimensions.commandRunProjectModule: '${FlutterProject.current().isModule}', 235 CustomDimensions.commandRunProjectHostLanguage: hostLanguage.join(','), 236 }; 237 } 238 239 @override 240 void printNoConnectedDevices() { 241 super.printNoConnectedDevices(); 242 if (getCurrentHostPlatform() == HostPlatform.darwin_x64 && 243 xcode.isInstalledAndMeetsVersionCheck) { 244 printStatus(''); 245 printStatus("Run 'flutter emulators' to list and start any available device emulators."); 246 printStatus(''); 247 printStatus('If you expected your device to be detected, please run "flutter doctor" to diagnose'); 248 printStatus('potential issues, or visit https://flutter.dev/setup/ for troubleshooting tips.'); 249 } 250 } 251 252 @override 253 bool get shouldRunPub { 254 // If we are running with a prebuilt application, do not run pub. 255 if (runningWithPrebuiltApplication) 256 return false; 257 258 return super.shouldRunPub; 259 } 260 261 bool shouldUseHotMode() { 262 final bool hotArg = argResults['hot'] ?? false; 263 final bool shouldUseHotMode = hotArg && !traceStartup; 264 return getBuildInfo().isDebug && shouldUseHotMode; 265 } 266 267 bool get runningWithPrebuiltApplication => 268 argResults['use-application-binary'] != null; 269 270 bool get stayResident => argResults['resident']; 271 bool get awaitFirstFrameWhenTracing => argResults['await-first-frame-when-tracing']; 272 273 @override 274 Future<void> validateCommand() async { 275 // When running with a prebuilt application, no command validation is 276 // necessary. 277 if (!runningWithPrebuiltApplication) 278 await super.validateCommand(); 279 devices = await findAllTargetDevices(); 280 if (devices == null) 281 throwToolExit(null); 282 if (deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication) 283 throwToolExit('Using -d all with --use-application-binary is not supported'); 284 } 285 286 DebuggingOptions _createDebuggingOptions() { 287 final BuildInfo buildInfo = getBuildInfo(); 288 if (buildInfo.isRelease) { 289 return DebuggingOptions.disabled(buildInfo); 290 } else { 291 return DebuggingOptions.enabled( 292 buildInfo, 293 startPaused: argResults['start-paused'], 294 disableServiceAuthCodes: argResults['disable-service-auth-codes'], 295 dartFlags: argResults['dart-flags'] ?? '', 296 useTestFonts: argResults['use-test-fonts'], 297 enableSoftwareRendering: argResults['enable-software-rendering'], 298 skiaDeterministicRendering: argResults['skia-deterministic-rendering'], 299 traceSkia: argResults['trace-skia'], 300 traceSystrace: argResults['trace-systrace'], 301 dumpSkpOnShaderCompilation: argResults['dump-skp-on-shader-compilation'], 302 observatoryPort: observatoryPort, 303 verboseSystemLogs: argResults['verbose-system-logs'], 304 ); 305 } 306 } 307 308 @override 309 Future<FlutterCommandResult> runCommand() async { 310 Cache.releaseLockEarly(); 311 312 // Enable hot mode by default if `--no-hot` was not passed and we are in 313 // debug mode. 314 final bool hotMode = shouldUseHotMode(); 315 316 writePidFile(argResults['pid-file']); 317 318 if (argResults['machine']) { 319 if (devices.length > 1) 320 throwToolExit('--machine does not support -d all.'); 321 final Daemon daemon = Daemon(stdinCommandStream, stdoutCommandResponse, 322 notifyingLogger: NotifyingLogger(), logToStdout: true); 323 AppInstance app; 324 try { 325 final String applicationBinaryPath = argResults['use-application-binary']; 326 app = await daemon.appDomain.startApp( 327 devices.first, fs.currentDirectory.path, targetFile, route, 328 _createDebuggingOptions(), hotMode, 329 applicationBinary: applicationBinaryPath == null 330 ? null 331 : fs.file(applicationBinaryPath), 332 trackWidgetCreation: argResults['track-widget-creation'], 333 projectRootPath: argResults['project-root'], 334 packagesFilePath: globalResults['packages'], 335 dillOutputPath: argResults['output-dill'], 336 ipv6: ipv6, 337 ); 338 } catch (error) { 339 throwToolExit(error.toString()); 340 } 341 final DateTime appStartedTime = systemClock.now(); 342 final int result = await app.runner.waitForAppToFinish(); 343 if (result != 0) 344 throwToolExit(null, exitCode: result); 345 return FlutterCommandResult( 346 ExitStatus.success, 347 timingLabelParts: <String>['daemon'], 348 endTimeOverride: appStartedTime, 349 ); 350 } 351 352 if (argResults['dart-flags'] != null && !FlutterVersion.instance.isMaster) { 353 throw UsageException('--dart-flags is not available on the stable ' 354 'channel.', null); 355 } 356 357 for (Device device in devices) { 358 if (await device.isLocalEmulator) { 359 if (await device.supportsHardwareRendering) { 360 final bool enableSoftwareRendering = argResults['enable-software-rendering'] == true; 361 if (enableSoftwareRendering) { 362 printStatus( 363 'Using software rendering with device ${device.name}. You may get better performance ' 364 'with hardware mode by configuring hardware rendering for your device.' 365 ); 366 } else { 367 printStatus( 368 'Using hardware rendering with device ${device.name}. If you get graphics artifacts, ' 369 'consider enabling software rendering with "--enable-software-rendering".' 370 ); 371 } 372 } 373 374 if (!isEmulatorBuildMode(getBuildMode())) { 375 throwToolExit('${toTitleCase(getFriendlyModeName(getBuildMode()))} mode is not supported for emulators.'); 376 } 377 } 378 } 379 380 if (hotMode) { 381 for (Device device in devices) { 382 if (!device.supportsHotReload) 383 throwToolExit('Hot reload is not supported by ${device.name}. Run with --no-hot.'); 384 } 385 } 386 387 List<String> expFlags; 388 if (argParser.options.containsKey(FlutterOptions.kEnableExperiment) && 389 argResults[FlutterOptions.kEnableExperiment].isNotEmpty) { 390 expFlags = argResults[FlutterOptions.kEnableExperiment]; 391 } 392 final List<FlutterDevice> flutterDevices = <FlutterDevice>[]; 393 final FlutterProject flutterProject = FlutterProject.current(); 394 for (Device device in devices) { 395 final FlutterDevice flutterDevice = await FlutterDevice.create( 396 device, 397 flutterProject: flutterProject, 398 trackWidgetCreation: argResults['track-widget-creation'], 399 fileSystemRoots: argResults['filesystem-root'], 400 fileSystemScheme: argResults['filesystem-scheme'], 401 viewFilter: argResults['isolate-filter'], 402 experimentalFlags: expFlags, 403 target: argResults['target'], 404 buildMode: getBuildMode(), 405 ); 406 flutterDevices.add(flutterDevice); 407 } 408 // Only support "web mode" with a single web device due to resident runner 409 // refactoring required otherwise. 410 final bool webMode = featureFlags.isWebEnabled && 411 devices.length == 1 && 412 await devices.single.targetPlatform == TargetPlatform.web_javascript; 413 414 ResidentRunner runner; 415 final String applicationBinaryPath = argResults['use-application-binary']; 416 if (hotMode && !webMode) { 417 runner = HotRunner( 418 flutterDevices, 419 target: targetFile, 420 debuggingOptions: _createDebuggingOptions(), 421 benchmarkMode: argResults['benchmark'], 422 applicationBinary: applicationBinaryPath == null 423 ? null 424 : fs.file(applicationBinaryPath), 425 projectRootPath: argResults['project-root'], 426 packagesFilePath: globalResults['packages'], 427 dillOutputPath: argResults['output-dill'], 428 stayResident: stayResident, 429 ipv6: ipv6, 430 ); 431 } else if (webMode) { 432 runner = webRunnerFactory.createWebRunner( 433 devices.single, 434 target: targetFile, 435 flutterProject: flutterProject, 436 ipv6: ipv6, 437 debuggingOptions: _createDebuggingOptions(), 438 ); 439 } else { 440 runner = ColdRunner( 441 flutterDevices, 442 target: targetFile, 443 debuggingOptions: _createDebuggingOptions(), 444 traceStartup: traceStartup, 445 awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing, 446 applicationBinary: applicationBinaryPath == null 447 ? null 448 : fs.file(applicationBinaryPath), 449 ipv6: ipv6, 450 stayResident: stayResident, 451 ); 452 } 453 454 DateTime appStartedTime; 455 // Sync completer so the completing agent attaching to the resident doesn't 456 // need to know about analytics. 457 // 458 // Do not add more operations to the future. 459 final Completer<void> appStartedTimeRecorder = Completer<void>.sync(); 460 // This callback can't throw. 461 unawaited(appStartedTimeRecorder.future.then<void>( 462 (_) { 463 appStartedTime = systemClock.now(); 464 if (stayResident) { 465 TerminalHandler(runner) 466 ..setupTerminal() 467 ..registerSignalHandlers(); 468 } 469 } 470 )); 471 472 final int result = await runner.run( 473 appStartedCompleter: appStartedTimeRecorder, 474 route: route, 475 ); 476 if (result != 0) { 477 throwToolExit(null, exitCode: result); 478 } 479 return FlutterCommandResult( 480 ExitStatus.success, 481 timingLabelParts: <String>[ 482 hotMode ? 'hot' : 'cold', 483 getModeName(getBuildMode()), 484 devices.length == 1 485 ? getNameForTargetPlatform(await devices[0].targetPlatform) 486 : 'multiple', 487 devices.length == 1 && await devices[0].isLocalEmulator ? 'emulator' : null, 488 ], 489 endTimeOverride: appStartedTime, 490 ); 491 } 492} 493