vmservice_driver.dart 22.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:file/file.dart' as f;
import 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart' as fuchsia;
import 'package:path/path.dart' as p;
12
import 'package:vm_service/vm_service.dart' as vms;
13
import 'package:webdriver/async_io.dart' as async_io;
14 15 16 17 18 19

import '../../flutter_driver.dart';

/// An implementation of the Flutter Driver over the vmservice protocol.
class VMServiceFlutterDriver extends FlutterDriver {
  /// Creates a driver that uses a connection provided by the given
20
  /// [serviceClient] and [appIsolate].
21
  VMServiceFlutterDriver.connectedTo(
22 23 24 25 26 27
    this._serviceClient,
    this._appIsolate, {
      bool printCommunication = false,
      bool logCommunicationToFile = true,
    }) : _printCommunication = printCommunication,
      _logCommunicationToFile = logCommunicationToFile,
28 29 30 31
      _driverId = _nextDriverId++
    {
      _logFilePathName = p.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log');
    }
32 33 34 35 36

  /// Connects to a Flutter application.
  ///
  /// See [FlutterDriver.connect] for more documentation.
  static Future<FlutterDriver> connect({
37
    String? dartVmServiceUrl,
38 39
    bool printCommunication = false,
    bool logCommunicationToFile = true,
40 41 42
    int? isolateNumber,
    Pattern? fuchsiaModuleTarget,
    Map<String, dynamic>? headers,
43 44 45 46 47 48 49 50 51 52
  }) async {
    // If running on a Fuchsia device, connect to the first isolate whose name
    // matches FUCHSIA_MODULE_TARGET.
    //
    // If the user has already supplied an isolate number/URL to the Dart VM
    // service, then this won't be run as it is unnecessary.
    if (Platform.isFuchsia && isolateNumber == null) {
      // TODO(awdavies): Use something other than print. On fuchsia
      // `stderr`/`stdout` appear to have issues working correctly.
      driverLog = (String source, String message) {
53
        print('$source: $message'); // ignore: avoid_print
54 55 56 57 58
      };
      fuchsiaModuleTarget ??= Platform.environment['FUCHSIA_MODULE_TARGET'];
      if (fuchsiaModuleTarget == null) {
        throw DriverError(
            'No Fuchsia module target has been specified.\n'
59 60
            'Please make sure to specify the FUCHSIA_MODULE_TARGET '
            'environment variable.'
61 62
        );
      }
63 64 65 66 67
      final fuchsia.FuchsiaRemoteConnection fuchsiaConnection = await FuchsiaCompat.connect();
      final List<fuchsia.IsolateRef> refs = await fuchsiaConnection.getMainIsolatesByPattern(fuchsiaModuleTarget);
      if (refs.isEmpty) {
        throw DriverError('Failed to get any isolate refs!');
      }
68 69 70 71 72 73 74 75 76 77 78 79
      final fuchsia.IsolateRef ref = refs.first;
      isolateNumber = ref.number;
      dartVmServiceUrl = ref.dartVm.uri.toString();
      await fuchsiaConnection.stop();
      FuchsiaCompat.cleanup();
    }

    dartVmServiceUrl ??= Platform.environment['VM_SERVICE_URL'];

    if (dartVmServiceUrl == null) {
      throw DriverError(
          'Could not determine URL to connect to application.\n'
80 81
          'Either the VM_SERVICE_URL environment variable should be set, or an explicit '
          'URL should be provided to the FlutterDriver.connect() method.'
82 83 84 85 86
      );
    }

    // Connect to Dart VM services
    _log('Connecting to Flutter application at $dartVmServiceUrl');
87
    final vms.VmService client = await vmServiceConnectFunction(dartVmServiceUrl, headers);
88

89 90
    Future<vms.IsolateRef?> waitForRootIsolate() async {
      bool checkIsolate(vms.IsolateRef ref) => ref.number == isolateNumber.toString();
91
      while (true) {
92
        final vms.VM vm = await client.getVM();
93
        if (vm.isolates!.isEmpty || (isolateNumber != null && !vm.isolates!.any(checkIsolate))) {
94 95 96 97
          await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
          continue;
        }
        return isolateNumber == null
98
          ? vm.isolates!.first
99
          : vm.isolates!.firstWhere(checkIsolate);
100 101 102
      }
    }

103
    final vms.IsolateRef isolateRef = (await _warnIfSlow<vms.IsolateRef?>(
104
      future: waitForRootIsolate(),
105 106
      timeout: kUnusuallyLongTimeout,
      message: isolateNumber == null
107
        ? 'The root isolate is taking an unusually long time to start.'
108
        : 'Isolate $isolateNumber is taking an unusually long time to start.',
109
    ))!;
110
    _log('Isolate found with number: ${isolateRef.number}');
111
    vms.Isolate isolate = await client.getIsolate(isolateRef.id!);
112

113 114
    if (isolate.pauseEvent!.kind == vms.EventKind.kNone) {
      isolate = await client.getIsolate(isolateRef.id!);
115 116 117
    }

    final VMServiceFlutterDriver driver = VMServiceFlutterDriver.connectedTo(
118 119
      client,
      isolate,
120 121 122 123
      printCommunication: printCommunication,
      logCommunicationToFile: logCommunicationToFile,
    );

124 125 126
    // Attempts to resume the isolate, but does not crash if it fails because
    // the isolate is already resumed. There could be a race with other tools,
    // such as a debugger, any of which could have resumed the isolate.
127
    Future<vms.Success> resumeLeniently() async {
128
      _log('Attempting to resume isolate');
129 130
      // Let subsequent isolates start automatically.
      try {
131 132
        final vms.Response result = await client.setFlag('pause_isolates_on_start', 'false');
        if (result == null || result.type != 'Success') {
133 134 135 136 137 138
          _log('setFlag failure: $result');
        }
      } catch (e) {
        _log('Failed to set pause_isolates_on_start=false, proceeding. Error: $e');
      }

139
      return client.resume(isolate.id!).catchError((Object e) {
140
        const int vmMustBePausedCode = 101;
141
        if (e is vms.RPCError && e.code == vmMustBePausedCode) {
142 143 144
          // No biggie; something else must have resumed the isolate
          _log(
              'Attempted to resume an already resumed isolate. This may happen '
145 146
              'when another tool (usually a debugger) resumed the isolate '
              'before the flutter_driver did.'
147
          );
148
          return vms.Success();
149 150
        } else {
          // Failed to resume due to another reason. Fail hard.
151
          throw e; // ignore: only_throw_errors, proxying the error from upstream.
152 153 154 155
        }
      });
    }

156
    /// Waits for a signal from the VM service that the extension is registered.
157 158 159 160
    ///
    /// Looks at the list of loaded extensions for the current [isolateRef], as
    /// well as the stream of added extensions.
    Future<void> waitForServiceExtension() async {
161 162 163
      await client.streamListen(vms.EventStreams.kIsolate);

      final Future<void> extensionAlreadyAdded = client
164
        .getIsolate(isolateRef.id!)
165
        .then((vms.Isolate isolate) async {
166
          if (isolate.extensionRPCs!.contains(_flutterExtensionMethodName)) {
167 168 169 170 171 172 173 174
            return;
          }
          // Never complete. Rely on the stream listener to find the service
          // extension instead.
          return Completer<void>().future;
        });

      final Completer<void> extensionAdded = Completer<void>();
175
      late StreamSubscription<vms.Event> isolateAddedSubscription;
176 177 178 179

      isolateAddedSubscription = client.onIsolateEvent.listen(
        (vms.Event data) {
          if (data.kind == vms.EventKind.kServiceExtensionAdded && data.extensionRPC == _flutterExtensionMethodName) {
180 181 182 183 184
            extensionAdded.complete();
            isolateAddedSubscription.cancel();
          }
        },
        onError: extensionAdded.completeError,
185 186
        cancelOnError: true,
      );
187 188 189 190 191

      await Future.any(<Future<void>>[
        extensionAlreadyAdded,
        extensionAdded.future,
      ]);
192 193
      await isolateAddedSubscription.cancel();
      await client.streamCancel(vms.EventStreams.kIsolate);
194 195
    }

196
    // Attempt to resume isolate if it was paused
197
    if (isolate.pauseEvent!.kind == vms.EventKind.kPauseStart) {
198 199 200
      _log('Isolate is paused at start.');

      await resumeLeniently();
201 202 203 204
    } else if (isolate.pauseEvent!.kind == vms.EventKind.kPauseExit ||
        isolate.pauseEvent!.kind == vms.EventKind.kPauseBreakpoint ||
        isolate.pauseEvent!.kind == vms.EventKind.kPauseException ||
        isolate.pauseEvent!.kind == vms.EventKind.kPauseInterrupted) {
205 206 207 208
      // If the isolate is paused for any other reason, assume the extension is
      // already there.
      _log('Isolate is paused mid-flight.');
      await resumeLeniently();
209
    } else if (isolate.pauseEvent!.kind == vms.EventKind.kResume) {
210 211 212 213
      _log('Isolate is not paused. Assuming application is ready.');
    } else {
      _log(
          'Unknown pause event type ${isolate.pauseEvent.runtimeType}. '
214
          'Assuming application is ready.'
215 216 217
      );
    }

218 219 220 221 222 223 224 225 226 227
    // We will never receive the extension event if the user does not register
    // it. If that happens, show a message but continue waiting.
    await _warnIfSlow<void>(
      future: waitForServiceExtension(),
      timeout: kUnusuallyLongTimeout,
      message: 'Flutter Driver extension is taking a long time to become available. '
          'Ensure your test app (often "lib/main.dart") imports '
          '"package:flutter_driver/driver_extension.dart" and '
          'calls enableFlutterDriverExtension() as the first call in main().',
    );
228

229
    final Health health = await driver.checkHealth();
230
    if (health.status != HealthStatus.ok) {
231
      await client.dispose();
232
      await client.onDone;
233 234 235 236 237 238 239 240 241 242 243 244 245 246
      throw DriverError('Flutter application health check failed.');
    }

    _log('Connected to Flutter application.');
    return driver;
  }

