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

import 'dart:async';

7
import 'package:devtools_server/devtools_server.dart' as devtools_server;
8
import 'package:meta/meta.dart';
9
import 'package:package_config/package_config.dart';
10
import 'package:vm_service/vm_service.dart' as vm_service;
11

12
import 'application_package.dart';
13
import 'artifacts.dart';
14
import 'asset.dart';
15
import 'base/command_help.dart';
16
import 'base/common.dart';
17
import 'base/file_system.dart';
18
import 'base/io.dart' as io;
19
import 'base/logger.dart';
20
import 'base/signals.dart';
21
import 'base/utils.dart';
22
import 'build_info.dart';
23 24
import 'build_system/build_system.dart';
import 'build_system/targets/localizations.dart';
25
import 'bundle.dart';
26
import 'cache.dart';
27
import 'compile.dart';
28
import 'devfs.dart';
29
import 'device.dart';
30
import 'features.dart';
31
import 'globals.dart' as globals;
32
import 'project.dart';
33 34
import 'run_cold.dart';
import 'run_hot.dart';
35
import 'vmservice.dart';
36
import 'widget_cache.dart';
37

38
class FlutterDevice {
39 40
  FlutterDevice(
    this.device, {
41
    @required this.buildInfo,
42 43
    this.fileSystemRoots,
    this.fileSystemScheme,
44
    this.viewFilter,
45
    TargetModel targetModel = TargetModel.flutter,
46
    TargetPlatform targetPlatform,
47
    ResidentCompiler generator,
48
    this.userIdentifier,
49
    this.widgetCache,
50
  }) : assert(buildInfo.trackWidgetCreation != null),
51
       generator = generator ?? ResidentCompiler(
52
         globals.artifacts.getArtifactPath(
53 54
           Artifact.flutterPatchedSdkPath,
           platform: targetPlatform,
55
           mode: buildInfo.mode,
56
         ),
57 58
         buildMode: buildInfo.mode,
         trackWidgetCreation: buildInfo.trackWidgetCreation,
59
         fileSystemRoots: fileSystemRoots ?? <String>[],
60
         fileSystemScheme: fileSystemScheme,
61
         targetModel: targetModel,
62
         dartDefines: buildInfo.dartDefines,
63
         packagesPath: buildInfo.packagesPath,
64
         extraFrontEndOptions: buildInfo.extraFrontEndOptions,
65 66 67
         artifacts: globals.artifacts,
         processManager: globals.processManager,
         logger: globals.logger,
68 69
       );

70
  /// Create a [FlutterDevice] with optional code generation enabled.
71 72
  static Future<FlutterDevice> create(
    Device device, {
73
    @required FlutterProject flutterProject,
74
    @required String target,
75
    @required BuildInfo buildInfo,
76 77 78 79 80 81
    List<String> fileSystemRoots,
    String fileSystemScheme,
    String viewFilter,
    TargetModel targetModel = TargetModel.flutter,
    List<String> experimentalFlags,
    ResidentCompiler generator,
82
    String userIdentifier,
83
    WidgetCache widgetCache,
84 85
  }) async {
    ResidentCompiler generator;
86 87 88 89
    final TargetPlatform targetPlatform = await device.targetPlatform;
    if (device.platformType == PlatformType.fuchsia) {
      targetModel = TargetModel.flutterRunner;
    }
90 91 92 93 94 95
    // 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.
96
    if (targetPlatform == TargetPlatform.web_javascript) {
97 98 99 100 101 102 103 104 105 106 107 108 109 110
      Artifact platformDillArtifact;
      List<String> extraFrontEndOptions;
      if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
        platformDillArtifact = Artifact.webPlatformKernelDill;
        extraFrontEndOptions = buildInfo.extraFrontEndOptions;
      } else {
        platformDillArtifact = Artifact.webPlatformSoundKernelDill;
        extraFrontEndOptions = <String>[
          ...?buildInfo?.extraFrontEndOptions,
          if (!(buildInfo?.extraFrontEndOptions?.contains('--sound-null-safety') ?? false))
            '--sound-null-safety'
        ];
      }

111
      generator = ResidentCompiler(
112 113 114
        globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: buildInfo.mode),
        buildMode: buildInfo.mode,
        trackWidgetCreation: buildInfo.trackWidgetCreation,
115 116 117 118
        fileSystemRoots: fileSystemRoots ?? <String>[],
        // Override the filesystem scheme so that the frontend_server can find
        // the generated entrypoint code.
        fileSystemScheme: 'org-dartlang-app',
119 120
        initializeFromDill: getDefaultCachedKernelPath(
          trackWidgetCreation: buildInfo.trackWidgetCreation,
121
          dartDefines: buildInfo.dartDefines,
122
          extraFrontEndOptions: extraFrontEndOptions
123
        ),
124
        targetModel: TargetModel.dartdevc,
125
        extraFrontEndOptions: extraFrontEndOptions,
126
        platformDill: globals.fs.file(globals.artifacts
127
          .getArtifactPath(platformDillArtifact, mode: buildInfo.mode))
128
          .absolute.uri.toString(),
129
        dartDefines: buildInfo.dartDefines,
130
        librariesSpec: globals.fs.file(globals.artifacts
131
          .getArtifactPath(Artifact.flutterWebLibrariesJson)).uri.toString(),
132
        packagesPath: buildInfo.packagesPath,
133 134 135
        artifacts: globals.artifacts,
        processManager: globals.processManager,
        logger: globals.logger,
136
      );
137 138
    } else {
      generator = ResidentCompiler(
139
        globals.artifacts.getArtifactPath(
140 141
          Artifact.flutterPatchedSdkPath,
          platform: targetPlatform,
142
          mode: buildInfo.mode,
143
        ),
144 145
        buildMode: buildInfo.mode,
        trackWidgetCreation: buildInfo.trackWidgetCreation,
146 147 148
        fileSystemRoots: fileSystemRoots,
        fileSystemScheme: fileSystemScheme,
        targetModel: targetModel,
149
        dartDefines: buildInfo.dartDefines,
150
        extraFrontEndOptions: buildInfo.extraFrontEndOptions,
151 152
        initializeFromDill: getDefaultCachedKernelPath(
          trackWidgetCreation: buildInfo.trackWidgetCreation,
153
          dartDefines: buildInfo.dartDefines,
154
          extraFrontEndOptions: buildInfo.extraFrontEndOptions,
155
        ),
156
        packagesPath: buildInfo.packagesPath,
157 158 159
        artifacts: globals.artifacts,
        processManager: globals.processManager,
        logger: globals.logger,
160 161
      );
    }
162

163 164 165 166 167 168
    return FlutterDevice(
      device,
      fileSystemRoots: fileSystemRoots,
      fileSystemScheme:fileSystemScheme,
      viewFilter: viewFilter,
      targetModel: targetModel,
169
      targetPlatform: targetPlatform,
170
      generator: generator,
171
      buildInfo: buildInfo,
172
      userIdentifier: userIdentifier,
173
      widgetCache: widgetCache,
174 175 176
    );
  }

