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

5 6
// @dart = 2.8

7 8
import 'dart:async';

9
import 'package:dwds/dwds.dart';
10
import 'package:meta/meta.dart';
11
import 'package:package_config/package_config.dart';
12
import 'package:vm_service/vm_service.dart' as vmservice;
13 14
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
    hide StackTrace;
15

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

/// Injectable factory to create a [ResidentWebRunner].
class DwdsWebRunnerFactory extends WebRunnerFactory {
  @override
  ResidentRunner createWebRunner(
51
    FlutterDevice device, {
52
    String target,
53
    @required bool stayResident,
54 55
    @required FlutterProject flutterProject,
    @required bool ipv6,
56
    @required DebuggingOptions debuggingOptions,
57
    @required UrlTunneller urlTunneller,
58 59 60 61
    @required Logger logger,
    @required FileSystem fileSystem,
    @required SystemClock systemClock,
    @required Usage usage,
62
    bool machine = false,
63
  }) {
64
    return ResidentWebRunner(
65 66 67 68 69
      device,
      target: target,
      flutterProject: flutterProject,
      debuggingOptions: debuggingOptions,
      ipv6: ipv6,
70
      stayResident: stayResident,
71
      urlTunneller: urlTunneller,
72
      machine: machine,
73 74 75 76
      usage: usage,
      systemClock: systemClock,
      fileSystem: fileSystem,
      logger: logger,
77 78 79
    );
  }
}
80

81
const String kExitMessage = 'Failed to establish connection with the application '
82 83 84
  '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.';

85
class ResidentWebRunner extends ResidentRunner {
86
  ResidentWebRunner(
87
    FlutterDevice device, {
88
    String target,
89 90
    bool stayResident = true,
    bool machine = false,
91 92
    @required this.flutterProject,
    @required bool ipv6,
93
    @required DebuggingOptions debuggingOptions,
94 95 96 97 98
    @required FileSystem fileSystem,
    @required Logger logger,
    @required SystemClock systemClock,
    @required Usage usage,
    @required UrlTunneller urlTunneller,
99
    ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
100 101 102 103 104 105
  }) : _fileSystem = fileSystem,
       _logger = logger,
       _systemClock = systemClock,
       _usage = usage,
       _urlTunneller = urlTunneller,
       super(
106
          <FlutterDevice>[device],
107
          target: target ?? fileSystem.path.join('lib', 'main.dart'),
108
          debuggingOptions: debuggingOptions,
109
          ipv6: ipv6,
110
          stayResident: stayResident,
111
          machine: machine,
112
          devtoolsHandler: devtoolsHandler,
113 114
        );

115 116 117 118 119 120
  final FileSystem _fileSystem;
  final Logger _logger;
  final SystemClock _systemClock;
  final Usage _usage;
  final UrlTunneller _urlTunneller;

121 122 123 124 125 126
  @override
  Logger get logger => _logger;

  @override
  FileSystem get fileSystem => _fileSystem;

127
  FlutterDevice get device => flutterDevices.first;
128
  final FlutterProject flutterProject;
129
  DateTime firstBuildTime;
130

Dan Field's avatar
Dan Field committed
131 132 133 134
  // Used with the new compiler to generate a bootstrap file containing plugins
  // and platform initialization.
  Directory _generatedEntrypointDirectory;

135 136
  // Only the debug builds of the web support the service protocol.
  @override
137
  bool get supportsServiceProtocol => isRunningDebug && deviceIsDebuggable;
138 139

