vmservice.dart 35 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
import 'dart:async';

7
import 'package:meta/meta.dart' show visibleForTesting;
8
import 'package:vm_service/vm_service.dart' as vm_service;
9

10
import 'base/common.dart';
11
import 'base/context.dart';
12
import 'base/io.dart' as io;
13
import 'base/logger.dart';
14
import 'base/utils.dart';
15
import 'convert.dart';
16
import 'device.dart';
17
import 'version.dart';
18

19 20 21
const String kGetSkSLsMethod = '_flutter.getSkSLs';
const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath';
const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks';
22
const String kRunInViewMethod = '_flutter.runInView';
23
const String kListViewsMethod = '_flutter.listViews';
24 25
const String kScreenshotSkpMethod = '_flutter.screenshotSkp';
const String kScreenshotMethod = '_flutter.screenshot';
26
const String kRenderFrameWithRasterStatsMethod = '_flutter.renderFrameWithRasterStats';
27
const String kReloadAssetFonts = '_flutter.reloadAssetFonts';
28

29 30 31
/// The error response code from an unrecoverable compilation failure.
const int kIsolateReloadBarred = 1005;

32 33
/// Override `WebSocketConnector` in [context] to use a different constructor
/// for [WebSocket]s (used by tests).
34
typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression, required Logger logger});
35

36 37
typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event);

38
WebSocketConnector _openChannel = _defaultOpenChannel;
39

40 41 42 43
/// A testing only override of the WebSocket connector.
///
/// Provide a `null` value to restore the original connector.
@visibleForTesting
44
set openChannelForTesting(WebSocketConnector? connector) {
45 46 47
  _openChannel = connector ?? _defaultOpenChannel;
}

48 49
/// The error codes for the JSON-RPC standard, including VM service specific
/// error codes.
50 51 52 53 54 55 56 57 58 59 60 61
///
/// See also: https://www.jsonrpc.org/specification#error_object
abstract class RPCErrorCodes {
  /// The method does not exist or is not available.
  static const int kMethodNotFound = -32601;

  /// Invalid method parameter(s), such as a mismatched type.
  static const int kInvalidParams = -32602;

  /// Internal JSON-RPC error.
  static const int kInternalError = -32603;

62
  /// Application specific error codes.
63
  static const int kServerError = -32000;
64 65 66 67 68

  /// Non-standard JSON-RPC error codes:

  /// The VM service or extension service has disappeared.
  static const int kServiceDisappeared = 112;
69
}
70

71 72 73 74 75 76 77 78 79 80 81
/// A function that reacts to the invocation of the 'reloadSources' service.
///
/// The VM Service Protocol allows clients to register custom services that
/// can be invoked by other clients through the service protocol itself.
///
/// Clients like Observatory use external 'reloadSources' services,
/// when available, instead of the VM internal one. This allows these clients to
/// invoke Flutter HotReload when connected to a Flutter Application started in
/// hot mode.
///
/// See: https://github.com/dart-lang/sdk/issues/30023
82
typedef ReloadSources = Future<void> Function(
83 84 85 86 87
  String isolateId, {
  bool force,
  bool pause,
});

88 89
typedef Restart = Future<void> Function({ bool pause });

90
typedef CompileExpression = Future<String> Function(
91 92 93 94 95
  String isolateId,
  String expression,
  List<String> definitions,
  List<String> typeDefinitions,
  String libraryUri,
96
  String? klass,
97 98 99
  bool isStatic,
);

100 101 102
/// A method that pulls an SkSL shader from the device and writes it to a file.
///
/// The name of the file returned as a result.
103
typedef GetSkSLMethod = Future<String?> Function();
104

