package rhogenwizard.debugger.backend; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.Vector; /** * Rhodes Debug Server implementation. * @author Albert R. Timashev */ public class DebugServer extends Thread { static private PrintStream debugOutput = null; private static int debugServerPort0 = 9000; private static int debugServerPort1 = 9999; private int debugServerPort = 0; private IDebugCallback debugCallback; private DebugProtocol debugProtocol = null; private ServerSocket serverSocket = null; private BufferedReader inFromClient = null; private OutputStreamWriter outToClient = null; /** * Create a debug server. * @param callback - object to receive events from the debug target * (Rhodes application). */ public DebugServer(IDebugCallback callback) { this.debugCallback = callback; this.initialize(); } /** * Set port range to bind Debug Server to (default 9000-9999). * @param port0 - starting port number. * @param port1 - ending port number. */ public static void setDebugPortRange(int port0, int port1) { debugServerPort0 = port0; debugServerPort1 = port1; } /** * Set an output stream for a detailed debug information. * @param stream - output stream (if null, no debug information * will be passed anywhere). */ public static void setDebugOutputStream(PrintStream stream) { debugOutput = stream; } /** * Get the debug server port. * @return Port number the debug server is bound/listening to. */ public int getPort() { return this.debugServerPort; } private void initialize() { try { // find & bind free local port this.serverSocket = null; this.debugServerPort = 0; for (int i=debugServerPort0; i<=debugServerPort1; i++) { try { ServerSocket s = new ServerSocket(i); this.serverSocket = s; break; } catch( IOException ioe ) { } } if (this.serverSocket == null) { throw new IOException(String.format( "Unable to open server in port range (%d..%d)", debugServerPort0,debugServerPort1)); } assert this.serverSocket.isBound(); if (this.serverSocket.isBound()) { this.debugServerPort = this.serverSocket.getLocalPort(); if ((debugOutput != null)) { debugOutput.println("Debug server port " + this.debugServerPort + " is ready and waiting for Rhodes application to connect..."); } } } catch (SocketException se) { se.printStackTrace(); } catch (IOException ioe) { ioe.printStackTrace(); } } public void run() { try { Socket clientSocket = serverSocket.accept(); inFromClient = new BufferedReader(new InputStreamReader( clientSocket.getInputStream())); outToClient = new OutputStreamWriter(new BufferedOutputStream( clientSocket.getOutputStream()), "US-ASCII"); debugProtocol = new DebugProtocol(this, this.debugCallback); try { String data; while ((data = inFromClient.readLine()) != null) { if (debugOutput != null) debugOutput.println("Received: " + data); debugProtocol.processCommand(data); } } catch (EOFException e) { } catch (IOException e) { } finally { try { clientSocket.close(); } catch (IOException e) { } } } catch (IOException ioe) { } catch (NullPointerException e) { } } /** * Shutdown the debug server (close the connection and the server socket). */ public void shutdown() { this.interrupt(); try { if (this.inFromClient != null) this.inFromClient.close(); if (this.outToClient != null) this.outToClient.close(); if (this.serverSocket != null) this.serverSocket.close(); } catch (IOException ioe) { } this.inFromClient = null; this.outToClient = null; this.serverSocket = null; if (debugOutput != null) debugOutput.println("Debug server stopped."); } protected void send(String cmd) { try { if (!cmd.endsWith("\n")) cmd += "\n"; outToClient.write(cmd); outToClient.flush(); } catch (IOException ioe) { } catch (NullPointerException e) { } } /** * Get current state of the connected Rhodes application. * @return Returns a {@link DebugState}. */ public DebugState debugGetState() { return this.debugProtocol!=null ? this.debugProtocol.getState() : DebugState.NOTCONNECTED; } /** * Get current file path within <code>app</code> folder of the Rhodes application. * @return Returns current file path if current state of Rhodes application is either * {@link DebugState#BREAKPOINT} or {@link DebugState#STEP}. Otherwise returns "". */ public String debugGetFile() { if (this.debugProtocol!=null) { if (DebugState.paused(this.debugProtocol.getState())) return this.debugProtocol.getCurrentFile(); } return ""; } /** * Get current line number. * @return Returns current line number if current state of Rhodes application is either * {@link DebugState#BREAKPOINT} or {@link DebugState#STEP}. Otherwise returns 0. */ public int debugGetLine() { if (this.debugProtocol!=null) { if (DebugState.paused(this.debugProtocol.getState())) return this.debugProtocol.getCurrentLine(); } return 0; } /** * Get current class of the Rhodes application. * @return Returns current class name if current state of Rhodes application is either * {@link DebugState#BREAKPOINT} or {@link DebugState#STEP}. Otherwise returns "". */ public String debugGetClass() { if (this.debugProtocol!=null) { if (DebugState.paused(this.debugProtocol.getState())) return this.debugProtocol.getCurrentClass(); } return ""; } /** * Get currently executing method of the Rhodes application. * @return Returns current method name if current state of Rhodes application is either * {@link DebugState#BREAKPOINT} or {@link DebugState#STEP}. Otherwise returns "". */ public String debugGetMethod() { if (this.debugProtocol!=null) { if (DebugState.paused(this.debugProtocol.getState())) return this.debugProtocol.getCurrentMethod(); } return ""; } /** * Step over the next method call (without entering it) at the currently * executing line of Ruby code. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugStepOver() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.stepOver(); } /** * Step into the next method call at the currently executing line of Ruby code. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugStepInto() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.stepInto(); } /** * Run until return from the current method of Ruby code. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugStepReturn() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.stepReturn(); } /** * Resume a normal execution of the Rhodes application (after the stop at * breakpoint or after {@link #debugStepInto()}, {@link #debugStepOver()} * or {@link #debugStepReturn()} method call). * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugResume() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.resume(); } /** * Add a breakpoint. * @param file - file path within <code>app</code> folder of the Rhodes * application, e.g. <code>"application.rb"</code> * (always use <code>'/'</code> as a folder/file name separator). * @param line - effective line number (starting with 1). Must point to * non-empty line of code. */ public void debugBreakpoint(String file, int line) { if (this.debugProtocol!=null) this.debugProtocol.addBreakpoint(file, line); } /** * Remove an existing breakpoint. * @param file - file path within <code>app</code> folder of the Rhodes * application, e.g. <code>"application.rb"</code> * (always use <code>'/'</code> as a folder/file name separator). * @param line - breakpoint effective line number (starting with 1). */ public void debugRemoveBreakpoint(String file, int line) { if (this.debugProtocol!=null) this.debugProtocol.removeBreakpoint(file, line); } /** * Remove all breakpoints. */ public void debugRemoveAllBreakpoints() { if (this.debugProtocol!=null) this.debugProtocol.removeAllBreakpoints(); } /** * Toggle breakpoints skip mode. * @param skip - if <code>true</code>, skip all breakpoints; * if <code>false</code>, stop at breakpoints. */ public void debugSkipBreakpoints(boolean skip) { if (this.debugProtocol!=null) this.debugProtocol.skipBreakpoints(skip); } /** * Evaluate Ruby expression or execute arbitrary Ruby code. * @param expression - expression to evaluate or Ruby code to execute. * Result of evaluation/execution is returned by the * {@link IDebugCallback#evaluation(boolean, String, String)} method call. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugEvaluate(String expression) throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.evaluate(expression); } /** * Get list and values of variables of all types. Result is returned by the * number of {@link IDebugCallback#watch(DebugVariableType, String, String)} * method calls preceded by {@link IDebugCallback#watchBOL(DebugVariableType)} * and concluded by {@link IDebugCallback#watchEOL(DebugVariableType)} for each * type of variables. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugGetVariables() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.getVariables(new DebugVariableType[] { DebugVariableType.GLOBAL, DebugVariableType.LOCAL, DebugVariableType.CLASS, DebugVariableType.INSTANCE }); } /** * Get list and values of variables of the specified types. Result is returned * by the number of {@link IDebugCallback#watch(DebugVariableType, String, String)} * method calls preceded by {@link IDebugCallback#watchBOL(DebugVariableType)} * and concluded by {@link IDebugCallback#watchEOL(DebugVariableType)} for each * type of variables. * @param types - list of variable types ({@link DebugVariableType}) to watch. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugGetVariables(DebugVariableType[] types) throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.getVariables(types); } /** * Get list and values of all available variables. This method waits for a debugged * application to respond and returns all variables in a single array. * This method <em>must</em> be called from the thread <em>other</em> * than the instance of {@link DebugServer}, i.e. it is <em>forbidden</em> * to call it directly from {@link IDebugCallback} methods. * @return Returns an {@link Vector} of {@link DebugVariable} objects if application is * currently paused (see {@link DebugState#paused(DebugState)}). Otherwise returns null. */ public Vector<DebugVariable> debugWatchList() { if (this.debugProtocol!=null) return this.debugProtocol.getWatchList(new DebugVariableType[] { DebugVariableType.LOCAL, DebugVariableType.INSTANCE, DebugVariableType.CLASS, DebugVariableType.GLOBAL, }); else return null; } /** * Get list and values of variables of the specified types. This method waits for * a debugged application to respond and returns all variables in a single array. * This method <em>must</em> be called from the thread <em>other</em> * than the instance of {@link DebugServer}, i.e. it is <em>forbidden</em> * to call it directly from {@link IDebugCallback} methods. * @return Returns an {@link Vector} of {@link DebugVariable} objects if application is * currently paused (see {@link DebugState#paused(DebugState)}). Otherwise returns null. */ public Vector<DebugVariable> debugWatchList(DebugVariableType[] types) { if (this.debugProtocol!=null) return this.debugProtocol.getWatchList(types); else return null; } /** * Evaluate Ruby expression or execute arbitrary Ruby code and returns * the result. * This method <em>must</em> be called from the thread <em>other</em> * than the instance of {@link DebugServer}, i.e. it is <em>forbidden</em> * to call it directly from {@link IDebugCallback} methods. * @param expression - expression to evaluate or Ruby code to execute. * @return Returns a {@link DebugEvaluation} object if application is * currently paused (see {@link DebugState#paused(DebugState)}). * Otherwise returns null. */ public DebugEvaluation debugInstantEvaluate(String expression) { if (this.debugProtocol!=null) return this.debugProtocol.instantEvaluate(expression); else return null; } /** * Suspend the execution of Ruby code. The * {@link IDebugCallback#step(String, int, String, String)} * method is called when the actual stop occurs. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugSuspend() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.suspend(); } /** * Terminates execution of the Rhodes application. The * {@link IDebugCallback#exited()} method is called when the actual * exit occurs. * @throws DebugServerException if the debugger is busy and cannot * respond right now. */ public void debugTerminate() throws DebugServerException { if (this.debugProtocol!=null) this.debugProtocol.terminate(); } /** * Check if the debugger is busy and cannot respond right now. * @return Returns <code>true</code> if the debugger is processing * some synchronous command (like {@link DebugServer#debugWatchList()}). */ public boolean debugIsProcessing() { return (this.debugProtocol!=null) && this.debugProtocol.isProcessing(); } }