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
  // all at the end of this method.
190
  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
      .then<vm_service.Success?>((vm_service.Success success) => success)
      .catchError((Object? error) => null, test: (Object? error) => error is vm_service.RPCError)
294 295 296 297 298 299 300
    );
  }

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

305 306 307 308 309 310 311 312
/// 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
313
Future<FlutterVmService> connectToVmService(
314
  Uri httpUri, {
315 316 317 318 319
    ReloadSources? reloadSources,
    Restart? restart,
    CompileExpression? compileExpression,
    GetSkSLMethod? getSkSLMethod,
    PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
320
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
321 322
    Device? device,
    required Logger logger,
323 324 325 326 327 328 329 330
  }) async {
  final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect;
  return connector(httpUri,
    reloadSources: reloadSources,
    restart: restart,
    compileExpression: compileExpression,
    compression: compression,
    device: device,
331
    getSkSLMethod: getSkSLMethod,
332
    printStructuredErrorLogMethod: printStructuredErrorLogMethod,
333
    logger: logger,
334
  );
335 336
}

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

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

368
  final vm_service.VmService service = await setUpVmService(
369 370 371 372
    reloadSources,
    restart,
    compileExpression,
    device,
373
    getSkSLMethod,
374
    printStructuredErrorLogMethod,
375 376 377 378 379 380
    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();
381
  return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri);
382
}
383

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

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

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

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

428
  final vm_service.IsolateRef? uiIsolate;
429 430 431
  final String id;

  bool get hasIsolate => uiIsolate != null;
432 433 434

  @override
  String toString() => id;
435

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

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

452
  final vm_service.VmService service;
453 454
  final Uri? wsAddress;
  final Uri? httpAddress;
455

456
  Future<vm_service.Response?> callMethodWrapper(
457
    String method, {
458 459
    String? isolateId,
    Map<String, Object?>? args
460 461
  }) async {
    try {
462
      return await service.callMethod(method, isolateId: isolateId, args: args);
463 464 465 466 467 468 469 470 471 472 473 474
    } 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;
    }
  }

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

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

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

  /// 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({
529 530 531
    required String viewId,
    required Uri main,
    required Uri assetsDirectory,
532 533
  }) async {
    try {
534
      await service.streamListen(vm_service.EventStreams.kIsolate);
535 536 537
    } on vm_service.RPCError {
      // Do nothing, since the tool is already subscribed.
    }
538
    final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) {
539 540
      return event.kind == vm_service.EventKind.kIsolateRunnable;
    });
541
    await callMethodWrapper(
542 543 544 545 546 547 548 549 550
      kRunInViewMethod,
      args: <String, Object>{
        'viewId': viewId,
        'mainScript': main.toString(),
        'assetDirectory': assetsDirectory.toString(),
      },
    );
    await onRunnable;
  }
551

552 553 554 555 556 557
  /// 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.
558
  Future<Map<String, Object?>?> renderFrameWithRasterStats({
559 560 561 562 563 564 565 566 567 568
    required String? viewId,
    required String? uiIsolateId,
  }) async {
    final vm_service.Response? response = await callMethodWrapper(
      kRenderFrameWithRasterStatsMethod,
      isolateId: uiIsolateId,
      args: <String, String?>{
        'viewId': viewId,
      },
    );
569
    return response?.json;
570 571
  }

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

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

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

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

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

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

    return state;
  }

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

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

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

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

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

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

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

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

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

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

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

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


743 744 745 746
  /// Exit the application by calling [exit] from `dart:io`.
  ///
  /// This method is only supported by certain embedders. This is
  /// described by [Device.supportsFlutterExit].
747
  Future<bool> flutterExit({
748
    required String isolateId,
749 750
  }) async {
    try {
751
      final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
752 753 754 755
        'ext.flutter.exit',
        isolateId: isolateId,
      );
      // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError
756
      // with a missing method code. This can happen when attempting to quit a Flutter app
757 758 759 760 761 762 763 764 765 766
      // 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;
767 768 769 770 771 772 773 774
  }

  /// 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({
775 776
    String? platform,
    required String isolateId,
777
  }) async {
778
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
779 780 781
      'ext.flutter.platformOverride',
      isolateId: isolateId,
      args: platform != null
782
        ? <String, Object>{'value': platform}
783 784 785
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
786
      return result['value']! as String;
787 788 789 790
    }
    return 'unknown';
  }

