• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8
9import 'application_package.dart';
10import 'artifacts.dart';
11import 'asset.dart';
12import 'base/common.dart';
13import 'base/file_system.dart';
14import 'base/io.dart' as io;
15import 'base/logger.dart';
16import 'base/terminal.dart';
17import 'base/utils.dart';
18import 'build_info.dart';
19import 'codegen.dart';
20import 'compile.dart';
21import 'dart/package_map.dart';
22import 'devfs.dart';
23import 'device.dart';
24import 'globals.dart';
25import 'project.dart';
26import 'run_cold.dart';
27import 'run_hot.dart';
28import 'vmservice.dart';
29
30class FlutterDevice {
31  FlutterDevice(
32    this.device, {
33    @required this.trackWidgetCreation,
34    this.fileSystemRoots,
35    this.fileSystemScheme,
36    this.viewFilter,
37    TargetModel targetModel = TargetModel.flutter,
38    List<String> experimentalFlags,
39    ResidentCompiler generator,
40    @required BuildMode buildMode,
41  }) : assert(trackWidgetCreation != null),
42       generator = generator ?? ResidentCompiler(
43         artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
44         trackWidgetCreation: trackWidgetCreation,
45         fileSystemRoots: fileSystemRoots,
46         fileSystemScheme: fileSystemScheme,
47         targetModel: targetModel,
48         experimentalFlags: experimentalFlags,
49       );
50
51  /// Create a [FlutterDevice] with optional code generation enabled.
52  static Future<FlutterDevice> create(
53    Device device, {
54    @required FlutterProject flutterProject,
55    @required bool trackWidgetCreation,
56    @required String target,
57    @required BuildMode buildMode,
58    List<String> fileSystemRoots,
59    String fileSystemScheme,
60    String viewFilter,
61    TargetModel targetModel = TargetModel.flutter,
62    List<String> experimentalFlags,
63    ResidentCompiler generator,
64  }) async {
65    ResidentCompiler generator;
66    if (flutterProject.hasBuilders) {
67      generator = await CodeGeneratingResidentCompiler.create(
68        flutterProject: flutterProject,
69      );
70    } else {
71      generator = ResidentCompiler(
72        artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
73        trackWidgetCreation: trackWidgetCreation,
74        fileSystemRoots: fileSystemRoots,
75        fileSystemScheme: fileSystemScheme,
76        targetModel: targetModel,
77        experimentalFlags: experimentalFlags,
78      );
79    }
80    return FlutterDevice(
81      device,
82      trackWidgetCreation: trackWidgetCreation,
83      fileSystemRoots: fileSystemRoots,
84      fileSystemScheme:fileSystemScheme,
85      viewFilter: viewFilter,
86      experimentalFlags: experimentalFlags,
87      targetModel: targetModel,
88      generator: generator,
89      buildMode: buildMode,
90    );
91  }
92
93  final Device device;
94  final ResidentCompiler generator;
95  List<Uri> observatoryUris;
96  List<VMService> vmServices;
97  DevFS devFS;
98  ApplicationPackage package;
99  List<String> fileSystemRoots;
100  String fileSystemScheme;
101  StreamSubscription<String> _loggingSubscription;
102  final String viewFilter;
103  final bool trackWidgetCreation;
104
105  /// If the [reloadSources] parameter is not null the 'reloadSources' service
106  /// will be registered.
107  /// The 'reloadSources' service can be used by other Service Protocol clients
108  /// connected to the VM (e.g. Observatory) to request a reload of the source
109  /// code of the running application (a.k.a. HotReload).
110  /// The 'compileExpression' service can be used to compile user-provided
111  /// expressions requested during debugging of the application.
112  /// This ensures that the reload process follows the normal orchestration of
113  /// the Flutter Tools and not just the VM internal service.
114  Future<void> connect({
115    ReloadSources reloadSources,
116    Restart restart,
117    CompileExpression compileExpression,
118  }) async {
119    if (vmServices != null)
120      return;
121    final List<VMService> localVmServices = List<VMService>(observatoryUris.length);
122    for (int i = 0; i < observatoryUris.length; i += 1) {
123      printTrace('Connecting to service protocol: ${observatoryUris[i]}');
124      localVmServices[i] = await VMService.connect(
125        observatoryUris[i],
126        reloadSources: reloadSources,
127        restart: restart,
128        compileExpression: compileExpression,
129      );
130      printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
131    }
132    vmServices = localVmServices;
133  }
134
135  Future<void> refreshViews() async {
136    if (vmServices == null || vmServices.isEmpty)
137      return Future<void>.value(null);
138    final List<Future<void>> futures = <Future<void>>[];
139    for (VMService service in vmServices)
140      futures.add(service.vm.refreshViews(waitForViews: true));
141    await Future.wait(futures);
142  }
143
144  List<FlutterView> get views {
145    if (vmServices == null)
146      return <FlutterView>[];
147
148    return vmServices
149      .where((VMService service) => !service.isClosed)
150      .expand<FlutterView>(
151        (VMService service) {
152          return viewFilter != null
153               ? service.vm.allViewsWithName(viewFilter)
154               : service.vm.views;
155        },
156      )
157      .toList();
158  }
159
160  Future<void> getVMs() async {
161    for (VMService service in vmServices)
162      await service.getVM();
163  }
164
165  Future<void> exitApps() async {
166    if (!device.supportsFlutterExit) {
167      await device.stopApp(package);
168      return;
169    }
170    final List<FlutterView> flutterViews = views;
171    if (flutterViews == null || flutterViews.isEmpty)
172      return;
173    final List<Future<void>> futures = <Future<void>>[];
174    // If any of the flutter views are paused, we might not be able to
175    // cleanly exit since the service extension may not have been registered.
176    if (flutterViews.any((FlutterView view) {
177      return view != null &&
178             view.uiIsolate != null &&
179             view.uiIsolate.pauseEvent.isPauseEvent;
180      }
181    )) {
182      await device.stopApp(package);
183      return;
184    }
185    for (FlutterView view in flutterViews) {
186      if (view != null && view.uiIsolate != null) {
187        assert(!view.uiIsolate.pauseEvent.isPauseEvent);
188        futures.add(view.uiIsolate.flutterExit());
189      }
190    }
191    // The flutterExit message only returns if it fails, so just wait a few
192    // seconds then assume it worked.
193    // TODO(ianh): We should make this return once the VM service disconnects.
194    await Future.wait(futures).timeout(const Duration(seconds: 2), onTimeout: () => <void>[]);
195  }
196
197  Future<Uri> setupDevFS(
198    String fsName,
199    Directory rootDirectory, {
200    String packagesFilePath,
201  }) {
202    // One devFS per device. Shared by all running instances.
203    devFS = DevFS(
204      vmServices[0],
205      fsName,
206      rootDirectory,
207      packagesFilePath: packagesFilePath,
208    );
209    return devFS.create();
210  }
211
212  List<Future<Map<String, dynamic>>> reloadSources(
213    String entryPath, {
214    bool pause = false,
215  }) {
216    final Uri deviceEntryUri = devFS.baseUri.resolveUri(fs.path.toUri(entryPath));
217    final Uri devicePackagesUri = devFS.baseUri.resolve('.packages');
218    final List<Future<Map<String, dynamic>>> reports = <Future<Map<String, dynamic>>>[];
219    for (FlutterView view in views) {
220      final Future<Map<String, dynamic>> report = view.uiIsolate.reloadSources(
221        pause: pause,
222        rootLibUri: deviceEntryUri,
223        packagesUri: devicePackagesUri,
224      );
225      reports.add(report);
226    }
227    return reports;
228  }
229
230  Future<void> resetAssetDirectory() async {
231    final Uri deviceAssetsDirectoryUri = devFS.baseUri.resolveUri(
232        fs.path.toUri(getAssetBuildDirectory()));
233    assert(deviceAssetsDirectoryUri != null);
234    await Future.wait<void>(views.map<Future<void>>(
235      (FlutterView view) => view.setAssetDirectory(deviceAssetsDirectoryUri)
236    ));
237  }
238
239  // Lists program elements changed in the most recent reload that have not
240  // since executed.
241  Future<List<ProgramElement>> unusedChangesInLastReload() async {
242    final List<Future<List<ProgramElement>>> reports =
243      <Future<List<ProgramElement>>>[];
244    for (FlutterView view in views)
245      reports.add(view.uiIsolate.getUnusedChangesInLastReload());
246    final List<ProgramElement> elements = <ProgramElement>[];
247    for (Future<List<ProgramElement>> report in reports) {
248      for (ProgramElement element in await report)
249        elements.add(ProgramElement(element.qualifiedName,
250                                        devFS.deviceUriToHostUri(element.uri),
251                                        element.line,
252                                        element.column));
253    }
254    return elements;
255  }
256
257  Future<void> debugDumpApp() async {
258    for (FlutterView view in views)
259      await view.uiIsolate.flutterDebugDumpApp();
260  }
261
262  Future<void> debugDumpRenderTree() async {
263    for (FlutterView view in views)
264      await view.uiIsolate.flutterDebugDumpRenderTree();
265  }
266
267  Future<void> debugDumpLayerTree() async {
268    for (FlutterView view in views)
269      await view.uiIsolate.flutterDebugDumpLayerTree();
270  }
271
272  Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
273    for (FlutterView view in views)
274      await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder();
275  }
276
277  Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
278    for (FlutterView view in views)
279      await view.uiIsolate.flutterDebugDumpSemanticsTreeInInverseHitTestOrder();
280  }
281
282  Future<void> toggleDebugPaintSizeEnabled() async {
283    for (FlutterView view in views)
284      await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
285  }
286
287  Future<void> toggleDebugCheckElevationsEnabled() async {
288    for (FlutterView view in views)
289      await view.uiIsolate.flutterToggleDebugCheckElevationsEnabled();
290  }
291
292  Future<void> debugTogglePerformanceOverlayOverride() async {
293    for (FlutterView view in views)
294      await view.uiIsolate.flutterTogglePerformanceOverlayOverride();
295  }
296
297  Future<void> toggleWidgetInspector() async {
298    for (FlutterView view in views)
299      await view.uiIsolate.flutterToggleWidgetInspector();
300  }
301
302  Future<void> toggleProfileWidgetBuilds() async {
303    for (FlutterView view in views) {
304      await view.uiIsolate.flutterToggleProfileWidgetBuilds();
305    }
306  }
307
308  Future<String> togglePlatform({ String from }) async {
309    String to;
310    switch (from) {
311      case 'iOS':
312        to = 'android';
313        break;
314      case 'android':
315      default:
316        to = 'iOS';
317        break;
318    }
319    for (FlutterView view in views)
320      await view.uiIsolate.flutterPlatformOverride(to);
321    return to;
322  }
323
324  void startEchoingDeviceLog() {
325    if (_loggingSubscription != null) {
326      return;
327    }
328    final Stream<String> logStream = device.getLogReader(app: package).logLines;
329    if (logStream == null) {
330      printError('Failed to read device log stream');
331      return;
332    }
333    _loggingSubscription = logStream.listen((String line) {
334      if (!line.contains('Observatory listening on http'))
335        printStatus(line, wrap: false);
336    });
337  }
338
339  Future<void> stopEchoingDeviceLog() async {
340    if (_loggingSubscription == null)
341      return;
342    await _loggingSubscription.cancel();
343    _loggingSubscription = null;
344  }
345
346  void initLogReader() {
347    device.getLogReader(app: package).appPid = vmServices.first.vm.pid;
348  }
349
350  Future<int> runHot({
351    HotRunner hotRunner,
352    String route,
353  }) async {
354    final bool prebuiltMode = hotRunner.applicationBinary != null;
355    final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName;
356    printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...');
357
358    final TargetPlatform targetPlatform = await device.targetPlatform;
359    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
360      targetPlatform,
361      applicationBinary: hotRunner.applicationBinary,
362    );
363
364    if (package == null) {
365      String message = 'No application found for $targetPlatform.';
366      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
367      if (hint != null)
368        message += '\n$hint';
369      printError(message);
370      return 1;
371    }
372
373    final Map<String, dynamic> platformArgs = <String, dynamic>{};
374
375    startEchoingDeviceLog();
376
377    // Start the application.
378    final Future<LaunchResult> futureResult = device.startApp(
379      package,
380      mainPath: hotRunner.mainPath,
381      debuggingOptions: hotRunner.debuggingOptions,
382      platformArgs: platformArgs,
383      route: route,
384      prebuiltApplication: prebuiltMode,
385      usesTerminalUi: hotRunner.usesTerminalUi,
386      ipv6: hotRunner.ipv6,
387    );
388
389    final LaunchResult result = await futureResult;
390
391    if (!result.started) {
392      printError('Error launching application on ${device.name}.');
393      await stopEchoingDeviceLog();
394      return 2;
395    }
396    if (result.hasObservatory) {
397      observatoryUris = <Uri>[result.observatoryUri];
398    } else {
399      observatoryUris = <Uri>[];
400    }
401    return 0;
402  }
403
404
405  Future<int> runCold({
406    ColdRunner coldRunner,
407    String route,
408  }) async {
409    final TargetPlatform targetPlatform = await device.targetPlatform;
410    package = await ApplicationPackageFactory.instance.getPackageForPlatform(
411      targetPlatform,
412      applicationBinary: coldRunner.applicationBinary,
413    );
414
415    final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName;
416    final bool prebuiltMode = coldRunner.applicationBinary != null;
417    if (coldRunner.mainPath == null) {
418      assert(prebuiltMode);
419      printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...');
420    } else {
421      printStatus('Launching ${getDisplayPath(coldRunner.mainPath)} on ${device.name} in $modeName mode...');
422    }
423
424    if (package == null) {
425      String message = 'No application found for $targetPlatform.';
426      final String hint = await getMissingPackageHintForPlatform(targetPlatform);
427      if (hint != null)
428        message += '\n$hint';
429      printError(message);
430      return 1;
431    }
432
433    final Map<String, dynamic> platformArgs = <String, dynamic>{};
434    if (coldRunner.traceStartup != null)
435      platformArgs['trace-startup'] = coldRunner.traceStartup;
436
437    startEchoingDeviceLog();
438
439    final LaunchResult result = await device.startApp(
440      package,
441      mainPath: coldRunner.mainPath,
442      debuggingOptions: coldRunner.debuggingOptions,
443      platformArgs: platformArgs,
444      route: route,
445      prebuiltApplication: prebuiltMode,
446      usesTerminalUi: coldRunner.usesTerminalUi,
447      ipv6: coldRunner.ipv6,
448    );
449
450    if (!result.started) {
451      printError('Error running application on ${device.name}.');
452      await stopEchoingDeviceLog();
453      return 2;
454    }
455    if (result.hasObservatory) {
456      observatoryUris = <Uri>[result.observatoryUri];
457    } else {
458      observatoryUris = <Uri>[];
459    }
460    return 0;
461  }
462
463  Future<UpdateFSReport> updateDevFS({
464    String mainPath,
465    String target,
466    AssetBundle bundle,
467    DateTime firstBuildTime,
468    bool bundleFirstUpload = false,
469    bool bundleDirty = false,
470    bool fullRestart = false,
471    String projectRootPath,
472    String pathToReload,
473    @required String dillOutputPath,
474    @required List<Uri> invalidatedFiles,
475  }) async {
476    final Status devFSStatus = logger.startProgress(
477      'Syncing files to device ${device.name}...',
478      timeout: timeoutConfiguration.fastOperation,
479    );
480    UpdateFSReport report;
481    try {
482      report = await devFS.update(
483        mainPath: mainPath,
484        target: target,
485        bundle: bundle,
486        firstBuildTime: firstBuildTime,
487        bundleFirstUpload: bundleFirstUpload,
488        generator: generator,
489        fullRestart: fullRestart,
490        dillOutputPath: dillOutputPath,
491        trackWidgetCreation: trackWidgetCreation,
492        projectRootPath: projectRootPath,
493        pathToReload: pathToReload,
494        invalidatedFiles: invalidatedFiles,
495      );
496    } on DevFSException {
497      devFSStatus.cancel();
498      return UpdateFSReport(success: false);
499    }
500    devFSStatus.stop();
501    printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
502    return report;
503  }
504
505  Future<void> updateReloadStatus(bool wasReloadSuccessful) async {
506    if (wasReloadSuccessful)
507      generator?.accept();
508    else
509      await generator?.reject();
510  }
511}
512
513// Issue: https://github.com/flutter/flutter/issues/33050
514// Matches the following patterns:
515//    HttpException: Connection closed before full header was received, uri = *
516//    HttpException: , uri = *
517final RegExp kAndroidQHttpConnectionClosedExp = RegExp(r'^HttpException\:.+\, uri \=.+$');
518
519/// Returns `true` if any of the devices is running Android Q.
520Future<bool> hasDeviceRunningAndroidQ(List<FlutterDevice> flutterDevices) async {
521  for (FlutterDevice flutterDevice in flutterDevices) {
522    final String sdkNameAndVersion = await flutterDevice.device.sdkNameAndVersion;
523    if (sdkNameAndVersion != null && sdkNameAndVersion.startsWith('Android 10')) {
524      return true;
525    }
526  }
527  return false;
528}
529
530// Shared code between different resident application runners.
531abstract class ResidentRunner {
532  ResidentRunner(
533    this.flutterDevices, {
534    this.target,
535    this.debuggingOptions,
536    String projectRootPath,
537    String packagesFilePath,
538    this.ipv6,
539    this.usesTerminalUi = true,
540    this.stayResident = true,
541    this.hotMode = true,
542    this.dillOutputPath,
543  }) : mainPath = findMainDartFile(target),
544       projectRootPath = projectRootPath ?? fs.currentDirectory.path,
545       packagesFilePath = packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath),
546       assetBundle = AssetBundleFactory.instance.createBundle() {
547    // TODO(jonahwilliams): this is transitionary logic to allow us to support
548    // platforms that are not yet using flutter assemble. In the "new world",
549    // builds are isolated based on a number of factors. Thus, we cannot assume
550    // that a debug build will create the expected `build/app.dill` file. For
551    // now, I'm working around this by just creating it if it is missing here.
552    // In the future, once build & run are more strongly separated, the build
553    // environment will be plumbed through so that it all comes from a single
554    // source of truth, the [Environment].
555    final File dillOutput = fs.file(dillOutputPath ?? fs.path.join('build', 'app.dill'));
556    if (!dillOutput.existsSync()) {
557      dillOutput.createSync(recursive: true);
558    }
559  }
560
561  @protected
562  @visibleForTesting
563  final List<FlutterDevice> flutterDevices;
564  final String target;
565  final DebuggingOptions debuggingOptions;
566  final bool usesTerminalUi;
567  final bool stayResident;
568  final bool ipv6;
569  final Completer<int> _finished = Completer<int>();
570  final String dillOutputPath;
571  final String packagesFilePath;
572  final String projectRootPath;
573  final String mainPath;
574  final AssetBundle assetBundle;
575
576  bool _exited = false;
577  bool hotMode ;
578  String getReloadPath({ bool fullRestart }) => mainPath + (fullRestart ? '' : '.incremental') + '.dill';
579
580  bool get isRunningDebug => debuggingOptions.buildInfo.isDebug;
581  bool get isRunningProfile => debuggingOptions.buildInfo.isProfile;
582  bool get isRunningRelease => debuggingOptions.buildInfo.isRelease;
583  bool get supportsServiceProtocol => isRunningDebug || isRunningProfile;
584
585  /// Whether this runner can hot restart.
586  ///
587  /// To prevent scenarios where only a subset of devices are hot restarted,
588  /// the runner requires that all attached devices can support hot restart
589  /// before enabling it.
590  bool get canHotRestart {
591    return flutterDevices.every((FlutterDevice device) {
592      return device.device.supportsHotRestart;
593    });
594  }
595
596  /// Invoke an RPC extension method on the first attached ui isolate of the first device.
597  // TODO(jonahwilliams): Update/Remove this method when refactoring the resident
598  // runner to support a single flutter device.
599  Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate(
600    String method, {
601    Map<String, dynamic> params,
602  }) {
603    return flutterDevices.first.views.first.uiIsolate
604        .invokeFlutterExtensionRpcRaw(method, params: params);
605  }
606
607  /// Whether this runner can hot reload.
608  bool get canHotReload => hotMode;
609
610  /// Start the app and keep the process running during its lifetime.
611  ///
612  /// Returns the exit code that we should use for the flutter tool process; 0
613  /// for success, 1 for user error (e.g. bad arguments), 2 for other failures.
614  Future<int> run({
615    Completer<DebugConnectionInfo> connectionInfoCompleter,
616    Completer<void> appStartedCompleter,
617    String route,
618  });
619
620  Future<int> attach({
621    Completer<DebugConnectionInfo> connectionInfoCompleter,
622    Completer<void> appStartedCompleter,
623  });
624
625  bool get supportsRestart => false;
626
627  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) {
628    final String mode = isRunningProfile ? 'profile' :
629        isRunningRelease ? 'release' : 'this';
630    throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode';
631  }
632
633  Future<void> exit() async {
634    _exited = true;
635    await stopEchoingDeviceLog();
636    await preExit();
637    await exitApp();
638  }
639
640  Future<void> detach() async {
641    await stopEchoingDeviceLog();
642    await preExit();
643    appFinished();
644  }
645
646  Future<void> refreshViews() async {
647    final List<Future<void>> futures = <Future<void>>[];
648    for (FlutterDevice device in flutterDevices)
649      futures.add(device.refreshViews());
650    await Future.wait(futures);
651  }
652
653  Future<void> debugDumpApp() async {
654    await refreshViews();
655    for (FlutterDevice device in flutterDevices)
656      await device.debugDumpApp();
657  }
658
659  Future<void> debugDumpRenderTree() async {
660    await refreshViews();
661    for (FlutterDevice device in flutterDevices)
662      await device.debugDumpRenderTree();
663  }
664
665  Future<void> debugDumpLayerTree() async {
666    await refreshViews();
667    for (FlutterDevice device in flutterDevices)
668      await device.debugDumpLayerTree();
669  }
670
671  Future<void> debugDumpSemanticsTreeInTraversalOrder() async {
672    await refreshViews();
673    for (FlutterDevice device in flutterDevices)
674      await device.debugDumpSemanticsTreeInTraversalOrder();
675  }
676
677  Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async {
678    await refreshViews();
679    for (FlutterDevice device in flutterDevices)
680      await device.debugDumpSemanticsTreeInInverseHitTestOrder();
681  }
682
683  Future<void> debugToggleDebugPaintSizeEnabled() async {
684    await refreshViews();
685    for (FlutterDevice device in flutterDevices)
686      await device.toggleDebugPaintSizeEnabled();
687  }
688
689  Future<void> debugToggleDebugCheckElevationsEnabled() async {
690    await refreshViews();
691    for (FlutterDevice device in flutterDevices)
692      await device.toggleDebugCheckElevationsEnabled();
693  }
694
695  Future<void> debugTogglePerformanceOverlayOverride() async {
696    await refreshViews();
697    for (FlutterDevice device in flutterDevices)
698      await device.debugTogglePerformanceOverlayOverride();
699  }
700
701  Future<void> debugToggleWidgetInspector() async {
702    await refreshViews();
703    for (FlutterDevice device in flutterDevices)
704      await device.toggleWidgetInspector();
705  }
706
707  Future<void> debugToggleProfileWidgetBuilds() async {
708    await refreshViews();
709    for (FlutterDevice device in flutterDevices) {
710      await device.toggleProfileWidgetBuilds();
711    }
712  }
713
714  /// Take a screenshot on the provided [device].
715  ///
716  /// If the device has a connected vmservice, this method will attempt to hide
717  /// and restore the debug banner before taking the screenshot.
718  ///
719  /// Throws an [AssertionError] if [Devce.supportsScreenshot] is not true.
720  Future<void> screenshot(FlutterDevice device) async {
721    assert(device.device.supportsScreenshot);
722    final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation);
723    final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
724    try {
725      if (supportsServiceProtocol && isRunningDebug) {
726        await device.refreshViews();
727        try {
728          for (FlutterView view in device.views)
729            await view.uiIsolate.flutterDebugAllowBanner(false);
730        } catch (error) {
731          status.cancel();
732          printError('Error communicating with Flutter on the device: $error');
733          return;
734        }
735      }
736      try {
737        await device.device.takeScreenshot(outputFile);
738      } finally {
739        if (supportsServiceProtocol && isRunningDebug) {
740          try {
741            for (FlutterView view in device.views)
742              await view.uiIsolate.flutterDebugAllowBanner(true);
743          } catch (error) {
744            status.cancel();
745            printError('Error communicating with Flutter on the device: $error');
746            return;
747          }
748        }
749      }
750      final int sizeKB = (await outputFile.length()) ~/ 1024;
751      status.stop();
752      printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).');
753    } catch (error) {
754      status.cancel();
755      printError('Error taking screenshot: $error');
756    }
757  }
758
759  Future<void> debugTogglePlatform() async {
760    await refreshViews();
761    final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride();
762    String to;
763    for (FlutterDevice device in flutterDevices)
764      to = await device.togglePlatform(from: from);
765    printStatus('Switched operating system to $to');
766  }
767
768  Future<void> stopEchoingDeviceLog() async {
769    await Future.wait<void>(
770      flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog())
771    );
772  }
773
774  /// If the [reloadSources] parameter is not null the 'reloadSources' service
775  /// will be registered.
776  //
777  // Failures should be indicated by completing the future with an error, using
778  // a string as the error object, which will be used by the caller (attach())
779  // to display an error message.
780  Future<void> connectToServiceProtocol({
781    ReloadSources reloadSources,
782    Restart restart,
783    CompileExpression compileExpression,
784  }) async {
785    if (!debuggingOptions.debuggingEnabled)
786      throw 'The service protocol is not enabled.';
787
788    bool viewFound = false;
789    for (FlutterDevice device in flutterDevices) {
790      await device.connect(
791        reloadSources: reloadSources,
792        restart: restart,
793        compileExpression: compileExpression,
794      );
795      await device.getVMs();
796      await device.refreshViews();
797      if (device.views.isNotEmpty)
798        viewFound = true;
799    }
800    if (!viewFound) {
801      if (flutterDevices.length == 1)
802        throw 'No Flutter view is available on ${flutterDevices.first.device.name}.';
803      throw 'No Flutter view is available on any device '
804            '(${flutterDevices.map<String>((FlutterDevice device) => device.device.name).join(', ')}).';
805    }
806
807    // Listen for service protocol connection to close.
808    for (FlutterDevice device in flutterDevices) {
809      for (VMService service in device.vmServices) {
810        // This hooks up callbacks for when the connection stops in the future.
811        // We don't want to wait for them. We don't handle errors in those callbacks'
812        // futures either because they just print to logger and is not critical.
813        unawaited(service.done.then<void>(
814          _serviceProtocolDone,
815          onError: _serviceProtocolError,
816        ).whenComplete(_serviceDisconnected));
817      }
818    }
819  }
820
821  Future<void> _serviceProtocolDone(dynamic object) {
822    printTrace('Service protocol connection closed.');
823    return Future<void>.value(object);
824  }
825
826  Future<void> _serviceProtocolError(dynamic error, StackTrace stack) {
827    printTrace('Service protocol connection closed with an error: $error\n$stack');
828    return Future<void>.error(error, stack);
829  }
830
831  void _serviceDisconnected() {
832    if (_exited) {
833      // User requested the application exit.
834      return;
835    }
836    if (_finished.isCompleted)
837      return;
838    printStatus('Lost connection to device.');
839    _finished.complete(0);
840  }
841
842  void appFinished() {
843    if (_finished.isCompleted)
844      return;
845    printStatus('Application finished.');
846    _finished.complete(0);
847  }
848
849  Future<int> waitForAppToFinish() async {
850    final int exitCode = await _finished.future;
851    assert(exitCode != null);
852    await cleanupAtFinish();
853    return exitCode;
854  }
855
856  Future<void> preExit() async { }
857
858  Future<void> exitApp() async {
859    final List<Future<void>> futures = <Future<void>>[];
860    for (FlutterDevice device in flutterDevices)
861      futures.add(device.exitApps());
862    await Future.wait(futures);
863    appFinished();
864  }
865
866  /// Called to print help to the terminal.
867  void printHelp({ @required bool details });
868
869  void printHelpDetails() {
870    if (supportsServiceProtocol) {
871      printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".');
872      printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
873      if (isRunningDebug) {
874        printStatus('For layers (debugDumpLayerTree), use "L"; for accessibility (debugDumpSemantics), use "S" (for traversal order) or "U" (for inverse hit test order).');
875        printStatus('To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".');
876        printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
877        printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
878        printStatus('To toggle the elevation checker, press "z".');
879      } else {
880        printStatus('To dump the accessibility tree (debugDumpSemantics), press "S" (for traversal order) or "U" (for inverse hit test order).');
881      }
882      printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".');
883      printStatus('To enable timeline events for all widget build methods, (debugProfileWidgetBuilds), press "a"');
884    }
885    if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) {
886      printStatus('To save a screenshot to flutter.png, press "s".');
887    }
888  }
889
890  /// Called when a signal has requested we exit.
891  Future<void> cleanupAfterSignal();
892
893  /// Called right before we exit.
894  Future<void> cleanupAtFinish();
895}
896
897class OperationResult {
898  OperationResult(this.code, this.message, { this.fatal = false });
899
900  /// The result of the operation; a non-zero code indicates a failure.
901  final int code;
902
903  /// A user facing message about the results of the operation.
904  final String message;
905
906  /// Whether this error should cause the runner to exit.
907  final bool fatal;
908
909  bool get isOk => code == 0;
910
911  static final OperationResult ok = OperationResult(0, '');
912}
913
914/// Given the value of the --target option, return the path of the Dart file
915/// where the app's main function should be.
916String findMainDartFile([ String target ]) {
917  target ??= '';
918  final String targetPath = fs.path.absolute(target);
919  if (fs.isDirectorySync(targetPath))
920    return fs.path.join(targetPath, 'lib', 'main.dart');
921  else
922    return targetPath;
923}
924
925Future<String> getMissingPackageHintForPlatform(TargetPlatform platform) async {
926  switch (platform) {
927    case TargetPlatform.android_arm:
928    case TargetPlatform.android_arm64:
929    case TargetPlatform.android_x64:
930    case TargetPlatform.android_x86:
931      final FlutterProject project = FlutterProject.current();
932      final String manifestPath = fs.path.relative(project.android.appManifestFile.path);
933      return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
934    case TargetPlatform.ios:
935      return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
936    default:
937      return null;
938  }
939}
940
941/// Redirects terminal commands to the correct resident runner methods.
942class TerminalHandler {
943  TerminalHandler(this.residentRunner);
944
945  final ResidentRunner residentRunner;
946  bool _processingUserRequest = false;
947  StreamSubscription<void> subscription;
948
949  @visibleForTesting
950  String lastReceivedCommand;
951
952  void setupTerminal() {
953    if (!logger.quiet) {
954      printStatus('');
955      residentRunner.printHelp(details: false);
956    }
957    terminal.singleCharMode = true;
958    subscription = terminal.keystrokes.listen(processTerminalInput);
959  }
960
961  void registerSignalHandlers() {
962    assert(residentRunner.stayResident);
963    io.ProcessSignal.SIGINT.watch().listen((io.ProcessSignal signal) {
964      _cleanUp(signal);
965      io.exit(0);
966    });
967    io.ProcessSignal.SIGTERM.watch().listen((io.ProcessSignal signal) {
968      _cleanUp(signal);
969      io.exit(0);
970    });
971    if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart)
972      return;
973    io.ProcessSignal.SIGUSR1.watch().listen(_handleSignal);
974    io.ProcessSignal.SIGUSR2.watch().listen(_handleSignal);
975  }
976
977  /// Returns [true] if the input has been handled by this function.
978  Future<bool> _commonTerminalInputHandler(String character) async {
979    printStatus(''); // the key the user tapped might be on this line
980    switch(character) {
981      case 'a':
982        if (residentRunner.supportsServiceProtocol) {
983          await residentRunner.debugToggleProfileWidgetBuilds();
984          return true;
985        }
986        return false;
987      case 'd':
988      case 'D':
989        await residentRunner.detach();
990        return true;
991      case 'h':
992      case 'H':
993      case '?':
994        // help
995        residentRunner.printHelp(details: true);
996        return true;
997      case 'i':
998      case 'I':
999        if (residentRunner.supportsServiceProtocol) {
1000          await residentRunner.debugToggleWidgetInspector();
1001          return true;
1002        }
1003        return false;
1004      case 'l':
1005        final List<FlutterView> views = residentRunner.flutterDevices
1006            .expand((FlutterDevice d) => d.views).toList();
1007        printStatus('Connected ${pluralize('view', views.length)}:');
1008        for (FlutterView v in views) {
1009          printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
1010        }
1011        return true;
1012      case 'L':
1013        if (residentRunner.supportsServiceProtocol) {
1014          await residentRunner.debugDumpLayerTree();
1015          return true;
1016        }
1017        return false;
1018      case 'o':
1019      case 'O':
1020        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
1021          await residentRunner.debugTogglePlatform();
1022          return true;
1023        }
1024        return false;
1025      case 'p':
1026        if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) {
1027          await residentRunner.debugToggleDebugPaintSizeEnabled();
1028          return true;
1029        }
1030        return false;
1031      case 'P':
1032        if (residentRunner.supportsServiceProtocol) {
1033          await residentRunner.debugTogglePerformanceOverlayOverride();
1034          return true;
1035        }
1036        return false;
1037      case 'q':
1038      case 'Q':
1039        // exit
1040        await residentRunner.exit();
1041        return true;
1042      case 's':
1043        for (FlutterDevice device in residentRunner.flutterDevices) {
1044          if (device.device.supportsScreenshot)
1045            await residentRunner.screenshot(device);
1046        }
1047        return true;
1048      case 'r':
1049        if (!residentRunner.canHotReload) {
1050          return false;
1051        }
1052        final OperationResult result = await residentRunner.restart(fullRestart: false);
1053        if (result.fatal) {
1054          throwToolExit(result.message);
1055        }
1056        if (!result.isOk) {
1057          printStatus('Try again after fixing the above error(s).', emphasis: true);
1058        }
1059        return true;
1060      case 'R':
1061        // If hot restart is not supported for all devices, ignore the command.
1062        if (!residentRunner.canHotRestart || !residentRunner.hotMode) {
1063          return false;
1064        }
1065        final OperationResult result = await residentRunner.restart(fullRestart: true);
1066        if (result.fatal) {
1067          throwToolExit(result.message);
1068        }
1069        if (!result.isOk) {
1070          printStatus('Try again after fixing the above error(s).', emphasis: true);
1071        }
1072        return true;
1073      case 'S':
1074        if (residentRunner.supportsServiceProtocol) {
1075          await residentRunner.debugDumpSemanticsTreeInTraversalOrder();
1076          return true;
1077        }
1078        return false;
1079      case 't':
1080      case 'T':
1081        if (residentRunner.supportsServiceProtocol) {
1082          await residentRunner.debugDumpRenderTree();
1083          return true;
1084        }
1085        return false;
1086      case 'U':
1087        if (residentRunner.supportsServiceProtocol) {
1088          await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder();
1089          return true;
1090        }
1091        return false;
1092      case 'w':
1093      case 'W':
1094        if (residentRunner.supportsServiceProtocol) {
1095          await residentRunner.debugDumpApp();
1096          return true;
1097        }
1098        return false;
1099      case 'z':
1100      case 'Z':
1101        await residentRunner.debugToggleDebugCheckElevationsEnabled();
1102        return true;
1103    }
1104    return false;
1105  }
1106
1107  Future<void> processTerminalInput(String command) async {
1108    // When terminal doesn't support line mode, '\n' can sneak into the input.
1109    command = command.trim();
1110    if (_processingUserRequest) {
1111      printTrace('Ignoring terminal input: "$command" because we are busy.');
1112      return;
1113    }
1114    _processingUserRequest = true;
1115    try {
1116      lastReceivedCommand = command;
1117      await _commonTerminalInputHandler(command);
1118    } catch (error, st) {
1119      // Don't print stack traces for known error types.
1120      if (error is! ToolExit) {
1121        printError('$error\n$st');
1122      }
1123      await _cleanUp(null);
1124      rethrow;
1125    } finally {
1126      _processingUserRequest = false;
1127    }
1128  }
1129
1130  Future<void> _handleSignal(io.ProcessSignal signal) async {
1131    if (_processingUserRequest) {
1132      printTrace('Ignoring signal: "$signal" because we are busy.');
1133      return;
1134    }
1135    _processingUserRequest = true;
1136
1137    final bool fullRestart = signal == io.ProcessSignal.SIGUSR2;
1138
1139    try {
1140      await residentRunner.restart(fullRestart: fullRestart);
1141    } finally {
1142      _processingUserRequest = false;
1143    }
1144  }
1145
1146  Future<void> _cleanUp(io.ProcessSignal signal) async {
1147    terminal.singleCharMode = false;
1148    await subscription?.cancel();
1149    await residentRunner.cleanupAfterSignal();
1150  }
1151}
1152
1153class DebugConnectionInfo {
1154  DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri });
1155
1156  // TODO(danrubel): the httpUri field should be removed as part of
1157  // https://github.com/flutter/flutter/issues/7050
1158  final Uri httpUri;
1159  final Uri wsUri;
1160  final String baseUri;
1161}
1162