resident_runner.dart 61.1 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 51
    this.fileSystemRoots,
    this.fileSystemScheme,
52
    TargetModel targetModel = TargetModel.flutter,
53
    this.targetPlatform,
54
    ResidentCompiler generator,
55
    this.userIdentifier,
56
  }) : assert(buildInfo.trackWidgetCreation != null),
57
       generator = generator ?? ResidentCompiler(
58
         globals.artifacts.getArtifactPath(
59 60
           Artifact.flutterPatchedSdkPath,
           platform: targetPlatform,
61
           mode: buildInfo.mode,
62
         ),
63 64
         buildMode: buildInfo.mode,
         trackWidgetCreation: buildInfo.trackWidgetCreation,
65
         fileSystemRoots: fileSystemRoots ?? <String>[],
66
         fileSystemScheme: fileSystemScheme,
67
         targetModel: targetModel,
68
         dartDefines: buildInfo.dartDefines,
69
         packagesPath: buildInfo.packagesPath,
70
         extraFrontEndOptions: buildInfo.extraFrontEndOptions,
71 72 73
         artifacts: globals.artifacts,
         processManager: globals.processManager,
         logger: globals.logger,
74
         platform: globals.platform,
75
         fileSystem: globals.fs,
76 77
       );

78
  /// Create a [FlutterDevice] with optional code generation enabled.
79 80
  static Future<FlutterDevice> create(
    Device device, {
81
    @required String target,
82
    @required BuildInfo buildInfo,
83
    @required Platform platform,
84 85 86 87 88
    List<String> fileSystemRoots,
    String fileSystemScheme,
    TargetModel targetModel = TargetModel.flutter,
    List<String> experimentalFlags,
    ResidentCompiler generator,
89
    String userIdentifier,
90 91
  }) async {
    ResidentCompiler generator;
92 93 94 95
    final TargetPlatform targetPlatform = await device.targetPlatform;
    if (device.platformType == PlatformType.fuchsia) {
      targetModel = TargetModel.flutterRunner;
    }
96 97 98 99 100 101
    // 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.
102
    if (targetPlatform == TargetPlatform.web_javascript) {
103
      // TODO(jonahwilliams): consistently provide these flags across platforms.
104
      HostArtifact platformDillArtifact;
105
      final List<String> extraFrontEndOptions = List<String>.of(buildInfo.extraFrontEndOptions ?? <String>[]);
106
      if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
107
        platformDillArtifact = HostArtifact.webPlatformKernelDill;
108 109 110
        if (!extraFrontEndOptions.contains('--no-sound-null-safety')) {
          extraFrontEndOptions.add('--no-sound-null-safety');
        }
111
      } else if (buildInfo.nullSafetyMode == NullSafetyMode.sound) {
112
        platformDillArtifact = HostArtifact.webPlatformSoundKernelDill;
113 114
        if (!extraFrontEndOptions.contains('--sound-null-safety')) {
          extraFrontEndOptions.add('--sound-null-safety');
115
        }
116 117
      } else {
        assert(false);
118 119
      }

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

185 186 187 188 189
    return FlutterDevice(
      device,
      fileSystemRoots: fileSystemRoots,
      fileSystemScheme:fileSystemScheme,
      targetModel: targetModel,
190
      targetPlatform: targetPlatform,
191
      generator: generator,
192
      buildInfo: buildInfo,
193
      userIdentifier: userIdentifier,
194 195 196
    );
  }

197
  final TargetPlatform targetPlatform;
198
  final Device device;
199
  final ResidentCompiler generator;
200
  final BuildInfo buildInfo;
201
  final String userIdentifier;
202 203

  DevFSWriter devFSWriter;
204
  Stream<Uri> observatoryUris;
205
  FlutterVmService vmService;
206 207
  DevFS devFS;
  ApplicationPackage package;
208 209
  List<String> fileSystemRoots;
  String fileSystemScheme;
210
  StreamSubscription<String> _loggingSubscription;
211
  bool _isListeningForObservatoryUri;
212

213 214 215
  /// Whether the stream [observatoryUris] is still open.
  bool get isWaitingForObservatory => _isListeningForObservatoryUri ?? false;

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

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

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

