resident_runner.dart 61.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:async';

9
import 'package:dds/dds.dart' as dds;
10
import 'package:meta/meta.dart';
11
import 'package:package_config/package_config.dart';
12
import 'package:vm_service/vm_service.dart' as vm_service;
13

14
import 'application_package.dart';
15
import 'artifacts.dart';
16
import 'asset.dart';
17
import 'base/command_help.dart';
18
import 'base/common.dart';
19
import 'base/context.dart';
20
import 'base/file_system.dart';
21
import 'base/io.dart' as io;
22
import 'base/logger.dart';
23
import 'base/platform.dart';
24
import 'base/signals.dart';
25
import 'base/terminal.dart';
26
import 'base/utils.dart';
27
import 'build_info.dart';
28
import 'build_system/build_system.dart';
29
import 'build_system/targets/dart_plugin_registrant.dart';
30
import 'build_system/targets/localizations.dart';
31
import 'bundle.dart';
32
import 'cache.dart';
33
import 'compile.dart';
34
import 'convert.dart';
35
import 'devfs.dart';
36
import 'device.dart';
37
import 'features.dart';
38
import 'globals.dart' as globals;
39
import 'project.dart';
40
import 'resident_devtools_handler.dart';
41 42
import 'run_cold.dart';
import 'run_hot.dart';
43
import 'sksl_writer.dart';
44
import 'vmservice.dart';
45

46
class FlutterDevice {
47 48
  FlutterDevice(
    this.device, {
49
    @required this.buildInfo,
50
    TargetModel targetModel = TargetModel.flutter,
51
    this.targetPlatform,
52
    ResidentCompiler generator,
53
    this.userIdentifier,
54
  }) : assert(buildInfo.trackWidgetCreation != null),
55
       generator = generator ?? ResidentCompiler(
56
         globals.artifacts.getArtifactPath(
57 58
           Artifact.flutterPatchedSdkPath,
           platform: targetPlatform,
59
           mode: buildInfo.mode,
60
         ),
61 62
         buildMode: buildInfo.mode,
         trackWidgetCreation: buildInfo.trackWidgetCreation,
63 64
         fileSystemRoots: buildInfo.fileSystemRoots ?? <String>[],
         fileSystemScheme: buildInfo.fileSystemScheme,
65
         targetModel: targetModel,
66
         dartDefines: buildInfo.dartDefines,
67
         packagesPath: buildInfo.packagesPath,
68
         extraFrontEndOptions: buildInfo.extraFrontEndOptions,
69 70 71
         artifacts: globals.artifacts,
         processManager: globals.processManager,
         logger: globals.logger,
72
         platform: globals.platform,
73
         fileSystem: globals.fs,
74 75
       );

76
  /// Create a [FlutterDevice] with optional code generation enabled.
77 78
  static Future<FlutterDevice> create(
    Device device, {
79
    @required String target,
80
    @required BuildInfo buildInfo,
81
    @required Platform platform,
82 83 84
    TargetModel targetModel = TargetModel.flutter,
    List<String> experimentalFlags,
    ResidentCompiler generator,
85
    String userIdentifier,
86 87
  }) async {
    ResidentCompiler generator;
88 89 90 91
    final TargetPlatform targetPlatform = await device.targetPlatform;
    if (device.platformType == PlatformType.fuchsia) {
      targetModel = TargetModel.flutterRunner;
    }
92 93 94 95 96 97
    // For both web and non-web platforms we initialize dill to/from
    // a shared location for faster bootstrapping. If the compiler fails
    // due to a kernel target or version mismatch, no error is reported
    // and the compiler starts up as normal. Unexpected errors will print
    // a warning message and dump some debug information which can be
    // used to file a bug, but the compiler will still start up correctly.
98
    if (targetPlatform == TargetPlatform.web_javascript) {
99
      // TODO(zanderso): consistently provide these flags across platforms.
100
      HostArtifact platformDillArtifact;
101
      final List<String> extraFrontEndOptions = List<String>.of(buildInfo.extraFrontEndOptions ?? <String>[]);
102
      if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
103
        platformDillArtifact = HostArtifact.webPlatformKernelDill;
104 105 106
        if (!extraFrontEndOptions.contains('--no-sound-null-safety')) {
          extraFrontEndOptions.add('--no-sound-null-safety');
        }
107
      } else if (buildInfo.nullSafetyMode == NullSafetyMode.sound) {
108
        platformDillArtifact = HostArtifact.webPlatformSoundKernelDill;
109 110
        if (!extraFrontEndOptions.contains('--sound-null-safety')) {
          extraFrontEndOptions.add('--sound-null-safety');
111
        }
112 113
      } else {
        assert(false);
114 115
      }

116
      generator = ResidentCompiler(
117
        globals.artifacts.getHostArtifact(HostArtifact.flutterWebSdk).path,
118 119
        buildMode: buildInfo.mode,
        trackWidgetCreation: buildInfo.trackWidgetCreation,
120
        fileSystemRoots: buildInfo.fileSystemRoots ?? <String>[],
121 122 123
        // Override the filesystem scheme so that the frontend_server can find
        // the generated entrypoint code.
        fileSystemScheme: 'org-dartlang-app',
124
        initializeFromDill: buildInfo.initializeFromDill ?? getDefaultCachedKernelPath(
125
          trackWidgetCreation: buildInfo.trackWidgetCreation,
126
          dartDefines: buildInfo.dartDefines,
127
          extraFrontEndOptions: extraFrontEndOptions,
128
        ),
129
        assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate,
130
        targetModel: TargetModel.dartdevc,
131
        extraFrontEndOptions: extraFrontEndOptions,
132
        platformDill: globals.fs.file(globals.artifacts
133
          .getHostArtifact(platformDillArtifact))
134
          .absolute.uri.toString(),
135
        dartDefines: buildInfo.dartDefines,
136
        librariesSpec: globals.fs.file(globals.artifacts
137
          .getHostArtifact(HostArtifact.flutterWebLibrariesJson)).uri.toString(),
138
        packagesPath: buildInfo.packagesPath,
139 140 141
        artifacts: globals.artifacts,
        processManager: globals.processManager,
        logger: globals.logger,
142
        fileSystem: globals.fs,
143
        platform: platform,
144
      );
145
    } else {
146 147
      // The flutter-widget-cache feature only applies to run mode.
      List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions;
148 149 150
      extraFrontEndOptions = <String>[
        if (featureFlags.isSingleWidgetReloadEnabled)
         '--flutter-widget-cache',
151
        '--enable-experiment=alternative-invalidation-strategy',
152 153
        ...?extraFrontEndOptions,
      ];
154
      generator = ResidentCompiler(
155
        globals.artifacts.getArtifactPath(
156 157
          Artifact.flutterPatchedSdkPath,
          platform: targetPlatform,
158
          mode: buildInfo.mode,
159
        ),
160 161
        buildMode: buildInfo.mode,
        trackWidgetCreation: buildInfo.trackWidgetCreation,
162 163
        fileSystemRoots: buildInfo.fileSystemRoots,
        fileSystemScheme: buildInfo.fileSystemScheme,
164
        targetModel: targetModel,
165
        dartDefines: buildInfo.dartDefines,
166
        extraFrontEndOptions: extraFrontEndOptions,
167
        initializeFromDill: buildInfo.initializeFromDill ?? getDefaultCachedKernelPath(
168
          trackWidgetCreation: buildInfo.trackWidgetCreation,
169
          dartDefines: buildInfo.dartDefines,
170
          extraFrontEndOptions: extraFrontEndOptions,
171
        ),
172
        assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate,
173
        packagesPath: buildInfo.packagesPath,
174 175 176
        artifacts: globals.artifacts,
        processManager: globals.processManager,
        logger: globals.logger,
177
        platform: platform,
178
        fileSystem: globals.fs,
179 180
      );
    }
181

182 183 184
    return FlutterDevice(
      device,
      targetModel: targetModel,
185
      targetPlatform: targetPlatform,
186
      generator: generator,
187
      buildInfo: buildInfo,
188
      userIdentifier: userIdentifier,
189 190 191
    );
  }

