resident_web_runner.dart 23.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
// ignore: import_of_legacy_library_into_null_safe
8
import 'package:dwds/dwds.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
import '../base/terminal.dart';
22
import '../base/time.dart';
23 24
import '../base/utils.dart';
import '../build_info.dart';
25
import '../cache.dart';
26
import '../dart/language_version.dart';
27
import '../devfs.dart';
28
import '../device.dart';
29
import '../flutter_plugins.dart';
30
import '../globals.dart' as globals;
31
import '../project.dart';
32
import '../reporting/reporting.dart';
33
import '../resident_devtools_handler.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/file_generators/flutter_service_worker_js.dart';
40
import '../web/file_generators/main_dart.dart' as main_dart;
41
import '../web/web_device.dart';
42
import '../web/web_runner.dart';
43
import 'devfs_web.dart';
44 45 46 47 48

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

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

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

113
  final FileSystem _fileSystem;
114
  final Logger _logger;
115 116
  final SystemClock _systemClock;
  final Usage _usage;
117
  final UrlTunneller? _urlTunneller;
118

119
  @override
120
  Logger get logger => _logger;
121 122

  @override
123
  FileSystem get fileSystem => _fileSystem;
124

125
  FlutterDevice? get device => flutterDevices.first;
126
  final FlutterProject flutterProject;
127
  DateTime? firstBuildTime;
128

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

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

  @override
138 139 140
  bool get debuggingEnabled => isRunningDebug && deviceIsDebuggable;

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

143 144 145
  @override
  bool get supportsWriteSkSL => false;

146 147 148 149
  @override
  // Web uses a different plugin registry.
  bool get generateDartPluginRegistry => false;

150
  bool get _enableDwds => debuggingEnabled;
151

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

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

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

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

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

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

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

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

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

256 257
    try {
      return await asyncGuard(() async {
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
        Future<int> getPort() async {
          if (debuggingOptions.port == null) {
            return globals.os.findFreePort();
          }

          final int? port = int.tryParse(debuggingOptions.port ?? '');

          if (port == null) {
            logger.printError('''
Received a non-integer value for port: ${debuggingOptions.port}
A randomly-chosen available port will be used instead.
''');
            return globals.os.findFreePort();
          }

          if (port < 0 || port > 65535) {
            throwToolExit('''
Invalid port: ${debuggingOptions.port}
Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
    ''');
          }

          return port;
        }

283
        final ExpressionCompiler? expressionCompiler =
284
          debuggingOptions.webEnableExpressionEvaluation
285
              ? WebExpressionCompiler(device!.generator!, fileSystem: _fileSystem)
286
              : null;
287
        device!.devFS = WebDevFS(
288
          hostname: debuggingOptions.hostname ?? 'localhost',
289
          port: await getPort(),
290
          packagesFilePath: packagesFilePath,
291
          urlTunneller: _urlTunneller,
292
          useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy,
293
          useSseForDebugBackend: debuggingOptions.webUseSseForDebugBackend,
294
          useSseForInjectedClient: debuggingOptions.webUseSseForInjectedClient,
295
          buildInfo: debuggingOptions.buildInfo,
296
          enableDwds: _enableDwds,
297
          enableDds: debuggingOptions.enableDds,
298
          entrypoint: _fileSystem.file(target).uri,
299
          expressionCompiler: expressionCompiler,
300
          chromiumLauncher: _chromiumLauncher,
301 302
          nullAssertions: debuggingOptions.nullAssertions,
          nullSafetyMode: debuggingOptions.buildInfo.nullSafetyMode,
303
          nativeNullAssertions: debuggingOptions.nativeNullAssertions,
304
        );
305
        final Uri url = await device!.devFS!.create();
306
        if (debuggingOptions.buildInfo.isDebug) {
307
          await runSourceGenerators();
308 309
          final UpdateFSReport report = await _updateDevFS(fullRestart: true);
          if (!report.success) {
310
            _logger.printError('Failed to compile application.');
311
            appFailedToStart();
312 313
            return 1;
          }
314
          device!.generator!.accept();
315
          cacheInitialDillCompilation();
316
        } else {
317 318
          final WebBuilder webBuilder = WebBuilder(
            logger: _logger,
319
            processManager: globals.processManager,
320 321 322 323 324 325
            buildSystem: globals.buildSystem,
            fileSystem: _fileSystem,
            flutterVersion: globals.flutterVersion,
            usage: globals.flutterUsage,
          );
          await webBuilder.buildWeb(
326 327 328
            flutterProject,
            target,
            debuggingOptions.buildInfo,
329
            ServiceWorkerStrategy.none,
330
            compilerConfig: JsCompilerConfig.run(nativeNullAssertions: debuggingOptions.nativeNullAssertions)
331 332
          );
        }
333
        await device!.device!.startApp(
334 335 336 337 338 339 340 341 342 343
          package,
          mainPath: target,
          debuggingOptions: debuggingOptions,
          platformArgs: <String, Object>{
            'uri': url.toString(),
          },
        );
        return attach(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
344
          enableDevTools: enableDevTools,
345 346
        );
      });
347
    } on WebSocketException catch (error, stackTrace) {
348
      appFailedToStart();
349
      _logger.printError('$error', stackTrace: stackTrace);
350
      throwToolExit(kExitMessage);
351
    } on ChromeDebugException catch (error, stackTrace) {
352
      appFailedToStart();
353
      _logger.printError('$error', stackTrace: stackTrace);
354
      throwToolExit(kExitMessage);
355
    } on AppConnectionException catch (error, stackTrace) {
356
      appFailedToStart();
357
      _logger.printError('$error', stackTrace: stackTrace);
358
      throwToolExit(kExitMessage);
359
    } on SocketException catch (error, stackTrace) {
360
      appFailedToStart();
361
      _logger.printError('$error', stackTrace: stackTrace);
362
      throwToolExit(kExitMessage);
363 364 365
    } on Exception {
      appFailedToStart();
      rethrow;
366
    }