344 345 346
  Future<void> exitApps({
    @visibleForTesting Duration timeoutDelay = const Duration(seconds: 10),
  }) async {
347
    if (!device.supportsFlutterExit || vmService == null) {
348
      return device.stopApp(package, userIdentifier: userIdentifier);
349
    }
350
    final List<FlutterView> views = await vmService.getFlutterViews();
351
    if (views == null || views.isEmpty) {
352
      return device.stopApp(package, userIdentifier: userIdentifier);
353
    }
354 355
    // If any of the flutter views are paused, we might not be able to
    // cleanly exit since the service extension may not have been registered.
356 357 358 359 360 361 362
    for (final FlutterView flutterView in views) {
      final vm_service.Isolate isolate = await vmService
        .getIsolateOrNull(flutterView.uiIsolate.id);
      if (isolate == null) {
        continue;
      }
      if (isPauseEvent(isolate.pauseEvent.kind)) {
363
        return device.stopApp(package, userIdentifier: userIdentifier);
364 365
      }
    }
366
    for (final FlutterView view in views) {
367
      if (view != null && view.uiIsolate != null) {
368 369
        // If successful, there will be no response from flutterExit. If the exit
        // method is not registered, this will complete with `false`.
370
        unawaited(vmService.flutterExit(
371
          isolateId: view.uiIsolate.id,
372 373 374 375 376 377
        ).then((bool exited) async {
          // If exiting the app failed, fall back to stopApp
          if (!exited) {
            await device.stopApp(package, userIdentifier: userIdentifier);
          }
        }));
378
      }
379
    }
380
    return vmService.service.onDone
381 382
      .catchError((dynamic error, StackTrace stackTrace) {
        globals.logger.printError(
383
          'unhandled error waiting for vm service exit:\n $error',
384 385 386
          stackTrace: stackTrace,
         );
      })
387
      .timeout(timeoutDelay, onTimeout: () {
388
        return device.stopApp(package, userIdentifier: userIdentifier);
389
      });
390 391
  }

392 393
  Future<Uri> setupDevFS(
    String fsName,
394 395
    Directory rootDirectory,
  ) {
396
    // One devFS per device. Shared by all running instances.
397
    devFS = DevFS(
398
      vmService,
399 400
      fsName,
      rootDirectory,
401
      osUtils: globals.os,
402 403
      fileSystem: globals.fs,
      logger: globals.logger,
404 405 406 407
    );
    return devFS.create();
  }

408
  Future<void> startEchoingDeviceLog() async {
409
    if (_loggingSubscription != null) {
410
      return;
411
    }
412
    final Stream<String> logStream = (await device.getLogReader(app: package)).logLines;
413
    if (logStream == null) {
414
      globals.printError('Failed to read device log stream');
415 416 417
      return;
    }
    _loggingSubscription = logStream.listen((String line) {
418
      if (!line.contains('Observatory listening on http')) {
419
        globals.printStatus(line, wrap: false);
420
      }
421 422 423
    });
  }

424
  Future<void> stopEchoingDeviceLog() async {
425
    if (_loggingSubscription == null) {
426
      return;
427
    }
428 429 430 431
    await _loggingSubscription.cancel();
    _loggingSubscription = null;
  }

432
  Future<void> initLogReader() async {
433
    final vm_service.VM vm = await vmService.service.getVM();
434 435
    final DeviceLogReader logReader = await device.getLogReader(app: package);
    logReader.appPid = vm.pid;
436 437 438 439 440 441 442
  }

  Future<int> runHot({
    HotRunner hotRunner,
    String route,
  }) async {
    final bool prebuiltMode = hotRunner.applicationBinary != null;
443
    final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName;
444
    globals.printStatus(
445
      'Launching ${getDisplayPath(hotRunner.mainPath, globals.fs)} '
446 447
      'on ${device.name} in $modeName mode...',
    );
448 449

    final TargetPlatform targetPlatform = await device.targetPlatform;
450
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
451
      targetPlatform,
452
      buildInfo: hotRunner.debuggingOptions.buildInfo,
453
      applicationBinary: hotRunner.applicationBinary,
454 455 456 457
    );

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
458
      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
459
      if (hint != null) {
460
        message += '\n$hint';
461
      }
462
      globals.printError(message);
463 464
      return 1;
    }
465
    devFSWriter = device.createDevFSWriter(package, userIdentifier);
466 467 468

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

469
    await startEchoingDeviceLog();
470 471 472 473 474 475 476 477 478

    // Start the application.
    final Future<LaunchResult> futureResult = device.startApp(
      package,
      mainPath: hotRunner.mainPath,
      debuggingOptions: hotRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
479
      ipv6: hotRunner.ipv6,
480
      userIdentifier: userIdentifier,
481 482 483 484 485
    );

    final LaunchResult result = await futureResult;

    if (!result.started) {
486
      globals.printError('Error launching application on ${device.name}.');
487 488 489
      await stopEchoingDeviceLog();
      return 2;
    }
490
    if (result.hasObservatory) {
491 492 493
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
494
    } else {
495 496 497
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
498
    }
499 500 501 502 503 504 505 506
    return 0;
  }

  Future<int> runCold({
    ColdRunner coldRunner,
    String route,
  }) async {
    final TargetPlatform targetPlatform = await device.targetPlatform;
507
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
508
      targetPlatform,
509
      buildInfo: coldRunner.debuggingOptions.buildInfo,
510
      applicationBinary: coldRunner.applicationBinary,
511
    );
