vmservice.dart 39.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
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 18
import 'ios/xcodeproj.dart';
import 'project.dart';
19
import 'version.dart';
20

21 22 23
const String kResultType = 'type';
const String kResultTypeSuccess = 'Success';

24 25 26
const String kGetSkSLsMethod = '_flutter.getSkSLs';
const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath';
const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks';
27
const String kRunInViewMethod = '_flutter.runInView';
28
const String kListViewsMethod = '_flutter.listViews';
29 30
const String kScreenshotSkpMethod = '_flutter.screenshotSkp';
const String kScreenshotMethod = '_flutter.screenshot';
31
const String kRenderFrameWithRasterStatsMethod = '_flutter.renderFrameWithRasterStats';
32
const String kReloadAssetFonts = '_flutter.reloadAssetFonts';
33

34 35 36 37 38 39 40 41 42 43
const String kFlutterToolAlias = 'Flutter Tools';

const String kReloadSourcesServiceName = 'reloadSources';
const String kHotRestartServiceName = 'hotRestart';
const String kFlutterVersionServiceName = 'flutterVersion';
const String kCompileExpressionServiceName = 'compileExpression';
const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo';
const String kFlutterGetSkSLServiceName = 'flutterGetSkSL';
const String kFlutterGetIOSBuildOptionsServiceName = 'flutterGetIOSBuildOptions';
const String kFlutterGetAndroidBuildVariantsServiceName = 'flutterGetAndroidBuildVariants';
44
const String kFlutterGetIOSUniversalLinkSettingsServiceName = 'flutterGetIOSUniversalLinkSettings';
45

46 47 48
/// The error response code from an unrecoverable compilation failure.
const int kIsolateReloadBarred = 1005;

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

53 54
typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event);

55
WebSocketConnector _openChannel = _defaultOpenChannel;
56

57 58 59 60
/// A testing only override of the WebSocket connector.
///
/// Provide a `null` value to restore the original connector.
@visibleForTesting
61
set openChannelForTesting(WebSocketConnector? connector) {
62 63 64
  _openChannel = connector ?? _defaultOpenChannel;
}

65 66
/// The error codes for the JSON-RPC standard, including VM service specific
/// error codes.
67 68 69 70 71 72 73 74 75 76 77 78
///
/// 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;

79
  /// Application specific error codes.
80
  static const int kServerError = -32000;
81 82 83 84 85

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

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

88 89 90 91 92
/// 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.
///
93
/// Clients like VmService use external 'reloadSources' services,
94 95 96 97 98
/// 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
99
typedef ReloadSources = Future<void> Function(
100 101 102 103 104
  String isolateId, {
  bool force,
  bool pause,
});

105 106
typedef Restart = Future<void> Function({ bool pause });

107
typedef CompileExpression = Future<String> Function(
108 109 110 111 112
  String isolateId,
  String expression,
  List<String> definitions,
  List<String> typeDefinitions,
  String libraryUri,
113
  String? klass,
114 115 116
  bool isStatic,
);

117 118 119
/// 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.
120
typedef GetSkSLMethod = Future<String?> Function();
121

