resident_web_runner.dart 26.8 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:dwds/dwds.dart';
8
import 'package:meta/meta.dart';
9
import 'package:package_config/package_config.dart';
10
import 'package:vm_service/vm_service.dart' as vmservice;
11 12
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
    hide StackTrace;
13

14
import '../application_package.dart';
15
import '../base/async_guard.dart';
16
import '../base/common.dart';
17
import '../base/file_system.dart';
18
import '../base/io.dart';
19
import '../base/logger.dart';
20
import '../base/net.dart';
21 22 23
import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
24
import '../build_system/targets/web.dart';
25
import '../dart/language_version.dart';
26
import '../devfs.dart';
27
import '../device.dart';
28
import '../features.dart';
29
import '../globals.dart' as globals;
Dan Field's avatar
Dan Field committed
30 31
import '../platform_plugins.dart';
import '../plugins.dart';
32
import '../project.dart';
33
import '../reporting/reporting.dart';
34
import '../resident_runner.dart';
35
import '../run_hot.dart';
36
import '../vmservice.dart';
37
import '../web/chrome.dart';
38
import '../web/compile.dart';
39
import '../web/web_device.dart';
40
import '../web/web_runner.dart';
41
import 'devfs_web.dart';
42 43 44 45 46

/// Injectable factory to create a [ResidentWebRunner].
class DwdsWebRunnerFactory extends WebRunnerFactory {
  @override
  ResidentRunner createWebRunner(
47
    FlutterDevice device, {
48
    String target,
49
    @required bool stayResident,
50 51
    @required FlutterProject flutterProject,
    @required bool ipv6,
52
    @required DebuggingOptions debuggingOptions,
53
    @required UrlTunneller urlTunneller,
54
    bool machine = false,
55
  }) {
56
    return _ResidentWebRunner(
57 58 59 60 61
      device,
      target: target,
      flutterProject: flutterProject,
      debuggingOptions: debuggingOptions,
      ipv6: ipv6,
62
      stayResident: stayResident,
63
      urlTunneller: urlTunneller,
64
      machine: machine,
65 66 67
    );
  }
}
68

69
const String kExitMessage = 'Failed to establish connection with the application '
70 71 72
  'instance in Chrome.\nThis can happen if the websocket connection used by the '
  'web tooling is unable to correctly establish a connection, for example due to a firewall.';

73
/// A hot-runner which handles browser specific delegation.
74 75
abstract class ResidentWebRunner extends ResidentRunner {
  ResidentWebRunner(
76
    FlutterDevice device, {
77 78 79
    String target,
    @required this.flutterProject,
    @required bool ipv6,
80
    @required DebuggingOptions debuggingOptions,
81
    bool stayResident = true,
82
    bool machine = false,
83
  }) : super(
84
          <FlutterDevice>[device],
85
          target: target ?? globals.fs.path.join('lib', 'main.dart'),
86
          debuggingOptions: debuggingOptions,
87
          ipv6: ipv6,
88
          stayResident: stayResident,
89
          machine: machine,
90 91
        );

92
  FlutterDevice get device => flutterDevices.first;
93
  final FlutterProject flutterProject;
94
  DateTime firstBuildTime;
95

Dan Field's avatar
Dan Field committed
96 97 98 99
  // Used with the new compiler to generate a bootstrap file containing plugins
  // and platform initialization.
  Directory _generatedEntrypointDirectory;

100 101
  // Only the debug builds of the web support the service protocol.
  @override
102
  bool get supportsServiceProtocol => isRunningDebug && deviceIsDebuggable;
103 104

  @override
105 106 107 108 109
  bool get debuggingEnabled => isRunningDebug && deviceIsDebuggable;

  /// WebServer device is debuggable when running with --start-paused.
  bool get deviceIsDebuggable => device.device is! WebServerDevice || debuggingOptions.startPaused;

110 111 112
  @override
  bool get supportsWriteSkSL => false;

113
  bool get _enableDwds => debuggingEnabled;
114

115
  ConnectionResult _connectionResult;
116
  StreamSubscription<vmservice.Event> _stdOutSub;
117
  StreamSubscription<vmservice.Event> _stdErrSub;
118
  StreamSubscription<vmservice.Event> _extensionEventSub;
119
  bool _exited = false;
120
  WipConnection _wipConnection;
121
  ChromiumLauncher _chromiumLauncher;
122

123 124
  vmservice.VmService get _vmService =>
      _connectionResult?.debugConnection?.vmService;
125

126
  @override
127 128 129
  bool get canHotRestart {
    return true;
  }
130

131
  @override
132 133 134 135
  Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate(
    String method, {
    Map<String, dynamic> params,
  }) async {
136 137
    final vmservice.Response response =
        await _vmService.callServiceExtension(method, args: params);
138
    return response.toJson();
139 140 141
  }

