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

5 6
// @dart = 2.8

7 8
import 'dart:async';

9
import 'package:meta/meta.dart' show required, visibleForTesting;
10
import 'package:vm_service/vm_service.dart' as vm_service;
11

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

21 22 23
const String kGetSkSLsMethod = '_flutter.getSkSLs';
const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath';
const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks';
24
const String kRunInViewMethod = '_flutter.runInView';
25
const String kListViewsMethod = '_flutter.listViews';
26 27
const String kScreenshotSkpMethod = '_flutter.screenshotSkp';
const String kScreenshotMethod = '_flutter.screenshot';
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 44 45 46 47
/// A testing only override of the WebSocket connector.
///
/// Provide a `null` value to restore the original connector.
@visibleForTesting
set openChannelForTesting(WebSocketConnector connector) {
  _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 96 97 98 99
  String isolateId,
  String expression,
  List<String> definitions,
  List<String> typeDefinitions,
  String libraryUri,
  String klass,
  bool isStatic,
);

100 101 102 103 104
/// 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.
typedef GetSkSLMethod = Future<String> Function();

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

113
  Future<void> handleError(dynamic 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 145 146 147
  final WebSocketConnector constructor = context.get<WebSocketConnector>() ?? (String url, {
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
    @required Logger logger,
  }) => io.WebSocket.connect(url, compression: compression);

148
  while (socket == null) {
149 150
    attempts += 1;
    try {
151
      socket = await constructor(url, compression: compression);
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
  ReloadSources reloadSources,
  Restart restart,
  CompileExpression compileExpression,
167
  GetSkSLMethod getSkSLMethod,
168
  PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
169 170
  io.CompressionOptions compression,
  Device device,
171
  @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
  ReloadSources reloadSources,
  Restart restart,
  CompileExpression compileExpression,
  Device device,
183
  GetSkSLMethod skSLMethod,
184
  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 192
  if (reloadSources != null) {
    vmService.registerServiceCallback('reloadSources', (Map<String, dynamic> params) async {
193 194 195 196 197 198 199 200 201 202 203
      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);

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

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

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

  if (compileExpression != null) {
    vmService.registerServiceCallback('compileExpression', (Map<String, dynamic> 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<dynamic>);
      final List<String> typeDefinitions = List<String>.from(params['typeDefinitions'] as List<dynamic>);
      final String libraryUri = params['libraryUri'] as String;
      final String klass = params['klass'] as String;
243 244 245 246 247 248 249
      final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic');

      final String kernelBytesBase64 = await compileExpression(isolateId,
          expression, definitions, typeDefinitions, libraryUri, klass,
          isStatic);
      return <String, dynamic>{
        'type': 'Success',
250
        'result': <String, dynamic>{'kernelBytes': kernelBytesBase64},
251
      };
252
    });
253
    registrationRequests.add(vmService.registerService('compileExpression', 'Flutter Tools'));
254
  }
255 256
  if (device != null) {
    vmService.registerServiceCallback('flutterMemoryInfo', (Map<String, dynamic> params) async {
257 258 259 260 261 262 263
      final MemoryInfo result = await device.queryMemoryInfo();
      return <String, dynamic>{
        'result': <String, Object>{
          'type': 'Success',
          ...result.toJson(),
        }
      };
264
    });
265
    registrationRequests.add(vmService.registerService('flutterMemoryInfo', 'Flutter Tools'));
266
  }
267 268 269 270 271 272 273 274 275 276
  if (skSLMethod != null) {
    vmService.registerServiceCallback('flutterGetSkSL', (Map<String, dynamic> params) async {
      final String filename = await skSLMethod();
      return <String, dynamic>{
        'result': <String, Object>{
          'type': 'Success',
          'filename': filename,
        }
      };
    });
277
    registrationRequests.add(vmService.registerService('flutterGetSkSL', 'Flutter Tools'));
278
  }
279 280
  if (printStructuredErrorLogMethod != null) {
    vmService.onExtensionEvent.listen(printStructuredErrorLogMethod);
281 282 283 284 285 286 287 288 289 290 291 292
    // 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)
      .catchError((dynamic error) {}, test: (dynamic error) => error is vm_service.RPCError)
    );
  }

  try {
    await Future.wait(registrationRequests);
  } on vm_service.RPCError catch (e) {
    throwToolExit('Failed to register service methods on attached VM Service: $e');
293
  }
294
  return vmService;
295
}
296

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

329 330
Future<vm_service.VmService> createVmServiceDelegate(
  Uri wsUri, {
331 332 333
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
  @required Logger logger,
}) async {
334 335 336 337 338 339 340 341 342 343 344
  final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger);
  return vm_service.VmService(
    channel,
    channel.add,
    log: null,
    disposeHandler: () async {
      await channel.close();
    },
  );
}

345
Future<FlutterVmService> _connect(
346 347 348 349
  Uri httpUri, {
  ReloadSources reloadSources,
  Restart restart,
  CompileExpression compileExpression,
350
  GetSkSLMethod getSkSLMethod,
351
  PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
352 353
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
  Device device,
354
  @required Logger logger,
355
}) async {
356
  final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws'));
357 358
  final vm_service.VmService delegateService = await createVmServiceDelegate(
    wsUri, compression: compression, logger: logger,
359 360
  );

361
  final vm_service.VmService service = await setUpVmService(
362 363 364 365
    reloadSources,
    restart,
    compileExpression,
    device,
366
    getSkSLMethod,
367
    printStructuredErrorLogMethod,
368 369 370 371 372 373
    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();
374
  return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri);
375
}
376

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
String _validateRpcStringParam(String methodName, Map<String, dynamic> params, String paramName) {
  final dynamic value = params[paramName];
  if (value is! String || (value as String).isEmpty) {
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
  return value as String;
}

bool _validateRpcBoolParam(String methodName, Map<String, dynamic> params, String paramName) {
  final dynamic value = params[paramName];
  if (value != null && value is! bool) {
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
  return (value as bool) ?? false;
}

401
/// Peered to an Android/iOS FlutterView widget on a device.
402 403 404 405 406
class FlutterView {
  FlutterView({
    @required this.id,
    @required this.uiIsolate,
  });
407

408 409 410 411 412 413 414 415 416 417 418
  factory FlutterView.parse(Map<String, Object> json) {
    final Map<String, Object> rawIsolate = json['isolate'] as Map<String, Object>;
    vm_service.IsolateRef isolate;
    if (rawIsolate != null) {
      rawIsolate['number'] = rawIsolate['number']?.toString();
      isolate = vm_service.IsolateRef.parse(rawIsolate);
    }
    return FlutterView(
      id: json['id'] as String,
      uiIsolate: isolate,
    );
419
  }
420

421 422 423 424
  final vm_service.IsolateRef uiIsolate;
  final String id;

  bool get hasIsolate => uiIsolate != null;
425 426 427

  @override
  String toString() => id;
428 429 430 431 432 433 434

  Map<String, Object> toJson() {
    return <String, Object>{
      'id': id,
      'isolate': uiIsolate?.toJson(),
    };
  }
435 436 437
}

/// Flutter specific VM Service functionality.
438 439
class FlutterVmService {
  FlutterVmService(this.service, {this.wsAddress, this.httpAddress});
440

441 442 443
  final vm_service.VmService service;
  final Uri wsAddress;
  final Uri httpAddress;
444

445 446 447 448 449 450
  Future<vm_service.Response> callMethodWrapper(
    String method, {
    String isolateId,
    Map<String, dynamic> args
  }) async {
    try {
451
      return await service.callMethod(method, isolateId: isolateId, args: args);
452 453 454 455 456 457 458 459 460 461 462 463
    } 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;
    }
  }

464 465 466 467 468 469
  /// Set the asset directory for the an attached Flutter view.
  Future<void> setAssetDirectory({
    @required Uri assetsDirectory,
    @required String viewId,
    @required String uiIsolateId,
  }) async {
470
    assert(assetsDirectory != null);
471
    await callMethodWrapper(kSetAssetBundlePathMethod,
472 473 474 475 476 477 478
      isolateId: uiIsolateId,
      args: <String, dynamic>{
        'viewId': viewId,
        'assetDirectory': assetsDirectory.toFilePath(windows: false),
      });
  }

479
  /// Retrieve the cached SkSL shaders from an attached Flutter view.
480 481
  ///
  /// This method will only return data if `--cache-sksl` was provided as a
482
  /// flutter run argument, and only then on physical devices.
483 484 485
  Future<Map<String, Object>> getSkSLs({
    @required String viewId,
  }) async {
486
    final vm_service.Response response = await callMethodWrapper(
487 488 489
      kGetSkSLsMethod,
      args: <String, String>{
        'viewId': viewId,
490 491
      },
    );
492 493 494
    if (response == null) {
      return null;
    }
495
    return response.json['SkSLs'] as Map<String, Object>;
496 497
  }

498
  /// Flush all tasks on the UI thread for an attached Flutter view.
499 500 501 502 503
  ///
  /// This method is currently used only for benchmarking.
  Future<void> flushUIThreadTasks({
    @required String uiIsolateId,
  }) async {
504
    await callMethodWrapper(
505 506 507 508 509
      kFlushUIThreadTasksMethod,
      args: <String, String>{
        'isolateId': uiIsolateId,
      },
    );
510
  }
511 512 513 514 515 516 517 518 519 520 521 522

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

541
  Future<String> flutterDebugDumpApp({
542
    @required String isolateId,
543 544
  }) async {
    final Map<String, Object> response = await invokeFlutterExtensionRpcRaw(
545 546 547
      'ext.flutter.debugDumpApp',
      isolateId: isolateId,
    );
548
    return response != null ? response['data']?.toString() : '';
549 550
  }

551
  Future<String> flutterDebugDumpRenderTree({
552
    @required String isolateId,
553 554
  }) async {
    final Map<String, Object> response = await invokeFlutterExtensionRpcRaw(
555 556
      'ext.flutter.debugDumpRenderTree',
      isolateId: isolateId,
557
      args: <String, Object>{}
558
    );
559
    return response != null ? response['data']?.toString() : '';
560 561
  }

562
  Future<String> flutterDebugDumpLayerTree({
563
    @required String isolateId,
564 565
  }) async {
    final Map<String, Object> response = await invokeFlutterExtensionRpcRaw(
566 567 568
      'ext.flutter.debugDumpLayerTree',
      isolateId: isolateId,
    );
569
    return response != null ? response['data']?.toString() : '';
570 571
  }

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

582
  Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({
583
    @required String isolateId,
584 585
  }) async {
    final Map<String, Object> response = await invokeFlutterExtensionRpcRaw(
586 587 588
      'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder',
      isolateId: isolateId,
    );
589
    return response != null ? response['data']?.toString() : '';
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
  }

  Future<Map<String, dynamic>> _flutterToggle(String name, {
    @required String isolateId,
  }) async {
    Map<String, dynamic> state = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.$name',
      isolateId: isolateId,
    );
    if (state != null && state.containsKey('enabled') && state['enabled'] is String) {
      state = await invokeFlutterExtensionRpcRaw(
        'ext.flutter.$name',
        isolateId: isolateId,
        args: <String, dynamic>{
          'enabled': state['enabled'] == 'true' ? 'false' : 'true',
        },
      );
    }

    return state;
  }

  Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled({
    @required String isolateId,
  }) => _flutterToggle('debugPaint', isolateId: isolateId);

  Future<Map<String, dynamic>> flutterToggleDebugCheckElevationsEnabled({
    @required String isolateId,
  }) => _flutterToggle('debugCheckElevationsEnabled', isolateId: isolateId);

  Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride({
    @required String isolateId,
  }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId);

  Future<Map<String, dynamic>> flutterToggleWidgetInspector({
    @required String isolateId,
  }) => _flutterToggle('inspector.show', isolateId: isolateId);

628 629 630 631
  Future<Map<String,dynamic>> flutterToggleInvertOversizedImages({
    @required String isolateId,
  }) => _flutterToggle('invertOversizedImages', isolateId: isolateId);

632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654
  Future<Map<String, dynamic>> flutterToggleProfileWidgetBuilds({
    @required String isolateId,
  }) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId);

  Future<Map<String, dynamic>> flutterDebugAllowBanner(bool show, {
    @required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugAllowBanner',
      isolateId: isolateId,
      args: <String, dynamic>{'enabled': show ? 'true' : 'false'},
    );
  }

  Future<Map<String, dynamic>> flutterReassemble({
    @required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.reassemble',
      isolateId: isolateId,
    );
  }

655
  Future<Map<String, dynamic>> flutterFastReassemble({
656
   @required String isolateId,
657
   @required String className,
658 659 660 661
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.fastReassemble',
      isolateId: isolateId,
662 663 664
      args: <String, Object>{
        'className': className,
      },
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
    );
  }

  Future<bool> flutterAlreadyPaintedFirstUsefulFrame({
    @required String isolateId,
  }) async {
    final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.didSendFirstFrameRasterizedEvent',
      isolateId: isolateId,
    );
    // result might be null when the service extension is not initialized
    return result != null && result['enabled'] == 'true';
  }

  Future<Map<String, dynamic>> uiWindowScheduleFrame({
    @required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.scheduleFrame',
      isolateId: isolateId,
    );
  }

  Future<Map<String, dynamic>> flutterEvictAsset(String assetPath, {
   @required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.evict',
      isolateId: isolateId,
      args: <String, dynamic>{
        'value': assetPath,
      },
    );
  }

  /// Exit the application by calling [exit] from `dart:io`.
  ///
  /// This method is only supported by certain embedders. This is
  /// described by [Device.supportsFlutterExit].