512
    devFSWriter = device.createDevFSWriter(package, userIdentifier);
513

514
    final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName;
515 516 517
    final bool prebuiltMode = coldRunner.applicationBinary != null;
    if (coldRunner.mainPath == null) {
      assert(prebuiltMode);
518 519 520 521
      globals.printStatus(
        'Launching ${package.displayName} '
        'on ${device.name} in $modeName mode...',
      );
522
    } else {
523
      globals.printStatus(
524
        'Launching ${getDisplayPath(coldRunner.mainPath, globals.fs)} '
525 526
        'on ${device.name} in $modeName mode...',
      );
527 528 529 530
    }

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
531
      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
532
      if (hint != null) {
533
        message += '\n$hint';
534
      }
535
      globals.printError(message);
536 537 538
      return 1;
    }

539
    final Map<String, dynamic> platformArgs = <String, dynamic>{};
540
    if (coldRunner.traceStartup != null) {
541
      platformArgs['trace-startup'] = coldRunner.traceStartup;
542
    }
543

544
    await startEchoingDeviceLog();
545 546 547 548 549 550 551 552

    final LaunchResult result = await device.startApp(
      package,
      mainPath: coldRunner.mainPath,
      debuggingOptions: coldRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
553
      ipv6: coldRunner.ipv6,
554
      userIdentifier: userIdentifier,
555 556 557
    );

    if (!result.started) {
558
      globals.printError('Error running application on ${device.name}.');
559 560 561
      await stopEchoingDeviceLog();
      return 2;
    }
562
    if (result.hasObservatory) {
563 564 565
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
566
    } else {
567 568 569
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
570
    }
571 572 573
    return 0;
  }

574
  Future<UpdateFSReport> updateDevFS({
575
    Uri mainUri,
576
    String target,
577
    AssetBundle bundle,
578
    DateTime firstBuildTime,
579 580 581
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
582
    String projectRootPath,
583
    String pathToReload,
584
    @required String dillOutputPath,
585
    @required List<Uri> invalidatedFiles,
586
    @required PackageConfig packageConfig,
587
  }) async {
588
    final Status devFSStatus = globals.logger.startProgress(
589 590
      'Syncing files to device ${device.name}...',
    );
591
    UpdateFSReport report;
592
    try {
593 594 595 596 597 598 599 600 601 602 603 604 605 606
      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,
607
        devFSWriter: devFSWriter,
608
      );
609 610
    } on DevFSException {
      devFSStatus.cancel();
611
      return UpdateFSReport(success: false);
612
    }
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
    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;

663 664
  ResidentDevtoolsHandler get residentDevtoolsHandler;

665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
  @protected
  Logger get logger;

  @protected
  FileSystem get fileSystem;

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

  /// Perfor a hot reload or hot restart of all attached applications.
  ///
  /// 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) {
690 691
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
692
        final String data = await device.vmService.flutterDebugDumpApp(
693
          isolateId: view.uiIsolate.id,
694 695 696 697 698 699 700 701 702 703 704 705 706
        );
        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) {
707 708
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
709
        final String data = await device.vmService.flutterDebugDumpRenderTree(
710
          isolateId: view.uiIsolate.id,
711 712 713 714 715 716 717 718 719 720 721 722 723
        );
        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) {
724 725
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
726
        final String data = await device.vmService.flutterDebugDumpLayerTree(
727
          isolateId: view.uiIsolate.id,
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
        );
        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) {
743 744
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
745
        final String data = await device.vmService.flutterDebugDumpSemanticsTreeInTraversalOrder(
746
          isolateId: view.uiIsolate.id,
747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
        );
        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) {
762 763
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
764
        final String data = await device.vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
765
          isolateId: view.uiIsolate.id,
766 767 768 769 770 771 772 773 774 775 776 777 778
        );
        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) {
779 780
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
781
        await device.vmService.flutterToggleDebugPaintSizeEnabled(
782
          isolateId: view.uiIsolate.id,
783 784 785 786 787 788 789 790 791 792 793 794
        );
      }
    }
    return true;
  }

  /// Toggle the "elevation check" debugging feature.
  Future<bool> debugToggleDebugCheckElevationsEnabled() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
795 796
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
797
        await device.vmService.flutterToggleDebugCheckElevationsEnabled(
798
          isolateId: view.uiIsolate.id,
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815
        );
      }
    }
    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;
      }
816 817
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
818
        await device.vmService.flutterTogglePerformanceOverlayOverride(
819
          isolateId: view.uiIsolate.id,
820 821 822 823 824 825 826 827 828 829 830 831
        );
      }
    }
    return true;
  }

  /// Toggle the widget inspector.
  Future<bool> debugToggleWidgetInspector() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