367 368 369 370 371
  }

  @override
  Future<OperationResult> restart({
    bool fullRestart = false,
372 373
    bool? pause = false,
    String? reason,
374 375
    bool benchmarkMode = false,
  }) async {
376
    final DateTime start = _systemClock.now();
377
    final Status status = _logger.startProgress(
378 379 380 381
      'Performing hot restart...',
      progressId: 'hot.restart',
    );

382
    if (debuggingOptions.buildInfo.isDebug) {
383
      await runSourceGenerators();
384
      // Full restart is always false for web, since the extra recompile is wasteful.
385
      final UpdateFSReport report = await _updateDevFS();
386
      if (report.success) {
387
        device!.generator!.accept();
388 389
      } else {
        status.stop();
390
        await device!.generator!.reject();
391 392
        return OperationResult(1, 'Failed to recompile application.');
      }
393
    } else {
394
      try {
395 396
        final WebBuilder webBuilder = WebBuilder(
          logger: _logger,
397
          processManager: globals.processManager,
398 399 400 401 402 403
          buildSystem: globals.buildSystem,
          fileSystem: _fileSystem,
          flutterVersion: globals.flutterVersion,
          usage: globals.flutterUsage,
        );
        await webBuilder.buildWeb(
404 405 406
          flutterProject,
          target,
          debuggingOptions.buildInfo,
407
          ServiceWorkerStrategy.none,
408
          compilerConfig: JsCompilerConfig.run(nativeNullAssertions: debuggingOptions.nativeNullAssertions),
409 410 411 412
        );
      } on ToolExit {
        return OperationResult(1, 'Failed to recompile application.');
      }
413 414 415
    }

    try {
416
      if (!deviceIsDebuggable) {
417
        _logger.printStatus('Recompile complete. Page requires refresh.');
418
      } else if (isRunningDebug) {
419
        await _vmService.service.callMethod('hotRestart');
420
      } else {
421 422 423 424 425
        // 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,
        });
426
      }
427 428
    } on Exception catch (err) {
      return OperationResult(1, err.toString(), fatal: true);
429 430 431
    } finally {
      status.stop();
    }
