resident_runner.dart 58.9 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_null_migrated.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(jonahwilliams): 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
        targetModel: TargetModel.dartdevc,
130
        extraFrontEndOptions: extraFrontEndOptions,
131
        platformDill: globals.fs.file(globals.artifacts
132
          .getHostArtifact(platformDillArtifact))
133
          .absolute.uri.toString(),
134
        dartDefines: buildInfo.dartDefines,
135
        librariesSpec: globals.fs.file(globals.artifacts
136
          .getHostArtifact(HostArtifact.flutterWebLibrariesJson)).uri.toString(),
137
        packagesPath: buildInfo.packagesPath,
138 139 140
        artifacts: globals.artifacts,
        processManager: globals.processManager,
        logger: globals.logger,
141
        fileSystem: globals.fs,
142
        platform: platform,
143
      );
144
    } else {
145 146
      // The flutter-widget-cache feature only applies to run mode.
      List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions;
147 148 149
      extraFrontEndOptions = <String>[
        if (featureFlags.isSingleWidgetReloadEnabled)
         '--flutter-widget-cache',
150
        '--enable-experiment=alternative-invalidation-strategy',
151 152
        ...?extraFrontEndOptions,
      ];
153
      generator = ResidentCompiler(
154
        globals.artifacts.getArtifactPath(
155 156
          Artifact.flutterPatchedSdkPath,
          platform: targetPlatform,
157
          mode: buildInfo.mode,
158
        ),
159 160
        buildMode: buildInfo.mode,
        trackWidgetCreation: buildInfo.trackWidgetCreation,
161 162
        fileSystemRoots: buildInfo.fileSystemRoots,
        fileSystemScheme: buildInfo.fileSystemScheme,
163
        targetModel: targetModel,
164
        dartDefines: buildInfo.dartDefines,
165
        extraFrontEndOptions: extraFrontEndOptions,
166
        initializeFromDill: buildInfo.initializeFromDill ?? getDefaultCachedKernelPath(
167
          trackWidgetCreation: buildInfo.trackWidgetCreation,
168
          dartDefines: buildInfo.dartDefines,
169
          extraFrontEndOptions: extraFrontEndOptions,
170
        ),
171
        packagesPath: buildInfo.packagesPath,
172 173 174
        artifacts: globals.artifacts,
        processManager: globals.processManager,
        logger: globals.logger,
175
        platform: platform,
176
        fileSystem: globals.fs,
177 178
      );
    }
179

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

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

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

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

207 208 209 210 211
  /// 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).
212 213
  /// The 'compileExpression' service can be used to compile user-provided
  /// expressions requested during debugging of the application.
214 215
  /// This ensures that the reload process follows the normal orchestration of
  /// the Flutter Tools and not just the VM internal service.
216
  Future<void> connect({
217 218 219
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
220
    GetSkSLMethod getSkSLMethod,
221
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
222 223 224
    int hostVmServicePort,
    int ddsPort,
    bool disableServiceAuthCodes = false,
225
    bool enableDds = true,
226
    @required bool allowExistingDdsInstance,
227
    bool ipv6 = false,
228 229 230 231 232 233 234
  }) {
    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.
235
      globals.printTrace('Connecting to service protocol: $observatoryUri');
236
      isWaitingForVm = true;
237
      bool existingDds = false;
238
      FlutterVmService service;
239
      if (enableDds) {
240
        void handleError(Exception e, StackTrace st) {
241 242
          globals.printTrace('Fail to connect to service protocol: $observatoryUri: $e');
          if (!completer.isCompleted) {
243
            completer.completeError('failed to connect to $observatoryUri', st);
244 245
          }
        }
246 247 248 249
        // 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 {
250
          service = await connectToVmService(observatoryUri, logger: globals.logger);
251
          await service.dispose();
252 253 254 255 256 257 258 259
        } 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;
        }

260 261 262 263 264 265
        // 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,
266 267 268
            hostPort: ddsPort,
            ipv6: ipv6,
            disableServiceAuthCodes: disableServiceAuthCodes,
269
            logger: globals.logger,
270
          );
271
        } on dds.DartDevelopmentServiceException catch (e, st) {
272 273
          if (!allowExistingDdsInstance ||
              (e.errorCode != dds.DartDevelopmentServiceException.existingDdsInstanceError)) {
274
            handleError(e, st);
275 276 277
            return;
          } else {
            existingDds = true;
278
          }
279 280
        } on ToolExit {
          rethrow;
281 282
        } on Exception catch (e, st) {
          handleError(e, st);
283 284
          return;
        }
285
      }
286 287 288 289
      // 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.
290
      try {
291 292 293
        service = await Future.any<dynamic>(
          <Future<dynamic>>[
            connectToVmService(
294
              enableDds ? device.dds.uri : observatoryUri,
295 296 297 298 299 300
              reloadSources: reloadSources,
              restart: restart,
              compileExpression: compileExpression,
              getSkSLMethod: getSkSLMethod,
              printStructuredErrorLogMethod: printStructuredErrorLogMethod,
              device: device,
301
              logger: globals.logger,
302
            ),
303 304
            if (!existingDds)
              device.dds.done.whenComplete(() => throw Exception('DDS shut down too early')),
305
          ]
306
        ) as FlutterVmService;
307
      } on Exception catch (exception) {
308
        globals.printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
309 310 311 312 313 314 315 316
        if (!completer.isCompleted && !_isListeningForObservatoryUri) {
          completer.completeError('failed to connect to $observatoryUri');
        }
        return;
      }
      if (completer.isCompleted) {
        return;
      }