  @override
142
  Future<void> cleanupAfterSignal() async {
143
    await _cleanup();
144 145 146
  }

  @override
147
  Future<void> cleanupAtFinish() async {
148 149 150 151
    await _cleanup();
  }

  Future<void> _cleanup() async {
152 153 154
    if (_exited) {
      return;
    }
155
    await _stdOutSub?.cancel();
156
    await _stdErrSub?.cancel();
157
    await _extensionEventSub?.cancel();
158
    await device.device.stopApp(null);
159 160 161 162 163 164 165 166
    try {
      _generatedEntrypointDirectory?.deleteSync(recursive: true);
    } on FileSystemException {
      // Best effort to clean up temp dirs.
      globals.printTrace(
        'Failed to clean up temp directory: ${_generatedEntrypointDirectory.path}',
      );
    }
167
    _exited = true;
168 169
  }

170 171 172 173 174
  Future<void> _cleanupAndExit() async {
    await _cleanup();
    appFinished();
  }

175
  @override
176 177 178 179
  void printHelp({bool details = true}) {
    if (details) {
      return printHelpDetails();
    }
180
    const String fire = '🔥';
181
    const String rawMessage =
182
        '  To hot restart changes while running, press "r" or "R".';
183 184
    final String message = globals.terminal.color(
      fire + globals.terminal.bolden(rawMessage),
185 186
      TerminalColor.red,
    );
187
    globals.printStatus(
188
        "Warning: Flutter's support for web development is not stable yet and hasn't");
189 190 191 192
    globals.printStatus('been thoroughly tested in production environments.');
    globals.printStatus('For more information see https://flutter.dev/web');
    globals.printStatus('');
    globals.printStatus(message);
193
    const String quitMessage = 'To quit, press "q".';
194 195 196
    if (device.device is! WebServerDevice) {
      globals.printStatus('For a more detailed help message, press "h". $quitMessage');
    }
197 198
  }

199
  @override
200 201 202 203
  Future<bool> debugDumpApp() async {
    if (!supportsServiceProtocol) {
      return false;
    }
204
    try {
205 206 207 208
      await _vmService
        ?.flutterDebugDumpApp(
          isolateId: null,
        );
209
    } on vmservice.RPCError {
210
      // do nothing.
211
    }
212
    return true;
213 214 215
  }

  @override
216 217 218 219
  Future<bool> debugDumpRenderTree() async {
    if (!supportsServiceProtocol) {
      return false;
    }
220
    try {
221 222 223 224
      await _vmService
        ?.flutterDebugDumpRenderTree(
          isolateId: null,
        );
225
    } on vmservice.RPCError {
226
      // do nothing.
227
    }
228
    return true;
229 230 231
  }

  @override
232 233 234 235
  Future<bool> debugDumpLayerTree() async {
    if (!supportsServiceProtocol) {
      return false;
    }
236
    try {
237 238 239 240
      await _vmService
        ?.flutterDebugDumpLayerTree(
          isolateId: null,
        );
241
    } on vmservice.RPCError {
242
      // do nothing.
243
    }
244
    return true;
245 246 247
  }

  @override
248 249 250 251
  Future<bool> debugDumpSemanticsTreeInTraversalOrder() async {
    if (!supportsServiceProtocol) {
      return false;
    }
252
    try {
253 254 255 256
      await _vmService
        ?.flutterDebugDumpSemanticsTreeInTraversalOrder(
          isolateId: null,
        );
257
    } on vmservice.RPCError {
258
      // do nothing.
259
    }
260
    return true;
261 262 263
  }