105
Future<io.WebSocket> _defaultOpenChannel(String url, {
106
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
107
  required Logger logger,
108
}) async {
109 110
  Duration delay = const Duration(milliseconds: 100);
  int attempts = 0;
111
  io.WebSocket? socket;
112

113
  Future<void> handleError(Object? e) async {
114
    void Function(String) printVisibleTrace = logger.printTrace;
115
    if (attempts == 10) {
116
      logger.printStatus('Connecting to the VM Service is taking longer than expected...');
117
    } else if (attempts == 20) {
118 119
      logger.printStatus('Still attempting to connect to the VM Service...');
      logger.printStatus(
120 121 122
        'If you do NOT see the Flutter application running, it might have '
        'crashed. The device logs (e.g. from adb or XCode) might have more '
        'details.');
123
      logger.printStatus(
124 125 126 127
        'If you do see the Flutter application running on the device, try '
        're-running with --host-vmservice-port to use a specific port known to '
        'be available.');
    } else if (attempts % 50 == 0) {
128
      printVisibleTrace = logger.printStatus;
129
    }
130

131 132 133
    printVisibleTrace('Exception attempting to connect to the VM Service: $e');
    printVisibleTrace('This was attempt #$attempts. Will retry in $delay.');

134
    // Delay next attempt.
135
    await Future<void>.delayed(delay);
136

137
    // Back off exponentially, up to 1600ms per attempt.
138
    if (delay < const Duration(seconds: 1)) {
139
      delay *= 2;
140
    }
141 142
  }

143 144
  final WebSocketConnector constructor = context.get<WebSocketConnector>() ?? (String url, {
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
145
    Logger? logger,
146 147
  }) => io.WebSocket.connect(url, compression: compression);

148
  while (socket == null) {
149 150
    attempts += 1;
    try {
151
      socket = await constructor(url, compression: compression, logger: logger);
152
    } on io.WebSocketException catch (e) {
153
      await handleError(e);
154
    } on io.SocketException catch (e) {
155
      await handleError(e);
156 157
    }
  }
158
  return socket;
159
}
160

161 162
/// Override `VMServiceConnector` in [context] to return a different VMService
/// from [VMService.connect] (used by tests).
163
typedef VMServiceConnector = Future<FlutterVmService> Function(Uri httpUri, {
164 165 166 167 168
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
169
  io.CompressionOptions compression,
170 171
  Device? device,
  required Logger logger,
172
});
173

174 175 176 177 178
/// Set up the VM Service client by attaching services for each of the provided
/// callbacks.
///
/// All parameters besides [vmService] may be null.
Future<vm_service.VmService> setUpVmService(
179 180 181 182 183 184
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  Device? device,
  GetSkSLMethod? skSLMethod,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
185
  vm_service.VmService vmService
186 187
) async {
  // Each service registration requires a request to the attached VM service. Since the
188
  // order of these requests does not matter, store each future in a list and await
189 190
  // all at the end of this method.
  final List<Future<vm_service.Success>> registrationRequests = <Future<vm_service.Success>>[];
191
  if (reloadSources != null) {
192
    vmService.registerServiceCallback('reloadSources', (Map<String, Object?> params) async {
193 194 195 196 197 198
      final String isolateId = _validateRpcStringParam('reloadSources', params, 'isolateId');
      final bool force = _validateRpcBoolParam('reloadSources', params, 'force');
      final bool pause = _validateRpcBoolParam('reloadSources', params, 'pause');

      await reloadSources(isolateId, force: force, pause: pause);

199
      return <String, Object>{
200 201
        'result': <String, Object>{
          'type': 'Success',
202
        },
203
      };
204
    });
205
    registrationRequests.add(vmService.registerService('reloadSources', 'Flutter Tools'));
206 207 208
  }

  if (restart != null) {
209
    vmService.registerServiceCallback('hotRestart', (Map<String, Object?> params) async {
210 211
      final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause');
      await restart(pause: pause);
212
      return <String, Object>{
213 214
        'result': <String, Object>{
          'type': 'Success',
215
        },
216
      };
217
    });
218
    registrationRequests.add(vmService.registerService('hotRestart', 'Flutter Tools'));
219 220
  }

221
  vmService.registerServiceCallback('flutterVersion', (Map<String, Object?> params) async {
222 223 224 225
    final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion();
    final Map<String, Object> versionJson = version.toJson();
    versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort;
    versionJson['engineRevisionShort'] = version.engineRevisionShort;
226
    return <String, Object>{
227 228 229
      'result': <String, Object>{
        'type': 'Success',
        ...versionJson,
230
      },
231
    };
232
  });
233
  registrationRequests.add(vmService.registerService('flutterVersion', 'Flutter Tools'));
234 235

  if (compileExpression != null) {
236
    vmService.registerServiceCallback('compileExpression', (Map<String, Object?> params) async {
237 238
      final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId');
      final String expression = _validateRpcStringParam('compileExpression', params, 'expression');
239 240 241 242
      final List<String> definitions = List<String>.from(params['definitions']! as List<Object?>);
      final List<String> typeDefinitions = List<String>.from(params['typeDefinitions']! as List<Object?>);
      final String libraryUri = params['libraryUri']! as String;
      final String? klass = params['klass'] as String?;
243 244 245 246 247
      final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic');

      final String kernelBytesBase64 = await compileExpression(isolateId,
          expression, definitions, typeDefinitions, libraryUri, klass,
          isStatic);
248
      return <String, Object>{
249
        'type': 'Success',
250
        'result': <String, String>{'kernelBytes': kernelBytesBase64},
251
      };
252
    });
253
    registrationRequests.add(vmService.registerService('compileExpression', 'Flutter Tools'));
254
  }
255
  if (device != null) {
256
    vmService.registerServiceCallback('flutterMemoryInfo', (Map<String, Object?> params) async {
257
      final MemoryInfo result = await device.queryMemoryInfo();
258
      return <String, Object>{
259 260 261
        'result': <String, Object>{
          'type': 'Success',
          ...result.toJson(),
262
        },
263
      };
264
    });
265
    registrationRequests.add(vmService.registerService('flutterMemoryInfo', 'Flutter Tools'));
266
  }
267
  if (skSLMethod != null) {
268
    vmService.registerServiceCallback('flutterGetSkSL', (Map<String, Object?> params) async {
269 270 271 272 273 274 275 276
      final String? filename = await skSLMethod();
      if (filename == null) {
        return <String, Object>{
          'result': <String, Object>{
            'type': 'Success',
          },
        };
      }
277
      return <String, Object>{
278 279 280
        'result': <String, Object>{
          'type': 'Success',
          'filename': filename,
281
        },
282 283
      };
    });
284
    registrationRequests.add(vmService.registerService('flutterGetSkSL', 'Flutter Tools'));
285
  }
286 287
  if (printStructuredErrorLogMethod != null) {
    vmService.onExtensionEvent.listen(printStructuredErrorLogMethod);
288 289 290 291
    // It is safe to ignore this error because we expect an error to be
    // thrown if we're already subscribed.
    registrationRequests.add(vmService
      .streamListen(vm_service.EventStreams.kExtension)
292 293 294
      // TODO(srawlins): Fix this static issue,
      // https://github.com/flutter/flutter/issues/105750.
      // ignore: body_might_complete_normally_catch_error
295
      .catchError((Object? error) {}, test: (Object? error) => error is vm_service.RPCError)
296 297 298 299 300 301 302
    );
  }

  try {
    await Future.wait(registrationRequests);
  } on vm_service.RPCError catch (e) {
    throwToolExit('Failed to register service methods on attached VM Service: $e');
303
  }
304
  return vmService;
305
}
306