791 792 793 794 795
  /// 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.
796 797 798
  Future<Brightness?> flutterBrightnessOverride({
    Brightness? brightness,
    required String isolateId,
799
  }) async {
800
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
801 802 803
      'ext.flutter.brightnessOverride',
      isolateId: isolateId,
      args: brightness != null
804
        ? <String, String>{'value': brightness.toString()}
805 806 807
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
808
      return result['value'] == 'Brightness.light'
809 810 811 812 813 814
        ? Brightness.light
        : Brightness.dark;
    }
    return null;
  }

815
  Future<vm_service.Response?> _checkedCallServiceExtension(
816
    String method, {
817
    Map<String, Object?>? args,
818 819
  }) async {
    try {
820
      return await service.callServiceExtension(method, args: args);
821
    } on vm_service.RPCError catch (err) {
822 823 824 825
      // 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)) {
826 827 828 829 830
        return null;
      }
      rethrow;
    }
  }
831

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

849
  /// List all [FlutterView]s attached to the current VM.
850 851 852 853 854 855 856 857 858 859
  ///
  /// 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) {
860
      final vm_service.Response? response = await callMethodWrapper(
861 862
        kListViewsMethod,
      );
863
      if (response == null) {
864 865 866 867
        // The service may have disappeared mid-request.
        // Return an empty list now, and let the shutdown logic elsewhere deal
        // with cleaning up.
        return <FlutterView>[];
868
      }
869
      final List<Object?>? rawViews = response.json?['views'] as List<Object?>?;
870
      final List<FlutterView> views = <FlutterView>[
871
        if (rawViews != null)
872
          for (final Map<String, Object?> rawView in rawViews.whereType<Map<String, Object?>>())
873
            FlutterView.parse(rawView),
874 875 876 877 878 879
      ];
      if (views.isNotEmpty || returnEarly) {
        return views;
      }
      await Future<void>.delayed(delay);
    }
880 881
  }

882 883 884 885 886 887 888 889 890 891 892 893 894 895
  /// 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,
      },
    );
  }

896 897 898 899 900
  /// 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.
  ///
901 902 903
  /// If [webIsolate] is true, this uses the VM Service isolate list instead of
  /// the `_flutter.listViews` method, which is not implemented by DWDS.
  ///
904 905
  /// Throws a [VmServiceDisappearedException] should the VM Service disappear
  /// while making calls to it.
906
  Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async {
907 908 909 910 911 912 913
    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>();
914
    late final StreamSubscription<vm_service.Event> isolateEvents;
915 916 917
    isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) {
      if (event.kind == vm_service.EventKind.kServiceExtensionAdded
          && event.extensionRPC == extensionName) {
918
        isolateEvents.cancel();
919 920 921 922 923
        extensionAdded.complete(event.isolate);
      }
    });

    try {
924
      final List<vm_service.IsolateRef> refs = await _getIsolateRefs();
925
      for (final vm_service.IsolateRef ref in refs) {
926
        final vm_service.Isolate? isolate = await getIsolateOrNull(ref.id!);
927
        if (isolate != null && (isolate.extensionRPCs?.contains(extensionName) ?? false)) {
928
          return ref;
929 930 931 932 933 934 935 936 937 938 939 940 941
        }
      }
      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.
      }
    }
  }

942
  Future<List<vm_service.IsolateRef>> _getIsolateRefs() async {
943 944 945 946 947 948 949
    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) {
950
      final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate;
951 952
      if (uiIsolate != null) {
        refs.add(uiIsolate);
953 954 955 956 957
      }
    }
    return refs;
  }

958 959
  /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has
  /// been collected.
960
  Future<vm_service.Isolate?> getIsolateOrNull(String isolateId) async {
961
    return service.getIsolate(isolateId)
962 963 964
      // 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) {
965
        return null;
966
      }, test: (Object? error) {
967 968 969
        return (error is vm_service.SentinelException) ||
          (error is vm_service.RPCError && error.code == RPCErrorCodes.kServiceDisappeared);
      });
970
  }
971 972 973

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

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

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

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

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

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

  Future<void> dispose() async {
     await service.dispose();
  }
1016 1017
}

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

1021 1022 1023 1024 1025 1026 1027 1028 1029 1030
/// 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;
1031
}
1032

1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
/// 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,
}
1048 1049 1050

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