  static int _nextDriverId = 0;

  static const String _flutterExtensionMethodName = 'ext.flutter.driver';
  static const String _collectAllGarbageMethodName = '_collectAllGarbage';

  // The additional blank line in the beginning is for _log.
  static const String _kDebugWarning = '''
247

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
┇ ⚠    THIS BENCHMARK IS BEING RUN IN DEBUG MODE     ⚠  ┇
┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦
│                                                       │
│  Numbers obtained from a benchmark while asserts are  │
│  enabled will not accurately reflect the performance  │
│  that will be experienced by end users using release  ╎
│  builds. Benchmarks should be run using this command  ┆
│  line:  flutter drive --profile test_perf.dart        ┊
│                                                       ┊
└─────────────────────────────────────────────────╌┄┈  🐢
''';
  /// The unique ID of this driver instance.
  final int _driverId;

263 264 265
  @override
  vms.Isolate get appIsolate => _appIsolate;

266
  /// Client connected to the Dart VM running the Flutter application.
267 268 269 270 271
  ///
  /// You can use [VMServiceClient] to check VM version, flags and get
  /// notified when a new isolate has been instantiated. That could be
  /// useful if your application spawns multiple isolates that you
  /// would like to instrument.
272
  final vms.VmService _serviceClient;
273 274

