/*
* Copyright (c) 2010-2016, Sikuli.org, sikulix.com
* Released under the MIT License.
*
*/
package org.sikuli.script;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
import java.util.regex.Matcher;
import javax.script.ScriptEngine;
import org.sikuli.basics.Debug;
/**
* EXPERIMENTAL --- NOT official API<br>
* not as is in version 2
*/
public class RunServer {
private static ServerSocket server = null;
private static PrintWriter out = null;
private static Scanner in = null;
private static boolean isHandling = false;
private static boolean shouldStop = false;
//TODO set loglevel at runtime
private static int logLevel = 0;
private static void log(int lvl, String message, Object... args) {
if (lvl < 0 || lvl >= logLevel) {
System.out.println((lvl < 0 ? "[error] " : "[info] ") +
String.format("RunServer: " + message, args));
}
}
private static void log(String message, Object... args) {
log(0, message, args);
}
private RunServer() {
}
static File isRunning = null;
static FileOutputStream isRunningFile = null;
public static boolean run(String[] args) {
if (args == null) {
args = new String[0];
}
String userArgs = "";
for (String userArg : RunTime.get().getArgs()) {
userArgs += userArg + " ";
}
if (!userArgs.isEmpty()) {
userArgs = "\nWith User parameters: " + userArgs;
}
int port = getPort(args.length > 0 ? args[0] : null);
try {
try {
if (port > 0) {
log(3, "Starting: trying port: %d %s", port, userArgs);
server = new ServerSocket(port);
}
} catch (Exception ex) {
log(-1, "Starting: " + ex.getMessage());
}
if (server == null) {
log(-1, "could not be started");
return false;
}
String theIP = InetAddress.getLocalHost().getHostAddress();
String theServer = String.format("%s %d", theIP, port);
isRunning = new File(RunTime.get().fSikulixStore, "RunServer.txt");
try {
isRunning.createNewFile();
isRunningFile = new FileOutputStream(isRunning);
if (null == isRunningFile.getChannel().tryLock()) {
log(-1, "Terminating on FatalError: already running");
return false;
}
isRunningFile.write(theServer.getBytes());
} catch (Exception ex) {
log(-1, "Terminating on FatalError: cannot access to lock for/n" + isRunning);
return false;
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
log(3, "final cleanup");
if (isRunning != null) {
try {
isRunningFile.close();
} catch (IOException ex) {
}
isRunning.delete();
}
}
});
while (true) {
log("now waiting on port: %d at %s", port, theIP);
Socket socket = server.accept();
out = new PrintWriter(socket.getOutputStream());
in = new Scanner(socket.getInputStream());
HandleClient client = new HandleClient(socket);
isHandling = true;
while (true) {
if (socket.isClosed()) {
shouldStop = client.getShouldStop();
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
}
}
if (shouldStop) {
break;
}
}
} catch (Exception e) {
}
if (!isHandling) {
log(-1, "start handling not possible: " + port);
return false;
}
log("now stopped on port: " + port);
return true;
}
private static int getPort(String p) {
int port;
int pDefault = 50001;
if (p != null) {
try {
port = Integer.parseInt(p);
} catch (NumberFormatException ex) {
log(-1, "given port not useable: %s --- using default", p);
return pDefault;
}
} else {
return pDefault;
}
if (port < 1024) {
port += pDefault;
}
return port;
}
static ScriptEngine jsRunner = null;
static File scriptFolder = null;
static String scriptFolderNet = null;
static File imageFolder = null;
static String imageFolderNet = null;
private static class HandleClient implements Runnable {
private volatile boolean keepRunning;
private boolean shouldKeep = false;
Thread thread;
Socket socket;
Boolean shouldStop = false;
public HandleClient(Socket sock) {
init(sock);
}
private void init(Socket sock) {
socket = sock;
if (in == null || out == null) {
RunServer.log(-1, "communication not established");
System.exit(1);
}
thread = new Thread(this, "HandleClient");
keepRunning = true;
thread.start();
}
public boolean getShouldStop() {
return shouldStop;
}
boolean isHTTP = false;
String request;
String rCommand;
String rRessource;
String rVersion = "HTTP/1.1";
String rQuery;
String[] rArgs;
String rMessage = "";
String rStatus;
String rStatusOK = "200 OK";
String rStatusBadRequest = "400 Bad Request";
String rStatusNotFound = "404 Not Found";
String rStatusServerError = "500 Internal Server Error";
String rStatusServiceNotAvail = "503 Service Unavailable";
Object evalReturnObject;
String runTypeJS = "JavaScript";
String runTypePY = "Python";
String runTypeRB = "Ruby";
String runType = runTypeJS;
@Override
public void run() {
Debug.on(3);
RunServer.log("now handling client: " + socket);
while (keepRunning) {
try {
String inLine = in.nextLine();
if (inLine != null) {
if (!isHTTP) {
RunServer.log("processing: <%s>", inLine);
}
boolean success = true;
if (inLine.startsWith("GET /") && inLine.contains("HTTP/")) {
isHTTP = true;
request = inLine;
continue;
}
if (isHTTP) {
if (!inLine.isEmpty()) {
continue;
}
}
if (!isHTTP) {
request = "GET /" + inLine + " HTTP/1.1";
}
success = checkRequest(request);
if (success) {
// STOP
if (rCommand.contains("STOP")) {
rMessage = "stopping server";
shouldStop = true;
shouldKeep = false;
} else if (rCommand.contains("EXIT")) {
rMessage = "stopping client";
shouldKeep = false;
// START
} else if (rCommand.startsWith("START")) {
runType = runTypeJS;
if (rCommand.length() > 5) {
if ("P".equals(rCommand.substring(5, 6))) {
runType = runTypePY;
} else if ("R".equals(rCommand.substring(5, 6))) {
runType = runTypeRB;
}
}
success = startRunner(runType, null, null);
rMessage = "startRunner for: " + runType;
if (!success) {
rMessage = "startRunner: not possible for: " + runType;
rStatus = rStatusServiceNotAvail;
}
// SCRIPTS
} else if (rCommand.startsWith("SCRIPTS")) {
if (rRessource.isEmpty()) {
rMessage = "no scriptFolder given ";
rStatus = rStatusBadRequest;
success = false;
} else {
scriptFolder = getFolder(rRessource);
if (scriptFolder.getPath().startsWith("__NET/")) {
scriptFolderNet = "http://" + scriptFolder.getPath().substring(6);
rMessage = "scriptFolder now: " + scriptFolderNet;
} else {
scriptFolderNet = null;
rMessage = "scriptFolder now: " + scriptFolder.getAbsolutePath();
if (!scriptFolder.exists()) {
rMessage = "scriptFolder not found: " + scriptFolder.getAbsolutePath();
rStatus = rStatusNotFound;
success = false;
}
}
}
// IMAGES
} else if (rCommand.startsWith("IMAGES")) {
String asImagePath;
if (rRessource.isEmpty()) {
rMessage = "no imageFolder given ";
rStatus = rStatusBadRequest;
success = false;
} else {
imageFolder = getFolder(rRessource);
if (imageFolder.getPath().startsWith("__NET/")) {
imageFolderNet = "http://" + imageFolder.getPath().substring(6);
rMessage = "imageFolder now: " + imageFolderNet;
asImagePath = imageFolderNet;
} else {
String fpGiven = imageFolder.getAbsolutePath();
if (!imageFolder.exists()) {
imageFolder = new File(imageFolder.getAbsolutePath() + ".sikuli");
if (!imageFolder.exists()) {
rMessage = "imageFolder not found: " + fpGiven;
rStatus = rStatusNotFound;
success = false;
}
}
asImagePath = imageFolder.getAbsolutePath();
}
rMessage = "imageFolder now: " + asImagePath;
ImagePath.add(asImagePath);
}
// RUN
} else if (rCommand.startsWith("RUN")) {
String script = rRessource;
File fScript = null;
File fScriptScript = null;
if (scriptFolderNet != null) {
rMessage = "runScript from net not yet supported";
rStatus = rStatusServiceNotAvail;
success = false;
}
if (success) {
Debug.log("Using script folder: " + RunServer.scriptFolder);
fScript = new File(RunServer.scriptFolder, script);
if (!fScript.exists()) {
if (script.endsWith(".sikuli")) {
script = script.replace(".sikuli", "");
} else {
script = script + ".sikuli";
}
fScript = new File(scriptFolder, script);
}
String scriptScript = script.replace(".sikuli", "");
fScriptScript = new File(fScript, scriptScript + ".js");
success = fScriptScript.exists();
if (!success) {
fScriptScript = new File(fScript, scriptScript + ".py");
success = fScript.exists() && fScriptScript.exists();
if (!success) {
RunServer.log("Script folder path: " + fScript.getAbsolutePath());
RunServer.log("Script file path: " + fScriptScript.getAbsolutePath());
rMessage = "runScript: script not found, not valid or not supported "
+ fScriptScript.toString();
}
runType = runTypePY;
}
}
if (success) {
ImagePath.setBundlePath(fScript.getAbsolutePath());
List<String> args = new ArrayList<String>();
if (this.rQuery != null && this.rQuery.length() > 0) {
String[] params = this.rQuery.split("[;&]");
for (String param : params) {
String[] pair = param.split("[=]");
if (pair != null && pair.length == 2) {
// Needs both a variable name and value, and supports repeated parameters
String arg = String.format("--%1$s=%2$s", pair[0], pair[1]);
args.add(arg);
}
}
}
success = this.startRunner(this.runType, fScript, fScriptScript, args.toArray(new String[0]));
}
} else if (rCommand.startsWith("EVAL")) {
if (jsRunner != null) {
String line = rQuery;
try {
evalReturnObject = jsRunner.eval(line);
rMessage = "runStatement: returned: "
+ (evalReturnObject == null ? "null" : evalReturnObject.toString());
success = true;
} catch (Exception ex) {
rMessage = "runStatement: raised exception on eval: " + ex.toString();
success = false;
}
} else {
rMessage = "runStatement: not possible --- no runner";
rStatus = rStatusServiceNotAvail;
success = false;
}
}
}
String retVal = "";
if (isHTTP) {
retVal = "HTTP/1.1 " + rStatus;
String state = (success ? "PASS " : "FAIL ") + rStatus.substring(0,3) + " ";
retVal += "\r\n\r\n" + state + rMessage + "\r";
} else {
retVal = (success ? "isok:\n" : "fail:\n") + rMessage + "\n###+++###";
}
try {
out.println(retVal);
out.flush();
RunServer.log("returned:\n" + retVal.replace("###+++###", ""));
} catch (Exception ex) {
RunServer.log(-1, "write response: Exception:\n" + ex.getMessage());
}
stopRunning();
}
} catch (Exception ex) {
RunServer.log(-1, "while processing: Exception:\n" + ex.getMessage());
shouldKeep = false;
stopRunning();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
shouldKeep = false;
stopRunning();
}
}
public void stopRunning() {
if (!shouldKeep) {
in.close();
out.close();
try {
socket.close();
} catch (IOException ex) {
RunServer.log(-1, "fatal: socket not closeable");
System.exit(1);
}
keepRunning = false;
}
}
private File getFolder(String path) {
File aFolder = new File(path);
Debug.log("Original path: " + aFolder);
if (path.toLowerCase().startsWith("/home/")) {
path = path.substring(6);
aFolder = new File(RunTime.get().fUserDir, path);
} else if (path.toLowerCase().startsWith("/net/")) {
path = "__NET/" + path.substring(5);
aFolder = new File(path);
} else if (RunTime.get().runningWindows) {
Matcher matcher = java.util.regex.Pattern.compile("(?ix: ^ (?: / ([a-z]) [:]? /) (.*) $)").matcher(path);
// Assume specified drive exists or fallback on the default/required drive
String newPath = matcher.matches() ? matcher.replaceAll("$1:/$2") : ("c:" + path);
aFolder = new File(newPath);
}
Debug.log("Transformed path: " + aFolder);
return aFolder;
}
private boolean checkRequest(String request) {
shouldKeep = false;
rCommand = "NOOP";
rMessage = "invalid: " + request;
rStatus = rStatusBadRequest;
String[] parts = request.split("\\s");
if (parts.length != 3 || !"GET".equals(parts[0]) || !parts[1].startsWith("/")) {
return false;
}
if (!rVersion.equals(parts[2])) {
return false;
}
String cmd = parts[1].substring(1);
if (cmd.startsWith("X")) {
cmd = cmd.substring(1);
shouldKeep = true;
}
parts = cmd.split("\\?");
cmd = parts[0];
rQuery = "";
if (parts.length > 1) {
rQuery = parts[1];
}
parts = cmd.split("/");
if (!"START,STARTP,STOP,EXIT,SCRIPTS,IMAGES,RUN,EVAL,".contains((parts[0]+",").toUpperCase())) {
rMessage = "invalid command: " + request;
return false;
}
rCommand = parts[0].toUpperCase();
rMessage = "";
rStatus = rStatusOK;
rRessource = "";
if (parts.length > 1) {
rRessource = cmd.substring(rCommand.length());
}
return true;
}
private boolean startRunner(String runType, File fScript, File fScriptScript) {
return this.startRunner(runType, fScript, fScriptScript, new String[0]);
}
private boolean startRunner(String runType, File fScript, File fScriptScript, String[] args) {
if (runTypeJS.equals(runType)) {
if (jsRunner == null) {
try {
jsRunner = Runner.initjs();
String prolog = "";
prolog = Runner.prologjs(prolog);
prolog = Runner.prologjs(prolog);
jsRunner.eval(prolog);
} catch (Exception ex) {
rMessage = "startRunner JavaScript: not possible";
rStatus = rStatusServiceNotAvail;
return false;
}
}
if (fScript == null) {
return true;
}
if (jsRunner != null) {
try {
evalReturnObject = jsRunner.eval(new java.io.FileReader(fScriptScript));
rMessage = "runScript: returned: "
+ (evalReturnObject == null ? "null" : evalReturnObject.toString());
return evalReturnObject != null;
} catch (Exception ex) {
rMessage = "runScript: script raised exception on run: " + ex.toString();
return false;
}
} else {
return false;
}
} else if (runTypePY.equals(runType)) {
Integer retval = 0;
if (!Runner.initpy()) {
retval = -1;
}
if (fScript != null && retval == 0) {
// Arguments are passed to Python in the long format: --name=value
evalReturnObject = Runner.run(fScript.getAbsolutePath(), args);
try {
retval = Integer.parseInt(evalReturnObject.toString());
if (retval == -999) {
retval = 0;
}
} catch (Exception ex) {
retval = 0;
}
}
if (retval < 0) {
rMessage = "startRunner Python: not possible or crashed with exception";
rStatus = rStatusServiceNotAvail;
return false;
}
if (fScript != null) {
rMessage = "runScript: returned: " + retval.toString();
}
}
return true;
}
}
}