317
      globals.printTrace('Successfully connected to service protocol: $observatoryUri');
318

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

335 336 337
  Future<void> exitApps({
    @visibleForTesting Duration timeoutDelay = const Duration(seconds: 10),
  }) async {
338 339 340 341
    // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/83127
    // 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);
342 343
  }

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

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

376
  Future<void> stopEchoingDeviceLog() async {
377
    if (_loggingSubscription == null) {
378
      return;
379
    }
380 381 382 383
    await _loggingSubscription.cancel();
    _loggingSubscription = null;
  }

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

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

    final TargetPlatform targetPlatform = await device.targetPlatform;
402
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
403
      targetPlatform,
404
      buildInfo: hotRunner.debuggingOptions.buildInfo,
405
      applicationBinary: hotRunner.applicationBinary,
406 407 408 409
    );

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

    final Map<String, dynamic> platformArgs = <String, dynamic>{};

421
    await startEchoingDeviceLog();
422 423 424 425 426 427 428 429 430

    // Start the application.
    final Future<LaunchResult> futureResult = device.startApp(
      package,
      mainPath: hotRunner.mainPath,
      debuggingOptions: hotRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
431
      ipv6: hotRunner.ipv6,
432
      userIdentifier: userIdentifier,
433 434 435 436 437
    );

    final LaunchResult result = await futureResult;

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

  Future<int> runCold({
    ColdRunner coldRunner,
    String route,
  }) async {
    final TargetPlatform targetPlatform = await device.targetPlatform;
459
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
460
      targetPlatform,
461
      buildInfo: coldRunner.debuggingOptions.buildInfo,
462
      applicationBinary: coldRunner.applicationBinary,
463
    );
464
    devFSWriter = device.createDevFSWriter(package, userIdentifier);
465

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

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
483
      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
484
      if (hint != null) {
485
        message += '\n$hint';
486
      }
487
      globals.printError(message);
488 489 490
      return 1;
    }

491
    final Map<String, dynamic> platformArgs = <String, dynamic>{};
492
    if (coldRunner.traceStartup != null) {
493
      platformArgs['trace-startup'] = coldRunner.traceStartup;
494
    }
495

496
    await startEchoingDeviceLog();
497 498 499 500 501 502 503 504

    final LaunchResult result = await device.startApp(
      package,
      mainPath: coldRunner.mainPath,
      debuggingOptions: coldRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
505
      ipv6: coldRunner.ipv6,
506
      userIdentifier: userIdentifier,
507 508 509
    );

    if (!result.started) {
510
      globals.printError('Error running application on ${device.name}.');
511 512 513
      await stopEchoingDeviceLog();
      return 2;
    }
514
    if (result.hasObservatory) {
515 516 517
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
518
    } else {
519 520 521
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
522
    }
523 524 525
    return 0;
  }

526
  Future<UpdateFSReport> updateDevFS({
527
    Uri mainUri,
528
    String target,
529
    AssetBundle bundle,
530
    DateTime firstBuildTime,
531 532 533
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
534
    String projectRootPath,
535
    String pathToReload,
536
    @required String dillOutputPath,
537
    @required List<Uri> invalidatedFiles,
538
    @required PackageConfig packageConfig,
539
  }) async {
540
    final Status devFSStatus = globals.logger.startProgress(
541 542
      'Syncing files to device ${device.name}...',
    );
543
    UpdateFSReport report;
544
    try {
545 546 547 548 549 550 551 552 553 554 555 556 557 558
      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,
559
        devFSWriter: devFSWriter,
560
      );
561 562
    } on DevFSException {
      devFSStatus.cancel();
563
      return UpdateFSReport(success: false);
564
    }
565 566 567 568 569 570 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
    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;

615 616
  ResidentDevtoolsHandler get residentDevtoolsHandler;

617 618 619 620 621 622 623 624 625
  @protected
  Logger get logger;

  @protected
  FileSystem get fileSystem;

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

626
  /// Perform a hot reload or hot restart of all attached applications.
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
  ///
  /// 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';
    throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode';
  }

  /// Dump the application's current widget tree to the terminal.
  Future<bool> debugDumpApp() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
642 643
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
644
        final String data = await device.vmService.flutterDebugDumpApp(
645
          isolateId: view.uiIsolate.id,
646 647 648 649 650 651 652 653 654 655 656 657 658
        );
        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) {
659 660
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
661
        final String data = await device.vmService.flutterDebugDumpRenderTree(
662
          isolateId: view.uiIsolate.id,
663 664 665 666 667 668 669 670 671 672 673 674 675
        );
        logger.printStatus(data);
      }
    }
    return true;
  }

  /// 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) {