  @override
140 141 142 143 144
  bool get debuggingEnabled => isRunningDebug && deviceIsDebuggable;

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

145 146 147
  @override
  bool get supportsWriteSkSL => false;

148
  bool get _enableDwds => debuggingEnabled;
149

150
  ConnectionResult _connectionResult;
151
  StreamSubscription<vmservice.Event> _stdOutSub;
152
  StreamSubscription<vmservice.Event> _stdErrSub;
153
  StreamSubscription<vmservice.Event> _extensionEventSub;
154
  bool _exited = false;
155
  WipConnection _wipConnection;
156
  ChromiumLauncher _chromiumLauncher;
157

158 159 160 161
  FlutterVmService get _vmService {
    if (_instance != null) {
      return _instance;
    }
162
    final vmservice.VmService service =_connectionResult?.vmService;
163 164 165 166 167
    final Uri websocketUri = Uri.parse(_connectionResult.debugConnection.uri);
    final Uri httpUri = _httpUriFromWebsocketUri(websocketUri);
    return _instance ??= FlutterVmService(service, wsAddress: websocketUri, httpAddress: httpUri);
  }
  FlutterVmService _instance;
168

169
  @override
170
  Future<void> cleanupAfterSignal() async {
171
    await _cleanup();
172 173 174
  }

  @override
175
  Future<void> cleanupAtFinish() async {
176 177 178 179
    await _cleanup();
  }