432

433
    final Duration elapsed = _systemClock.now().difference(start);
434
    final String elapsedMS = getElapsedAsMilliseconds(elapsed);
435
    _logger.printStatus('Restarted application in $elapsedMS.');
436
    unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices));
437 438 439

    // Don't track restart times for dart2js builds or web-server devices.
    if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) {
440
      _usage.sendTiming('hot', 'web-incremental-restart', elapsed);
441 442 443
      HotEvent(
        'restart',
        targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
444
        sdkName: await device!.device!.sdkNameAndVersion,
445 446 447
        emulator: false,
        fullRestart: true,
        reason: reason,
448
        overallTimeInMs: elapsed.inMilliseconds,
449
        fastReassemble: false,
450
      ).send();
451 452 453 454
    }
    return OperationResult.ok;
  }

Dan Field's avatar
Dan Field committed
455 456 457
  // 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.
458 459
  Future<Uri> _generateEntrypoint(Uri mainUri, PackageConfig? packageConfig) async {
    File? result = _generatedEntrypointDirectory?.childFile('web_entrypoint.dart');
Dan Field's avatar
Dan Field committed
460
    if (_generatedEntrypointDirectory == null) {
461
      _generatedEntrypointDirectory ??= _fileSystem.systemTempDirectory.createTempSync('flutter_tools.')
Dan Field's avatar
Dan Field committed
462
        ..createSync();
463
      result = _generatedEntrypointDirectory!.childFile('web_entrypoint.dart');
Dan Field's avatar
Dan Field committed
464

465
      // Generates the generated_plugin_registrar
466
      await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: _generatedEntrypointDirectory!);
467 468
      // The below works because `injectBuildTimePluginFiles` is configured to write
      // the web_plugin_registrant.dart file alongside the generated main.dart
469
      const String generatedImport = 'web_plugin_registrant.dart';
Dan Field's avatar
Dan Field committed
470

471
      Uri? importedEntrypoint = packageConfig!.toPackageUri(mainUri);
472 473
      // Special handling for entrypoints that are not under lib, such as test scripts.
      if (importedEntrypoint == null) {
474 475 476 477
        final String parent = _fileSystem.file(mainUri).parent.path;
        flutterDevices.first.generator!
          ..addFileSystemRoot(parent)
          ..addFileSystemRoot(_fileSystem.directory('test').absolute.path);
478 479
        importedEntrypoint = Uri(
          scheme: 'org-dartlang-app',
480
          path: '/${mainUri.pathSegments.last}',
481
        );
482
      }
483
      final LanguageVersion languageVersion = determineLanguageVersion(
484
        _fileSystem.file(mainUri),
485
        packageConfig[flutterProject.manifest.appName],
486
        Cache.flutterRoot!,
487
      );
Dan Field's avatar
Dan Field committed
488

489 490 491 492 493
      final String entrypoint = main_dart.generateMainDartFile(importedEntrypoint.toString(),
        languageVersion: languageVersion,
        pluginRegistrantEntrypoint: generatedImport,
      );

Dan Field's avatar
Dan Field committed
494 495
      result.writeAsStringSync(entrypoint);
    }
496
    return result!.absolute.uri;