122
Future<io.WebSocket> _defaultOpenChannel(String url, {
123
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
124
  required Logger logger,
125
}) async {
126 127
  Duration delay = const Duration(milliseconds: 100);
  int attempts = 0;
128
  io.WebSocket? socket;
129

130
  Future<void> handleError(Object? e) async {
131
    void Function(String) printVisibleTrace = logger.printTrace;
132
    if (attempts == 10) {
133
      logger.printStatus('Connecting to the VM Service is taking longer than expected...');
134
    } else if (attempts == 20) {
135 136
      logger.printStatus('Still attempting to connect to the VM Service...');
      logger.printStatus(
137 138 139
        '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.');
140
      logger.printStatus(
141 142 143 144
        '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) {
145
      printVisibleTrace = logger.printStatus;
146
    }
147

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

151
    // Delay next attempt.
152
    await Future<void>.delayed(delay);
153

154
    // Back off exponentially, up to 1600ms per attempt.
155
    if (delay < const Duration(seconds: 1)) {
156
      delay *= 2;
157
    }
158 159
  }

160 161
  final WebSocketConnector constructor = context.get<WebSocketConnector>() ?? (String url, {
    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
162
    Logger? logger,
163 164
  }) => io.WebSocket.connect(url, compression: compression);

165
  while (socket == null) {
166 167
    attempts += 1;
    try {
168
      socket = await constructor(url, compression: compression, logger: logger);
169
    } on io.WebSocketException catch (e) {
170
      await handleError(e);
171
    } on io.SocketException catch (e) {
172
      await handleError(e);
173 174
    }
  }
175
  return socket;
176
}
177

178 179
/// Override `VMServiceConnector` in [context] to return a different VMService
/// from [VMService.connect] (used by tests).
180
typedef VMServiceConnector = Future<FlutterVmService> Function(Uri httpUri, {
181 182 183 184
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
185
  FlutterProject? flutterProject,
186
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
187
  io.CompressionOptions compression,
188 189
  Device? device,
  required Logger logger,
190
});
191

192 193 194 195
/// Set up the VM Service client by attaching services for each of the provided
/// callbacks.
///
/// All parameters besides [vmService] may be null.
196
Future<vm_service.VmService> setUpVmService({
197 198 199 200 201
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  Device? device,
  GetSkSLMethod? skSLMethod,
202
  FlutterProject? flutterProject,
203
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
204 205
  required vm_service.VmService vmService,
}) async {
206
  // Each service registration requires a request to the attached VM service. Since the
207
  // order of these requests does not matter, store each future in a list and await
208
  // all at the end of this method.
209
  final List<Future<vm_service.Success?>> registrationRequests = <Future<vm_service.Success?>>[];
210
  if (reloadSources != null) {
211
    vmService.registerServiceCallback(kReloadSourcesServiceName, (Map<String, Object?> params) async {
212 213 214 215 216 217
      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);

218
      return <String, Object>{
219
        'result': <String, Object>{
220
          kResultType: kResultTypeSuccess,
221
        },
222
      };
223
    });
224
    registrationRequests.add(vmService.registerService(kReloadSourcesServiceName, kFlutterToolAlias));
225 226 227
  }

  if (restart != null) {
228
    vmService.registerServiceCallback(kHotRestartServiceName, (Map<String, Object?> params) async {
229 230
      final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause');
      await restart(pause: pause);
231
      return <String, Object>{
232
        'result': <String, Object>{
233
          kResultType: kResultTypeSuccess,
234
        },
235
      };
236
    });
237
    registrationRequests.add(vmService.registerService(kHotRestartServiceName, kFlutterToolAlias));
238 239
  }

240
  vmService.registerServiceCallback(kFlutterVersionServiceName, (Map<String, Object?> params) async {
241 242 243 244
    final FlutterVersion version = context.get<FlutterVersion>() ?? FlutterVersion();
    final Map<String, Object> versionJson = version.toJson();
    versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort;
    versionJson['engineRevisionShort'] = version.engineRevisionShort;
245
    return <String, Object>{
246
      'result': <String, Object>{
247
        kResultType: kResultTypeSuccess,
248
        ...versionJson,
249
      },
250
    };
251
  });
252
  registrationRequests.add(vmService.registerService(kFlutterVersionServiceName, kFlutterToolAlias));
253 254

  if (compileExpression != null) {
255
    vmService.registerServiceCallback(kCompileExpressionServiceName, (Map<String, Object?> params) async {
256 257
      final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId');
      final String expression = _validateRpcStringParam('compileExpression', params, 'expression');
258 259 260 261
      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?;
262 263 264 265 266
      final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic');

      final String kernelBytesBase64 = await compileExpression(isolateId,
          expression, definitions, typeDefinitions, libraryUri, klass,
          isStatic);
267
      return <String, Object>{
268
        kResultType: kResultTypeSuccess,
269
        'result': <String, String>{'kernelBytes': kernelBytesBase64},
270
      };
271
    });
272
    registrationRequests.add(vmService.registerService(kCompileExpressionServiceName, kFlutterToolAlias));
273
  }
274
  if (device != null) {
275
    vmService.registerServiceCallback(kFlutterMemoryInfoServiceName, (Map<String, Object?> params) async {
276
      final MemoryInfo result = await device.queryMemoryInfo();
277
      return <String, Object>{
278
        'result': <String, Object>{
279
          kResultType: kResultTypeSuccess,
280
          ...result.toJson(),
281
        },
282
      };
283
    });
284
    registrationRequests.add(vmService.registerService(kFlutterMemoryInfoServiceName, kFlutterToolAlias));
285
  }
286
  if (skSLMethod != null) {
287
    vmService.registerServiceCallback(kFlutterGetSkSLServiceName, (Map<String, Object?> params) async {
288 289 290 291
      final String? filename = await skSLMethod();
      if (filename == null) {
        return <String, Object>{
          'result': <String, Object>{
292
            kResultType: kResultTypeSuccess,
293 294 295
          },
        };
      }
296
      return <String, Object>{
297
        'result': <String, Object>{
298
          kResultType: kResultTypeSuccess,
299
          'filename': filename,
300
        },
301 302
      };
    });
303
    registrationRequests.add(vmService.registerService(kFlutterGetSkSLServiceName, kFlutterToolAlias));
304
  }
305 306

  if (flutterProject != null) {
307
    vmService.registerServiceCallback(kFlutterGetIOSBuildOptionsServiceName, (Map<String, Object?> params) async {
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
      final XcodeProjectInfo? info = await flutterProject.ios.projectInfo();
      if (info == null) {
        return <String, Object>{
          'result': <String, Object>{
            kResultType: kResultTypeSuccess,
          },
        };
      }
      return <String, Object>{
        'result': <String, Object>{
          kResultType: kResultTypeSuccess,
          'targets': info.targets,
          'schemes': info.schemes,
          'buildConfigurations': info.buildConfigurations,
        },
      };
    });
    registrationRequests.add(
326 327 328 329 330 331 332 333 334 335 336 337 338 339
      vmService.registerService(kFlutterGetIOSBuildOptionsServiceName, kFlutterToolAlias),
    );

    vmService.registerServiceCallback(kFlutterGetAndroidBuildVariantsServiceName, (Map<String, Object?> params) async {
      final List<String> options = await flutterProject.android.getBuildVariants();
      return <String, Object>{
        'result': <String, Object>{
          kResultType: kResultTypeSuccess,
          'variants': options,
        },
      };
    });
    registrationRequests.add(
      vmService.registerService(kFlutterGetAndroidBuildVariantsServiceName, kFlutterToolAlias),
340
    );
341

342
    vmService.registerServiceCallback(kFlutterGetIOSUniversalLinkSettingsServiceName, (Map<String, Object?> params) async {
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
      final XcodeUniversalLinkSettings settings = await flutterProject.ios.universalLinkSettings(
        configuration: params['configuration']! as String,
        scheme: params['scheme']! as String,
        target: params['target']! as String,
      );
      return <String, Object>{
        'result': <String, Object>{
          kResultType: kResultTypeSuccess,
          'bundleIdentifier': settings.bundleIdentifier ?? '',
          'teamIdentifier': settings.teamIdentifier ?? '',
          'associatedDomains': settings.associatedDomains,
        },
      };
    });
    registrationRequests.add(
358
      vmService.registerService(kFlutterGetIOSUniversalLinkSettingsServiceName, kFlutterToolAlias),
359
    );
360 361
  }

362 363
  if (printStructuredErrorLogMethod != null) {
    vmService.onExtensionEvent.listen(printStructuredErrorLogMethod);
364 365
    registrationRequests.add(vmService
      .streamListen(vm_service.EventStreams.kExtension)
366 367 368 369 370 371 372 373 374 375 376
      .then<vm_service.Success?>(
        (vm_service.Success success) => success,
        // It is safe to ignore this error because we expect an error to be
        // thrown if we're already subscribed.
        onError: (Object error, StackTrace stackTrace) {
          if (error is vm_service.RPCError) {
            return null;
          }
          return Future<vm_service.Success?>.error(error, stackTrace);
        },
      ),
377 378 379 380 381 382 383
    );
  }

  try {
    await Future.wait(registrationRequests);
  } on vm_service.RPCError catch (e) {
    throwToolExit('Failed to register service methods on attached VM Service: $e');
384
  }
385
  return vmService;
386
}
387

