/*
* Copyright (c) 2010-2016, Sikuli.org, sikulix.com
* Released under the MIT License.
*
*/
package org.sikuli.scriptrunner;
import org.sikuli.util.JythonHelper;
import java.io.File;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.python.core.PyList;
import org.python.util.PythonInterpreter;
import org.python.util.jython;
import org.sikuli.basics.Debug;
import org.sikuli.basics.FileManager;
import org.sikuli.script.RunTime;
import org.sikuli.script.Runner;
import org.sikuli.script.Sikulix;
/**
* Executes Sikuliscripts written in Python/Jython.
*/
public class JythonScriptRunner implements IScriptRunner {
private static RunTime runTime = ScriptingSupport.runTime;
//<editor-fold defaultstate="collapsed" desc="new logging concept">
private static final String me = "JythonScriptRunner: ";
private int lvl = 3;
private void log(int level, String message, Object... args) {
Debug.logx(level, me + message, args);
}
private void logp(String message, Object... args) {
if (runTime.runningWinApp) {
log(0, message, args);
} else {
System.out.println(String.format(message, args));
}
}
//</editor-fold>
/**
* The PythonInterpreter instance
*/
private PythonInterpreter interpreter = null;
private JythonHelper helper = null;
private int savedpathlen = 0;
private static final String COMPILE_ONLY = "# COMPILE ONLY";
/**
* sys.argv for the jython script
*/
private ArrayList<String> sysargv = new ArrayList<String>();
/**
* The header commands, that are executed before every script
*/
private static String[] SCRIPT_HEADER = new String[]{
"# -*- coding: utf-8 -*- ",
"from sikuli import *",
"use() #resetROI()",
"setShowActions(False)"
};
private ArrayList<String> codeBefore = null;
private ArrayList<String> codeAfter = null;
/**
* CommandLine args
*/
private int errorLine;
private int errorColumn;
private String errorType;
private String errorText;
private int errorClass;
private String errorTrace;
private static final int PY_SYNTAX = 0;
private static final int PY_RUNTIME = 1;
private static final int PY_JAVA = 2;
private static final int PY_UNKNOWN = -1;
private static final String NL = String.format("%n");
//TODO SikuliToHtmlConverter implement in Java
final static InputStream SikuliToHtmlConverter
= JythonScriptRunner.class.getResourceAsStream("/scripts/sikuli2html.py");
static String pyConverter
= FileManager.convertStreamToString(SikuliToHtmlConverter);
private String sikuliLibPath = null;
private boolean isReady = false;
private boolean isCompileOnly = false;
private boolean isFromIDE = false;
@Override
public void init(String[] param) {
if (runTime.runningWinApp) {
runTime.terminate(1, "JythonScriptRunner called in WinApp (packed .exe)");
}
if (isReady) {
return;
}
try {
getInterpreter();
helper = JythonHelper.set(interpreter);
helper.getSysPath();
String fpAPI = null;
String[] possibleJars = new String[]{"sikulixapi", "API/target/classes", "sikulix.jar"};
for (String aJar : possibleJars) {
if (null != (fpAPI = runTime.isOnClasspath(aJar))) {
break;
}
}
if (null == fpAPI) {
runTime.terminate(1, "JythonScriptRunner: no sikulix....jar on classpath");
}
String fpAPILib = new File(fpAPI, "Lib").getAbsolutePath();
helper.putSysPath(fpAPILib, 0);
helper.setSysPath();
helper.addSitePackages();
helper.showSysPath();
interpreter.exec("from sikuli import *");
log(3, "running Jython %s", interpreter.eval("SIKULIX_IS_WORKING").toString());
} catch (Exception ex) {
runTime.terminate(1, "JythonScriptRunner: cannot be initialized:\n%s", ex);
}
isReady = true;
}
/**
* Executes the jythonscript
*
* @param pyFile The file containing the script
* @param fScriptPath The directory containing the images
* @param argv The arguments passed by the --args parameter
* @param forIDE
* @return The exitcode
*/
@Override
public int runScript(File pyFile, File fScriptPath, String[] argv, String[] forIDE) {
if (null == pyFile) {
//run the Python statements from argv (special for setup functional test)
executeScriptHeader(null);
log(lvl, "runPython: running statements");
try {
for (String e : argv) {
interpreter.exec(e);
}
} catch (Exception ex) {
log(-1, "runPython: raised: %s", ex.getMessage());
return -1;
}
return 0;
}
isFromIDE = !(forIDE == null);
if (isFromIDE && forIDE.length > 1 && forIDE[0] != null) {
isCompileOnly = forIDE[0].toUpperCase().equals(COMPILE_ONLY);
}
if (isFromIDE) {
JythonHelper.get().insertSysPath(fScriptPath);
JythonHelper.get().reloadImported();
}
pyFile = new File(pyFile.getAbsolutePath());
fillSysArgv(pyFile, argv);
int exitCode = 0;
if (isFromIDE) {
executeScriptHeader(new String[]{forIDE[0]});
ScriptingSupport.setProject();
exitCode = runPython(pyFile, null, forIDE);
JythonHelper.get().removeSysPath(fScriptPath);
} else {
executeScriptHeader(new String[]{
pyFile.getParent(),
pyFile.getParentFile().getParent()});
exitCode = runPython(pyFile, null, new String[]{pyFile.getParentFile().getAbsolutePath()});
}
return exitCode;
}
private int runPython(File pyFile, String[] stmts, String[] scriptPaths) {
int exitCode = 0;
String stmt = "";
try {
if (scriptPaths != null) {
// TODO implement compile only
if (isCompileOnly) {
log(lvl, "runPython: running COMPILE_ONLY");
interpreter.compile(pyFile.getAbsolutePath());
} else {
String scr;
if (scriptPaths.length > 1) {
scr = FileManager.slashify(scriptPaths[0], true) + scriptPaths[1] + ".sikuli";
log(lvl, "runPython: running script from IDE: \n" + scr);
interpreter.exec("sys.argv[0] = \"" + scr + "\"");
} else {
scr = FileManager.slashify(scriptPaths[0], false);
log(lvl, "runPython: running script: \n%s", scr);
interpreter.exec("sys.argv[0] = \"" + scr + "\"");
}
interpreter.execfile(pyFile.getAbsolutePath());
}
} else {
log(-1, "runPython: invalid arguments");
exitCode = -1;
}
} catch (Exception e) {
java.util.regex.Pattern p
= java.util.regex.Pattern.compile("SystemExit: (-?[0-9]+)");
Matcher matcher = p.matcher(e.toString());
//TODO error stop I18N
if (matcher.find()) {
exitCode = Integer.parseInt(matcher.group(1));
Debug.info("Exit code: " + exitCode);
} else {
//log(-1,_I("msgStopped"));
if (null != pyFile) {
exitCode = findErrorSource(e, pyFile.getAbsolutePath(), scriptPaths);
} else {
Debug.error("runPython: Python exception: %s with %s", e.getMessage(), stmt);
}
if (isFromIDE) {
exitCode *= -1;
} else {
exitCode = 1;
}
}
}
if (System.out.checkError()) {
Sikulix.popError("System.out is broken (console output)!"
+ "\nYou will not see any messages anymore!"
+ "\nSave your work and restart the IDE!", "Fatal Error");
}
return exitCode;
}
private int findErrorSource(Throwable thr, String filename, String[] forIDE) {
errorClass = PY_UNKNOWN;
String err = "ERROR_UNKNOWN";
try {
err = thr.toString();
} catch (Exception ex) {
errorClass = PY_JAVA;
err = thr.getCause().toString();
}
// log(-1,"------------- Traceback -------------\n" + err +
// "------------- Traceback -------------\n");
errorLine = -1;
errorColumn = -1;
errorType = "--UnKnown--";
errorText = "--UnKnown--";
// File ".../mainpy.sikuli/mainpy.py", line 25, in <module> NL func() NL
// File ".../subpy.py", line 4, in func NL 1/0 NL
Pattern pFile = Pattern.compile("File..(.*?\\.py).*?"
+ ",.*?line.*?(\\d+),.*?in(.*?)" + NL + "(.*?)" + NL);
String msg;
Matcher mFile = null;
if (PY_JAVA != errorClass) {
if (err.startsWith("Traceback")) {
Pattern pError = Pattern.compile(NL + "(.*?):.(.*)$");
mFile = pFile.matcher(err);
if (mFile.find()) {
log(lvl + 2, "Runtime error line: " + mFile.group(2)
+ "\n in function: " + mFile.group(3)
+ "\n statement: " + mFile.group(4));
errorLine = Integer.parseInt(mFile.group(2));
errorClass = PY_RUNTIME;
Matcher mError = pError.matcher(err);
if (mError.find()) {
log(lvl + 2, "Error:" + mError.group(1));
log(lvl + 2, "Error:" + mError.group(2));
errorType = mError.group(1);
errorText = mError.group(2);
} else {
//org.sikuli.core.FindFailed: FindFailed: can not find 1352647716171.png on the screen
Pattern pFF = Pattern.compile(": FindFailed: (.*?)" + NL);
Matcher mFF = pFF.matcher(err);
if (mFF.find()) {
errorType = "FindFailed";
errorText = mFF.group(1);
} else {
errorClass = PY_UNKNOWN;
}
}
}
} else if (err.startsWith("SyntaxError")) {
Pattern pLineS = Pattern.compile(", (\\d+), (\\d+),");
java.util.regex.Matcher mLine = pLineS.matcher(err);
if (mLine.find()) {
log(lvl + 2, "SyntaxError error line: " + mLine.group(1));
Pattern pText = Pattern.compile("\\((.*?)\\(");
java.util.regex.Matcher mText = pText.matcher(err);
mText.find();
errorText = mText.group(1) == null ? errorText : mText.group(1);
log(lvl + 2, "SyntaxError: " + errorText);
errorLine = Integer.parseInt(mLine.group(1));
errorColumn = Integer.parseInt(mLine.group(2));
errorClass = PY_SYNTAX;
errorType = "SyntaxError";
}
}
}
msg = "script";
if (forIDE != null) {
if (forIDE.length > 1) {
msg += " [ " + forIDE[1] + " ]";
} else {
msg += " [ " + forIDE[0] + " ]";
}
} else {
msg += " [ UNKNOWN ]";
}
if (errorLine != -1) {
//log(-1,_I("msgErrorLine", srcLine));
msg += " stopped with error in line " + errorLine;
if (errorColumn != -1) {
msg += " at column " + errorColumn;
}
} else {
msg += " stopped with error at line --unknown--";
}
if (errorClass == PY_RUNTIME || errorClass == PY_SYNTAX) {
Debug.error(msg);
Debug.error(errorType + " ( " + errorText + " )");
if (errorClass == PY_RUNTIME) {
errorTrace = findErrorSourceWalkTrace(mFile, filename);
if (errorTrace.length() > 0) {
Debug.error("--- Traceback --- error source first\n"
+ "line: module ( function ) statement \n" + errorTrace
+ "[error] --- Traceback --- end --------------");
}
}
} else {
Debug.error(msg);
Debug.error("Error caused by: %s", err);
}
return errorLine;
}
private String findErrorSourceWalkTrace(Matcher m, String filename) {
Pattern pModule;
if (runTime.runningWindows) {
pModule = Pattern.compile(".*\\\\(.*?)\\.py");
} else {
pModule = Pattern.compile(".*/(.*?)\\.py");
}
String mod;
String modIgnore = "SikuliImporter,Region,";
StringBuilder trace = new StringBuilder();
String telem;
while (m.find()) {
if (m.group(1).equals(filename)) {
mod = "main";
} else {
Matcher mModule = pModule.matcher(m.group(1));
mModule.find();
mod = mModule.group(1);
if (modIgnore.contains(mod + ",")) {
continue;
}
}
telem = m.group(2) + ": " + mod + " ( "
+ m.group(3) + " ) " + m.group(4) + NL;
trace.insert(0, telem);
}
return trace.toString();
}
private void findErrorSourceFromJavaStackTrace(Throwable thr, String filename) {
log(-1, "findErrorSourceFromJavaStackTrace: seems to be an error in the Java API supporting code");
StackTraceElement[] s;
Throwable t = thr;
while (t != null) {
s = t.getStackTrace();
log(lvl + 2, "stack trace:");
for (int i = s.length - 1; i >= 0; i--) {
StackTraceElement si = s[i];
log(lvl + 2, si.getLineNumber() + " " + si.getFileName());
if (si.getLineNumber() >= 0 && filename.equals(si.getFileName())) {
errorLine = si.getLineNumber();
}
}
t = t.getCause();
log(lvl + 2, "cause: " + t);
}
}
@Override
public int runTest(File scriptfile, File imagepath, String[] argv, String[] forIDE) {
log(-1, "runTest: Sikuli Test Feature is not implemented at the moment");
return -1;
}
/**
* {@inheritDoc}
*/
@Override
public int runInteractive(String[] argv) {
String[] jy_args = null;
String[] iargs = {"-i", "-c",
"from sikuli import *; ScriptingSupport.runningInteractive(); use(); "
+ "print \"Hello, this is your interactive Sikuli (rules for interactive Python apply)\\n"
+ "use the UP/DOWN arrow keys to walk through the input history\\n"
+ "help()<enter> will output some basic Python information\\n"
+ "... use ctrl-d to end the session\""};
if (argv != null && argv.length > 0) {
jy_args = new String[argv.length + iargs.length];
System.arraycopy(iargs, 0, jy_args, 0, iargs.length);
System.arraycopy(argv, 0, jy_args, iargs.length, argv.length);
} else {
jy_args = iargs;
}
jython.main(jy_args);
return 0;
}
/**
* {@inheritDoc}
*/
@Override
public String getCommandLineHelp() {
return "You are using the Jython ScriptRunner";
}
/**
* {@inheritDoc}
*/
@Override
public String getInteractiveHelp() {
return "**** this might be helpful ****\n"
+ "-- execute a line of code by pressing <enter>\n"
+ "-- separate more than one statement on a line using ;\n"
+ "-- Unlike the iDE, this command window will not vanish, when using a Sikuli feature\n"
+ " so take care, that all you need is visible on the screen\n"
+ "-- to create an image interactively:\n"
+ "img = capture()\n"
+ "-- use a captured image later:\n"
+ "click(img)";
}
/**
* {@inheritDoc}
*/
@Override
public String getName() {
try {
Class.forName("org.python.util.PythonInterpreter");
} catch (ClassNotFoundException ex) {
return null;
}
return Runner.RPYTHON;
}
/**
* {@inheritDoc}
*/
@Override
public String[] getFileEndings() {
return new String[]{"py"};
}
/**
* {@inheritDoc}
*/
@Override
public String hasFileEnding(String ending) {
for (String suf : getFileEndings()) {
if (suf.equals(ending.toLowerCase())) {
return suf;
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void close() {
if (interpreter != null) {
try {
interpreter.cleanup();
} catch (Exception e) {
}
}
}
/**
* Fills the sysargv list for the Python script
*
* @param pyFile The file containing the script: Has to be passed as first parameter in Python
* @param argv The parameters passed to Sikuli with --args
*/
private void fillSysArgv(File pyFile, String[] argv) {
sysargv = new ArrayList<String>();
if (pyFile != null) {
sysargv.add(pyFile.getAbsolutePath());
}
if (argv != null) {
sysargv.addAll(Arrays.asList(argv));
}
}
private PythonInterpreter getInterpreter() {
if (interpreter == null) {
sysargv.add("");
PythonInterpreter.initialize(System.getProperties(), null, sysargv.toArray(new String[0]));
interpreter = new PythonInterpreter();
}
return interpreter;
}
@Override
public boolean doSomethingSpecial(String action, Object[] args) {
if ("redirect".equals(action)) {
return doRedirect((PipedInputStream[]) args);
} else if ("convertSrcToHtml".equals(action)) {
convertSrcToHtml((String) args[0]);
return true;
} else if ("createRegionForWith".equals(action)) {
args[0] = createRegionForWith(args[0]);
return true;
} else {
return false;
}
}
//TODO revise the before/after concept (to support IDE reruns)
/**
* {@inheritDoc}
*/
@Override
public void execBefore(String[] stmts) {
if (stmts == null) {
codeBefore = null;
return;
}
if (codeBefore == null) {
codeBefore = new ArrayList<String>();
}
codeBefore.addAll(Arrays.asList(stmts));
}
/**
* {@inheritDoc}
*/
@Override
public void execAfter(String[] stmts) {
if (stmts == null) {
codeAfter = null;
return;
}
if (codeAfter == null) {
codeAfter = new ArrayList<String>();
}
codeAfter.addAll(Arrays.asList(stmts));
}
/**
* Executes the defined header for the jython script.
*
* @param syspaths List of all syspath entries
*/
private void executeScriptHeader(String[] syspaths) {
// TODO implement compile only
for (String line : SCRIPT_HEADER) {
log(lvl + 1, "executeScriptHeader: %s", line);
interpreter.exec(line);
}
if (codeBefore != null) {
for (String line : codeBefore) {
interpreter.exec(line);
}
}
if (isCompileOnly) {
return;
}
PyList jyargv = interpreter.getSystemState().argv;
jyargv.clear();
for (String item : sysargv) {
jyargv.add(item);
}
}
private boolean doRedirect(PipedInputStream[] pin) {
PythonInterpreter py = getInterpreter();
Debug.saveRedirected(System.out, System.err);
try {
PipedOutputStream pout = new PipedOutputStream(pin[0]);
PrintStream ps = new PrintStream(pout, true);
if (!ScriptingSupport.systemRedirected) {
System.setOut(ps);
}
py.setOut(ps);
} catch (Exception e) {
log(-1, "%s: redirect STDOUT: %s", getName(), e.getMessage());
return false;
}
try {
PipedOutputStream eout = new PipedOutputStream(pin[1]);
PrintStream eps = new PrintStream(eout, true);
if (!ScriptingSupport.systemRedirected) {
System.setErr(eps);
}
py.setErr(eps);
} catch (Exception e) {
log(-1, "%s: redirect STDERR: %s", getName(), e.getMessage());
return false;
}
return true;
}
private void convertSrcToHtml(String bundle) {
PythonInterpreter py = new PythonInterpreter();
log(lvl, "Convert Sikuli source code " + bundle + " to HTML");
py.set("local_convert", true);
py.set("sikuli_src", bundle);
py.exec(pyConverter);
}
private Object createRegionForWith(Object reg) {
return null;
}
}