177
  final Device device;
178
  final ResidentCompiler generator;
179
  final BuildInfo buildInfo;
180
  final String userIdentifier;
181
  final WidgetCache widgetCache;
182
  Stream<Uri> observatoryUris;
183
  vm_service.VmService vmService;
184 185
  DevFS devFS;
  ApplicationPackage package;
186 187
  List<String> fileSystemRoots;
  String fileSystemScheme;
188
  StreamSubscription<String> _loggingSubscription;
189
  bool _isListeningForObservatoryUri;
190
  final String viewFilter;
191

192 193 194
  /// Whether the stream [observatoryUris] is still open.
  bool get isWaitingForObservatory => _isListeningForObservatoryUri ?? false;

195 196 197 198 199
  /// 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).
200 201
  /// The 'compileExpression' service can be used to compile user-provided
  /// expressions requested during debugging of the application.
202 203
  /// This ensures that the reload process follows the normal orchestration of
  /// the Flutter Tools and not just the VM internal service.
204
  Future<void> connect({
205 206 207
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
208
    ReloadMethod reloadMethod,
209
    GetSkSLMethod getSkSLMethod,
210
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
211 212 213 214 215 216 217
  }) {
    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.
218
      globals.printTrace('Connecting to service protocol: $observatoryUri');
219
      isWaitingForVm = true;
220
      vm_service.VmService service;
221

222
      try {
223
        service = await connectToVmService(
224 225 226 227
          observatoryUri,
          reloadSources: reloadSources,
          restart: restart,
          compileExpression: compileExpression,
228
          reloadMethod: reloadMethod,
229
          getSkSLMethod: getSkSLMethod,
230
          printStructuredErrorLogMethod: printStructuredErrorLogMethod,
231
          device: device,
232 233
        );
      } on Exception catch (exception) {
234
        globals.printTrace('Fail to connect to service protocol: $observatoryUri: $exception');
235 236 237 238 239 240 241 242
        if (!completer.isCompleted && !_isListeningForObservatoryUri) {
          completer.completeError('failed to connect to $observatoryUri');
        }
        return;
      }
      if (completer.isCompleted) {
        return;
      }
243
      globals.printTrace('Successfully connected to service protocol: $observatoryUri');
244

245
      vmService = service;
246
      (await device.getLogReader(app: package)).connectedVMService = vmService;
247 248 249
      completer.complete();
      await subscription.cancel();
    }, onError: (dynamic error) {
250
      globals.printTrace('Fail to handle observatory URI: $error');
251 252 253 254 255 256 257 258
    }, onDone: () {
      _isListeningForObservatoryUri = false;
      if (!completer.isCompleted && !isWaitingForVm) {
        completer.completeError('connection to device ended too early');
      }
    });
    _isListeningForObservatoryUri = true;
    return completer.future;
259 260
  }

261 262 263
  Future<void> exitApps({
    @visibleForTesting Duration timeoutDelay = const Duration(seconds: 10),
  }) async {
264
    if (!device.supportsFlutterExit || vmService == null) {
265
      return device.stopApp(package, userIdentifier: userIdentifier);
266
    }
267
    final List<FlutterView> views = await vmService.getFlutterViews();
268
    if (views == null || views.isEmpty) {
269
      return device.stopApp(package, userIdentifier: userIdentifier);
270
    }
271 272
    // 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.
273 274 275 276 277 278 279
    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)) {
280
        return device.stopApp(package, userIdentifier: userIdentifier);
281 282
      }
    }
283
    for (final FlutterView view in views) {
284
      if (view != null && view.uiIsolate != null) {
285 286
        // If successful, there will be no response from flutterExit.
        unawaited(vmService.flutterExit(
287 288
          isolateId: view.uiIsolate.id,
        ));
289
      }
290
    }
291 292 293 294 295 296 297
    return vmService.onDone
      .catchError((dynamic error, StackTrace stackTrace) {
        globals.logger.printError(
          'unhanlded error waiting for vm service exit:\n $error',
          stackTrace: stackTrace,
         );
      })
298
      .timeout(timeoutDelay, onTimeout: () {
299 300 301 302
        // TODO(jonahwilliams): this only seems to fail on CI in the
        // flutter_attach_android_test. This log should help verify this
        // is where the tool is getting stuck.
        globals.logger.printTrace('error: vm service shutdown failed');
303
        return device.stopApp(package, userIdentifier: userIdentifier);
304
      });
305 306
  }

307 308
  Future<Uri> setupDevFS(
    String fsName,
309
    Directory rootDirectory, {
310
    String packagesFilePath,
311
  }) {
312
    // One devFS per device. Shared by all running instances.
313
    devFS = DevFS(
314
      vmService,
315 316
      fsName,
      rootDirectory,
317
      osUtils: globals.os,
318 319
      fileSystem: globals.fs,
      logger: globals.logger,
320 321 322 323
    );
    return devFS.create();
  }

324
  Future<List<Future<vm_service.ReloadReport>>> reloadSources(
325
    String entryPath, {
326
    bool pause = false,
327
  }) async {
328 329
    final String deviceEntryUri = devFS.baseUri
      .resolveUri(globals.fs.path.toUri(entryPath)).toString();
330
    final vm_service.VM vm = await vmService.getVM();
331
    return <Future<vm_service.ReloadReport>>[
332
      for (final vm_service.IsolateRef isolateRef in vm.isolates)
333
        vmService.reloadSources(
334
          isolateRef.id,
335 336
          pause: pause,
          rootLibUri: deviceEntryUri,
337
        )
338
    ];
339 340
  }

341
  Future<void> resetAssetDirectory() async {
342
    final Uri deviceAssetsDirectoryUri = devFS.baseUri.resolveUri(
343
        globals.fs.path.toUri(getAssetBuildDirectory()));
344
    assert(deviceAssetsDirectoryUri != null);
345
    final List<FlutterView> views = await vmService.getFlutterViews();
346
    await Future.wait<void>(views.map<Future<void>>(
347 348 349 350 351
      (FlutterView view) => vmService.setAssetDirectory(
        assetsDirectory: deviceAssetsDirectoryUri,
        uiIsolateId: view.uiIsolate.id,
        viewId: view.id,
      )
352 353 354
    ));
  }

355
  Future<void> debugDumpApp() async {
356
    final List<FlutterView> views = await vmService.getFlutterViews();
357
    for (final FlutterView view in views) {
358 359 360
      await vmService.flutterDebugDumpApp(
        isolateId: view.uiIsolate.id,
      );
361
    }
362 363
  }

364
  Future<void> debugDumpRenderTree() async {
365
    final List<FlutterView> views = await vmService.getFlutterViews();
366
    for (final FlutterView view in views) {
367 368 369
      await vmService.flutterDebugDumpRenderTree(
        isolateId: view.uiIsolate.id,
      );
370
    }
371 372
  }