388 389 390 391 392 393 394 395
/// 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
396
Future<FlutterVmService> connectToVmService(
397
  Uri httpUri, {
398 399 400 401 402 403 404 405 406 407
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
  FlutterProject? flutterProject,
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
  Device? device,
  required Logger logger,
}) async {
408 409 410 411 412 413 414
  final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect;
  return connector(httpUri,
    reloadSources: reloadSources,
    restart: restart,
    compileExpression: compileExpression,
    compression: compression,
    device: device,
415
    getSkSLMethod: getSkSLMethod,
416
    flutterProject: flutterProject,
417
    printStructuredErrorLogMethod: printStructuredErrorLogMethod,
418
    logger: logger,
419
  );
420 421
}

422 423
Future<vm_service.VmService> createVmServiceDelegate(
  Uri wsUri, {
424
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
425
  required Logger logger,
426
}) async {
427 428 429 430 431 432 433 434 435 436
  final io.WebSocket channel = await _openChannel(wsUri.toString(), compression: compression, logger: logger);
  return vm_service.VmService(
    channel,
    channel.add,
    disposeHandler: () async {
      await channel.close();
    },
  );
}

437
Future<FlutterVmService> _connect(
438
  Uri httpUri, {
439 440 441 442
  ReloadSources? reloadSources,
  Restart? restart,
  CompileExpression? compileExpression,
  GetSkSLMethod? getSkSLMethod,
443
  FlutterProject? flutterProject,
444
  PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
445
  io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
446 447
  Device? device,
  required Logger logger,
448
}) async {
449
  final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws'));
450 451
  final vm_service.VmService delegateService = await createVmServiceDelegate(
    wsUri, compression: compression, logger: logger,
452 453
  );

454
  final vm_service.VmService service = await setUpVmService(
455 456 457 458 459 460 461 462
    reloadSources: reloadSources,
    restart: restart,
    compileExpression: compileExpression,
    device: device,
    skSLMethod: getSkSLMethod,
    flutterProject: flutterProject,
    printStructuredErrorLogMethod: printStructuredErrorLogMethod,
    vmService: delegateService,
463 464 465 466 467
  );

  // 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();
468
  return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri);