  Future<void> _cleanup() async {
180 181 182
    if (_exited) {
      return;
    }
183
    await residentDevtoolsHandler.shutdown();
184
    await _stdOutSub?.cancel();
185
    await _stdErrSub?.cancel();
186
    await _extensionEventSub?.cancel();
187
    await device.device.stopApp(null);
188 189 190 191
    try {
      _generatedEntrypointDirectory?.deleteSync(recursive: true);
    } on FileSystemException {
      // Best effort to clean up temp dirs.
192
      _logger.printTrace(
193 194 195
        'Failed to clean up temp directory: ${_generatedEntrypointDirectory.path}',
      );
    }
196
    _exited = true;
197 198
  }

199 200 201 202 203
  Future<void> _cleanupAndExit() async {
    await _cleanup();
    appFinished();
  }

204
  @override
205 206 207 208
  void printHelp({bool details = true}) {
    if (details) {
      return printHelpDetails();
    }
209
    const String fire = '🔥';
210
    const String rawMessage =
211
        '  To hot restart changes while running, press "r" or "R".';
212 213
    final String message = _logger.terminal.color(
      fire + _logger.terminal.bolden(rawMessage),
214 215
      TerminalColor.red,
    );
216
    _logger.printStatus(message);
217
    const String quitMessage = 'To quit, press "q".';
218
    _logger.printStatus('For a more detailed help message, press "h". $quitMessage');
219 220
    _logger.printStatus('');
    printDebuggerList();
221 222
  }

223 224 225 226 227 228
  @override
  Future<void> stopEchoingDeviceLog() async {
    // Do nothing for ResidentWebRunner
    await device.stopEchoingDeviceLog();
  }

229 230 231 232
  @override
  Future<int> run({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
233
    bool enableDevTools = false, // ignored, we don't yet support devtools for web
234 235
    String route,
  }) async {
236
    firstBuildTime = DateTime.now();
237 238
    final ApplicationPackage package = await ApplicationPackageFactory.instance.getPackageForPlatform(
      TargetPlatform.web_javascript,
239
      buildInfo: debuggingOptions.buildInfo,
240 241 242
      applicationBinary: null,
    );
    if (package == null) {
243 244
      _logger.printStatus('This application is not configured to build on the web.');
      _logger.printStatus('To add web support to a project, run `flutter create .`.');
245
    }
246
    final String modeName = debuggingOptions.buildInfo.friendlyModeName;
247 248
    _logger.printStatus(
      'Launching ${getDisplayPath(target, _fileSystem)} '
249 250
      'on ${device.device.name} in $modeName mode...',
    );
251 252 253 254
    if (device.device is ChromiumDevice) {
      _chromiumLauncher = (device.device as ChromiumDevice).chromeLauncher;
    }

255 256
    try {
      return await asyncGuard(() async {
257 258
        final ExpressionCompiler expressionCompiler =
          debuggingOptions.webEnableExpressionEvaluation
259
              ? WebExpressionCompiler(device.generator, fileSystem: _fileSystem)
260
              : null;
261
        device.devFS = WebDevFS(
262 263 264 265
          hostname: debuggingOptions.hostname ?? 'localhost',
          port: debuggingOptions.port != null
            ? int.tryParse(debuggingOptions.port)
            : null,
266
          packagesFilePath: packagesFilePath,
267
          urlTunneller: _urlTunneller,
268
          useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy,
269
          useSseForDebugBackend: debuggingOptions.webUseSseForDebugBackend,
270
          useSseForInjectedClient: debuggingOptions.webUseSseForInjectedClient,
271
          buildInfo: debuggingOptions.buildInfo,
272
          enableDwds: _enableDwds,
273
          enableDds: debuggingOptions.enableDds,
274
          entrypoint: _fileSystem.file(target).uri,
275
          expressionCompiler: expressionCompiler,
276
          chromiumLauncher: _chromiumLauncher,
277
          nullAssertions: debuggingOptions.nullAssertions,
278
          nullSafetyMode: debuggingOptions.buildInfo.nullSafetyMode,
279
          nativeNullAssertions: debuggingOptions.nativeNullAssertions,
280 281 282 283 284
        );
        final Uri url = await device.devFS.create();
        if (debuggingOptions.buildInfo.isDebug) {
          final UpdateFSReport report = await _updateDevFS(fullRestart: true);
          if (!report.success) {
285
            _logger.printError('Failed to compile application.');
286
            appFailedToStart();
287 288 289
            return 1;
          }
          device.generator.accept();
290
          cacheInitialDillCompilation();
291 292 293 294 295 296
        } else {
          await buildWeb(
            flutterProject,
            target,
            debuggingOptions.buildInfo,
            false,
297
            kNoneWorker,
298
            true,
299
            debuggingOptions.nativeNullAssertions,
300
            null,
301 302 303 304 305 306 307 308 309 310 311 312 313
          );
        }
        await device.device.startApp(
          package,
          mainPath: target,
          debuggingOptions: debuggingOptions,
          platformArgs: <String, Object>{
            'uri': url.toString(),
          },
        );
        return attach(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
314
          enableDevTools: enableDevTools,
315 316
        );
      });
317
    } on WebSocketException catch (error, stackTrace) {
318
      appFailedToStart();
319
      _logger.printError('$error', stackTrace: stackTrace);
320
      throwToolExit(kExitMessage);
321
    } on ChromeDebugException catch (error, stackTrace) {
322
      appFailedToStart();
323
      _logger.printError('$error', stackTrace: stackTrace);
324
      throwToolExit(kExitMessage);
325
    } on AppConnectionException catch (error, stackTrace) {
326
      appFailedToStart();
327
      _logger.printError('$error', stackTrace: stackTrace);
328
      throwToolExit(kExitMessage);
329
    } on SocketException catch (error, stackTrace) {
330
      appFailedToStart();
331
      _logger.printError('$error', stackTrace: stackTrace);
332
      throwToolExit(kExitMessage);
333 334 335
    } on Exception {
      appFailedToStart();
      rethrow;
336
    }
337 338 339 340 341
  }

  @override
  Future<OperationResult> restart({
    bool fullRestart = false,
342
    bool pause = false,
343 344 345
    String reason,
    bool benchmarkMode = false,
  }) async {
346 347
    final DateTime start = _systemClock.now();
    final Status status = _logger.startProgress(
348 349 350 351
      'Performing hot restart...',
      progressId: 'hot.restart',
    );

352
    if (debuggingOptions.buildInfo.isDebug) {
353
      await runSourceGenerators();
354 355 356 357 358 359 360 361 362
      // 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.');
      }
363
    } else {
364 365 366 367 368 369
      try {
        await buildWeb(
          flutterProject,
          target,
          debuggingOptions.buildInfo,
          false,
370
          kNoneWorker,
371
          true,
372
          debuggingOptions.nativeNullAssertions,
373
          kBaseHref,
374 375 376 377
        );
      } on ToolExit {
        return OperationResult(1, 'Failed to recompile application.');
      }
378 379 380
    }

