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.
}
}
}