469
}
470

471 472 473
String _validateRpcStringParam(String methodName, Map<String, Object?> params, String paramName) {
  final Object? value = params[paramName];
  if (value is! String || value.isEmpty) {
474 475 476 477 478 479
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
480
  return value;
481 482
}

483 484
bool _validateRpcBoolParam(String methodName, Map<String, Object?> params, String paramName) {
  final Object? value = params[paramName];
485 486 487 488 489 490 491
  if (value != null && value is! bool) {
    throw vm_service.RPCError(
      methodName,
      RPCErrorCodes.kInvalidParams,
      "Invalid '$paramName': $value",
    );
  }
492
  return (value as bool?) ?? false;
493 494
}

495
/// Peered to an Android/iOS FlutterView widget on a device.
496 497
class FlutterView {
  FlutterView({
498 499
    required this.id,
    required this.uiIsolate,
500
  });
501

502
  factory FlutterView.parse(Map<String, Object?> json) {
503 504
    final Map<String, Object?>? rawIsolate = json['isolate'] as Map<String, Object?>?;
    vm_service.IsolateRef? isolate;
505 506 507 508 509
    if (rawIsolate != null) {
      rawIsolate['number'] = rawIsolate['number']?.toString();
      isolate = vm_service.IsolateRef.parse(rawIsolate);
    }
    return FlutterView(
510
      id: json['id']! as String,
511 512
      uiIsolate: isolate,
    );
513
  }
514

515
  final vm_service.IsolateRef? uiIsolate;
516 517 518
  final String id;

  bool get hasIsolate => uiIsolate != null;
519 520 521

  @override
  String toString() => id;
522

523 524
  Map<String, Object?> toJson() {
    return <String, Object?>{
525 526 527 528
      'id': id,
      'isolate': uiIsolate?.toJson(),
    };
  }
529 530 531
}

/// Flutter specific VM Service functionality.
532
class FlutterVmService {
533 534 535 536 537
  FlutterVmService(
    this.service, {
    this.wsAddress,
    this.httpAddress,
  });
538

539
  final vm_service.VmService service;
540 541
  final Uri? wsAddress;
  final Uri? httpAddress;
542

543
  Future<vm_service.Response?> callMethodWrapper(
544
    String method, {
545 546
    String? isolateId,
    Map<String, Object?>? args
547 548
  }) async {
    try {
549
      return await service.callMethod(method, isolateId: isolateId, args: args);
550 551 552 553 554 555 556 557 558 559 560 561
    } 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;
    }
  }

562 563
  /// Set the asset directory for the an attached Flutter view.
  Future<void> setAssetDirectory({
564 565 566
    required Uri assetsDirectory,
    required String? viewId,
    required String? uiIsolateId,
567
    required bool windows,
568
  }) async {
569
    await callMethodWrapper(kSetAssetBundlePathMethod,
570
      isolateId: uiIsolateId,
571
      args: <String, Object?>{
572
        'viewId': viewId,
573
        'assetDirectory': assetsDirectory.toFilePath(windows: windows),
574 575 576
      });
  }

577
  /// Retrieve the cached SkSL shaders from an attached Flutter view.
578 579
  ///
  /// This method will only return data if `--cache-sksl` was provided as a
580
  /// flutter run argument, and only then on physical devices.
581
  Future<Map<String, Object?>?> getSkSLs({
582
    required String viewId,
583
  }) async {
584
    final vm_service.Response? response = await callMethodWrapper(
585 586 587
      kGetSkSLsMethod,
      args: <String, String>{
        'viewId': viewId,
588 589
      },
    );
590 591 592
    if (response == null) {
      return null;
    }
593
    return response.json?['SkSLs'] as Map<String, Object?>?;
594 595
  }

596
  /// Flush all tasks on the UI thread for an attached Flutter view.
597 598 599
  ///
  /// This method is currently used only for benchmarking.
  Future<void> flushUIThreadTasks({
600
    required String uiIsolateId,
601
  }) async {
602
    await callMethodWrapper(
603 604 605 606 607
      kFlushUIThreadTasksMethod,
      args: <String, String>{
        'isolateId': uiIsolateId,
      },
    );
608
  }
609 610 611 612 613 614 615

  /// 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({
616 617 618
    required String viewId,
    required Uri main,
    required Uri assetsDirectory,
619 620
  }) async {
    try {
621
      await service.streamListen(vm_service.EventStreams.kIsolate);
622 623 624
    } on vm_service.RPCError {
      // Do nothing, since the tool is already subscribed.
    }
625
    final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) {
626 627
      return event.kind == vm_service.EventKind.kIsolateRunnable;
    });