    try {
381
      if (!deviceIsDebuggable) {
382
        _logger.printStatus('Recompile complete. Page requires refresh.');
383
      } else if (isRunningDebug) {
384
        await _vmService.service.callMethod('hotRestart');
385
      } else {
386 387 388 389 390
        // 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,
        });
391
      }
392 393
    } on Exception catch (err) {
      return OperationResult(1, err.toString(), fatal: true);
394 395 396
    } finally {
      status.stop();
    }
397

398
    final Duration elapsed = _systemClock.now().difference(start);
399
    final String elapsedMS = getElapsedAsMilliseconds(elapsed);
400
    _logger.printStatus('Restarted application in $elapsedMS.');
401
    unawaited(residentDevtoolsHandler.hotRestart(flutterDevices));
402 403 404

    // Don't track restart times for dart2js builds or web-server devices.
    if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) {
405
      _usage.sendTiming('hot', 'web-incremental-restart', elapsed);
406 407 408 409 410 411 412
      HotEvent(
        'restart',
        targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
        sdkName: await device.device.sdkNameAndVersion,
        emulator: false,
        fullRestart: true,
        reason: reason,
413
        overallTimeInMs: elapsed.inMilliseconds,
414
        fastReassemble: null,
415
      ).send();
416 417 418 419
    }
    return OperationResult.ok;
  }

Dan Field's avatar
Dan Field committed
420 421 422
  // 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.
423
  Future<Uri> _generateEntrypoint(Uri mainUri, PackageConfig packageConfig) async {
Dan Field's avatar
Dan Field committed
424 425
    File result = _generatedEntrypointDirectory?.childFile('web_entrypoint.dart');
    if (_generatedEntrypointDirectory == null) {
426
      _generatedEntrypointDirectory ??= _fileSystem.systemTempDirectory.createTempSync('flutter_tools.')
Dan Field's avatar
Dan Field committed
427 428 429
        ..createSync();
      result = _generatedEntrypointDirectory.childFile('web_entrypoint.dart');

430
      final bool hasWebPlugins = (await findPlugins(flutterProject))
Dan Field's avatar
Dan Field committed
431
        .any((Plugin p) => p.platforms.containsKey(WebPlugin.kConfigKey));
432
      await injectPlugins(flutterProject, webPlatform: true);
Dan Field's avatar
Dan Field committed
433

434
      final Uri generatedUri = _fileSystem.currentDirectory
Dan Field's avatar
Dan Field committed
435 436
        .childDirectory('lib')
        .childFile('generated_plugin_registrant.dart')
437 438 439
        .absolute.uri;
      final Uri generatedImport = packageConfig.toPackageUri(generatedUri);
      Uri importedEntrypoint = packageConfig.toPackageUri(mainUri);
440 441
      // Special handling for entrypoints that are not under lib, such as test scripts.
      if (importedEntrypoint == null) {
442
        final String parent = _fileSystem.file(mainUri).parent.path;
443
        flutterDevices.first.generator.addFileSystemRoot(parent);
444
        flutterDevices.first.generator.addFileSystemRoot(_fileSystem.directory('test').absolute.path);
445 446
        importedEntrypoint = Uri(
          scheme: 'org-dartlang-app',
447
          path: '/${mainUri.pathSegments.last}',
448
        );
449
      }
450
      final LanguageVersion languageVersion =  determineLanguageVersion(
451
        _fileSystem.file(mainUri),
452
        packageConfig[flutterProject.manifest.appName],
453
        Cache.flutterRoot,
454
      );
Dan Field's avatar
Dan Field committed
455 456

      final String entrypoint = <String>[
457
        '// @dart=${languageVersion.major}.${languageVersion.minor}',
458 459 460
        '// Flutter web bootstrap script for $importedEntrypoint.',
        '',
        "import 'dart:ui' as ui;",
461
        "import 'dart:async';",
462 463
        '',
        "import '$importedEntrypoint' as entrypoint;",
Dan Field's avatar
Dan Field committed
464
        if (hasWebPlugins)
465
          "import 'package:flutter_web_plugins/flutter_web_plugins.dart';",
Dan Field's avatar
Dan Field committed
466
        if (hasWebPlugins)
467 468
          "import '$generatedImport';",
        '',
469 470
        'typedef _UnaryFunction = dynamic Function(List<String> args);',
        'typedef _NullaryFunction = dynamic Function();',
Dan Field's avatar
Dan Field committed
471 472
        'Future<void> main() async {',
        if (hasWebPlugins)
473
          '  registerPlugins(webPluginRegistrar);',
Dan Field's avatar
Dan Field committed
474
        '  await ui.webOnlyInitializePlatform();',
475 476 477 478
        '  if (entrypoint.main is _UnaryFunction) {',
        '    return (entrypoint.main as _UnaryFunction)(<String>[]);',
        '  }',
        '  return (entrypoint.main as _NullaryFunction)();',
Dan Field's avatar
Dan Field committed
479
        '}',
480
        '',
Dan Field's avatar
Dan Field committed
481 482 483
      ].join('\n');
      result.writeAsStringSync(entrypoint);
    }
484
    return result.absolute.uri;
Dan Field's avatar
Dan Field committed
485 486
  }

