package plugins.ronline;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.molgenis.util.DetectOS;
public class RProcess implements Runnable
{
private File outputFile;
private long outputFileLength;
private long timeSinceLastResponse; // seconds
private long timeOut; // seconds
private BufferedReader bis;
private BufferedOutputStream bos;
private boolean quit;
private List<String> startupMessage;
private BufferedInputStream bisError;
InputStream isError;
String requestDone = "\\\\r3qu35tc0mpl3t3\\\\";
String requestDoneOutputMarker = "[1] \"\\\\r3qu35tc0mpl3t3\\\\\"";
String requestDonePrint = "print(\"" + requestDone + "\");\n";
public List<String> getStartupMessage()
{
return startupMessage;
}
/**
* Instantiates a new RProcess with timeOut in seconds. If there is no
* output activity for this period of time, the process will quit.
*
* Usage example:
*
* RProcess rp = new RProcess(1); new Thread(rp).start();
* rp.execute("1+pi"); rp.quit();
*
* @param timeOut
* @throws Exception
*/
public RProcess(long timeOut) throws Exception
{
timeSinceLastResponse = 0;
this.timeOut = timeOut;
quit = false;
createOutputFile();
Process process = Runtime.getRuntime().exec(startupCommand());
OutputStream os = process.getOutputStream();
isError = process.getErrorStream(); // !!!!
bisError = new BufferedInputStream(isError); // !!!!
bos = new BufferedOutputStream(os);
FileReader is = new FileReader(outputFile);
bis = new BufferedReader(is);
outputFileLength = outputFile.length();
List<String> startupMessage = retrieveRawResults("Type 'q()' to quit R.");
this.startupMessage = startupMessage;
initErrorHandling();
retrieveRawResults(requestDoneOutputMarker);
}
/**
* Initiate error handling functions, needed to keep the pipe from breaking.
*
* @throws IOException
* @throws InterruptedException
*/
private void initErrorHandling() throws IOException, InterruptedException
{
List<String> commands = new ArrayList<String>();
commands.add("merrorfun <- function(ex) {\n");
commands.add(" cat(\"Error: \", ex[[1]], \"\\n\", sep=\"\")\n");
commands.add("}\n");
commands.add("\n");
commands.add("mfinallyfun <- function(ex) {\n");
commands.add(" cat(\"Prevented pipebreak\\n\")\n");
commands.add("}\n");
commands.add(requestDonePrint);
for (String command : commands)
{
bos.write(command.getBytes());
}
bos.flush();
}
private List<String> retrieveRawResults(String requestEndMarker) throws InterruptedException, IOException
{
List<String> results = new ArrayList<String>();
boolean requestCompleted = false;
while (!requestCompleted)
{
while (outputFile.length() == outputFileLength)
{
Thread.sleep(15);
// System.out.print(".");
}
outputFileLength = outputFile.length();
String line;
while ((line = bis.readLine()) != null)
{
if (line.equals(requestEndMarker))
{
requestCompleted = true;
break;
}
else
{
results.add(line);
}
}
}
return results;
}
/**
* Helper function to create a suitable temporary file.
*
* @throws Exception
*/
private void createOutputFile() throws Exception
{
outputFile = new File(System.getProperty("java.io.tmpdir") + File.separator + "r_output_tmp_"
+ System.nanoTime() + ".txt");
if (outputFile.exists())
{
boolean delete = outputFile.delete();
if (!delete)
{
throw new Exception("Deletion of tmp file " + outputFile.getAbsolutePath() + " failed");
}
}
boolean create = outputFile.createNewFile();
if (!create)
{
throw new Exception("Creation of tmp file " + outputFile.getAbsolutePath() + " failed");
}
}
/**
* Helper function to perform startup depending on the underlying OS.
*
* @return
* @throws Exception
*/
private String[] startupCommand() throws Exception
{
String osD = DetectOS.getOS();
if (osD.equals("mac") || osD.equals("unix"))
{
String command = "R --vanilla >> " + outputFile.getAbsolutePath();
String[] cmd =
{ "/bin/sh", "-c", command };
return cmd;
}
else if (osD.equals("windowslegacy"))
{
String command = "R --vanilla >> " + outputFile.getAbsolutePath() + "";
String[] cmd =
{ "command.com /c set", command };
return cmd;
}
else if (osD.equals("windows"))
{
String command = "R --vanilla >> " + outputFile.getAbsolutePath() + "";
// String[] cmd = { "cmd.exe /c set", command };
String[] cmd =
{ command }; // zegt danny
return cmd;
}
else
{
throw new Exception("Operating system '" + System.getProperty("os.name") + "' is not supported");
}
}
/**
* Run the process and keep it alive until quit() is called or timeOut is
* reached.
*/
@Override
public void run()
{
while (!quit)
{
try
{
Thread.sleep(1000);
timeSinceLastResponse += 1;
if (timeSinceLastResponse > timeOut)
{
System.out.println("RProcess timeout (" + timeSinceLastResponse
+ " seconds passed since last response, timeout set to " + timeOut + " seconds), quitting");
quit();
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
/**
* Execute multiple commands
*
* @param commands
* @return
* @throws Exception
*/
public List<String> executeMulti(List<String> commands) throws Exception
{
List<String> result = new ArrayList<String>();
for (String command : commands)
{
result.addAll(execute(command));
}
return result;
}
/**
* Execute a single command
*
* @param command
* @return
* @throws Exception
*/
public List<String> execute(String command) throws Exception
{
if (quit)
{
throw new Exception("RProcess is no longer running and as such, no longer accepting new commands.");
}
// escape \ to \\
command = command.replace("\\", "\\\\");
// escape " to \"
command = command.replace("\"", "\\\"");
command = "tryCatch({eval(parse(text=\"" + command + "\"))}, error = merrorfun, finally = mfinallyfun );\n";
command += "print(\"" + requestDone + "\");\n";
bos.write(command.getBytes());
bos.flush();
List<String> result = retrieveRawResults(requestDoneOutputMarker);
if (result.size() > 2)
{
// Result looks like: command, answer(s), prompt. Get answer(s) as
// response.
result = result.subList(1, result.size() - 1);
}
else if (result.size() == 2)
{
// Result looks like: command, prompt. Empty list as response.
result = new ArrayList<String>();
}
else
{
// Bad result. Empty list as response and throw error.
result = new ArrayList<String>();
String error = checkForErrors();
if (error.length() > 0)
{
throw new Exception("Bad result: " + error);
}
}
return result;
}
private String checkForErrors() throws IOException, Exception
{
if (bisError.available() > 0)
{
byte[] buff = new byte[bisError.available()];
bisError.read(buff);
String error = new String(buff);
return error;
}
return "";
}
/**
* Allows the outside to see if the process is still running or not.
*
* @return
*/
public boolean isRunning()
{
return !quit;
}
/**
* Quits the thread. The in- and outputstream close() and file deletion
* statements are attempted after the quit is set to true, because it's more
* important to end the thread than to properly close the streams. Ofcourse,
* an exception is always thrown when something goes wrong. (ie. trying to
* close the streams)
*
* @throws Exception
*/
public void quit() throws Exception
{
quit = true;
bis.close();
bisError.close();
bos.close();
boolean delete = outputFile.delete();
if (!delete)
{
throw new Exception("Deletion of tmp file " + outputFile.getAbsolutePath() + " failed");
}
}
}