| Index: runtime/observatory/lib/src/elements/debugger.dart
|
| diff --git a/runtime/observatory/lib/src/elements/debugger.dart b/runtime/observatory/lib/src/elements/debugger.dart
|
| index f17bacb4a7a12f5d7e89b93e9439745c4848004b..9c18f941245e7b0657c7354a6ae4eea5ce9ff922 100644
|
| --- a/runtime/observatory/lib/src/elements/debugger.dart
|
| +++ b/runtime/observatory/lib/src/elements/debugger.dart
|
| @@ -4,15 +4,431 @@
|
|
|
| library debugger_page_element;
|
|
|
| +import 'dart:async';
|
| import 'dart:html';
|
| import 'observatory_element.dart';
|
| +import 'package:observatory/cli.dart';
|
| import 'package:observatory/service.dart';
|
| import 'package:polymer/polymer.dart';
|
|
|
| +// TODO(turnidge): Move Debugger, DebuggerCommand to their own lib.
|
| +abstract class DebuggerCommand extends Command {
|
| + Debugger debugger;
|
| +
|
| + DebuggerCommand(this.debugger, name, children)
|
| + : super(name, children);
|
| +}
|
| +
|
| +class HelpCommand extends DebuggerCommand {
|
| + HelpCommand(Debugger debugger) : super(debugger, 'help', []);
|
| +
|
| + Future run(List<String> args) {
|
| + var con = debugger.console;
|
| + con.printLine('List of commands:');
|
| + con.newline();
|
| +
|
| + // TODO(turnidge): Build a real help system.
|
| + List completions = debugger.cmd.completeCommand('');
|
| + completions = completions.map((s )=> s.trimRight()).toList();
|
| + completions.sort();
|
| + con.printLine(completions.toString());
|
| + con.newline();
|
| + con.printLine("Command prefixes are accepted (e.g. 'h' for 'help')");
|
| + con.printLine("Hit [TAB] to complete a command (try 'i[TAB][TAB]')");
|
| + con.printLine("Hit [ENTER] to repeat the last command");
|
| +
|
| + return new Future.value(null);
|
| + }
|
| +}
|
| +
|
| +class PauseCommand extends DebuggerCommand {
|
| + PauseCommand(Debugger debugger) : super(debugger, 'pause', []);
|
| +
|
| + Future run(List<String> args) {
|
| + if (!debugger.isolatePaused()) {
|
| + return debugger.isolate.pause();
|
| + } else {
|
| + debugger.console.printLine('The program is already paused');
|
| + return new Future.value(null);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class ContinueCommand extends DebuggerCommand {
|
| + ContinueCommand(Debugger debugger) : super(debugger, 'continue', []);
|
| +
|
| + Future run(List<String> args) {
|
| + if (debugger.isolatePaused()) {
|
| + return debugger.isolate.resume().then((_) {
|
| + debugger.warnOutOfDate();
|
| + });
|
| + } else {
|
| + debugger.console.printLine('The program must be paused');
|
| + return new Future.value(null);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class NextCommand extends DebuggerCommand {
|
| + NextCommand(Debugger debugger) : super(debugger, 'next', []);
|
| +
|
| + Future run(List<String> args) {
|
| + if (debugger.isolatePaused()) {
|
| + var event = debugger.isolate.pauseEvent;
|
| + if (event.eventType == 'IsolateCreated') {
|
| + debugger.console.printLine("Type 'continue' to start the isolate");
|
| + return new Future.value(null);
|
| + }
|
| + if (event.eventType == 'IsolateShutdown') {
|
| + debugger.console.printLine("Type 'continue' to exit the isolate");
|
| + return new Future.value(null);
|
| + }
|
| + return debugger.isolate.stepOver();
|
| + } else {
|
| + debugger.console.printLine('The program is already running');
|
| + return new Future.value(null);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class StepCommand extends DebuggerCommand {
|
| + StepCommand(Debugger debugger) : super(debugger, 'step', []);
|
| +
|
| + Future run(List<String> args) {
|
| + if (debugger.isolatePaused()) {
|
| + var event = debugger.isolate.pauseEvent;
|
| + if (event.eventType == 'IsolateCreated') {
|
| + debugger.console.printLine("Type 'continue' to start the isolate");
|
| + return new Future.value(null);
|
| + }
|
| + if (event.eventType == 'IsolateShutdown') {
|
| + debugger.console.printLine("Type 'continue' to exit the isolate");
|
| + return new Future.value(null);
|
| + }
|
| + return debugger.isolate.stepInto();
|
| + } else {
|
| + debugger.console.printLine('The program is already running');
|
| + return new Future.value(null);
|
| + }
|
| + }
|
| +}
|
| +
|
| +class FinishCommand extends DebuggerCommand {
|
| + FinishCommand(Debugger debugger) : super(debugger, 'finish', []);
|
| +
|
| + Future run(List<String> args) {
|
| + if (debugger.isolatePaused()) {
|
| + return debugger.isolate.stepOut();
|
| + } else {
|
| + debugger.console.printLine('The program is already running');
|
| + return new Future.value(null);
|
| + }
|
| + }
|
| +}
|
| +
|
| +// TODO(turnidge): Add argument completion.
|
| +class DeleteCommand extends DebuggerCommand {
|
| + DeleteCommand(Debugger debugger) : super(debugger, 'delete', []);
|
| +
|
| + Future run(List<String> args) {
|
| + if (args.length < 1) {
|
| + debugger.console.printLine('delete expects one or more arguments');
|
| + return new Future.value(null);
|
| + }
|
| + List toDelete = [];
|
| + for (var arg in args) {
|
| + int id = int.parse(arg);
|
| + var bpt = null;
|
| + for (var candidate in debugger.isolate.breakpoints) {
|
| + if (candidate['breakpointNumber'] == id) {
|
| + bpt = candidate;
|
| + break;
|
| + }
|
| + }
|
| + if (bpt == null) {
|
| + debugger.console.printLine("Invalid breakpoint id '${id}'");
|
| + return new Future.value(null);
|
| + }
|
| + toDelete.add(bpt);
|
| + }
|
| + List pending = [];
|
| + for (var bpt in toDelete) {
|
| + pending.add(debugger.isolate.removeBreakpoint(bpt).then((_) {
|
| + var id = bpt['breakpointNumber'];
|
| + debugger.console.printLine("Removed breakpoint $id");
|
| + }));
|
| + }
|
| + return Future.wait(pending);
|
| + }
|
| +}
|
| +
|
| +class InfoBreakpointsCommand extends DebuggerCommand {
|
| + InfoBreakpointsCommand(Debugger debugger)
|
| + : super(debugger, 'breakpoints', []);
|
| +
|
| + Future run(List<String> args) {
|
| + return debugger.isolate.reloadBreakpoints().then((_) {
|
| + if (debugger.isolate.breakpoints.isEmpty) {
|
| + debugger.console.printLine('No breakpoints');
|
| + }
|
| + for (var bpt in debugger.isolate.breakpoints) {
|
| + var bpId = bpt['breakpointNumber'];
|
| + var script = bpt['location']['script'];
|
| + var tokenPos = bpt['location']['tokenPos'];
|
| + var line = script.tokenToLine(tokenPos);
|
| + var col = script.tokenToCol(tokenPos);
|
| + debugger.console.printLine(
|
| + 'Breakpoint ${bpId} at ${script.name}:${line}:${col}');
|
| + }
|
| + });
|
| + }
|
| +}
|
| +
|
| +class InfoIsolatesCommand extends DebuggerCommand {
|
| + InfoIsolatesCommand(Debugger debugger) : super(debugger, 'isolates', []);
|
| +
|
| + Future run(List<String> args) {
|
| + for (var isolate in debugger.isolate.vm.isolates) {
|
| + debugger.console.printLine(
|
| + "Isolate ${isolate.id} '${isolate.name}'");
|
| + }
|
| + return new Future.value(null);
|
| + }
|
| +}
|
| +
|
| +class InfoCommand extends DebuggerCommand {
|
| + InfoCommand(Debugger debugger) : super(debugger, 'info', [
|
| + new InfoBreakpointsCommand(debugger),
|
| + new InfoIsolatesCommand(debugger),
|
| + ]);
|
| +
|
| + Future run(List<String> args) {
|
| + debugger.console.printLine("Invalid info command");
|
| + return new Future.value(null);
|
| + }
|
| +}
|
| +
|
| +class RefreshCoverageCommand extends DebuggerCommand {
|
| + RefreshCoverageCommand(Debugger debugger) : super(debugger, 'coverage', []);
|
| +
|
| + Future run(List<String> args) {
|
| + Set<Script> scripts = debugger.stackElement.activeScripts();
|
| + List pending = [];
|
| + for (var script in scripts) {
|
| + pending.add(script.refreshCoverage().then((_) {
|
| + debugger.console.printLine('Refreshed coverage for ${script.name}');
|
| + }));
|
| + }
|
| + return Future.wait(pending);
|
| + }
|
| +}
|
| +
|
| +class RefreshCommand extends DebuggerCommand {
|
| + RefreshCommand(Debugger debugger) : super(debugger, 'refresh', [
|
| + new RefreshCoverageCommand(debugger),
|
| + ]);
|
| +
|
| + Future run(List<String> args) {
|
| + return debugger.refreshStack();
|
| + }
|
| +}
|
| +
|
| +// Tracks the state for an isolate debugging session.
|
| +class Debugger {
|
| + RootCommand cmd;
|
| + DebuggerConsoleElement console;
|
| + DebuggerStackElement stackElement;
|
| + ServiceMap stack;
|
| +
|
| + Debugger() {
|
| + cmd = new RootCommand([
|
| + new HelpCommand(this),
|
| + new PauseCommand(this),
|
| + new ContinueCommand(this),
|
| + new NextCommand(this),
|
| + new StepCommand(this),
|
| + new FinishCommand(this),
|
| + new DeleteCommand(this),
|
| + new InfoCommand(this),
|
| + new RefreshCommand(this),
|
| + ]);
|
| + }
|
| +
|
| + void set isolate(Isolate iso) {
|
| + _isolate = iso;
|
| + if (_isolate != null) {
|
| + _isolate.reload().then((_) {
|
| + _isolate.vm.events.stream.listen(_onEvent);
|
| + _refreshStack(isolate.pauseEvent).then((_) {
|
| + reportStatus();
|
| + });
|
| + });
|
| + }
|
| + }
|
| + Isolate get isolate => _isolate;
|
| + Isolate _isolate;
|
| +
|
| + void init() {
|
| + console.newline();
|
| + console.printBold("Type 'h' for help");
|
| + }
|
| +
|
| + Future refreshStack() {
|
| + return _refreshStack(isolate.pauseEvent).then((_) {
|
| + reportStatus();
|
| + });
|
| + }
|
| +
|
| + bool isolatePaused() {
|
| + // TODO(turnidge): Stop relying on the isolate to track the last
|
| + // pause event. Since we listen to events directly in the
|
| + // debugger, this could introduce a race.
|
| + return isolate.pauseEvent != null;
|
| + }
|
| +
|
| + void warnOutOfDate() {
|
| + // Wait a bit, then tell the user that the stack may be out of date.
|
| + new Timer(const Duration(seconds:2), () {
|
| + if (!isolatePaused()) {
|
| + stackElement.isSampled = true;
|
| + }
|
| + });
|
| + }
|
| +
|
| + Future<ServiceMap> _refreshStack(ServiceEvent pauseEvent) {
|
| + return isolate.getStack().then((result) {
|
| + stack = result;
|
| + // TODO(turnidge): Replace only the changed part of the stack to
|
| + // reduce flicker.
|
| + // stackElement.stack = stack;
|
| + stackElement.updateStack(stack, pauseEvent);
|
| + });
|
| + }
|
| +
|
| + void reportStatus() {
|
| + if (_isolate.idle) {
|
| + console.printLine('Isolate is idle');
|
| + } else if (_isolate.running) {
|
| + console.printLine("Isolate is running (type 'pause' to interrupt)");
|
| + } else if (_isolate.pauseEvent != null) {
|
| + _reportPause(_isolate.pauseEvent);
|
| + } else {
|
| + console.printLine('Isolate is in unknown state');
|
| + }
|
| + }
|
| +
|
| + void _reportPause(ServiceEvent event) {
|
| + if (event.eventType == 'IsolateCreated') {
|
| + console.printLine(
|
| + "Paused at isolate start (type 'continue' to start the isolate')");
|
| + } else if (event.eventType == 'IsolateShutdown') {
|
| + console.printLine(
|
| + "Paused at isolate exit (type 'continue' to exit the isolate')");
|
| + }
|
| + if (stack['frames'].length > 0) {
|
| + var frame = stack['frames'][0];
|
| + var script = frame['script'];
|
| + script.load().then((_) {
|
| + var line = script.tokenToLine(frame['tokenPos']);
|
| + var col = script.tokenToCol(frame['tokenPos']);
|
| + if (event.breakpoint != null) {
|
| + var bpId = event.breakpoint['breakpointNumber'];
|
| + console.printLine('Breakpoint ${bpId} at ${script.name}:${line}:${col}');
|
| + } else if (event.exception != null) {
|
| + // TODO(turnidge): Test this.
|
| + console.printLine(
|
| + 'Exception ${event.exception} at ${script.name}:${line}:${col}');
|
| + } else {
|
| + console.printLine('Paused at ${script.name}:${line}:${col}');
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + void _onEvent(ServiceEvent event) {
|
| + if (event.owner != isolate) {
|
| + return;
|
| + }
|
| + switch(event.eventType) {
|
| + case 'IsolateShutdown':
|
| + console.printLine('Isolate shutdown');
|
| + isolate = null;
|
| + break;
|
| +
|
| + case 'BreakpointReached':
|
| + case 'IsolateInterrupted':
|
| + case 'ExceptionThrown':
|
| + _refreshStack(event).then((_) {
|
| + _reportPause(event);
|
| + });
|
| + break;
|
| +
|
| + case 'IsolateResumed':
|
| + console.printLine('Continuing...');
|
| + break;
|
| +
|
| + case '_Graph':
|
| + case 'BreakpointResolved':
|
| + case 'IsolateCreated':
|
| + case 'GC':
|
| + // Ignore these events for now.
|
| + break;
|
| +
|
| + default:
|
| + console.printLine('Unrecognized event: $event');
|
| + break;
|
| + }
|
| + }
|
| +
|
| + String complete(String line) {
|
| + List<String> completions = cmd.completeCommand(line);
|
| + if (completions.length == 0) {
|
| + // No completions. Leave the line alone.
|
| + return line;
|
| + } else if (completions.length == 1) {
|
| + // Unambiguous completion.
|
| + return completions[0];
|
| + } else {
|
| + // Ambigous completion.
|
| + completions = completions.map((s )=> s.trimRight()).toList();
|
| + completions.sort();
|
| + console.printBold(completions.toString());
|
| +
|
| + // TODO(turnidge): Complete to common prefix of all completions.
|
| + return line;
|
| + }
|
| + }
|
| +
|
| + // TODO(turnidge): Implement real command line history.
|
| + String lastCommand;
|
| + bool busy = false;
|
| +
|
| + Future run(String command) {
|
| + assert(!busy);
|
| + busy = true;
|
| + if (command == '') {
|
| + command = lastCommand;
|
| + }
|
| + lastCommand = command;
|
| + console.printBold('\$ $command');
|
| + return cmd.runCommand(command).then((_) {
|
| + busy = false;
|
| + }).catchError((e) {
|
| + console.printLine('ERROR $e');
|
| + });
|
| + }
|
| +}
|
| +
|
| @CustomTag('debugger-page')
|
| class DebuggerPageElement extends ObservatoryElement {
|
| @published Isolate isolate;
|
| - @published bool showConsole = false;
|
| +
|
| + isolateChanged(oldValue) {
|
| + if (isolate != null) {
|
| + debugger.isolate = isolate;
|
| + }
|
| + }
|
| + Debugger debugger = new Debugger();
|
|
|
| DebuggerPageElement.created() : super.created();
|
|
|
| @@ -20,34 +436,127 @@ class DebuggerPageElement extends ObservatoryElement {
|
| void attached() {
|
| super.attached();
|
|
|
| - // TODO(turnidge): Get these values from the DOM.
|
| - // TODO(turnidge): splitterHeight is 0 until I implement it.
|
| - const int navbarHeight = 56;
|
| - const int splitterHeight = 0;
|
| - const int cmdHeight = 22;
|
| + var navbarDiv = $['navbarDiv'];
|
| + var stackDiv = $['stackDiv'];
|
| + var splitterDiv = $['splitterDiv'];
|
| + var cmdDiv = $['commandDiv'];
|
| + var consoleDiv = $['consoleDiv'];
|
| +
|
| + int navbarHeight = navbarDiv.clientHeight;
|
| + int splitterHeight = splitterDiv.clientHeight;
|
| + int cmdHeight = cmdDiv.clientHeight;
|
|
|
| - var stack = $['stack'];
|
| int windowHeight = window.innerHeight;
|
| - int available = windowHeight - (navbarHeight + splitterHeight);
|
| - int stackHeight = available ~/ 1.3;
|
| - if (showConsole) {
|
| - stack.style.setProperty('height', '${stackHeight}px');
|
| - } else {
|
| - stack.style.setProperty('height', '${available}px');
|
| - }
|
| + int fixedHeight = navbarHeight + splitterHeight + cmdHeight;
|
| + int available = windowHeight - fixedHeight;
|
| + int stackHeight = available ~/ 1.6;
|
| + stackDiv.style.setProperty('height', '${stackHeight}px');
|
| +
|
| + // Wire the debugger object to the stack, console, and command line.
|
| + var stackElement = $['stackElement'];
|
| + debugger.stackElement = stackElement;
|
| + stackElement.debugger = debugger;
|
| + debugger.console = $['console'];
|
| + $['commandline'].debugger = debugger;
|
| + debugger.init();
|
| }
|
| +
|
| }
|
|
|
| @CustomTag('debugger-stack')
|
| class DebuggerStackElement extends ObservatoryElement {
|
| @published Isolate isolate;
|
| - @published ServiceMap stack;
|
| - @published int activeFrame = 0;
|
| + @observable bool hasStack = false;
|
| + @observable bool isSampled = false;
|
| + Debugger debugger = null;
|
|
|
| - isolateChanged(oldValue) {
|
| - isolate.getStack().then((result) {
|
| - stack = result;
|
| - });
|
| + _addFrame(List frameList, ObservableMap frameInfo, bool expand) {
|
| + DebuggerFrameElement frameElement = new Element.tag('debugger-frame');
|
| + frameElement.expand = expand;
|
| + frameElement.frame = frameInfo;
|
| +
|
| + var li = new LIElement();
|
| + li.classes.add('list-group-item');
|
| + li.children.insert(0, frameElement);
|
| +
|
| + frameList.insert(0, li);
|
| + }
|
| +
|
| + void updateStack(ServiceMap newStack, ServiceEvent pauseEvent) {
|
| + List frameElements = $['frameList'].children;
|
| + List newFrames = newStack['frames'];
|
| +
|
| + // Remove any frames whose functions don't match, starting from
|
| + // bottom of stack.
|
| + int oldPos = frameElements.length - 1;
|
| + int newPos = newFrames.length - 1;
|
| + while (oldPos >= 0 && newPos >= 0) {
|
| + if (!frameElements[oldPos].children[0].matchFrame(newFrames[newPos])) {
|
| + // The rest of the frame elements no longer match. Remove them.
|
| + for (int i = 0; i <= oldPos; i++) {
|
| + // NOTE(turnidge): removeRange is missing, sadly.
|
| + frameElements.removeAt(0);
|
| + }
|
| + break;
|
| + }
|
| + oldPos--;
|
| + newPos--;
|
| + }
|
| +
|
| + // Remove any extra frames.
|
| + if (frameElements.length > newFrames.length) {
|
| + // Remove old frames from the top of stack.
|
| + int removeCount = frameElements.length - newFrames.length;
|
| + for (int i = 0; i < removeCount; i++) {
|
| + frameElements.removeAt(0);
|
| + }
|
| + }
|
| +
|
| + // Add any new frames.
|
| + int newCount = 0;
|
| + if (frameElements.length < newFrames.length) {
|
| + // Add new frames to the top of stack.
|
| + newCount = newFrames.length - frameElements.length;
|
| + for (int i = newCount-1; i >= 0; i--) {
|
| + _addFrame(frameElements, newFrames[i], i == 0);
|
| + }
|
| + }
|
| + assert(frameElements.length == newFrames.length);
|
| +
|
| + if (frameElements.isNotEmpty) {
|
| + frameElements[0].children[0].expand = true;
|
| + for (int i = newCount; i < frameElements.length; i++) {
|
| + frameElements[i].children[0].updateFrame(newFrames[i]);
|
| + }
|
| + }
|
| +
|
| + isSampled = pauseEvent == null;
|
| + hasStack = frameElements.isNotEmpty;
|
| + }
|
| +
|
| + Set<Script> activeScripts() {
|
| + var s = new Set<Script>();
|
| + List frameElements = $['frameList'].children;
|
| + for (var frameElement in frameElements) {
|
| + s.add(frameElement.children[0].script);
|
| + }
|
| + return s;
|
| + }
|
| +
|
| + doPauseIsolate(_) {
|
| + if (debugger != null) {
|
| + return debugger.isolate.pause();
|
| + } else {
|
| + return new Future.value(null);
|
| + }
|
| + }
|
| +
|
| + doRefreshStack(_) {
|
| + if (debugger != null) {
|
| + return debugger.refreshStack();
|
| + } else {
|
| + return new Future.value(null);
|
| + }
|
| }
|
|
|
| DebuggerStackElement.created() : super.created();
|
| @@ -66,6 +575,19 @@ class DebuggerFrameElement extends ObservatoryElement {
|
|
|
| DebuggerFrameElement.created() : super.created();
|
|
|
| + bool matchFrame(ObservableMap newFrame) {
|
| + return newFrame['function'].id == frame['function'].id;
|
| + }
|
| +
|
| + void updateFrame(ObservableMap newFrame) {
|
| + assert(matchFrame(newFrame));
|
| + frame['depth'] = newFrame['depth'];
|
| + frame['tokenPos'] = newFrame['tokenPos'];
|
| + frame['vars'] = newFrame['vars'];
|
| + }
|
| +
|
| + Script get script => frame['script'];
|
| +
|
| @override
|
| void attached() {
|
| super.attached();
|
| @@ -102,12 +624,35 @@ class DebuggerConsoleElement extends ObservatoryElement {
|
| @published Isolate isolate;
|
|
|
| DebuggerConsoleElement.created() : super.created();
|
| +
|
| + void printLine(String line) {
|
| + var div = new DivElement();
|
| + div.classes.add('normal');
|
| + div.appendText(line);
|
| + $['consoleText'].children.add(div);
|
| + div.scrollIntoView();
|
| + }
|
| +
|
| + void printBold(String line) {
|
| + var div = new DivElement();
|
| + div.classes.add('bold');
|
| + div.appendText(line);
|
| + $['consoleText'].children.add(div);
|
| + div.scrollIntoView();
|
| + }
|
| +
|
| + void newline() {
|
| + var br = new BRElement();
|
| + $['consoleText'].children.add(br);
|
| + br.scrollIntoView();
|
| + }
|
| }
|
|
|
| @CustomTag('debugger-input')
|
| class DebuggerInputElement extends ObservatoryElement {
|
| @published Isolate isolate;
|
| @published String text = '';
|
| + @observable Debugger debugger;
|
|
|
| @override
|
| void ready() {
|
| @@ -118,13 +663,17 @@ class DebuggerInputElement extends ObservatoryElement {
|
| switch (e.keyCode) {
|
| case KeyCode.TAB:
|
| e.preventDefault();
|
| - textBox.setRangeText('TAB');
|
| - textBox.setSelectionRange(textBox.selectionStart + 3,
|
| - textBox.selectionStart + 3);
|
| + int cursorPos = textBox.selectionStart;
|
| + var completion = debugger.complete(text.substring(0, cursorPos));
|
| + text = completion + text.substring(cursorPos);
|
| + // TODO(turnidge): Move the cursor to the end of the
|
| + // completion, rather than the end of the string.
|
| break;
|
| case KeyCode.ENTER:
|
| - print('Debugger command (not implemented): $text');
|
| - text = '';
|
| + if (!debugger.busy) {
|
| + debugger.run(text);
|
| + text = '';
|
| + }
|
| break;
|
| }
|
| });
|
|
|