  @override
264 265 266 267
  Future<bool> debugTogglePlatform() async {
    if (!supportsServiceProtocol) {
      return false;
    }
268
    try {
269 270 271 272
      final String currentPlatform = await _vmService
        ?.flutterPlatformOverride(
          isolateId: null,
        );
273
      final String platform = nextPlatform(currentPlatform, featureFlags);
274 275 276 277 278
      await _vmService
        ?.flutterPlatformOverride(
            platform: platform,
            isolateId: null,
          );
279
      globals.printStatus('Switched operating system to $platform');
280
    } on vmservice.RPCError {
281
      // do nothing.
282
    }
283
    return true;
284 285
  }

286
  @override
287 288 289 290
  Future<bool> debugToggleBrightness() async {
    if (!supportsServiceProtocol) {
      return false;
    }
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
    try {
      final Brightness currentBrightness = await _vmService
        ?.flutterBrightnessOverride(
          isolateId: null,
        );
      Brightness next;
      if (currentBrightness == Brightness.light) {
        next = Brightness.dark;
      } else if (currentBrightness == Brightness.dark) {
        next = Brightness.light;
      }
      next = await _vmService
        ?.flutterBrightnessOverride(
            brightness: next,
            isolateId: null,
          );
      globals.logger.printStatus('Changed brightness to $next.');
    } on vmservice.RPCError {
309
      // do nothing.
310
    }
311
    return true;
312 313
  }

314 315 316 317 318 319
  @override
  Future<void> stopEchoingDeviceLog() async {
    // Do nothing for ResidentWebRunner
    await device.stopEchoingDeviceLog();
  }

320
  @override
321 322 323 324
  Future<bool> debugDumpSemanticsTreeInInverseHitTestOrder() async {
    if (!supportsServiceProtocol) {
      return false;
    }
325
    try {
326 327 328 329
      await _vmService
        ?.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(
          isolateId: null,
        );
330
    } on vmservice.RPCError {
331
      // do nothing.
332
    }
333
    return true;
334 335 336
  }

  @override
337 338 339 340
  Future<bool> debugToggleDebugPaintSizeEnabled() async {
    if (!supportsServiceProtocol) {
      return false;
    }
341
    try {
342 343 344 345
      await _vmService
        ?.flutterToggleDebugPaintSizeEnabled(
          isolateId: null,
        );
346
    } on vmservice.RPCError {
347
      // do nothing.
348
    }
349
    return true;
350 351 352
  }

  @override
353 354 355 356
  Future<bool> debugToggleDebugCheckElevationsEnabled() async {
    if (!supportsServiceProtocol) {
      return false;
    }
357
    try {
358 359 360 361
      await _vmService
        ?.flutterToggleDebugCheckElevationsEnabled(
          isolateId: null,
        );
362
    } on vmservice.RPCError {
363
      // do nothing.
364
    }
365
    return true;
366 367 368
  }

  @override
369 370 371 372
  Future<bool> debugTogglePerformanceOverlayOverride() async {
    if (!supportsServiceProtocol) {
      return false;
    }
373
    try {
374 375 376 377
      await _vmService
        ?.flutterTogglePerformanceOverlayOverride(
          isolateId: null,
        );
378
    } on vmservice.RPCError {
379
      // do nothing.
380
    }
381
    return true;
382 383 384
  }

  @override
385 386 387 388
  Future<bool> debugToggleWidgetInspector() async {
    if (!supportsServiceProtocol) {
      return false;
    }
389
    try {
390 391 392 393
      await _vmService
        ?.flutterToggleWidgetInspector(
          isolateId: null,
        );
394
    } on vmservice.RPCError {
395
      // do nothing.
396
    }
397
    return true;
398 399
  }

400
  @override
401 402 403 404
  Future<bool> debugToggleInvertOversizedImages() async {
    if (!supportsServiceProtocol) {
      return false;
    }
405 406 407 408 409 410
    try {
      await _vmService
        ?.flutterToggleInvertOversizedImages(
          isolateId: null,
        );
    } on vmservice.RPCError {
411
      // do nothing.
412
    }
413
    return true;
414 415
  }

416
  @override
417 418 419 420
  Future<bool> debugToggleProfileWidgetBuilds() async {
    if (!supportsServiceProtocol) {
      return false;
    }
421
    try {
422 423 424 425
      await _vmService
        ?.flutterToggleProfileWidgetBuilds(
          isolateId: null,
        );
426
    } on vmservice.RPCError {
427
      // do nothing.
428
    }
429
    return true;
430 431 432
  }
}