628
    await callMethodWrapper(
629 630 631 632 633 634 635 636 637
      kRunInViewMethod,
      args: <String, Object>{
        'viewId': viewId,
        'mainScript': main.toString(),
        'assetDirectory': assetsDirectory.toString(),
      },
    );
    await onRunnable;
  }
638

639 640 641 642 643 644
  /// 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.
645
  Future<Map<String, Object?>?> renderFrameWithRasterStats({
646 647 648 649 650 651 652 653 654 655
    required String? viewId,
    required String? uiIsolateId,
  }) async {
    final vm_service.Response? response = await callMethodWrapper(
      kRenderFrameWithRasterStatsMethod,
      isolateId: uiIsolateId,
      args: <String, String?>{
        'viewId': viewId,
      },
    );
656
    return response?.json;
657 658
  }

659
  Future<String> flutterDebugDumpApp({
660
    required String isolateId,
661
  }) async {
662
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
663 664 665
      'ext.flutter.debugDumpApp',
      isolateId: isolateId,
    );
666
    return response?['data']?.toString() ?? '';
667 668
  }

669
  Future<String> flutterDebugDumpRenderTree({
670
    required String isolateId,
671
  }) async {
672
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
673 674
      'ext.flutter.debugDumpRenderTree',
      isolateId: isolateId,
675
      args: <String, Object>{}
676
    );
677
    return response?['data']?.toString() ?? '';
678 679
  }

680
  Future<String> flutterDebugDumpLayerTree({
681
    required String isolateId,
682
  }) async {
683
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
684 685 686
      'ext.flutter.debugDumpLayerTree',
      isolateId: isolateId,
    );