832 833
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
834
        await device.vmService.flutterToggleWidgetInspector(
835
          isolateId: view.uiIsolate.id,
836 837 838 839 840 841 842 843 844 845 846 847
        );
      }
    }
    return true;
  }

  /// Toggle the "invert images" debugging feature.
  Future<bool> debugToggleInvertOversizedImages() async {
    if (!supportsServiceProtocol || !isRunningDebug) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
848 849
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
850
        await device.vmService.flutterToggleInvertOversizedImages(
851
          isolateId: view.uiIsolate.id,
852 853 854 855 856 857 858 859 860 861 862 863
        );
      }
    }
    return true;
  }

  /// Toggle the "profile widget builds" debugging feature.
  Future<bool> debugToggleProfileWidgetBuilds() async {
    if (!supportsServiceProtocol) {
      return false;
    }
    for (final FlutterDevice device in flutterDevices) {
864 865
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
866
        await device.vmService.flutterToggleProfileWidgetBuilds(
867
          isolateId: view.uiIsolate.id,
868 869 870 871 872 873
        );
      }
    }
    return true;
  }

874
  /// Toggle the operating system brightness (light or dark).
875 876 877 878
  Future<bool> debugToggleBrightness() async {
    if (!supportsServiceProtocol) {
      return false;
    }
879
    final List<FlutterView> views = await flutterDevices.first.vmService.getFlutterViews();
880
    final Brightness current = await flutterDevices.first.vmService.flutterBrightnessOverride(
881
      isolateId: views.first.uiIsolate.id,
882 883 884 885 886 887 888 889
    );
    Brightness next;
    if (current == Brightness.light) {
      next = Brightness.dark;
    } else {
      next = Brightness.light;
    }
    for (final FlutterDevice device in flutterDevices) {
890 891
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
892
        await device.vmService.flutterBrightnessOverride(
893
          isolateId: view.uiIsolate.id,
894 895 896 897 898 899 900 901 902 903 904 905 906
          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;
    }
907
    final List<FlutterView> views = await flutterDevices.first.vmService.getFlutterViews();
908 909
    final String from = await flutterDevices
      .first.vmService.flutterPlatformOverride(
910
        isolateId: views.first.uiIsolate.id,
911 912 913
      );
    final String to = nextPlatform(from);
    for (final FlutterDevice device in flutterDevices) {
914 915
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
916 917
        await device.vmService.flutterPlatformOverride(
          platform: to,
918
          isolateId: view.uiIsolate.id,
919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947
        );
      }
    }
    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.
  ///
948 949 950 951 952 953 954 955
  /// 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.
956
  Future<void> screenshot(FlutterDevice device) async {
957 958 959
    if (!device.device.supportsScreenshot && !supportsServiceProtocol) {
      return;
    }
960 961 962 963 964 965 966 967
    final Status status = logger.startProgress(
      'Taking screenshot for ${device.device.name}...',
    );
    final File outputFile = getUniqueFile(
      fileSystem.currentDirectory,
      'flutter',
      'png',
    );
968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006

    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 {
1007
    List<FlutterView> views = <FlutterView>[];
1008
    if (supportsServiceProtocol) {
1009
      views = await device.vmService.getFlutterViews();
1010 1011
    }

1012 1013
    Future<bool> setDebugBanner(bool value) async {
      try {
1014
        for (final FlutterView view in views) {
1015 1016
          await device.vmService.flutterDebugAllowBanner(
            value,
1017
            isolateId: view.uiIsolate.id,
1018 1019 1020
          );
        }
        return true;
1021
      } on vm_service.RPCError catch (error) {
1022 1023 1024 1025
        logger.printError('Error communicating with Flutter on the device: $error');
        return false;
      }
    }
1026 1027 1028 1029
    if (!await setDebugBanner(false)) {
      return false;
    }
    bool succeeded = true;
1030
    try {
1031 1032 1033 1034
      await cb();
    } finally {
      if (!await setDebugBanner(true)) {
        succeeded = false;
1035 1036
      }
    }
1037
    return succeeded;
1038
  }
1039

1040

1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
  /// 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();
1058 1059
}

1060
// Shared code between different resident application runners.
1061
abstract class ResidentRunner extends ResidentHandlers {
1062 1063
  ResidentRunner(
    this.flutterDevices, {
1064
    @required this.target,
1065
    @required this.debuggingOptions,
1066
    String projectRootPath,
1067
    this.ipv6,
1068 1069
    this.stayResident = true,
    this.hotMode = true,
1070
    String dillOutputPath,
1071
    this.machine = false,
1072
    ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
1073
  }) : mainPath = globals.fs.path.absolute(target),
1074
       packagesFilePath = debuggingOptions.buildInfo.packagesPath,
1075
       projectRootPath = projectRootPath ?? globals.fs.currentDirectory.path,
1076 1077
       _dillOutputPath = dillOutputPath,
       artifactDirectory = dillOutputPath == null
1078 1079
          ? globals.fs.systemTempDirectory.createTempSync('flutter_tool.')
          : globals.fs.file(dillOutputPath).parent,
1080 1081 1082 1083 1084
       assetBundle = AssetBundleFactory.instance.createBundle(),
       commandHelp = CommandHelp(
         logger: globals.logger,
         terminal: globals.terminal,
         platform: globals.platform,
1085
         outputPreferences: globals.outputPreferences,
1086
       ) {
1087 1088
    if (!artifactDirectory.existsSync()) {
      artifactDirectory.createSync(recursive: true);
1089
    }
1090
    _residentDevtoolsHandler = devtoolsHandler(DevtoolsLauncher.instance, this, globals.logger);
1091
  }
1092

1093 1094 1095 1096 1097 1098 1099
  @override
  Logger get logger => globals.logger;

  @override
  FileSystem get fileSystem => globals.fs;

  @override
1100
  final List<FlutterDevice> flutterDevices;
1101

1102 1103
  final String target;
  final DebuggingOptions debuggingOptions;
1104 1105

  @override
1106
  final bool stayResident;
1107
  final bool ipv6;
1108 1109 1110
  final String _dillOutputPath;
  /// The parent location of the incremental artifacts.
  final Directory artifactDirectory;
1111 1112 1113 1114 1115
  final String packagesFilePath;
  final String projectRootPath;
  final String mainPath;
  final AssetBundle assetBundle;

1116
  final CommandHelp commandHelp;
1117
  final bool machine;
1118

1119
  @override
1120 1121
  ResidentDevtoolsHandler get residentDevtoolsHandler => _residentDevtoolsHandler;
  ResidentDevtoolsHandler _residentDevtoolsHandler;
1122

1123
  bool _exited = false;
1124
  Completer<int> _finished = Completer<int>();
1125 1126 1127 1128
  BuildResult _lastBuild;
  Environment _environment;

  @override
1129 1130 1131 1132 1133 1134 1135 1136
  bool hotMode;

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

1138
  String get dillOutputPath => _dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill');
1139 1140 1141 1142 1143
  String getReloadPath({
    bool fullRestart = false,
    @required bool swap,
  }) {
    if (!fullRestart) {
1144
      return 'main.dart.incremental.dill';
1145
    }
1146
    return 'main.dart${swap ? '.swap' : ''}.dill';
1147
  }
1148

1149
  bool get debuggingEnabled => debuggingOptions.debuggingEnabled;
1150 1151

  @override
1152
  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
1153 1154

  @override
1155
  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
1156 1157

  @override
1158
  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
1159 1160

  @override
1161
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
1162 1163

  @override
1164
  bool get supportsWriteSkSL => supportsServiceProtocol;
1165

1166
  bool get trackWidgetCreation => debuggingOptions.buildInfo.trackWidgetCreation;
1167

1168 1169 1170 1171 1172 1173 1174
  // 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;

1175 1176 1177
  /// Returns [true] if the resident runner exited after invoking [exit()].
  bool get exited => _exited;

1178 1179 1180
  @override
  bool get supportsRestart {
    return isRunningDebug && flutterDevices.every((FlutterDevice device) {
1181 1182 1183 1184
      return device.device.supportsHotRestart;
    });
  }

1185
  @override
1186 1187
  bool get canHotReload => hotMode;

1188
  /// Start the app and keep the process running during its lifetime.
1189 1190 1191
  ///
  /// 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.
1192
  Future<int> run({
1193
    Completer<DebugConnectionInfo> connectionInfoCompleter,
1194
    Completer<void> appStartedCompleter,
1195
    bool enableDevTools = false,
1196 1197
    String route,
  });
1198

1199 1200 1201
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
1202
    bool allowExistingDdsInstance = false,
1203
    bool enableDevTools = false,
1204 1205
  });

