• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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