192
  final TargetPlatform targetPlatform;
193
  final Device device;
194
  final ResidentCompiler generator;
195
  final BuildInfo buildInfo;
196
  final String userIdentifier;
197 198

  DevFSWriter devFSWriter;
199
  Stream<Uri> observatoryUris;
200
  FlutterVmService vmService;
201 202 203
  DevFS devFS;
  ApplicationPackage package;
  StreamSubscription<String> _loggingSubscription;
204
  bool _isListeningForObservatoryUri;
205

206 207 208
  /// Whether the stream [observatoryUris] is still open.
  bool get isWaitingForObservatory => _isListeningForObservatoryUri ?? false;

209 210 211 212 213
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
  /// will be registered.
  /// The 'reloadSources' service can be used by other Service Protocol clients
  /// connected to the VM (e.g. Observatory) to request a reload of the source
  /// code of the running application (a.k.a. HotReload).
214 215
  /// The 'compileExpression' service can be used to compile user-provided
  /// expressions requested during debugging of the application.
216 217
  /// This ensures that the reload process follows the normal orchestration of
  /// the Flutter Tools and not just the VM internal service.
218
  Future<void> connect({
219 220 221
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
222
    GetSkSLMethod getSkSLMethod,
223
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
224 225 226
    int hostVmServicePort,
    int ddsPort,
    bool disableServiceAuthCodes = false,
227
    bool cacheStartupProfile = false,
228
    bool enableDds = true,
229
    @required bool allowExistingDdsInstance,
230
    bool ipv6 = false,
231 232 233 234 235 236 237
  }) {
    final Completer<void> completer = Completer<void>();
    StreamSubscription<void> subscription;
    bool isWaitingForVm = false;

    subscription = observatoryUris.listen((Uri observatoryUri) async {
      // FYI, this message is used as a sentinel in tests.
238
      globals.printTrace('Connecting to service protocol: $observatoryUri');
239
      isWaitingForVm = true;
240
      bool existingDds = false;
241
      FlutterVmService service;
242
      if (enableDds) {
243
        void handleError(Exception e, StackTrace st) {
244 245
          globals.printTrace('Fail to connect to service protocol: $observatoryUri: $e');
          if (!completer.isCompleted) {
246
            completer.completeError('failed to connect to $observatoryUri', st);
247 248
          }
        }
249 250 251 252
        // First check if the VM service is actually listening on observatoryUri as
        // this may not be the case when scraping logcat for URIs. If this URI is
        // from an old application instance, we shouldn't try and start DDS.
        try {
253
          service = await connectToVmService(observatoryUri, logger: globals.logger);
254
          await service.dispose();
255 256 257 258 259 260 261 262
        } on Exception catch (exception) {
          globals.printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
          if (!completer.isCompleted && !_isListeningForObservatoryUri) {
            completer.completeError('failed to connect to $observatoryUri');
          }
          return;
        }

263 264 265 266 267 268
        // This first try block is meant to catch errors that occur during DDS startup
        // (e.g., failure to bind to a port, failure to connect to the VM service,
        // attaching to a VM service with existing clients, etc.).
        try {
          await device.dds.startDartDevelopmentService(
            observatoryUri,
269 270 271
            hostPort: ddsPort,
            ipv6: ipv6,
            disableServiceAuthCodes: disableServiceAuthCodes,
272
            logger: globals.logger,
273
            cacheStartupProfile: cacheStartupProfile,
274
          );
275
        } on dds.DartDevelopmentServiceException catch (e, st) {
276 277
          if (!allowExistingDdsInstance ||
              (e.errorCode != dds.DartDevelopmentServiceException.existingDdsInstanceError)) {
278
            handleError(e, st);
279 280 281
            return;
          } else {
            existingDds = true;
282
          }
283 284
        } on ToolExit {
          rethrow;
285 286
        } on Exception catch (e, st) {
          handleError(e, st);
287 288
          return;
        }
289
      }
290 291 292 293
      // This second try block handles cases where the VM service connection goes down
      // before flutter_tools connects to DDS. The DDS `done` future completes when DDS
      // shuts down, including after an error. If `done` completes before `connectToVmService`,
      // something went wrong that caused DDS to shutdown early.
294
      try {
295 296 297
        service = await Future.any<dynamic>(
          <Future<dynamic>>[
            connectToVmService(
298
              enableDds ? device.dds.uri : observatoryUri,
299 300 301 302 303 304
              reloadSources: reloadSources,
              restart: restart,
              compileExpression: compileExpression,
              getSkSLMethod: getSkSLMethod,
              printStructuredErrorLogMethod: printStructuredErrorLogMethod,
              device: device,
305
              logger: globals.logger,
306
            ),
307 308
            if (!existingDds)
              device.dds.done.whenComplete(() => throw Exception('DDS shut down too early')),
309
          ]
310
        ) as FlutterVmService;
311
      } on Exception catch (exception) {
312
        globals.printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
313 314 315 316 317 318 319 320
        if (!completer.isCompleted && !_isListeningForObservatoryUri) {
          completer.completeError('failed to connect to $observatoryUri');
        }
        return;
      }
      if (completer.isCompleted) {
        return;
      }
321
      globals.printTrace('Successfully connected to service protocol: $observatoryUri');
322

323
      vmService = service;
324
      (await device.getLogReader(app: package)).connectedVMService = vmService;
325 326 327
      completer.complete();
      await subscription.cancel();
    }, onError: (dynamic error) {
328
      globals.printTrace('Fail to handle observatory URI: $error');
329 330 331
    }, onDone: () {
      _isListeningForObservatoryUri = false;
      if (!completer.isCompleted && !isWaitingForVm) {
332
        completer.completeError(Exception('connection to device ended too early'));
333 334 335 336
      }
    });
    _isListeningForObservatoryUri = true;
    return completer.future;
337 338
  }

339 340 341
  Future<void> exitApps({
    @visibleForTesting Duration timeoutDelay = const Duration(seconds: 10),
  }) async {
342
    // TODO(zanderso): https://github.com/flutter/flutter/issues/83127
343 344 345
    // When updating `flutter attach` to support running without a device,
    // this will need to be changed to fall back to io exit.
    return device.stopApp(package, userIdentifier: userIdentifier);
346 347
  }

348 349
  Future<Uri> setupDevFS(
    String fsName,
350 351
    Directory rootDirectory,
  ) {
352
    // One devFS per device. Shared by all running instances.
353
    devFS = DevFS(
354
      vmService,
355 356
      fsName,
      rootDirectory,
357
      osUtils: globals.os,
358 359
      fileSystem: globals.fs,
      logger: globals.logger,
360 361 362 363
    );
    return devFS.create();
  }

364
  Future<void> startEchoingDeviceLog() async {
365
    if (_loggingSubscription != null) {
366
      return;
367
    }
368
    final Stream<String> logStream = (await device.getLogReader(app: package)).logLines;
369
    if (logStream == null) {
370
      globals.printError('Failed to read device log stream');
371 372 373
      return;
    }
    _loggingSubscription = logStream.listen((String line) {
374
      if (!line.contains(globals.kVMServiceMessageRegExp)) {
375
        globals.printStatus(line, wrap: false);
376
      }
377 378 379
    });
  }

380
  Future<void> stopEchoingDeviceLog() async {
381
    if (_loggingSubscription == null) {
382
      return;
383
    }
384 385 386 387
    await _loggingSubscription.cancel();
    _loggingSubscription = null;
  }

388
  Future<void> initLogReader() async {
389
    final vm_service.VM vm = await vmService.service.getVM();
390 391
    final DeviceLogReader logReader = await device.getLogReader(app: package);
    logReader.appPid = vm.pid;
392 393 394 395 396 397 398
  }

  Future<int> runHot({
    HotRunner hotRunner,
    String route,
  }) async {
    final bool prebuiltMode = hotRunner.applicationBinary != null;
399
    final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName;
400
    globals.printStatus(
401
      'Launching ${getDisplayPath(hotRunner.mainPath, globals.fs)} '
402 403
      'on ${device.name} in $modeName mode...',
    );
404 405

    final TargetPlatform targetPlatform = await device.targetPlatform;
406
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
407
      targetPlatform,
408
      buildInfo: hotRunner.debuggingOptions.buildInfo,
409
      applicationBinary: hotRunner.applicationBinary,
410 411 412 413
    );

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
414
      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
