blob: 9d063911f738308d92ad5f15e440e6bd2b3a59a8 [file] [log] [blame]
// Copyright (c) 2015, 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.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:async/async.dart';
import 'package:collection/collection.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'breakpoint.dart';
import 'error.dart';
import 'exceptions.dart';
import 'library.dart';
import 'pause_event.dart';
import 'scope.dart';
import 'sentinel.dart';
import 'source_report.dart';
import 'stack.dart';
import 'stream_manager.dart';
import 'utils.dart';
VMIsolateRef newVMIsolateRef(rpc.Peer peer, StreamManager streams, Map json) {
if (json == null) return null;
assert(json["type"] == "@Isolate" || json["type"] == "Isolate");
var scope = new Scope(peer, streams, json["id"]);
return new VMIsolateRef._(scope, json);
}
/// A reference to an isolate on the remote VM.
///
/// The full isolate with additional metadata can be loaded using [load].
class VMIsolateRef {
final Scope _scope;
/// A unique numeric ID for this isolate.
///
/// Note that this may be larger than can be represented in Dart
/// implementations that compile to JS; it's generally safer to use
/// [numberAsString] instead.
final int number;
/// The string representation of [number].
final String numberAsString;
/// A name identifying this isolate for debugging.
///
/// This isn't guaranteed to be unique. It can be set using [setName].
final String name;
/// A *relative* URL for humans to inspect and interact with this isolate in
/// the Observatory UI.
///
/// Because the VM service client doesn't always know the full location of the
/// Observatory UI, this needs to be resolved against the absolute URL of the
/// Observatory UI in order to be usable.
Uri get observatoryUrl => Uri.parse(
"#/inspect?isolateId=${Uri.encodeQueryComponent(_scope.isolateId)}");
/// A broadcast stream that emits a `null` value every time a garbage
/// collection occurs in this isolate.
Stream get onGC => _onGC;
Stream _onGC;
/// A broadcast stream that emits a new reference to this isolate every time
/// its metadata changes.
Stream<VMIsolateRef> get onUpdate => _onUpdate;
Stream<VMIsolateRef> _onUpdate;
/// A broadcast stream that emits a [VMPauseEvent] whenever this isolate is
/// paused or resumed.
Stream<VMPauseEvent> get onPauseOrResume => _onPauseOrResume;
Stream<VMPauseEvent> _onPauseOrResume;
/// A broadcast stream that emits a [VMBreakpoint] whenever a breakpoint is
/// added.
Stream<VMBreakpoint> get onBreakpointAdded => _onBreakpointAdded;
Stream<VMBreakpoint> _onBreakpointAdded;
/// A broadcast stream that emits custom events posted using `postEvent` from
/// `dart:developer`.
///
/// This is supported as of VM service version 3.1, or Dart SDK version 1.14.
Stream<VMExtensionEvent> get onExtensionEvent => _onExtensionEvent;
Stream<VMExtensionEvent> _onExtensionEvent;
/// A broadcast stream that emits this isolate's standard output.
///
/// This is only usable for embedders that provide access to `dart:io`.
///
/// Note that as of the VM service version 3.0, this stream doesn't emit
/// strings passed to the `print()` function unless the host process's
/// standard output is actively being drained (see [sdk#24351][]).
///
/// [sdk#24351]: https://github.com/dart-lang/sdk/issues/24351
Stream<List<int>> get stdout => _stdout;
Stream<List<int>> _stdout;
/// A broadcast stream that emits this isolate's standard error.
///
/// This is only usable for embedders that provide access to `dart:io`.
Stream<List<int>> get stderr => _stderr;
Stream<List<int>> _stderr;
/// A broadcast stream that emits an event whenever a new VM service extension
/// RPC is registered.
///
/// Each event is the name of a registered RPC.
///
/// This is supported as of VM service version 3.1, or Dart SDK version 1.14.
Stream<String> get onExtensionAdded => _onExtensionAdded;
Stream<String> _onExtensionAdded;
/// A future that fires when the isolate exits.
///
/// If the isolate has already exited, this will complete immediately.
Future get onExit => _onExitMemo.runOnce(() async {
try {
await _scope.getInState(_scope.streams.isolate, () async {
try {
await load();
return null;
} on VMSentinelException catch (_) {
// Return a non-null value to indicate that the breakpoint is in the
// expected state—that is, it no longer exists.
return true;
}
}, (json) {
if (json["isolate"]["id"] != _scope.isolateId) return null;
if (json["kind"] != "IsolateExit") return null;
return true;
});
} on StateError catch (_) {
// Ignore state errors. They indicate that the underlying stream closed
// before an exit event was fired, which means that the process and thus
// this isolate is dead.
}
});
final _onExitMemo = new AsyncMemoizer();
VMIsolateRef._(this._scope, Map json)
: number = int.parse(json["number"]),
numberAsString = json["number"],
name = json["name"] {
_onGC = _transform(_scope.streams.gc, (json, sink) {
if (json["kind"] == "GC") sink.add(null);
});
_onUpdate = _transform(_scope.streams.isolate, (json, sink) {
if (json["kind"] != "IsolateUpdate") return;
sink.add(new VMIsolateRef._(_scope, json["isolate"]));
});
_onExtensionAdded = _transform(_scope.streams.isolate, (json, sink) {
if (json["kind"] != "ServiceExtensionAdded") return;
sink.add(json["extensionRPC"]);
});
_onPauseOrResume = _transform(_scope.streams.debug, (json, sink) {
var event = newVMPauseEvent(_scope, json);
if (event != null) sink.add(event);
});
_onBreakpointAdded = _transform(_scope.streams.debug, (json, sink) {
if (json["kind"] != "BreakpointAdded") return;
sink.add(newVMBreakpoint(_scope, json["breakpoint"]));
});
_stdout = _transform(_scope.streams.stdout, (json, sink) {
if (json["kind"] != "WriteEvent") return;
var bytes = base64Decode(json["bytes"]);
sink.add(bytes);
});
_stderr = _transform(_scope.streams.stderr, (json, sink) {
if (json["kind"] != "WriteEvent") return;
sink.add(base64Decode(json["bytes"]));
});
_onExtensionEvent = _transform(_scope.streams.extension, (json, sink) {
sink.add(new VMExtensionEvent._(json));
});
}
/// Like [transform], but only calls [handleData] for events related to this
/// isolate.
Stream<T> _transform<T>(
Stream<Map> stream, void handleData(Map json, Sink<T> sink)) {
return transform(stream, (json, sink) {
if (json["isolate"]["id"] != _scope.isolateId) return;
handleData(json, sink);
});
}
/// Loads the full representation of this isolate once it becomes runnable.
///
/// This will work whether this isolate is already runnable or has yet to
/// become runnable.
///
/// This is only supported on the VM service protocol version 3.0 and greater.
Future<VMRunnableIsolate> loadRunnable() {
return _scope.getInState(_scope.streams.isolate, () async {
var isolate = await load();
return isolate is VMRunnableIsolate ? isolate : null;
}, (json) async {
if (json["kind"] != "IsolateRunnable") return null;
return (await load()) as VMRunnableIsolate;
});
}
// Note that if anyone else is using the VM service, the VM may be unpaused by
// the time this fires.
/// Returns a future that completes once this isolate is paused.
///
/// This works whether the isolate is already paused or has yet to be paused.
/// Note that if any other code (other VM service clients or other isolates)
/// unpauses the isolate, it may be unpaused by the time the returned future
/// fires.
Future waitUntilPaused() {
return _scope.getInState(_scope.streams.debug, () async {
return (await load()).isPaused;
}, (json) {
return json["kind"] == "PauseStart" ||
json["kind"] == "PauseException" ||
json["kind"] == "PauseExit" ||
json["kind"] == "PauseInterrupted" ||
json["kind"] == "PauseBreakpoint";
});
}
/// Loads the full representation of this isolate.
///
/// Throws a [VMSentinelException] if this isolate is no longer available.
Future<VMIsolate> load() async {
var response = await _scope.sendRequest("getIsolate");
// Work around sdk#24142.
if (response["type"] == "Error") {
throw new VMSentinelException(VMSentinel.collected);
} else if (response["type"] == "Sentinel") {
throw new VMSentinelException(newVMSentinel(response));
} else {
return response["rootLib"] == null ||
// Work around sdk#24140
response["rootLib"]["type"] == "@Instance"
? new VMIsolate._(_scope, response)
: new VMRunnableIsolate._(_scope, response);
}
}
/// Returns a broadcast stream that emits custom events posted via `postEvent`
/// from the `dart:developer` package.
///
/// Unlike [onExtensionEvent], this only emits events for which
/// [VMExtensionEvent.kind] is [kind]. If [prefix] is `true`, it also emits
/// events for which [VMExtensionEvent.kind] starts with [kind].
///
/// This is supported as of VM service version 3.1, or Dart SDK version 1.14.
Stream<VMExtensionEvent> selectExtensionEvents(String kind,
{bool prefix: false}) {
return transform(_onExtensionEvent, (event, sink) {
if (prefix == null ? event.kind == kind : event.kind.startsWith(kind)) {
sink.add(event);
}
});
}
/// Returns the isolate's current execution stack and message queue.
Future<VMStack> getStack() async =>
newVMStack(_scope, await _scope.sendRequest("getStack"));
/// Pauses this isolate.
///
/// The returned future may complete before the isolate is paused.
Future pause() async {
await _scope.sendRequest("pause");
}
/// Resumes execution of this isolate, if it's paused.
///
/// [step] controls how execution proceeds; it defaults to [VMStep.resume].
///
/// Throws an [rpc.RpcException] if the isolate isn't paused.
Future resume({VMStep step}) {
if (step == null) step = VMStep.resume;
return _scope.sendRequest(
"resume", step == VMStep.resume ? {} : {"step": step._value});
}
/// Sets the pause behaviour for exceptions.
Future setExceptionPauseMode(VMExceptionPauseMode mode) =>
_scope.sendRequest("setExceptionPauseMode", {"mode": mode._value});
/// Sets the [name] of the isolate.
///
/// Note that since this object is immutable, it needs to be reloaded to see
/// the new name.
Future setName(String name) => _scope.sendRequest("setName", {"name": name});
/// Adds a breakpoint at [line] (and optionally [column]) in the script with
/// the given canonical [uri].
///
/// Both [line] and [column] are 1-based. The [uri] may be a [String] or a
/// [Uri].
Future<VMBreakpoint> addBreakpoint(uri, int line, {int column}) async {
if (uri is! String && uri is! Uri) {
throw new ArgumentError("Invalid uri '$uri', must be a Uri or a String.");
}
var params = {"scriptUri": uri.toString(), "line": line};
if (column != null) params["column"] = column;
try {
var response =
await _scope.sendRequest("addBreakpointWithScriptUri", params);
return newVMBreakpoint(_scope, response);
} on rpc.RpcException catch (error) {
// Error 102 indicates that the breakpoint couldn't be created.
if (error.code == 102) return null;
rethrow;
}
}
/// Returns a future that completes once the VM service extension RPC with the
/// given [name] is available.
///
/// This works whether the extension is already registered or has yet to be
/// registered.
///
/// This is supported as of VM service version 3.1, or Dart SDK version 1.14.
Future waitForExtension(String name) async {
return _scope.getInState(_scope.streams.isolate, () async {
var extensions = (await load()).extensionRpcs;
return extensions.contains(name);
}, (Map json) {
return json["kind"] == "ServiceExtensionAdded" &&
json["extensionRPC"] == name;
}).then((_) => null);
}
/// Invokes the VM service extension RPC named [method] registered in this
/// isolate.
///
/// VM service extensions are registered using the `registerExtension` method
/// in `dart:developer`. The [method] name must be the same as the name passed
/// to `registerExtension`, which means it must begin with "ext.".
///
/// The [params] are passed to the extension handler.
///
/// Returns the extension handler's decoded response.
///
/// This is supported as of VM service version 3.1, or Dart SDK version 1.14.
Future<Object> invokeExtension(String method, [Map<String, String> params]) {
if (!method.startsWith('ext.')) {
throw new ArgumentError.value(
method, 'method', 'must begin with "ext." prefix');
}
return _scope.sendRequestRaw(method, params);
}
/// Generates a report of code coverage information and possible break points
/// for the scripts in this isolate.
///
/// If [includeCoverageReport] is `true`, the report includes code coverage
/// information via [VMSourceReportRange.hits] and
/// [VMSourceReportRange.misses] in [VMSourceReport.ranges]. Otherwise,
/// these properties are `null`.
///
/// If [includePossibleBreakpoints] is `true`, the report includes a list of
/// token positions which correspond to possible breakpoints via
/// [VMSourceReportRange.possibleBreakpoints] in [VMSourceReport.ranges].
/// Otherwise, [VMSourceReportRange.possibleBreakpoints] is `null`.
///
/// If [forceCompile] is `true`, all functions in the range of the report
/// will be compiled. If `false`, functions that are never used may not appear
/// in [VMSourceReportRange.misses]. Forcing compilation can cause a
/// compilation error, which could terminate the running Dart program.
Future<VMSourceReport> getSourceReport(
{bool includeCoverageReport: true,
bool includePossibleBreakpoints: true,
bool forceCompile: false}) async {
var reports = <String>[];
if (includeCoverageReport) reports.add('Coverage');
if (includePossibleBreakpoints) reports.add('PossibleBreakpoints');
var params = <String, dynamic>{'reports': reports};
if (forceCompile) params['forceCompile'] = true;
var json = await _scope.sendRequest('getSourceReport', params);
return newSourceReport(_scope, json);
}
bool operator ==(other) =>
other is VMIsolateRef && other._scope.isolateId == _scope.isolateId;
int get hashCode => _scope.isolateId.hashCode;
String toString() => name;
}
/// A full isolate on the remote VM.
class VMIsolate extends VMIsolateRef {
/// The current pause on exception mode for this isolate.
final VMExceptionPauseMode exceptionPauseMode;
/// The time that the isolate started running.
final DateTime startTime;
/// The number of live ports on this isolate.
final int livePorts;
/// Whether this isolate will pause before it exits.
final bool pauseOnExit;
/// The last pause event delivered to this isolate.
///
/// If the isolate is running, this will be a [VMResumeEvent].
///
/// As of VM service version 3.4, this will be a [VMNoneEvent] before the
/// isolate is runnable.
final VMPauseEvent pauseEvent;
/// Whether this isolate is paused.
bool get isPaused =>
pauseEvent is! VMNoneEvent && pauseEvent is! VMResumeEvent;
/// The error that's causing the isolate to exit or `null`.
final VMError error;
/// All breakpoints currently registered for this isolate.
final List<VMBreakpoint> breakpoints;
/// The VM service extension RPCs that are currently registered for this
/// isolate.
///
/// This is supported as of VM service version 3.1, or Dart SDK version 1.14.
final List<String> extensionRpcs;
VMIsolate._(Scope scope, Map json)
: exceptionPauseMode =
new VMExceptionPauseMode._(json["exceptionPauseMode"]),
startTime = new DateTime.fromMillisecondsSinceEpoch(
// Prior to v3.0, this was emitted as a double rather than an int.
json["startTime"].round()),
livePorts = json["livePorts"],
pauseOnExit = json["pauseOnExit"],
pauseEvent = newVMPauseEvent(scope, json["pauseEvent"]),
error = newVMError(scope, json["error"]),
breakpoints = new List.unmodifiable(json["breakpoints"]
.map((breakpoint) => newVMBreakpoint(scope, breakpoint))),
extensionRpcs = new UnmodifiableListView(
List.castFrom(json["extensionRPCs"] ?? [])),
super._(scope, json);
}
/// A full isolate on the remote VM that's ready to run code.
///
/// The VM service exposes isolates very early, before their contents are
/// fully-loaded. These in-progress isolates, represented by plain [VMIsolate]
/// instances, have limited amounts of metadata available. Only once they're
/// runnable is the full suite of metadata available.
///
/// A [VMRunnableIsolate] can always be retrieved using
/// [VMIsolateRef.loadRunnable]. In addition, one will be returned by
/// [VMIsolate.load] if the remote isolate is runnable.
class VMRunnableIsolate extends VMIsolate {
/// The root library for this isolate.
final VMLibraryRef rootLibrary;
/// All the libraries (transitively) loaded in this isolate, indexed by their
/// canonical URIs.
final Map<Uri, VMLibraryRef> libraries;
VMRunnableIsolate._(Scope scope, Map json)
: rootLibrary = newVMLibraryRef(scope, json["rootLib"]),
libraries = new UnmodifiableMapView(new Map.fromIterable(
json["libraries"],
key: (library) => Uri.parse(library["uri"]),
value: (library) => newVMLibraryRef(scope, library))),
super._(scope, json);
Future<VMRunnableIsolate> loadRunnable() => load();
Future<VMRunnableIsolate> load() =>
super.load().then((v) => v as VMRunnableIsolate);
String toString() => "Isolate running $rootLibrary";
}
/// An enum of ways to resume an isolate's execution using
/// [VMIsolateRef.resume].
class VMStep {
/// The isolate resumes regular execution.
static const resume = const VMStep._("Resume");
/// The isolate takes a single step into a function call.
static const into = const VMStep._("Into");
/// The isolate takes a single step, skipping over function calls.
static const over = const VMStep._("Over");
/// The isolate continues until the execution returns from the an await, yield,
/// or yield* statement.
///
/// Note that this value is only valid from VM service version 3.3 and when
/// [VMPauseEvent.atAsyncSuspension] is true.
static const overAsyncSuspension = const VMStep._("OverAsyncSuspension");
/// The isolate continues until it exits the current function.
static const out = const VMStep._("Out");
/// The string name of the step type.
final String _value;
const VMStep._(this._value);
String toString() => _value;
}
/// An enum of exception pause behaviour for use in [VMIsolateRef.setExceptionPauseMode].
class VMExceptionPauseMode {
/// The isolate will not pause on any exceptions.
static const none = const VMExceptionPauseMode._("None");
/// The isolate will pause on any unhandled exceptions.
static const unhandled = const VMExceptionPauseMode._("Unhandled");
/// The isolate will pause on all exceptions.
static const all = const VMExceptionPauseMode._("All");
/// The string name of the exception pause mode.
final String _value;
const VMExceptionPauseMode._(this._value);
String toString() => _value;
}
/// An event posted via `postEvent` from the `dart:developer` package.
class VMExtensionEvent {
/// The kind, which identifies the type of event and its source.
final String kind;
/// The event's payload.
final Map data;
VMExtensionEvent._(Map json)
: kind = json['extensionKind'],
data = json['extensionData'];
}