  @override
275
  vms.VmService get serviceClient => _serviceClient;
276

277 278 279
  @override
  async_io.WebDriver get webDriver => throw UnsupportedError('VMServiceFlutterDriver does not support webDriver');

280 281 282
  /// The main isolate hosting the Flutter application.
  ///
  /// If you used the [registerExtension] API to instrument your application,
283
  /// you can use this [vms.Isolate] to call these extension methods via
284
  /// [invokeExtension].
285
  final vms.Isolate _appIsolate;
286 287 288 289 290 291 292

  /// Whether to print communication between host and app to `stdout`.
  final bool _printCommunication;

  /// Whether to log communication between host and app to `flutter_driver_commands.log`.
  final bool _logCommunicationToFile;

293 294 295 296 297 298
  /// Logs are written here when _logCommunicationToFile is true.
  late final String _logFilePathName;

  /// Getter for file pathname where logs are written when _logCommunicationToFile is true.
  String get logFilePathName => _logFilePathName;

299

300 301
  @override
  Future<Map<String, dynamic>> sendCommand(Command command) async {
302
    late Map<String, dynamic> response;
303 304 305
    try {
      final Map<String, String> serialized = command.serialize();
      _logCommunication('>>> $serialized');
306
      final Future<Map<String, dynamic>> future = _serviceClient.callServiceExtension(
307
        _flutterExtensionMethodName,
308 309
        isolateId: _appIsolate.id,
        args: serialized,
310
      ).then<Map<String, dynamic>>((vms.Response value) => value.json!);
311
      response = await _warnIfSlow<Map<String, dynamic>>(
312 313 314
        future: future,
        timeout: command.timeout ?? kUnusuallyLongTimeout,
        message: '${command.kind} message is taking a long time to complete...',
315
      );
316 317 318 319 320 321 322 323
      _logCommunication('<<< $response');
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to fulfill ${command.runtimeType} due to remote error',
        error,
        stackTrace,
      );
    }
324
    if ((response['isError'] as bool?) ?? false) {
325
      throw DriverError('Error in Flutter application: ${response['response']}');
326
    }
327 328 329 330
    return response['response'] as Map<String, dynamic>;
  }

  void _logCommunication(String message) {
331
    if (_printCommunication) {
332
      _log(message);
333
    }
334
    if (_logCommunicationToFile) {
335 336
      assert(_logFilePathName != null);
      final f.File file = fs.file(_logFilePathName);
337 338 339 340 341 342 343 344 345
      file.createSync(recursive: true); // no-op if file exists
      file.writeAsStringSync('${DateTime.now()} $message\n', mode: f.FileMode.append, flush: true);
    }
  }

  @override
  Future<List<int>> screenshot() async {
    await Future<void>.delayed(const Duration(seconds: 2));

346
    final vms.Response result = await _serviceClient.callMethod('_flutter.screenshot');
347
    return base64.decode(result.json!['screenshot'] as String);
348 349 350 351
  }

  @override
  Future<List<Map<String, dynamic>>> getVmFlags() async {
352
    final vms.FlagList result = await _serviceClient.getFlagList();
353 354
    return result.flags != null
        ? result.flags!.map((vms.Flag flag) => flag.toJson()).toList()
355 356 357
        : const <Map<String, dynamic>>[];
  }