415
      if (hint != null) {
416
        message += '\n$hint';
417
      }
418
      globals.printError(message);
419 420
      return 1;
    }
421
    devFSWriter = device.createDevFSWriter(package, userIdentifier);
422

423 424 425
    final Map<String, dynamic> platformArgs = <String, dynamic>{
      'multidex': hotRunner.multidexEnabled,
    };
426

427
    await startEchoingDeviceLog();
428 429 430 431 432 433 434 435 436

    // Start the application.
    final Future<LaunchResult> futureResult = device.startApp(
      package,
      mainPath: hotRunner.mainPath,
      debuggingOptions: hotRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
437
      ipv6: hotRunner.ipv6,
438
      userIdentifier: userIdentifier,
439 440 441 442 443
    );

    final LaunchResult result = await futureResult;

    if (!result.started) {
444
      globals.printError('Error launching application on ${device.name}.');
445 446 447
      await stopEchoingDeviceLog();
      return 2;
    }
448
    if (result.hasObservatory) {
449 450 451
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
452
    } else {
453 454 455
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
456
    }
457 458 459 460 461 462 463 464
    return 0;
  }

  Future<int> runCold({
    ColdRunner coldRunner,
    String route,
  }) async {
    final TargetPlatform targetPlatform = await device.targetPlatform;
465
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
466
      targetPlatform,
467
      buildInfo: coldRunner.debuggingOptions.buildInfo,
468
      applicationBinary: coldRunner.applicationBinary,
469
    );
470
    devFSWriter = device.createDevFSWriter(package, userIdentifier);
471

472
    final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName;
473 474 475
    final bool prebuiltMode = coldRunner.applicationBinary != null;
    if (coldRunner.mainPath == null) {
      assert(prebuiltMode);
476 477 478 479
      globals.printStatus(
        'Launching ${package.displayName} '
        'on ${device.name} in $modeName mode...',
      );
480
    } else {
481
      globals.printStatus(
482
        'Launching ${getDisplayPath(coldRunner.mainPath, globals.fs)} '
483 484
        'on ${device.name} in $modeName mode...',
      );
485 486 487 488
    }

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
489
      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
490
      if (hint != null) {
491
        message += '\n$hint';
492
      }
493
      globals.printError(message);
494 495 496
      return 1;
    }

497
    final Map<String, dynamic> platformArgs = <String, dynamic>{};
498
    if (coldRunner.traceStartup != null) {
499
      platformArgs['trace-startup'] = coldRunner.traceStartup;
500
    }
501
    platformArgs['multidex'] = coldRunner.multidexEnabled;
502

503
    await startEchoingDeviceLog();
504 505 506 507 508 509 510 511

    final LaunchResult result = await device.startApp(
      package,
      mainPath: coldRunner.mainPath,
      debuggingOptions: coldRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
512
      ipv6: coldRunner.ipv6,
513
      userIdentifier: userIdentifier,
514 515 516
    );

    if (!result.started) {
517
      globals.printError('Error running application on ${device.name}.');
518 519 520
      await stopEchoingDeviceLog();
      return 2;
    }
521
    if (result.hasObservatory) {
522 523 524
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
525
    } else {
526 527 528
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
529
    }
530 531 532
    return 0;
  }

533
  Future<UpdateFSReport> updateDevFS({
534
    Uri mainUri,
535
    String target,
536
    AssetBundle bundle,
537
    DateTime firstBuildTime,
538 539 540
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
541
    String projectRootPath,
542
    String pathToReload,
543
    @required String dillOutputPath,
544
    @required List<Uri> invalidatedFiles,
545
    @required PackageConfig packageConfig,
546
  }) async {
547
    final Status devFSStatus = globals.logger.startProgress(
548 549
      'Syncing files to device ${device.name}...',
    );
550
    UpdateFSReport report;
551
    try {
552 553 554 555 556 557 558 559 560 561 562 563 564 565
      report = await devFS.update(
        mainUri: mainUri,
        target: target,
        bundle: bundle,
        firstBuildTime: firstBuildTime,
        bundleFirstUpload: bundleFirstUpload,
        generator: generator,
        fullRestart: fullRestart,
        dillOutputPath: dillOutputPath,
        trackWidgetCreation: buildInfo.trackWidgetCreation,
        projectRootPath: projectRootPath,
        pathToReload: pathToReload,
        invalidatedFiles: invalidatedFiles,
        packageConfig: packageConfig,
566
        devFSWriter: devFSWriter,
567
      );
568 569
    } on DevFSException {
      devFSStatus.cancel();
570
      return UpdateFSReport();
571
    }
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
    devFSStatus.stop();
    globals.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
    return report;
  }

  Future<void> updateReloadStatus(bool wasReloadSuccessful) async {
    if (wasReloadSuccessful) {
      generator?.accept();
    } else {
      await generator?.reject();
    }
  }
}

/// A subset of the [ResidentRunner] for delegating to attached flutter devices.
abstract class ResidentHandlers {
  List<FlutterDevice> get flutterDevices;

  /// Whether the resident runner has hot reload and restart enabled.
  bool get hotMode;

  /// Whether the resident runner is connect to the device's VM Service.
  bool get supportsServiceProtocol;

  /// The application is running in debug mode.
  bool get isRunningDebug;

  /// The application is running in profile mode.
  bool get isRunningProfile;

  /// The application is running in release mode.
  bool get isRunningRelease;

  /// The resident runner should stay resident after establishing a connection with the
  /// application.
  bool get stayResident;

  /// Whether all of the connected devices support hot restart.
  ///
  /// To prevent scenarios where only a subset of devices are hot restarted,
  /// the runner requires that all attached devices can support hot restart
  /// before enabling it.
  bool get supportsRestart;

  /// Whether all of the connected devices support gathering SkSL.
  bool get supportsWriteSkSL;

  /// Whether all of the connected devices support hot reload.
  bool get canHotReload;

622 623
  ResidentDevtoolsHandler get residentDevtoolsHandler;

624 625 626 627 628 629 630 631 632
  @protected
  Logger get logger;

  @protected
  FileSystem get fileSystem;

  /// Called to print help to the terminal.
  void printHelp({ @required bool details });

633
  /// Perform a hot reload or hot restart of all attached applications.
634 635 636 637 638 639
  ///
  /// If [fullRestart] is true, a hot restart is performed. Otherwise a hot reload
  /// is run instead. On web devices, this only performs a hot restart regardless of
  /// the value of [fullRestart].
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
    final String mode = isRunningProfile ? 'profile' :isRunningRelease ? 'release' : 'this';
640
    throw Exception('${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode');
641 642 643 644 645 646 647 648
  }

  /// Dump the application's current widget tree to the terminal.
  Future<bool> debugDumpApp() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
649 650
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
651
        final String data = await device.vmService.flutterDebugDumpApp(
652
          isolateId: view.uiIsolate.id,
653 654 655 656 657 658 659 660 661 662 663 664 665
        );
        logger.printStatus(data);
      }
    }
    return true;
  }

  /// Dump the application's current render tree to the terminal.
  Future<bool> debugDumpRenderTree() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
666 667
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
668
        final String data = await device.vmService.flutterDebugDumpRenderTree(
669
          isolateId: view.uiIsolate.id,
670 671 672 673 674 675 676
        );
        logger.printStatus(data);
      }
    }
    return true;
  }