1206
  @override
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216
  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,
1217
      platform: globals.platform,
1218
      projectDir: globals.fs.currentDirectory,
1219
      generateDartPluginRegistry: true,
1220
    );
1221 1222

    final CompositeTarget compositeTarget = CompositeTarget(<Target>[
1223
      const GenerateLocalizationsTarget(),
1224 1225 1226 1227 1228
      const DartPluginRegistrantTarget(),
    ]);

    _lastBuild = await globals.buildSystem.buildIncremental(
      compositeTarget,
1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244
      _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');
  }

1245
  @protected
1246
  void writeVmServiceFile() {
1247 1248
    if (debuggingOptions.vmserviceOutFile != null) {
      try {
1249
        final String address = flutterDevices.first.vmService.wsAddress.toString();
1250
        final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile);
1251 1252 1253
        vmserviceOutFile.createSync(recursive: true);
        vmserviceOutFile.writeAsStringSync(address);
      } on FileSystemException {
1254
        globals.printError('Failed to write vmservice-out-file at ${debuggingOptions.vmserviceOutFile}');
1255 1256 1257 1258
      }
    }
  }

1259
  @override
1260 1261
  Future<void> exit() async {
    _exited = true;
1262
    await residentDevtoolsHandler.shutdown();
1263
    await stopEchoingDeviceLog();
1264
    await preExit();
1265
    await exitApp(); // calls appFinished
1266
    await shutdownDartDevelopmentService();
1267 1268
  }