373
  Future<void> debugDumpLayerTree() async {
374
    final List<FlutterView> views = await vmService.getFlutterViews();
375
    for (final FlutterView view in views) {
376 377 378
      await vmService.flutterDebugDumpLayerTree(
        isolateId: view.uiIsolate.id,
      );
379
    }
380 381
  }

382
  Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
383
    final List<FlutterView> views = await vmService.getFlutterViews();
384
    for (final FlutterView view in views) {
385 386 387
      await vmService.flutterDebugDumpSemanticsTreeInTraversalOrder(
        isolateId: view.uiIsolate.id,
      );
388
    }
389 390
  }

391
  Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
392
    final List<FlutterView> views = await vmService.getFlutterViews();
393
    for (final FlutterView view in views) {
394 395 396
      await vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
        isolateId: view.uiIsolate.id,
      );
397
    }
398 399
  }

400
  Future<void> toggleDebugPaintSizeEnabled() async {
401
    final List<FlutterView> views = await vmService.getFlutterViews();
402
    for (final FlutterView view in views) {
403 404 405
      await vmService.flutterToggleDebugPaintSizeEnabled(
        isolateId: view.uiIsolate.id,
      );
406
    }
407 408
  }

409
  Future<void> toggleDebugCheckElevationsEnabled() async {
410
    final List<FlutterView> views = await vmService.getFlutterViews();
411
    for (final FlutterView view in views) {
412 413 414
      await vmService.flutterToggleDebugCheckElevationsEnabled(
        isolateId: view.uiIsolate.id,
      );
415
    }
416 417
  }

418
  Future<void> debugTogglePerformanceOverlayOverride() async {
419
    final List<FlutterView> views = await vmService.getFlutterViews();
420
    for (final FlutterView view in views) {
421 422 423
      await vmService.flutterTogglePerformanceOverlayOverride(
        isolateId: view.uiIsolate.id,
      );
424
    }
425 426
  }

427
  Future<void> toggleWidgetInspector() async {
428
    final List<FlutterView> views = await vmService.getFlutterViews();
429
    for (final FlutterView view in views) {
430 431 432
      await vmService.flutterToggleWidgetInspector(
        isolateId: view.uiIsolate.id,
      );
433
    }
434 435
  }

436 437 438 439 440 441 442 443 444
  Future<void> toggleInvertOversizedImages() async {
    final List<FlutterView> views = await vmService.getFlutterViews();
    for (final FlutterView view in views) {
      await vmService.flutterToggleInvertOversizedImages(
        isolateId: view.uiIsolate.id,
      );
    }
  }

445
  Future<void> toggleProfileWidgetBuilds() async {
446
    final List<FlutterView> views = await vmService.getFlutterViews();
447
    for (final FlutterView view in views) {
448 449 450
      await vmService.flutterToggleProfileWidgetBuilds(
        isolateId: view.uiIsolate.id,
      );
451 452 453
    }
  }

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
  Future<Brightness> toggleBrightness({ Brightness current }) async {
    final List<FlutterView> views = await vmService.getFlutterViews();
    Brightness next;
    if (current == Brightness.light) {
      next = Brightness.dark;
    } else if (current == Brightness.dark) {
      next = Brightness.light;
    }

    for (final FlutterView view in views) {
      next = await vmService.flutterBrightnessOverride(
        isolateId: view.uiIsolate.id,
        brightness: next,
      );
    }
    return next;
  }

472
  Future<String> togglePlatform({ String from }) async {
473
    final List<FlutterView> views = await vmService.getFlutterViews();
474
    final String to = nextPlatform(from, featureFlags);
475
    for (final FlutterView view in views) {
476 477 478 479
      await vmService.flutterPlatformOverride(
        platform: to,
        isolateId: view.uiIsolate.id,
      );
480
    }
481 482 483
    return to;
  }

484
  Future<void> startEchoingDeviceLog() async {
485
    if (_loggingSubscription != null) {
486
      return;
487
    }
488
    final Stream<String> logStream = (await device.getLogReader(app: package)).logLines;
489
    if (logStream == null) {
490
      globals.printError('Failed to read device log stream');
491 492 493
      return;
    }
    _loggingSubscription = logStream.listen((String line) {
494
      if (!line.contains('Observatory listening on http')) {
495
        globals.printStatus(line, wrap: false);
496
      }
497 498 499
    });
  }

500
  Future<void> stopEchoingDeviceLog() async {
501
    if (_loggingSubscription == null) {
502
      return;
503
    }
504 505 506 507
    await _loggingSubscription.cancel();
    _loggingSubscription = null;
  }

508
  Future<void> initLogReader() async {
509 510 511
    final vm_service.VM vm = await vmService.getVM();
    final DeviceLogReader logReader = await device.getLogReader(app: package);
    logReader.appPid = vm.pid;
512 513 514 515 516 517 518
  }

  Future<int> runHot({
    HotRunner hotRunner,
    String route,
  }) async {
    final bool prebuiltMode = hotRunner.applicationBinary != null;
519
    final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName;
520
    globals.printStatus(
521
      'Launching ${globals.fsUtils.getDisplayPath(hotRunner.mainPath)} '
522 523
      'on ${device.name} in $modeName mode...',
    );
524 525

    final TargetPlatform targetPlatform = await device.targetPlatform;
526
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
527
      targetPlatform,
528
      buildInfo: hotRunner.debuggingOptions.buildInfo,
529
      applicationBinary: hotRunner.applicationBinary,
530 531 532 533
    );

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

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

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

    // Start the application.
    final Future<LaunchResult> futureResult = device.startApp(
      package,
      mainPath: hotRunner.mainPath,
      debuggingOptions: hotRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
554
      ipv6: hotRunner.ipv6,
555
      userIdentifier: userIdentifier,
556 557 558 559 560
    );

    final LaunchResult result = await futureResult;

    if (!result.started) {
561
      globals.printError('Error launching application on ${device.name}.');
562 563 564
      await stopEchoingDeviceLog();
      return 2;
    }
565
    if (result.hasObservatory) {
566 567 568
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
569
    } else {
570 571 572
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
573
    }
574 575 576 577 578 579 580 581 582
    return 0;
  }


  Future<int> runCold({
    ColdRunner coldRunner,
    String route,
  }) async {
    final TargetPlatform targetPlatform = await device.targetPlatform;
583
    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
584
      targetPlatform,
585
      buildInfo: coldRunner.debuggingOptions.buildInfo,
586
      applicationBinary: coldRunner.applicationBinary,
587 588
    );

589
    final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName;
590 591 592
    final bool prebuiltMode = coldRunner.applicationBinary != null;
    if (coldRunner.mainPath == null) {
      assert(prebuiltMode);
593 594 595 596
      globals.printStatus(
        'Launching ${package.displayName} '
        'on ${device.name} in $modeName mode...',
      );
597
    } else {
598
      globals.printStatus(
599
        'Launching ${globals.fsUtils.getDisplayPath(coldRunner.mainPath)} '
600 601
        'on ${device.name} in $modeName mode...',
      );
602 603 604 605
    }

    if (package == null) {
      String message = 'No application found for $targetPlatform.';
606
      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
607
      if (hint != null) {
608
        message += '\n$hint';
609
      }
610
      globals.printError(message);
611 612 613
      return 1;
    }

