/* * Copyright 2010-2016, Sikuli.org, sikulix.com * Released under the MIT License. * * WhoIsWho 2014 */ package org.sikuli.scriptrunner; //import java.io.File; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jruby.CompatVersion;import org.jruby.Ruby; import org.jruby.RubyInstanceConfig.CompileMode; import org.jruby.RubyProc; import org.jruby.embed.LocalContextScope; import org.jruby.embed.ScriptingContainer; import org.jruby.javasupport.JavaEmbedUtils.EvalUnit; import org.jruby.javasupport.JavaUtil; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.sikuli.basics.Debug; import org.sikuli.basics.FileManager; import org.sikuli.script.RunTime; import org.sikuli.script.Runner; public class JRubyScriptRunner implements IScriptRunner { static RunTime sxRunTime = RunTime.get(); //<editor-fold defaultstate="collapsed" desc="new logging concept"> private static final String me = "JRubyScriptRunner: "; private int lvl = 3; private void log(int level, String message, Object... args) { Debug.logx(level, me + message, args); } //</editor-fold> /** * The ScriptingContainer instance */ private static ScriptingContainer interpreter = null; private static int savedpathlen = 0; private static final String COMPILE_ONLY = "# COMPILE ONLY"; /** * sys.argv for the jruby script */ private static ArrayList<String> sysargv = null; /** * The header commands, that are executed before every script */ private final static String SCRIPT_HEADER = "# coding: utf-8\n" + "require 'Lib/sikulix'\n" + "include Sikulix\n"; private static ArrayList<String> codeBefore = null; private static 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 String sikuliLibPath; private boolean isFromIDE; private boolean isCompileOnly; private static Ruby runtime; private static ThreadContext context; @Override public void init(String[] args) { //TODO classpath and other path handlings sikuliLibPath = sxRunTime.fSikulixLib.getAbsolutePath(); } @Override public int runScript(File scriptfile, File imagedirectory, String[] scriptArgs, String[] forIDE) { if (null == scriptfile) { //run the Ruby statements from argv (special for setup functional test) fillSysArgv(null, null); createScriptingContainer(); executeScriptHeader(new String[0]); return runRuby(null, scriptArgs, null); } scriptfile = new File(scriptfile.getAbsolutePath()); fillSysArgv(scriptfile, scriptArgs); createScriptingContainer(); int exitCode = 0; isFromIDE = ! (forIDE == null); if (isFromIDE && forIDE.length > 1 && forIDE[0] != null ) { isCompileOnly = forIDE[0].toUpperCase().equals(COMPILE_ONLY); } if (forIDE == null) { executeScriptHeader(new String[]{ scriptfile.getParentFile().getAbsolutePath(), scriptfile.getParentFile().getParentFile().getAbsolutePath()}); exitCode = runRuby(scriptfile, null, new String[]{scriptfile.getParentFile().getAbsolutePath()}); } else { executeScriptHeader(new String[]{forIDE[0]}); exitCode = runRuby(scriptfile, null, forIDE); } log(lvl + 1, "runScript: at exit: path:"); for (Object p : interpreter.getLoadPaths()) { log(lvl + 1, "runScript: " + p.toString()); } log(lvl + 1, "runScript: at exit: --- end ---"); return exitCode; } @Override public int runTest(File scriptfile, File imagedirectory, String[] scriptArgs, String[] forIDE) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public int runInteractive(String[] scriptArgs) { fillSysArgv(null, scriptArgs); String[] args = null; String[] iargs = {/*"-i", "-c",*/ "require 'irb'\n" + "ScriptRunner.runningInteractive();\n" + "print \"Hello, this is your interactive Sikuli (rules for interactive Ruby apply)\\n" + "use the UP/DOWN arrow keys to walk through the input history\\n" + "help()<enter> will output some basic Ruby information\\n" + "... use ctrl-d to end the session\"\n" + "IRB.start(__FILE__)\n" }; if (scriptArgs != null && scriptArgs.length > 0) { args = new String[scriptArgs.length + iargs.length]; System.arraycopy(iargs, 0, args, 0, iargs.length); System.arraycopy(scriptArgs, 0, args, iargs.length, scriptArgs.length); } else { args = iargs; } StringBuilder buffer = new StringBuilder(); for (String e : args) { buffer.append(e); } createScriptingContainer(); executeScriptHeader(new String[0]); interpreter.runScriptlet(buffer.toString()); return 0; } @Override public String getCommandLineHelp() { return "You are using the JRuby ScriptRunner"; } @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)"; } @Override public String getName() { try { Class.forName("org.jruby.embed.ScriptingContainer"); } catch (ClassNotFoundException ex) { return null; } return Runner.RRUBY; } @Override public String[] getFileEndings() { return new String[]{"rb"}; } @Override public String hasFileEnding(String ending) { for (String suf : getFileEndings()) { if (suf.equals(ending.toLowerCase())) { return suf; } } return null; } @Override public void close() { if (interpreter != null) { interpreter.terminate(); interpreter = null; } } @Override public boolean doSomethingSpecial(String action, Object[] args) { if ("redirect".equals(action)) { return doRedirect((PipedInputStream[]) args); } else if ("checkCallback".equals(action)) { return checkCallback(args); } else if ("runLoggerCallback".equals(action)) { return runLoggerCallback(args); } else if ("runObserveCallback".equals(action)) { return runObserveCallback(args); } else if ("runCallback".equals(action)) { return runCallback(args); } else { return false; } } private boolean checkCallback(Object[] args) { log(-1, "checkCallback: no implementation yet"); return false; } private boolean runLoggerCallback(Object[] args) { log(-1, "runLoggerCallback: no implementation yet"); return false; } private boolean runObserveCallback(Object[] args) { if (runtime == null) { runtime = ((RubyProc) args[0]).getRuntime(); context = runtime.getCurrentContext(); } IRubyObject[] pargs; try { pargs = new IRubyObject[] {JavaUtil.convertJavaToRuby(runtime, args[1])}; ((RubyProc) args[0]).call(context, pargs); } catch (Exception ex) { log(-1, "runObserveCallback: jruby invoke: %s\n%s\n%s", args[0].getClass(),args[0], ex.getMessage()); } return true; } private boolean runCallback(Object[] args) { log(-1, "runCallback: no implementation yet"); return false; } @Override public void execBefore(String[] stmts) { if (stmts == null) { codeBefore = null; return; } if (codeBefore == null) { codeBefore = new ArrayList<String>(); } codeBefore.addAll(Arrays.asList(stmts)); } @Override public void execAfter(String[] stmts) { if (stmts == null) { codeAfter = null; return; } if (codeAfter == null) { codeAfter = new ArrayList<String>(); } codeAfter.addAll(Arrays.asList(stmts)); } private int runRuby(File ruFile, String[] stmts, String[] scriptPaths) { int exitCode = 0; String stmt = ""; boolean fromIDE = false; String filename = "<script>"; try { if (null == ruFile) { log(lvl, "runRuby: running statements"); StringBuilder buffer = new StringBuilder(); for (String e : stmts) { buffer.append(e); } interpreter.setScriptFilename(filename); interpreter.runScriptlet(buffer.toString()); } else { filename = ruFile.getAbsolutePath(); if (scriptPaths != null) { BufferedReader script = new BufferedReader( new InputStreamReader( new FileInputStream(ruFile.getAbsolutePath()), "UTF-8")); // TODO implement compile only !!! if (isCompileOnly) { log(lvl, "runRuby: running COMPILE_ONLY"); EvalUnit unit = interpreter.parse(script, filename); //unit.run(); } else { if (scriptPaths.length > 1) { filename = FileManager.slashify(scriptPaths[0], true) + scriptPaths[1] + ".sikuli"; log(lvl, "runRuby: running script from IDE: \n" + filename); if (scriptPaths[0] == null) { filename = ""; } fromIDE = true; } else { filename = scriptPaths[0]; log(lvl, "runRuby: running script: \n" + filename); } interpreter.runScriptlet(script, filename); } } else { log(-1, "runRuby: 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 != ruFile) { exitCode = findErrorSource(e, filename, scriptPaths); } else { Debug.error("runRuby: Ruby exception: %s with %s", e.getMessage(), stmt); } if (fromIDE) { exitCode *= -1; } else { exitCode = 1; } } } return exitCode; } private int findErrorSource(Throwable thr, String filename, String[] forIDE) { String err = thr.getMessage(); errorLine = -1; errorColumn = -1; errorClass = PY_UNKNOWN; errorType = "--UnKnown--"; errorText = "--UnKnown--"; String msg; if (err.startsWith("(SyntaxError)")) { //org.jruby.parser.ParserSyntaxException //(SyntaxError) /tmp/sikuli-3213678404470696048.rb:2: syntax error, unexpected tRCURLY Pattern pLineS = Pattern.compile("(?<=:)(\\d+):(.*)"); Matcher mLine = pLineS.matcher(err); if (mLine.find()) { log(lvl + 2, "SyntaxError error line: " + mLine.group(1)); errorText = mLine.group(2) == null ? errorText : mLine.group(2); log(lvl + 2, "SyntaxError: " + errorText); errorLine = Integer.parseInt(mLine.group(1)); errorColumn = -1; errorClass = PY_SYNTAX; errorType = "SyntaxError"; } } else { //if (err.startsWith("(NameError)")) { // org.jruby.embed.EvalFailedException //(NameError) undefined local variable or method `asdf' for main:Object Pattern type = Pattern.compile("(?<=\\()(\\w*)"); Matcher mLine = type.matcher(err); if (mLine.find()) { errorType = mLine.group(1); } Throwable cause = thr.getCause(); //cause.printStackTrace(); for (StackTraceElement line : cause.getStackTrace()) { if (line.getFileName().equals(filename)) { errorText = cause.getMessage(); errorColumn = -1; errorLine = line.getLineNumber(); errorClass = PY_RUNTIME; this.errorText = thr.getMessage(); Pattern sikType = Pattern.compile( "(?<=org.sikuli.script.)(.*)(?=:)"); Matcher mSikType = sikType.matcher(this.errorText); if (mSikType.find()) { errorType = mSikType.group(1); } else if (errorType.equals("RuntimeError")) { errorClass = PY_JAVA; } break; } } } msg = "script"; if (forIDE != null) { msg += " [ " + forIDE[1] + " ]"; } 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) { Throwable cause = thr.getCause(); //cause.printStackTrace(); StackTraceElement[] stack = cause.getStackTrace(); /*StringWriter writer = new StringWriter(); PrintWriter out = new PrintWriter(writer); cause.printStackTrace(out); errorTrace = writer.toString();*/ StringBuilder builder = new StringBuilder(); for (StackTraceElement line : stack) { builder.append(line.getLineNumber()); builder.append(":\t"); builder.append(line.getClassName()); builder.append(" ( "); builder.append(line.getMethodName()); builder.append(" )\t"); builder.append(line.getFileName()); builder.append('\n'); } errorTrace = builder.toString(); if (errorTrace.length() > 0) { Debug.error("--- Traceback --- error source first\n" + "line: class ( method ) file \n" + errorTrace + "[error] --- Traceback --- end --------------"); log(lvl + 2, "--- Traceback --- error source first\n" + "line: class ( method ) file \n" + errorTrace + "[error] --- Traceback --- end --------------"); } } } else if (errorClass == PY_JAVA) { } else { Debug.error(msg); Debug.error("Could not evaluate error source nor reason. Analyze StackTrace!"); Debug.error(err); } return errorLine; } /** * Initializes the ScriptingContainer and creates an instance. */ private void createScriptingContainer() { //TODO create a specific RubyPath (sys.path) if (interpreter == null) { //ScriptingContainer.initialize(System.getProperties(), null, sysargv.toArray(new String[0])); interpreter = new ScriptingContainer( LocalContextScope.THREADSAFE); interpreter.setCompatVersion(CompatVersion.RUBY2_0); interpreter.setCompileMode(CompileMode.JIT); } } public ScriptingContainer getScriptingContainer() { if (interpreter == null) { sysargv = new ArrayList<String>(); sysargv.add("--???--"); sysargv.addAll(Arrays.asList(sxRunTime.getArgs())); createScriptingContainer(); } return interpreter; } /** * Executes the defined header for the jruby script. * * @param syspaths List of all syspath entries */ private void executeScriptHeader(String[] syspaths) { // TODO implement compile only if (isCompileOnly) { return; } List<String> path = interpreter.getLoadPaths(); if (path.size() == 0 || !FileManager.pathEquals((String) path.get(0), sikuliLibPath)) { log(lvl, "executeScriptHeader: adding SikuliX Lib path to sys.path\n" + sikuliLibPath); int pathLength = path.size(); String[] pathNew = new String[pathLength + 1]; pathNew[0] = sikuliLibPath; for (int i = 0; i < pathLength; i++) { log(lvl + 1, "executeScriptHeader: before: %d: %s", i, path.get(i)); pathNew[i + 1] = (String) path.get(i); } for (int i = 0; i < pathLength; i++) { path.set(i, pathNew[i]); } path.add(pathNew[pathNew.length - 1]); for (int i = 0; i < pathNew.length; i++) { log(lvl + 1, "executeScriptHeader: after: %d: %s", i, path.get(i)); } } if (savedpathlen == 0) { savedpathlen = interpreter.getLoadPaths().size(); log(lvl + 1, "executeScriptHeader: saved sys.path: %d", savedpathlen); } while (interpreter.getLoadPaths().size() > savedpathlen) { interpreter.getLoadPaths().remove(savedpathlen); } log(lvl + 1, "executeScriptHeader: at entry: path:"); for (String p : interpreter.getLoadPaths()) { log(lvl + 1, p); } log(lvl + 1, "executeScriptHeader: at entry: --- end ---"); for (String syspath : syspaths) { path.add(FileManager.slashify(syspath, false)); } interpreter.runScriptlet(SCRIPT_HEADER); if (codeBefore != null) { StringBuilder buffer = new StringBuilder(); for (String line : codeBefore) { buffer.append(line); } interpreter.runScriptlet(buffer.toString()); } } private boolean doRedirect(PipedInputStream[] pin) { ScriptingContainer interpreter = getScriptingContainer(); try { PipedOutputStream pout = new PipedOutputStream(pin[0]); PrintStream ps = new PrintStream(pout, true); if (!ScriptingSupport.systemRedirected) { System.setOut(ps); } interpreter.setOutput(ps); } catch (Exception e) { log(-1, "%s: redirect STDOUT: %s", getName(), e.getMessage()); return false; } try { PipedOutputStream pout = new PipedOutputStream(pin[1]); PrintStream ps = new PrintStream(pout, true); if (!ScriptingSupport.systemRedirected) { System.setErr(ps); } interpreter.setError(ps); } catch (Exception e) { log(-1, "%s: redirect STDERR: %s", getName(), e.getMessage()); return false; } return true; } /** * Fills the sysargv list for the Ruby script * * @param filename The file containing the script: Has to be passed as first * parameter in Ruby * @param argv The parameters passed to Sikuli with --args */ private void fillSysArgv(File filename, String[] argv) { sysargv = new ArrayList<String>(); if (filename != null) { sysargv.add(filename.getAbsolutePath()); } if (argv != null) { sysargv.addAll(Arrays.asList(argv)); } } }