433 434
class _ResidentWebRunner extends ResidentWebRunner {
  _ResidentWebRunner(
435 436 437 438 439 440
    FlutterDevice device, {
    String target,
    @required FlutterProject flutterProject,
    @required bool ipv6,
    @required DebuggingOptions debuggingOptions,
    bool stayResident = true,
441
    @required this.urlTunneller,
442
    bool machine = false,
443 444 445
  }) : super(
          device,
          flutterProject: flutterProject,
446
          target: target ?? globals.fs.path.join('lib', 'main.dart'),
447 448 449
          debuggingOptions: debuggingOptions,
          ipv6: ipv6,
          stayResident: stayResident,
450
          machine: machine,
451 452
        );

453
  final UrlTunneller urlTunneller;
454

455 456 457 458 459 460
  @override
  Future<int> run({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
    String route,
  }) async {
461
    firstBuildTime = DateTime.now();
462 463
    final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform(
      TargetPlatform.web_javascript,
464
      buildInfo: debuggingOptions.buildInfo,
465 466 467
      applicationBinary: null,
    );
    if (package == null) {
468 469
      globals.printStatus('This application is not configured to build on the web.');
      globals.printStatus('To add web support to a project, run `flutter create .`.');
470
    }
471
    if (!globals.fs.isFileSync(mainPath)) {
472 473 474 475 476
      String message = 'Tried to run $mainPath, but that file does not exist.';
      if (target == null) {
        message +=
            '\nConsider using the -t option to specify the Dart file to start.';
      }
477
      globals.printError(message);
478
      appFailedToStart();
479 480
      return 1;
    }
481
    final String modeName = debuggingOptions.buildInfo.friendlyModeName;
482
    globals.printStatus(
483
      'Launching ${globals.fsUtils.getDisplayPath(target)} '
484 485
      'on ${device.device.name} in $modeName mode...',
    );
486 487
    final String effectiveHostname = debuggingOptions.hostname ?? 'localhost';
    final int hostPort = debuggingOptions.port == null
488
        ? await globals.os.findFreePort()
489
        : int.tryParse(debuggingOptions.port);
490

491 492 493 494
    if (device.device is ChromiumDevice) {
      _chromiumLauncher = (device.device as ChromiumDevice).chromeLauncher;
    }

495 496
    try {
      return await asyncGuard(() async {
497 498 499 500 501
        final ExpressionCompiler expressionCompiler =
          debuggingOptions.webEnableExpressionEvaluation
              ? WebExpressionCompiler(device.generator)
              : null;

502 503 504 505 506
        device.devFS = WebDevFS(
          hostname: effectiveHostname,
          port: hostPort,
          packagesFilePath: packagesFilePath,
          urlTunneller: urlTunneller,
507
          useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy,
508
          useSseForDebugBackend: debuggingOptions.webUseSseForDebugBackend,
509
          buildInfo: debuggingOptions.buildInfo,
510 511
          enableDwds: _enableDwds,
          entrypoint: globals.fs.file(target).uri,
512
          expressionCompiler: expressionCompiler,
513
          chromiumLauncher: _chromiumLauncher,
514
          nullAssertions: debuggingOptions.nullAssertions,
515 516 517 518 519 520
        );
        final Uri url = await device.devFS.create();
        if (debuggingOptions.buildInfo.isDebug) {
          final UpdateFSReport report = await _updateDevFS(fullRestart: true);
          if (!report.success) {
            globals.printError('Failed to compile application.');
521
            appFailedToStart();
522 523 524
            return 1;
          }
          device.generator.accept();
525
          cacheInitialDillCompilation();
526 527 528 529 530 531 532
        } else {
          await buildWeb(
            flutterProject,
            target,
            debuggingOptions.buildInfo,
            debuggingOptions.initializePlatform,
            false,
533
            kNoneWorker,
534
            true,
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
          );
        }
        await device.device.startApp(
          package,
          mainPath: target,
          debuggingOptions: debuggingOptions,
          platformArgs: <String, Object>{
            'uri': url.toString(),
          },
        );
        return attach(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
        );
      });
    } on WebSocketException {
551
      appFailedToStart();
552 553
      throwToolExit(kExitMessage);
    } on ChromeDebugException {
554
      appFailedToStart();
555 556
      throwToolExit(kExitMessage);
    } on AppConnectionException {
557
      appFailedToStart();
558 559
      throwToolExit(kExitMessage);
    } on SocketException {
560
      appFailedToStart();
561
      throwToolExit(kExitMessage);
562 563 564
    } on Exception {
      appFailedToStart();
      rethrow;
565
    }
566
    return 0;
567 568 569 570 571
  }