677 678 679 680 681 682 683 684
  /// Dump frame rasterization metrics for the last rendered frame.
  ///
  /// The last frames gets re-painted while recording additional tracing info
  /// pertaining to the various draw calls issued by the frame. The timings
  /// recorded here are not indicative of production performance. The intended
  /// use case is to look at the various layers in proportion to see what
  /// contributes the most towards raster performance.
  Future<bool> debugFrameJankMetrics() async {
685
    if (!supportsServiceProtocol) {
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
        final Map<String, Object> rasterData =
          await device.vmService.renderFrameWithRasterStats(
            viewId: view.id,
            uiIsolateId: view.uiIsolate.id,
          );
        if (rasterData != null) {
          final File tempFile = globals.fsUtils.getUniqueFile(
            globals.fs.currentDirectory,
            'flutter_jank_metrics',
            'json',
          );
          tempFile.writeAsStringSync(jsonEncode(rasterData), flush: true);
          logger.printStatus('Wrote jank metrics to ${tempFile.absolute.path}');
        } else {
          logger.printWarning('Unable to get jank metrics.');
        }
      }
    }
    return true;
  }

712 713 714 715 716 717
  /// Dump the application's current layer tree to the terminal.
  Future<bool> debugDumpLayerTree() async {
    if (!supportsServiceProtocol || !isRunningDebug) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
718 719
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
720
        final String data = await device.vmService.flutterDebugDumpLayerTree(
721
          isolateId: view.uiIsolate.id,
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
        );
        logger.printStatus(data);
      }
    }
    return true;
  }

  /// Dump the application's current semantics tree to the terminal.
  ///
  /// If semantics are not enabled, nothing is returned.
  Future<bool> debugDumpSemanticsTreeInTraversalOrder() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
737 738
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
739
        final String data = await device.vmService.flutterDebugDumpSemanticsTreeInTraversalOrder(
740
          isolateId: view.uiIsolate.id,
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
        );
        logger.printStatus(data);
      }
    }
    return true;
  }

  /// Dump the application's current semantics tree to the terminal.
  ///
  /// If semantics are not enabled, nothing is returned.
  Future<bool> debugDumpSemanticsTreeInInverseHitTestOrder() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
756 757
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
758
        final String data = await device.vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
759
          isolateId: view.uiIsolate.id,
760 761 762 763 764 765 766 767 768 769 770 771 772
        );
        logger.printStatus(data);
      }
    }
    return true;
  }

  /// Toggle the "paint size" debugging feature.
  Future<bool> debugToggleDebugPaintSizeEnabled() async {
    if (!supportsServiceProtocol || !isRunningDebug) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
773 774
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
775
        await device.vmService.flutterToggleDebugPaintSizeEnabled(
776
          isolateId: view.uiIsolate.id,
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
        );
      }
    }
    return true;
  }

  /// Toggle the performance overlay.
  ///
  /// This is not supported in web mode.
  Future<bool> debugTogglePerformanceOverlayOverride() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
      if (device.targetPlatform == TargetPlatform.web_javascript) {
        continue;
      }
794 795
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
796
        await device.vmService.flutterTogglePerformanceOverlayOverride(
797
          isolateId: view.uiIsolate.id,
798 799 800 801 802 803 804 805 806 807 808 809
        );
      }
    }
    return true;
  }

  /// Toggle the widget inspector.
  Future<bool> debugToggleWidgetInspector() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
810 811
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
812
        await device.vmService.flutterToggleWidgetInspector(
813
          isolateId: view.uiIsolate.id,
814 815 816 817 818 819 820 821 822 823 824 825
        );
      }
    }
    return true;
  }

  /// Toggle the "invert images" debugging feature.
  Future<bool> debugToggleInvertOversizedImages() async {
    if (!supportsServiceProtocol || !isRunningDebug) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
826 827
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
828
        await device.vmService.flutterToggleInvertOversizedImages(
829
          isolateId: view.uiIsolate.id,
830 831 832 833 834 835 836 837 838 839 840 841
        );
      }
    }
    return true;
  }

  /// Toggle the "profile widget builds" debugging feature.
  Future<bool> debugToggleProfileWidgetBuilds() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
842 843
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
844
        await device.vmService.flutterToggleProfileWidgetBuilds(
845
          isolateId: view.uiIsolate.id,
846 847 848 849 850 851
        );
      }
    }
    return true;
  }

852
  /// Toggle the operating system brightness (light or dark).
853 854 855 856
  Future<bool> debugToggleBrightness() async {
    if (!supportsServiceProtocol) {
      return false;
    }
857
    final List<FlutterView> views = await flutterDevices.first.vmService.getFlutterViews();
858
    final Brightness current = await flutterDevices.first.vmService.flutterBrightnessOverride(
859
      isolateId: views.first.uiIsolate.id,
860 861 862 863 864 865 866 867
    );
    Brightness next;
    if (current == Brightness.light) {
      next = Brightness.dark;
    } else {
      next = Brightness.light;
    }
    for (final FlutterDevice device in flutterDevices) {
868 869
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
870
        await device.vmService.flutterBrightnessOverride(
871
          isolateId: view.uiIsolate.id,
872 873 874 875 876 877 878 879 880 881 882 883 884
          brightness: next,
        );
      }
      logger.printStatus('Changed brightness to $next.');
    }
    return true;
  }

  /// Rotate the application through different `defaultTargetPlatform` values.
  Future<bool> debugTogglePlatform() async {
    if (!supportsServiceProtocol || !isRunningDebug) {
      return false;
    }
885
    final List<FlutterView> views = await flutterDevices.first.vmService.getFlutterViews();
886 887
    final String from = await flutterDevices
      .first.vmService.flutterPlatformOverride(
888
        isolateId: views.first.uiIsolate.id,
889 890 891
      );
    final String to = nextPlatform(from);
    for (final FlutterDevice device in flutterDevices) {
892 893
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
894 895
        await device.vmService.flutterPlatformOverride(
          platform: to,
896
          isolateId: view.uiIsolate.id,
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
        );
      }
    }
    logger.printStatus('Switched operating system to $to');
    return true;
  }

  /// Write the SkSL shaders to a zip file in build directory.
  ///
  /// Returns the name of the file, or `null` on failures.
  Future<String> writeSkSL() async {
    if (!supportsWriteSkSL) {
      throw Exception('writeSkSL is not supported by this runner.');
    }
    final List<FlutterView> views = await flutterDevices
      .first
      .vmService.getFlutterViews();
    final Map<String, Object> data = await flutterDevices.first.vmService.getSkSLs(
      viewId: views.first.id,
    );
    final Device device = flutterDevices.first.device;
    return sharedSkSlWriter(device, data);
  }

  /// Take a screenshot on the provided [device].
  ///
  /// If the device has a connected vmservice, this method will attempt to hide
  /// and restore the debug banner before taking the screenshot.
  ///
926 927 928 929 930 931 932 933
  /// If the device type does not support a "native" screenshot, then this
  /// will fallback to a rasterizer screenshot from the engine. This has the
  /// downside of being unable to display the contents of platform views.
  ///
  /// This method will return without writing the screenshot file if any
  /// RPC errors are encountered, printing them to stderr. This is true even
  /// if an error occurs after the data has already been received, such as
  /// from restoring the debug banner.