704
  Future<bool> flutterExit({
705
    @required String isolateId,
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
  }) async {
    try {
      final Map<String, Object> result = await invokeFlutterExtensionRpcRaw(
        'ext.flutter.exit',
        isolateId: isolateId,
      );
      // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError
      // with a missing method code. This can happen when attempting to quit a flutter app
      // 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;
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747
  }

  /// 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({
    String platform,
    @required String isolateId,
  }) async {
    final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.platformOverride',
      isolateId: isolateId,
      args: platform != null
        ? <String, dynamic>{'value': platform}
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
      return result['value'] as String;
    }
    return 'unknown';
  }

748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771
  /// 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.
  Future<Brightness> flutterBrightnessOverride({
    Brightness brightness,
    @required String isolateId,
  }) async {
    final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.brightnessOverride',
      isolateId: isolateId,
      args: brightness != null
        ? <String, dynamic>{'value': brightness.toString()}
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
      return (result['value'] as String) == 'Brightness.light'
        ? Brightness.light
        : Brightness.dark;
    }
    return null;
  }

772
  Future<vm_service.Response> _checkedCallServiceExtension(
773 774 775 776
    String method, {
    Map<String, dynamic> args,
  }) async {
    try {
777
      return await service.callServiceExtension(method, args: args);
778
    } on vm_service.RPCError catch (err) {
779 780 781 782
      // 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)) {
783 784 785 786 787
        return null;
      }
      rethrow;
    }
  }
