run.dart 18.4 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7 8
import 'package:args/command_runner.dart';

9
import '../base/common.dart';
10
import '../base/file_system.dart';
11
import '../base/time.dart';
12
import '../base/utils.dart';
13
import '../build_info.dart';
14
import '../cache.dart';
15
import '../device.dart';
16
import '../features.dart';
17
import '../globals.dart';
18
import '../macos/xcode.dart';
19
import '../project.dart';
20
import '../reporting/reporting.dart';
21
import '../resident_runner.dart';
22 23
import '../run_cold.dart';
import '../run_hot.dart';
24
import '../runner/flutter_command.dart';
25
import '../tracing.dart';
26
import '../version.dart';
27
import '../web/web_runner.dart';
28
import 'daemon.dart';
29

30
abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
31
  // Used by run and drive commands.
32 33
  RunCommandBase({ bool verboseHelp = false }) {
    addBuildModeFlags(defaultToRelease: false, verboseHelp: verboseHelp);
34
    usesFlavorOption();
35 36
    argParser
      ..addFlag('trace-startup',
37
        negatable: false,
38
        help: 'Trace application startup, then exit, saving the trace to a file.',
39
      )
40 41
      ..addFlag('verbose-system-logs',
        negatable: false,
42
        help: 'Include verbose logging from the flutter engine.',
43
      )
44 45
      ..addOption('route',
        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

60
class RunCommand extends RunCommandBase {
61
  RunCommand({ bool verboseHelp = false }) : super(verboseHelp: verboseHelp) {
62
    requiresPubspecYaml();
63
    usesFilesystemOptions(hide: !verboseHelp);
64 65
    argParser
      ..addFlag('start-paused',
66
        negatable: false,
67 68 69
        help: 'Start in a paused mode and wait for a debugger to connect.',
      )
      ..addFlag('enable-software-rendering',
70
        negatable: false,
71 72 73 74
        help: 'Enable rendering using the Skia software backend. '
              'This is useful when testing Flutter on emulators. By default, '
              'Flutter will attempt to either use OpenGL or Vulkan and fall back '
              '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 80 81
              'deterministic Skia rendering.',
      )
      ..addFlag('trace-skia',
82
        negatable: false,
83
        help: 'Enable tracing of Skia code. This is useful when debugging '
84 85
              'the GPU thread. By default, Flutter will not log skia code.',
      )
86 87 88 89 90
      ..addFlag('trace-systrace',
        negatable: false,
        help: 'Enable tracing to the system tracer. This is only useful on '
              'platforms where such a tracer is available (Android and Fuchsia).',
      )
91 92 93 94 95 96 97
      ..addFlag('dump-skp-on-shader-compilation',
        negatable: false,
        help: 'Automatically dump the skp that triggers new shader compilations. '
              'This is useful for wrting custom ShaderWarmUp to reduce jank. '
              'By default, this is not enabled to reduce the overhead. '
              'This is only available in profile or debug build. ',
      )
98 99 100 101 102 103 104 105
      ..addFlag('await-first-frame-when-tracing',
        defaultsTo: true,
        help: 'Whether to wait for the first frame when tracing startup ("--trace-startup"), '
              'or just dump the trace as soon as the application is running. The first frame '
              'is detected by looking for a Timeline event with the name '
              '"${Tracing.firstUsefulFrameEventName}". '
              'By default, the widgets library\'s binding takes care of sending this event. ',
      )
106
      ..addFlag('use-test-fonts',
107
        negatable: true,
108 109 110 111
        help: 'Enable (and default to) the "Ahem" font. This is a special font '
              'used in tests to remove any dependencies on the font metrics. It '
              'is enabled when you use "flutter test". Set this flag when running '
              'a test using "flutter run" for debugging purposes. This flag is '
112 113 114
              'only available when running in debug mode.',
      )
      ..addFlag('build',
115
        defaultsTo: true,
116 117
        help: 'If necessary, build the app before running.',
      )
118 119 120 121 122 123 124 125 126 127
      ..addOption('dart-flags',
        hide: !verboseHelp,
        help: 'Pass a list of comma separated flags to the Dart instance at '
              'application startup. Flags passed through this option must be '
              'present on the whitelist defined within the Flutter engine. If '
              'a non-whitelisted flag is encountered, the process will be '
              'terminated immediately.\n\n'
              'This flag is not available on the stable channel and is only '
              'applied in debug and profile modes. This option should only '
              'be used for experiments and should not be used by typical users.')
128
      ..addOption('use-application-binary',
129
        hide: !verboseHelp,
130 131 132
        help: 'Specify a pre-built application binary to use when running.',
      )
      ..addOption('project-root',
133
        hide: !verboseHelp,
134 135 136
        help: 'Specify the project root directory.',
      )
      ..addFlag('machine',
137
        hide: !verboseHelp,
138
        negatable: false,
139
        help: 'Handle machine structured JSON command input and provide output '
140 141 142
              'and progress in machine friendly format.',
      )
      ..addFlag('hot',
143 144
        negatable: true,
        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 149 150
        negatable: true,
        defaultsTo: true,
        hide: !verboseHelp,
151 152 153 154 155 156
        help: 'Stay resident after launching the application. Not available with "--trace-startup".',
      )
      ..addOption('pid-file',
        help: 'Specify a file to write the process id to. '
              'You can send SIGUSR1 to trigger a hot reload '
              'and SIGUSR2 to trigger a hot restart.',
157 158 159
      )
      ..addFlag('benchmark',
        negatable: false,
160
        hide: !verboseHelp,
161 162 163
        help: 'Enable a benchmarking mode. This will run the given application, '
              'measure the startup time and the app restart time, write the '
              'results out to "refresh_benchmark.json", and exit. This flag is '
164 165
              'intended for use in generating automated flutter benchmarks.',
      )
166 167 168 169 170
      ..addFlag('disable-service-auth-codes',
        negatable: false,
        hide: !verboseHelp,
        help: 'No longer require an authentication code to connect to the VM '
              'service (not recommended).')
171
      ..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true)
172 173 174 175 176
      ..addOption(FlutterOptions.kExtraGenSnapshotOptions, hide: true)
      ..addMultiOption(FlutterOptions.kEnableExperiment,
        splitCommas: true,
        hide: true,
      );
177 178
  }

179 180 181 182 183 184
  @override
  final String name = 'run';

  @override
  final String description = 'Run your Flutter app on an attached device.';

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 195 196
      return '$command/all';
    else
      return '$command/${getNameForTargetPlatform(await devices[0].targetPlatform)}';
197 198
  }

199
  @override
200
  Future<Map<CustomDimensions, String>> get usageValues async {
201
    String deviceType, deviceOsVersion;
202 203 204 205 206 207 208
    bool isEmulator;

    if (devices == null || devices.isEmpty) {
      deviceType = 'none';
      deviceOsVersion = 'none';
      isEmulator = false;
    } else if (devices.length == 1) {
209 210
      deviceType = getNameForTargetPlatform(await devices[0].targetPlatform);
      deviceOsVersion = await devices[0].sdkNameAndVersion;
211
      isEmulator = await devices[0].isLocalEmulator;
212 213 214
    } else {
      deviceType = 'multiple';
      deviceOsVersion = 'multiple';
215
      isEmulator = false;
216 217
    }
    final String modeName = getBuildInfo().modeName;
218 219 220
    final AndroidProject androidProject = FlutterProject.current().android;
    final IosProject iosProject = FlutterProject.current().ios;
    final List<String> hostLanguage = <String>[];
221

222 223 224 225 226 227 228
    if (androidProject != null && androidProject.existsSync()) {
      hostLanguage.add(androidProject.isKotlin ? 'kotlin' : 'java');
    }
    if (iosProject != null && iosProject.exists) {
      hostLanguage.add(iosProject.isSwift ? 'swift' : 'objc');
    }

229 230 231 232 233 234 235
    return <CustomDimensions, String>{
      CustomDimensions.commandRunIsEmulator: '$isEmulator',
      CustomDimensions.commandRunTargetName: deviceType,
      CustomDimensions.commandRunTargetOsVersion: deviceOsVersion,
      CustomDimensions.commandRunModeName: modeName,
      CustomDimensions.commandRunProjectModule: '${FlutterProject.current().isModule}',
      CustomDimensions.commandRunProjectHostLanguage: hostLanguage.join(','),
236
    };
237 238
  }

239 240 241 242
  @override
  void printNoConnectedDevices() {
    super.printNoConnectedDevices();
    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 253 254 255 256 257 258 259 260
  @override
  bool get shouldRunPub {
    // If we are running with a prebuilt application, do not run pub.
    if (runningWithPrebuiltApplication)
      return false;

    return super.shouldRunPub;
  }

261
  bool shouldUseHotMode() {
262
    final bool hotArg = argResults['hot'] ?? false;
263
    final bool shouldUseHotMode = hotArg && !traceStartup;
264
    return getBuildInfo().isDebug && shouldUseHotMode;
265 266
  }

267 268 269
  bool get runningWithPrebuiltApplication =>
      argResults['use-application-binary'] != null;

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 276 277 278
    // When running with a prebuilt application, no command validation is
    // necessary.
    if (!runningWithPrebuiltApplication)
      await super.validateCommand();
279 280
    devices = await findAllTargetDevices();
    if (devices == null)
281
      throwToolExit(null);
282 283
    if (deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication)
      throwToolExit('Using -d all with --use-application-binary is not supported');
284 285
  }

286
  DebuggingOptions _createDebuggingOptions() {
287 288
    final BuildInfo buildInfo = getBuildInfo();
    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 311 312 313 314 315
    Cache.releaseLockEarly();

    // Enable hot mode by default if `--no-hot` was not passed and we are in
    // debug mode.
    final bool hotMode = shouldUseHotMode();

316 317
    writePidFile(argResults['pid-file']);

318
    if (argResults['machine']) {
319 320
      if (devices.length > 1)
        throwToolExit('--machine does not support -d all.');
321 322
      final Daemon daemon = Daemon(stdinCommandStream, stdoutCommandResponse,
          notifyingLogger: NotifyingLogger(), logToStdout: true);
323 324
      AppInstance app;
      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 330 331
          applicationBinary: applicationBinaryPath == null
              ? null
              : 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 339 340
      } catch (error) {
        throwToolExit(error.toString());
      }
341
      final DateTime appStartedTime = systemClock.now();
342
      final int result = await app.runner.waitForAppToFinish();
343 344
      if (result != 0)
        throwToolExit(null, exitCode: result);
345
      return FlutterCommandResult(
346
        ExitStatus.success,
347
        timingLabelParts: <String>['daemon'],
348 349
        endTimeOverride: appStartedTime,
      );
350 351
    }

352
    if (argResults['dart-flags'] != null && !FlutterVersion.instance.isMaster) {
353 354 355 356
      throw UsageException('--dart-flags is not available on the stable '
                           'channel.', null);
    }

357
    for (Device device in devices) {
358 359 360 361 362
      if (await device.isLocalEmulator) {
        if (await device.supportsHardwareRendering) {
          final bool enableSoftwareRendering = argResults['enable-software-rendering'] == true;
          if (enableSoftwareRendering) {
            printStatus(
363
              'Using software rendering with device ${device.name}. You may get better performance '
364 365 366 367
              'with hardware mode by configuring hardware rendering for your device.'
            );
          } else {
            printStatus(
368
              'Using hardware rendering with device ${device.name}. If you get graphics artifacts, '
369 370 371 372 373 374
              'consider enabling software rendering with "--enable-software-rendering".'
            );
          }
        }

        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 383
        if (!device.supportsHotReload)
          throwToolExit('Hot reload is not supported by ${device.name}. Run with --no-hot.');
384
      }
385 386
    }

387 388 389 390 391
    List<String> expFlags;
    if (argParser.options.containsKey(FlutterOptions.kEnableExperiment) &&
        argResults[FlutterOptions.kEnableExperiment].isNotEmpty) {
      expFlags = argResults[FlutterOptions.kEnableExperiment];
    }
392
    final List<FlutterDevice> flutterDevices = <FlutterDevice>[];
393
    final FlutterProject flutterProject = FlutterProject.current();
394 395
    for (Device device in devices) {
      final FlutterDevice flutterDevice = await FlutterDevice.create(
396
        device,
397
        flutterProject: flutterProject,
398
        trackWidgetCreation: argResults['track-widget-creation'],
399 400
        fileSystemRoots: argResults['filesystem-root'],
        fileSystemScheme: argResults['filesystem-scheme'],
401
        viewFilter: argResults['isolate-filter'],
402
        experimentalFlags: expFlags,
403
        target: argResults['target'],
404
        buildMode: getBuildMode(),
405
      );
406 407
      flutterDevices.add(flutterDevice);
    }
408 409 410 411 412
    // Only support "web mode" with a single web device due to resident runner
    // refactoring required otherwise.
    final bool webMode = featureFlags.isWebEnabled &&
                         devices.length == 1  &&
                         await devices.single.targetPlatform == TargetPlatform.web_javascript;
413 414

    ResidentRunner runner;
asiva's avatar
asiva committed
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'],
asiva's avatar
asiva committed
422 423 424
        applicationBinary: applicationBinaryPath == null
            ? null
            : 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 435 436
        target: targetFile,
        flutterProject: flutterProject,
        ipv6: ipv6,
437
        debuggingOptions: _createDebuggingOptions(),
438
      );