934
  Future<void> screenshot(FlutterDevice device) async {
935 936 937
    if (!device.device.supportsScreenshot && !supportsServiceProtocol) {
      return;
    }
938 939 940 941 942 943 944 945
    final Status status = logger.startProgress(
      'Taking screenshot for ${device.device.name}...',
    );
    final File outputFile = getUniqueFile(
      fileSystem.currentDirectory,
      'flutter',
      'png',
    );
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984

    try {
      bool result;
      if (device.device.supportsScreenshot) {
        result = await _toggleDebugBanner(device, () => device.device.takeScreenshot(outputFile));
      } else {
        result = await _takeVmServiceScreenshot(device, outputFile);
      }
      if (!result) {
        return;
      }
      final int sizeKB = outputFile.lengthSync() ~/ 1024;
      status.stop();
      logger.printStatus(
        'Screenshot written to ${fileSystem.path.relative(outputFile.path)} (${sizeKB}kB).',
      );
    } on Exception catch (error) {
      status.cancel();
      logger.printError('Error taking screenshot: $error');
    }
  }

  Future<bool> _takeVmServiceScreenshot(FlutterDevice device, File outputFile) async {
    final bool isWebDevice = device.targetPlatform == TargetPlatform.web_javascript;
    assert(supportsServiceProtocol);

    return _toggleDebugBanner(device, () async {
      final vm_service.Response response = isWebDevice
        ? await device.vmService.callMethodWrapper('ext.dwds.screenshot')
        : await device.vmService.screenshot();
      if (response == null) {
       throw Exception('Failed to take screenshot');
      }
      final String data = response.json[isWebDevice ? 'data' : 'screenshot'] as String;
      outputFile.writeAsBytesSync(base64.decode(data));
    });
  }

  Future<bool> _toggleDebugBanner(FlutterDevice device, Future<void> Function() cb) async {
985
    List<FlutterView> views = <FlutterView>[];
986
    if (supportsServiceProtocol) {
987
      views = await device.vmService.getFlutterViews();
988 989
    }

990 991
    Future<bool> setDebugBanner(bool value) async {
      try {
992
        for (final FlutterView view in views) {
993 994
          await device.vmService.flutterDebugAllowBanner(
            value,
995
            isolateId: view.uiIsolate.id,
996 997 998
          );
        }
        return true;
999
      } on vm_service.RPCError catch (error) {
1000 1001 1002 1003
        logger.printError('Error communicating with Flutter on the device: $error');
        return false;
      }
    }
1004 1005 1006 1007
    if (!await setDebugBanner(false)) {
      return false;
    }
    bool succeeded = true;
1008
    try {
1009 1010 1011 1012
      await cb();
    } finally {
      if (!await setDebugBanner(true)) {
        succeeded = false;
1013 1014
      }
    }
1015
    return succeeded;
1016
  }
1017

1018

1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
  /// Remove sigusr signal handlers.
  Future<void> cleanupAfterSignal();

  /// Tear down the runner and leave the application running.
  ///
  /// This is not supported on web devices where the runner is running
  /// the application server as well.
  Future<void> detach();

  /// Tear down the runner and exit the application.
  Future<void> exit();

  /// Run any source generators, such as localizations.
  ///
  /// These are automatically run during hot restart, but can be
  /// triggered manually to see the updated generated code.
  Future<void> runSourceGenerators();
1036 1037
}

1038
// Shared code between different resident application runners.
1039
abstract class ResidentRunner extends ResidentHandlers {
1040 1041
  ResidentRunner(
    this.flutterDevices, {
1042
    @required this.target,
1043
    @required this.debuggingOptions,
1044
    String projectRootPath,
1045
    this.ipv6,
1046 1047
    this.stayResident = true,
    this.hotMode = true,
1048
    String dillOutputPath,
1049
    this.machine = false,
1050
    ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
1051
  }) : mainPath = globals.fs.file(target).absolute.path,
1052
       packagesFilePath = debuggingOptions.buildInfo.packagesPath,
1053
       projectRootPath = projectRootPath ?? globals.fs.currentDirectory.path,
1054 1055
       _dillOutputPath = dillOutputPath,
       artifactDirectory = dillOutputPath == null
1056 1057
          ? globals.fs.systemTempDirectory.createTempSync('flutter_tool.')
          : globals.fs.file(dillOutputPath).parent,
1058 1059 1060 1061 1062
       assetBundle = AssetBundleFactory.instance.createBundle(),
       commandHelp = CommandHelp(
         logger: globals.logger,
         terminal: globals.terminal,
         platform: globals.platform,
1063
         outputPreferences: globals.outputPreferences,
1064
       ) {
1065 1066
    if (!artifactDirectory.existsSync()) {
      artifactDirectory.createSync(recursive: true);
1067
    }
1068
    _residentDevtoolsHandler = devtoolsHandler(DevtoolsLauncher.instance, this, globals.logger);
1069
  }
1070

1071 1072 1073 1074 1075 1076 1077
  @override
  Logger get logger => globals.logger;

  @override
  FileSystem get fileSystem => globals.fs;

  @override
1078
  final List<FlutterDevice> flutterDevices;
1079

1080 1081
  final String target;
  final DebuggingOptions debuggingOptions;
1082 1083

  @override
1084
  final bool stayResident;
1085
  final bool ipv6;
1086 1087 1088
  final String _dillOutputPath;
  /// The parent location of the incremental artifacts.
  final Directory artifactDirectory;
1089 1090 1091 1092 1093
  final String packagesFilePath;
  final String projectRootPath;
  final String mainPath;
  final AssetBundle assetBundle;

1094
  final CommandHelp commandHelp;
1095
  final bool machine;
1096

1097
  @override
1098 1099
  ResidentDevtoolsHandler get residentDevtoolsHandler => _residentDevtoolsHandler;
  ResidentDevtoolsHandler _residentDevtoolsHandler;
1100

1101
  bool _exited = false;
1102
  Completer<int> _finished = Completer<int>();
1103 1104 1105 1106
  BuildResult _lastBuild;
  Environment _environment;

  @override
1107 1108 1109 1110 1111 1112 1113 1114
  bool hotMode;

  /// Returns true if every device is streaming observatory URIs.
  bool get isWaitingForObservatory {
    return flutterDevices.every((FlutterDevice device) {
      return device.isWaitingForObservatory;
    });
  }
1115

1116
  String get dillOutputPath => _dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill');
1117 1118 1119 1120 1121
  String getReloadPath({
    bool fullRestart = false,
    @required bool swap,
  }) {
    if (!fullRestart) {
1122
      return 'main.dart.incremental.dill';
1123
    }
1124
    return 'main.dart${swap ? '.swap' : ''}.dill';
1125
  }
1126

1127
  bool get debuggingEnabled => debuggingOptions.debuggingEnabled;
1128 1129

  @override
1130
  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
1131 1132

  @override
1133
  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
1134 1135

  @override
1136
  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
1137 1138

  @override
1139
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
1140 1141

  @override
1142
  bool get supportsWriteSkSL => supportsServiceProtocol;
1143

1144
  bool get trackWidgetCreation => debuggingOptions.buildInfo.trackWidgetCreation;
1145

1146 1147 1148 1149
  /// True if the shared Dart plugin registry (which is different than the one
  /// used for web) should be generated during source generation.
  bool get generateDartPluginRegistry => true;

1150 1151 1152 1153 1154 1155 1156
  // Returns the Uri of the first connected device for mobile,
  // and only connected device for web.
  //
  // Would be null if there is no device connected or
  // there is no devFS associated with the first device.
  Uri get uri => flutterDevices.first?.devFS?.baseUri;

1157 1158 1159
  /// Returns [true] if the resident runner exited after invoking [exit()].
  bool get exited => _exited;

1160 1161 1162
  @override
  bool get supportsRestart {
    return isRunningDebug && flutterDevices.every((FlutterDevice device) {
1163 1164 1165 1166
      return device.device.supportsHotRestart;
    });
  }

1167
  @override
1168 1169
  bool get canHotReload => hotMode;

1170
  /// Start the app and keep the process running during its lifetime.
1171 1172 1173
  ///
  /// Returns the exit code that we should use for the flutter tool process; 0
  /// for success, 1 for user error (e.g. bad arguments), 2 for other failures.
1174
  Future<int> run({
1175
    Completer<DebugConnectionInfo> connectionInfoCompleter,
1176
    Completer<void> appStartedCompleter,
1177
    bool enableDevTools = false,
1178 1179
    String route,
  });
1180

1181 1182 1183
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
1184
    bool allowExistingDdsInstance = false,
1185
    bool enableDevTools = false,
1186 1187
  });