676 677
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
678
        final String data = await device.vmService.flutterDebugDumpLayerTree(
679
          isolateId: view.uiIsolate.id,
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
        );
        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) {
695 696
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
697
        final String data = await device.vmService.flutterDebugDumpSemanticsTreeInTraversalOrder(
698
          isolateId: view.uiIsolate.id,
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713
        );
        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) {
714 715
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
716
        final String data = await device.vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
717
          isolateId: view.uiIsolate.id,
718 719 720 721 722 723 724 725 726 727 728 729 730
        );
        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) {
731 732
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
733
        await device.vmService.flutterToggleDebugPaintSizeEnabled(
734
          isolateId: view.uiIsolate.id,
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
        );
      }
    }
    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;
      }
752 753
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
754
        await device.vmService.flutterTogglePerformanceOverlayOverride(
755
          isolateId: view.uiIsolate.id,
756 757 758 759 760 761 762 763 764 765 766 767
        );
      }
    }
    return true;
  }

  /// Toggle the widget inspector.
  Future<bool> debugToggleWidgetInspector() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
768 769
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
770
        await device.vmService.flutterToggleWidgetInspector(
771
          isolateId: view.uiIsolate.id,
772 773 774 775 776 777 778 779 780 781 782 783
        );
      }
    }
    return true;
  }

  /// Toggle the "invert images" debugging feature.
  Future<bool> debugToggleInvertOversizedImages() async {
    if (!supportsServiceProtocol || !isRunningDebug) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
784 785
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
786
        await device.vmService.flutterToggleInvertOversizedImages(
787
          isolateId: view.uiIsolate.id,
788 789 790 791 792 793 794 795 796 797 798 799
        );
      }
    }
    return true;
  }

  /// Toggle the "profile widget builds" debugging feature.
  Future<bool> debugToggleProfileWidgetBuilds() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
800 801
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
802
        await device.vmService.flutterToggleProfileWidgetBuilds(
803
          isolateId: view.uiIsolate.id,
804 805 806 807 808 809
        );
      }
    }
    return true;
  }

810
  /// Toggle the operating system brightness (light or dark).
811 812 813 814
  Future<bool> debugToggleBrightness() async {
    if (!supportsServiceProtocol) {
      return false;
    }
815
    final List<FlutterView> views = await flutterDevices.first.vmService.getFlutterViews();
816
    final Brightness current = await flutterDevices.first.vmService.flutterBrightnessOverride(
817
      isolateId: views.first.uiIsolate.id,
818 819 820 821 822 823 824 825
    );
    Brightness next;
    if (current == Brightness.light) {
      next = Brightness.dark;
    } else {
      next = Brightness.light;
    }
    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.flutterBrightnessOverride(
829
          isolateId: view.uiIsolate.id,
830 831 832 833 834 835 836 837 838 839 840 841 842
          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;
    }
843
    final List<FlutterView> views = await flutterDevices.first.vmService.getFlutterViews();
844 845
    final String from = await flutterDevices
      .first.vmService.flutterPlatformOverride(
846
        isolateId: views.first.uiIsolate.id,
847 848 849
      );
    final String to = nextPlatform(from);
    for (final FlutterDevice device in flutterDevices) {
850 851
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
852 853
        await device.vmService.flutterPlatformOverride(
          platform: to,
854
          isolateId: view.uiIsolate.id,
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883
        );
      }
    }
    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.
  ///
884 885 886 887 888 889 890 891
  /// 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.
892
  Future<void> screenshot(FlutterDevice device) async {
893 894 895
    if (!device.device.supportsScreenshot && !supportsServiceProtocol) {
      return;
    }
896 897 898 899 900 901 902 903
    final Status status = logger.startProgress(
      'Taking screenshot for ${device.device.name}...',
    );
    final File outputFile = getUniqueFile(
      fileSystem.currentDirectory,
      'flutter',
      'png',
    );
904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942

    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 {
943
    List<FlutterView> views = <FlutterView>[];
944
    if (supportsServiceProtocol) {
945
      views = await device.vmService.getFlutterViews();
946 947
    }

948 949
    Future<bool> setDebugBanner(bool value) async {
      try {
950
        for (final FlutterView view in views) {
951 952
          await device.vmService.flutterDebugAllowBanner(
            value,
953
            isolateId: view.uiIsolate.id,
954 955 956
          );
        }
        return true;
957
      } on vm_service.RPCError catch (error) {
958 959 960 961
        logger.printError('Error communicating with Flutter on the device: $error');
        return false;
      }
    }
962 963 964 965
    if (!await setDebugBanner(false)) {
      return false;
    }
    bool succeeded = true;
966
    try {
967 968 969 970
      await cb();
    } finally {
      if (!await setDebugBanner(true)) {
        succeeded = false;
971 972
      }
    }
973
    return succeeded;
974
  }
975

976

977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
  /// 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();
994 995
}