614
    final Map<String, dynamic> platformArgs = <String, dynamic>{};
615
    if (coldRunner.traceStartup != null) {
616
      platformArgs['trace-startup'] = coldRunner.traceStartup;
617
    }
618

619
    await startEchoingDeviceLog();
620 621 622 623 624 625 626 627

    final LaunchResult result = await device.startApp(
      package,
      mainPath: coldRunner.mainPath,
      debuggingOptions: coldRunner.debuggingOptions,
      platformArgs: platformArgs,
      route: route,
      prebuiltApplication: prebuiltMode,
628
      ipv6: coldRunner.ipv6,
629
      userIdentifier: userIdentifier,
630 631 632
    );

    if (!result.started) {
633
      globals.printError('Error running application on ${device.name}.');
634 635 636
      await stopEchoingDeviceLog();
      return 2;
    }
637
    if (result.hasObservatory) {
638 639 640
      observatoryUris = Stream<Uri>
        .value(result.observatoryUri)
        .asBroadcastStream();
641
    } else {
642 643 644
      observatoryUris = const Stream<Uri>
        .empty()
        .asBroadcastStream();
645
    }
646 647 648
    return 0;
  }

649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
  /// Validates whether this hot reload is a candidate for a fast reassemble.
  Future<bool> _attemptFastReassembleCheck(List<Uri> invalidatedFiles, PackageConfig packageConfig) async {
    if (invalidatedFiles.length != 1 || widgetCache == null) {
      return false;
    }
    final List<FlutterView> views = await vmService.getFlutterViews();
    final String widgetName = await widgetCache?.validateLibrary(invalidatedFiles.single);
    if (widgetName == null) {
      return false;
    }
    final String packageUri = packageConfig.toPackageUri(invalidatedFiles.single)?.toString()
      ?? invalidatedFiles.single.toString();
    for (final FlutterView view in views) {
      final vm_service.Isolate isolate = await vmService.getIsolateOrNull(view.uiIsolate.id);
      final vm_service.LibraryRef targetLibrary = isolate.libraries
        .firstWhere(
          (vm_service.LibraryRef libraryRef) => libraryRef.uri == packageUri,
          orElse: () => null,
        );
      if (targetLibrary == null) {
        return false;
      }
      try {
        // Evaluate an expression to allow type checking for that invalidated widget
        // name. For more information, see `debugFastReassembleMethod` in flutter/src/widgets/binding.dart
        await vmService.evaluate(
          view.uiIsolate.id,
          targetLibrary.id,
          '((){debugFastReassembleMethod=(Object _fastReassembleParam) => _fastReassembleParam is $widgetName})()',
        );
      } on Exception catch (err) {
        globals.printTrace(err.toString());
        return false;
      }
    }
    return true;
  }

687
  Future<UpdateFSReport> updateDevFS({
688
    Uri mainUri,
689
    String target,
690
    AssetBundle bundle,
691
    DateTime firstBuildTime,
692 693 694
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
695
    String projectRootPath,
696
    String pathToReload,
697
    @required String dillOutputPath,
698
    @required List<Uri> invalidatedFiles,
699
    @required PackageConfig packageConfig,
700
  }) async {
701
    final Status devFSStatus = globals.logger.startProgress(
702
      'Syncing files to device ${device.name}...',
703
      timeout: timeoutConfiguration.fastOperation,
704
    );
705
    UpdateFSReport report;
706
    bool fastReassemble = false;
707
    try {
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
      await Future.wait(<Future<void>>[
        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,
        ).then((UpdateFSReport newReport) => report = newReport),
        if (!fullRestart)
          _attemptFastReassembleCheck(
            invalidatedFiles,
            packageConfig,
          ).then((bool newFastReassemble) => fastReassemble = newFastReassemble)
      ]);
      if (fastReassemble) {
        globals.logger.printTrace('Attempting fast reassemble.');
      }
      report.fastReassemble = fastReassemble;
734 735
    } on DevFSException {
      devFSStatus.cancel();
736
      return UpdateFSReport(success: false);
737 738
    }
    devFSStatus.stop();
739
    globals.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
740
    return report;
741
  }
742

743
  Future<void> updateReloadStatus(bool wasReloadSuccessful) async {
744
    if (wasReloadSuccessful) {
745
      generator?.accept();
746
    } else {
747
      await generator?.reject();
748
    }
749
  }
750 751
}

752 753
// Shared code between different resident application runners.
abstract class ResidentRunner {
754 755
  ResidentRunner(
    this.flutterDevices, {
756
    this.target,
757
    @required this.debuggingOptions,
758
    String projectRootPath,
759
    this.ipv6,
760 761
    this.stayResident = true,
    this.hotMode = true,
762
    String dillOutputPath,
763
    this.machine = false,
764
  }) : mainPath = findMainDartFile(target),
765
       packagesFilePath = debuggingOptions.buildInfo.packagesPath,
766
       projectRootPath = projectRootPath ?? globals.fs.currentDirectory.path,
767 768
       _dillOutputPath = dillOutputPath,
       artifactDirectory = dillOutputPath == null
769 770
          ? globals.fs.systemTempDirectory.createTempSync('flutter_tool.')
          : globals.fs.file(dillOutputPath).parent,
771 772 773 774 775
       assetBundle = AssetBundleFactory.instance.createBundle(),
       commandHelp = CommandHelp(
         logger: globals.logger,
         terminal: globals.terminal,
         platform: globals.platform,
776
         outputPreferences: globals.outputPreferences,
777
       ) {
778 779
    if (!artifactDirectory.existsSync()) {
      artifactDirectory.createSync(recursive: true);
780
    }
781
  }
782

783 784
  @protected
  @visibleForTesting
785
  final List<FlutterDevice> flutterDevices;
786

787 788
  final String target;
  final DebuggingOptions debuggingOptions;
789
  final bool stayResident;
790
  final bool ipv6;
791 792 793
  final String _dillOutputPath;
  /// The parent location of the incremental artifacts.
  final Directory artifactDirectory;
794 795 796 797 798
  final String packagesFilePath;
  final String projectRootPath;
  final String mainPath;
  final AssetBundle assetBundle;

799
  final CommandHelp commandHelp;
800
  final bool machine;
801

802 803
  io.HttpServer _devtoolsServer;

804
  bool _exited = false;
805 806 807
  Completer<int> _finished = Completer<int>();
  bool hotMode;

808 809 810 811 812
  /// Whether the compiler was instructed to run with null-safety enabled.
  @protected
  bool get usageNullSafety => debuggingOptions?.buildInfo
    ?.extraFrontEndOptions?.any((String option) => option.contains('non-nullable')) ?? false;

813 814 815 816 817 818
  /// Returns true if every device is streaming observatory URIs.
  bool get isWaitingForObservatory {
    return flutterDevices.every((FlutterDevice device) {
      return device.isWaitingForObservatory;
    });
  }
819

820
  String get dillOutputPath => _dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill');
821
  String getReloadPath({ bool fullRestart }) => mainPath + (fullRestart ? '' : '.incremental') + '.dill';
822

823
  bool get debuggingEnabled => debuggingOptions.debuggingEnabled;
824 825 826
  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
827
  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
828
  bool get supportsCanvasKit => false;
829
  bool get supportsWriteSkSL => supportsServiceProtocol;
830
  bool get trackWidgetCreation => debuggingOptions.buildInfo.trackWidgetCreation;
831

832 833 834 835 836 837 838
  // 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;

839 840 841
  /// Returns [true] if the resident runner exited after invoking [exit()].
  bool get exited => _exited;

842 843 844 845 846 847 848 849 850 851 852
  /// Whether this runner can 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 canHotRestart {
    return flutterDevices.every((FlutterDevice device) {
      return device.device.supportsHotRestart;
    });
  }