788

789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
  /// Invoke a flutter extension method, if the flutter extension is not
  /// available, returns null.
  Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
    String method, {
    @required String isolateId,
    Map<String, dynamic> args,
  }) async {
    final vm_service.Response response = await _checkedCallServiceExtension(
      method,
      args: <String, Object>{
        'isolateId': isolateId,
        ...?args,
      },
    );
    return response?.json;
  }

806
  /// List all [FlutterView]s attached to the current VM.
807 808 809 810 811 812 813 814 815 816
  ///
  /// 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) {
817
      final vm_service.Response response = await callMethodWrapper(
818 819
        kListViewsMethod,
      );
820
      if (response == null) {
821 822 823 824
        // The service may have disappeared mid-request.
        // Return an empty list now, and let the shutdown logic elsewhere deal
        // with cleaning up.
        return <FlutterView>[];
825
      }
826 827 828 829 830 831 832 833 834 835
      final List<Object> rawViews = response.json['views'] as List<Object>;
      final List<FlutterView> views = <FlutterView>[
        for (final Object rawView in rawViews)
          FlutterView.parse(rawView as Map<String, Object>)
      ];
      if (views.isNotEmpty || returnEarly) {
        return views;
      }
      await Future<void>.delayed(delay);
    }
836 837
  }

838 839 840 841 842
  /// 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.
  ///