1188
  @override
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198
  Future<void> runSourceGenerators() async {
    _environment ??= Environment(
      artifacts: globals.artifacts,
      logger: globals.logger,
      cacheDir: globals.cache.getRoot(),
      engineVersion: globals.flutterVersion.engineRevision,
      fileSystem: globals.fs,
      flutterRootDir: globals.fs.directory(Cache.flutterRoot),
      outputDir: globals.fs.directory(getBuildDirectory()),
      processManager: globals.processManager,
1199
      platform: globals.platform,
1200
      projectDir: globals.fs.currentDirectory,
1201
      generateDartPluginRegistry: generateDartPluginRegistry,
1202 1203 1204 1205
      defines: <String, String>{
        // Needed for Dart plugin registry generation.
        kTargetFile: mainPath,
      },
1206
    );
1207 1208

    final CompositeTarget compositeTarget = CompositeTarget(<Target>[
1209
      const GenerateLocalizationsTarget(),
1210 1211 1212 1213 1214
      const DartPluginRegistrantTarget(),
    ]);

    _lastBuild = await globals.buildSystem.buildIncremental(
      compositeTarget,
1215 1216 1217 1218 1219
      _environment,
      _lastBuild,
    );
    if (!_lastBuild.success) {
      for (final ExceptionMeasurement exceptionMeasurement in _lastBuild.exceptions.values) {
1220
        globals.printError(
1221 1222 1223 1224 1225 1226 1227
          exceptionMeasurement.exception.toString(),
          stackTrace: globals.logger.isVerbose
            ? exceptionMeasurement.stackTrace
            : null,
        );
      }
    }
1228
    globals.printTrace('complete');
1229 1230
  }

1231
  @protected
1232
  void writeVmServiceFile() {
1233 1234
    if (debuggingOptions.vmserviceOutFile != null) {
      try {
1235
        final String address = flutterDevices.first.vmService.wsAddress.toString();
1236
        final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile);
1237 1238 1239
        vmserviceOutFile.createSync(recursive: true);
        vmserviceOutFile.writeAsStringSync(address);
      } on FileSystemException {
1240
        globals.printError('Failed to write vmservice-out-file at ${debuggingOptions.vmserviceOutFile}');
1241 1242 1243 1244
      }
    }
  }

1245
  @override
1246 1247
  Future<void> exit() async {
    _exited = true;
1248
    await residentDevtoolsHandler.shutdown();
1249
    await stopEchoingDeviceLog();
1250
    await preExit();
1251
    await exitApp(); // calls appFinished
1252
    await shutdownDartDevelopmentService();
1253 1254
  }

1255
  @override
1256
  Future<void> detach() async {
1257
    await residentDevtoolsHandler.shutdown();
1258
    await stopEchoingDeviceLog();
1259
    await preExit();
1260
    await shutdownDartDevelopmentService();
1261 1262 1263
    appFinished();
  }

1264 1265 1266
  Future<void> stopEchoingDeviceLog() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
1267
    );
1268 1269
  }

1270 1271 1272 1273 1274 1275 1276 1277
  Future<void> shutdownDartDevelopmentService() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>(
        (FlutterDevice device) => device.device?.dds?.shutdown()
      ).where((Future<void> element) => element != null)
    );
  }

1278 1279 1280 1281 1282
  @protected
  void cacheInitialDillCompilation() {
    if (_dillOutputPath != null) {
      return;
    }
1283
    globals.printTrace('Caching compiled dill');
1284 1285 1286 1287
    final File outputDill = globals.fs.file(dillOutputPath);
    if (outputDill.existsSync()) {
      final String copyPath = getDefaultCachedKernelPath(
        trackWidgetCreation: trackWidgetCreation,
1288
        dartDefines: debuggingOptions.buildInfo.dartDefines,
1289
        extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions,
1290
      );
1291 1292 1293 1294
      globals.fs
          .file(copyPath)
          .parent
          .createSync(recursive: true);
1295 1296 1297 1298
      outputDill.copySync(copyPath);
    }
  }

1299
  void printStructuredErrorLog(vm_service.Event event) {
1300
    if (event.extensionKind == 'Flutter.Error' && !machine) {
1301 1302 1303 1304 1305 1306 1307
      final Map<dynamic, dynamic> json = event.extensionData?.data;
      if (json != null && json.containsKey('renderedErrorText')) {
        globals.printStatus('\n${json['renderedErrorText']}');
      }
    }
  }

1308
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
1309 1310 1311 1312 1313
  /// will be registered.
  //
  // Failures should be indicated by completing the future with an error, using
  // a string as the error object, which will be used by the caller (attach())
  // to display an error message.
1314 1315 1316 1317
  Future<void> connectToServiceProtocol({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
1318
    GetSkSLMethod getSkSLMethod,
1319
    @required bool allowExistingDdsInstance,
1320
  }) async {
1321
    if (!debuggingOptions.debuggingEnabled) {
1322
      throw Exception('The service protocol is not enabled.');
1323
    }
1324
    _finished = Completer<int>();
1325
    // Listen for service protocol connection to close.
1326
    for (final FlutterDevice device in flutterDevices) {
1327
      await device.connect(
1328 1329 1330
        reloadSources: reloadSources,
        restart: restart,
        compileExpression: compileExpression,
1331
        enableDds: debuggingOptions.enableDds,
1332
        ddsPort: debuggingOptions.ddsPort,
1333
        allowExistingDdsInstance: allowExistingDdsInstance,
1334
        hostVmServicePort: debuggingOptions.hostVmServicePort,
1335 1336
        getSkSLMethod: getSkSLMethod,
        printStructuredErrorLogMethod: printStructuredErrorLog,
1337
        ipv6: ipv6,
1338 1339
        disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes,
        cacheStartupProfile: debuggingOptions.cacheStartupProfile,
1340
      );
1341 1342
      await device.vmService.getFlutterViews();

1343 1344 1345
      // This hooks up callbacks for when the connection stops in the future.
      // We don't want to wait for them. We don't handle errors in those callbacks'
      // futures either because they just print to logger and is not critical.
1346
      unawaited(device.vmService.service.onDone.then<void>(
1347 1348 1349
        _serviceProtocolDone,
        onError: _serviceProtocolError,
      ).whenComplete(_serviceDisconnected));
1350
    }
1351 1352
  }

1353
  Future<void> _serviceProtocolDone(dynamic object) async {
1354
    globals.printTrace('Service protocol connection closed.');
1355 1356
  }

1357
  Future<void> _serviceProtocolError(dynamic error, StackTrace stack) {
1358
    globals.printTrace('Service protocol connection closed with an error: $error\n$stack');
1359
    return Future<void>.error(error, stack);
1360 1361
  }

1362
  void _serviceDisconnected() {
1363
    if (_exited) {
1364 1365 1366
      // User requested the application exit.
      return;
    }
1367
    if (_finished.isCompleted) {
1368
      return;
1369
    }
1370
    globals.printStatus('Lost connection to device.');
1371 1372 1373
    _finished.complete(0);
  }

1374
  void appFinished() {
1375
    if (_finished.isCompleted) {
1376
      return;
1377
    }
1378
    globals.printStatus('Application finished.');
1379 1380 1381
    _finished.complete(0);
  }

1382 1383 1384 1385 1386 1387
  void appFailedToStart() {
    if (!_finished.isCompleted) {
      _finished.complete(1);
    }
  }

1388
  Future<int> waitForAppToFinish() async {
1389
    final int exitCode = await _finished.future;
1390
    assert(exitCode != null);
1391 1392 1393 1394
    await cleanupAtFinish();
    return exitCode;
  }

1395 1396
  @mustCallSuper
  Future<void> preExit() async {
1397 1398
    // If _dillOutputPath is null, the tool created a temporary directory for
    // the dill.
1399 1400 1401 1402
    if (_dillOutputPath == null && artifactDirectory.existsSync()) {
      artifactDirectory.deleteSync(recursive: true);
    }
  }
1403

1404
  Future<void> exitApp() async {
1405
    final List<Future<void>> futures = <Future<void>>[
1406
      for (final FlutterDevice device in flutterDevices) device.exitApps(),
1407
    ];
1408
    await Future.wait(futures);
1409 1410 1411
    appFinished();
  }

1412
  bool get reportedDebuggers => _reportedDebuggers;