487 488 489 490
  Future<UpdateFSReport> _updateDevFS({bool fullRestart = false}) async {
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
    final bool rebuildBundle = assetBundle.needsBuild();
    if (rebuildBundle) {
491
      _logger.printTrace('Updating assets');
492 493 494 495
      final int result = await assetBundle.build(
        packagesPath: debuggingOptions.buildInfo.packagesPath,
        targetPlatform: TargetPlatform.web_javascript,
      );
496 497 498 499
      if (result != 0) {
        return UpdateFSReport(success: false);
      }
    }
500
    final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated(
501 502 503
      lastCompiled: device.devFS.lastCompiled,
      urisToMonitor: device.devFS.sources,
      packagesPath: packagesFilePath,
504 505
      packageConfig: device.devFS.lastPackageConfig
        ?? debuggingOptions.buildInfo.packageConfig,
506
    );
507
    final Status devFSStatus = _logger.startProgress(
508
      'Waiting for connection from debug service on ${device.device.name}...',
509 510
    );
    final UpdateFSReport report = await device.devFS.update(
511
      mainUri: await _generateEntrypoint(
512
        _fileSystem.file(mainPath).absolute.uri,
513 514
        invalidationResult.packageConfig,
      ),
515 516 517 518 519 520 521 522
      target: target,
      bundle: assetBundle,
      firstBuildTime: firstBuildTime,
      bundleFirstUpload: isFirstUpload,
      generator: device.generator,
      fullRestart: fullRestart,
      dillOutputPath: dillOutputPath,
      projectRootPath: projectRootPath,
523
      pathToReload: getReloadPath(fullRestart: fullRestart, swap: false),
524 525
      invalidatedFiles: invalidationResult.uris,
      packageConfig: invalidationResult.packageConfig,
526
      trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,
527
      devFSWriter: null,
528 529
    );
    devFSStatus.stop();
530
    _logger.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
531 532 533 534 535 536 537
    return report;
  }

  @override
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
538
    bool allowExistingDdsInstance = false,
539
    bool enableDevTools = false, // ignored, we don't yet support devtools for web
540
  }) async {
541 542
    if (_chromiumLauncher != null) {
      final Chromium chrome = await _chromiumLauncher.connectedInstance;
543
      final ChromeTab chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
544
        return !chromeTab.url.startsWith('chrome-extension');
545
      });
546 547 548
      if (chromeTab == null) {
        throwToolExit('Failed to connect to Chrome instance.');
      }