996
// Shared code between different resident application runners.
997
abstract class ResidentRunner extends ResidentHandlers {
998 999
  ResidentRunner(
    this.flutterDevices, {
1000
    @required this.target,
1001
    @required this.debuggingOptions,
1002
    String projectRootPath,
1003
    this.ipv6,
1004 1005
    this.stayResident = true,
    this.hotMode = true,
1006
    String dillOutputPath,
1007
    this.machine = false,
1008
    ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
1009
  }) : mainPath = globals.fs.file(target).absolute.path,
1010
       packagesFilePath = debuggingOptions.buildInfo.packagesPath,
1011
       projectRootPath = projectRootPath ?? globals.fs.currentDirectory.path,
1012 1013
       _dillOutputPath = dillOutputPath,
       artifactDirectory = dillOutputPath == null
1014 1015
          ? globals.fs.systemTempDirectory.createTempSync('flutter_tool.')
          : globals.fs.file(dillOutputPath).parent,
1016 1017 1018 1019 1020
       assetBundle = AssetBundleFactory.instance.createBundle(),
       commandHelp = CommandHelp(
         logger: globals.logger,
         terminal: globals.terminal,
         platform: globals.platform,
1021
         outputPreferences: globals.outputPreferences,
1022
       ) {
1023 1024
    if (!artifactDirectory.existsSync()) {
      artifactDirectory.createSync(recursive: true);
1025
    }
1026
    _residentDevtoolsHandler = devtoolsHandler(DevtoolsLauncher.instance, this, globals.logger);
1027
  }
1028

1029 1030 1031 1032 1033 1034 1035
  @override
  Logger get logger => globals.logger;

  @override
  FileSystem get fileSystem => globals.fs;

  @override
1036
  final List<FlutterDevice> flutterDevices;
1037

1038 1039
  final String target;
  final DebuggingOptions debuggingOptions;
1040 1041

  @override
1042
  final bool stayResident;
1043
  final bool ipv6;
1044 1045 1046
  final String _dillOutputPath;
  /// The parent location of the incremental artifacts.
  final Directory artifactDirectory;
1047 1048 1049 1050 1051
  final String packagesFilePath;
  final String projectRootPath;
  final String mainPath;
  final AssetBundle assetBundle;

1052
  final CommandHelp commandHelp;
1053
  final bool machine;
1054

1055
  @override
1056 1057
  ResidentDevtoolsHandler get residentDevtoolsHandler => _residentDevtoolsHandler;
  ResidentDevtoolsHandler _residentDevtoolsHandler;
1058

1059
  bool _exited = false;
1060
  Completer<int> _finished = Completer<int>();
1061 1062 1063 1064
  BuildResult _lastBuild;
  Environment _environment;

  @override
1065 1066 1067 1068 1069 1070 1071 1072
  bool hotMode;

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

1074
  String get dillOutputPath => _dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill');
1075 1076 1077 1078 1079
  String getReloadPath({
    bool fullRestart = false,
    @required bool swap,
  }) {
    if (!fullRestart) {
1080
      return 'main.dart.incremental.dill';
1081
    }
1082
    return 'main.dart${swap ? '.swap' : ''}.dill';
1083
  }
1084

1085
  bool get debuggingEnabled => debuggingOptions.debuggingEnabled;
1086 1087

  @override
1088
  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
1089 1090

  @override
1091
  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
1092 1093

  @override
1094
  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
1095 1096

  @override
1097
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
1098 1099

  @override
1100
  bool get supportsWriteSkSL => supportsServiceProtocol;
1101

1102
  bool get trackWidgetCreation => debuggingOptions.buildInfo.trackWidgetCreation;
1103

1104 1105 1106 1107 1108 1109 1110
  // 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;

1111 1112 1113
  /// Returns [true] if the resident runner exited after invoking [exit()].
  bool get exited => _exited;

1114 1115 1116
  @override
  bool get supportsRestart {
    return isRunningDebug && flutterDevices.every((FlutterDevice device) {
1117 1118 1119 1120
      return device.device.supportsHotRestart;
    });
  }

1121
  @override
1122 1123
  bool get canHotReload => hotMode;

1124
  /// Start the app and keep the process running during its lifetime.
1125 1126 1127
  ///
  /// 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.
1128
  Future<int> run({
1129
    Completer<DebugConnectionInfo> connectionInfoCompleter,
1130
    Completer<void> appStartedCompleter,
1131
    bool enableDevTools = false,
1132 1133
    String route,
  });
1134

1135 1136 1137
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
1138
    bool allowExistingDdsInstance = false,
1139
    bool enableDevTools = false,
1140 1141
  });

1142
  @override
1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
  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,
1153
      platform: globals.platform,
1154
      projectDir: globals.fs.currentDirectory,
1155
      generateDartPluginRegistry: true,
1156
    );
1157 1158

    final CompositeTarget compositeTarget = CompositeTarget(<Target>[
1159
      const GenerateLocalizationsTarget(),
1160 1161 1162 1163 1164
      const DartPluginRegistrantTarget(),
    ]);

    _lastBuild = await globals.buildSystem.buildIncremental(
      compositeTarget,
1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180
      _environment,
      _lastBuild,
    );
    if (!_lastBuild.success) {
      for (final ExceptionMeasurement exceptionMeasurement in _lastBuild.exceptions.values) {
        globals.logger.printError(
          exceptionMeasurement.exception.toString(),
          stackTrace: globals.logger.isVerbose
            ? exceptionMeasurement.stackTrace
            : null,
        );
      }
    }
    globals.logger.printTrace('complete');
  }