307 308 309 310 311 312 313 314
/// Connect to a Dart VM Service at [httpUri].
///
/// If the [reloadSources] parameter is not null, the 'reloadSources' service
/// will be registered. The VM Service Protocol allows clients to register
/// custom services that can be invoked by other clients through the service
/// protocol itself.
///
/// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217
315
Future<FlutterVmService> connectToVmService(
316
  Uri httpUri, {
317 318 319 320 321
    ReloadSources? reloadSources,
    Restart? restart,
    CompileExpression? compileExpression,
    GetSkSLMethod? getSkSLMethod,
    PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
322
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
323 324
    Device? device,
    required Logger logger,
325 326 327 328 329 330 331 332
  }) async {
  final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect;
  return connector(httpUri,
    reloadSources: reloadSources,
    restart: restart,
    compileExpression: compileExpression,
    compression: compression,
    device: device,
333
    getSkSLMethod: getSkSLMethod,
334
    printStructuredErrorLogMethod: printStructuredErrorLogMethod,
335
    logger: logger,
336
  );
337 338
}

339 340
Future<vm_service.VmService> createVmServiceDelegate(
  Uri wsUri, {
341
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
342
  required Logger logger,
343
}) async {
344 345 346 347 348 349 350 351 352 353
  final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger);
  return vm_service.VmService(
    channel,
    channel.add,
    disposeHandler: () async {
      await channel.close();
    },
  );
}

354
Future<FlutterVmService> _connect(
355
  Uri httpUri, {
356 357 358 359 360
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
361
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
362 363
  Device? device,
  required Logger logger,
364
}) async {
365
  final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws'));
366 367
  final vm_service.VmService delegateService = await createVmServiceDelegate(
    wsUri, compression: compression, logger: logger,
368 369
  );

370
  final vm_service.VmService service = await setUpVmService(
371 372 373 374
    reloadSources,
    restart,
    compileExpression,
    device,
375
    getSkSLMethod,
376
    printStructuredErrorLogMethod,
377 378 379 380 381 382
    delegateService,
  );

  // This call is to ensure we are able to establish a connection instead of
  // keeping on trucking and failing farther down the process.
  await delegateService.getVersion();
383
  return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri);
384
}
385

