Package net.cakenet.jsaton.script.ruby

Source Code of net.cakenet.jsaton.script.ruby.RubyScript

package net.cakenet.jsaton.script.ruby;

import net.cakenet.jsaton.nativedef.WindowReference;
import net.cakenet.jsaton.script.Script;
import net.cakenet.jsaton.script.ScriptLanguage;
import net.cakenet.jsaton.script.debug.BreakInformation;
import net.cakenet.jsaton.script.debug.Breakpoint;
import net.cakenet.jsaton.script.debug.DebugFrame;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.parser.*;
import org.jruby.Ruby;
import org.jruby.RubyException;
import org.jruby.RubyIO;
import org.jruby.ast.BlockNode;
import org.jruby.ast.Node;
import org.jruby.embed.*;
import org.jruby.embed.internal.EmbedEvalUnitImpl;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.GlobalVariables;
import org.jruby.parser.StaticScope;
import org.jruby.parser.StaticScopeFactory;
import org.jruby.runtime.Block;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.Frame;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.backtrace.RubyStackTraceElement;
import org.jruby.runtime.backtrace.TraceType;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.scope.ManyVarsDynamicScope;

import javax.script.ScriptException;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

public class RubyScript extends Script implements Parser {
    private ScriptingContainer container = new ScriptingContainer(LocalContextScope.SINGLETHREAD, LocalVariableBehavior.TRANSIENT);
    private EmbedEvalUnit eval;
    private volatile boolean step, stepOver;
    private int stepOverDepth;
    private boolean astDirty;
    private ThreadContext breakContext;
    private Node breakSource, previous, ast;