Devon Carew's avatar
Devon Carew committed
439
    } else {
440
      runner = ColdRunner(
441
        flutterDevices,
442
        target: targetFile,
443
        debuggingOptions: _createDebuggingOptions(),
Devon Carew's avatar
Devon Carew committed
444
        traceStartup: traceStartup,
445
        awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
asiva's avatar
asiva committed
446 447 448
        applicationBinary: applicationBinaryPath == null
            ? null
            : fs.file(applicationBinaryPath),
449
        ipv6: ipv6,
450
        stayResident: stayResident,
Devon Carew's avatar
Devon Carew committed
451 452
      );
    }
453

454
    DateTime appStartedTime;
455 456
    // Sync completer so the completing agent attaching to the resident doesn't
    // 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 464 465 466 467 468 469
      (_) {
        appStartedTime = systemClock.now();
        if (stayResident) {
          TerminalHandler(runner)
            ..setupTerminal()
            ..registerSignalHandlers();
        }
      }
470
    ));
471

472
    final int result = await runner.run(
473
      appStartedCompleter: appStartedTimeRecorder,
474 475
      route: route,
    );
476
    if (result != 0) {
477
      throwToolExit(null, exitCode: result);
478
    }
479
    return FlutterCommandResult(
480
      ExitStatus.success,
481
      timingLabelParts: <String>[
482 483
        hotMode ? 'hot' : 'cold',
        getModeName(getBuildMode()),
484
        devices.length == 1
485 486
            ? getNameForTargetPlatform(await devices[0].targetPlatform)
            : 'multiple',
487
        devices.length == 1 && await devices[0].isLocalEmulator ? 'emulator' : null,
488 489 490
      ],
      endTimeOverride: appStartedTime,
    );
Adam Barth's avatar
Adam Barth committed
491 492
  }
}