1269
  @override
1270
  Future<void> detach() async {
1271
    await residentDevtoolsHandler.shutdown();
1272
    await stopEchoingDeviceLog();
1273
    await preExit();
1274
    await shutdownDartDevelopmentService();
1275 1276 1277
    appFinished();
  }

1278 1279 1280
  Future<void> stopEchoingDeviceLog() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
1281
    );
1282 1283
  }

1284 1285 1286 1287 1288 1289 1290 1291
  Future<void> shutdownDartDevelopmentService() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>(
        (FlutterDevice device) => device.device?.dds?.shutdown()
      ).where((Future<void> element) => element != null)
    );
  }

1292 1293 1294 1295 1296 1297 1298 1299 1300 1301
  @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,
1302
        dartDefines: debuggingOptions.buildInfo.dartDefines,
1303
        extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions,
1304
      );
1305 1306 1307 1308
      globals.fs
          .file(copyPath)
          .parent
          .createSync(recursive: true);
1309 1310 1311 1312
      outputDill.copySync(copyPath);
    }
  }

1313
  void printStructuredErrorLog(vm_service.Event event) {
1314
    if (event.extensionKind == 'Flutter.Error' && !machine) {
1315 1316 1317 1318 1319 1320 1321
      final Map<dynamic, dynamic> json = event.extensionData?.data;
      if (json != null && json.containsKey('renderedErrorText')) {
        globals.printStatus('\n${json['renderedErrorText']}');
      }
    }
  }

1322
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
1323 1324 1325 1326 1327
  /// 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.
1328 1329 1330 1331
  Future<void> connectToServiceProtocol({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
1332
    GetSkSLMethod getSkSLMethod,
1333
    @required bool allowExistingDdsInstance,
1334
  }) async {
1335
    if (!debuggingOptions.debuggingEnabled) {
1336
      throw 'The service protocol is not enabled.';
1337
    }
1338
    _finished = Completer<int>();
1339
    // Listen for service protocol connection to close.
1340
    for (final FlutterDevice device in flutterDevices) {
1341
      await device.connect(
1342 1343 1344
        reloadSources: reloadSources,
        restart: restart,
        compileExpression: compileExpression,
1345
        enableDds: debuggingOptions.enableDds,
1346
        ddsPort: debuggingOptions.ddsPort,
1347
        allowExistingDdsInstance: allowExistingDdsInstance,
1348
        hostVmServicePort: debuggingOptions.hostVmServicePort,
1349 1350
        getSkSLMethod: getSkSLMethod,
        printStructuredErrorLogMethod: printStructuredErrorLog,
1351
        ipv6: ipv6,
1352
        disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes
1353
      );
1354 1355
      await device.vmService.getFlutterViews();

1356 1357 1358
      // 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.
1359
      unawaited(device.vmService.service.onDone.then<void>(
1360 1361 1362
        _serviceProtocolDone,
        onError: _serviceProtocolError,
      ).whenComplete(_serviceDisconnected));
1363
    }
1364 1365
  }

1366
  Future<void> _serviceProtocolDone(dynamic object) async {
1367
    globals.printTrace('Service protocol connection closed.');
1368 1369
  }

1370
  Future<void> _serviceProtocolError(dynamic error, StackTrace stack) {
1371
    globals.printTrace('Service protocol connection closed with an error: $error\n$stack');
1372
    return Future<void>.error(error, stack);
1373 1374
  }

1375
  void _serviceDisconnected() {
1376
    if (_exited) {
1377 1378 1379
      // User requested the application exit.
      return;
    }
1380
    if (_finished.isCompleted) {
1381
      return;
1382
    }
1383
    globals.printStatus('Lost connection to device.');
1384 1385 1386
    _finished.complete(0);
  }

1387
  void appFinished() {
1388
    if (_finished.isCompleted) {
1389
      return;
1390
    }
1391
    globals.printStatus('Application finished.');
1392 1393 1394
    _finished.complete(0);
  }

1395 1396 1397 1398 1399 1400
  void appFailedToStart() {
    if (!_finished.isCompleted) {
      _finished.complete(1);
    }
  }