843 844 845
  /// If [webIsolate] is true, this uses the VM Service isolate list instead of
  /// the `_flutter.listViews` method, which is not implemented by DWDS.
  ///
846 847
  /// Throws a [VmServiceDisappearedException] should the VM Service disappear
  /// while making calls to it.
848
  Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async {
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
    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>();
    StreamSubscription<vm_service.Event> isolateEvents;
    isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) {
      if (event.kind == vm_service.EventKind.kServiceExtensionAdded
          && event.extensionRPC == extensionName) {
        isolateEvents.cancel();
        extensionAdded.complete(event.isolate);
      }
    });

    try {
866
      final List<vm_service.IsolateRef> refs = await _getIsolateRefs();
867
      for (final vm_service.IsolateRef ref in refs) {
868 869
        final vm_service.Isolate isolate = await getIsolateOrNull(ref.id);
        if (isolate != null && isolate.extensionRPCs.contains(extensionName)) {
870
          return ref;
871 872 873 874 875 876 877 878 879 880 881 882 883
        }
      }
      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.
      }
    }
  }

884
  Future<List<vm_service.IsolateRef>> _getIsolateRefs() async {
885 886 887 888 889 890 891 892 893 894 895 896 897 898
    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) {
      if (flutterView.uiIsolate != null) {
        refs.add(flutterView.uiIsolate);
      }
    }
    return refs;
  }

