Create EngineServer Java program

This program can be run on a PC and works as an engine server that
DroidFish can connect to using its "network engine" feature.
This commit is contained in:
Peter Osterlund 2019-05-18 21:20:27 +02:00
parent 490bacfce0
commit fa9cf93245
8 changed files with 694 additions and 1 deletions

1
EngineServer/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

15
EngineServer/build.gradle Normal file
View File

@ -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"

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.petero.engineserver;
public interface ErrorHandler {
void reportError(String title, String message);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
});
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<String> 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());
}
}

View File

@ -1 +1 @@
include ':DroidFishApp', ':CuckooChessEngine', ':CuckooChessApp', ':CuckooChess' include ':DroidFishApp', ':CuckooChessEngine', ':CuckooChessApp', ':CuckooChess', ':EngineServer'