853 854 855 856 857 858
  /// Invoke an RPC extension method on the first attached ui isolate of the first device.
  // TODO(jonahwilliams): Update/Remove this method when refactoring the resident
  // runner to support a single flutter device.
  Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate(
    String method, {
    Map<String, dynamic> params,
859 860 861 862
  }) async {
    final List<FlutterView> views = await flutterDevices
      .first
      .vmService.getFlutterViews();
863 864 865 866 867 868
    return flutterDevices
      .first
      .vmService
      .invokeFlutterExtensionRpcRaw(
        method,
        args: params,
869
        isolateId: views
870 871
          .first.uiIsolate.id
      );
872 873
  }

874 875 876
  /// Whether this runner can hot reload.
  bool get canHotReload => hotMode;

877
  /// Start the app and keep the process running during its lifetime.
878 879 880
  ///
  /// 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.
881
  Future<int> run({
882
    Completer<DebugConnectionInfo> connectionInfoCompleter,
883
    Completer<void> appStartedCompleter,
884 885
    String route,
  });
886

887 888 889 890 891
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
  });

892 893
  bool get supportsRestart => false;

894
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
895 896 897
    final String mode = isRunningProfile ? 'profile' :
        isRunningRelease ? 'release' : 'this';
    throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode';
898
  }
899

900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933

  BuildResult _lastBuild;
  Environment _environment;
  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,
      projectDir: globals.fs.currentDirectory,
    );
    globals.logger.printTrace('Starting incremental build...');
    _lastBuild = await globals.buildSystem.buildIncremental(
      const GenerateLocalizationsTarget(),
      _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');
  }

934 935
  /// Toggle whether canvaskit is being used for rendering, returning the new
  /// state.
936 937
  ///
  /// Only supported on the web.
938
  Future<bool> toggleCanvaskit() {
939 940 941
    throw Exception('Canvaskit not supported by this runner.');
  }

942 943
  /// List the attached flutter views.
  Future<List<FlutterView>> listFlutterViews() async {
944 945 946 947 948 949 950 951
    final List<List<FlutterView>> views = await Future.wait(<Future<List<FlutterView>>>[
      for (FlutterDevice device in flutterDevices)
        if (device.vmService != null)
          device.vmService.getFlutterViews()
    ]);
    return views
      .expand((List<FlutterView> viewList) => viewList)
      .toList();
952 953
  }

954
  /// Write the SkSL shaders to a zip file in build directory.
955 956 957
  ///
  /// Returns the name of the file, or `null` on failures.
  Future<String> writeSkSL() async {
958 959 960
    if (!supportsWriteSkSL) {
      throw Exception('writeSkSL is not supported by this runner.');
    }
961 962 963
    final List<FlutterView> views = await flutterDevices
      .first
      .vmService.getFlutterViews();
964
    final Map<String, Object> data = await flutterDevices.first.vmService.getSkSLs(
965
      viewId: views.first.id,
966
    );
967
    final Device device = flutterDevices.first.device;
968
    return sharedSkSlWriter(device, data);
969 970
  }

971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986
  /// The resident runner API for interaction with the reloadMethod vmservice
  /// request.
  ///
  /// This API should only be called for UI only-changes spanning a single
  /// library/Widget.
  ///
  /// The value [classId] should be the identifier of the StatelessWidget that
  /// was invalidated, or the StatefulWidget for the corresponding State class
  /// that was invalidated. This must be provided.
  ///
  /// The value [libraryId] should be the absolute file URI for the containing
  /// library of the widget that was invalidated. This must be provided.
  Future<OperationResult> reloadMethod({ String classId, String libraryId }) {
    throw UnsupportedError('Method is not supported.');
  }

987 988 989 990
  @protected
  void writeVmserviceFile() {
    if (debuggingOptions.vmserviceOutFile != null) {
      try {
991
        final String address = flutterDevices.first.vmService.wsAddress.toString();
992
        final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile);
993 994 995
        vmserviceOutFile.createSync(recursive: true);
        vmserviceOutFile.writeAsStringSync(address);
      } on FileSystemException {
996
        globals.printError('Failed to write vmservice-out-file at ${debuggingOptions.vmserviceOutFile}');
997 998 999 1000
      }
    }
  }

1001 1002
  Future<void> exit() async {
    _exited = true;
1003
    await shutdownDevtools();
1004
    await stopEchoingDeviceLog();
1005 1006
    await preExit();
    await exitApp();
1007 1008
  }

1009
  Future<void> detach() async {
1010
    await shutdownDevtools();
1011
    await stopEchoingDeviceLog();
1012
    await preExit();
1013 1014 1015
    appFinished();
  }

1016
  Future<void> debugDumpApp() async {
1017
    for (final FlutterDevice device in flutterDevices) {
1018
      await device.debugDumpApp();
1019
    }
1020 1021
  }

1022
  Future<void> debugDumpRenderTree() async {
1023
    for (final FlutterDevice device in flutterDevices) {
1024
      await device.debugDumpRenderTree();
1025
    }
1026 1027
  }

1028
  Future<void> debugDumpLayerTree() async {
1029
    for (final FlutterDevice device in flutterDevices) {
1030
      await device.debugDumpLayerTree();
1031
    }
1032 1033
  }

1034
  Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
1035
    for (final FlutterDevice device in flutterDevices) {
1036
      await device.debugDumpSemanticsTreeInTraversalOrder();
1037
    }
1038 1039
  }

1040
  Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
1041
    for (final FlutterDevice device in flutterDevices) {
1042
      await device.debugDumpSemanticsTreeInInverseHitTestOrder();
1043
    }
1044 1045
  }

1046
  Future<void> debugToggleDebugPaintSizeEnabled() async {
1047
    for (final FlutterDevice device in flutterDevices) {
1048
      await device.toggleDebugPaintSizeEnabled();
1049
    }
1050 1051
  }

1052
  Future<void> debugToggleDebugCheckElevationsEnabled() async {
1053
    for (final FlutterDevice device in flutterDevices) {
1054
      await device.toggleDebugCheckElevationsEnabled();
1055
    }
1056 1057
  }