  @override
  Future<OperationResult> restart({
    bool fullRestart = false,
572
    bool pause = false,
573 574 575 576
    String reason,
    bool benchmarkMode = false,
  }) async {
    final Stopwatch timer = Stopwatch()..start();
577
    final Status status = globals.logger.startProgress(
578 579 580 581
      'Performing hot restart...',
      progressId: 'hot.restart',
    );

582
    if (debuggingOptions.buildInfo.isDebug) {
583
      await runSourceGenerators();
584 585 586 587 588 589 590 591 592
      // Full restart is always false for web, since the extra recompile is wasteful.
      final UpdateFSReport report = await _updateDevFS(fullRestart: false);
      if (report.success) {
        device.generator.accept();
      } else {
        status.stop();
        await device.generator.reject();
        return OperationResult(1, 'Failed to recompile application.');
      }
593
    } else {
594 595 596 597 598 599 600
      try {
        await buildWeb(
          flutterProject,
          target,
          debuggingOptions.buildInfo,
          debuggingOptions.initializePlatform,
          false,
601
          kNoneWorker,
602
          true,
603 604 605 606
        );
      } on ToolExit {
        return OperationResult(1, 'Failed to recompile application.');
      }
607 608 609
    }

    try {
610 611
      if (!deviceIsDebuggable) {
        globals.printStatus('Recompile complete. Page requires refresh.');
612 613 614
      } else if (isRunningDebug) {
        await _vmService.callMethod('hotRestart');
      } else {
615 616 617 618 619
        // On non-debug builds, a hard refresh is required to ensure the
        // up to date sources are loaded.
        await _wipConnection?.sendCommand('Page.reload', <String, Object>{
          'ignoreCache': !debuggingOptions.buildInfo.isDebug,
        });
620
      }
621 622
    } on Exception catch (err) {
      return OperationResult(1, err.toString(), fatal: true);
623 624 625
    } finally {
      status.stop();
    }
626

627 628
    final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
    globals.printStatus('Restarted application in $elapsed.');
629 630 631

    // Don't track restart times for dart2js builds or web-server devices.
    if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) {
632
      globals.flutterUsage.sendTiming('hot', 'web-incremental-restart', timer.elapsed);
633 634 635 636 637 638 639 640
      HotEvent(
        'restart',
        targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
        sdkName: await device.device.sdkNameAndVersion,
        emulator: false,
        fullRestart: true,
        reason: reason,
        overallTimeInMs: timer.elapsed.inMilliseconds,
641
        nullSafety: usageNullSafety,
642
        fastReassemble: null,
643
      ).send();
644 645 646 647
    }
    return OperationResult.ok;
  }

Dan Field's avatar
Dan Field committed
648 649 650
  // Flutter web projects need to include a generated main entrypoint to call the
  // appropriate bootstrap method and inject plugins.
  // Keep this in sync with build_system/targets/web.dart.