358
  Future<vms.Timestamp> _getVMTimelineMicros() async {
359
    return _serviceClient.getVMTimelineMicros();
360 361
  }

362 363 364 365 366 367 368 369
  @override
  Future<void> startTracing({
    List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
    Duration timeout = kUnusuallyLongTimeout,
  }) async {
    assert(streams != null && streams.isNotEmpty);
    assert(timeout != null);
    try {
370 371 372 373
      await _warnIfSlow<vms.Success>(
        future: _serviceClient.setVMTimelineFlags(
          _timelineStreamsToString(streams),
        ),
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to start tracing...',
      );
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to start tracing due to remote error',
        error,
        stackTrace,
      );
    }
  }

  @override
  Future<Timeline> stopTracingAndDownloadTimeline({
    Duration timeout = kUnusuallyLongTimeout,
389 390
    int? startTime,
    int? endTime,
391 392
  }) async {
    assert(timeout != null);
393 394 395
    assert((startTime == null && endTime == null) ||
           (startTime != null && endTime != null));

396
    try {
397 398
      await _warnIfSlow<vms.Success>(
        future: _serviceClient.setVMTimelineFlags(const <String>[]),
399 400 401
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to stop tracing...',
      );
402
      if (startTime == null) {
403
        final vms.Timeline timeline = await _serviceClient.getVMTimeline();
404
        return Timeline.fromJson(timeline.json!);
405 406 407 408
      }
      const int kSecondInMicros = 1000000;
      int currentStart = startTime;
      int currentEnd = startTime + kSecondInMicros; // 1 second of timeline
409
      final List<Map<String, Object?>?> chunks = <Map<String, Object?>?>[];
410
      do {
411 412
        final vms.Timeline chunk = await _serviceClient.getVMTimeline(
          timeOriginMicros: currentStart,
413 414
          // The range is inclusive, avoid double counting on the chance something
          // aligns on the boundary.
415 416 417
          timeExtentMicros: kSecondInMicros - 1,
        );
        chunks.add(chunk.json);
418 419
        currentStart = currentEnd;
        currentEnd += kSecondInMicros;
420
      } while (currentStart < endTime!);
421
      return Timeline.fromJson(<String, Object>{
422
        'traceEvents': <Object?> [
423
          for (Map<String, Object?>? chunk in chunks)
424
            ...chunk!['traceEvents']! as List<Object?>,
425 426
        ],
      });
427 428 429 430 431 432 433 434 435 436 437
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to stop tracing due to remote error',
        error,
        stackTrace,
      );
    }
  }

  Future<bool> _isPrecompiledMode() async {
    final List<Map<String, dynamic>> flags = await getVmFlags();
438
    for(final Map<String, dynamic> flag in flags) {
439 440 441 442 443 444 445 446 447
      if (flag['name'] == 'precompiled_mode') {
        return flag['valueAsString'] == 'true';
      }
    }
    return false;
  }

  @override
  Future<Timeline> traceAction(
448
      Future<dynamic> Function() action, {
449 450 451
        List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
        bool retainPriorEvents = false,
      }) async {
452 453 454 455 456 457 458 459 460
    if (retainPriorEvents) {
      await startTracing(streams: streams);
      await action();

      if (!(await _isPrecompiledMode())) {
        _log(_kDebugWarning);
      }

      return stopTracingAndDownloadTimeline();
461
    }
462 463 464

    await clearTimeline();

465
    final vms.Timestamp startTimestamp = await _getVMTimelineMicros();
466 467
    await startTracing(streams: streams);
    await action();
468
    final vms.Timestamp endTimestamp = await _getVMTimelineMicros();
469 470 471 472

    if (!(await _isPrecompiledMode())) {
      _log(_kDebugWarning);
    }
473 474

    return stopTracingAndDownloadTimeline(
475 476
      startTime: startTimestamp.timestamp,
      endTime: endTimestamp.timestamp,
477
    );
478 479 480 481 482 483 484 485
  }

  @override
  Future<void> clearTimeline({
    Duration timeout = kUnusuallyLongTimeout,
  }) async {
    assert(timeout != null);
    try {
486 487
      await _warnIfSlow<vms.Success>(
        future: _serviceClient.clearVMTimeline(),
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
        timeout: timeout,
        message: 'VM is taking an unusually long time to respond to being told to clear its timeline buffer...',
      );
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to clear event timeline due to remote error',
        error,
        stackTrace,
      );
    }
  }

  @override
  Future<void> forceGC() async {
    try {
503
      await _serviceClient.callMethod(_collectAllGarbageMethodName, isolateId: _appIsolate.id);
504 505 506 507 508 509 510 511 512 513 514
    } catch (error, stackTrace) {
      throw DriverError(
        'Failed to force a GC due to remote error',
        error,
        stackTrace,
      );
    }
  }

  @override
  Future<void> close() async {
515
    await _serviceClient.dispose();
516
    await _serviceClient.onDone;
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
  }
}