386 387 388
String _validateRpcStringParam(String methodName, Map<String, Object?> params, String paramName) {
  final Object? value = params[paramName];
  if (value is! String || value.isEmpty) {
389 390 391 392 393 394
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
395
  return value;
396 397
}

398 399
bool _validateRpcBoolParam(String methodName, Map<String, Object?> params, String paramName) {
  final Object? value = params[paramName];
400 401 402 403 404 405 406
  if (value != null && value is! bool) {
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
407
  return (value as bool?) ?? false;
408 409
}

410
/// Peered to an Android/iOS FlutterView widget on a device.
411 412
class FlutterView {
  FlutterView({
413 414
    required this.id,
    required this.uiIsolate,
415
  });
416

417
  factory FlutterView.parse(Map<String, Object?> json) {
418 419
    final Map<String, Object?>? rawIsolate = json['isolate'] as Map<String, Object?>?;
    vm_service.IsolateRef? isolate;
420 421 422 423 424
    if (rawIsolate != null) {
      rawIsolate['number'] = rawIsolate['number']?.toString();
      isolate = vm_service.IsolateRef.parse(rawIsolate);
    }
    return FlutterView(
425
      id: json['id']! as String,
426 427
      uiIsolate: isolate,
    );
428
  }
429

430
  final vm_service.IsolateRef? uiIsolate;
431 432 433
  final String id;

  bool get hasIsolate => uiIsolate != null;
434 435 436

  @override
  String toString() => id;
437

438 439
  Map<String, Object?> toJson() {
    return <String, Object?>{
440 441 442 443
      'id': id,
      'isolate': uiIsolate?.toJson(),
    };
  }
444 445 446
}

/// Flutter specific VM Service functionality.
447
class FlutterVmService {
448 449 450 451 452
  FlutterVmService(
    this.service, {
    this.wsAddress,
    this.httpAddress,
  });
453

454
  final vm_service.VmService service;
455 456
  final Uri? wsAddress;
  final Uri? httpAddress;
457

458
  Future<vm_service.Response?> callMethodWrapper(
459
    String method, {
460 461
    String? isolateId,
    Map<String, Object?>? args
462 463
  }) async {
    try {
464
      return await service.callMethod(method, isolateId: isolateId, args: args);
465 466 467 468 469 470 471 472 473 474 475 476
    } on vm_service.RPCError catch (e) {
      // If the service disappears mid-request the tool is unable to recover
      // and should begin to shutdown due to the service connection closing.
      // Swallow the exception here and let the shutdown logic elsewhere deal
      // with cleaning up.
      if (e.code == RPCErrorCodes.kServiceDisappeared) {
        return null;
      }
      rethrow;
    }
  }

477 478
  /// Set the asset directory for the an attached Flutter view.
  Future<void> setAssetDirectory({
479 480 481
    required Uri assetsDirectory,
    required String? viewId,
    required String? uiIsolateId,
482
    required bool windows,
483
  }) async {
484
    await callMethodWrapper(kSetAssetBundlePathMethod,
485
      isolateId: uiIsolateId,
486
      args: <String, Object?>{
487
        'viewId': viewId,
488
        'assetDirectory': assetsDirectory.toFilePath(windows: windows),
489 490 491
      });
  }

492
  /// Retrieve the cached SkSL shaders from an attached Flutter view.
493 494
  ///
  /// This method will only return data if `--cache-sksl` was provided as a
495
  /// flutter run argument, and only then on physical devices.
496
  Future<Map<String, Object?>?> getSkSLs({
497
    required String viewId,
498
  }) async {
499
    final vm_service.Response? response = await callMethodWrapper(
500 501 502
      kGetSkSLsMethod,
      args: <String, String>{
        'viewId': viewId,
503 504
      },
    );
505 506 507
    if (response == null) {
      return null;
    }
508
    return response.json?['SkSLs'] as Map<String, Object?>?;
509 510
  }

511
  /// Flush all tasks on the UI thread for an attached Flutter view.
512 513 514
  ///
  /// This method is currently used only for benchmarking.
  Future<void> flushUIThreadTasks({
515
    required String uiIsolateId,
516
  }) async {
517
    await callMethodWrapper(
518 519 520 521 522
      kFlushUIThreadTasksMethod,
      args: <String, String>{
        'isolateId': uiIsolateId,
      },
    );
523
  }
524 525 526 527 528 529 530

  /// Launch the Dart isolate with entrypoint [main] in the Flutter engine [viewId]
  /// with [assetsDirectory] as the devFS.
  ///
  /// This method is used by the tool to hot restart an already running Flutter
  /// engine.
  Future<void> runInView({
531 532 533
    required String viewId,
    required Uri main,
    required Uri assetsDirectory,
534 535
  }) async {
    try {
536
      await service.streamListen(vm_service.EventStreams.kIsolate);
537 538 539
    } on vm_service.RPCError {
      // Do nothing, since the tool is already subscribed.
    }
540
    final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) {
541 542
      return event.kind == vm_service.EventKind.kIsolateRunnable;
    });
543
    await callMethodWrapper(
544 545 546 547 548 549 550 551 552
      kRunInViewMethod,
      args: <String, Object>{
        'viewId': viewId,
        'mainScript': main.toString(),
        'assetDirectory': assetsDirectory.toString(),
      },
    );
    await onRunnable;
  }
553

554 555 556 557 558 559
  /// Renders the last frame with additional raster tracing enabled.
  ///
  /// When a frame is rendered using this method it will incur additional cost
  /// for rasterization which is not reflective of how long the frame takes in
  /// production. This is primarily intended to be used to identify the layers
  /// that result in the most raster perf degradation.
560
  Future<Map<String, Object?>?> renderFrameWithRasterStats({
561 562 563 564 565 566 567 568 569 570
    required String? viewId,
    required String? uiIsolateId,
  }) async {
    final vm_service.Response? response = await callMethodWrapper(
      kRenderFrameWithRasterStatsMethod,
      isolateId: uiIsolateId,
      args: <String, String?>{
        'viewId': viewId,
      },
    );
571
    return response?.json;
572 573
  }

574
  Future<String> flutterDebugDumpApp({
575
    required String isolateId,
576
  }) async {
577
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
578 579 580
      'ext.flutter.debugDumpApp',
      isolateId: isolateId,
    );
581
    return response?['data']?.toString() ?? '';
582 583
  }