1413 1414 1415
  bool _reportedDebuggers = false;

  void printDebuggerList({ bool includeObservatory = true, bool includeDevtools = true }) {
1416
    final DevToolsServerAddress devToolsServerAddress = residentDevtoolsHandler.activeDevToolsServer;
1417
    if (!residentDevtoolsHandler.readyToAnnounce) {
1418 1419
      includeDevtools = false;
    }
1420
    assert(!includeDevtools || devToolsServerAddress != null);
1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438
    for (final FlutterDevice device in flutterDevices) {
      if (device.vmService == null) {
        continue;
      }
      if (includeObservatory) {
        // Caution: This log line is parsed by device lab tests.
        globals.printStatus(
          'An Observatory debugger and profiler on ${device.device.name} is available at: '
          '${device.vmService.httpAddress}',
        );
      }
      if (includeDevtools) {
        final Uri uri = devToolsServerAddress.uri?.replace(
          queryParameters: <String, dynamic>{'uri': '${device.vmService.httpAddress}'},
        );
        if (uri != null) {
          globals.printStatus(
            'The Flutter DevTools debugger and profiler '
1439
            'on ${device.device.name} is available at: ${urlToDisplayString(uri)}',
1440 1441 1442 1443 1444 1445 1446
          );
        }
      }
    }
    _reportedDebuggers = true;
  }

1447
  void printHelpDetails() {
1448
    commandHelp.v.print();
1449
    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) {
1450
      commandHelp.s.print();
1451
    }
1452
    if (supportsServiceProtocol) {
1453 1454
      commandHelp.w.print();
      commandHelp.t.print();
1455
      if (isRunningDebug) {
1456 1457 1458 1459 1460
        commandHelp.L.print();
        commandHelp.S.print();
        commandHelp.U.print();
        commandHelp.i.print();
        commandHelp.p.print();
1461
        commandHelp.I.print();
1462
        commandHelp.o.print();
1463
        commandHelp.b.print();
1464
      } else {
1465 1466
        commandHelp.S.print();
        commandHelp.U.print();
1467
      }
1468 1469 1470
      // Performance related features: `P` should precede `a`, which should precede `M`.
      commandHelp.P.print();
      commandHelp.a.print();
1471 1472 1473
      if (supportsWriteSkSL) {
        commandHelp.M.print();
      }
1474 1475 1476
      if (isRunningDebug) {
        commandHelp.g.print();
      }
1477
      commandHelp.j.print();
1478
    }
1479 1480
  }

1481
  @override
1482
  Future<void> cleanupAfterSignal();
1483

1484
  /// Called right before we exit.
1485
  Future<void> cleanupAtFinish();
1486
}
1487

Devon Carew's avatar
Devon Carew committed
1488
class OperationResult {
1489
  OperationResult(this.code, this.message, { this.fatal = false, this.updateFSReport });
Devon Carew's avatar
Devon Carew committed
1490

1491
  /// The result of the operation; a non-zero code indicates a failure.
Devon Carew's avatar
Devon Carew committed
1492
  final int code;
1493 1494

  /// A user facing message about the results of the operation.
Devon Carew's avatar
Devon Carew committed
1495
  final String message;
1496

1497 1498 1499
  /// Whether this error should cause the runner to exit.
  final bool fatal;

1500 1501
  final UpdateFSReport updateFSReport;

Devon Carew's avatar
Devon Carew committed
1502
  bool get isOk => code == 0;
1503

1504
  static final OperationResult ok = OperationResult(0, '');
Devon Carew's avatar
Devon Carew committed
1505 1506
}

1507
Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async {
1508 1509
  switch (platform) {
    case TargetPlatform.android_arm:
1510
    case TargetPlatform.android_arm64:
1511
    case TargetPlatform.android_x64:
1512
    case TargetPlatform.android_x86:
1513
      final FlutterProject project = FlutterProject.current();
1514
      final String manifestPath = globals.fs.path.relative(project.android.appManifestFile.path);
1515
      return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
1516 1517
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
1518 1519 1520 1521 1522 1523 1524 1525 1526
    case TargetPlatform.android:
    case TargetPlatform.darwin:
    case TargetPlatform.fuchsia_arm64:
    case TargetPlatform.fuchsia_x64:
    case TargetPlatform.linux_arm64:
    case TargetPlatform.linux_x64:
    case TargetPlatform.tester:
    case TargetPlatform.web_javascript:
    case TargetPlatform.windows_x64:
1527 1528
      return null;
  }
1529
  return null; // dead code, remove after null safety migration
1530
}
1531

1532 1533
/// Redirects terminal commands to the correct resident runner methods.
class TerminalHandler {
1534 1535 1536 1537
  TerminalHandler(this.residentRunner, {
    @required Logger logger,
    @required Terminal terminal,
    @required Signals signals,
1538 1539 1540
    @required io.ProcessInfo processInfo,
    @required bool reportReady,
    String pidFile,
1541 1542
  }) : _logger = logger,
       _terminal = terminal,
1543 1544 1545 1546
       _signals = signals,
       _processInfo = processInfo,
       _reportReady = reportReady,
       _pidFile = pidFile;
1547 1548 1549 1550

  final Logger _logger;
  final Terminal _terminal;
  final Signals _signals;
1551 1552 1553
  final io.ProcessInfo _processInfo;
  final bool _reportReady;
  final String _pidFile;
1554

1555
  final ResidentHandlers residentRunner;
1556 1557
  bool _processingUserRequest = false;
  StreamSubscription<void> subscription;
1558
  File _actualPidFile;
1559

1560 1561 1562
  @visibleForTesting
  String lastReceivedCommand;

1563 1564 1565 1566
  /// This is only a buffer logger in unit tests
  @visibleForTesting
  BufferLogger get logger => _logger as BufferLogger;

1567
  void setupTerminal() {
1568 1569
    if (!_logger.quiet) {
      _logger.printStatus('');
1570 1571
      residentRunner.printHelp(details: false);
    }
1572 1573
    _terminal.singleCharMode = true;
    subscription = _terminal.keystrokes.listen(processTerminalInput);
1574 1575
  }

1576 1577 1578
  final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{};

  void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) {
1579
    _signalTokens[signal] = _signals.addHandler(signal, handler);
1580 1581
  }

1582 1583
  void registerSignalHandlers() {
    assert(residentRunner.stayResident);
1584 1585
    _addSignalHandler(io.ProcessSignal.sigint, _cleanUp);
    _addSignalHandler(io.ProcessSignal.sigterm, _cleanUp);
1586
    if (residentRunner.supportsServiceProtocol && residentRunner.supportsRestart) {
1587 1588
      _addSignalHandler(io.ProcessSignal.sigusr1, _handleSignal);
      _addSignalHandler(io.ProcessSignal.sigusr2, _handleSignal);
1589 1590 1591 1592
      if (_pidFile != null) {
        _logger.printTrace('Writing pid to: $_pidFile');
        _actualPidFile = _processInfo.writePidFile(_pidFile);
      }
1593
    }
1594 1595 1596 1597 1598
  }

  /// Unregisters terminal signal and keystroke handlers.
  void stop() {
    assert(residentRunner.stayResident);
1599 1600 1601 1602 1603
    if (_actualPidFile != null) {
      try {
        _logger.printTrace('Deleting pid file (${_actualPidFile.path}).');
        _actualPidFile.deleteSync();
      } on FileSystemException catch (error) {
1604
        _logger.printWarning('Failed to delete pid file (${_actualPidFile.path}): ${error.message}');
1605 1606 1607
      }
      _actualPidFile = null;
    }
1608
    for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
1609
      _signals.removeHandler(entry.key, entry.value);
1610 1611 1612
    }
    _signalTokens.clear();
    subscription.cancel();
