diff --git a/EngineServer/.gitignore b/EngineServer/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/EngineServer/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/EngineServer/build.gradle b/EngineServer/build.gradle
new file mode 100644
index 0000000..50394ab
--- /dev/null
+++ b/EngineServer/build.gradle
@@ -0,0 +1,15 @@
+apply plugin: 'java-library'
+
+jar {
+ manifest {
+ attributes "Main-Class": "org.petero.engineserver.EngineServer"
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ testImplementation 'junit:junit:4.12'
+}
+
+sourceCompatibility = "8"
+targetCompatibility = "8"
diff --git a/EngineServer/src/main/java/org/petero/engineserver/EngineConfig.java b/EngineServer/src/main/java/org/petero/engineserver/EngineConfig.java
new file mode 100644
index 0000000..ea588b4
--- /dev/null
+++ b/EngineServer/src/main/java/org/petero/engineserver/EngineConfig.java
@@ -0,0 +1,35 @@
+/*
+ EngineServer - Network engine server for DroidFish
+ Copyright (C) 2019 Peter Österlund, peterosterlund2@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package org.petero.engineserver;
+
+
+/** Defines settings for one network engine. */
+public class EngineConfig {
+ boolean enabled;
+ int port;
+ String filename;
+ String arguments;
+
+ public EngineConfig(boolean e, int p, String fn, String args) {
+ enabled = e;
+ port = p;
+ filename = fn;
+ arguments = args;
+ }
+}
diff --git a/EngineServer/src/main/java/org/petero/engineserver/EngineServer.java b/EngineServer/src/main/java/org/petero/engineserver/EngineServer.java
new file mode 100644
index 0000000..e7abda1
--- /dev/null
+++ b/EngineServer/src/main/java/org/petero/engineserver/EngineServer.java
@@ -0,0 +1,134 @@
+/*
+ EngineServer - Network engine server for DroidFish
+ Copyright (C) 2019 Peter Österlund, peterosterlund2@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package org.petero.engineserver;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Properties;
+
+
+/** Manages starting and stopping PortListeners. */
+public class EngineServer implements ErrorHandler {
+ private EngineConfig[] configs;
+ private PortListener[] portListeners;
+ private MainWindow window;
+
+ public EngineServer(int numEngines) {
+ configs = new EngineConfig[numEngines];
+ portListeners = new PortListener[numEngines];
+ for (int i = 0; i < numEngines; i++) {
+ configs[i] = new EngineConfig(false, 4567 + i, "", "");
+ }
+ readConfig();
+ for (int i = 0; i < numEngines; i++)
+ configChanged(i);
+ }
+
+ private File getConfigFile() {
+ String home = System.getProperty("user.home");
+ return new File(home, ".engineServer.ini");
+ }
+
+ private void readConfig() {
+ try {
+ Properties prop = new Properties();
+ InputStream is = new FileInputStream(getConfigFile());
+ prop.load(is);
+ for (int i = 0; i < configs.length; i++) {
+ boolean enabled = Boolean.valueOf(prop.getProperty("enabled" + i, "false"));
+ String defPort = Integer.toString(4567 + i);
+ int port = Integer.valueOf(prop.getProperty("port" + i, defPort));
+ String filename = prop.getProperty("filename" + i, "");
+ String arguments = prop.getProperty("arguments" + i, "");
+ configs[i] = new EngineConfig(enabled, port, filename, arguments);
+ }
+ } catch (IOException ignore) {
+ } catch (NumberFormatException ignore) {
+ }
+ }
+
+ private void writeConfig() {
+ Properties prop = new Properties();
+ for (int i = 0; i < configs.length; i++) {
+ EngineConfig config = configs[i];
+ String enabled = config.enabled ? "true" : "false";
+ String port = Integer.toString(config.port);
+ String filename = config.filename;
+ String arguments = config.arguments;
+ prop.setProperty("enabled" + i, enabled);
+ prop.setProperty("port" + i, port);
+ prop.setProperty("filename" + i, filename);
+ prop.setProperty("arguments" + i, arguments);
+ }
+ try {
+ OutputStream os = new FileOutputStream(getConfigFile());
+ prop.store(os, "Created by EngineServer for DroidFish");
+ } catch (IOException ignore) {
+ }
+ }
+
+ private void run() {
+ window = new MainWindow(this, configs);
+ }
+
+ public void configChanged(int engineNo) {
+ EngineConfig config = configs[engineNo];
+ if (portListeners[engineNo] != null) {
+ portListeners[engineNo].shutdown();
+ portListeners[engineNo] = null;
+ }
+ if (config.enabled)
+ portListeners[engineNo] = new PortListener(config, this);
+ }
+
+ public void shutdown() {
+ writeConfig();
+ for (PortListener pl : portListeners)
+ if (pl != null)
+ pl.shutdown();
+ System.exit(0);
+ }
+
+ public static void main(String[] args) {
+ int numEngines = 8;
+ if (args.length > 0) {
+ try {
+ numEngines = Integer.valueOf(args[0]);
+ numEngines = Math.max(1, numEngines);
+ numEngines = Math.min(20, numEngines);
+ } catch (NumberFormatException ignore) {
+ }
+ }
+ EngineServer server = new EngineServer(numEngines);
+ server.run();
+ }
+
+ @Override
+ public void reportError(String title, String message) {
+ if (window != null) {
+ window.reportError(title, message);
+ } else {
+ System.err.printf("%s\n%s\n", title, message);
+ }
+ }
+}
diff --git a/EngineServer/src/main/java/org/petero/engineserver/ErrorHandler.java b/EngineServer/src/main/java/org/petero/engineserver/ErrorHandler.java
new file mode 100644
index 0000000..a626ea0
--- /dev/null
+++ b/EngineServer/src/main/java/org/petero/engineserver/ErrorHandler.java
@@ -0,0 +1,24 @@
+/*
+ EngineServer - Network engine server for DroidFish
+ Copyright (C) 2019 Peter Österlund, peterosterlund2@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package org.petero.engineserver;
+
+
+public interface ErrorHandler {
+ void reportError(String title, String message);
+}
diff --git a/EngineServer/src/main/java/org/petero/engineserver/MainWindow.java b/EngineServer/src/main/java/org/petero/engineserver/MainWindow.java
new file mode 100644
index 0000000..5095e3b
--- /dev/null
+++ b/EngineServer/src/main/java/org/petero/engineserver/MainWindow.java
@@ -0,0 +1,283 @@
+/*
+ EngineServer - Network engine server for DroidFish
+ Copyright (C) 2019 Peter Österlund, peterosterlund2@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package org.petero.engineserver;
+
+import java.awt.Container;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+
+
+/** Displays the GUI to configure engine settings. */
+public class MainWindow {
+ private JFrame frame;
+ private JCheckBox[] enabled;
+ private JTextField[] port;
+ private JTextField[] filename;
+ private JTextField[] arguments;
+ private JButton[] browse;
+
+ private EngineServer server;
+ private EngineConfig[] configs;
+
+ public MainWindow(EngineServer server, EngineConfig[] configs) {
+ this.server = server;
+ this.configs = configs;
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ initUI();
+ }
+ });
+ }
+
+ private void initUI() {
+ final int numEngines = configs.length;
+
+ frame = new JFrame();
+ enabled = new JCheckBox[numEngines];
+ port = new JTextField[numEngines];
+ filename = new JTextField[numEngines];
+ arguments = new JTextField[numEngines];
+ browse = new JButton[numEngines];
+
+ Container pane = frame.getContentPane();
+ frame.setTitle("Chess Engine Server");
+ frame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent event) {
+ server.shutdown();
+ }
+ });
+
+ GridBagLayout layout = new GridBagLayout();
+ pane.setLayout(layout);
+ int row = 0;
+
+ GridBagConstraints constr = new GridBagConstraints();
+ Insets inset = new Insets(0, 0, 5, 5);
+ constr.insets = inset;
+ constr.gridx = 0;
+ constr.gridy = row;
+ pane.add(new JLabel("Enabled"), constr);
+
+ constr = new GridBagConstraints();
+ constr.insets = inset;
+ constr.gridx = 1;
+ constr.gridy = row;
+ pane.add(new JLabel("Port"), constr);
+
+ constr = new GridBagConstraints();
+ constr.insets = inset;
+ constr.gridx = 2;
+ constr.gridy = row;
+ pane.add(new JLabel("Program and arguments"), constr);
+
+ row++;
+
+ for (int r = 0; r < numEngines; r++) {
+ final int engineNo = r;
+ final EngineConfig config = configs[r];
+ enabled[r] = new JCheckBox("");
+ constr = new GridBagConstraints();
+ constr.insets = inset;
+ constr.gridx = 0;
+ constr.gridy = row;
+ pane.add(enabled[r], constr);
+ enabled[r].setSelected(config.enabled);
+ enabled[r].addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ enabledChanged(engineNo);
+ }
+ });
+
+ port[r] = new JTextField();
+ constr = new GridBagConstraints();
+ constr.anchor = GridBagConstraints.WEST;
+ constr.insets = inset;
+ constr.gridx = 1;
+ constr.gridy = row;
+ pane.add(port[r], constr);
+ port[r].setColumns(5);
+ port[r].setText(Integer.toString(config.port));
+ port[r].addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ portChanged(engineNo);
+ }
+ });
+ port[r].addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent focusEvent) {
+ portChanged(engineNo);
+ }
+ });
+
+ filename[r] = new JTextField();
+ constr = new GridBagConstraints();
+ constr.insets = inset;
+ constr.fill = GridBagConstraints.HORIZONTAL;
+ constr.gridx = 2;
+ constr.gridy = row;
+ pane.add(filename[r], constr);
+ filename[r].setColumns(40);
+ filename[r].setText(config.filename);
+ filename[r].addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ filenameChanged(engineNo);
+ }
+ });
+ filename[r].addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent focusEvent) {
+ filenameChanged(engineNo);
+ }
+ });
+
+ arguments[r] = new JTextField();
+ constr = new GridBagConstraints();
+ constr.insets = inset;
+ constr.fill = GridBagConstraints.HORIZONTAL;
+ constr.gridx = 2;
+ constr.gridy = row + 1;
+ pane.add(arguments[r], constr);
+ arguments[r].setColumns(40);
+ arguments[r].setText(config.arguments);
+ arguments[r].addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ argumentsChanged(engineNo);
+ }
+ });
+ arguments[r].addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent focusEvent) {
+ argumentsChanged(engineNo);
+ }
+ });
+
+ browse[r] = new JButton("Browse");
+ constr = new GridBagConstraints();
+ constr.anchor = GridBagConstraints.NORTH;
+ constr.insets = inset;
+ constr.gridx = 3;
+ constr.gridy = row;
+ constr.gridheight = 2;
+ pane.add(browse[r], constr);
+ browse[r].addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent event) {
+ browseFile(engineNo);
+ }
+ });
+
+ row += 2;
+ }
+
+ frame.pack();
+ frame.setLocationRelativeTo(null);
+ frame.setVisible(true);
+ }
+
+ private void enabledChanged(int engineNo) {
+ EngineConfig config = configs[engineNo];
+ boolean e = enabled[engineNo].isSelected();
+ if (e != config.enabled) {
+ config.enabled = e;
+ server.configChanged(engineNo);
+ }
+ }
+
+ private void portChanged(int engineNo) {
+ EngineConfig config = configs[engineNo];
+ try {
+ int p = Integer.valueOf(port[engineNo].getText());
+ if (p >= 1024 && p < 65536 && p != config.port) {
+ config.port = p;
+ server.configChanged(engineNo);
+ }
+ } catch (NumberFormatException ignore) {
+ }
+ }
+
+ private void filenameChanged(int engineNo) {
+ EngineConfig config = configs[engineNo];
+ String fn = filename[engineNo].getText().trim();
+ if (!fn.equals(config.filename)) {
+ config.filename = fn;
+ server.configChanged(engineNo);
+ }
+ }
+
+ private void browseFile(int engineNo) {
+ String fn = filename[engineNo].getText();
+ JFileChooser chooser = new JFileChooser();
+ chooser.setDialogTitle("Select chess engine");
+ chooser.setSelectedFile(new File(fn));
+ if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION)
+ fn = chooser.getSelectedFile().getAbsolutePath();
+ filename[engineNo].setText(fn);
+ filenameChanged(engineNo);
+ }
+
+ private void argumentsChanged(int engineNo) {
+ EngineConfig config = configs[engineNo];
+ String args = arguments[engineNo].getText().trim();
+ if (!args.equals(config.arguments)) {
+ config.arguments = args;
+ server.configChanged(engineNo);
+ }
+ }
+
+ public void reportError(String title, String message) {
+ StringBuilder sb = new StringBuilder();
+ int lineLen = 100;
+ while (message.length() > lineLen) {
+ sb.append(message.substring(0, lineLen));
+ sb.append('\n');
+ message = message.substring(lineLen);
+ }
+ sb.append(message);
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ JOptionPane.showMessageDialog(frame, sb.toString(), title, JOptionPane.ERROR_MESSAGE);
+ }
+ });
+ }
+}
diff --git a/EngineServer/src/main/java/org/petero/engineserver/PortListener.java b/EngineServer/src/main/java/org/petero/engineserver/PortListener.java
new file mode 100644
index 0000000..8e47003
--- /dev/null
+++ b/EngineServer/src/main/java/org/petero/engineserver/PortListener.java
@@ -0,0 +1,201 @@
+/*
+ EngineServer - Network engine server for DroidFish
+ Copyright (C) 2019 Peter Österlund, peterosterlund2@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+package org.petero.engineserver;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+
+
+/** Listens to a TCP port and connects an engine process to a TCP socket. */
+public class PortListener {
+ private EngineConfig config;
+ private ErrorHandler errorHandler;
+
+ private Thread thread;
+ private ServerSocket serverSocket;
+ private Socket clientSocket;
+ private Process proc;
+ private volatile boolean shutDownFlag = false;
+
+ public PortListener(EngineConfig config, ErrorHandler errorHandler) {
+ this.config = config;
+ this.errorHandler = errorHandler;
+
+ thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ mainLoop();
+ } catch (InterruptedException ex) {
+ if (!shutDownFlag)
+ reportError("Background thread interrupted", ex);
+ } catch (IOException ex) {
+ if (!shutDownFlag)
+ reportError("IO error in background thread", ex);
+ }
+ }
+ };
+ thread.start();
+ }
+
+ private void mainLoop() throws IOException, InterruptedException {
+ try (ServerSocket serverSocket = new ServerSocket()) {
+ serverSocket.setReuseAddress(true);
+ serverSocket.bind(new InetSocketAddress(config.port));
+
+ this.serverSocket = serverSocket;
+ while (!shutDownFlag) {
+ try (Socket clientSocket = serverSocket.accept()) {
+ this.clientSocket = clientSocket;
+
+ ProcessBuilder builder = new ProcessBuilder();
+ ArrayList args = new ArrayList<>();
+ args.add(config.filename);
+ addArguments(args, config.arguments);
+ builder.command(args);
+ File dir = new File(config.filename).getParentFile();
+ if (dir != null)
+ builder.directory(dir);
+ builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+
+ Process proc = builder.start();
+ this.proc = proc;
+ Thread t1 = forwardIO(proc.getInputStream(), clientSocket.getOutputStream());
+ Thread t2 = forwardIO(clientSocket.getInputStream(), proc.getOutputStream());
+ try {
+ int exitCode = proc.waitFor();
+ if (exitCode != 0) {
+// errorHandler.reportError("Engine error",
+// "Engine terminated with status " + exitCode);
+ }
+ } catch (InterruptedException ex) {
+ proc.getOutputStream().close();
+ proc.destroyForcibly();
+ } finally {
+ close(clientSocket);
+ t1.join();
+ t2.join();
+ proc.waitFor();
+ this.proc = null;
+ }
+ }
+ }
+ }
+ }
+
+ private void addArguments(ArrayList cmdList, String argString) {
+ boolean inQuote = false;
+ StringBuilder sb = new StringBuilder();
+ int len = argString.length();
+ for (int i = 0; i < len; i++) {
+ char c = argString.charAt(i);
+ switch (c) {
+ case '"':
+ inQuote = !inQuote;
+ if (!inQuote) {
+ cmdList.add(sb.toString());
+ sb = new StringBuilder();
+ }
+ break;
+ case '\\':
+ if (i < len - 1) {
+ sb.append(argString.charAt(i + 1));
+ i++;
+ }
+ break;
+ case ' ':
+ case '\t':
+ if (!inQuote) {
+ if (!sb.toString().isEmpty()) {
+ cmdList.add(sb.toString());
+ sb = new StringBuilder();
+ }
+ break;
+ }
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ if (!sb.toString().isEmpty())
+ cmdList.add(sb.toString());
+ }
+
+ private Thread forwardIO(InputStream is, OutputStream os) {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ byte[] buffer = new byte[4096];
+ int len = is.read(buffer);
+ if (len < 0)
+ break;
+ os.write(buffer, 0, len);
+ os.flush();
+ }
+ } catch (IOException ignore) {
+ }
+ close(is);
+ close(os);
+ Process p = proc;
+ if (p != null)
+ p.destroyForcibly();
+ }
+ };
+ t.start();
+ return t;
+ }
+
+ public void shutdown() {
+ shutDownFlag = true;
+ thread.interrupt();
+ ServerSocket ss = serverSocket;
+ if (ss != null) {
+ close(ss);
+ }
+ Socket s = clientSocket;
+ if (s != null) {
+ close(s);
+ }
+ try {
+ thread.join();
+ } catch (InterruptedException ex) {
+ reportError("Failed to shutdown background thread", ex);
+ }
+ }
+
+ private void close(Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException ignore) {
+ }
+ }
+
+ private void reportError(String errMsg, Exception ex) {
+ errorHandler.reportError(errMsg, ex.getMessage());
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 3dca471..289ccf3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':DroidFishApp', ':CuckooChessEngine', ':CuckooChessApp', ':CuckooChess'
+include ':DroidFishApp', ':CuckooChessEngine', ':CuckooChessApp', ':CuckooChess', ':EngineServer'