584
  Future<String> flutterDebugDumpRenderTree({
585
    required String isolateId,
586
  }) async {
587
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
588 589
      'ext.flutter.debugDumpRenderTree',
      isolateId: isolateId,
590
      args: <String, Object>{}
591
    );
592
    return response?['data']?.toString() ?? '';
593 594
  }

595
  Future<String> flutterDebugDumpLayerTree({
596
    required String isolateId,
597
  }) async {
598
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
599 600 601
      'ext.flutter.debugDumpLayerTree',
      isolateId: isolateId,
    );
602
    return response?['data']?.toString() ?? '';
603 604
  }

605
  Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({
606
    required String isolateId,
607
  }) async {
608
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
609 610 611
      'ext.flutter.debugDumpSemanticsTreeInTraversalOrder',
      isolateId: isolateId,
    );
612
    return response?['data']?.toString() ?? '';
613 614
  }

615
  Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({
616
    required String isolateId,
617
  }) async {
618
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
619 620 621
      'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder',
      isolateId: isolateId,
    );
622 623 624 625
    if (response != null) {
      return response['data']?.toString() ?? '';
    }
    return '';
626 627
  }

628 629
  Future<Map<String, Object?>?> _flutterToggle(String name, {
    required String isolateId,
630
  }) async {
631
    Map<String, Object?>? state = await invokeFlutterExtensionRpcRaw(
632 633 634 635 636 637 638
      'ext.flutter.$name',
      isolateId: isolateId,
    );
    if (state != null && state.containsKey('enabled') && state['enabled'] is String) {
      state = await invokeFlutterExtensionRpcRaw(
        'ext.flutter.$name',
        isolateId: isolateId,
639
        args: <String, Object>{
640 641 642 643 644 645 646 647
          'enabled': state['enabled'] == 'true' ? 'false' : 'true',
        },
      );
    }

    return state;
  }

648 649
  Future<Map<String, Object?>?> flutterToggleDebugPaintSizeEnabled({
    required String isolateId,
650 651
  }) => _flutterToggle('debugPaint', isolateId: isolateId);

652 653
  Future<Map<String, Object?>?> flutterTogglePerformanceOverlayOverride({
    required String isolateId,
654 655
  }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId);

656 657
  Future<Map<String, Object?>?> flutterToggleWidgetInspector({
    required String isolateId,
658 659
  }) => _flutterToggle('inspector.show', isolateId: isolateId);

660 661
  Future<Map<String, Object?>?> flutterToggleInvertOversizedImages({
    required String isolateId,
662 663
  }) => _flutterToggle('invertOversizedImages', isolateId: isolateId);