1181
  @protected
1182
  void writeVmServiceFile() {
1183 1184
    if (debuggingOptions.vmserviceOutFile != null) {
      try {
1185
        final String address = flutterDevices.first.vmService.wsAddress.toString();
1186
        final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile);
1187 1188 1189
        vmserviceOutFile.createSync(recursive: true);
        vmserviceOutFile.writeAsStringSync(address);
      } on FileSystemException {
1190
        globals.printError('Failed to write vmservice-out-file at ${debuggingOptions.vmserviceOutFile}');
1191 1192 1193 1194
      }
    }
  }

1195
  @override
1196 1197
  Future<void> exit() async {
    _exited = true;
1198
    await residentDevtoolsHandler.shutdown();
1199
    await stopEchoingDeviceLog();
1200
    await preExit();
1201
    await exitApp(); // calls appFinished
1202
    await shutdownDartDevelopmentService();
1203 1204
  }

1205
  @override
1206
  Future<void> detach() async {
1207
    await residentDevtoolsHandler.shutdown();
1208
    await stopEchoingDeviceLog();
1209
    await preExit();
1210
    await shutdownDartDevelopmentService();
1211 1212 1213
    appFinished();
  }

1214 1215 1216
  Future<void> stopEchoingDeviceLog() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
1217
    );
1218 1219
  }

1220 1221 1222 1223 1224 1225 1226 1227
  Future<void> shutdownDartDevelopmentService() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>(
        (FlutterDevice device) => device.device?.dds?.shutdown()
      ).where((Future<void> element) => element != null)
    );
  }

1228 1229 1230 1231 1232 1233 1234 1235 1236 1237
  @protected
  void cacheInitialDillCompilation() {
    if (_dillOutputPath != null) {
      return;
    }
    globals.logger.printTrace('Caching compiled dill');
    final File outputDill = globals.fs.file(dillOutputPath);
    if (outputDill.existsSync()) {
      final String copyPath = getDefaultCachedKernelPath(
        trackWidgetCreation: trackWidgetCreation,
1238
        dartDefines: debuggingOptions.buildInfo.dartDefines,
1239
        extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions,
1240
      );
1241 1242 1243 1244
      globals.fs
          .file(copyPath)
          .parent
          .createSync(recursive: true);
1245 1246 1247 1248
      outputDill.copySync(copyPath);
    }
  }

1249
  void printStructuredErrorLog(vm_service.Event event) {
1250
    if (event.extensionKind == 'Flutter.Error' && !machine) {
1251 1252 1253 1254 1255 1256 1257
      final Map<dynamic, dynamic> json = event.extensionData?.data;
      if (json != null && json.containsKey('renderedErrorText')) {
        globals.printStatus('\n${json['renderedErrorText']}');
      }
    }
  }

1258
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
1259 1260 1261 1262 1263
  /// 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.
1264 1265 1266 1267
  Future<void> connectToServiceProtocol({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
1268
    GetSkSLMethod getSkSLMethod,
1269
    @required bool allowExistingDdsInstance,
1270
  }) async {
1271
    if (!debuggingOptions.debuggingEnabled) {
1272
      throw 'The service protocol is not enabled.';
1273
    }
1274
    _finished = Completer<int>();
1275
    // Listen for service protocol connection to close.
1276
    for (final FlutterDevice device in flutterDevices) {
1277
      await device.connect(
1278 1279 1280
        reloadSources: reloadSources,
        restart: restart,
        compileExpression: compileExpression,
1281
        enableDds: debuggingOptions.enableDds,
1282
        ddsPort: debuggingOptions.ddsPort,
1283
        allowExistingDdsInstance: allowExistingDdsInstance,
1284
        hostVmServicePort: debuggingOptions.hostVmServicePort,
1285 1286
        getSkSLMethod: getSkSLMethod,
        printStructuredErrorLogMethod: printStructuredErrorLog,
1287
        ipv6: ipv6,
1288
        disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes
1289
      );
1290 1291
      await device.vmService.getFlutterViews();

1292 1293 1294
      // 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.
1295
      unawaited(device.vmService.service.onDone.then<void>(
1296 1297 1298
        _serviceProtocolDone,
        onError: _serviceProtocolError,
      ).whenComplete(_serviceDisconnected));
1299
    }
1300 1301
  }

1302
  Future<void> _serviceProtocolDone(dynamic object) async {
1303
    globals.printTrace('Service protocol connection closed.');
1304 1305
  }

1306
  Future<void> _serviceProtocolError(dynamic error, StackTrace stack) {
1307
    globals.printTrace('Service protocol connection closed with an error: $error\n$stack');
1308
    return Future<void>.error(error, stack);
1309 1310
  }

