package org.develnext.jphp.debug.impl; import org.develnext.jphp.debug.impl.breakpoint.Breakpoint; import org.develnext.jphp.debug.impl.breakpoint.BreakpointManager; import org.develnext.jphp.debug.impl.command.AbstractCommand; import org.develnext.jphp.debug.impl.command.InitCommand; import org.develnext.jphp.debug.impl.command.UnimplementedCommand; import org.develnext.jphp.debug.impl.command.support.CommandArguments; import org.w3c.dom.Document; import php.runtime.common.StringUtils; import php.runtime.env.Environment; import php.runtime.env.TraceInfo; import php.runtime.memory.ArrayMemory; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.*; import java.net.ConnectException; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class Debugger { public enum Status { STARTING, STOPPING, STOPPED, BREAK } public enum Step { RUN, OVER, INTO, FORCE_INTO, OUT, DROP_FRAME, } protected Status status = Status.STARTING; protected Socket socket; protected final Thread loopThread; protected boolean shutdown = false; protected boolean init = false; protected boolean working = true; protected Step waitStep = Step.INTO; protected static DocumentBuilderFactory xmlBuilderFactory = DocumentBuilderFactory.newInstance(); protected static DocumentBuilder xmlBuilder; protected static TransformerFactory transformerFactory = TransformerFactory.newInstance(); protected static Transformer transformer; protected String ideKey; protected String rootPath; private CommandArguments currentArguments = null; private AbstractCommand currentCommand = null; private DebugTick registeredTick = null; public final IdeFeatures ideFeatures; public final BreakpointManager breakpointManager; static { try { xmlBuilder = xmlBuilderFactory.newDocumentBuilder(); transformer = transformerFactory.newTransformer(); //transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); } catch (ParserConfigurationException | TransformerConfigurationException e) { throw new DebuggerException(e); } } public Debugger(int port) throws IOException { this(port, "127.0.0.1"); } public Debugger(int port, String hostname) throws IOException { this.breakpointManager = new BreakpointManager(this); this.ideFeatures = new IdeFeatures(); log("waiting for IDE, " + hostname + ":" + port + " ..."); while (true) { try { if (shutdown) { log("the program shutdown."); break; } this.socket = new Socket(hostname, port); log("IDE has been connected."); log("-----", false); break; } catch (ConnectException e) { // nop... try { Thread.sleep(1000); } catch (InterruptedException e1) { throw new DebuggerException(e1); } } } this.loopThread = new Thread(new Runnable() { @Override public void run() { try { InputStream in = socket.getInputStream(); StringBuilder sb = new StringBuilder(); while (true) { if (socket.isClosed()) { return; } if (!socket.isConnected()) { return; } if (shutdown) { return; } int ch = in.read(); if (ch > -1) { if (ch == '\0') { Debugger.this.onRequest(sb.toString()); sb = new StringBuilder(); } else { sb.append((char)ch); } } } } catch (IOException e) { if (socket.isClosed()) { return; } throw new DebuggerException(e); } } }); } public void log(String message) { log(message, true); } public void log(String message, boolean prefix) { System.out.println((prefix ? "~ Debugger: " : "") + message); } public Status getStatus() { return status; } public String getIdeKey() { return ideKey; } public void setIdeKey(String ideKey) { this.ideKey = ideKey; } public void setRootPath(String rootPath) { try { this.rootPath = new File(rootPath).getCanonicalPath().replace('\\', '/'); } catch (IOException e) { throw new DebuggerException(e); } } public String getFileName(String fileName) { return "file://" + rootPath + "/" + fileName; } public boolean isWorking() { return working; } public Step getWaitStep() { return waitStep; } public void shutdown() { this.shutdown = true; this.working = false; } public DebugTick runTicks() { return waitTick(Step.RUN); } public DebugTick waitTick(Step step) { working = false; waitStep = step; DebugTick oldTick = registeredTick; while (registeredTick == oldTick) { if (shutdown) { break; } try { Thread.sleep(50); } catch (InterruptedException e) { throw new DebuggerException(e); } } return registeredTick; } public void registerBreak(Breakpoint breakpoint, Environment env, TraceInfo trace, ArrayMemory locals) { status = Status.BREAK; registeredTick = new DebugTick(breakpoint, env, trace, locals); this.working = true; } public void registerEnd(Environment env) { registeredTick = new DebugTick(null, env, null, null); this.working = true; } public DebugTick getRegisteredTick() { return registeredTick; } public void run() { InitCommand initCommand = new InitCommand("JPHP_APP", "file://" + rootPath); initCommand.setIdeKey(ideKey); write(null, initCommand); loopThread.start(); } public void close() { try { socket.close(); } catch (IOException e) { throw new DebuggerException(e); } } protected void onRequest(String input) { String[] args = StringUtils.split(input, " ", 200); if (args.length > 0) { String command = args[0].trim(); String name = null; CommandArguments result = new CommandArguments(); for (int i = 1; i < args.length; i++) { if (i % 2 == 0) { if (name != null) { String value = args[i]; if (value.startsWith("\"") && value.endsWith("\"")) { value = value.substring(1, value.length() - 2); } result.put(name, value); } } else { name = args[i]; if (name.startsWith("-")) { name = name.substring(1); } else { name = null; } } } AbstractCommand cmd = commands.get(command); if (cmd == null) { cmd = new UnimplementedCommand(command); } currentArguments = result; currentCommand = cmd; write(result, cmd); if (cmd.afterExecutionContinueNeeded()) { runTicks(); } } } protected void write(CommandArguments args, AbstractCommand command) { try { Document document = xmlBuilder.newDocument(); command.run(this, args, document); StringWriter xmlWriter = new StringWriter(); StreamResult xmlResult = new StreamResult(xmlWriter); transformer.transform(new DOMSource(document), xmlResult); byte[] xmlBytes = xmlWriter.toString().getBytes(); DataOutputStream out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); out.write(String.valueOf(xmlBytes.length).getBytes()); out.write("\0".getBytes()); out.write(xmlBytes); out.write("\0".getBytes()); out.flush(); } catch (IOException | TransformerException e) { throw new DebuggerException(e); } } protected final static Map<String, AbstractCommand> commands = new HashMap<>(); public static void registerCommand(AbstractCommand command) { commands.put(command.getName(), command); } }