1613 1614 1615 1616
  }

  /// Returns [true] if the input has been handled by this function.
  Future<bool> _commonTerminalInputHandler(String character) async {
1617 1618
    _logger.printStatus(''); // the key the user tapped might be on this line
    switch (character) {
1619
      case 'a':
1620
        return residentRunner.debugToggleProfileWidgetBuilds();
1621
      case 'b':
1622
        return residentRunner.debugToggleBrightness();
1623
      case 'c':
1624
        _logger.clear();
1625
        return true;
1626 1627 1628 1629
      case 'd':
      case 'D':
        await residentRunner.detach();
        return true;
1630 1631 1632
      case 'g':
        await residentRunner.runSourceGenerators();
        return true;
1633 1634 1635 1636 1637 1638 1639
      case 'h':
      case 'H':
      case '?':
        // help
        residentRunner.printHelp(details: true);
        return true;
      case 'i':
1640
        return residentRunner.debugToggleWidgetInspector();
1641
      case 'I':
1642
        return residentRunner.debugToggleInvertOversizedImages();
1643 1644 1645
      case 'j':
      case 'J':
        return residentRunner.debugFrameJankMetrics();
1646
      case 'L':
1647
        return residentRunner.debugDumpLayerTree();
1648 1649
      case 'o':
      case 'O':
1650
        return residentRunner.debugTogglePlatform();
1651 1652 1653 1654 1655 1656
      case 'M':
        if (residentRunner.supportsWriteSkSL) {
          await residentRunner.writeSkSL();
          return true;
        }
        return false;
1657
      case 'p':
1658
        return residentRunner.debugToggleDebugPaintSizeEnabled();
1659
      case 'P':
1660
        return residentRunner.debugTogglePerformanceOverlayOverride();
1661 1662 1663 1664 1665
      case 'q':
      case 'Q':
        // exit
        await residentRunner.exit();
        return true;
1666 1667 1668 1669
      case 'r':
        if (!residentRunner.canHotReload) {
          return false;
        }
1670
        final OperationResult result = await residentRunner.restart();
1671 1672 1673
        if (result.fatal) {
          throwToolExit(result.message);
        }
1674
        if (!result.isOk) {
1675
          _logger.printStatus('Try again after fixing the above error(s).', emphasis: true);
1676 1677 1678 1679
        }
        return true;
      case 'R':
        // If hot restart is not supported for all devices, ignore the command.
1680
        if (!residentRunner.supportsRestart || !residentRunner.hotMode) {
1681 1682 1683
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: true);
1684 1685 1686
        if (result.fatal) {
          throwToolExit(result.message);
        }
1687
        if (!result.isOk) {
1688
          _logger.printStatus('Try again after fixing the above error(s).', emphasis: true);
1689 1690
        }
        return true;
1691 1692
      case 's':
        for (final FlutterDevice device in residentRunner.flutterDevices) {
1693
          await residentRunner.screenshot(device);
1694 1695
        }
        return true;
1696
      case 'S':
1697
        return residentRunner.debugDumpSemanticsTreeInTraversalOrder();
1698 1699
      case 't':
      case 'T':
1700
        return residentRunner.debugDumpRenderTree();
1701
      case 'U':
1702
        return residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
1703 1704 1705
      case 'v':
      case 'V':
        return residentRunner.residentDevtoolsHandler.launchDevToolsInBrowser(flutterDevices: residentRunner.flutterDevices);
1706 1707
      case 'w':
      case 'W':
1708
        return residentRunner.debugDumpApp();
1709 1710 1711 1712 1713 1714 1715 1716
    }
    return false;
  }

  Future<void> processTerminalInput(String command) async {
    // When terminal doesn't support line mode, '\n' can sneak into the input.
    command = command.trim();
    if (_processingUserRequest) {
1717
      _logger.printTrace('Ignoring terminal input: "$command" because we are busy.');
1718 1719 1720 1721
      return;
    }
    _processingUserRequest = true;
    try {
1722 1723
      lastReceivedCommand = command;
      await _commonTerminalInputHandler(command);
1724 1725
    // Catch all exception since this is doing cleanup and rethrowing.
    } catch (error, st) { // ignore: avoid_catches_without_on_clauses
1726 1727
      // Don't print stack traces for known error types.
      if (error is! ToolExit) {
1728
        _logger.printError('$error\n$st');
1729
      }
1730 1731
      await _cleanUp(null);
      rethrow;
1732 1733
    } finally {
      _processingUserRequest = false;
1734 1735 1736
      if (_reportReady) {
        _logger.printStatus('ready');
      }
1737 1738 1739 1740 1741
    }
  }

  Future<void> _handleSignal(io.ProcessSignal signal) async {
    if (_processingUserRequest) {
1742
      _logger.printTrace('Ignoring signal: "$signal" because we are busy.');
1743 1744 1745 1746
      return;
    }
    _processingUserRequest = true;

1747
    final bool fullRestart = signal == io.ProcessSignal.sigusr2;
1748 1749 1750 1751 1752 1753 1754 1755

    try {
      await residentRunner.restart(fullRestart: fullRestart);
    } finally {
      _processingUserRequest = false;
    }
  }

1756
  Future<void> _cleanUp(io.ProcessSignal signal) async {
1757
    _terminal.singleCharMode = false;
1758
    await subscription?.cancel();
1759 1760 1761 1762
    await residentRunner.cleanupAfterSignal();
  }
}

1763
class DebugConnectionInfo {
1764
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
1765

1766 1767
  final Uri httpUri;
  final Uri wsUri;
1768 1769
  final String baseUri;
}
1770 1771 1772 1773

/// Returns the next platform value for the switcher.
///
/// These values must match what is available in
1774
/// `packages/flutter/lib/src/foundation/binding.dart`.
1775
String nextPlatform(String currentPlatform) {
1776 1777 1778 1779 1780 1781
  switch (currentPlatform) {
    case 'android':
      return 'iOS';
    case 'iOS':
      return 'fuchsia';
    case 'fuchsia':
1782
      return 'macOS';
1783 1784 1785 1786 1787 1788 1789
    case 'macOS':
      return 'android';
    default:
      assert(false); // Invalid current platform.
      return 'android';
  }
}
1790

1791 1792
/// A launcher for the devtools debugger and analysis tool.
abstract class DevtoolsLauncher {
1793 1794 1795 1796 1797 1798 1799
  static DevtoolsLauncher get instance => context.get<DevtoolsLauncher>();

  /// Serve Dart DevTools and return the host and port they are available on.
  ///
  /// This method must return a future that is guaranteed not to fail, because it
  /// will be used in unawaited contexts. It may, however, return null.
  Future<DevToolsServerAddress> serve();
1800

1801 1802
  /// Launch a Dart DevTools process, optionally targeting a specific VM Service
  /// URI if [vmServiceUri] is non-null.
1803
  ///
1804 1805 1806
  /// [additionalArguments] may be optionally specified and are passed directly
  /// to the devtools run command.
  ///
1807 1808
  /// This method must return a future that is guaranteed not to fail, because it
  /// will be used in unawaited contexts.
1809
  Future<void> launch(Uri vmServiceUri, {List<String> additionalArguments});
1810

1811
  Future<void> close();
1812

1813
  /// When measuring devtools memory via additional arguments, the launch process
1814 1815 1816 1817 1818
  /// will technically never complete.
  ///
  /// Us this as an indicator that the process has started.
  Future<void> processStart;

1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836
  /// Returns a future that completes when the DevTools server is ready.
  ///
  /// Completes when [devToolsUrl] is set. That can be set either directly, or
  /// by calling [serve].
  Future<void> get ready => _readyCompleter.future;
  Completer<void> _readyCompleter = Completer<void>();

  Uri get devToolsUrl => _devToolsUrl;
  Uri _devToolsUrl;
  set devToolsUrl(Uri value) {
    assert((_devToolsUrl == null) != (value == null));
    _devToolsUrl = value;
    if (_devToolsUrl != null) {
      _readyCompleter.complete();
    } else {
      _readyCompleter = Completer<void>();
    }
  }
1837

1838 1839 1840
  /// The URL of the current DevTools server.
  ///
  /// Returns null if [ready] is not complete.
1841
  DevToolsServerAddress get activeDevToolsServer {
1842
    if (_devToolsUrl == null) {
1843 1844
      return null;
    }
1845
    return DevToolsServerAddress(devToolsUrl.host, devToolsUrl.port);
1846
  }
1847
}
1848 1849 1850 1851 1852 1853

class DevToolsServerAddress {
  DevToolsServerAddress(this.host, this.port);

  final String host;
  final int port;
1854 1855 1856 1857 1858 1859 1860

  Uri get uri {
    if (host == null || port == null) {
      return null;
    }
    return Uri(scheme: 'http', host: host, port: port);
  }
1861
}