1311
  void _serviceDisconnected() {
1312
    if (_exited) {
1313 1314 1315
      // User requested the application exit.
      return;
    }
1316
    if (_finished.isCompleted) {
1317
      return;
1318
    }
1319
    globals.printStatus('Lost connection to device.');
1320 1321 1322
    _finished.complete(0);
  }

1323
  void appFinished() {
1324
    if (_finished.isCompleted) {
1325
      return;
1326
    }
1327
    globals.printStatus('Application finished.');
1328 1329 1330
    _finished.complete(0);
  }

1331 1332 1333 1334 1335 1336
  void appFailedToStart() {
    if (!_finished.isCompleted) {
      _finished.complete(1);
    }
  }

1337
  Future<int> waitForAppToFinish() async {
1338
    final int exitCode = await _finished.future;
1339
    assert(exitCode != null);
1340 1341 1342 1343
    await cleanupAtFinish();
    return exitCode;
  }

1344 1345
  @mustCallSuper
  Future<void> preExit() async {
1346 1347
    // If _dillOutputPath is null, the tool created a temporary directory for
    // the dill.
1348 1349 1350 1351
    if (_dillOutputPath == null && artifactDirectory.existsSync()) {
      artifactDirectory.deleteSync(recursive: true);
    }
  }
1352

1353
  Future<void> exitApp() async {
1354
    final List<Future<void>> futures = <Future<void>>[
1355
      for (final FlutterDevice device in flutterDevices) device.exitApps(),
1356
    ];
1357
    await Future.wait(futures);
1358 1359 1360
    appFinished();
  }

1361
  bool get reportedDebuggers => _reportedDebuggers;
1362 1363 1364
  bool _reportedDebuggers = false;

  void printDebuggerList({ bool includeObservatory = true, bool includeDevtools = true }) {
1365
    final DevToolsServerAddress devToolsServerAddress = residentDevtoolsHandler.activeDevToolsServer;
1366
    if (!residentDevtoolsHandler.readyToAnnounce) {
1367 1368
      includeDevtools = false;
    }
1369
    assert(!includeDevtools || devToolsServerAddress != null);
1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387
    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 '
1388
            'on ${device.device.name} is available at: ${urlToDisplayString(uri)}',
1389 1390 1391 1392 1393 1394 1395
          );
        }
      }
    }
    _reportedDebuggers = true;
  }

1396
  void printHelpDetails() {
1397
    commandHelp.v.print();
1398
    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) {
1399
      commandHelp.s.print();
1400
    }
1401
    if (supportsServiceProtocol) {
1402 1403
      commandHelp.w.print();
      commandHelp.t.print();
1404
      if (isRunningDebug) {
1405 1406 1407 1408 1409
        commandHelp.L.print();
        commandHelp.S.print();
        commandHelp.U.print();
        commandHelp.i.print();
        commandHelp.p.print();
1410
        commandHelp.I.print();
1411
        commandHelp.o.print();
1412
        commandHelp.b.print();
1413
      } else {
1414 1415
        commandHelp.S.print();
        commandHelp.U.print();
1416
      }
1417 1418 1419
      // Performance related features: `P` should precede `a`, which should precede `M`.
      commandHelp.P.print();
      commandHelp.a.print();
1420 1421 1422
      if (supportsWriteSkSL) {
        commandHelp.M.print();
      }
1423 1424 1425
      if (isRunningDebug) {
        commandHelp.g.print();
      }
1426
    }
1427 1428
  }

1429
  @override
1430
  Future<void> cleanupAfterSignal();
1431

1432
  /// Called right before we exit.
1433
  Future<void> cleanupAtFinish();
1434
}
1435

Devon Carew's avatar
Devon Carew committed
1436
class OperationResult {
1437
  OperationResult(this.code, this.message, { this.fatal = false, this.updateFSReport });
Devon Carew's avatar
Devon Carew committed
1438

1439
  /// The result of the operation; a non-zero code indicates a failure.
Devon Carew's avatar
Devon Carew committed
1440
  final int code;
1441 1442

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

1445 1446 1447
  /// Whether this error should cause the runner to exit.
  final bool fatal;

1448 1449
  final UpdateFSReport updateFSReport;

Devon Carew's avatar
Devon Carew committed
1450
  bool get isOk => code == 0;
1451

1452
  static final OperationResult ok = OperationResult(0, '');
Devon Carew's avatar
Devon Carew committed
1453 1454
}

1455
Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async {
1456 1457
  switch (platform) {
    case TargetPlatform.android_arm:
1458
    case TargetPlatform.android_arm64:
1459
    case TargetPlatform.android_x64:
1460
    case TargetPlatform.android_x86:
1461
      final FlutterProject project = FlutterProject.current();
1462
      final String manifestPath = globals.fs.path.relative(project.android.appManifestFile.path);
1463
      return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
1464 1465 1466 1467 1468 1469
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
    default:
      return null;
  }
}
1470