651
  Future<Uri> _generateEntrypoint(Uri mainUri, PackageConfig packageConfig) async {
Dan Field's avatar
Dan Field committed
652 653 654 655 656 657
    File result = _generatedEntrypointDirectory?.childFile('web_entrypoint.dart');
    if (_generatedEntrypointDirectory == null) {
      _generatedEntrypointDirectory ??= globals.fs.systemTempDirectory.createTempSync('flutter_tools.')
        ..createSync();
      result = _generatedEntrypointDirectory.childFile('web_entrypoint.dart');

658
      final bool hasWebPlugins = (await findPlugins(flutterProject))
Dan Field's avatar
Dan Field committed
659 660 661
        .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
      await injectPlugins(flutterProject, checkProjects: true);

662
      final Uri generatedUri = globals.fs.currentDirectory
Dan Field's avatar
Dan Field committed
663 664
        .childDirectory('lib')
        .childFile('generated_plugin_registrant.dart')
665 666 667
        .absolute.uri;
      final Uri generatedImport = packageConfig.toPackageUri(generatedUri);
      Uri importedEntrypoint = packageConfig.toPackageUri(mainUri);
668 669
      // Special handling for entrypoints that are not under lib, such as test scripts.
      if (importedEntrypoint == null) {
670
        final String parent = globals.fs.file(mainUri).parent.path;
671
        flutterDevices.first.generator.addFileSystemRoot(parent);
672
        flutterDevices.first.generator.addFileSystemRoot(globals.fs.directory('test').absolute.path);
673 674 675 676
        importedEntrypoint = Uri(
          scheme: 'org-dartlang-app',
          path: '/' + mainUri.pathSegments.last,
        );
677
      }
Dan Field's avatar
Dan Field committed
678 679

      final String entrypoint = <String>[
680 681 682 683
        determineLanguageVersion(
          globals.fs.file(mainUri),
          packageConfig[flutterProject.manifest.appName],
        ),
684 685 686
        '// Flutter web bootstrap script for $importedEntrypoint.',
        '',
        "import 'dart:ui' as ui;",
687
        "import 'dart:async';",
688 689
        '',
        "import '$importedEntrypoint' as entrypoint;",
Dan Field's avatar
Dan Field committed
690
        if (hasWebPlugins)
691
          "import 'package:flutter_web_plugins/flutter_web_plugins.dart';",
Dan Field's avatar
Dan Field committed
692
        if (hasWebPlugins)
693 694
          "import '$generatedImport';",
        '',
695 696
        'typedef _UnaryFunction = dynamic Function(List<String> args);',
        'typedef _NullaryFunction = dynamic Function();',
Dan Field's avatar
Dan Field committed
697 698
        'Future<void> main() async {',
        if (hasWebPlugins)
699
          '  registerPlugins(webPluginRegistry);',
Dan Field's avatar
Dan Field committed
700
        '  await ui.webOnlyInitializePlatform();',
701 702 703 704
        '  if (entrypoint.main is _UnaryFunction) {',
        '    return (entrypoint.main as _UnaryFunction)(<String>[]);',
        '  }',
        '  return (entrypoint.main as _NullaryFunction)();',
Dan Field's avatar
Dan Field committed
705
        '}',
706
        '',
Dan Field's avatar
Dan Field committed
707 708 709
      ].join('\n');
      result.writeAsStringSync(entrypoint);
    }
710
    return result.absolute.uri;
Dan Field's avatar
Dan Field committed
711 712
  }

713 714 715 716
  Future<UpdateFSReport> _updateDevFS({bool fullRestart = false}) async {
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
    final bool rebuildBundle = assetBundle.needsBuild();
    if (rebuildBundle) {
717
      globals.printTrace('Updating assets');
718
      final int result = await assetBundle.build(packagesPath: debuggingOptions.buildInfo.packagesPath);
719 720 721 722
      if (result != 0) {
        return UpdateFSReport(success: false);
      }
    }
723
    final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated(
724 725 726
      lastCompiled: device.devFS.lastCompiled,
      urisToMonitor: device.devFS.sources,
      packagesPath: packagesFilePath,
727
      packageConfig: device.devFS.lastPackageConfig,
728
    );
729
    final Status devFSStatus = globals.logger.startProgress(
730 731 732
      'Syncing files to device ${device.device.name}...',
    );
    final UpdateFSReport report = await device.devFS.update(
733 734 735 736
      mainUri: await _generateEntrypoint(
        globals.fs.file(mainPath).absolute.uri,
        invalidationResult.packageConfig,
      ),
737 738 739 740 741 742 743 744
      target: target,
      bundle: assetBundle,
      firstBuildTime: firstBuildTime,
      bundleFirstUpload: isFirstUpload,
      generator: device.generator,
      fullRestart: fullRestart,
      dillOutputPath: dillOutputPath,
      projectRootPath: projectRootPath,
745
      pathToReload: getReloadPath(fullRestart: fullRestart, swap: false),
746 747
      invalidatedFiles: invalidationResult.uris,
      packageConfig: invalidationResult.packageConfig,
748
      trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,
749
      devFSWriter: null,
750 751
    );
    devFSStatus.stop();
752
    globals.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
753 754 755 756 757 758 759 760
    return report;
  }

  @override
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
  }) async {
761 762
    if (_chromiumLauncher != null) {
      final Chromium chrome = await _chromiumLauncher.connectedInstance;
763
      final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
764
        return !chromeTab.url.startsWith('chrome-extension');
765
      });
