/* * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.oracle.truffle.tools.debug.shell.client; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeSet; import jline.console.ConsoleReader; import com.oracle.truffle.api.source.Source; import com.oracle.truffle.tools.debug.shell.server.REPLServer; import java.io.File; /** * A very simple line-oriented, language-agnostic debugging client shell: the first step toward a * general, extensible debugging framework designed to be adapted for remote debugging. * <p> * The architecture of this debugging framework is modeled loosely on * <a href="https://github.com/clojure/tools.nrepl">nREPL</a>, a network REPL developed by the * Clojure community with a focus on generality: * <ul> * <li>Client and (possibly remote) server communicate via <em>messages</em> carried over some * <em>transport</em>;</li> * <li>A message is a <em>map</em> of key/value pairs;</li> * <li>Keys and values are <em>strings</em>;</li> * <li>The client sends messages as <em>requests</em> to a server;</li> * <li>A server dispatches each incoming request to an appropriate <em>handler</em> that takes * appropriate action and responds to the client with one or more messages; and</li> * <li>Many implementations of the <em>transport</em> are possible.</li> * </ul> * <p> * <strong>Compromises:</strong> * <p> * In order to get * <ol> * <li>The current startup sequence is based on method calls, not messages;</li> * <li>Only a very few request types and keys are implemented, omitting for example request and * session ids;</li> * <li>Message passing is synchronous and "transported" via method calls;</li> * <li>Asynchrony is emulated by having each call to the server pass only a message, and by having * the server return only a list of messages.</li> * </ol> * * @see REPLServer * @see com.oracle.truffle.tools.debug.shell.com.oracle.truffle.tools.debug.shell.REPLMessage */ @SuppressWarnings("deprecation") @Deprecated public class SimpleREPLClient implements com.oracle.truffle.tools.debug.shell.REPLClient { private static final String REPLY_PREFIX = "==> "; private static final String FAIL_PREFIX = "**> "; private static final String WARNING_PREFIX = "!!> "; private static final String TRACE_PREFIX = ">>> "; private static final String[] NULL_ARGS = new String[0]; static final String INFO_LINE_FORMAT = " %s\n"; static final String CODE_LINE_FORMAT = " %3d %s\n"; static final String CODE_LINE_BREAK_FORMAT = "--> %3d %s\n"; private static final String STACK_FRAME_FORMAT = " %3d: at %s in %s %s\n"; private static final String STACK_FRAME_SELECTED_FORMAT = "==> %3d: at %s in %s %s\n"; public static void main(String[] args) { final SimpleREPLClient client = new SimpleREPLClient(); final REPLServer replServer = new REPLServer(client); replServer.start(); client.start(replServer); } // Top level commands private final Map<String, REPLCommand> commandMap = new HashMap<>(); private final Collection<String> commandNames = new TreeSet<>(); // Local options private final Map<String, LocalOption> localOptions = new HashMap<>(); private final Collection<String> optionNames = new TreeSet<>(); // Current local context ClientContextImpl clientContext; private final ConsoleReader reader; private final PrintStream writer; private REPLServer replServer; private final LocalOption astDepthOption = new IntegerOption(9, "astdepth", "default depth for AST display"); private final LocalOption autoWhereOption = new BooleanOption(true, "autowhere", "run the \"where\" command after each navigation"); private final LocalOption autoNodeOption = new BooleanOption(false, "autonode", "run the \"truffle node\" command after each navigation"); private final LocalOption autoSubtreeOption = new BooleanOption(false, "autosubtree", "run the \"truffle subtree\" command after each navigation"); private final LocalOption autoASTOption = new BooleanOption(false, "autoast", "run the \"truffle ast\" command after each navigation"); private final LocalOption listSizeOption = new IntegerOption(25, "listsize", "default number of lines to list"); private final LocalOption traceMessagesOption = new BooleanOption(false, "tracemessages", "trace REPL messages between client and server"); private final LocalOption verboseBreakpointInfoOption = new BooleanOption(true, "verbosebreakpointinfo", "\"info breakpoint\" displays more info"); private void addOption(LocalOption localOption) { final String optionName = localOption.getName(); localOptions.put(optionName, localOption); optionNames.add(optionName); } /** * Non-null when the user has named a file other than where halted, providing context for * commands such as "break"; if no explicit selection, then defaults to where halted. This is * session state, so it persists across halting contexts. */ private Source selectedSource = null; private boolean quitting; // User has requested to "Quit" public SimpleREPLClient() { this.writer = System.out; try { this.reader = new ConsoleReader(); } catch (IOException e) { throw new RuntimeException("Unable to create console " + e); } addCommand(backtraceCommand); addCommand(REPLRemoteCommand.BREAK_AT_LINE_CMD); addCommand(REPLRemoteCommand.BREAK_AT_LINE_ONCE_CMD); addCommand(REPLRemoteCommand.CALL_CMD); addCommand(REPLRemoteCommand.CALL_STEP_INTO_CMD); addCommand(REPLRemoteCommand.CLEAR_BREAK_CMD); addCommand(REPLRemoteCommand.CONDITION_BREAK_CMD); addCommand(REPLRemoteCommand.CONTINUE_CMD); addCommand(REPLRemoteCommand.DELETE_CMD); addCommand(REPLRemoteCommand.DISABLE_CMD); addCommand(REPLRemoteCommand.DOWN_CMD); addCommand(REPLRemoteCommand.ENABLE_CMD); addCommand(REPLRemoteCommand.EVAL_CMD); addCommand(REPLRemoteCommand.EVAL_STEP_INTO_CMD); addCommand(fileCommand); addCommand(REPLRemoteCommand.FRAME_CMD); addCommand(helpCommand); addCommand(infoCommand); addCommand(REPLRemoteCommand.KILL_CMD); addCommand(listCommand); addCommand(REPLRemoteCommand.LOAD_CMD); addCommand(REPLRemoteCommand.LOAD_STEP_INTO_CMD); addCommand(quitCommand); addCommand(setCommand); addCommand(REPLRemoteCommand.SET_LANG_CMD); addCommand(REPLRemoteCommand.STEP_INTO_CMD); addCommand(REPLRemoteCommand.STEP_OUT_CMD); addCommand(REPLRemoteCommand.STEP_OVER_CMD); addCommand(truffleCommand); addCommand(REPLRemoteCommand.UP_CMD); addCommand(whereCommand); infoCommand.addCommand(infoBreakCommand); infoCommand.addCommand(infoLanguageCommand); infoCommand.addCommand(infoSetCommand); truffleCommand.addCommand(truffleASTCommand); truffleCommand.addCommand(truffleNodeCommand); truffleCommand.addCommand(truffleSubtreeCommand); addOption(astDepthOption); addOption(autoASTOption); addOption(autoNodeOption); addOption(autoSubtreeOption); addOption(autoWhereOption); addOption(listSizeOption); addOption(traceMessagesOption); addOption(verboseBreakpointInfoOption); } public void start(REPLServer server) { this.replServer = server; clientContext = new ClientContextImpl(null, null); showWelcome(); try { final com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = replServer.receive(infoLanguageCommand.createRequest(clientContext, NULL_ARGS)); if (replies.length == 0) { clientContext.displayFailReply("No languages could be loaded"); } else if (replies.length == 1) { final String[] args = new String[]{"", replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.LANG_NAME)}; final com.oracle.truffle.tools.debug.shell.REPLMessage[] results = replServer.receive(REPLRemoteCommand.SET_LANG_CMD.createRequest(clientContext, args)); if (results[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { final String message = results[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG); clientContext.displayFailReply(message != null ? message : results[0].toString()); } else { clientContext.updatePrompt(); clientContext.startContextSession(); } } else { clientContext.displayInfo("Languages supported (type \"lang <name>\" to set default)"); displayLanguages(replies); clientContext.updatePrompt(); clientContext.startContextSession(); } } finally { clientContext.displayReply("Goodbye"); } } private void showWelcome() { final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage( com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.INFO); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.TOPIC, com.oracle.truffle.tools.debug.shell.REPLMessage.WELCOME_MESSAGE); final com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = clientContext.sendToServer(request); if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { clientContext.displayReply("Welcome"); } else { clientContext.displayReply(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.INFO_VALUE)); } } public void addCommand(REPLCommand replCommand) { final String commandName = replCommand.getCommand(); final String abbreviation = replCommand.getAbbreviation(); commandNames.add(commandName); commandMap.put(commandName, replCommand); if (abbreviation != null) { commandMap.put(abbreviation, replCommand); } } private class ClientContextImpl implements REPLClientContext { private final ClientContextImpl predecessor; private final int level; // Information about where the execution is halted /** The source where execution, if any, is halted; null if none. */ private Source haltedSource; /** The line number where execution, if any, is halted; 0 if none. */ private int haltedLineNumber = 0; /** The name of a source that we can't locate. */ private String unknownSourceName = "<unavailable>"; /** The stack where execution, if any, is halted; null if none. Evaluated lazily. */ private List<REPLFrame> frames = null; /** The frame number currently selected by user. */ private int selectedFrameNumber = 0; private String currentPrompt; // A execution context stacked on top of this one was explicitly // killed, and this context will receive the resulting exception. private boolean killPending; /** * Create a new context on the occasion of an execution halting. */ ClientContextImpl(ClientContextImpl predecessor, com.oracle.truffle.tools.debug.shell.REPLMessage message) { this.predecessor = predecessor; this.level = predecessor == null ? 0 : predecessor.level + 1; if (message != null) { final String sourceName = message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.SOURCE_NAME); try { this.haltedSource = Source.newBuilder(new File(sourceName)).build(); } catch (IOException ex) { final String code = message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.SOURCE_TEXT); if (code != null) { this.haltedSource = Source.newBuilder(code).name(sourceName).mimeType("content/unknown").build(); } } if (this.haltedSource != null) { selectedSource = haltedSource; try { haltedLineNumber = Integer.parseInt(message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.LINE_NUMBER)); } catch (NumberFormatException e) { haltedLineNumber = 0; } } else { this.haltedSource = null; this.haltedLineNumber = 0; this.unknownSourceName = sourceName; } } updatePrompt(); } private void selectSource(String fileName) { try { selectedSource = Source.newBuilder(new File(fileName)).build(); } catch (IOException e1) { selectedSource = null; } updatePrompt(); } @Override public void updatePrompt() { String languageName = "???"; final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.INFO); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.TOPIC, com.oracle.truffle.tools.debug.shell.REPLMessage.INFO_CURRENT_LANGUAGE); final com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = replServer.receive(request); if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.SUCCEEDED)) { languageName = replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.LANG_NAME); } final String showLang = languageName == null ? "() " : "( " + languageName + " )"; if (level == 0) { // 0-level context; no executions halted. if (selectedSource == null) { currentPrompt = showLang + " "; } else { currentPrompt = "(" + selectedSource.getName() + ") " + showLang + " "; } } else if (selectedSource != null && selectedSource != haltedSource) { // User is focusing somewhere else than the current locn; show no line number. final StringBuilder sb = new StringBuilder(); sb.append("(<" + Integer.toString(level) + "> "); sb.append(selectedSource.getName()); sb.append(")"); sb.append(showLang); sb.append(" "); currentPrompt = sb.toString(); } else { // Prompt reveals where currently halted. final StringBuilder sb = new StringBuilder(); sb.append("(<" + Integer.toString(level) + "> "); sb.append(haltedSource == null ? "??" : haltedSource.getName()); if (haltedLineNumber > 0) { sb.append(":" + Integer.toString(haltedLineNumber)); } sb.append(")"); sb.append(showLang); sb.append(" "); currentPrompt = sb.toString(); } } public Source source() { return haltedSource; } public int lineNumber() { return haltedLineNumber; } public List<REPLFrame> frames() { if (frames == null) { final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.BACKTRACE); final com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = sendToServer(request); if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { return null; } frames = new ArrayList<>(); for (com.oracle.truffle.tools.debug.shell.REPLMessage reply : replies) { final int index = reply.getIntValue(com.oracle.truffle.tools.debug.shell.REPLMessage.FRAME_NUMBER); final String locationFilePath = reply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.FILE_PATH); final Integer locationLineNumber = reply.getIntValue(com.oracle.truffle.tools.debug.shell.REPLMessage.LINE_NUMBER); final String locationDescription = reply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.SOURCE_LOCATION); final String name = reply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.METHOD_NAME); final String sourceLineText = reply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.SOURCE_LINE_TEXT); frames.add(new REPLFrameImpl(index, locationFilePath, locationLineNumber, locationDescription, name, sourceLineText)); } frames = Collections.unmodifiableList(frames); } return frames; } public int level() { return this.level; } public Source getSelectedSource() { return selectedSource == null ? haltedSource : selectedSource; } public int getSelectedFrameNumber() { return selectedFrameNumber; } public String stringQuery(String op) { assert op != null; com.oracle.truffle.tools.debug.shell.REPLMessage request = null; switch (op) { case com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE_AST: request = truffleASTCommand.createRequest(clientContext, NULL_ARGS); break; case com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE_SUBTREE: request = truffleSubtreeCommand.createRequest(clientContext, NULL_ARGS); break; default: request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, op); } if (request == null) { return null; } final com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = sendToServer(request); if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { return null; } return replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG); } public void selectFrameNumber(int frameNumber) { this.selectedFrameNumber = frameNumber; } void displayWhere() { if (level == 0) { displayFailReply("no active execution"); return; } Source whereSource = null; int whereLineNumber = 0; if (selectedFrameNumber == 0) { whereSource = haltedSource; whereLineNumber = haltedLineNumber; } else { final REPLFrame frame = frames().get(selectedFrameNumber); final String locationFileName = frame.locationFilePath(); if (locationFileName != null) { try { whereSource = Source.newBuilder(new File(locationFileName)).build(); } catch (IOException e) { } } whereLineNumber = frame.locationLineNumber(); } if (whereSource == null) { displayFailReply("Unavalable source=\"" + this.unknownSourceName + "\""); return; } final int listSize = listSizeOption.getInt(); final int fileLineCount = whereSource.getLineCount(); final String code = whereSource.getCode(); writer.println("Frame " + selectedFrameNumber + " in " + whereSource.getName()); final int halfListSize = listSize / 2; final int startLineNumber = Math.max(1, whereLineNumber - halfListSize); final int lastLineNumber = Math.min(startLineNumber + listSize - 1, fileLineCount); for (int line = startLineNumber; line <= lastLineNumber; line++) { final int offset = whereSource.getLineStartOffset(line); final String lineText = code.substring(offset, offset + whereSource.getLineLength(line)); if (line == whereLineNumber) { writer.format(CODE_LINE_BREAK_FORMAT, line, lineText); } else { writer.format(CODE_LINE_FORMAT, line, lineText); } } } public void displayStack() { final List<REPLFrame> frameList = frames(); if (frameList == null) { writer.println("<empty stack>"); } else { for (REPLFrame frame : frameList) { String sourceLineText = frame.sourceLineText(); if (sourceLineText == null) { sourceLineText = ""; } else { sourceLineText = "line=\"" + sourceLineText + "\""; } if (frame.index() == selectedFrameNumber) { writer.format(STACK_FRAME_SELECTED_FORMAT, frame.index(), frame.locationDescription(), frame.name(), sourceLineText); } else { writer.format(STACK_FRAME_FORMAT, frame.index(), frame.locationDescription(), frame.name(), sourceLineText); } } } } public void displayInfo(String message) { writer.format(INFO_LINE_FORMAT, message); } public void displayReply(String message) { writer.println(REPLY_PREFIX + message); } public void displayFailReply(String message) { // Suppress kill-induced failure, since kill is announced if (!message.equals("KillException")) { writer.println(FAIL_PREFIX + message); } } public void notifyKilled() { // A kill will not be received until after // This context is released, so the exception // must be handles specially by the // context that will catch it. predecessor.killPending = true; writer.println(clientContext.currentPrompt + " killed"); } public void displayWarnings(String warnings) { for (String warning : warnings.split("\\n")) { writer.println(WARNING_PREFIX + warning); } } public void traceMessage(String message) { writer.println(TRACE_PREFIX + message); } private void startContextSession() { while (true) { try { REPLCommand command = null; String[] args = NULL_ARGS; if (quitting) { if (level == 0) { return; } command = REPLRemoteCommand.KILL_CMD; } else { String line = reader.readLine(currentPrompt).trim(); if (line.startsWith("eval ")) { args = new String[]{"eval", line.substring(5)}; } else { args = line.split("[ \t]+"); } if (args.length == 0) { break; } final String cmd = args[0]; if (cmd.isEmpty()) { continue; } command = commandMap.get(cmd); while (command instanceof REPLIndirectCommand) { if (traceMessagesOption.getBool()) { traceMessage("Executing indirect: " + command.getCommand()); } command = ((REPLIndirectCommand) command).getCommand(args); } if (command == quitCommand) { if (level == 0) { return; } quitting = true; command = REPLRemoteCommand.KILL_CMD; } if (command == null) { clientContext.displayFailReply("Unrecognized command \"" + cmd + "\""); continue; } } if (command instanceof REPLLocalCommand) { if (traceMessagesOption.getBool()) { traceMessage("Executing local: " + command.getCommand()); } ((REPLLocalCommand) command).execute(args); } else if (command instanceof REPLRemoteCommand) { final REPLRemoteCommand remoteCommand = (REPLRemoteCommand) command; final com.oracle.truffle.tools.debug.shell.REPLMessage request = remoteCommand.createRequest(clientContext, args); if (request == null) { continue; } com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = sendToServer(request); remoteCommand.processReply(clientContext, replies); final String path = replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.FILE_PATH); if (path != null && !path.isEmpty()) { selectSource(path); } } else { assert false; // Should not happen. } } catch (ThreadDeath ex) { if (ex.getClass() != ThreadDeath.class && killPending) { // If the previous context was killed by REPL command, then // assume this exception is the result and the REPL should // continue debugging in this context. killPending = false; } else { // A legitimate use of the exception throw ex; } } catch (REPLContinueException ex) { break; } catch (IOException e) { e.printStackTrace(); } } } private com.oracle.truffle.tools.debug.shell.REPLMessage[] sendToServer(com.oracle.truffle.tools.debug.shell.REPLMessage request) { if (traceMessagesOption.getBool()) { clientContext.traceMessage("Sever request:"); request.print(writer, " "); } com.oracle.truffle.tools.debug.shell.REPLMessage[] replies = replServer.receive(request); assert replies != null && replies.length > 0; if (traceMessagesOption.getBool()) { if (replies.length > 1) { clientContext.traceMessage("Received " + replies.length + " server replies"); int replyCount = 0; for (com.oracle.truffle.tools.debug.shell.REPLMessage reply : replies) { clientContext.traceMessage("Server Reply " + replyCount++ + ":"); reply.print(writer, " "); } } else { clientContext.traceMessage("Received reply:"); replies[0].print(writer, " "); } } return replies; } private final class REPLFrameImpl implements REPLFrame { private final int index; private final String locationFilePath; private final Integer locationLineNumber; private final String locationDescription; private final String name; private final String sourceLineText; REPLFrameImpl(int index, String locationFilePath, Integer locationLineNumber, String locationDescription, String name, String sourceLineText) { this.index = index; this.locationFilePath = locationFilePath; this.locationLineNumber = locationLineNumber; this.locationDescription = locationDescription; this.name = name; this.sourceLineText = sourceLineText; } public int index() { return index; } public String locationFilePath() { return locationFilePath; } public Integer locationLineNumber() { return locationLineNumber; } public String locationDescription() { return locationDescription; } public String name() { return name; } public String sourceLineText() { return sourceLineText; } } } // Cheating with synchrony: asynchronous replies should arrive here, but don't. @Override public com.oracle.truffle.tools.debug.shell.REPLMessage receive(com.oracle.truffle.tools.debug.shell.REPLMessage request) { final String result = request.get("result"); clientContext.displayReply(result != null ? result : request.toString()); return null; } /** * Cheating with synchrony: take a direct call from the server that execution has halted and * we've entered a nested debugging context. */ public void halted(com.oracle.truffle.tools.debug.shell.REPLMessage message) { // Push a new context for where we've stopped. clientContext = new ClientContextImpl(clientContext, message); final String warnings = message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.WARNINGS); if (warnings != null) { clientContext.displayWarnings(warnings); } if (autoWhereOption.getBool()) { clientContext.displayWhere(); } if (autoNodeOption.getBool()) { final String result = clientContext.stringQuery(com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE_NODE); if (result != null) { displayTruffleNode(result); } } if (autoASTOption.getBool()) { final String result = clientContext.stringQuery(com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE_AST); if (result != null) { displayTruffleAST(result); } } if (autoSubtreeOption.getBool()) { final String result = clientContext.stringQuery(com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE_SUBTREE); if (result != null) { displayTruffleSubtree(result); } } try { clientContext.startContextSession(); } finally { // To continue execution, pop the context and return this.clientContext = clientContext.predecessor; } } /** * A command that can be executed without (direct) communication with the server; it may rely on * some other method that goes to the server for information. */ private abstract class REPLLocalCommand extends REPLCommand { REPLLocalCommand(String command, String abbreviation, String description) { super(command, abbreviation, description); } abstract void execute(String[] args); } /** * A command that redirects to other commands, based on arguments. */ private abstract class REPLIndirectCommand extends REPLCommand { REPLIndirectCommand(String command, String abbreviation, String description) { super(command, abbreviation, description); } abstract void addCommand(REPLCommand command); abstract REPLCommand getCommand(String[] args); } private final REPLCommand backtraceCommand = new REPLLocalCommand("backtrace", "bt", "Display current stack") { @Override void execute(String[] args) { if (clientContext.level == 0) { clientContext.displayFailReply("no active execution"); } else { clientContext.displayStack(); } } }; private final REPLCommand fileCommand = new REPLRemoteCommand("file", null, "Set/display current file for viewing") { final String[] help = {"file: display current file path", "file <filename>: Set file to be current file for viewing"}; @Override public String[] getHelp() { return help; } @Override public com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { if (args.length == 1) { final Source source = clientContext.getSelectedSource(); if (source == null) { clientContext.displayFailReply("no file currently selected"); } else { clientContext.displayReply(source.getPath()); } return null; } final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.FILE); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.SOURCE_NAME, args[1]); return request; } @Override void processReply(REPLClientContext context, com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { com.oracle.truffle.tools.debug.shell.REPLMessage firstReply = replies[0]; if (firstReply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { final String result = firstReply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG); clientContext.displayFailReply(result != null ? result : firstReply.toString()); return; } final String fileName = firstReply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.SOURCE_NAME); final String path = firstReply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.FILE_PATH); clientContext.selectSource(path == null ? fileName : path); clientContext.displayReply(clientContext.getSelectedSource().getPath()); for (int i = 1; i < replies.length; i++) { com.oracle.truffle.tools.debug.shell.REPLMessage reply = replies[i]; final String result = reply.get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG); clientContext.displayInfo(result != null ? result : reply.toString()); } } }; private final REPLCommand helpCommand = new REPLLocalCommand("help", null, "Describe commands") { final String[] help = {"help: list available commands", "help <command>: additional information about <command>"}; @Override public String[] getHelp() { return help; } @Override public void execute(String[] args) { if (args.length == 1) { clientContext.displayReply("Available commands:"); for (String commandName : commandNames) { final REPLCommand command = commandMap.get(commandName); if (command == null) { clientContext.displayInfo(commandName + ": Error, no implementation for command"); } else { final String abbrev = command.getAbbreviation(); if (abbrev == null) { clientContext.displayInfo(commandName + ": " + command.getDescription()); } else { clientContext.displayInfo(commandName + "(" + abbrev + "): " + command.getDescription()); } } } } else { final String cmdName = args[1]; final REPLCommand cmd = commandMap.get(cmdName); if (cmd == null) { clientContext.displayReply("command \"" + cmdName + "\" not recognized"); } else { final String[] helpLines = cmd.getHelp(); if (helpLines == null) { clientContext.displayReply("\"" + cmdName + "\":"); } else if (helpLines.length == 1) { clientContext.displayInfo(helpLines[0]); } else { clientContext.displayReply("\"" + cmdName + "\":"); for (String line : helpLines) { clientContext.displayInfo(line); } } } } } }; private final REPLIndirectCommand infoCommand = new REPLIndirectCommand(com.oracle.truffle.tools.debug.shell.REPLMessage.INFO, null, "Additional information on topics") { // "Info" commands private final Map<String, REPLCommand> infoCommandMap = new HashMap<>(); private final Collection<String> infoCommandNames = new TreeSet<>(); @Override public String[] getHelp() { final ArrayList<String> lines = new ArrayList<>(); for (String infoCommandName : infoCommandNames) { final REPLCommand cmd = infoCommandMap.get(infoCommandName); if (cmd == null) { lines.add("\"" + com.oracle.truffle.tools.debug.shell.REPLMessage.INFO + " " + infoCommandName + "\" not implemented"); } else { lines.add("\"" + com.oracle.truffle.tools.debug.shell.REPLMessage.INFO + " " + infoCommandName + "\": " + cmd.getDescription()); } } return lines.toArray(new String[0]); } @Override void addCommand(REPLCommand replCommand) { final String commandName = replCommand.getCommand(); final String abbreviation = replCommand.getAbbreviation(); infoCommandNames.add(commandName); infoCommandMap.put(commandName, replCommand); if (abbreviation != null) { infoCommandMap.put(abbreviation, replCommand); } } @Override REPLCommand getCommand(String[] args) { if (args.length == 1) { clientContext.displayFailReply("info topic not specified; try \"help info\""); return null; } final String topic = args[1]; REPLCommand command = infoCommandMap.get(topic); if (command == null) { clientContext.displayFailReply("topic \"" + topic + "\" not recognized"); return null; } return command; } }; private final REPLCommand infoBreakCommand = new REPLRemoteCommand("breakpoint", "break", "info about breakpoints") { @Override public com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_INFO); return request; } @Override void processReply(REPLClientContext context, com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { clientContext.displayFailReply(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG)); } else { Arrays.sort(replies, new Comparator<com.oracle.truffle.tools.debug.shell.REPLMessage>() { public int compare(com.oracle.truffle.tools.debug.shell.REPLMessage o1, com.oracle.truffle.tools.debug.shell.REPLMessage o2) { try { final int n1 = Integer.parseInt(o1.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_ID)); final int n2 = Integer.parseInt(o2.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_ID)); return Integer.compare(n1, n2); } catch (Exception ex) { } return 0; } }); clientContext.displayReply("Breakpoints set:"); for (com.oracle.truffle.tools.debug.shell.REPLMessage message : replies) { final StringBuilder sb = new StringBuilder(); sb.append(Integer.parseInt(message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_ID)) + ": "); sb.append("@" + message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.INFO_VALUE)); sb.append(" (state=" + message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_STATE)); if (verboseBreakpointInfoOption.getBool()) { sb.append(", hits=" + Integer.parseInt(message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_HIT_COUNT))); sb.append(", ignore=" + Integer.parseInt(message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_IGNORE_COUNT))); } final String condition = message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.BREAKPOINT_CONDITION); if (condition != null) { sb.append(", condition=\"" + condition + "\""); } sb.append(")"); clientContext.displayInfo(sb.toString()); } } } }; private final REPLRemoteCommand infoLanguageCommand = new REPLRemoteCommand("languages", "lang", "languages supported") { final String[] help = {"info language: list details about supported languages"}; @Override public String[] getHelp() { return help; } @Override public com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.INFO); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.TOPIC, com.oracle.truffle.tools.debug.shell.REPLMessage.INFO_SUPPORTED_LANGUAGES); return request; } @Override void processReply(REPLClientContext context, com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { clientContext.displayFailReply(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG)); } else { clientContext.displayReply("Languages supported:"); displayLanguages(replies); } } }; private void displayLanguages(com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { for (com.oracle.truffle.tools.debug.shell.REPLMessage message : replies) { final StringBuilder sb = new StringBuilder(); final String name = message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.LANG_NAME); if (!name.equals("")) { sb.append(name); sb.append(" ver. "); sb.append(message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.LANG_VER)); clientContext.displayInfo(sb.toString()); } } } private final REPLCommand infoSetCommand = new REPLLocalCommand("set", null, "info about settings") { final String[] help = {"info sets: list local options that can be set"}; @Override public String[] getHelp() { return help; } @Override public void execute(String[] args) { clientContext.displayReply("Settable options:"); for (String optionName : optionNames) { final LocalOption localOption = localOptions.get(optionName); if (localOption == null) { clientContext.displayInfo(localOption + ": Error, no implementation for option"); } else { clientContext.displayInfo(optionName + "=" + localOption.getValue() + ": " + localOption.getDescription()); } } } }; private final REPLCommand listCommand = new REPLLocalCommand("list", null, "Display selected source file") { final String[] help = {"list: list <listsize> lines of selected file (see option \"listsize\")", "list all: list all lines", "list <n>: list <listsize> lines centered around line <n>"}; private Source lastListedSource = null; private int nextLineToList = 1; @Override public String[] getHelp() { return help; } @Override public void execute(String[] args) { final Source source = clientContext.getSelectedSource(); if (source == null) { clientContext.displayFailReply("No selected file"); reset(); return; } final int listSize = listSizeOption.getInt(); if (args.length == 1) { if (!source.equals(lastListedSource)) { reset(); } else if (nextLineToList > source.getLineCount()) { reset(); } final int lastListedLine = printLines(source, nextLineToList, listSize); lastListedSource = source; nextLineToList = lastListedLine > source.getLineCount() ? 1 : lastListedLine + 1; } else if (args.length == 2) { reset(); if (args[1].equals("all")) { printLines(source, 1, source.getLineCount()); } else { try { final int line = Integer.parseInt(args[1]); final int halfListSize = listSize / 2; final int start = Math.max(1, line - halfListSize); final int count = Math.min(source.getLineCount() + 1 - start, listSize); printLines(source, start, count); } catch (NumberFormatException e) { clientContext.displayFailReply("\"" + args[1] + "\" not recognized"); } } } } private int printLines(Source printSource, int start, int listSize) { clientContext.displayReply(printSource.getName() + ":"); final int lastLineNumber = Math.min(start + listSize - 1, printSource.getLineCount()); for (int line = start; line <= lastLineNumber; line++) { writer.format(CODE_LINE_FORMAT, line, printSource.getCode(line)); } return lastLineNumber; } /** * Forget where we were in a sequence of list commands with no arguments */ private void reset() { lastListedSource = clientContext.getSelectedSource(); nextLineToList = 1; } }; private final REPLCommand quitCommand = new REPLRemoteCommand("quit", "q", "Quit execution and REPL") { @Override protected com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { return null; } }; private final REPLCommand setCommand = new REPLLocalCommand("set", null, "set <option>=<value>") { @Override public String[] getHelp() { return new String[]{"Sets an option \"set <option-name>=<value>\"; see also \"info set\""}; } @Override public void execute(String[] args) { com.oracle.truffle.tools.debug.shell.REPLMessage request = null; if (args.length == 1) { clientContext.displayFailReply("No option specified, try \"help set\""); } else if (args.length == 2) { String[] split = new String[0]; try { split = args[1].split("="); } catch (Exception ex) { } if (split.length == 0) { clientContext.displayFailReply("Arguments not understood, try \"help set\""); } else if (split.length == 1) { clientContext.displayFailReply("No option value specified, try \"help set\""); } else if (split.length > 2) { clientContext.displayFailReply("Arguments not understood, try \"help set\""); } else { final String optionName = split[0]; final String newValue = split[1]; final LocalOption localOption = localOptions.get(optionName); if (localOption != null) { if (!localOption.setValue(newValue)) { clientContext.displayFailReply("Invalid option value \"" + newValue + "\""); } clientContext.displayInfo(localOption.name + " = " + localOption.getValue()); } else { request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.SET); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OPTION, optionName); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.VALUE, newValue); } } } else { clientContext.displayFailReply("Arguments not understood, try \"help set\""); } } }; private final REPLIndirectCommand truffleCommand = new REPLIndirectCommand(com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE, "t", "Access to Truffle internals") { // "Truffle" commands private final Map<String, REPLCommand> truffleCommandMap = new HashMap<>(); private final Collection<String> truffleCommandNames = new TreeSet<>(); @Override public String[] getHelp() { final ArrayList<String> lines = new ArrayList<>(); for (String truffleCommandName : truffleCommandNames) { final REPLCommand cmd = truffleCommandMap.get(truffleCommandName); if (cmd == null) { lines.add("\"" + com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE + " " + truffleCommandName + "\" not implemented"); } else { for (String line : cmd.getHelp()) { lines.add(line); } } } return lines.toArray(new String[0]); } @Override void addCommand(REPLCommand replCommand) { final String commandName = replCommand.getCommand(); final String abbreviation = replCommand.getAbbreviation(); truffleCommandNames.add(commandName); truffleCommandMap.put(commandName, replCommand); if (abbreviation != null) { truffleCommandMap.put(abbreviation, replCommand); } } @Override REPLCommand getCommand(String[] args) { if (args.length == 1) { clientContext.displayFailReply("truffle request not specified; try \"help truffle\""); return null; } final String topic = args[1]; REPLCommand command = truffleCommandMap.get(topic); if (command == null) { clientContext.displayFailReply("truffle request \"" + topic + "\" not recognized"); return null; } return command; } }; private final REPLRemoteCommand truffleASTCommand = new REPLRemoteCommand("ast", null, "print the AST that contains the current node") { final String[] help = {"truffle ast: print the AST subtree that contains current node (see \"set treedepth\")", "truffle ast <n>: print the AST subtree that contains current node to a maximum depth of <n>"}; @Override public String[] getHelp() { return help; } @Override public com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { if (clientContext.level() == 0) { context.displayFailReply("no active execution"); return null; } final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.TOPIC, com.oracle.truffle.tools.debug.shell.REPLMessage.AST); int astDepth = astDepthOption.getInt(); if (args.length > 2) { final String depthText = args[2]; try { astDepth = Integer.parseInt(depthText); } catch (NumberFormatException e) { } } request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.AST_DEPTH, Integer.toString(astDepth)); return request; } @Override void processReply(REPLClientContext context, com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { clientContext.displayFailReply(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG)); } else { clientContext.displayReply("AST containing the Current Node:"); for (com.oracle.truffle.tools.debug.shell.REPLMessage message : replies) { for (String line : message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG).split("\n")) { clientContext.displayInfo(line); } } } } }; private void displayTruffleAST(String text) { clientContext.displayReply("AST containing Current Node:"); for (String line : text.split("\n")) { clientContext.displayInfo(line); } } private final REPLRemoteCommand truffleNodeCommand = new REPLRemoteCommand("node", null, "describe current AST node") { final String[] help = {"truffle node: describe the AST node at the current execution context"}; @Override public String[] getHelp() { return help; } @Override public com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { if (clientContext.level() == 0) { context.displayFailReply("no active execution"); return null; } final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE_NODE); return request; } @Override void processReply(REPLClientContext context, com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { clientContext.displayFailReply(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG)); } else { displayTruffleNode(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG)); } } }; private void displayTruffleNode(String nodeString) { clientContext.displayReply("Current Node: " + nodeString); } private final REPLRemoteCommand truffleSubtreeCommand = new REPLRemoteCommand("subtree", "sub", "print the AST subtree rooted at the current node") { final String[] help = {"truffle sub: print the AST subtree at the current node (see \"set treedepth\")", "truffle sub <n>: print the AST subtree at the current node to maximum depth <n>", "truffle subtree: print the AST subtree at the current node (see \"set treedepth\")", "truffle sub <n>: print the AST subtree at the current node to maximum depth <n>"}; @Override public String[] getHelp() { return help; } @Override public com.oracle.truffle.tools.debug.shell.REPLMessage createRequest(REPLClientContext context, String[] args) { if (clientContext.level() == 0) { context.displayFailReply("no active execution"); return null; } final com.oracle.truffle.tools.debug.shell.REPLMessage request = new com.oracle.truffle.tools.debug.shell.REPLMessage(); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.OP, com.oracle.truffle.tools.debug.shell.REPLMessage.TRUFFLE); request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.TOPIC, com.oracle.truffle.tools.debug.shell.REPLMessage.SUBTREE); int astDepth = astDepthOption.getInt(); if (args.length > 2) { final String depthText = args[2]; try { astDepth = Integer.parseInt(depthText); } catch (NumberFormatException e) { } } request.put(com.oracle.truffle.tools.debug.shell.REPLMessage.AST_DEPTH, Integer.toString(astDepth)); return request; } @Override void processReply(REPLClientContext context, com.oracle.truffle.tools.debug.shell.REPLMessage[] replies) { if (replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.STATUS).equals(com.oracle.truffle.tools.debug.shell.REPLMessage.FAILED)) { clientContext.displayFailReply(replies[0].get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG)); } else { clientContext.displayReply("AST subtree at Current Node:"); for (com.oracle.truffle.tools.debug.shell.REPLMessage message : replies) { for (String line : message.get(com.oracle.truffle.tools.debug.shell.REPLMessage.DISPLAY_MSG).split("\n")) { clientContext.displayInfo(line); } } } } }; private void displayTruffleSubtree(String text) { clientContext.displayReply("AST subtree at Current Node:"); for (String line : text.split("\n")) { clientContext.displayInfo(line); } } private final REPLCommand whereCommand = new REPLLocalCommand("where", null, "Show code around current break location") { @Override public void execute(String[] args) { clientContext.displayWhere(); } }; private abstract static class LocalOption { private final String name; private final String description; protected LocalOption(String name, String description) { this.name = name; this.description = description; } public String getName() { return name; } public String getDescription() { return description; } public abstract boolean setValue(String newValue); public boolean getBool() { assert false; return false; } public int getInt() { assert false; return 0; } public abstract String getValue(); } private static final class BooleanOption extends LocalOption { private Boolean value; BooleanOption(boolean value, String name, String description) { super(name, description); this.value = value; } @Override public boolean setValue(String newValue) { final Boolean valueOf = Boolean.valueOf(newValue); if (valueOf == null) { return false; } value = valueOf; return true; } @Override public boolean getBool() { return value; } @Override public String getValue() { return value.toString(); } } private static final class IntegerOption extends LocalOption { private Integer value; IntegerOption(int value, String name, String description) { super(name, description); this.value = value; } @Override public boolean setValue(String newValue) { Integer valueOf; try { valueOf = Integer.valueOf(newValue); } catch (NumberFormatException e) { return false; } value = valueOf; return true; } @Override public int getInt() { return value; } @Override public String getValue() { return value.toString(); } } }