package org.jruby.runtime.backtrace;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.stream.Stream;
import com.headius.backport9.stack.StackWalker;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyException;
import org.jruby.RubyHash;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyString;
import org.jruby.ast.util.ArgsUtil;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;
import org.jruby.util.TypeConverter;
public class TraceType {
private static final Logger LOG = LoggerFactory.getLogger(TraceType.class);
private static final StackWalker WALKER = ThreadContext.WALKER;
private static final String[] FULL_MESSAGE_KEYS = {"highlight", "order"};
private final Gather gather;
private final Format format;
public TraceType(Gather gather, Format format) {
this.gather = gather;
this.format = format;
}
public Gather getGather() {
return gather;
}
public Format getFormat() {
return format;
}
public BacktraceData getBacktrace(ThreadContext context) {
return gather.getBacktraceData(context);
}
public BacktraceData getIntegratedBacktrace(ThreadContext context, StackTraceElement[] javaTrace) {
return gather.getIntegratedBacktraceData(context, javaTrace);
}
public String printBacktrace(RubyException exception, boolean console) {
return format.printBacktrace(exception, console);
}
public static void logBacktrace(Ruby runtime, RubyStackTraceElement[] trace) {
if (trace == null) trace = RubyStackTraceElement.EMPTY_ARRAY;
final StringBuilder buffer = new StringBuilder(64 + trace.length * 48);
buffer.append("Backtrace generated:\n");
renderBacktraceJRuby(runtime, trace, buffer, false);
LOG.info(buffer.toString());
}
public static void logException(RubyException exception) {
LOG.info("Exception raised: {} : {}", exception.getMetaClass(), exception);
}
public static void dumpException(RubyException exception) {
logException(exception);
}
public static void dumpBacktrace(RubyException exception) {
Ruby runtime = exception.getRuntime();
System.err.println("Backtrace generated:\n" + printBacktraceJRuby(exception, runtime.getPosix().isatty(FileDescriptor.err)));
}
public static void logCaller(RubyArray trace) {
StringBuilder buffer = new StringBuilder(64 + trace.size() * 48);
buffer.append("Caller backtrace generated:\n");
for (int i = 0; i < trace.size(); i++) {
buffer.append(" ").append(trace.eltInternal(i)).append('\n');
}
LOG.info(buffer.toString());
}
public static void dumpCaller(RubyArray trace) {
logCaller(trace);
}
public static void logCaller(RubyStackTraceElement[] trace) {
if (trace == null) trace = RubyStackTraceElement.EMPTY_ARRAY;
LOG.info( formatWithMRIBacktrace("Caller backtrace generated:\n", trace).toString() );
}
private static StringBuilder formatWithMRIBacktrace(final String message, RubyStackTraceElement[] trace) {
StringBuilder buffer = new StringBuilder(64 + trace.length * 48);
buffer.append(message);
renderBacktraceMRI(trace, " ", buffer, false);
return buffer;
}
public static void dumpCaller(RubyStackTraceElement[] trace) {
logCaller(trace);
}
public static void logWarning(RubyStackTraceElement[] trace) {
if (trace == null) trace = RubyStackTraceElement.EMPTY_ARRAY;
LOG.info( formatWithMRIBacktrace("Warning backtrace generated:\n", trace).toString() );
}
public static void dumpWarning(RubyStackTraceElement[] trace) {
logWarning(trace);
}
public static TraceType traceTypeFor(String style) {
if (style.equalsIgnoreCase("raw")) return new TraceType(Gather.RAW, Format.JRUBY);
else if (style.equalsIgnoreCase("ruby_framed")) return new TraceType(Gather.NORMAL, Format.JRUBY);
else if (style.equalsIgnoreCase("normal")) return new TraceType(Gather.NORMAL, Format.JRUBY);
else if (style.equalsIgnoreCase("full")) return new TraceType(Gather.FULL, Format.JRUBY);
else if (style.equalsIgnoreCase("mri")) return new TraceType(Gather.NORMAL, Format.MRI);
else return new TraceType(Gather.NORMAL, Format.JRUBY);
}
public enum Gather {
RAW {
public BacktraceData getBacktraceData(ThreadContext context, Stream<StackWalker.StackFrame> stackStream) {
return new BacktraceData(
stackStream,
Stream.empty(),
true,
false,
false);
}
},
FULL {
public BacktraceData getBacktraceData(ThreadContext context, Stream<StackWalker.StackFrame> stackStream) {
return new BacktraceData(
stackStream,
context.getBacktrace(),
true,
false,
false);
}
},
INTEGRATED {
public BacktraceData getBacktraceData(ThreadContext context, Stream<StackWalker.StackFrame> stackStream) {
return new BacktraceData(
stackStream,
context.getBacktrace(),
false,
false,
true);
}
},
NORMAL {
public BacktraceData getBacktraceData(ThreadContext context, Stream<StackWalker.StackFrame> stackStream) {
return new BacktraceData(
stackStream,
context.getBacktrace(),
false,
context.runtime.getInstanceConfig().getBacktraceMask(),
false);
}
},
CALLER {
public BacktraceData getBacktraceData(ThreadContext context, Stream<StackWalker.StackFrame> stackStream) {
return new BacktraceData(
stackStream,
context.getBacktrace(),
false,
true,
false);
}
};
public BacktraceData getBacktraceData(ThreadContext context) {
return WALKER.walk(Thread.currentThread().getStackTrace(), stream -> {
BacktraceData data = getBacktraceData(context, stream);
context.runtime.incrementBacktraceCount();
if (RubyInstanceConfig.LOG_BACKTRACES) {
logBacktrace(context.runtime, data.getBacktrace(context.runtime));
}
return data;
});
}
public BacktraceData getIntegratedBacktraceData(ThreadContext context, StackTraceElement[] javaTrace) {
Gather useGather = this == NORMAL ? INTEGRATED : this;
return WALKER.walk(javaTrace, stream -> {
BacktraceData data = useGather.getBacktraceData(context, stream);
context.runtime.incrementBacktraceCount();
if (RubyInstanceConfig.LOG_BACKTRACES)
logBacktrace(context.runtime, data.getBacktrace(context.runtime));
return data;
});
}
public abstract BacktraceData getBacktraceData(ThreadContext context, Stream<StackWalker.StackFrame> javaTrace);
}
public enum Format {
MRI {
public String printBacktrace(RubyException exception, boolean console) {
return printBacktraceMRI(exception, console);
}
public void renderBacktrace(RubyStackTraceElement[] elts, StringBuilder buffer, boolean color) {
renderBacktraceMRI(elts, buffer, color);
}
},
JRUBY {
public String printBacktrace(RubyException exception, boolean console) {
return printBacktraceJRuby(exception, console);
}
public void renderBacktrace(RubyStackTraceElement[] elts, StringBuilder buffer, boolean color) {
renderBacktraceJRuby(null, elts, buffer, color);
}
};
public abstract String printBacktrace(RubyException exception, boolean console);
public abstract void renderBacktrace(RubyStackTraceElement[] elts, StringBuilder buffer, boolean color);
}
public static String printFullMessage(ThreadContext context, IRubyObject exception, IRubyObject opts) {
Ruby runtime = context.runtime;
IRubyObject optArg = ArgsUtil.getOptionsArg(runtime, opts);
boolean highlight = false;
boolean reverse = false;
if (!optArg.isNil()) {
IRubyObject[] highlightOrder = ArgsUtil.extractKeywordArgs(context, (RubyHash) optArg, FULL_MESSAGE_KEYS);
IRubyObject vHigh = highlightOrder[0];
if (vHigh == null) vHigh = context.nil;
if (vHigh != context.nil && vHigh != context.fals && vHigh != context.tru) {
throw runtime.newArgumentError("expected true or false as highlight: " + vHigh);
}
highlight = vHigh.isTrue();
IRubyObject vOrder = highlightOrder[1];
if (vOrder != null) {
vOrder = TypeConverter.checkID(vOrder);
if (vOrder == runtime.newSymbol("bottom")) reverse = true;
else if (vOrder == runtime.newSymbol("top")) reverse = false;
else {
throw runtime.newArgumentError("expected :top or :bottom as order: " + vOrder);
}
}
}
return printBacktraceMRI(exception, highlight);
}
private static String printBacktraceMRI(IRubyObject exception, boolean console) {
final Ruby runtime = exception.getRuntime();
final ThreadContext context = runtime.getCurrentContext();
final IRubyObject backtrace = exception.callMethod(context, "backtrace");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream errorStream = new PrintStream(baos);
boolean printedPosition = false;
if (backtrace.isNil() || !(backtrace instanceof RubyArray)) {
if (context.getFile() != null && context.getFile().length() > 0) {
errorStream.print(context.getFile() + ':' + context.getLine());
printedPosition = true;
} else {
errorStream.print(context.getLine());
printedPosition = true;
}
} else if (((RubyArray) backtrace).getLength() == 0) {
printErrorPos(context, errorStream);
} else {
IRubyObject mesg = ((RubyArray) backtrace).first();
if (mesg.isNil()) {
printErrorPos(context, errorStream);
} else {
errorStream.print(mesg);
printedPosition = true;
}
}
RubyClass type = exception.getMetaClass();
String info = exception.toString();
if (type == runtime.getRuntimeError() && (info == null || info.length() == 0)) {
errorStream.print(": unhandled exception\n");
} else {
if (printedPosition) errorStream.print(": ");
String path = type.getName();
if (info.length() == 0) {
errorStream.print(path + '\n');
} else {
if (path.startsWith("#")) {
path = null;
}
String tail = null;
int idx = info.indexOf('\n');
if (idx != -1) {
tail = info.substring(idx + 1);
info = info.substring(0, idx);
}
errorStream.print(info);
if (path != null) {
errorStream.print(" (" + path + ")\n");
} else {
errorStream.print('\n');
}
if (tail != null) {
errorStream.print(tail + '\n');
}
}
}
printBacktraceToStream(backtrace, errorStream, 1);
return baos.toString();
}
private static final String FIRST_COLOR = "\033[0;31m";
private static final String KERNEL_COLOR = "\033[0;36m";
private static final String EVAL_COLOR = "\033[0;33m";
private static final String CLEAR_COLOR = "\033[0m";
public static String printBacktraceJRuby(Ruby runtime, RubyStackTraceElement[] frames, String type, String message, boolean color) {
if (frames == null) frames = RubyStackTraceElement.EMPTY_ARRAY;
StringBuilder buffer = new StringBuilder(64 + frames.length * 48);
buffer.append(type).append(": ").append(message).append('\n');
renderBacktraceJRuby(runtime, frames, buffer, color);
return buffer.toString();
}
protected static String printBacktraceJRuby(RubyException exception, boolean console) {
final Ruby runtime = exception.getRuntime();
final ThreadContext context = runtime.getCurrentContext();
boolean color = console && runtime.getInstanceConfig().getBacktraceColor();
String message;
try {
message = exception.callMethod(context, "message").toString();
} catch (org.jruby.exceptions.Exception unused) {
message = exception.message(context).toString();
}
if (exception.getMetaClass() == runtime.getRuntimeError() && message.length() == 0) {
message = "No current exception";
}
String type = exception.getMetaClass().getName();
return printBacktraceJRuby(exception.getRuntime(), exception.getBacktraceElements(), type, message, color);
}
private static void renderBacktraceJRuby(Ruby runtime, RubyStackTraceElement[] frames, StringBuilder buffer, boolean color) {
int longestMethod = 0;
for (RubyStackTraceElement frame : frames) {
longestMethod = Math.max(longestMethod, frame.getMethodName().length());
}
boolean first = true;
for (RubyStackTraceElement frame : frames) {
if (color) {
if (first) {
buffer.append(FIRST_COLOR);
} else if (frame.isBinding() || frame.getFileName().equals("(eval)")) {
buffer.append(EVAL_COLOR);
} else if (frame.getFileName().indexOf(".java") != -1) {
buffer.append(KERNEL_COLOR);
}
first = false;
}
buffer.append(" ");
String methodName = runtime == null ? frame.getMethodName() : runtime.newSymbol(frame.getMethodName()).idString();
for (int j = 0; j < longestMethod - methodName.length(); j++) {
buffer.append(' ');
}
buffer
.append(methodName)
.append(" at ")
.append(frame.getFileName())
.append(':')
.append(frame.getLineNumber());
if (color) {
buffer.append(CLEAR_COLOR);
}
buffer
.append('\n');
}
}
private static void renderBacktraceMRI(RubyStackTraceElement[] trace, StringBuilder buffer, boolean color) {
renderBacktraceMRI(trace, "", buffer, color);
}
private static void renderBacktraceMRI(RubyStackTraceElement[] trace, String linePrefix, StringBuilder buffer, boolean color) {
for (int i = 0; i < trace.length; i++) {
RubyStackTraceElement element = trace[i];
buffer
.append(linePrefix)
.append(element.getFileName())
.append(':')
.append(element.getLineNumber())
.append(":in `")
.append(element.getMethodName())
.append("'\n");
}
}
private static void printErrorPos(ThreadContext context, PrintStream errorStream) {
if (context.getFile() != null && context.getFile().length() > 0) {
if (context.getFrameName() != null) {
errorStream.print(context.getFile() + ':' + context.getLine());
errorStream.print(":in '" + context.getFrameName() + '\'');
} else if (context.getLine() != 0) {
errorStream.print(context.getFile() + ':' + context.getLine());
} else {
errorStream.print(context.getFile());
}
}
}
public static void printBacktraceToStream(IRubyObject backtrace, PrintStream errorStream, int skip) {
if ( backtrace.isNil() ) return;
if ( backtrace instanceof RubyArray ) {
IRubyObject[] elements = ((RubyArray) backtrace).toJavaArrayMaybeUnsafe();
for (int i = skip; i < elements.length; i++) {
IRubyObject stackTraceLine = elements[i];
if (stackTraceLine instanceof RubyString) {
errorStream.println("\tfrom " + stackTraceLine);
}
else {
errorStream.println("\t" + stackTraceLine);
}
}
}
}
@Deprecated
public static IRubyObject generateMRIBacktrace(Ruby runtime, RubyStackTraceElement[] trace) {
if (trace == null) return runtime.getNil();
ThreadContext context = runtime.getCurrentContext();
final IRubyObject[] traceArray = new IRubyObject[trace.length];
for (int i = 0; i < trace.length; i++) {
traceArray[i] = RubyStackTraceElement.to_s_mri(context, trace[i]);
}
return RubyArray.newArrayMayCopy(runtime, traceArray);
}
@Deprecated
public RubyStackTraceElement getBacktraceElement(ThreadContext context, int uplevel) {
RubyStackTraceElement[] elements = getBacktrace(context).getBacktrace(context.runtime);
if (elements.length <= uplevel + 1) uplevel = -1;
return elements[uplevel + 1];
}
}