package com.aptana.ruby.internal.debug.core; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.model.IBreakpoint; import com.aptana.ruby.debug.core.IRubyBreakpoint; import com.aptana.ruby.debug.core.IRubyLineBreakpoint; import com.aptana.ruby.debug.core.IRubyMethodBreakpoint; import com.aptana.ruby.debug.core.RubyDebugCorePlugin; import com.aptana.ruby.debug.core.RubyDebugModel; import com.aptana.ruby.debug.core.model.IEvaluationResult; import com.aptana.ruby.debug.core.model.IRubyExceptionBreakpoint; import com.aptana.ruby.debug.core.model.IRubyStackFrame; import com.aptana.ruby.internal.debug.core.commands.AbstractDebuggerConnection; import com.aptana.ruby.internal.debug.core.commands.BreakpointCommand; import com.aptana.ruby.internal.debug.core.commands.BreakpointConditionSetCommand; import com.aptana.ruby.internal.debug.core.commands.ClassicDebuggerConnection; import com.aptana.ruby.internal.debug.core.commands.ExceptionBreakpointCommand; import com.aptana.ruby.internal.debug.core.commands.GenericCommand; import com.aptana.ruby.internal.debug.core.commands.RubyDebugConnection; import com.aptana.ruby.internal.debug.core.model.IRubyDebugTarget; import com.aptana.ruby.internal.debug.core.model.RubyDebugTarget; import com.aptana.ruby.internal.debug.core.model.RubyEvaluationResult; import com.aptana.ruby.internal.debug.core.model.RubyProcessingException; import com.aptana.ruby.internal.debug.core.model.RubyStackFrame; import com.aptana.ruby.internal.debug.core.model.RubyThread; import com.aptana.ruby.internal.debug.core.model.RubyVariable; import com.aptana.ruby.internal.debug.core.model.ThreadInfo; import com.aptana.ruby.internal.debug.core.parsing.AbstractReadStrategy; import com.aptana.ruby.internal.debug.core.parsing.ErrorReader; import com.aptana.ruby.internal.debug.core.parsing.FramesReader; import com.aptana.ruby.internal.debug.core.parsing.LoadResultReader; import com.aptana.ruby.internal.debug.core.parsing.SuspensionReader; import com.aptana.ruby.internal.debug.core.parsing.ThreadInfoReader; import com.aptana.ruby.internal.debug.core.parsing.VariableReader; @SuppressWarnings("nls") public class RubyDebuggerProxy { // TODO What the heck is this "key" for? public final static String DEBUGGER_ACTIVE_KEY = "com.aptana.ruby.debug.ui.debuggerActive"; //$NON-NLS-1$ private AbstractDebuggerConnection debuggerConnection; private IRubyDebugTarget debugTarget; private RubyLoop rubyLoop; private ICommandFactory commandFactory; private Thread threadUpdater; private Thread errorReader; // private boolean isLoopFinished; public RubyDebuggerProxy(IRubyDebugTarget debugTarget, boolean isRubyDebug) { this.debugTarget = debugTarget; debugTarget.setRubyDebuggerProxy(this); commandFactory = isRubyDebug ? new RubyDebugCommandFactory() : new ClassicDebuggerCommandFactory(); debuggerConnection = isRubyDebug ? new RubyDebugConnection(debugTarget.getHost(), debugTarget.getPort()) : new ClassicDebuggerConnection(debugTarget.getPort()); } public boolean checkConnection() { return debuggerConnection.isCommandPortConnected(); } public void start() throws RubyProcessingException, IOException { // isLoopFinished = false; debuggerConnection.connect(); this.setBreakPoints(); this.startRubyLoop(); } public void stop() throws IOException { if (rubyLoop == null) { // only in tests, where no real connection is established return; } rubyLoop.setShouldStop(); rubyLoop.interrupt(); closeConnection(); } protected void setBreakPoints() throws IOException { IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager().getBreakpoints( RubyDebugModel.getModelIdentifier()); for (int i = 0; i < breakpoints.length; i++) { this.addBreakpoint(breakpoints[i]); } } public void addBreakpoint(IBreakpoint breakpoint) { try { if (breakpoint.isEnabled()) { if (breakpoint instanceof IRubyExceptionBreakpoint) { String command = commandFactory.createCatchOn((IRubyExceptionBreakpoint) breakpoint); new ExceptionBreakpointCommand(command).executeWithResult(debuggerConnection); } else if (breakpoint instanceof IRubyMethodBreakpoint) { IRubyMethodBreakpoint rubymethodBreakpoint = (IRubyMethodBreakpoint) breakpoint; String command = commandFactory.createAddMethodBreakpoint(rubymethodBreakpoint.getFilePath().toOSString(), rubymethodBreakpoint.getTypeName(), rubymethodBreakpoint.getMethodName(), rubymethodBreakpoint.getLineNumber()); int index = new BreakpointCommand(command).executeWithResult(debuggerConnection); rubymethodBreakpoint.setIndex(index); } else if (breakpoint instanceof IRubyLineBreakpoint) { IRubyLineBreakpoint rubyLineBreakpoint = (IRubyLineBreakpoint) breakpoint; String command = commandFactory.createAddBreakpoint(rubyLineBreakpoint.getLocation().toOSString(), rubyLineBreakpoint.getLineNumber()); int index = new BreakpointCommand(command).executeWithResult(debuggerConnection); rubyLineBreakpoint.setIndex(index); if (rubyLineBreakpoint.isConditionEnabled()) { command = commandFactory.createSetCondition(rubyLineBreakpoint); new BreakpointConditionSetCommand(command).execute(debuggerConnection); } } } } catch (IOException e) { RubyDebugCorePlugin.log(e); } catch (CoreException e) { RubyDebugCorePlugin.log(e); } } public void removeBreakpoint(IBreakpoint breakpoint) { try { if (breakpoint instanceof IRubyExceptionBreakpoint) { String command = commandFactory.createCatchOff((IRubyExceptionBreakpoint) breakpoint); if (command != null) new BreakpointCommand(command).execute(debuggerConnection); } else if (breakpoint instanceof IRubyLineBreakpoint) { IRubyLineBreakpoint rubyLineBreakpoint = (IRubyLineBreakpoint) breakpoint; if (rubyLineBreakpoint.getIndex() != -1) { String command = commandFactory.createRemoveBreakpoint(rubyLineBreakpoint.getIndex()); // TODO: check for errors new BreakpointCommand(command).executeWithResult(debuggerConnection); rubyLineBreakpoint.setIndex(-1); } } } catch (IOException e) { RubyDebugCorePlugin.log(e); } catch (CoreException e) { RubyDebugCorePlugin.log(e); } } public void updateBreakpoint(IBreakpoint breakpoint, IMarkerDelta markerDelta) { this.removeBreakpoint(breakpoint); this.addBreakpoint(breakpoint); } public void startRubyLoop() throws DebuggerNotFoundException, IOException { debuggerConnection.start(); rubyLoop = new RubyLoop(); rubyLoop.start(); Runnable runnable = new Runnable() { public void run() { try { RubyDebugCorePlugin.debug("Command Connection error handler started."); while (debuggerConnection.getCommandReadStrategy().isConnected()) { // The read strategy resumes read() after the connection to the debugger // has been dropped new ErrorReader(debuggerConnection.getCommandReadStrategy()).read(); } } catch (Exception e) { RubyDebugCorePlugin.log(e); } finally { RubyDebugCorePlugin.debug("Command Connection error handler finished."); } }; }; errorReader = new Thread(runnable, "Error Reader"); errorReader.start(); // TODO: Check if it would not be better if the ruby part created the threadinfos // only after a change to the thread status has occurred Runnable threadListener = new Runnable() { public void run() { try { RubyDebugCorePlugin.debug("Thread updater started."); Thread.sleep(2000); GenericCommand cmd = null; while (cmd == null || cmd.getReadStrategy().isConnected()) { if (!getDebugTarget().isSuspended()) { String command = commandFactory.createReadThreads(); cmd = new GenericCommand(command, true /* isControl */); cmd.execute(debuggerConnection); ThreadInfo[] threadInfos = new ThreadInfoReader(cmd.getReadStrategy()).readThreads(); ((RubyDebugTarget) getDebugTarget()).updateThreads(threadInfos); } Thread.sleep(2000); } } catch (Exception e) { RubyDebugCorePlugin.log(e); } finally { RubyDebugCorePlugin.debug("Thread updater finished."); } }; }; threadUpdater = new Thread(threadListener, "Ruby Thread Updater"); threadUpdater.start(); } public void resume(RubyThread thread) { try { println(commandFactory.createResume(thread)); } catch (IOException e) { // terminate ? } } protected void println(String s) throws IOException { try { // TOOD: GenericCommand is only temporary solution new GenericCommand(s, false /* isControl */).execute(debuggerConnection); } catch (IOException e) { RubyDebugCorePlugin.debug("Could not send to debugger. Exception occured.", e); throw e; } } protected IRubyDebugTarget getDebugTarget() { return debugTarget; } public RubyVariable[] readVariables(RubyStackFrame frame) throws DebugException { try { this.println(commandFactory.createReadLocalVariables(frame)); return new VariableReader(getMultiReaderStrategy()).readVariables(frame); } catch (Exception e) { throw new DebugException(new Status(IStatus.ERROR, RubyDebugCorePlugin.getPluginIdentifier(), -1, e .getMessage(), e)); } } public RubyVariable[] readInstanceVariables(RubyVariable variable) { try { this.println(commandFactory.createReadInstanceVariable(variable)); return new VariableReader(getMultiReaderStrategy()).readVariables(variable); } catch (Exception ioex) { ioex.printStackTrace(); throw new RuntimeException(ioex.getMessage()); } } public RubyVariable readInspectExpression(IRubyStackFrame frame, String expression) throws RubyProcessingException { try { expression = expression.replaceAll("\\n", "\\\\n"); RubyEvaluationResult result = new RubyEvaluationResult(expression, frame.getThread()); this.println(commandFactory.createInspect(frame, expression)); RubyVariable[] variables = new VariableReader(getMultiReaderStrategy()).readVariables(frame); if (variables.length == 0) { return null; } result.setValue(variables[0].getValue()); return variables[0]; } catch (IOException ioex) { ioex.printStackTrace(); throw new RuntimeException(ioex.getMessage()); } } public IEvaluationResult evaluate(RubyStackFrame frame, String expression) { expression = expression.replaceAll("\\r\\n", "\n"); expression = expression.replaceAll("\\n", "; "); expression = expression.trim(); RubyEvaluationResult result = new RubyEvaluationResult(expression, frame.getThread()); try { this.println(commandFactory.createInspect(frame, expression)); RubyVariable[] variables = new VariableReader(getMultiReaderStrategy()).readVariables(frame); if (variables.length > 0) { result.setValue(variables[0].getValue()); } } catch (IOException ioex) { DebugException ex = new DebugException(new Status(IStatus.ERROR, RubyDebugCorePlugin.PLUGIN_ID, DebugException.INTERNAL_ERROR, ioex.getMessage(), ioex)); result.setException(ex); } catch (RubyProcessingException e) { DebugException ex = new DebugException(new Status(IStatus.ERROR, RubyDebugCorePlugin.PLUGIN_ID, DebugException.TARGET_REQUEST_FAILED, e.getMessage(), e)); result.setException(ex); } return result; } public void sendStepOverEnd(RubyStackFrame stackFrame) { try { this.println(commandFactory.createStepOver(stackFrame)); } catch (Exception e) { RubyDebugCorePlugin.log(e); } } public void sendStepReturnEnd(RubyStackFrame stackFrame) { try { this.println(commandFactory.createStepReturn(stackFrame)); } catch (Exception e) { RubyDebugCorePlugin.log(e); } } public void sendStepIntoEnd(RubyStackFrame stackFrame) { try { this.println(commandFactory.createStepInto(stackFrame)); } catch (Exception e) { RubyDebugCorePlugin.log(e); } } public void sendThreadStop(RubyThread thread) { try { String command = commandFactory.createThreadStop(thread); new GenericCommand(command, true /* isControl */).execute(debuggerConnection); } catch (Exception e) { RubyDebugCorePlugin.log(e); } } public RubyStackFrame[] readFrames(RubyThread thread) { try { this.println(commandFactory.createReadFrames(thread)); return new FramesReader(getMultiReaderStrategy()).readFrames(thread); } catch (IOException e) { RubyDebugCorePlugin.log(e); return null; } } public ThreadInfo[] readThreads() { try { String command = commandFactory.createReadThreads(); new GenericCommand(command, true /* isControl */).execute(debuggerConnection); return new ThreadInfoReader(getMultiReaderStrategy()).readThreads(); } catch (Exception e) { RubyDebugCorePlugin.log(e); return null; } } public IStatus readLoadResult(String filename) { try { this.println(commandFactory.createLoad(filename)); return new LoadResultReader(getMultiReaderStrategy()).readLoadResult(); } catch (Exception e) { return new Status(IStatus.ERROR, RubyDebugCorePlugin.getPluginIdentifier(), -1, e.getMessage(), e); } } public void closeConnection() throws IOException { debuggerConnection.exit(); } private AbstractReadStrategy getMultiReaderStrategy() { return debuggerConnection.getCommandReadStrategy(); } class RubyLoop extends Thread { public RubyLoop() { this.setName("RubyDebuggerLoop"); } public void setShouldStop() { } public void run() { Map<IRubyBreakpoint, Integer> map = new HashMap<IRubyBreakpoint, Integer>(3); try { System.setProperty(DEBUGGER_ACTIVE_KEY, "true"); RubyDebugCorePlugin.debug("Waiting for breakpoints."); while (true) { final SuspensionPoint hit = new SuspensionReader(getMultiReaderStrategy()).readSuspension(); if (hit == null) { break; } // HACK Implement hit count here, since debugger backends don't support it! if (!hit.isStep()) { IRubyBreakpoint breakpoint = getBreakpoint(hit); if (breakpoint != null) { int hitCount = breakpoint.getHitCount(); if (hitCount != -1) { // increment our counter for this breakpoint int count = 0; if (map.containsKey(breakpoint)) { count = map.get(breakpoint); } count++; // Check counter versus hit count if (count != hitCount) { // haven't hit our target count yet, update counter map.put(breakpoint, count); // resume RubyDebuggerProxy.this.resume(((RubyDebugTarget) getDebugTarget()) .getThreadById(hit.getThreadId())); continue; } // hit the desired count! Disable this breakpoint from now on! removeBreakpoint(breakpoint); map.remove(breakpoint); } } } RubyDebugCorePlugin.debug(hit); // TODO: should this be using the JOB API? new Thread("RubyDebuggerProxy suspension notifier") { public void run() { getDebugTarget().suspensionOccurred(hit); } }.start(); } } catch (DebuggerNotFoundException ex) { throw ex; } catch (Exception ex) { RubyDebugCorePlugin.debug("Exception in socket reader loop.", ex); } finally { if (map != null) map.clear(); map = null; System.setProperty(DEBUGGER_ACTIVE_KEY, "false"); try { getDebugTarget().terminate(); closeConnection(); } catch (Exception e) { RubyDebugCorePlugin.log(e); } RubyDebugCorePlugin.debug("Socket reader loop finished."); } } } public IRubyBreakpoint getBreakpoint(SuspensionPoint hit) { IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager().getBreakpoints( RubyDebugModel.getModelIdentifier()); for (IBreakpoint breakpoint : breakpoints) { if (hit.isBreakpoint() && breakpoint instanceof IRubyLineBreakpoint) { try { IRubyLineBreakpoint lineBreak = (IRubyLineBreakpoint) breakpoint; if (lineBreak.getLineNumber() == hit.getLine() && lineBreak.getLocation().toOSString().equals(hit.getFile())) return lineBreak; } catch (CoreException e) { RubyDebugCorePlugin.log(e); } } else if (hit.isException() && breakpoint instanceof IRubyExceptionBreakpoint) { try { IRubyExceptionBreakpoint exception = (IRubyExceptionBreakpoint) breakpoint; if (exception.getTypeName().equals(((ExceptionSuspensionPoint) hit).getExceptionType())) return exception; } catch (CoreException e) { RubyDebugCorePlugin.log(e); } } } return null; } }