664 665
  Future<Map<String, Object?>?> flutterToggleProfileWidgetBuilds({
    required String isolateId,
666 667
  }) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId);

668 669
  Future<Map<String, Object?>?> flutterDebugAllowBanner(bool show, {
    required String isolateId,
670 671 672 673
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugAllowBanner',
      isolateId: isolateId,
674
      args: <String, Object>{'enabled': show ? 'true' : 'false'},
675 676 677
    );
  }

678 679
  Future<Map<String, Object?>?> flutterReassemble({
    required String isolateId,
680 681 682 683 684 685 686
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.reassemble',
      isolateId: isolateId,
    );
  }

687 688 689
  Future<Map<String, Object?>?> flutterFastReassemble({
   required String isolateId,
   required String className,
690 691 692 693
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.fastReassemble',
      isolateId: isolateId,
694 695 696
      args: <String, Object>{
        'className': className,
      },
697 698 699 700
    );
  }

  Future<bool> flutterAlreadyPaintedFirstUsefulFrame({
701
    required String isolateId,
702
  }) async {
703
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
704 705 706 707
      'ext.flutter.didSendFirstFrameRasterizedEvent',
      isolateId: isolateId,
    );
    // result might be null when the service extension is not initialized
708
    return result?['enabled'] == 'true';
709 710
  }

711 712
  Future<Map<String, Object?>?> uiWindowScheduleFrame({
    required String isolateId,
713 714 715 716 717 718 719
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.scheduleFrame',
      isolateId: isolateId,
    );
  }

720 721
  Future<Map<String, Object?>?> flutterEvictAsset(String assetPath, {
   required String isolateId,
722 723 724 725
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.evict',
      isolateId: isolateId,
726
      args: <String, Object?>{
727 728 729 730 731
        'value': assetPath,
      },
    );
  }

732 733 734 735 736 737 738 739 740 741 742 743 744
  Future<Map<String, Object?>?> flutterEvictShader(String assetPath, {
   required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.reinitializeShader',
      isolateId: isolateId,
      args: <String, Object?>{
        'assetKey': assetPath,
      },
    );
  }


745 746 747 748
  /// Exit the application by calling [exit] from `dart:io`.
  ///
  /// This method is only supported by certain embedders. This is
  /// described by [Device.supportsFlutterExit].
749
  Future<bool> flutterExit({
750
    required String isolateId,
751 752
  }) async {
    try {
753
      final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
754 755 756 757
        'ext.flutter.exit',
        isolateId: isolateId,
      );
      // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError
758
      // with a missing method code. This can happen when attempting to quit a Flutter app
759 760 761 762 763 764 765 766 767 768
      // that never registered the methods in the bindings.
      if (result == null) {
        return false;
      }
    } on vm_service.SentinelException {
      // Do nothing on sentinel, the isolate already exited.
    } on vm_service.RPCError {
      // Do nothing on RPCError, the isolate already exited.
    }
    return true;
769 770 771 772 773 774 775 776
  }

  /// Return the current platform override for the flutter view running with
  /// the main isolate [isolateId].
  ///
  /// If a non-null value is provided for [platform], the platform override
  /// is updated with this value.
  Future<String> flutterPlatformOverride({
777 778
    String? platform,
    required String isolateId,
779
  }) async {
780
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
781 782 783
      'ext.flutter.platformOverride',
      isolateId: isolateId,
      args: platform != null
784
        ? <String, Object>{'value': platform}
785 786 787
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
788
      return result['value']! as String;
789 790 791 792
    }
    return 'unknown';
  }

793 794 795 796 797
  /// Return the current brightness value for the flutter view running with
  /// the main isolate [isolateId].
  ///
  /// If a non-null value is provided for [brightness], the brightness override
  /// is updated with this value.
798 799 800
  Future<Brightness?> flutterBrightnessOverride({
    Brightness? brightness,
    required String isolateId,
801
  }) async {
802
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
803 804 805
      'ext.flutter.brightnessOverride',
      isolateId: isolateId,
      args: brightness != null
806
        ? <String, String>{'value': brightness.toString()}
807 808 809
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
810
      return result['value'] == 'Brightness.light'
811 812 813 814 815 816
        ? Brightness.light
        : Brightness.dark;
    }
    return null;
  }

817
  Future<vm_service.Response?> _checkedCallServiceExtension(
818
    String method, {
819
    Map<String, Object?>? args,
820 821
  }) async {
    try {
822
      return await service.callServiceExtension(method, args: args);
823
    } on vm_service.RPCError catch (err) {
824 825 826 827
      // If an application is not using the framework or the VM service
      // disappears while handling a request, return null.
      if ((err.code == RPCErrorCodes.kMethodNotFound)
          || (err.code == RPCErrorCodes.kServiceDisappeared)) {
828 829 830 831 832
        return null;
      }
      rethrow;
    }
  }
