/* DroidFish - An Android chess program. Copyright (C) 2011-2014 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.droidfish.engine; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.channels.FileChannel; import org.petero.droidfish.EngineOptions; import org.petero.droidfish.R; import android.content.Context; /** Engine running as a process started from an external resource. */ public class ExternalEngine extends UCIEngineBase { protected final Context context; private File engineFileName; private final Report report; private Process engineProc; private Thread startupThread; private Thread exitThread; private Thread stdInThread; private Thread stdErrThread; private LocalPipe inLines; private boolean startedOk; private boolean isRunning; public ExternalEngine(Context context, String engine, Report report) { this.context = context; this.report = report; engineFileName = new File(engine); engineProc = null; startupThread = null; exitThread = null; stdInThread = null; stdErrThread = null; inLines = new LocalPipe(); startedOk = false; isRunning = false; } protected String internalSFPath() { return context.getFilesDir().getAbsolutePath() + "/internal_sf"; } /** @inheritDoc */ @Override protected void startProcess() { try { File exeDir = new File(context.getFilesDir(), "engine"); exeDir.mkdir(); String exePath = copyFile(engineFileName, exeDir); chmod(exePath); cleanUpExeDir(exeDir, exePath); ProcessBuilder pb = new ProcessBuilder(exePath); synchronized (EngineUtil.nativeLock) { engineProc = pb.start(); } startupThread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { return; } if (startedOk && isRunning && !isUCI) report.reportError(context.getString(R.string.uci_protocol_error)); } }); startupThread.start(); exitThread = new Thread(new Runnable() { @Override public void run() { try { Process ep = engineProc; if (ep != null) ep.waitFor(); isRunning = false; if (!startedOk) report.reportError(context.getString(R.string.failed_to_start_engine)); else { report.reportError(context.getString(R.string.engine_terminated)); } } catch (InterruptedException e) { } } }); exitThread.start(); // Start a thread to read stdin stdInThread = new Thread(new Runnable() { @Override public void run() { Process ep = engineProc; if (ep == null) return; InputStream is = ep.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr, 8192); String line; try { boolean first = true; while ((line = br.readLine()) != null) { if ((ep == null) || Thread.currentThread().isInterrupted()) return; synchronized (inLines) { inLines.addLine(line); if (first) { startedOk = true; isRunning = true; first = false; } } } } catch (IOException e) { } inLines.close(); } }); stdInThread.start(); // Start a thread to ignore stderr stdErrThread = new Thread(new Runnable() { @Override public void run() { byte[] buffer = new byte[128]; while (true) { Process ep = engineProc; if ((ep == null) || Thread.currentThread().isInterrupted()) return; try { int len = ep.getErrorStream().read(buffer, 0, 1); if (len < 0) break; } catch (IOException e) { return; } } } }); stdErrThread.start(); } catch (IOException ex) { report.reportError(ex.getMessage()); } } /** Remove all files except exePath from exeDir. */ private void cleanUpExeDir(File exeDir, String exePath) { try { exePath = new File(exePath).getCanonicalPath(); File[] files = exeDir.listFiles(); if (files == null) return; for (File f : files) { if (!f.getCanonicalPath().equals(exePath)) f.delete(); } new File(context.getFilesDir(), "engine.exe").delete(); } catch (IOException e) { } } private int hashMB = -1; private String gaviotaTbPath = ""; private String syzygyPath = ""; private boolean optionsInitialized = false; /** @inheritDoc */ @Override public void initOptions(EngineOptions engineOptions) { super.initOptions(engineOptions); hashMB = getHashMB(engineOptions.hashMB); setOption("Hash", hashMB); syzygyPath = engineOptions.getEngineRtbPath(false); setOption("SyzygyPath", syzygyPath); gaviotaTbPath = engineOptions.getEngineGtbPath(false); setOption("GaviotaTbPath", gaviotaTbPath); optionsInitialized = true; } @Override protected File getOptionsFile() { return new File(engineFileName.getAbsolutePath() + ".ini"); } /** Reduce too large hash sizes. */ private final int getHashMB(int hashMB) { if (hashMB > 16) { int maxMem = (int)(Runtime.getRuntime().maxMemory() / (1024*1024)); if (maxMem < 16) maxMem = 16; if (hashMB > maxMem) hashMB = maxMem; } return hashMB; } /** @inheritDoc */ @Override public boolean optionsOk(EngineOptions engineOptions) { if (!optionsInitialized) return true; if (hashMB != getHashMB(engineOptions.hashMB)) return false; if (hasOption("gaviotatbpath") && !gaviotaTbPath.equals(engineOptions.getEngineGtbPath(false))) return false; if (hasOption("syzygypath") && !syzygyPath.equals(engineOptions.getEngineRtbPath(false))) return false; return true; } /** @inheritDoc */ @Override public void setStrength(int strength) { } /** @inheritDoc */ @Override public String readLineFromEngine(int timeoutMillis) { String ret = inLines.readLine(timeoutMillis); if (ret == null) return null; if (ret.length() > 0) { // System.out.printf("Engine -> GUI: %s\n", ret); } return ret; } /** @inheritDoc */ @Override public void writeLineToEngine(String data) { // System.out.printf("GUI -> Engine: %s\n", data); data += "\n"; try { Process ep = engineProc; if (ep != null) ep.getOutputStream().write(data.getBytes()); } catch (IOException e) { } } /** @inheritDoc */ @Override public void shutDown() { if (startupThread != null) startupThread.interrupt(); if (exitThread != null) exitThread.interrupt(); super.shutDown(); if (engineProc != null) engineProc.destroy(); engineProc = null; if (stdInThread != null) stdInThread.interrupt(); if (stdErrThread != null) stdErrThread.interrupt(); } protected String copyFile(File from, File exeDir) throws IOException { File to = new File(exeDir, "engine.exe"); new File(internalSFPath()).delete(); if (to.exists() && (from.length() == to.length()) && (from.lastModified() == to.lastModified())) return to.getAbsolutePath(); if (to.exists()) to.delete(); to.createNewFile(); FileChannel inFC = null; FileChannel outFC = null; try { inFC = new FileInputStream(from).getChannel(); outFC = new FileOutputStream(to).getChannel(); long cnt = outFC.transferFrom(inFC, 0, inFC.size()); if (cnt < inFC.size()) throw new IOException("File copy failed"); } finally { if (inFC != null) { try { inFC.close(); } catch (IOException ex) {} } if (outFC != null) { try { outFC.close(); } catch (IOException ex) {} } to.setLastModified(from.lastModified()); } return to.getAbsolutePath(); } private final void chmod(String exePath) throws IOException { if (!EngineUtil.chmod(exePath)) throw new IOException("chmod failed"); } }