1401
  Future<int> waitForAppToFinish() async {
1402
    final int exitCode = await _finished.future;
1403
    assert(exitCode != null);
1404 1405 1406 1407
    await cleanupAtFinish();
    return exitCode;
  }

1408 1409
  @mustCallSuper
  Future<void> preExit() async {
1410 1411
    // If _dillOutputPath is null, the tool created a temporary directory for
    // the dill.
1412 1413 1414 1415
    if (_dillOutputPath == null && artifactDirectory.existsSync()) {
      artifactDirectory.deleteSync(recursive: true);
    }
  }
1416

1417
  Future<void> exitApp() async {
1418
    final List<Future<void>> futures = <Future<void>>[
1419
      for (final FlutterDevice device in flutterDevices)  device.exitApps(),
1420
    ];
1421
    await Future.wait(futures);
1422 1423 1424
    appFinished();
  }

1425
  bool get reportedDebuggers => _reportedDebuggers;
1426 1427 1428
  bool _reportedDebuggers = false;

  void printDebuggerList({ bool includeObservatory = true, bool includeDevtools = true }) {
1429
    final DevToolsServerAddress devToolsServerAddress = residentDevtoolsHandler.activeDevToolsServer;
1430
    if (!residentDevtoolsHandler.readyToAnnounce) {
1431 1432
      includeDevtools = false;
    }
1433
    assert(!includeDevtools || devToolsServerAddress != null);
1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459
    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 '
            'on ${device.device.name} is available at: $uri',
          );
        }
      }
    }
    _reportedDebuggers = true;
  }

1460
  void printHelpDetails() {
1461
    commandHelp.v.print();
1462
    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) {
1463
      commandHelp.s.print();
1464
    }
1465
    if (supportsServiceProtocol) {
1466 1467
      commandHelp.w.print();
      commandHelp.t.print();
1468
      if (isRunningDebug) {
1469 1470 1471 1472 1473
        commandHelp.L.print();
        commandHelp.S.print();
        commandHelp.U.print();
        commandHelp.i.print();
        commandHelp.p.print();
1474
        commandHelp.I.print();
1475
        commandHelp.o.print();
1476
        commandHelp.b.print();
1477
        commandHelp.z.print();
1478
      } else {
1479 1480
        commandHelp.S.print();
        commandHelp.U.print();
1481
      }
1482 1483 1484
      // Performance related features: `P` should precede `a`, which should precede `M`.
      commandHelp.P.print();
      commandHelp.a.print();
1485 1486 1487
      if (supportsWriteSkSL) {
        commandHelp.M.print();
      }
1488 1489 1490
      if (isRunningDebug) {
        commandHelp.g.print();
      }
1491
    }
1492 1493
  }

1494
  @override
1495
  Future<void> cleanupAfterSignal();
1496

1497
  /// Called right before we exit.
1498
  Future<void> cleanupAtFinish();
1499
}
1500

Devon Carew's avatar
Devon Carew committed
1501
class OperationResult {
1502
  OperationResult(this.code, this.message, { this.fatal = false });
Devon Carew's avatar
Devon Carew committed
1503

1504
  /// The result of the operation; a non-zero code indicates a failure.
Devon Carew's avatar
Devon Carew committed
1505
  final int code;
1506 1507

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

1510 1511 1512
  /// Whether this error should cause the runner to exit.
  final bool fatal;

Devon Carew's avatar
Devon Carew committed
1513
  bool get isOk => code == 0;
1514

1515
  static final OperationResult ok = OperationResult(0, '');
Devon Carew's avatar
Devon Carew committed
1516 1517
}

1518
Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async {
1519 1520
  switch (platform) {
    case TargetPlatform.android_arm:
1521
    case TargetPlatform.android_arm64:
1522
    case TargetPlatform.android_x64:
1523
    case TargetPlatform.android_x86:
1524
      final FlutterProject project = FlutterProject.current();
1525
      final String manifestPath = globals.fs.path.relative(project.android.appManifestFile.path);
1526
      return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
1527 1528 1529 1530 1531 1532
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
    default:
      return null;
  }
}
1533

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

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

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

1562 1563 1564
  @visibleForTesting
  String lastReceivedCommand;

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

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

1578 1579 1580
  final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{};

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

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

  /// Unregisters terminal signal and keystroke handlers.
  void stop() {
    assert(residentRunner.stayResident);
1601 1602 1603 1604 1605 1606 1607 1608 1609
    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;
    }
1610
    for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
1611
      _signals.removeHandler(entry.key, entry.value);
1612 1613 1614
    }
    _signalTokens.clear();
    subscription.cancel();