1058
  Future<void> debugTogglePerformanceOverlayOverride() async {
1059
    for (final FlutterDevice device in flutterDevices) {
1060
      await device.debugTogglePerformanceOverlayOverride();
1061
    }
1062 1063
  }

1064
  Future<void> debugToggleWidgetInspector() async {
1065
    for (final FlutterDevice device in flutterDevices) {
1066
      await device.toggleWidgetInspector();
1067
    }
1068 1069
  }

1070 1071 1072 1073 1074 1075
  Future<void> debugToggleInvertOversizedImages() async {
    for (final FlutterDevice device in flutterDevices) {
      await device.toggleInvertOversizedImages();
    }
  }

1076
  Future<void> debugToggleProfileWidgetBuilds() async {
1077
    for (final FlutterDevice device in flutterDevices) {
1078 1079 1080 1081
      await device.toggleProfileWidgetBuilds();
    }
  }

1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092
  Future<void> debugToggleBrightness() async {
    final Brightness brightness = await flutterDevices.first.toggleBrightness();
    Brightness next;
    for (final FlutterDevice device in flutterDevices) {
      next = await device.toggleBrightness(
        current: brightness,
      );
      globals.logger.printStatus('Changed brightness to $next.');
    }
  }

1093 1094 1095 1096 1097 1098
  /// 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.
  ///
  /// Throws an [AssertionError] if [Devce.supportsScreenshot] is not true.
1099
  Future<void> screenshot(FlutterDevice device) async {
1100
    assert(device.device.supportsScreenshot);
1101

1102 1103 1104 1105
    final Status status = globals.logger.startProgress(
      'Taking screenshot for ${device.device.name}...',
      timeout: timeoutConfiguration.fastOperation,
    );
1106
    final File outputFile = globals.fsUtils.getUniqueFile(
1107 1108 1109 1110
      globals.fs.currentDirectory,
      'flutter',
      'png',
    );
1111 1112
    final List<FlutterView> views = await device
      .vmService.getFlutterViews();
1113
    try {
1114 1115
      if (supportsServiceProtocol && isRunningDebug) {
        try {
1116
          for (final FlutterView view in views) {
1117 1118 1119 1120
            await device.vmService.flutterDebugAllowBanner(
              false,
              isolateId: view.uiIsolate.id,
            );
1121
          }
1122
        } on Exception catch (error) {
1123
          status.cancel();
1124
          globals.printError('Error communicating with Flutter on the device: $error');
1125
          return;
1126
        }
1127 1128
      }
      try {
1129
        await device.device.takeScreenshot(outputFile);
1130
      } finally {
1131 1132
        if (supportsServiceProtocol && isRunningDebug) {
          try {
1133
            for (final FlutterView view in views) {
1134 1135 1136 1137
              await device.vmService.flutterDebugAllowBanner(
                true,
                isolateId: view.uiIsolate.id,
              );
1138
            }
1139
          } on Exception catch (error) {
1140
            status.cancel();
1141
            globals.printError('Error communicating with Flutter on the device: $error');
1142
            return;
1143
          }
1144 1145
        }
      }
1146
      final int sizeKB = outputFile.lengthSync() ~/ 1024;
1147
      status.stop();
1148 1149 1150
      globals.printStatus(
        'Screenshot written to ${globals.fs.path.relative(outputFile.path)} (${sizeKB}kB).',
      );
1151
    } on Exception catch (error) {
1152
      status.cancel();
1153
      globals.printError('Error taking screenshot: $error');
1154 1155 1156
    }
  }

1157
  Future<void> debugTogglePlatform() async {
1158 1159 1160 1161
    final List<FlutterView> views = await flutterDevices
      .first
      .vmService.getFlutterViews();
    final String isolateId = views.first.uiIsolate.id;
1162 1163 1164 1165
    final String from = await flutterDevices
      .first.vmService.flutterPlatformOverride(
        isolateId: isolateId,
      );
1166
    String to;
1167
    for (final FlutterDevice device in flutterDevices) {
1168
      to = await device.togglePlatform(from: from);
1169
    }
1170
    globals.printStatus('Switched operating system to $to');
1171 1172
  }

1173 1174 1175
  Future<void> stopEchoingDeviceLog() async {
    await Future.wait<void>(
      flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
1176
    );
1177 1178
  }

1179 1180 1181 1182 1183 1184 1185 1186 1187 1188
  @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,
1189
        dartDefines: debuggingOptions.buildInfo.dartDefines,
1190
        extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions,
1191
      );
1192 1193 1194 1195
      globals.fs
          .file(copyPath)
          .parent
          .createSync(recursive: true);
1196 1197 1198 1199
      outputDill.copySync(copyPath);
    }
  }

1200
  void printStructuredErrorLog(vm_service.Event event) {
1201
    if (event.extensionKind == 'Flutter.Error' && !machine) {
1202 1203 1204 1205 1206 1207 1208
      final Map<dynamic, dynamic> json = event.extensionData?.data;
      if (json != null && json.containsKey('renderedErrorText')) {
        globals.printStatus('\n${json['renderedErrorText']}');
      }
    }
  }

1209
  /// If the [reloadSources] parameter is not null the 'reloadSources' service
1210 1211 1212 1213 1214
  /// 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.
1215 1216 1217 1218
  Future<void> connectToServiceProtocol({
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
1219
    ReloadMethod reloadMethod,
1220
    GetSkSLMethod getSkSLMethod,
1221
  }) async {
1222
    if (!debuggingOptions.debuggingEnabled) {
1223
      throw 'The service protocol is not enabled.';
1224
    }
1225
    _finished = Completer<int>();
1226
    // Listen for service protocol connection to close.
1227
    for (final FlutterDevice device in flutterDevices) {
1228
      await device.connect(
1229 1230 1231
        reloadSources: reloadSources,
        restart: restart,
        compileExpression: compileExpression,
1232
        reloadMethod: reloadMethod,
1233 1234
        getSkSLMethod: getSkSLMethod,
        printStructuredErrorLogMethod: printStructuredErrorLog,
1235
      );
1236 1237 1238 1239 1240 1241 1242 1243 1244
      // This will wait for at least one flutter view before returning.
      final Status status = globals.logger.startProgress(
        'Waiting for ${device.device.name} to report its views...',
        timeout: const Duration(milliseconds: 200),
      );
      try {
        await device.vmService.getFlutterViews();
      } finally {
        status.stop();
1245
      }
1246 1247 1248
      // 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.
1249
      unawaited(device.vmService.onDone.then<void>(
1250 1251 1252
        _serviceProtocolDone,
        onError: _serviceProtocolError,
      ).whenComplete(_serviceDisconnected));
1253
    }
1254 1255
  }