Dan Field's avatar
Dan Field committed
497 498
  }

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

  @override
  Future<int> attach({
548 549
    Completer<DebugConnectionInfo>? connectionInfoCompleter,
    Completer<void>? appStartedCompleter,
550
    bool allowExistingDdsInstance = false,
551
    bool enableDevTools = false, // ignored, we don't yet support devtools for web
552
    bool needsFullRestart = true,
553
  }) async {
554
    if (_chromiumLauncher != null) {
555
      final Chromium chrome = await _chromiumLauncher!.connectedInstance;
556
      final ChromeTab? chromeTab = await chrome.chromeConnection.getTab((ChromeTab chromeTab) {
557
        return !chromeTab.url.startsWith('chrome-extension');
558
      }, retryFor: const Duration(seconds: 5));
559 560 561
      if (chromeTab == null) {
        throwToolExit('Failed to connect to Chrome instance.');
      }
562 563
      _wipConnection = await chromeTab.connect();
    }
564
    Uri? websocketUri;
565
    if (supportsServiceProtocol) {
566 567
      final WebDevFS webDevFS = device!.devFS! as WebDevFS;
      final bool useDebugExtension = device!.device is WebServerDevice && debuggingOptions.startPaused;
568
      _connectionResult = await webDevFS.connect(useDebugExtension);
569
      unawaited(_connectionResult!.debugConnection!.onDone.whenComplete(_cleanupAndExit));
570

571 572
      void onLogEvent(vmservice.Event event)  {
        final String message = processVmServiceMessage(event);
573
        _logger.printStatus(message);
574 575
      }

576 577
      _stdOutSub = _vmService.service.onStdoutEvent.listen(onLogEvent);
      _stdErrSub = _vmService.service.onStderrEvent.listen(onLogEvent);
578
      try {
579
        await _vmService.service.streamListen(vmservice.EventStreams.kStdout);
580 581 582 583 584
      } 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 {
585
        await _vmService.service.streamListen(vmservice.EventStreams.kStderr);
586 587 588
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
589
      }
590
      try {
591
        await _vmService.service.streamListen(vmservice.EventStreams.kIsolate);
592 593 594
      } on vmservice.RPCError {
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're not already subscribed.
595
      }
596
      await setUpVmService(
597
        reloadSources: (String isolateId, {bool? force, bool? pause}) async {
598
          await restart(pause: pause);
599
        },
600 601 602 603
        device: device!.device,
        flutterProject: flutterProject,
        printStructuredErrorLogMethod: printStructuredErrorLog,
        vmService: _vmService.service,
604 605
      );

606

607 608
      websocketUri = Uri.parse(_connectionResult!.debugConnection!.uri);
      device!.vmService = _vmService;
609

610 611 612
      // 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.
613
      if (!debuggingOptions.startPaused || !supportsServiceProtocol) {
614
        _connectionResult!.appConnection!.runMain();
615
      } else {
616
        late StreamSubscription<void> resumeSub;
617
        resumeSub = _vmService.service.onDebugEvent
618 619
            .listen((vmservice.Event event) {
          if (event.type == vmservice.EventKind.kResume) {
620
            _connectionResult!.appConnection!.runMain();
621 622
            resumeSub.cancel();
          }
623
        });
624
      }
625 626
      if (enableDevTools) {
        // The method below is guaranteed never to return a failing future.
627
        unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools(
628 629 630 631
          devToolsServerAddress: debuggingOptions.devToolsServerAddress,
          flutterDevices: flutterDevices,
        ));
      }
632
    }
633
    if (websocketUri != null) {
634
      if (debuggingOptions.vmserviceOutFile != null) {
635
        _fileSystem.file(debuggingOptions.vmserviceOutFile)
636 637 638
          ..createSync(recursive: true)
          ..writeAsStringSync(websocketUri.toString());
      }
639
      _logger.printStatus('Debug service listening on $websocketUri');
640 641 642 643 644 645 646 647 648 649
      if (debuggingOptions.buildInfo.nullSafetyMode !=  NullSafetyMode.sound) {
        _logger.printStatus('');
        _logger.printStatus(
          'Running without sound null safety ⚠️',
          emphasis: true,
        );
        _logger.printStatus(
          'Dart 3 will only support sound null safety, see https://dart.dev/null-safety',
        );
      }
650
    }
651
    appStartedCompleter?.complete();
652 653 654 655 656 657
    connectionInfoCompleter?.complete(DebugConnectionInfo(wsUri: websocketUri));
    if (stayResident) {
      await waitForAppToFinish();
    } else {
      await stopEchoingDeviceLog();
      await exitApp();
658
    }
659 660
    await cleanupAtFinish();
    return 0;
661
  }
662 663 664

  @override
  Future<void> exitApp() async {
665
    await device!.exitApps();
666 667
    appFinished();
  }
668
}
669

670 671 672 673
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));
674
}