833

834 835
  /// Invoke a flutter extension method, if the flutter extension is not
  /// available, returns null.
836
  Future<Map<String, Object?>?> invokeFlutterExtensionRpcRaw(
837
    String method, {
838 839
    required String isolateId,
    Map<String, Object?>? args,
840
  }) async {
841
    final vm_service.Response? response = await _checkedCallServiceExtension(
842
      method,
843
      args: <String, Object?>{
844 845 846 847 848 849 850
        'isolateId': isolateId,
        ...?args,
      },
    );
    return response?.json;
  }

851
  /// List all [FlutterView]s attached to the current VM.
852 853 854 855 856 857 858 859 860 861
  ///
  /// If this returns an empty list, it will poll forever unless [returnEarly]
  /// is set to true.
  ///
  /// By default, the poll duration is 50 milliseconds.
  Future<List<FlutterView>> getFlutterViews({
    bool returnEarly = false,
    Duration delay = const Duration(milliseconds: 50),
  }) async {
    while (true) {
862
      final vm_service.Response? response = await callMethodWrapper(
863 864
        kListViewsMethod,
      );
865
      if (response == null) {
866 867 868 869
        // The service may have disappeared mid-request.
        // Return an empty list now, and let the shutdown logic elsewhere deal
        // with cleaning up.
        return <FlutterView>[];
870
      }
871
      final List<Object?>? rawViews = response.json?['views'] as List<Object?>?;
872
      final List<FlutterView> views = <FlutterView>[
873
        if (rawViews != null)
874
          for (final Map<String, Object?> rawView in rawViews.whereType<Map<String, Object?>>())
875
            FlutterView.parse(rawView),
876 877 878 879 880 881
      ];
      if (views.isNotEmpty || returnEarly) {
        return views;
      }
      await Future<void>.delayed(delay);
    }
882 883
  }

884 885 886 887 888 889 890 891 892 893 894 895 896 897
  /// Tell the provided flutter view that the font manifest has been updated
  /// and asset fonts should be reloaded.
  Future<void> reloadAssetFonts({
    required String isolateId,
    required String viewId,
  }) async {
    await callMethodWrapper(
      kReloadAssetFonts,
      isolateId: isolateId, args: <String, Object?>{
        'viewId': viewId,
      },
    );
  }

898 899 900 901 902
  /// Waits for a signal from the VM service that [extensionName] is registered.
  ///
  /// Looks at the list of loaded extensions for first Flutter view, as well as
  /// the stream of added extensions to avoid races.
  ///
903 904 905
  /// If [webIsolate] is true, this uses the VM Service isolate list instead of
  /// the `_flutter.listViews` method, which is not implemented by DWDS.
  ///
906 907
  /// Throws a [VmServiceDisappearedException] should the VM Service disappear
  /// while making calls to it.
908
  Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async {
909 910 911 912 913 914 915
    try {
      await service.streamListen(vm_service.EventStreams.kIsolate);
    } on vm_service.RPCError {
      // Do nothing, since the tool is already subscribed.
    }

    final Completer<vm_service.IsolateRef> extensionAdded = Completer<vm_service.IsolateRef>();
916
    late final StreamSubscription<vm_service.Event> isolateEvents;
917 918 919
    isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) {
      if (event.kind == vm_service.EventKind.kServiceExtensionAdded
          && event.extensionRPC == extensionName) {
920
        isolateEvents.cancel();
921 922 923 924 925
        extensionAdded.complete(event.isolate);
      }
    });

    try {
926
      final List<vm_service.IsolateRef> refs = await _getIsolateRefs();
927
      for (final vm_service.IsolateRef ref in refs) {
928
        final vm_service.Isolate? isolate = await getIsolateOrNull(ref.id!);
929
        if (isolate != null && (isolate.extensionRPCs?.contains(extensionName) ?? false)) {
930
          return ref;
931 932 933 934 935 936 937 938 939 940 941 942 943
        }
      }
      return await extensionAdded.future;
    } finally {
      await isolateEvents.cancel();
      try {
        await service.streamCancel(vm_service.EventStreams.kIsolate);
      } on vm_service.RPCError {
        // It's ok for cleanup to fail, such as when the service disappears.
      }
    }
  }