1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
  Future<void> launchDevTools() async {
    try {
      assert(supportsServiceProtocol);
      _devtoolsServer ??= await devtools_server.serveDevTools(
        enableStdinCommands: false,
      );
      await devtools_server.launchDevTools(
        <String, dynamic>{
          'reuseWindows': true,
        },
1266
        flutterDevices.first.vmService.httpAddress,
1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280
        'http://${_devtoolsServer.address.host}:${_devtoolsServer.port}',
        false,  // headless mode,
        false,  // machine mode
      );
    } on Exception catch (e, st) {
      globals.printTrace('Failed to launch DevTools: $e\n$st');
    }
  }

  Future<void> shutdownDevtools() async {
    await _devtoolsServer?.close();
    _devtoolsServer = null;
  }

1281
  Future<void> _serviceProtocolDone(dynamic object) async {
1282
    globals.printTrace('Service protocol connection closed.');
1283 1284
  }

1285
  Future<void> _serviceProtocolError(dynamic error, StackTrace stack) {
1286
    globals.printTrace('Service protocol connection closed with an error: $error\n$stack');
1287
    return Future<void>.error(error, stack);
1288 1289
  }

1290
  void _serviceDisconnected() {
1291
    if (_exited) {
1292 1293 1294
      // User requested the application exit.
      return;
    }
1295
    if (_finished.isCompleted) {
1296
      return;
1297
    }
1298
    globals.printStatus('Lost connection to device.');
1299 1300 1301
    _finished.complete(0);
  }

1302
  void appFinished() {
1303
    if (_finished.isCompleted) {
1304
      return;
1305
    }
1306
    globals.printStatus('Application finished.');
1307 1308 1309
    _finished.complete(0);
  }

1310 1311 1312 1313 1314 1315
  void appFailedToStart() {
    if (!_finished.isCompleted) {
      _finished.complete(1);
    }
  }

1316
  Future<int> waitForAppToFinish() async {
1317
    final int exitCode = await _finished.future;
1318
    assert(exitCode != null);
1319 1320 1321 1322
    await cleanupAtFinish();
    return exitCode;
  }

1323 1324
  @mustCallSuper
  Future<void> preExit() async {
1325 1326
    // If _dillOutputPath is null, the tool created a temporary directory for
    // the dill.
1327 1328 1329 1330
    if (_dillOutputPath == null && artifactDirectory.existsSync()) {
      artifactDirectory.deleteSync(recursive: true);
    }
  }
1331

1332
  Future<void> exitApp() async {
1333
    final List<Future<void>> futures = <Future<void>>[
1334
      for (final FlutterDevice device in flutterDevices)  device.exitApps(),
1335
    ];
1336
    await Future.wait(futures);
1337 1338 1339
    appFinished();
  }

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

  void printHelpDetails() {
1344
    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) {
1345
      commandHelp.s.print();
1346
    }
1347
    if (supportsServiceProtocol) {
1348
      commandHelp.b.print();
1349 1350
      commandHelp.w.print();
      commandHelp.t.print();
1351
      if (isRunningDebug) {
1352 1353 1354 1355
        commandHelp.L.print();
        commandHelp.S.print();
        commandHelp.U.print();
        commandHelp.i.print();
1356
        commandHelp.I.print();
1357 1358 1359
        commandHelp.p.print();
        commandHelp.o.print();
        commandHelp.z.print();
1360
        commandHelp.g.print();
1361
      } else {
1362 1363
        commandHelp.S.print();
        commandHelp.U.print();
1364
      }
1365 1366 1367
      if (supportsCanvasKit){
        commandHelp.k.print();
      }
1368 1369 1370
      if (supportsWriteSkSL) {
        commandHelp.M.print();
      }
1371
      commandHelp.v.print();
1372
      // `P` should precede `a`
1373 1374
      commandHelp.P.print();
      commandHelp.a.print();
1375
    }
1376 1377
  }

1378
  /// Called when a signal has requested we exit.
1379
  Future<void> cleanupAfterSignal();
1380

1381
  /// Called right before we exit.
1382
  Future<void> cleanupAtFinish();
1383 1384 1385

  // Clears the screen.
  void clearScreen() => globals.logger.clear();
1386 1387
}

Devon Carew's avatar
Devon Carew committed
1388
class OperationResult {
1389
  OperationResult(this.code, this.message, { this.fatal = false });
Devon Carew's avatar
Devon Carew committed
1390

1391
  /// The result of the operation; a non-zero code indicates a failure.
Devon Carew's avatar
Devon Carew committed
1392
  final int code;
1393 1394

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

1397 1398 1399
  /// Whether this error should cause the runner to exit.
  final bool fatal;

Devon Carew's avatar
Devon Carew committed
1400
  bool get isOk => code == 0;
1401

1402
  static final OperationResult ok = OperationResult(0, '');
Devon Carew's avatar
Devon Carew committed
1403 1404
}

1405 1406
/// Given the value of the --target option, return the path of the Dart file
/// where the app's main function should be.
1407
String findMainDartFile([ String target ]) {
1408
  target ??= '';
1409 1410 1411
  final String targetPath = globals.fs.path.absolute(target);
  if (globals.fs.isDirectorySync(targetPath)) {
    return globals.fs.path.join(targetPath, 'lib', 'main.dart');
1412 1413
  }
  return targetPath;
1414 1415
}

1416
Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async {
1417 1418
  switch (platform) {
    case TargetPlatform.android_arm:
1419
    case TargetPlatform.android_arm64:
1420
    case TargetPlatform.android_x64:
1421
    case TargetPlatform.android_x86:
1422
      final FlutterProject project = FlutterProject.current();
1423
      final String manifestPath = globals.fs.path.relative(project.android.appManifestFile.path);
1424
      return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
1425 1426 1427 1428 1429 1430
    case TargetPlatform.ios:
      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
    default:
      return null;
  }
}
1431

1432 1433 1434 1435 1436 1437 1438 1439
/// Redirects terminal commands to the correct resident runner methods.
class TerminalHandler {
  TerminalHandler(this.residentRunner);

  final ResidentRunner residentRunner;
  bool _processingUserRequest = false;
  StreamSubscription<void> subscription;

1440 1441 1442
  @visibleForTesting
  String lastReceivedCommand;

1443
  void setupTerminal() {
1444 1445
    if (!globals.logger.quiet) {
      globals.printStatus('');
1446 1447
      residentRunner.printHelp(details: false);
    }
1448 1449
    globals.terminal.singleCharMode = true;
    subscription = globals.terminal.keystrokes.listen(processTerminalInput);
1450 1451
  }

1452 1453 1454 1455

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

  void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) {
1456
    _signalTokens[signal] = globals.signals.addHandler(signal, handler);
1457 1458
  }

1459 1460
  void registerSignalHandlers() {
    assert(residentRunner.stayResident);
1461 1462 1463

    _addSignalHandler(io.ProcessSignal.SIGINT, _cleanUp);
    _addSignalHandler(io.ProcessSignal.SIGTERM, _cleanUp);
1464
    if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) {
1465
      return;
1466
    }
