// GtpEngine.java package net.sf.gogui.gtp; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import net.sf.gogui.go.GoPoint; import net.sf.gogui.go.InvalidPointException; import net.sf.gogui.go.PointList; import net.sf.gogui.util.StringUtil; /** Base class for Go programs and tools implementing GTP. */ public class GtpEngine { public GtpEngine(PrintStream log) { m_log = log; register("known_command", new GtpCallback() { public void run(GtpCommand cmd) throws GtpError { cmdKnownCommand(cmd); } }); register("list_commands", new GtpCallback() { public void run(GtpCommand cmd) throws GtpError { cmdListCommands(cmd); } }); register("name", new GtpCallback() { public void run(GtpCommand cmd) throws GtpError { cmdName(cmd); } }); register("protocol_version", new GtpCallback() { public void run(GtpCommand cmd) throws GtpError { cmdProtocolVersion(cmd); } }); register("quit", new GtpCallback() { public void run(GtpCommand cmd) throws GtpError { cmdQuit(cmd); } }); register("version", new GtpCallback() { public void run(GtpCommand cmd) throws GtpError { cmdVersion(cmd); } }); } public void cmdKnownCommand(GtpCommand cmd) throws GtpError { String name = cmd.getArg(); cmd.setResponse(m_commands.containsKey(name) ? "true" : "false"); } public void cmdListCommands(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); StringBuilder response = cmd.getResponse(); Iterator it = m_commands.keySet().iterator(); while (it.hasNext()) { response.append(it.next()); response.append('\n'); } } public void cmdName(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); cmd.setResponse(m_name); } public void cmdProtocolVersion(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); cmd.setResponse("2"); } public void cmdQuit(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); setQuit(); } public void cmdUnknown(GtpCommand cmd) throws GtpError { throw new GtpError("unknown command: " + cmd.getCommand()); } public void cmdVersion(GtpCommand cmd) throws GtpError { cmd.checkArgNone(); cmd.setResponse(m_version); } /** Callback for interrupting commands. This callback will be invoked if the special comment line "# interrupt" is received. It will be invoked from a different thread. */ public void interruptCommand() { m_interrupted = true; } /** Handle command. The default implementation looks up the command within the registered commands and calls the registered callback. */ public void handleCommand(GtpCommand cmd) throws GtpError { m_interrupted = false; String name = cmd.getCommand(); GtpCallback callback = (GtpCallback)m_commands.get(name); if (callback == null) cmdUnknown(cmd); else callback.run(cmd); } public boolean isRegistered(String command) { return m_commands.containsKey(command); } public synchronized void log(String line) { assert m_log != null; m_log.println(line); } /** Main command loop. Reads commands and calls GtpEngine.handleCommand until the end of the input stream or the quit command is reached. */ public void mainLoop(InputStream in, OutputStream out) throws IOException { m_out = new PrintStream(out); m_in = in; ReadThread readThread = new ReadThread(this, m_in, m_log != null); readThread.start(); while (true) { GtpCommand cmd = readThread.getCommand(); if (cmd == null) return; boolean status = true; String response; try { handleCommand(cmd); response = cmd.getResponse().toString(); } catch (GtpError e) { response = e.getMessage(); status = false; } String sanitizedResponse = response.replaceAll("\\n\\n", "\n \n"); respond(status, cmd.hasId(), cmd.getId(), sanitizedResponse); // TODO: Use only quit flag not GtpCommand.isQuit once all // subclasses use the new registered quit command if (m_quit || cmd.isQuit()) return; } } /** Utility function for parsing a point argument. @param cmdArray Command line split into words. @param boardSize Board size is needed for parsing the point @return GoPoint argument */ public static GoPoint parsePointArgument(String[] cmdArray, int boardSize) throws GtpError { if (cmdArray.length != 2) throw new GtpError("Missing point argument"); try { return GoPoint.parsePoint(cmdArray[1], boardSize); } catch (InvalidPointException e) { throw new GtpError(e.getMessage()); } } /** Utility function for parsing an point list argument. @param cmdArray Command line split into words. @param boardSize Board size is needed for parsing the points @return Point list argument */ public static PointList parsePointListArgument(String[] cmdArray, int boardSize) throws GtpError { try { int length = cmdArray.length; assert length >= 1; PointList pointList = new PointList(); for (int i = 1; i < length; ++i) { GoPoint p = GoPoint.parsePoint(cmdArray[i], boardSize); pointList.add(p); } return pointList; } catch (InvalidPointException e) { throw new GtpError(e.getMessage()); } } /** Print invalid response directly to output stream. Should only be used for simulationg broken GTP implementations like used in the gogui-dummy_invalid command. @param text Text to print to output stream. No newline will be appended. */ public void printInvalidResponse(String text) { m_out.print(text); } /** Register new command. If a command was already registered with the same name, it will be replaced by the new command. */ public final void register(String command, GtpCallback callback) { unregister(command); m_commands.put(command, callback); } public void respond(boolean status, boolean hasId, int id, String response) { StringBuilder fullResponse = new StringBuilder(256); if (status) fullResponse.append('='); else fullResponse.append('?'); if (hasId) fullResponse.append(id); fullResponse.append(' '); fullResponse.append(response); if (response.length() == 0 || response.charAt(response.length() - 1) != '\n') fullResponse.append('\n'); m_out.println(fullResponse); if (m_log != null) m_log.println(fullResponse); } /** Set quit flag for terminating command loop. */ public void setQuit() { m_quit = true; } /** Set name for name command. */ public void setName(String name) { m_name = name; } /** Set version for version command. */ public void setVersion(String version) { m_version = version; } public final void unregister(String command) { if (m_commands.containsKey(command)) m_commands.remove(command); } protected boolean isInterrupted() { return m_interrupted; } private volatile boolean m_interrupted; private boolean m_quit; private String m_name = "Unknown"; /** Engine version. The GTP standard says to return empty string, if no meaningful reponse is available. */ private String m_version; /** Mapping from command to callback. */ private final Map<String,GtpCallback> m_commands = new TreeMap<String,GtpCallback>(); private InputStream m_in; private final PrintStream m_log; private PrintStream m_out; } /** Thread reading the command stream. Reading is done in a seperate thread to allow the notification of Server about an asynchronous interrupt received using the special comment line '# interrupt'. */ class ReadThread extends Thread { public ReadThread(GtpEngine server, InputStream in, boolean log) { m_in = new BufferedReader(new InputStreamReader(in)); m_server = server; m_log = log; } public synchronized boolean endOfFile() { return m_endOfFile; } public GtpCommand getCommand() { synchronized (this) { assert ! m_waitCommand; m_waitCommand = true; notifyAll(); try { wait(); } catch (InterruptedException e) { System.err.println("Interrupted"); } assert m_endOfFile || ! m_waitCommand; GtpCommand result = m_command; m_command = null; return result; } } public void run() { try { while (true) { String line = m_in.readLine(); if (line == null) { synchronized (this) { m_endOfFile = true; } } else { if (m_log) m_server.log(line); line = line.trim(); if (line.equals("# interrupt")) { m_server.interruptCommand(); } if (line.equals("") || line.charAt(0) == '#') continue; } synchronized (this) { while (! m_waitCommand) { wait(); } if (line == null) m_command = null; else m_command = new GtpCommand(line); notifyAll(); m_waitCommand = false; if (m_command == null || m_command.isQuit()) return; } } } catch (Throwable e) { StringUtil.printException(e); } } private boolean m_endOfFile; private final boolean m_log; private boolean m_waitCommand; private final BufferedReader m_in; private GtpCommand m_command; private final GtpEngine m_server; }