    public RubyScript() {
        super(ScriptLanguage.RUBY);
        container.setRunRubyInProcess(false);
        container.setScriptFilename(getName());
        container.setHomeDirectory("lib/ruby"); // Todo: unpack when in jar to users home dir somewhere...
        Thread kickstarter = new Thread(new Runnable() {
            public void run() {
                // hack: eval nothing so that the script manager loads all required dependencies in this threadgroup
                // (so we don't invoke the security manager of the script thread)
                try {
                    container.runScriptlet("");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        kickstarter.setDaemon(true);
        kickstarter.setPriority(1);
        kickstarter.start(); // Warm up our engine...
    }

    public void setScript(String script) {
        if (!script.equals(this.getScript()))
            astDirty = true;
        super.setScript(script);
    }

    public void setName(String name) {
        super.setName(name);
        container.setScriptFilename(getName());
    }

    public void setTarget(WindowReference target) {
        super.setTarget(target);
        container.put("$target", getTarget());
    }

    protected void prepare() {
        container.put("$target", getTarget());
        container.put("$source", this);
        container.setScriptFilename(getName());
        if (isDebug())
            eval = getDebugExecutor();
        else
            eval = getExecutor();
    }

    protected void execute() throws Exception {
        if (eval == null)
            return;
        eval.run();
    }

    public void stepInto() {
        step = true;
        resume();
    }

    public void stepOver() {
        stepOver = true;
        stepOverDepth = breakContext.getFrameCount();
        resume();
    }

    public boolean isBreakpointTarget(Node node) {
        switch (node.getNodeType()) {
            case CALLNODE:
            case FCALLNODE:
            case VCALLNODE:
            case BEGINNODE:
            case DASGNNODE:
            case LOCALASGNNODE:
            case INSTASGNNODE:
            case GLOBALASGNNODE:
            case CLASSVARASGNNODE:
            case MULTIPLEASGN19NODE:
            case MULTIPLEASGNNODE:
            case ATTRASSIGNNODE:
                return true;
        }
        return false;
    }

    private Node embedDebugger(Node node) throws IllegalAccessException {
        final List<Node> children = node.childNodes();
        for (int i = 0; i < children.size(); i++) {
            final Node child = children.get(i);
            final Node replacement = embedDebugger(child);
            if (replacement == null)
                continue;

            // Find the field...
            Field field = null;
            outer:
            for (Class c = node.getClass(); c != null; c = c.getSuperclass()) {
                for (Field f : c.getDeclaredFields()) {
                    if (f.getType() == Node.class) {
                        f.setAccessible(true);
                        Object value = f.get(node);
                        if (value != child)
                            continue;
                        field = f;
                        field.set(node, replacement);
                        break outer;
                    } else if (List.class.isAssignableFrom(f.getType())) {
                        f.setAccessible(true);
                        List a = (List) f.get(node);
                        if (!a.contains(child))
                            continue;
                        a.set(a.indexOf(child), replacement);
                        field = f;
                        break outer;
                    }
                }
            }
            if (field == null)
                throw new RuntimeException("Uh oh");
        }

        // This is where the magic happens...
        if (isBreakpointTarget(node))
            return new DebugHandlingNode(node);
        return null;
    }

    private ScriptException parseRubyParseException(ParseFailedException e) {
        RubyException re = ((RaiseException) e.getCause()).getException();
        String msg = re.message.asJavaString();
        msg = msg.substring(getName().length() + 1);
        int lidx = msg.indexOf(':');
        int line = Integer.parseInt(msg.substring(0, lidx));
        msg = msg.substring(lidx + 2);
        int col = 0;
        int posSplit = msg.lastIndexOf("\n\n");
        if (posSplit != -1) {
            String loc = msg.substring(posSplit + 2);
            msg = msg.substring(0, posSplit);
            posSplit = loc.lastIndexOf("\n");
            String caretData = loc.substring(posSplit + 1);
            col = caretData.indexOf('^');
        }
        return new ScriptException(msg, null, line, col);
    }

    private EmbedEvalUnit getExecutor() {
        String scr = getScript();
        String[] lines = scr.split("\n", -1);
        int lineCount = lines.length;
        int[] lineNumbers = new int[lineCount];
        for (int i = 0; i < lineCount; i++)
            lineNumbers[i] = i;

        // Don'
        Ruby runtime = container.getProvider().getRuntime();
        GlobalVariables gvars = runtime.getGlobalVariables();
        IRubyObject orig = gvars.get("$stderr");
        // Todo: cache this or find a better way of supressing output...
        gvars.set("$stderr", new RubyIO(runtime, new ByteArrayOutputStream()));
        try {
            return container.parse(scr, lineNumbers);
        } catch (ParseFailedException e) {
            /**/
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            gvars.set("$stderr", orig);
        }
        return null;
    }

    public Node getAST() {
        if (astDirty) {
            EmbedEvalUnit orig = getExecutor();
            if (orig == null)
                return null;
            ast = orig.getNode();
            astDirty = false;
        }
        return ast;
    }

    private EmbedEvalUnit getDebugExecutor() {
        EmbedEvalUnit orig = getExecutor();
        if (orig == null)
            return null; // Parse error...
        Node root = orig.getNode();
        try {
            Node embed = embedDebugger(root);
            if (embed != null)
                root = embed;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        ManyVarsDynamicScope scope;
        StaticScopeFactory scopeFactory = container.getProvider().getRuntime().getStaticScopeFactory();

        // root our parsing scope with a dummy scope
        StaticScope topStaticScope = scopeFactory.newLocalScope(null);
        topStaticScope.setModule(container.getProvider().getRuntime().getObject());

        DynamicScope currentScope = new ManyVarsDynamicScope(topStaticScope, null);
        String[] names4Injection = container.getVarMap().getLocalVarNames();
        StaticScope evalScope = names4Injection == null || names4Injection.length == 0 ?
                scopeFactory.newEvalScope(currentScope.getStaticScope()) :
                scopeFactory.newEvalScope(currentScope.getStaticScope(), names4Injection);
        scope = new ManyVarsDynamicScope(evalScope, currentScope);

        // JRUBY-5501: ensure we've set up a cref for the scope too
        scope.getStaticScope().determineModule();
        return new EmbedEvalUnitImpl(container, root, scope);
    }

    public ExtendedHyperlinkListener getHyperlinkListener() {
        return null;
    }

    public URL getImageBase() {
        return null;
    }

    public boolean isEnabled() {
        return true;
    }

    public ParseResult parse(RSyntaxDocument doc, String style) {
        String scr = getScript();
        if (scr == null)
            return null;
        String[] lines = scr.split("\n", -1);
        int lineCount = lines.length;
        int[] lineNumbers = new int[lineCount];
        for (int i = 0; i < lineCount; i++)
            lineNumbers[i] = i;
        DefaultParseResult dpr = new DefaultParseResult(this);
        dpr.setParsedLines(0, lineCount);

        // Suppress output...
        Ruby runtime = container.getProvider().getRuntime();
        GlobalVariables gvars = runtime.getGlobalVariables();
        IRubyObject orig = gvars.get("$stderr");
        // Todo: cache this or find a better way of supressing output...
        gvars.set("$stderr", new RubyIO(runtime, new ByteArrayOutputStream()));
        try {
            container.parse(scr, lineNumbers);
        } catch (ParseFailedException e) {
            ScriptException se = parseRubyParseException(e);

            // find start of error (column is normally the end)...
            int targetLine = se.getLineNumber() - 1;
            if (targetLine >= lineCount)
                targetLine = lineCount - 1; // For EOF errors
            int col = se.getColumnNumber();
            String l = lines[targetLine];
            char[] chars = l.toCharArray();
            int start = col;
            for (; start > 0; start--) {
                char c = chars[start];
                if (Character.isWhitespace(c)) {
                    start++;
                    break;
                }
            }
            int end = col;
            for (; end < chars.length; end++) {
                char c = chars[end];
                if (Character.isWhitespace(c))
                    break;
            }
            int len = end - start;
            dpr.setParsedLines(0, targetLine);
            int off = 0;
            for (int i = 0; i < targetLine; i++)
                off += lines[i].length() + 1;
            off += start;
            DefaultParserNotice not = new DefaultParserNotice(this, se.getLocalizedMessage(), targetLine, off, len);
            not.setShowInEditor(true);
            not.setLevel(DefaultParserNotice.ERROR);
            dpr.addNotice(not);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            gvars.set("$stderr", orig);
        }
        return dpr;
    }

    // We create our own custom node and inject it into the AST to handle debugging (nasty, I know!)
    private class DebugHandlingNode extends BlockNode {
        private final Node delegate;

        public DebugHandlingNode(Node delegate) {
            super(delegate.getPosition());
            this.delegate = delegate;
        }

        private boolean ancestorOf(Node root, Node needle) {
            for (Node child : root.childNodes()) {
                if (child == needle || ancestorOf(child, needle))
                    return true;
            }
            return false;
        }


        public IRubyObject interpret(Ruby runtime, ThreadContext context, IRubyObject self, Block aBlock) {
            try {
                // No breakpoints, not stepping in or over, continue...
                previous = delegate;
                int line = delegate.getPosition().getStartLine();
                if (!breakpoints.containsKey(line) && !step && !stepOver)
                    return delegate.interpret(runtime, context, self, aBlock);

                // If stepping over, we don't continue until we're at the same (or higher) stack depth and the node is not an ancestor of this node
                // OR if the next breakpoint encountered is on the same line as the last one we encountered... continue!
                if (stepOver && (ancestorOf(breakSource, delegate) || context.getFrameCount() > stepOverDepth ||
                        (previous != null && previous.getPosition().getStartLine() == line))) {
                    return delegate.interpret(runtime, context, self, aBlock);
                }

                breakContext = context;
                breakSource = delegate;
                BreakInformation info = new BreakInformation();
                info.addVariableConverter(IRubyObject.class, new RubyExtractor());
                step = stepOver = false;

                // Todo: oh god, copying frames is going to be nasty as fuck...
                // popFrame clears the frame before returning, getFrames clones them for backtrace
                // i.e. only name and backtrace data is copied...
                Stack<Frame> frames = new Stack<>();
                Stack<IRubyObject> selfs = new Stack<>();
                for (Frame cur = context.getCurrentFrame(); cur != null; ) {
                    Frame clone = new Frame();
                    clone.updateFrame(cur); // clone...
                    frames.push(clone);
                    selfs.push(context.getFrameSelf());
                    context.popFrame();
                    try {
                        cur = context.getCurrentFrame();
                    } catch (ArrayIndexOutOfBoundsException e) {
                        break;
                    }
                }
                Stack<DynamicScope> scopes = new Stack<>();
                for (DynamicScope scope = context.getCurrentScope(); scopes.size() != frames.size(); ) {
                    scopes.push(scope);
                    context.popScope();
                    try {
                        scope = context.getCurrentScope();
                    } catch (ArrayIndexOutOfBoundsException aioe) {
                        break;
                    }
                }

                RubyStackTraceElement[] trace = TraceType.Gather.NORMAL.getBacktraceData(context, false).getBacktrace(runtime);
                /*int traceEnd = trace.length;
                for (int i = 0; i < traceEnd; i++) {
                    RubyStackTraceElement rste = trace[i];
                    if (!rste.getFileName().equals(getName())) {
                        trace[i] = null;
                        if (i + 1 < traceEnd)
                            System.arraycopy(trace, i + 1, trace, i, (traceEnd - i) - 1);
                        traceEnd--;
                        trace[traceEnd] = null;
                    }
                }*/
                assert frames.size() == scopes.size() : "Frames don't match scopes :S";
                for (int i = 0; i < frames.size(); i++) {
                    Frame frame = frames.get(i);
                    IRubyObject s = selfs.get(i);
                    DynamicScope scope = scopes.get(i);
                    RubyStackTraceElement stackElement = trace[i];
                    StaticScope sScope = scope.getStaticScope();
                    Map<String, Object> var = new HashMap<>();
                    String[] names = sScope.getVariables();
                    IRubyObject[] values = scope.getValues();
                    assert names.length == values.length : "Scope name and value length mismatch";
                    for (int j = 0; j < names.length; j++)
                        var.put(names[j], values[j]);
                    DebugFrame created = info.pushFrame(stackElement.getClassName() + "." + stackElement.getMethodName(),
                            stackElement.getFileName(), stackElement.getLineNumber(), s, var);
                    created.self.setName("self");
                }

                // Restore scopes and frames
                while (!scopes.isEmpty()) {
                    context.pushScope(scopes.pop());
                    context.pushFrame();
                    context.getCurrentFrame().updateFrame(frames.pop());
                }

                // Might not be breaking, depending on breakpoint type...
                Breakpoint breakpoint = breakpoints.get(delegate.getPosition().getStartLine());
                if (breakpoint != null) {
                    boolean shouldSuspend = breakpoint.shouldSuspend();
                    breakpoint.doActions(info);
                    if (!shouldSuspend)
                        return delegate.interpret(runtime, context, self, aBlock);
                }

                breakInfo = info;
                fireDebugBreak(info);
                suspend();
                breakInfo = null;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return delegate.interpret(runtime, context, self, aBlock);
        }

        public IRubyObject assign(Ruby runtime, ThreadContext context, IRubyObject self, IRubyObject value, Block block, boolean checkArity) {
            return delegate.assign(runtime, context, self, value, block, checkArity);    //To change body of overridden methods use File | Settings | File Templates.
        }
    }
}
TOP

Related Classes of net.cakenet.jsaton.script.ruby.RubyScript

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.