1615 1616 1617 1618
  }

  /// Returns [true] if the input has been handled by this function.
  Future<bool> _commonTerminalInputHandler(String character) async {
1619 1620
    _logger.printStatus(''); // the key the user tapped might be on this line
    switch (character) {
1621
      case 'a':
1622
        return residentRunner.debugToggleProfileWidgetBuilds();
1623
      case 'b':
1624
        return residentRunner.debugToggleBrightness();
1625
      case 'c':
1626
        _logger.clear();
1627
        return true;
1628 1629 1630 1631
      case 'd':
      case 'D':
        await residentRunner.detach();
        return true;
1632 1633 1634
      case 'g':
        await residentRunner.runSourceGenerators();
        return true;
1635 1636 1637 1638 1639 1640 1641
      case 'h':
      case 'H':
      case '?':
        // help
        residentRunner.printHelp(details: true);
        return true;
      case 'i':
1642
        return residentRunner.debugToggleWidgetInspector();
1643
      case 'I':
1644
        return residentRunner.debugToggleInvertOversizedImages();
1645
      case 'L':
1646
        return residentRunner.debugDumpLayerTree();
1647 1648
      case 'o':
      case 'O':
1649
        return residentRunner.debugTogglePlatform();
1650 1651 1652 1653 1654 1655
      case 'M':
        if (residentRunner.supportsWriteSkSL) {
          await residentRunner.writeSkSL();
          return true;
        }
        return false;
1656
      case 'p':
1657
        return residentRunner.debugToggleDebugPaintSizeEnabled();
1658
      case 'P':
1659
        return residentRunner.debugTogglePerformanceOverlayOverride();
1660 1661 1662 1663 1664
      case 'q':
      case 'Q':
        // exit
        await residentRunner.exit();
        return true;
1665 1666 1667 1668 1669
      case 'r':
        if (!residentRunner.canHotReload) {
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: false);
1670 1671 1672
        if (result.fatal) {
          throwToolExit(result.message);
        }
1673
        if (!result.isOk) {
1674
          _logger.printStatus('Try again after fixing the above error(s).', emphasis: true);
1675 1676 1677 1678
        }
        return true;
      case 'R':
        // If hot restart is not supported for all devices, ignore the command.
1679
        if (!residentRunner.supportsRestart || !residentRunner.hotMode) {
1680 1681 1682
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: true);
1683 1684 1685
        if (result.fatal) {
          throwToolExit(result.message);
        }
1686
        if (!result.isOk) {
1687
          _logger.printStatus('Try again after fixing the above error(s).', emphasis: true);
1688 1689
        }
        return true;
1690 1691
      case 's':
        for (final FlutterDevice device in residentRunner.flutterDevices) {
1692
          await residentRunner.screenshot(device);
1693 1694
        }
        return true;
1695
      case 'S':
1696
        return residentRunner.debugDumpSemanticsTreeInTraversalOrder();
1697 1698
      case 't':
      case 'T':
1699
        return residentRunner.debugDumpRenderTree();
1700
      case 'U':
1701
        return residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
1702 1703 1704
      case 'v':
      case 'V':
        return residentRunner.residentDevtoolsHandler.launchDevToolsInBrowser(flutterDevices: residentRunner.flutterDevices);
1705 1706
      case 'w':
      case 'W':
1707
        return residentRunner.debugDumpApp();
1708 1709
      case 'z':
      case 'Z':
1710
        return residentRunner.debugToggleDebugCheckElevationsEnabled();
1711 1712 1713 1714 1715 1716 1717 1718
    }
    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) {
1719
      _logger.printTrace('Ignoring terminal input: "$command" because we are busy.');
1720 1721 1722 1723
      return;
    }
    _processingUserRequest = true;
    try {
1724 1725
      lastReceivedCommand = command;
      await _commonTerminalInputHandler(command);
1726 1727
    // Catch all exception since this is doing cleanup and rethrowing.
    } catch (error, st) { // ignore: avoid_catches_without_on_clauses
1728 1729
      // Don't print stack traces for known error types.
      if (error is! ToolExit) {
1730
        _logger.printError('$error\n$st');
1731
      }
1732 1733
      await _cleanUp(null);
      rethrow;
1734 1735
    } finally {
      _processingUserRequest = false;
1736 1737 1738
      if (_reportReady) {
        _logger.printStatus('ready');
      }
1739 1740 1741 1742 1743
    }
  }

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

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

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

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

1765
class DebugConnectionInfo {
1766
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
1767

1768 1769
  final Uri httpUri;
  final Uri wsUri;
1770 1771
  final String baseUri;
}
1772 1773 1774 1775

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

1793 1794
/// A launcher for the devtools debugger and analysis tool.
abstract class DevtoolsLauncher {
1795 1796 1797 1798 1799 1800 1801
  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();
1802

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

1813
  Future<void> close();
1814

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

1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838
  /// 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>();
    }
  }
1839

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

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

  final String host;
  final int port;
1856 1857 1858 1859 1860 1861 1862

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