package featurecat.lizzie.analysis; import com.zaxxer.nuprocess.NuAbstractProcessHandler; import com.zaxxer.nuprocess.NuProcess; import com.zaxxer.nuprocess.NuProcessBuilder; import com.zaxxer.nuprocess.NuProcessHandler; import org.apache.commons.lang3.tuple.ImmutablePair; import org.jtrim2.utils.ObjectFinalizer; import featurecat.lizzie.util.ArgumentTokenizer; import featurecat.lizzie.util.ThreadPoolUtil; import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; import java.util.Objects; import java.util.concurrent.*; import java.util.function.Consumer; import java.util.stream.Collectors; public class GeneralGtpClient implements GtpClient { protected class GeneralGtpProcessHandler extends NuAbstractProcessHandler implements Closeable { private final ObjectFinalizer objectFinalizer; protected ExecutorService stdoutProcessor; protected ExecutorService stderrProcessor; protected ExecutorService miscProcessor; protected StringBuilder stdoutLineBuilder; protected StringBuilder stderrLineBuilder; protected boolean inCommandResponse; public GeneralGtpProcessHandler() { objectFinalizer = new ObjectFinalizer(this::doCleanup, "GtpClientHandler.cleanup"); stdoutProcessor = Executors.newSingleThreadExecutor(); stderrProcessor = Executors.newSingleThreadExecutor(); miscProcessor = Executors.newSingleThreadExecutor(); stdoutLineBuilder = new StringBuilder(2048); stderrLineBuilder = new StringBuilder(2048); inCommandResponse = false; } @Override public void onStart(final NuProcess nuProcess) { miscProcessor.execute(() -> engineStartedObserverList.forEach(observer -> observer.accept(nuProcess.getPID()))); } @Override public void onExit(final int statusCode) { ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair; while ((futurePair = runningCommandQueue.poll()) != null) { futurePair.getLeft().markCompleted(); } while ((futurePair = stagineCommandQueue.poll()) != null) { futurePair.getLeft().markCompleted(); } miscProcessor.execute(() -> engineExitObserverList.forEach(observer -> observer.accept(statusCode))); engineExit = true; } @Override public void onStdout(final ByteBuffer buffer, final boolean closed) { if (!closed) { final byte[] bytes = new byte[buffer.remaining()]; // You must update buffer.position() before returning (either implicitly, // like this, or explicitly) to indicate how many bytes your handler has consumed. buffer.get(bytes); stdoutProcessor.execute(() -> { for (byte b : bytes) { stdoutLineBuilder.append((char) b); if (b == '\n') { onEngineStdoutLine(stdoutLineBuilder.toString()); stdoutLineBuilder = new StringBuilder(2048); } } }); } } @Override public void onStderr(final ByteBuffer buffer, final boolean closed) { if (!closed) { final byte[] bytes = new byte[buffer.remaining()]; // You must update buffer.position() before returning (either implicitly, // like this, or explicitly) to indicate how many bytes your handler has consumed. buffer.get(bytes); stderrProcessor.execute(() -> { for (byte b : bytes) { stderrLineBuilder.append((char) b); if (b == '\n') { onEngineStderrLine(stderrLineBuilder.toString()); stderrLineBuilder = new StringBuilder(2048); } } }); } } @Override public synchronized boolean onStdinReady(final ByteBuffer buffer) { if (!runningCommandQueue.isEmpty() && runningCommandQueue.stream().anyMatch(pair -> !pair.getLeft().isContinuous())) { return false; } ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair = stagineCommandQueue.poll(); if (futurePair != null) { GeneralGtpFuture future = futurePair.getLeft(); buffer.put(future.getCommand().getBytes()); if (!future.getCommand().endsWith("\n")) { buffer.put((byte) '\n'); } buffer.flip(); runningCommandQueue.offer(futurePair); miscProcessor.execute(() -> engineGtpCommandObserverList.forEach(observer -> observer.accept(future.getCommand()))); // In case of some particular situations if (future.isContinuous() && !stagineCommandQueue.isEmpty()) { return true; } } return false; } protected synchronized void onEngineStdoutLine(final String line) { engineStdoutLineConsumerList.forEach(consumer -> consumer.accept(line)); if (inCommandResponse) { ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair = Objects.requireNonNull(runningCommandQueue.peek()); GeneralGtpFuture future = Objects.requireNonNull(futurePair.getLeft()); Consumer<String> commandOutputConsumer = futurePair.getRight(); List<String> response = future.getResponse(); if (commandOutputConsumer == null) { response.add(line); } else { commandOutputConsumer.accept(line); } if (line.equals("\n") || line.equals("\r\n") || line.equals("\r")) { future.markCompleted(); runningCommandQueue.poll(); inCommandResponse = false; // Notify for next command processing if (runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous()) && !stagineCommandQueue.isEmpty()) { gtpProcess.wantWrite(); } } else { // Prevent stuck if (future.isContinuous() && runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous()) && !stagineCommandQueue.isEmpty()) { gtpProcess.wantWrite(); } } } else if (line.startsWith("=") || line.startsWith("?")) { inCommandResponse = true; ImmutablePair<GeneralGtpFuture, Consumer<String>> futurePair = Objects.requireNonNull(runningCommandQueue.peek()); GeneralGtpFuture future = Objects.requireNonNull(futurePair.getLeft()); Consumer<String> commandOutputConsumer = futurePair.getRight(); List<String> response = future.getResponse(); future.markStarted(); if (commandOutputConsumer == null) { response.add(line); } else { commandOutputConsumer.accept(line); // Prevent stuck if (future.isContinuous() && runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous()) && !stagineCommandQueue.isEmpty()) { gtpProcess.wantWrite(); } } } else { onEngineDiagnosticLine(line); } } protected void onEngineStderrLine(final String line) { engineStderrLineConsumerList.forEach(consumer -> consumer.accept(line)); } protected void onEngineDiagnosticLine(final String line) { engineDiagnosticLineConsumerList.forEach(consumer -> consumer.accept(line)); } private void doCleanup() { if (stdoutProcessor != null) { ThreadPoolUtil.shutdownAndAwaitTermination(stdoutProcessor); stdoutProcessor = null; } if (stderrProcessor != null) { ThreadPoolUtil.shutdownAndAwaitTermination(stderrProcessor); stderrProcessor = null; } if (miscProcessor != null) { ThreadPoolUtil.shutdownAndAwaitTermination(miscProcessor); miscProcessor = null; } } @Override public void close() { objectFinalizer.doFinalize(); } } private final ObjectFinalizer objectFinalizer; private List<String> gtpCommandLine; private NuProcessHandler gtpProcessHandler; private NuProcess gtpProcess; private ConcurrentLinkedQueue<ImmutablePair<GeneralGtpFuture, Consumer<String>>> stagineCommandQueue; private ConcurrentLinkedQueue<ImmutablePair<GeneralGtpFuture, Consumer<String>>> runningCommandQueue; private List<Consumer<String>> engineDiagnosticLineConsumerList; private List<Consumer<String>> engineStdoutLineConsumerList; private List<Consumer<String>> engineStderrLineConsumerList; private List<Consumer<Integer>> engineStartedObserverList; private List<Consumer<Integer>> engineExitObserverList; private List<Consumer<String>> engineGtpCommandObserverList; private boolean engineExit; public GeneralGtpClient(String commandLine) { this(ArgumentTokenizer.tokenize(commandLine)); } public GeneralGtpClient(List<String> commandLine) { objectFinalizer = new ObjectFinalizer(this::doCleanup, "GtpClient.cleanup"); gtpCommandLine = commandLine; stagineCommandQueue = new ConcurrentLinkedQueue<>(); runningCommandQueue = new ConcurrentLinkedQueue<>(); engineDiagnosticLineConsumerList = new CopyOnWriteArrayList<>(); engineStdoutLineConsumerList = new CopyOnWriteArrayList<>(); engineStderrLineConsumerList = new CopyOnWriteArrayList<>(); engineStartedObserverList = new CopyOnWriteArrayList<>(); engineExitObserverList = new CopyOnWriteArrayList<>(); engineGtpCommandObserverList = new CopyOnWriteArrayList<>(); engineExit = false; } public void registerDiagnosticLineConsumer(Consumer<String> consumer) { engineDiagnosticLineConsumerList.add(consumer); } public void unregisterDiagnosticLineConsumer(Consumer<String> consumer) { engineDiagnosticLineConsumerList.remove(consumer); } @Override public void registerStdoutLineConsumer(Consumer<String> consumer) { engineStdoutLineConsumerList.add(consumer); } @Override public void unregisterStdoutLineConsumer(Consumer<String> consumer) { engineStdoutLineConsumerList.remove(consumer); } @Override public void registerStderrLineConsumer(Consumer<String> consumer) { engineStderrLineConsumerList.add(consumer); } @Override public void unregisterStderrLineConsumer(Consumer<String> consumer) { engineStderrLineConsumerList.remove(consumer); } public boolean removeCommandFromStagineQueue(GeneralGtpFuture future) { return stagineCommandQueue.removeIf(futurePair -> futurePair.getLeft().equals(future)); } @Override public GtpFuture postCommand(String command, boolean continuous, Consumer<String> commandOutputConsumer) { GeneralGtpFuture future = new GeneralGtpFuture(command, this, continuous); synchronized (gtpProcessHandler) { stagineCommandQueue.offer(ImmutablePair.of(future, commandOutputConsumer)); if (runningCommandQueue.isEmpty() || runningCommandQueue.stream().allMatch(pair -> pair.getLeft().isContinuous())) { gtpProcess.wantWrite(); } } return future; } @Override public void start() { NuProcessBuilder processBuilder = new NuProcessBuilder(gtpCommandLine); gtpProcessHandler = provideProcessHandler(); processBuilder.setProcessListener(gtpProcessHandler); setUpOtherProcessParameters(processBuilder); gtpProcess = Objects.requireNonNull(processBuilder.start()); } protected void setUpOtherProcessParameters(NuProcessBuilder processBuilder) { } protected NuProcessHandler provideProcessHandler() { return this.new GeneralGtpProcessHandler(); } private int doShutdown(long timeout, TimeUnit timeUnit) { int exitCode = Integer.MIN_VALUE; if (gtpProcess != null && gtpProcess.isRunning()) { try { postCommand("quit"); exitCode = gtpProcess.waitFor(timeout, timeUnit); if (exitCode == Integer.MIN_VALUE) { gtpProcess.destroy(false); exitCode = gtpProcess.waitFor(timeout, timeUnit); if (exitCode == Integer.MIN_VALUE) { gtpProcess.destroy(true); } } } catch (InterruptedException e) { gtpProcess.destroy(true); } gtpProcess = null; } if (gtpProcessHandler instanceof Closeable) { try { ((Closeable) gtpProcessHandler).close(); gtpProcessHandler = null; } catch (IOException e) { // Do nothing } } return exitCode; } @Override public int shutdown(long timeout, TimeUnit timeUnit) { int exitCode = doShutdown(timeout, timeUnit); objectFinalizer.markFinalized(); return exitCode; } @Override public boolean isRunning() { return gtpProcess != null && gtpProcess.isRunning(); } @Override public boolean isShutdown() { return engineExit; } @Override public void registerEngineStartedObserver(Consumer<Integer> observer) { engineStartedObserverList.add(observer); } @Override public void unregisterEngineStartedObserver(Consumer<Integer> observer) { engineStartedObserverList.remove(observer); } @Override public void registerEngineExitObserver(Consumer<Integer> observer) { engineExitObserverList.add(observer); } @Override public void unregisterEngineExitObserver(Consumer<Integer> observer) { engineExitObserverList.remove(observer); } @Override public void registerGtpCommandObserver(Consumer<String> observer) { engineGtpCommandObserverList.add(observer); } @Override public void unregisterGtpCommandObserver(Consumer<String> observer) { engineGtpCommandObserverList.remove(observer); } private void doCleanup() { doShutdown(60, TimeUnit.SECONDS); } @Override public void close() { objectFinalizer.doFinalize(); } public static void main(String[] args) throws Exception { final GtpClient gtpClient = new GeneralGtpClient("leelaz.exe -g -t2 -wnetwork"); gtpClient.start(); Future<List<String>> timeResult = gtpClient.postCommand("888 time_settings 0 10 1"); new Thread(() -> { Future<List<String>> nameResult = gtpClient.postCommand("1 name"); for (int i = 0; i < 100; ++i) { gtpClient.postCommand(100 + i + " name"); gtpClient.postCommand(200 + i + " name"); } try { System.out.println(nameResult.get().stream().map(String::trim).collect(Collectors.toList())); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }).start(); new Thread(() -> { Future<List<String>> errorResult = gtpClient.postCommand("1234 estimate_score"); for (int i = 0; i < 145; ++i) { gtpClient.postCommand(300 + i + " name"); gtpClient.postCommand(400 + i + " list_commands"); } try { System.out.println(errorResult.get().stream().map(String::trim).collect(Collectors.toList())); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }).start(); gtpClient.postCommand("5 name"); gtpClient.postCommand("5 name"); Future<List<String>> nameResult = gtpClient.postCommand("6 name"); gtpClient.postCommand("list_commands"); Future<List<String>> genMoveResult = gtpClient.postCommand("100 genmove b"); Future<List<String>> listResult = gtpClient.postCommand("7 list_commands"); gtpClient.postCommand("8 list_commands"); gtpClient.postCommand("99999 list_commands", s -> System.out.println(s.trim())); gtpClient.postCommand("8 list_commands"); gtpClient.postCommand("111111 list_commands"); System.out.println(genMoveResult.get().stream().map(String::trim).collect(Collectors.toList())); System.out.println(listResult.get().stream().map(String::trim).collect(Collectors.toList())); System.out.println(nameResult.get().stream().map(String::trim).collect(Collectors.toList())); System.out.println(timeResult.get().stream().map(String::trim).collect(Collectors.toList())); gtpClient.postCommand("hello"); gtpClient.postCommand("hello"); gtpClient.postCommand("hello"); gtpClient.postCommand("hello"); gtpClient.postCommand("hello"); gtpClient.postCommand("hello"); System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size()); gtpClient.postCommand("97654 lz-analyze 50", true, s -> System.out.println(s.trim())); System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size()); gtpClient.postCommand("hello"); System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size()); Thread.sleep(2000); gtpClient.postCommand("87654 lz-analyze 50", true, s -> System.out.println(s.trim())); System.out.printf("Staging: %d, Running: %d\n", ((GeneralGtpClient) gtpClient).stagineCommandQueue.size(), ((GeneralGtpClient) gtpClient).runningCommandQueue.size()); Thread.sleep(10000); nameResult = gtpClient.postCommand("89012 name"); System.out.println(nameResult.get().stream().map(String::trim).collect(Collectors.toList())); gtpClient.shutdown(60, TimeUnit.SECONDS); } }