/// The connection function used by [FlutterDriver.connect].
///
/// Overwrite this function if you require a custom method for connecting to
/// the VM service.
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;

/// Restores [vmServiceConnectFunction] to its default value.
void restoreVmServiceConnectFunction() {
  vmServiceConnectFunction = _waitAndConnect;
}

String _getWebSocketUrl(String url) {
  Uri uri = Uri.parse(url);
  final List<String> pathSegments = <String>[
    // If there's an authentication code (default), we need to add it to our path.
    if (uri.pathSegments.isNotEmpty) uri.pathSegments.first,
    'ws',
  ];
538
  if (uri.scheme == 'http') {
539
    uri = uri.replace(scheme: 'ws', pathSegments: pathSegments);
540
  }
541 542 543 544 545
  return uri.toString();
}

/// Waits for a real Dart VM service to become available, then connects using
/// the [VMServiceClient].
546
Future<vms.VmService> _waitAndConnect(String url, Map<String, dynamic>? headers) async {
547 548
  final String webSocketUrl = _getWebSocketUrl(url);
  int attempts = 0;
549
  WebSocket? socket;
550 551
  while (true) {
    try {
552
      socket = await WebSocket.connect(webSocketUrl, headers: headers);
553 554 555 556 557 558
      final StreamController<dynamic> controller = StreamController<dynamic>();
      final Completer<void> streamClosedCompleter = Completer<void>();
      socket.listen(
        (dynamic data) => controller.add(data),
        onDone: () => streamClosedCompleter.complete(),
      );
559
      final vms.VmService service = vms.VmService(
560
        controller.stream,
561
        socket.add,
562
        disposeHandler: () => socket!.close(),
563
        streamClosed: streamClosedCompleter.future
564
      );
565 566 567 568
      // This call is to ensure we are able to establish a connection instead of
      // keeping on trucking and failing farther down the process.
      await service.getVersion();
      return service;
569
    } catch (e) {
570 571
      // We should not be catching all errors arbitrarily here, this might hide real errors.
      // TODO(ianh): Determine which exceptions to catch here.
572
      await socket?.close();
573
      if (attempts > 5) {
574
        _log('It is taking an unusually long time to connect to the VM...');
575
      }
576 577 578 579 580 581 582 583 584 585
      attempts += 1;
      await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
    }
  }
}