687
    return response?['data']?.toString() ?? '';
688 689
  }

690 691 692 693 694 695 696 697 698 699
  Future<String> flutterDebugDumpFocusTree({
    required String isolateId,
  }) async {
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugDumpFocusTree',
      isolateId: isolateId,
    );
    return response?['data']?.toString() ?? '';
  }

700
  Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({
701
    required String isolateId,
702
  }) async {
703
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
704 705 706
      'ext.flutter.debugDumpSemanticsTreeInTraversalOrder',
      isolateId: isolateId,
    );
707
    return response?['data']?.toString() ?? '';
708 709
  }

710
  Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({
711
    required String isolateId,
712
  }) async {
713
    final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw(
714 715 716
      'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder',
      isolateId: isolateId,
    );
717 718 719 720
    if (response != null) {
      return response['data']?.toString() ?? '';
    }
    return '';
721 722
  }

723 724
  Future<Map<String, Object?>?> _flutterToggle(String name, {
    required String isolateId,
725
  }) async {
726
    Map<String, Object?>? state = await invokeFlutterExtensionRpcRaw(
727 728 729 730 731 732 733
      'ext.flutter.$name',
      isolateId: isolateId,
    );
    if (state != null && state.containsKey('enabled') && state['enabled'] is String) {
      state = await invokeFlutterExtensionRpcRaw(
        'ext.flutter.$name',
        isolateId: isolateId,
734
        args: <String, Object>{
735 736 737 738 739 740 741 742
          'enabled': state['enabled'] == 'true' ? 'false' : 'true',
        },
      );
    }

    return state;
  }

743 744
  Future<Map<String, Object?>?> flutterToggleDebugPaintSizeEnabled({
    required String isolateId,
745 746
  }) => _flutterToggle('debugPaint', isolateId: isolateId);

747 748
  Future<Map<String, Object?>?> flutterTogglePerformanceOverlayOverride({
    required String isolateId,
749 750
  }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId);

751 752
  Future<Map<String, Object?>?> flutterToggleWidgetInspector({
    required String isolateId,
753 754
  }) => _flutterToggle('inspector.show', isolateId: isolateId);

755 756
  Future<Map<String, Object?>?> flutterToggleInvertOversizedImages({
    required String isolateId,
757 758
  }) => _flutterToggle('invertOversizedImages', isolateId: isolateId);

759 760
  Future<Map<String, Object?>?> flutterToggleProfileWidgetBuilds({
    required String isolateId,
761 762
  }) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId);

763 764
  Future<Map<String, Object?>?> flutterDebugAllowBanner(bool show, {
    required String isolateId,
765 766 767 768
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.debugAllowBanner',
      isolateId: isolateId,
769
      args: <String, Object>{'enabled': show ? 'true' : 'false'},
770 771 772
    );
  }

773 774
  Future<Map<String, Object?>?> flutterReassemble({
    required String isolateId,
775 776 777 778 779 780 781
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.reassemble',
      isolateId: isolateId,
    );
  }

782 783 784
  Future<Map<String, Object?>?> flutterFastReassemble({
   required String isolateId,
   required String className,
785 786 787 788
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.fastReassemble',
      isolateId: isolateId,
789 790 791
      args: <String, Object>{
        'className': className,
      },
792 793 794 795
    );
  }

  Future<bool> flutterAlreadyPaintedFirstUsefulFrame({
796
    required String isolateId,
797
  }) async {
798
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
799 800 801 802
      'ext.flutter.didSendFirstFrameRasterizedEvent',
      isolateId: isolateId,
    );
    // result might be null when the service extension is not initialized
803
    return result?['enabled'] == 'true';
804 805
  }

806 807
  Future<Map<String, Object?>?> uiWindowScheduleFrame({
    required String isolateId,
808 809 810 811 812 813 814
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.scheduleFrame',
      isolateId: isolateId,
    );
  }

815 816
  Future<Map<String, Object?>?> flutterEvictAsset(String assetPath, {
   required String isolateId,
817 818 819 820
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.flutter.evict',
      isolateId: isolateId,
821
      args: <String, Object?>{
822 823 824 825 826
        'value': assetPath,
      },
    );
  }