899 900 901
  /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has
  /// been collected.
  Future<vm_service.Isolate> getIsolateOrNull(String isolateId) {
902
    return service.getIsolate(isolateId)
903 904
      .catchError((dynamic error, StackTrace stackTrace) {
        return null;
905 906 907 908
      }, test: (dynamic error) {
        return (error is vm_service.SentinelException) ||
          (error is vm_service.RPCError && error.code == RPCErrorCodes.kServiceDisappeared);
      });
909
  }
910 911 912

  /// Create a new development file system on the device.
  Future<vm_service.Response> createDevFS(String fsName) {
913 914
    // Call the unchecked version of `callServiceExtension` because the caller
    // has custom handling of certain RPCErrors.
915
    return service.callServiceExtension(
916 917 918
      '_createDevFS',
      args: <String, dynamic>{'fsName': fsName},
    );
919 920 921
  }

  /// Delete an existing file system.
922 923 924 925 926
  Future<void> deleteDevFS(String fsName) async {
    await _checkedCallServiceExtension(
      '_deleteDevFS',
      args: <String, dynamic>{'fsName': fsName},
    );
927 928 929
  }

  Future<vm_service.Response> screenshot() {
930
    return _checkedCallServiceExtension(kScreenshotMethod);
931 932 933
  }

  Future<vm_service.Response> screenshotSkp() {
934
    return _checkedCallServiceExtension(kScreenshotSkpMethod);
935 936
  }

937
  /// Set the VM timeline flags.
938
  Future<void> setTimelineFlags(List<String> recordedStreams) async {
939
    assert(recordedStreams != null);
940
    await _checkedCallServiceExtension(
941 942 943 944 945 946 947
      'setVMTimelineFlags',
      args: <String, dynamic>{
        'recordedStreams': recordedStreams,
      },
    );
  }

948 949
  Future<vm_service.Response> getTimeline() {
    return _checkedCallServiceExtension('getVMTimeline');
950
  }
951 952 953 954

  Future<void> dispose() async {
     await service.dispose();
  }
955 956
}

957 958 959
/// Thrown when the VM Service disappears while calls are being made to it.
class VmServiceDisappearedException implements Exception {}

960 961 962 963 964 965 966 967 968 969
/// 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;
970
}
971

972 973 974 975 976 977 978 979 980 981 982 983 984 985 986
/// 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,
}
987 988 989 990 991 992 993 994 995 996

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