944
  Future<List<vm_service.IsolateRef>> _getIsolateRefs() async {
945 946 947 948 949 950 951
    final List<FlutterView> flutterViews = await getFlutterViews();
    if (flutterViews.isEmpty) {
      throw VmServiceDisappearedException();
    }

    final List<vm_service.IsolateRef> refs = <vm_service.IsolateRef>[];
    for (final FlutterView flutterView in flutterViews) {
952
      final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate;
953 954
      if (uiIsolate != null) {
        refs.add(uiIsolate);
955 956 957 958 959
      }
    }
    return refs;
  }

960 961
  /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has
  /// been collected.
962
  Future<vm_service.Isolate?> getIsolateOrNull(String isolateId) async {
963
    return service.getIsolate(isolateId)
964 965 966
      // The .then() call is required to cast from Future<Isolate> to Future<Isolate?>
      .then<vm_service.Isolate?>((vm_service.Isolate isolate) => isolate)
      .catchError((Object? error, StackTrace stackTrace) {
967
        return null;
968
      }, test: (Object? error) {
969 970 971
        return (error is vm_service.SentinelException) ||
          (error is vm_service.RPCError && error.code == RPCErrorCodes.kServiceDisappeared);
      });
972
  }
973 974 975

  /// Create a new development file system on the device.
  Future<vm_service.Response> createDevFS(String fsName) {
976 977
    // Call the unchecked version of `callServiceExtension` because the caller
    // has custom handling of certain RPCErrors.
978
    return service.callServiceExtension(
979
      '_createDevFS',
980
      args: <String, Object?>{'fsName': fsName},
981
    );
982 983 984
  }

  /// Delete an existing file system.
985 986 987
  Future<void> deleteDevFS(String fsName) async {
    await _checkedCallServiceExtension(
      '_deleteDevFS',
988
      args: <String, Object?>{'fsName': fsName},
989
    );
990 991
  }

992
  Future<vm_service.Response?> screenshot() {
993
    return _checkedCallServiceExtension(kScreenshotMethod);
994 995
  }

996
  Future<vm_service.Response?> screenshotSkp() {
997
    return _checkedCallServiceExtension(kScreenshotSkpMethod);
998 999
  }

1000
  /// Set the VM timeline flags.
1001
  Future<void> setTimelineFlags(List<String> recordedStreams) async {
1002
    assert(recordedStreams != null);
1003
    await _checkedCallServiceExtension(
1004
      'setVMTimelineFlags',
1005
      args: <String, Object?>{
1006 1007 1008 1009 1010
        'recordedStreams': recordedStreams,
      },
    );
  }

1011
  Future<vm_service.Response?> getTimeline() {
1012
    return _checkedCallServiceExtension('getVMTimeline');
1013
  }
1014 1015 1016 1017

  Future<void> dispose() async {
     await service.dispose();
  }
1018 1019
}

1020
/// Thrown when the VM Service disappears while calls are being made to it.
1021
class VmServiceDisappearedException implements Exception { }
1022

1023 1024 1025 1026 1027 1028 1029 1030 1031 1032
/// Whether the event attached to an [Isolate.pauseEvent] should be considered
/// a "pause" event.
bool isPauseEvent(String kind) {
  return kind == vm_service.EventKind.kPauseStart ||
         kind == vm_service.EventKind.kPauseExit ||
         kind == vm_service.EventKind.kPauseBreakpoint ||
         kind == vm_service.EventKind.kPauseInterrupted ||
         kind == vm_service.EventKind.kPauseException ||
         kind == vm_service.EventKind.kPausePostRequest ||
         kind == vm_service.EventKind.kNone;
1033
}
1034

1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
/// A brightness enum that matches the values https://github.com/flutter/engine/blob/3a96741247528133c0201ab88500c0c3c036e64e/lib/ui/window.dart#L1328
/// Describes the contrast of a theme or color palette.
enum Brightness {
  /// The color is dark and will require a light text color to achieve readable
  /// contrast.
  ///
  /// For example, the color might be dark grey, requiring white text.
  dark,

  /// The color is light and will require a dark text color to achieve readable
  /// contrast.
  ///
  /// For example, the color might be bright white, requiring black text.
  light,
}
1050 1051 1052

/// Process a VM service log event into a string message.
String processVmServiceMessage(vm_service.Event event) {
1053
  final String message = utf8.decode(base64.decode(event.bytes!));
1054 1055 1056 1057 1058 1059
  // Remove extra trailing newlines appended by the vm service.
  if (message.endsWith('\n')) {
    return message.substring(0, message.length - 1);
  }
  return message;
}