827 828 829 830 831 832 833 834 835 836 837 838
  Future<Map<String, Object?>?> flutterEvictShader(String assetPath, {
   required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.reinitializeShader',
      isolateId: isolateId,
      args: <String, Object?>{
        'assetKey': assetPath,
      },
    );
  }

839 840 841 842 843 844 845 846 847 848 849 850
  Future<Map<String, Object?>?> flutterEvictScene(String assetPath, {
   required String isolateId,
  }) {
    return invokeFlutterExtensionRpcRaw(
      'ext.ui.window.reinitializeScene',
      isolateId: isolateId,
      args: <String, Object?>{
        'assetKey': assetPath,
      },
    );
  }

851

852 853 854 855
  /// Exit the application by calling [exit] from `dart:io`.
  ///
  /// This method is only supported by certain embedders. This is
  /// described by [Device.supportsFlutterExit].
856
  Future<bool> flutterExit({
857
    required String isolateId,
858 859
  }) async {
    try {
860
      final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
861 862 863 864
        'ext.flutter.exit',
        isolateId: isolateId,
      );
      // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError
865
      // with a missing method code. This can happen when attempting to quit a Flutter app
866 867 868 869 870 871 872 873 874 875
      // 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;
876 877 878 879 880 881 882 883
  }

  /// 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({
884 885
    String? platform,
    required String isolateId,
886
  }) async {
887
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
888 889 890
      'ext.flutter.platformOverride',
      isolateId: isolateId,
      args: platform != null
891
        ? <String, Object>{'value': platform}
892 893 894
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
895
      return result['value']! as String;
896 897 898 899
    }
    return 'unknown';
  }

900 901 902 903 904
  /// 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.
905 906 907
  Future<Brightness?> flutterBrightnessOverride({
    Brightness? brightness,
    required String isolateId,
908
  }) async {
909
    final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw(
910 911 912
      'ext.flutter.brightnessOverride',
      isolateId: isolateId,
      args: brightness != null
913
        ? <String, String>{'value': brightness.toString()}
914 915 916
        : <String, String>{},
    );
    if (result != null && result['value'] is String) {
917
      return result['value'] == 'Brightness.light'
918 919 920 921 922 923
        ? Brightness.light
        : Brightness.dark;
    }
    return null;
  }

924
  Future<vm_service.Response?> _checkedCallServiceExtension(
925
    String method, {
926
    Map<String, Object?>? args,
927 928
  }) async {
    try {
929
      return await service.callServiceExtension(method, args: args);
930
    } on vm_service.RPCError catch (err) {
931 932 933 934
      // 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)) {
935 936 937 938 939
        return null;
      }
      rethrow;
    }
  }
940

941 942
  /// Invoke a flutter extension method, if the flutter extension is not
  /// available, returns null.
943
  Future<Map<String, Object?>?> invokeFlutterExtensionRpcRaw(
944
    String method, {
945 946
    required String isolateId,
    Map<String, Object?>? args,
947
  }) async {
948
    final vm_service.Response? response = await _checkedCallServiceExtension(
949
      method,
950
      args: <String, Object?>{
951 952 953 954 955 956 957
        'isolateId': isolateId,
        ...?args,
      },
    );
    return response?.json;
  }

958
  /// List all [FlutterView]s attached to the current VM.
959 960 961 962 963 964 965 966 967 968
  ///
  /// 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) {
969
      final vm_service.Response? response = await callMethodWrapper(
970 971
        kListViewsMethod,
      );
972
      if (response == null) {
973 974 975 976
        // The service may have disappeared mid-request.
        // Return an empty list now, and let the shutdown logic elsewhere deal
        // with cleaning up.
        return <FlutterView>[];
977
      }
978
      final List<Object?>? rawViews = response.json?['views'] as List<Object?>?;
979
      final List<FlutterView> views = <FlutterView>[
980
        if (rawViews != null)
981
          for (final Map<String, Object?> rawView in rawViews.whereType<Map<String, Object?>>())
982
            FlutterView.parse(rawView),
983 984 985 986 987 988
      ];
      if (views.isNotEmpty || returnEarly) {
        return views;
      }
      await Future<void>.delayed(delay);
    }
989 990
  }

991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004
  /// 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,
      },
    );
  }

