blob: 80fb3c39d7e48082c7994160e4515cfc80836ad5 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:logging/logging.dart' hide LogRecord;
import 'package:pedantic/pedantic.dart';
import 'package:pub_semver/pub_semver.dart' as semver;
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import '../../data/debug_event.dart';
import '../../data/register_event.dart';
import '../../dwds.dart';
import '../debugging/debugger.dart';
import '../debugging/execution_context.dart';
import '../debugging/inspector.dart';
import '../debugging/instance.dart';
import '../debugging/location.dart';
import '../debugging/modules.dart';
import '../debugging/remote_debugger.dart';
import '../debugging/skip_list.dart';
import '../events.dart';
import '../loaders/strategy.dart';
import '../utilities/dart_uri.dart';
import '../utilities/shared.dart';
import 'expression_evaluator.dart';
/// Adds [event] to the stream with [streamId] if there is anybody listening
/// on that stream.
typedef StreamNotify = void Function(String streamId, Event event);
/// Returns the [AppInspector] for the current tab.
///
/// This may be null during a hot restart or page refresh.
typedef AppInspectorProvider = AppInspector Function();
/// A proxy from the chrome debug protocol to the dart vm service protocol.
class ChromeProxyService implements VmServiceInterface {
/// Cache of all existing StreamControllers.
///
/// These are all created through [onEvent].
final _streamControllers = <String, StreamController<Event>>{};
/// The root `VM` instance. There can only be one of these, but its isolates
/// are dynamic and roughly map to chrome tabs.
final VM _vm;
/// Signals when isolate is intialized.
Completer<void> _initializedCompleter = Completer<void>();
Future<void> get isInitialized => _initializedCompleter.future;
/// Signals when expression compiler is ready to evaluate.
Completer<void> _compilerCompleter = Completer<void>();
Future<void> get isCompilerInitialized => _compilerCompleter.future;
/// The root URI at which we're serving.
final String uri;
final RemoteDebugger remoteDebugger;
final ExecutionContext executionContext;
/// Provides debugger-related functionality.
Future<Debugger> get _debugger => _debuggerCompleter.future;
final AssetReader _assetReader;
final Locations _locations;
final SkipLists _skipLists;
final Modules _modules;
final _debuggerCompleter = Completer<Debugger>();
AppInspector _inspector;
/// Public only for testing.
///
/// Returns the [AppInspector] this service uses.
AppInspector appInspectorProvider() => _inspector;
StreamSubscription<ConsoleAPIEvent> _consoleSubscription;
final _disabledBreakpoints = <Breakpoint>{};
final _previousBreakpoints = <Breakpoint>{};
final _logger = Logger('ChromeProxyService');
final ExpressionCompiler _compiler;
ExpressionEvaluator _expressionEvaluator;
bool terminatingIsolates = false;
ChromeProxyService._(
this._vm,
this.uri,
this._assetReader,
this.remoteDebugger,
this._modules,
this._locations,
this._skipLists,
this.executionContext,
this._compiler,
) {
var debugger = Debugger.create(
remoteDebugger,
_streamNotify,
appInspectorProvider,
_locations,
_skipLists,
uri,
);
_debuggerCompleter.complete(debugger);
}
static Future<ChromeProxyService> create(
RemoteDebugger remoteDebugger,
String tabUrl,
AssetReader assetReader,
LoadStrategy loadStrategy,
AppConnection appConnection,
ExecutionContext executionContext,
ExpressionCompiler expressionCompiler) async {
final vm = VM(
name: 'ChromeDebugProxy',
operatingSystem: Platform.operatingSystem,
startTime: DateTime.now().millisecondsSinceEpoch,
version: Platform.version,
isolates: [],
isolateGroups: [],
systemIsolates: [],
systemIsolateGroups: [],
targetCPU: 'Web',
hostCPU: 'DWDS',
architectureBits: -1,
pid: -1,
);
var modules = Modules(tabUrl);
var locations = Locations(assetReader, modules, tabUrl);
var skipLists = SkipLists();
var service = ChromeProxyService._(
vm,
tabUrl,
assetReader,
remoteDebugger,
modules,
locations,
skipLists,
executionContext,
expressionCompiler,
);
unawaited(service.createIsolate(appConnection));
return service;
}
/// Initializes metdata in [Locations], [Modules], and [ExpressionCompiler].
Future<void> _initializeEntrypoint(String entrypoint) async {
_locations.initialize(entrypoint);
_modules.initialize(entrypoint);
_skipLists.initialize();
// We do not need to wait for compiler dependencies to be udpated as the
// [ExpressionEvaluator] is robust to evaluation requests during updates.
unawaited(_updateCompilerDependencies(entrypoint));
}
Future<void> _updateCompilerDependencies(String entrypoint) async {
var metadataProvider = globalLoadStrategy.metadataProviderFor(entrypoint);
var moduleFormat = globalLoadStrategy.moduleFormat;
var soundNullSafety = await metadataProvider.soundNullSafety;
_logger.info('Initializing expression compiler for $entrypoint '
'with sound null safety: $soundNullSafety');
if (_compiler != null) {
await _compiler?.initialize(
moduleFormat: moduleFormat, soundNullSafety: soundNullSafety);
var dependencies =
await globalLoadStrategy.moduleInfoForEntrypoint(entrypoint);
var stopwatch = Stopwatch()..start();
await _compiler.updateDependencies(dependencies);
// Expression evaluation is ready after dependencies are updated.
if (!_compilerCompleter.isCompleted) _compilerCompleter.complete();
emitEvent(DwdsEvent('COMPILER_UPDATE_DEPENDENCIES', {
'entrypoint': entrypoint,
'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
}));
}
}
/// Creates a new isolate.
///
/// Only one isolate at a time is supported, but they should be cleaned up
/// with [destroyIsolate] and recreated with this method there is a hot
/// restart or full page refresh.
Future<void> createIsolate(AppConnection appConnection) async {
if (_inspector?.isolate != null) {
throw UnsupportedError(
'Cannot create multiple isolates for the same app');
}
// Waiting for the debugger to be ready before initializing the entrypoint.
//
// Note: moving `await _debugger` after the `_initalizeEntryPoint` call
// causes `getcwd` system calls to fail. Since that system call is used
// in first `Uri.base` call in the expression compiler service isolate,
// the expression compiler service will fail to start.
// Issue: https://github.com/dart-lang/webdev/issues/1282
var debugger = await _debugger;
await _initializeEntrypoint(appConnection.request.entrypointPath);
debugger.notifyPausedAtStart();
_inspector = await AppInspector.initialize(
appConnection,
remoteDebugger,
_assetReader,
_locations,
uri,
debugger,
executionContext,
);
_expressionEvaluator = _compiler == null
? null
: ExpressionEvaluator(_inspector, _locations, _modules, _compiler);
await debugger.reestablishBreakpoints(
_previousBreakpoints, _disabledBreakpoints);
_disabledBreakpoints.clear();
unawaited(appConnection.onStart.then((_) async {
await debugger.resumeFromStart();
}));
var isolateRef = _inspector.isolateRef;
var timestamp = DateTime.now().millisecondsSinceEpoch;
// Listen for `registerExtension` and `postEvent` calls.
_setUpChromeConsoleListeners(isolateRef);
_vm.isolates.add(isolateRef);
_streamNotify(
'Isolate',
Event(
kind: EventKind.kIsolateStart,
timestamp: timestamp,
isolate: isolateRef));
_streamNotify(
'Isolate',
Event(
kind: EventKind.kIsolateRunnable,
timestamp: timestamp,
isolate: isolateRef));
// TODO: We shouldn't need to fire these events since they exist on the
// isolate, but devtools doesn't recognize extensions after a page refresh
// otherwise.
for (var extensionRpc in _inspector.isolate.extensionRPCs) {
_streamNotify(
'Isolate',
Event(
kind: EventKind.kServiceExtensionAdded,
timestamp: timestamp,
isolate: isolateRef)
..extensionRPC = extensionRpc);
}
// The service is considered initialized when the first isolate is created.
if (!_initializedCompleter.isCompleted) _initializedCompleter.complete();
}
/// Should be called when there is a hot restart or full page refresh.
///
/// Clears out the [_inspector] and all related cached information.
void destroyIsolate() {
var isolate = _inspector?.isolate;
if (isolate == null) return;
_initializedCompleter = Completer<void>();
_compilerCompleter = Completer<void>();
_streamNotify(
'Isolate',
Event(
kind: EventKind.kIsolateExit,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: _inspector.isolateRef));
_vm.isolates.removeWhere((ref) => ref.id == isolate.id);
_inspector = null;
_previousBreakpoints.clear();
_previousBreakpoints.addAll(isolate.breakpoints);
_consoleSubscription?.cancel();
_consoleSubscription = null;
}
Future<void> disableBreakpoints() async {
_disabledBreakpoints.clear();
var isolate = _inspector?.isolate;
if (isolate == null) return;
_disabledBreakpoints.addAll(isolate.breakpoints);
for (var breakpoint in isolate.breakpoints.toList()) {
await (await _debugger).removeBreakpoint(isolate.id, breakpoint.id);
}
}
@override
Future<Breakpoint> addBreakpoint(String isolateId, String scriptId, int line,
{int column}) async {
await isInitialized;
return (await _debugger)
.addBreakpoint(isolateId, scriptId, line, column: column);
}
@override
Future<Breakpoint> addBreakpointAtEntry(String isolateId, String functionId) {
return _rpcNotSupportedFuture('addBreakpointAtEntry');
}
@override
Future<Breakpoint> addBreakpointWithScriptUri(
String isolateId, String scriptUri, int line,
{int column}) async {
await isInitialized;
var dartUri = DartUri(scriptUri, uri);
var ref = await _inspector.scriptRefFor(dartUri.serverPath);
return (await _debugger)
.addBreakpoint(isolateId, ref.id, line, column: column);
}
@override
Future<Response> callServiceExtension(String method,
{String isolateId, Map args}) async {
await isInitialized;
// Validate the isolate id is correct, _getIsolate throws if not.
if (isolateId != null) _getIsolate(isolateId);
args ??= <String, String>{};
var stringArgs = args.map((k, v) => MapEntry(
k is String ? k : jsonEncode(k), v is String ? v : jsonEncode(v)));
var expression = '''
${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension(
"$method", JSON.stringify(${jsonEncode(stringArgs)}));
''';
var response =
await remoteDebugger.sendCommand('Runtime.evaluate', params: {
'expression': expression,
'awaitPromise': true,
'contextId': await executionContext.id,
});
handleErrorIfPresent(response, evalContents: expression);
var decodedResponse =
jsonDecode(response.result['result']['value'] as String)
as Map<String, dynamic>;
if (decodedResponse.containsKey('code') &&
decodedResponse.containsKey('message') &&
decodedResponse.containsKey('data')) {
// ignore: only_throw_errors
throw RPCError(method, decodedResponse['code'] as int,
decodedResponse['message'] as String, decodedResponse['data'] as Map);
} else {
return Response()..json = decodedResponse;
}
}
@override
Future<Success> clearVMTimeline() {
return _rpcNotSupportedFuture('clearVMTimeline');
}
void _validateIsolateId(String isolateId) {
var isolate = _inspector?.isolate;
if (isolate?.id != isolateId) {
throw RPCError('evaluateInFrame', RPCError.kInvalidParams,
'Unrecognized isolate id: $isolateId. Supported isolate: ${isolate?.id}');
}
}
Future<Response> _getEvaluationResult(
Future<RemoteObject> Function() evaluation, String expression) async {
try {
var result = await evaluation();
// Handle compilation errors, internal errors,
// and reference errors from JavaScript evaluation in chrome.
if (result.type.contains('Error')) {
if (!result.type.startsWith('CompilationError')) {
_logger.warning('Failed to evaluate expression \'$expression\': '
'${result.type}: ${result.value}.');
_logger.info('Please follow instructions at '
'https://github.com/dart-lang/webdev/issues/956 '
'to file a bug.');
}
return ErrorRef(
kind: 'error',
message: '${result.type}: ${result.value}',
id: createId(),
);
}
return _inspector?.instanceHelper?.instanceRefFor(result);
} on RPCError catch (_) {
rethrow;
} catch (e, s) {
// Handle errors that throw exceptions, such as invalid JavaScript
// generated by the expression evaluator.
_logger.warning('Failed to evaluate expression \'$expression\'. ');
_logger.info('Please follow instructions at '
'https://github.com/dart-lang/webdev/issues/956 '
'to file a bug.');
_logger.info('$e:$s');
return ErrorRef(kind: 'error', message: '<unknown>', id: createId());
}
}
@override
Future<Response> evaluate(
String isolateId,
String targetId,
String expression, {
Map<String, String> scope,
bool disableBreakpoints,
}) async {
// TODO(798) - respect disableBreakpoints.
var stopwatch = Stopwatch()..start();
dynamic error;
try {
await isInitialized;
if (_expressionEvaluator != null) {
await isCompilerInitialized;
_validateIsolateId(isolateId);
var library = await _inspector?.getLibrary(isolateId, targetId);
var result = await _getEvaluationResult(
() => _expressionEvaluator.evaluateExpression(
isolateId, library.uri, expression, scope),
expression);
if (result is ErrorRef) {
error = result;
}
return result;
}
// fall back to javascript evaluation
var remote = await _inspector?.evaluate(isolateId, targetId, expression,
scope: scope);
return _inspector?.instanceHelper?.instanceRefFor(remote);
} catch (e) {
error = e;
rethrow;
} finally {
emitEvent(DwdsEvent('EVALUATE', {
'expression': expression,
'success': error == null,
'exception': error,
'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
}));
}
}
@override
Future<Response> evaluateInFrame(
String isolateId, int frameIndex, String expression,
{Map<String, String> scope, bool disableBreakpoints}) async {
// TODO(798) - respect disableBreakpoints.
var stopwatch = Stopwatch()..start();
dynamic error;
try {
await isInitialized;
if (_expressionEvaluator != null) {
await isCompilerInitialized;
_validateIsolateId(isolateId);
if (scope != null) {
// TODO(annagrin): Implement scope support.
// Issue: https://github.com/dart-lang/webdev/issues/1344
throw RPCError(
'evaluateInFrame',
RPCError.kInvalidRequest,
'Expression evaluation with scope is not supported '
'for this configuration.');
}
var result = await _getEvaluationResult(
() => _expressionEvaluator.evaluateExpressionInFrame(
isolateId, frameIndex, expression, scope),
expression);
if (result is ErrorRef) {
error = result;
}
return result;
}
throw RPCError('evaluateInFrame', RPCError.kInvalidRequest,
'Expression evaluation is not supported for this configuration.');
} catch (e) {
error = e;
rethrow;
} finally {
emitEvent(DwdsEvent('EVALUATE_IN_FRAME', {
'expression': expression,
'success': error == null,
'exception': error,
'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
}));
}
}
@override
Future<AllocationProfile> getAllocationProfile(String isolateId,
{bool gc, bool reset}) {
return _rpcNotSupportedFuture('getAllocationProfile');
}
@override
Future<ClassList> getClassList(String isolateId) {
// See dart-lang/webdev/issues/971.
return _rpcNotSupportedFuture('getClassList');
}
@override
Future<FlagList> getFlagList() async {
// VM flags do not apply to web apps.
return FlagList(flags: []);
}
@override
Future<InstanceSet> getInstances(
String isolateId, String classId, int limit) {
return _rpcNotSupportedFuture('getInstances');
}
/// Sync version of [getIsolate] for internal use, also has stronger typing
/// than the public one which has to be dynamic.
Isolate _getIsolate(String isolateId) {
var isolate = _inspector?.isolate;
if (isolate?.id == isolateId) return isolate;
// TODO: Throw an RPC error here.
throw ArgumentError.value(isolateId, 'isolateId',
'Unrecognized isolate id. (Supported isolate: ${isolate?.id})');
}
@override
Future<Isolate> getIsolate(String isolateId) async {
await isInitialized;
return _getIsolate(isolateId);
}
@override
Future<MemoryUsage> getMemoryUsage(String isolateId) async {
await isInitialized;
return _inspector.getMemoryUsage(isolateId);
}
@override
Future<Obj> getObject(String isolateId, String objectId,
{int offset, int count}) async {
await isInitialized;
return _inspector?.getObject(isolateId, objectId,
offset: offset, count: count);
}
@override
Future<ScriptList> getScripts(String isolateId) async {
await isInitialized;
return _inspector?.getScripts(isolateId);
}
@override
Future<SourceReport> getSourceReport(String isolateId, List<String> reports,
{String scriptId,
int tokenPos,
int endTokenPos,
bool forceCompile}) async {
await isInitialized;
return _inspector?.getSourceReport(isolateId, reports,
scriptId: scriptId,
tokenPos: tokenPos,
endTokenPos: endTokenPos,
forceCompile: forceCompile);
}
/// Returns the current stack.
///
/// Returns null if the corresponding isolate is not paused.
///
/// The returned stack will contain up to [limit] frames if provided.
@override
Future<Stack> getStack(String isolateId, {int limit}) async {
await isInitialized;
return (await _debugger).getStack(isolateId, limit: limit);
}
@override
Future<VM> getVM() async {
await isInitialized;
return _vm;
}
@override
Future<Timeline> getVMTimeline({int timeOriginMicros, int timeExtentMicros}) {
return _rpcNotSupportedFuture('getVMTimeline');
}
@override
Future<TimelineFlags> getVMTimelineFlags() {
return _rpcNotSupportedFuture('getVMTimelineFlags');
}
@override
Future<Version> getVersion() async {
var version = semver.Version.parse(vmServiceVersion);
return Version(major: version.major, minor: version.minor);
}
@override
Future<Response> invoke(
String isolateId, String targetId, String selector, List argumentIds,
{bool disableBreakpoints}) async {
await isInitialized;
// TODO(798) - respect disableBreakpoints.
var remote =
await _inspector?.invoke(isolateId, targetId, selector, argumentIds);
var result = _inspector?.instanceHelper?.instanceRefFor(remote);
if (result == null) {
throw ChromeDebugException(
{'text': 'null result from invoke of $selector'});
}
return result;
}
@override
Future<Success> kill(String isolateId) {
return _rpcNotSupportedFuture('kill');
}
@override
Stream<Event> onEvent(String streamId) {
return _streamControllers.putIfAbsent(streamId, () {
switch (streamId) {
case EventStreams.kExtension:
return StreamController<Event>.broadcast();
case EventStreams.kIsolate:
// TODO: right now we only support the `ServiceExtensionAdded` event
// for the Isolate stream.
return StreamController<Event>.broadcast();
case EventStreams.kVM:
return StreamController<Event>.broadcast();
case EventStreams.kGC:
return StreamController<Event>.broadcast();
case EventStreams.kTimeline:
return StreamController<Event>.broadcast();
case EventStreams.kService:
return StreamController<Event>.broadcast();
case EventStreams.kDebug:
return StreamController<Event>.broadcast();
case EventStreams.kLogging:
return StreamController<Event>.broadcast();
case EventStreams.kStdout:
return _chromeConsoleStreamController(
(e) => _stdoutTypes.contains(e.type));
case EventStreams.kStderr:
return _chromeConsoleStreamController(
(e) => _stderrTypes.contains(e.type),
includeExceptions: true);
default:
throw RPCError(
'streamListen',
RPCError.kMethodNotFound,
'The stream `$streamId` is not supported on web devices',
);
}
}).stream;
}
@override
Future<Success> pause(String isolateId) async {
await isInitialized;
return (await _debugger).pause();
}
@override
Future<Success> registerService(String service, String alias) async {
return _rpcNotSupportedFuture('registerService');
}
@override
Future<ReloadReport> reloadSources(String isolateId,
{bool force, bool pause, String rootLibUri, String packagesUri}) {
return Future.error(RPCError(
'reloadSources',
RPCError.kMethodNotFound,
'Hot reload not supported on web devices',
));
}
@override
Future<Success> removeBreakpoint(
String isolateId, String breakpointId) async {
await isInitialized;
_disabledBreakpoints
.removeWhere((breakpoint) => breakpoint.id == breakpointId);
return (await _debugger).removeBreakpoint(isolateId, breakpointId);
}
@override
Future<Success> resume(String isolateId,
{String step, int frameIndex}) async {
if (_inspector == null) throw StateError('No running isolate.');
if (_inspector.appConnection.isStarted) {
var stopwatch = Stopwatch()..start();
await isInitialized;
var result = await (await _debugger)
.resume(isolateId, step: step, frameIndex: frameIndex);
emitEvent(DwdsEvent('RESUME', {
'step': step,
'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
}));
return result;
} else {
_inspector.appConnection.runMain();
return Success();
}
}
@override
Future<Success> setExceptionPauseMode(String isolateId, String mode) async {
await isInitialized;
return (await _debugger).setExceptionPauseMode(isolateId, mode);
}
@override
Future<Success> setFlag(String name, String value) {
return _rpcNotSupportedFuture('setFlag');
}
@override
Future<Success> setLibraryDebuggable(
String isolateId, String libraryId, bool isDebuggable) {
return _rpcNotSupportedFuture('setLibraryDebuggable');
}
@override
Future<Success> setName(String isolateId, String name) async {
await isInitialized;
var isolate = _getIsolate(isolateId);
isolate.name = name;
return Success();
}
@override
Future<Success> setVMName(String name) async {
_vm.name = name;
_streamNotify(
'VM',
Event(
kind: EventKind.kVMUpdate,
timestamp: DateTime.now().millisecondsSinceEpoch,
// We are not guaranteed to have an isolate at this point an time.
isolate: null)
..vm = toVMRef(_vm));
return Success();
}
@override
Future<Success> setVMTimelineFlags(List<String> recordedStreams) {
return _rpcNotSupportedFuture('setVMTimelineFlags');
}
@override
Future<Success> streamCancel(String streamId) {
// TODO: We should implement this (as we've already implemented
// streamListen).
return _rpcNotSupportedFuture('streamCancel');
}
@override
Future<Success> streamListen(String streamId) async {
// TODO: This should return an error if the stream is already being listened
// to.
onEvent(streamId);
return Success();
}
@override
Future<Success> clearCpuSamples(String isolateId) {
return _rpcNotSupportedFuture('clearCpuSamples');
}
@override
Future<CpuSamples> getCpuSamples(
String isolateId, int timeOriginMicros, int timeExtentMicros) {
return _rpcNotSupportedFuture('getCpuSamples');
}
/// Returns a streamController that listens for console logs from chrome and
/// adds all events passing [filter] to the stream.
StreamController<Event> _chromeConsoleStreamController(
bool Function(ConsoleAPIEvent) filter,
{bool includeExceptions = false}) {
StreamController<Event> controller;
StreamSubscription chromeConsoleSubscription;
StreamSubscription exceptionsSubscription;
// This is an edge case for this lint apparently
//
// ignore: join_return_with_assignment
controller = StreamController<Event>.broadcast(onCancel: () {
chromeConsoleSubscription?.cancel();
exceptionsSubscription?.cancel();
}, onListen: () {
chromeConsoleSubscription = remoteDebugger.onConsoleAPICalled.listen((e) {
var isolate = _inspector?.isolate;
if (isolate == null) return;
if (!filter(e)) return;
var args = e.params['args'] as List;
var item = args[0] as Map;
var value = '${item["value"]}\n';
controller.add(Event(
kind: EventKind.kWriteEvent,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: _inspector.isolateRef)
..bytes = base64.encode(utf8.encode(value))
..timestamp = e.timestamp.toInt());
});
if (includeExceptions) {
exceptionsSubscription = remoteDebugger.onExceptionThrown.listen((e) {
var isolate = _inspector?.isolate;
if (isolate == null) return;
controller.add(Event(
kind: EventKind.kWriteEvent,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: _inspector.isolateRef)
..bytes = base64.encode(
utf8.encode(e.exceptionDetails.exception.description ?? '')));
});
}
});
return controller;
}
/// Parses the [DebugEvent] and emits a corresponding Dart VM Service
/// protocol [Event].
Future<void> parseDebugEvent(DebugEvent debugEvent) async {
if (terminatingIsolates) return;
var isolate = _inspector?.isolate;
if (isolate == null) return;
_streamNotify(
EventStreams.kExtension,
Event(
kind: EventKind.kExtension,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolate)
..extensionKind = debugEvent.kind
..extensionData = ExtensionData.parse(
jsonDecode(debugEvent.eventData) as Map<String, dynamic>));
}
/// Parses the [RegisterEvent] and emits a corresponding Dart VM Service
/// protocol [Event].
Future<void> parseRegisterEvent(RegisterEvent registerEvent) async {
if (terminatingIsolates) return;
var isolate = _inspector?.isolate;
if (isolate == null) return;
var service = registerEvent.eventData;
isolate.extensionRPCs.add(service);
_streamNotify(
EventStreams.kIsolate,
Event(
kind: EventKind.kServiceExtensionAdded,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolate)
..extensionRPC = service);
}
/// Listens for chrome console events and handles the ones we care about.
void _setUpChromeConsoleListeners(IsolateRef isolateRef) {
_consoleSubscription =
remoteDebugger.onConsoleAPICalled.listen((event) async {
if (terminatingIsolates) return;
if (event.type != 'debug') return;
var isolate = _inspector?.isolate;
if (isolate == null) return;
if (isolateRef.id != isolate.id) return;
var firstArgValue = event.args[0].value as String;
// TODO(grouma) - Remove when the min SDK has updated to migrate users
// over to the injected client communication approach.
switch (firstArgValue) {
case 'dart.developer.registerExtension':
var service = event.args[1].value as String;
isolate.extensionRPCs.add(service);
_streamNotify(
EventStreams.kIsolate,
Event(
kind: EventKind.kServiceExtensionAdded,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolateRef)
..extensionRPC = service);
break;
case 'dart.developer.postEvent':
_streamNotify(
EventStreams.kExtension,
Event(
kind: EventKind.kExtension,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolateRef)
..extensionKind = event.args[1].value as String
..extensionData = ExtensionData.parse(
jsonDecode(event.args[2].value as String)
as Map<String, dynamic>));
break;
case 'dart.developer.inspect':
// All inspected objects should be real objects.
if (event.args[1].type != 'object') break;
var inspectee =
await _inspector.instanceHelper.instanceRefFor(event.args[1]);
_streamNotify(
EventStreams.kDebug,
Event(
kind: EventKind.kInspect,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolateRef)
..inspectee = inspectee
..timestamp = event.timestamp.toInt());
break;
case 'dart.developer.log':
_handleDeveloperLog(isolateRef, event);
break;
default:
break;
}
});
}
void _streamNotify(String streamId, Event event) {
var controller = _streamControllers[streamId];
if (controller == null) return;
controller.add(event);
}
void _handleDeveloperLog(IsolateRef isolateRef, ConsoleAPIEvent event) async {
var logObject = event.params['args'][1] as Map;
var logParams = <String, RemoteObject>{};
for (dynamic obj in logObject['preview']['properties']) {
if (obj['name'] != null && obj is Map<String, dynamic>) {
logParams[obj['name'] as String] = RemoteObject(obj);
}
}
var logRecord = LogRecord(
message: await _instanceRef(logParams['message']),
loggerName: await _instanceRef(logParams['name']),
level: logParams['level'] != null
? int.tryParse(logParams['level'].value.toString())
: 0,
error: await _instanceRef(logParams['error']),
time: event.timestamp.toInt(),
sequenceNumber: logParams['sequenceNumber'] != null
? int.tryParse(logParams['sequenceNumber'].value.toString())
: 0,
stackTrace: await _instanceRef(logParams['stackTrace']),
zone: await _instanceRef(logParams['zone']),
);
_streamNotify(
EventStreams.kLogging,
Event(
kind: EventKind.kLogging,
timestamp: DateTime.now().millisecondsSinceEpoch,
isolate: isolateRef)
..logRecord = logRecord
..timestamp = event.timestamp.toInt(),
);
}
@override
Future<Timestamp> getVMTimelineMicros() {
return _rpcNotSupportedFuture('getVMTimelineMicros');
}
@override
Future<InboundReferences> getInboundReferences(
String isolateId, String targetId, int limit) {
return _rpcNotSupportedFuture('getInboundReferences');
}
@override
Future<RetainingPath> getRetainingPath(
String isolateId, String targetId, int limit) {
return _rpcNotSupportedFuture('getRetainingPath');
}
@override
Future<Success> requestHeapSnapshot(String isolateId) {
return _rpcNotSupportedFuture('requestHeapSnapshot');
}
@override
Future<IsolateGroup> getIsolateGroup(String isolateGroupId) {
return _rpcNotSupportedFuture('getIsolateGroup');
}
@override
Future<MemoryUsage> getIsolateGroupMemoryUsage(String isolateGroupId) {
return _rpcNotSupportedFuture('getIsolateGroupMemoryUsage');
}
@override
Future<ProtocolList> getSupportedProtocols() async {
var version = semver.Version.parse(vmServiceVersion);
return ProtocolList(protocols: [
Protocol(
protocolName: 'VM Service',
major: version.major,
minor: version.minor,
)
]);
}
Future<InstanceRef> _instanceRef(RemoteObject obj) async {
if (obj == null) {
return InstanceHelper.kNullInstanceRef;
} else {
return _inspector.instanceHelper.instanceRefFor(obj);
}
}
static RPCError _rpcNotSupported(String method) {
return RPCError(method, RPCError.kMethodNotFound,
'$method: Not supported on web devices');
}
static Future<T> _rpcNotSupportedFuture<T>(String method) {
return Future.error(_rpcNotSupported(method));
}
@override
Future<ProcessMemoryUsage> getProcessMemoryUsage() =>
_rpcNotSupportedFuture('getProcessMemoryUsage');
@override
Future<PortList> getPorts(String isolateId) => throw UnimplementedError();
@override
Future<CpuSamples> getAllocationTraces(String isolateId,
{int timeOriginMicros, int timeExtentMicros, String classId}) =>
throw UnimplementedError();
@override
Future<Success> setTraceClassAllocation(
String isolateId, String classId, bool enable) =>
throw UnimplementedError();
@override
Future<Breakpoint> setBreakpointState(
String isolateId, String breakpointId, bool enable) =>
throw UnimplementedError();
}
/// The `type`s of [ConsoleAPIEvent]s that are treated as `stderr` logs.
const _stderrTypes = ['error'];
/// The `type`s of [ConsoleAPIEvent]s that are treated as `stdout` logs.
const _stdoutTypes = ['log', 'info', 'warning'];
class ChromeDebugException extends ExceptionDetails implements Exception {
/// Optional, additional information about the exception.
final Object additionalDetails;
/// Optional, the exact contents of the eval that was attempted.
final String evalContents;
ChromeDebugException(Map<String, dynamic> exceptionDetails,
{this.additionalDetails, this.evalContents})
: super(exceptionDetails);
@override
String toString() {
var description = StringBuffer()
..writeln('Unexpected error from chrome devtools:');
if (text != null) {
description.writeln('text: $text');
}
if (exception != null) {
description.writeln('exception:');
description.writeln(' description: ${exception.description}');
description.writeln(' type: ${exception.type}');
description.writeln(' value: ${exception.value}');
}
if (evalContents != null) {
description.writeln('attempted JS eval: `$evalContents`');
}
if (additionalDetails != null) {
description.writeln('additional details:\n $additionalDetails');
}
return description.toString();
}
}