1467 1468 1469 1470 1471 1472 1473
    _addSignalHandler(io.ProcessSignal.SIGUSR1, _handleSignal);
    _addSignalHandler(io.ProcessSignal.SIGUSR2, _handleSignal);
  }

  /// Unregisters terminal signal and keystroke handlers.
  void stop() {
    assert(residentRunner.stayResident);
1474
    for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
1475
      globals.signals.removeHandler(entry.key, entry.value);
1476 1477 1478
    }
    _signalTokens.clear();
    subscription.cancel();
1479 1480 1481 1482
  }

  /// Returns [true] if the input has been handled by this function.
  Future<bool> _commonTerminalInputHandler(String character) async {
1483
    globals.printStatus(''); // the key the user tapped might be on this line
1484 1485 1486 1487 1488 1489 1490
    switch(character) {
      case 'a':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugToggleProfileWidgetBuilds();
          return true;
        }
        return false;
1491 1492 1493
      case 'b':
        await residentRunner.debugToggleBrightness();
        return true;
1494 1495 1496
      case 'c':
        residentRunner.clearScreen();
        return true;
1497 1498 1499 1500
      case 'd':
      case 'D':
        await residentRunner.detach();
        return true;
1501 1502 1503
      case 'g':
        await residentRunner.runSourceGenerators();
        return true;
1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515
      case 'h':
      case 'H':
      case '?':
        // help
        residentRunner.printHelp(details: true);
        return true;
      case 'i':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugToggleWidgetInspector();
          return true;
        }
        return false;
1516 1517 1518 1519 1520 1521
      case 'I':
        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
          await residentRunner.debugToggleInvertOversizedImages();
          return true;
        }
        return false;
1522 1523
      case 'k':
        if (residentRunner.supportsCanvasKit) {
1524 1525
          final bool result = await residentRunner.toggleCanvaskit();
          globals.printStatus('${result ? 'Enabled' : 'Disabled'} CanvasKit');
1526 1527 1528
          return true;
        }
        return false;
1529
      case 'l':
1530
        final List<FlutterView> views = await residentRunner.listFlutterViews();
1531
        globals.printStatus('Connected ${pluralize('view', views.length)}:');
1532
        for (final FlutterView v in views) {
1533
          globals.printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
1534 1535
        }
        return true;
1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548
      case 'L':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugDumpLayerTree();
          return true;
        }
        return false;
      case 'o':
      case 'O':
        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
          await residentRunner.debugTogglePlatform();
          return true;
        }
        return false;
1549 1550 1551 1552 1553 1554
      case 'M':
        if (residentRunner.supportsWriteSkSL) {
          await residentRunner.writeSkSL();
          return true;
        }
        return false;
1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571
      case 'p':
        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
          await residentRunner.debugToggleDebugPaintSizeEnabled();
          return true;
        }
        return false;
      case 'P':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugTogglePerformanceOverlayOverride();
          return true;
        }
        return false;
      case 'q':
      case 'Q':
        // exit
        await residentRunner.exit();
        return true;
1572 1573 1574 1575 1576
      case 'r':
        if (!residentRunner.canHotReload) {
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: false);
1577 1578 1579
        if (result.fatal) {
          throwToolExit(result.message);
        }
1580
        if (!result.isOk) {
1581
          globals.printStatus('Try again after fixing the above error(s).', emphasis: true);
1582 1583 1584 1585 1586 1587 1588 1589
        }
        return true;
      case 'R':
        // If hot restart is not supported for all devices, ignore the command.
        if (!residentRunner.canHotRestart || !residentRunner.hotMode) {
          return false;
        }
        final OperationResult result = await residentRunner.restart(fullRestart: true);
1590 1591 1592
        if (result.fatal) {
          throwToolExit(result.message);
        }
1593
        if (!result.isOk) {
1594
          globals.printStatus('Try again after fixing the above error(s).', emphasis: true);
1595 1596
        }
        return true;
1597 1598 1599 1600 1601 1602 1603
      case 's':
        for (final FlutterDevice device in residentRunner.flutterDevices) {
          if (device.device.supportsScreenshot) {
            await residentRunner.screenshot(device);
          }
        }
        return true;
1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622
      case 'S':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugDumpSemanticsTreeInTraversalOrder();
          return true;
        }
        return false;
      case 't':
      case 'T':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugDumpRenderTree();
          return true;
        }
        return false;
      case 'U':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
          return true;
        }
        return false;
1623 1624 1625 1626 1627 1628
      case 'v':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.launchDevTools();
          return true;
        }
        return false;
1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647
      case 'w':
      case 'W':
        if (residentRunner.supportsServiceProtocol) {
          await residentRunner.debugDumpApp();
          return true;
        }
        return false;
      case 'z':
      case 'Z':
        await residentRunner.debugToggleDebugCheckElevationsEnabled();
        return true;
    }
    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) {
1648
      globals.printTrace('Ignoring terminal input: "$command" because we are busy.');
1649 1650 1651 1652
      return;
    }
    _processingUserRequest = true;
    try {
1653 1654
      lastReceivedCommand = command;
      await _commonTerminalInputHandler(command);
1655 1656
    // Catch all exception since this is doing cleanup and rethrowing.
    } catch (error, st) { // ignore: avoid_catches_without_on_clauses
1657 1658
      // Don't print stack traces for known error types.
      if (error is! ToolExit) {
1659
        globals.printError('$error\n$st');
1660
      }
1661 1662
      await _cleanUp(null);
      rethrow;
1663 1664 1665 1666 1667 1668 1669
    } finally {
      _processingUserRequest = false;
    }
  }

  Future<void> _handleSignal(io.ProcessSignal signal) async {
    if (_processingUserRequest) {
1670
      globals.printTrace('Ignoring signal: "$signal" because we are busy.');
1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683
      return;
    }
    _processingUserRequest = true;

    final bool fullRestart = signal == io.ProcessSignal.SIGUSR2;

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

1684
  Future<void> _cleanUp(io.ProcessSignal signal) async {
1685
    globals.terminal.singleCharMode = false;
1686
    await subscription?.cancel();
1687 1688 1689 1690
    await residentRunner.cleanupAfterSignal();
  }
}

1691
class DebugConnectionInfo {
1692
  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
1693

1694 1695 1696 1697
  // TODO(danrubel): the httpUri field should be removed as part of
  // https://github.com/flutter/flutter/issues/7050
  final Uri httpUri;
  final Uri wsUri;
1698 1699
  final String baseUri;
}
1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722

/// Returns the next platform value for the switcher.
///
/// These values must match what is available in
/// packages/flutter/lib/src/foundation/binding.dart
String nextPlatform(String currentPlatform, FeatureFlags featureFlags) {
  switch (currentPlatform) {
    case 'android':
      return 'iOS';
    case 'iOS':
      return 'fuchsia';
    case 'fuchsia':
      if (featureFlags.isMacOSEnabled) {
        return 'macOS';
      }
      return 'android';
    case 'macOS':
      return 'android';
    default:
      assert(false); // Invalid current platform.
      return 'android';
  }
}