/// The amount of time we wait prior to making the next attempt to connect to
/// the VM service.
const Duration _kPauseBetweenReconnectAttempts = Duration(seconds: 1);

586
// See `timeline_streams` in
587
// https://github.com/dart-lang/sdk/blob/main/runtime/vm/timeline.cc
588 589
List<String> _timelineStreamsToString(List<TimelineStream> streams) {
  return streams.map<String>((TimelineStream stream) {
590 591 592 593
    switch (stream) {
      case TimelineStream.all: return 'all';
      case TimelineStream.api: return 'API';
      case TimelineStream.compiler: return 'Compiler';
594
      case TimelineStream.compilerVerbose: return 'CompilerVerbose';
595 596 597 598 599 600 601
      case TimelineStream.dart: return 'Dart';
      case TimelineStream.debugger: return 'Debugger';
      case TimelineStream.embedder: return 'Embedder';
      case TimelineStream.gc: return 'GC';
      case TimelineStream.isolate: return 'Isolate';
      case TimelineStream.vm: return 'VM';
    }
602
  }).toList();
603 604 605 606 607
}

void _log(String message) {
  driverLog('VMServiceFlutterDriver', message);
}
608

609 610
Future<T> _warnIfSlow<T>({
  required Future<T> future,
611 612
  required Duration timeout,
  required String message,
613
}) async {
614 615 616
  assert(future != null);
  assert(timeout != null);
  assert(message != null);
617 618 619 620 621 622 623 624
  final Completer<void> completer = Completer<void>();
  completer.future.timeout(timeout, onTimeout: () {
    _log(message);
    return null;
  });
  try {
    await future.whenComplete(() { completer.complete(); });
  } catch (e) {
625
    // Don't duplicate errors if [future] completes with an error.
626
  }
627
  return future;
628 629
}

630
/// A function that connects to a Dart VM service given the `url` and `headers`.
631
typedef VMServiceConnectFunction = Future<vms.VmService> Function(String url, Map<String, dynamic>? headers);