1471 1472
/// Redirects terminal commands to the correct resident runner methods.
class TerminalHandler {
1473 1474 1475 1476
  TerminalHandler(this.residentRunner, {
    @required Logger logger,
    @required Terminal terminal,
    @required Signals signals,
1477 1478 1479
    @required io.ProcessInfo processInfo,
    @required bool reportReady,
    String pidFile,
1480 1481
  }) : _logger = logger,
       _terminal = terminal,
1482 1483 1484 1485
       _signals = signals,
       _processInfo = processInfo,
       _reportReady = reportReady,
       _pidFile = pidFile;
1486 1487 1488 1489

  final Logger _logger;
  final Terminal _terminal;
  final Signals _signals;
1490 1491 1492
  final io.ProcessInfo _processInfo;
  final bool _reportReady;
  final String _pidFile;
1493

1494
  final ResidentHandlers residentRunner;
1495 1496
  bool _processingUserRequest = false;
  StreamSubscription<void> subscription;
1497
  File _actualPidFile;
1498

1499 1500 1501
  @visibleForTesting
  String lastReceivedCommand;

1502 1503 1504 1505
  /// This is only a buffer logger in unit tests
  @visibleForTesting
  BufferLogger get logger => _logger as BufferLogger;

1506
  void setupTerminal() {
1507 1508
    if (!_logger.quiet) {
      _logger.printStatus('');
1509 1510
      residentRunner.printHelp(details: false);
    }
1511 1512
    _terminal.singleCharMode = true;
    subscription = _terminal.keystrokes.listen(processTerminalInput);
1513 1514
  }

1515 1516 1517
  final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{};

  void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) {
1518
    _signalTokens[signal] = _signals.addHandler(signal, handler);
1519 1520
  }

1521 1522
  void registerSignalHandlers() {
    assert(residentRunner.stayResident);
1523 1524
    _addSignalHandler(io.ProcessSignal.sigint, _cleanUp);
    _addSignalHandler(io.ProcessSignal.sigterm, _cleanUp);
1525
    if (residentRunner.supportsServiceProtocol && residentRunner.supportsRestart) {
1526 1527
      _addSignalHandler(io.ProcessSignal.sigusr1, _handleSignal);
      _addSignalHandler(io.ProcessSignal.sigusr2, _handleSignal);
1528 1529 1530 1531
      if (_pidFile != null) {
        _logger.printTrace('Writing pid to: $_pidFile');
        _actualPidFile = _processInfo.writePidFile(_pidFile);
      }
1532
    }
1533 1534 1535 1536 1537
  }

  /// Unregisters terminal signal and keystroke handlers.
  void stop() {
    assert(residentRunner.stayResident);
1538 1539 1540 1541 1542 1543 1544 1545 1546
    if (_actualPidFile != null) {
      try {
        _logger.printTrace('Deleting pid file (${_actualPidFile.path}).');
        _actualPidFile.deleteSync();
      } on FileSystemException catch (error) {
        _logger.printError('Failed to delete pid file (${_actualPidFile.path}): ${error.message}');
      }
      _actualPidFile = null;
    }
1547
    for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
1548
      _signals.removeHandler(entry.key, entry.value);
1549 1550 1551
    }
    _signalTokens.clear();
    subscription.cancel();