549 550
      _wipConnection = await chromeTab.connect();
    }
551 552
    Uri websocketUri;
    if (supportsServiceProtocol) {
553 554 555 556 557
      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));

558 559
      void onLogEvent(vmservice.Event event)  {
        final String message = processVmServiceMessage(event);
560
        _logger.printStatus(message);
561 562
      }

563 564
      _stdOutSub = _vmService.service.onStdoutEvent.listen(onLogEvent);
      _stdErrSub = _vmService.service.onStderrEvent.listen(onLogEvent);
565
      try {
566
        await _vmService.service.streamListen(vmservice.EventStreams.kStdout);
567 568 569 570 571
      } 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 {
572
        await _vmService.service.streamListen(vmservice.EventStreams.kStderr);
573 574 575
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
576
      }
577
      try {
578
        await _vmService.service.streamListen(vmservice.EventStreams.kIsolate);
579 580 581
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
582
      }
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
      await setUpVmService(
        (String isolateId, {
          bool force,
          bool pause,
        }) async {
          await restart(benchmarkMode: false, pause: pause, fullRestart: false);
        },
        null,
        null,
        device.device,
        null,
        printStructuredErrorLog,
        _vmService.service,
      );

598

599
      websocketUri = Uri.parse(_connectionResult.debugConnection.uri);
600 601
      device.vmService = _vmService;

602 603 604
      // Run main immediately if the app is not started paused or if there
      // is no debugger attached. Otherwise, runMain when a resume event
      // is received.
605 606 607 608
      if (!debuggingOptions.startPaused || !supportsServiceProtocol) {
        _connectionResult.appConnection.runMain();
      } else {
        StreamSubscription<void> resumeSub;
609
        resumeSub = _vmService.service.onDebugEvent
610 611 612 613 614
            .listen((vmservice.Event event) {
          if (event.type == vmservice.EventKind.kResume) {
            _connectionResult.appConnection.runMain();
            resumeSub.cancel();
          }
615
        });
616
      }
617 618 619 620 621 622 623
      if (enableDevTools) {
        // The method below is guaranteed never to return a failing future.
        unawaited(residentDevtoolsHandler.serveAndAnnounceDevTools(
          devToolsServerAddress: debuggingOptions.devToolsServerAddress,
          flutterDevices: flutterDevices,
        ));
      }
624
    }
625
    if (websocketUri != null) {
626
      if (debuggingOptions.vmserviceOutFile != null) {
627
        _fileSystem.file(debuggingOptions.vmserviceOutFile)
628 629 630
          ..createSync(recursive: true)
          ..writeAsStringSync(websocketUri.toString());
      }
631 632
      _logger.printStatus('Debug service listening on $websocketUri');
      _logger.printStatus('');
633
      if (debuggingOptions.buildInfo.nullSafetyMode ==  NullSafetyMode.sound) {
634
        _logger.printStatus('💪 Running with sound null safety 💪', emphasis: true);
635
      } else {
636
        _logger.printStatus(
637 638 639
          'Running with unsound null safety',
          emphasis: true,
        );
640
        _logger.printStatus(
641 642 643
          'For more information see https://dart.dev/null-safety/unsound-null-safety',
        );
      }
644
    }
645
    appStartedCompleter?.complete();
646 647 648 649 650 651
    connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri));
    if (stayResident) {
      await waitForAppToFinish();
    } else {
      await stopEchoingDeviceLog();
      await exitApp();
652
    }
653 654
    await cleanupAtFinish();
    return 0;
655
  }
656 657 658 659 660 661

  @override
  Future<void> exitApp() async {
    await device.exitApps();
    appFinished();
  }
662
}
663

664 665 666 667
Uri _httpUriFromWebsocketUri(Uri websocketUri) {
  const String wsPath = '/ws';
  final String path = websocketUri.path;
  return websocketUri.replace(scheme: 'http', path: path.substring(0, path.length - wsPath.length));
668
}