766 767 768
      if (chromeTab == null) {
        throwToolExit('Failed to connect to Chrome instance.');
      }
769 770
      _wipConnection = await chromeTab.connect();
    }
771 772
    Uri websocketUri;
    if (supportsServiceProtocol) {
773 774 775 776 777
      final WebDevFS webDevFS = device.devFS as WebDevFS;
      final bool useDebugExtension = device.device is WebServerDevice && debuggingOptions.startPaused;
      _connectionResult = await webDevFS.connect(useDebugExtension);
      unawaited(_connectionResult.debugConnection.onDone.whenComplete(_cleanupAndExit));

778 779 780 781 782 783 784
      void onLogEvent(vmservice.Event event)  {
        final String message = processVmServiceMessage(event);
        globals.printStatus(message);
      }

      _stdOutSub = _vmService.onStdoutEvent.listen(onLogEvent);
      _stdErrSub = _vmService.onStderrEvent.listen(onLogEvent);
785 786
      _extensionEventSub =
          _vmService.onExtensionEvent.listen(printStructuredErrorLog);
787
      try {
788
        await _vmService.streamListen(vmservice.EventStreams.kStdout);
789 790 791 792 793
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
      }
      try {
794
        await _vmService.streamListen(vmservice.EventStreams.kStderr);
795 796 797
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
798
      }
799
      try {
800
        await _vmService.streamListen(vmservice.EventStreams.kIsolate);
801 802 803
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
804
      }
805 806 807 808 809 810
      try {
        await _vmService.streamListen(vmservice.EventStreams.kExtension);
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
      }
811
      unawaited(_vmService.registerService('reloadSources', 'FlutterTools'));
812 813 814 815 816 817
      _vmService.registerServiceCallback('reloadSources', (Map<String, Object> params) async {
        final bool pause = params['pause'] as bool ?? false;
        await restart(benchmarkMode: false, pause: pause, fullRestart: false);
        return <String, Object>{'type': 'Success'};
      });

818 819 820 821 822 823 824 825 826 827 828 829
      websocketUri = Uri.parse(_connectionResult.debugConnection.uri);
      // Always run main after connecting because start paused doesn't work yet.
      if (!debuggingOptions.startPaused || !supportsServiceProtocol) {
        _connectionResult.appConnection.runMain();
      } else {
        StreamSubscription<void> resumeSub;
        resumeSub = _connectionResult.debugConnection.vmService.onDebugEvent
            .listen((vmservice.Event event) {
          if (event.type == vmservice.EventKind.kResume) {
            _connectionResult.appConnection.runMain();
            resumeSub.cancel();
          }
830
        });
831
      }
832
    }
833
    if (websocketUri != null) {
834 835 836 837 838
      if (debuggingOptions.vmserviceOutFile != null) {
        globals.fs.file(debuggingOptions.vmserviceOutFile)
          ..createSync(recursive: true)
          ..writeAsStringSync(websocketUri.toString());
      }
839
      globals.printStatus('Debug service listening on $websocketUri');
840
    }
841
    appStartedCompleter?.complete();
842 843 844 845 846 847
    connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri));
    if (stayResident) {
      await waitForAppToFinish();
    } else {
      await stopEchoingDeviceLog();
      await exitApp();
848
    }
849 850
    await cleanupAtFinish();
    return 0;
851
  }
852

853
  @override
854
  Future<bool> toggleCanvaskit() async {
855 856 857
    final WebDevFS webDevFS = device.devFS as WebDevFS;
    webDevFS.webAssetServer.canvasKitRendering = !webDevFS.webAssetServer.canvasKitRendering;
    await _wipConnection?.sendCommand('Page.reload');
858
    return webDevFS.webAssetServer.canvasKitRendering;
859 860
  }

861 862 863 864 865
  @override
  Future<void> exitApp() async {
    await device.exitApps();
    appFinished();
  }
866
}