1005 1006 1007 1008 1009
  /// 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.
  ///
1010 1011 1012
  /// If [webIsolate] is true, this uses the VM Service isolate list instead of
  /// the `_flutter.listViews` method, which is not implemented by DWDS.
  ///
1013 1014
  /// Throws a [VmServiceDisappearedException] should the VM Service disappear
  /// while making calls to it.
1015
  Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async {
1016 1017 1018 1019 1020 1021 1022
    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>();
1023
    late final StreamSubscription<vm_service.Event> isolateEvents;
1024 1025 1026
    isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) {
      if (event.kind == vm_service.EventKind.kServiceExtensionAdded
          && event.extensionRPC == extensionName) {
1027
        isolateEvents.cancel();
1028 1029 1030 1031 1032
        extensionAdded.complete(event.isolate);
      }
    });

    try {
1033
      final List<vm_service.IsolateRef> refs = await _getIsolateRefs();
1034
      for (final vm_service.IsolateRef ref in refs) {
1035
        final vm_service.Isolate? isolate = await getIsolateOrNull(ref.id!);
1036
        if (isolate != null && (isolate.extensionRPCs?.contains(extensionName) ?? false)) {
1037
          return ref;
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
        }
      }
      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.
      }
    }
  }

1051
  Future<List<vm_service.IsolateRef>> _getIsolateRefs() async {
1052 1053 1054 1055 1056 1057 1058
    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) {
1059
      final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate;
1060 1061
      if (uiIsolate != null) {
        refs.add(uiIsolate);
1062 1063 1064 1065 1066
      }
    }
    return refs;
  }

1067 1068
  /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has
  /// been collected.
1069
  Future<vm_service.Isolate?> getIsolateOrNull(String isolateId) async {
1070
    return service.getIsolate(isolateId)
1071 1072 1073 1074 1075
      .then<vm_service.Isolate?>(
        (vm_service.Isolate isolate) => isolate,
        onError: (Object? error, StackTrace stackTrace) {
          if (error is vm_service.SentinelException ||
            error == null ||
1076
            (error is vm_service.RPCError && error.code == RPCErrorCodes.kServiceDisappeared)) {
1077 1078 1079 1080
            return null;
          }
          return Future<vm_service.Isolate?>.error(error, stackTrace);
        });
1081
  }
1082 1083 1084

  /// Create a new development file system on the device.
  Future<vm_service.Response> createDevFS(String fsName) {
1085 1086
    // Call the unchecked version of `callServiceExtension` because the caller
    // has custom handling of certain RPCErrors.
1087
    return service.callServiceExtension(
1088
      '_createDevFS',
1089
      args: <String, Object?>{'fsName': fsName},
1090
    );
1091 1092 1093
  }

  /// Delete an existing file system.
1094 1095 1096
  Future<void> deleteDevFS(String fsName) async {
    await _checkedCallServiceExtension(
      '_deleteDevFS',
1097
      args: <String, Object?>{'fsName': fsName},
1098
    );
1099 1100
  }

1101
  Future<vm_service.Response?> screenshot() {
1102
    return _checkedCallServiceExtension(kScreenshotMethod);
1103 1104
  }

1105
  Future<vm_service.Response?> screenshotSkp() {
1106
    return _checkedCallServiceExtension(kScreenshotSkpMethod);
1107 1108
  }

1109
  /// Set the VM timeline flags.
1110 1111
  Future<void> setTimelineFlags(List<String> recordedStreams) async {
    await _checkedCallServiceExtension(
1112
      'setVMTimelineFlags',
1113
      args: <String, Object?>{
1114 1115 1116 1117 1118
        'recordedStreams': recordedStreams,
      },
    );
  }

1119
  Future<vm_service.Response?> getTimeline() {
1120
    return _checkedCallServiceExtension('getVMTimeline');
1121
  }
1122 1123 1124 1125

  Future<void> dispose() async {
     await service.dispose();
  }
1126 1127
}

1128
/// Thrown when the VM Service disappears while calls are being made to it.
1129
class VmServiceDisappearedException implements Exception { }
1130

1131 1132 1133 1134 1135 1136 1137 1138 1139 1140
/// 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;
1141
}
1142

1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157
/// 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,
}
1158 1159 1160

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