package org.dynjs.debugger; import io.netty.channel.ChannelHandler; import org.dynjs.debugger.commands.*; import org.dynjs.debugger.events.BreakEvent; import org.dynjs.debugger.model.Breakpoint; import org.dynjs.debugger.model.Frame; import org.dynjs.debugger.model.Script; import org.dynjs.debugger.requests.Request; import org.dynjs.debugger.requests.Response; import org.dynjs.parser.Statement; import org.dynjs.runtime.ExecutionContext; import org.dynjs.runtime.SourceProvider; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; /** * @author Bob McWhirter */ public class Debugger { public enum StepAction { RUN, NEXT, IN, OUT, SUSPEND, } private ExecutionContext globalContext; private String fileName; private ExecutionContext basisContext; private List<ExecutionContext> contextStack = new LinkedList<>(); private final AtomicBoolean lock = new AtomicBoolean(); private DebugListener listener; private StepAction mode; private Map<String, AbstractCommand> commands = new HashMap<>(); private List<ChannelHandler> handlers = new ArrayList<>(); private AtomicLong breakPointCounter = new AtomicLong(); private List<Breakpoint> breakPoints = new ArrayList<>(); private ReferenceManager referenceManager = new ReferenceManager(); private Set<SourceProvider> sources = new HashSet<>(); private boolean paused = false; private Statement currentStatement = null; public Debugger() { this.mode = StepAction.RUN; register("version", new Version(this)); register("scripts", new Scripts(this)); register("source", new Source(this)); register("suspend", new Suspend(this)); register("continue", new Continue(this)); register("evaluate", new Evaluate(this)); register("lookup", new Lookup(this)); register("setbreakpoint", new SetBreakpoint(this)); register("listbreakpoints", new ListBreakpoints(this)); register("clearbreakpoint", new ClearBreakpoint(this)); register("backtrace", new Backtrace(this)); } public void setGlobalContext(ExecutionContext globalContext) { this.globalContext = globalContext; } public ExecutionContext getGlobalContext() { return this.globalContext; } void register(String name, AbstractCommand command) { this.commands.put(name, command); } public AbstractCommand getCommand(String command) { return this.commands.get(command); } public Collection<AbstractCommand> getCommands() { return this.commands.values(); } public void setWaitConnect(boolean waitConnect) { this.mode = StepAction.NEXT; } public ReferenceManager getReferenceManager() { return this.referenceManager; } public DebugListener getListener() { return this.listener; } public synchronized void setListener(DebugListener listener) { this.listener = listener; this.notifyAll(); } public List<Frame> getFrames(int fromFrame, int toFrame) { int frameIndex = 0; List<Frame> frames = new ArrayList<>(); for (ExecutionContext each : contextStack) { if (frameIndex >= fromFrame && frameIndex <= toFrame) { frames.add(new Frame(frameIndex, each)); } ++frameIndex; if (frameIndex > toFrame) { break; } } return frames; } public ExecutionContext getCurrentContext() { if (this.contextStack.isEmpty()) { return null; } return this.contextStack.get(0); } public ExecutionContext getContext(int frame) { return this.contextStack.get(frame); } public synchronized void enterContext(ExecutionContext context) { this.contextStack.add(0, context); this.sources.add(context.getSource()); } public synchronized void exitContext(ExecutionContext context) { this.contextStack.remove(0); } public Set<SourceProvider> getSources() { return this.sources; } public boolean isRunning() { return !this.paused; } public void debug(ExecutionContext context, Statement statement, Statement previousStatement) throws InterruptedException { if (this.paused) { // only got here because of an `evaluate` ? // so do not debug... return; } this.currentStatement = statement; if (statement.getPosition() != null) { this.fileName = statement.getPosition().getFileName(); } if (shouldBreak(statement, previousStatement)) { this.paused = true; try { doBreak(context, statement); } finally { this.paused = false; } } } public void setBreakpoint(Breakpoint breakpoint) { this.breakPoints.add(breakpoint); } public boolean removeBreakpoint(long number) { Iterator<Breakpoint> iter = this.breakPoints.iterator(); while (iter.hasNext()) { Breakpoint each = iter.next(); if (each.getNumber() == number) { iter.remove(); return true; } } return false; } private void setMode(StepAction action) { this.mode = action; unbreak(); } private void unbreak() { synchronized (this.lock) { this.lock.set(false); this.lock.notifyAll(); } } public void run() { setMode(StepAction.RUN); } public void stepIn() { setMode(StepAction.IN); } public void stepOut() { setMode(StepAction.OUT); } public void stepNext() { setMode(StepAction.NEXT); } public void suspend() { this.mode = StepAction.SUSPEND; if (this.paused) { this.listener.on(new BreakEvent(this, this.currentStatement.getPosition().getLine() - 1, this.currentStatement.getPosition().getColumn(), new Script(getCurrentContext().getSource(), false))); } } public String getFileName() { return this.fileName; } public <REQUEST extends Request<RESPONSE>, RESPONSE extends Response> RESPONSE handle(REQUEST request) { AbstractCommand<REQUEST, RESPONSE> command = getCommand(request.getCommand()); if (command != null) { return command.handle(request); } return null; } private void doBreak(ExecutionContext context, Statement statement) throws InterruptedException { this.paused = true; /* synchronized (this) { while (this.listener == null) { this.wait(); } } */ this.basisContext = getCurrentContext(); setBreak(); if (this.listener != null) { this.listener.on(new BreakEvent(this, statement.getPosition().getLine() - 1, statement.getPosition().getColumn(), new Script(getCurrentContext().getSource(), false))); } synchronized (this.lock) { while (this.lock.get()) { this.lock.wait(); } this.lock.set(false); this.referenceManager.reset(); this.paused = false; } } private boolean shouldBreak(Statement statement, Statement previousStatement) { boolean result = false; switch (this.mode) { case SUSPEND: result = true; break; case RUN: result = checkBreakpoints(statement, previousStatement); break; case NEXT: result = (isCurrentlyInBasisContext() || hasExitedBasisContext()); break; case IN: result = !hasExitedBasisContext(); break; case OUT: result = hasExitedBasisContext(); break; } return result; } private void setBreak() { synchronized (this.lock) { this.lock.set(true); } } private boolean checkBreakpoints(Statement statement, Statement previousStatement) { for (Breakpoint each : this.breakPoints) { if (each.shouldBreak(statement, previousStatement)) { return true; } } return false; } public List<Breakpoint> getBreakPoints() { return this.breakPoints; } private boolean isCurrentlyInBasisContext() { return this.basisContext == getCurrentContext(); } private boolean hasExitedBasisContext() { for (ExecutionContext cur : this.contextStack) { if (cur == this.basisContext) { return false; } } return true; } }