1552 1553 1554 1555
  }

  /// Returns [true] if the input has been handled by this function.
  Future<bool> _commonTerminalInputHandler(String character) async {
1556 1557
    _logger.printStatus(''); // the key the user tapped might be on this line
    switch (character) {
1558
      case 'a':
1559
        return residentRunner.debugToggleProfileWidgetBuilds();
1560
      case 'b':
1561
        return residentRunner.debugToggleBrightness();
1562
      case 'c':
1563
        _logger.clear();
1564
        return true;
1565 1566 1567 1568
      case 'd':
      case 'D':
        await residentRunner.detach();
        return true;
1569 1570 1571
      case 'g':
        await residentRunner.runSourceGenerators();
        return true;
1572 1573 1574 1575 1576 1577 1578
      case 'h':
      case 'H':
      case '?':
        // help
        residentRunner.printHelp(details: true);
        return true;
      case 'i':
1579
        return residentRunner.debugToggleWidgetInspector();
1580
      case 'I':
1581
        return residentRunner.debugToggleInvertOversizedImages();
1582
      case 'L':
1583
        return residentRunner.debugDumpLayerTree();
1584 1585
      case 'o':
      case 'O':
1586
        return residentRunner.debugTogglePlatform();
1587 1588 1589 1590 1591 1592
      case 'M':
        if (residentRunner.supportsWriteSkSL) {
          await residentRunner.writeSkSL();
          return true;
        }
        return false;
1593
      case 'p':
1594
        return residentRunner.debugToggleDebugPaintSizeEnabled();
1595
      case 'P':
1596
        return residentRunner.debugTogglePerformanceOverlayOverride();
1597 1598 1599 1600 1601
      case 'q':
      case 'Q':
        // exit
        await residentRunner.exit();
        return true;
1602 1603 1604 1605 1606
      case 'r':
        if (!residentRunner.canHotReload) {
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: false);
1607 1608 1609
        if (result.fatal) {
          throwToolExit(result.message);
        }
1610
        if (!result.isOk) {
1611
          _logger.printStatus('Try again after fixing the above error(s).', emphasis: true);
1612 1613 1614 1615
        }
        return true;
      case 'R':
        // If hot restart is not supported for all devices, ignore the command.
1616
        if (!residentRunner.supportsRestart || !residentRunner.hotMode) {
1617 1618 1619
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: true);
1620 1621 1622
        if (result.fatal) {
          throwToolExit(result.message);
        }
1623
        if (!result.isOk) {
1624
          _logger.printStatus('Try again after fixing the above error(s).', emphasis: true);
1625 1626
        }
        return true;
1627 1628
      case 's':
        for (final FlutterDevice device in residentRunner.flutterDevices) {
1629
          await residentRunner.screenshot(device);
1630 1631
        }
        return true;
1632
      case 'S':
1633
        return residentRunner.debugDumpSemanticsTreeInTraversalOrder();
1634 1635
      case 't':
      case 'T':
1636
        return residentRunner.debugDumpRenderTree();
1637
      case 'U':
1638
        return residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
1639 1640 1641
      case 'v':
      case 'V':
        return residentRunner.residentDevtoolsHandler.launchDevToolsInBrowser(flutterDevices: residentRunner.flutterDevices);
1642 1643
      case 'w':
      case 'W':
1644
        return residentRunner.debugDumpApp();
1645 1646 1647 1648 1649 1650 1651 1652
    }
    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) {
1653
      _logger.printTrace('Ignoring terminal input: "$command" because we are busy.');
1654 1655 1656 1657
      return;
    }
    _processingUserRequest = true;
    try {
1658 1659
      lastReceivedCommand = command;
      await _commonTerminalInputHandler(command);
1660 1661
    // Catch all exception since this is doing cleanup and rethrowing.
    } catch (error, st) { // ignore: avoid_catches_without_on_clauses
1662 1663
      // Don't print stack traces for known error types.
      if (error is! ToolExit) {
1664
        _logger.printError('$error\n$st');
1665
      }
1666 1667
      await _cleanUp(null);
      rethrow;
1668 1669
    } finally {
      _processingUserRequest = false;
1670 1671 1672
      if (_reportReady) {
        _logger.printStatus('ready');
      }
1673 1674 1675 1676 1677
    }
  }

  Future<void> _handleSignal(io.ProcessSignal signal) async {
    if (_processingUserRequest) {
1678
      _logger.printTrace('Ignoring signal: "$signal" because we are busy.');
1679 1680 1681 1682
      return;
    }
    _processingUserRequest = true;

1683
    final bool fullRestart = signal == io.ProcessSignal.sigusr2;
1684 1685 1686 1687 1688 1689 1690 1691

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

1692
  Future<void> _cleanUp(io.ProcessSignal signal) async {
1693
    _terminal.singleCharMode = false;
1694
    await subscription?.cancel();
1695 1696 1697 1698
    await residentRunner.cleanupAfterSignal();
  }
}

1699
class DebugConnectionInfo {
1700
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
1701

1702 1703
  final Uri httpUri;
  final Uri wsUri;
1704 1705
  final String baseUri;
}
1706 1707 1708 1709

/// Returns the next platform value for the switcher.
///
/// These values must match what is available in
1710
/// `packages/flutter/lib/src/foundation/binding.dart`.
1711
String nextPlatform(String currentPlatform) {
1712 1713 1714 1715 1716 1717
  switch (currentPlatform) {
    case 'android':
      return 'iOS';
    case 'iOS':
      return 'fuchsia';
    case 'fuchsia':
1718
      return 'macOS';
1719 1720 1721 1722 1723 1724 1725
    case 'macOS':
      return 'android';
    default:
      assert(false); // Invalid current platform.
      return 'android';
  }
}
1726

1727 1728
/// A launcher for the devtools debugger and analysis tool.
abstract class DevtoolsLauncher {
1729 1730 1731 1732 1733 1734 1735
  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();
1736

1737 1738
  /// Launch a Dart DevTools process, optionally targeting a specific VM Service
  /// URI if [vmServiceUri] is non-null.
1739
  ///
1740 1741 1742
  /// [additionalArguments] may be optionally specified and are passed directly
  /// to the devtools run command.
  ///
1743 1744
  /// This method must return a future that is guaranteed not to fail, because it
  /// will be used in unawaited contexts.
1745
  Future<void> launch(Uri vmServiceUri, {List<String> additionalArguments});
1746

1747
  Future<void> close();
1748

1749
  /// When measuring devtools memory via additional arguments, the launch process
1750 1751 1752 1753 1754
  /// will technically never complete.
  ///
  /// Us this as an indicator that the process has started.
  Future<void> processStart;

1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772
  /// 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>();
    }
  }
1773

1774 1775 1776
  /// The URL of the current DevTools server.
  ///
  /// Returns null if [ready] is not complete.
1777
  DevToolsServerAddress get activeDevToolsServer {
1778
    if (_devToolsUrl == null) {
1779 1780
      return null;
    }
1781
    return DevToolsServerAddress(devToolsUrl.host, devToolsUrl.port);
1782
  }
1783
}
1784 1785 1786 1787 1788 1789

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

  final String host;
  final int port;
1790 1791 1792 1793 1794 1795 1796

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