package org.jruby;
import org.jruby.exceptions.MainExitException;
import org.jruby.exceptions.JumpException;
import org.jruby.exceptions.RaiseException;
import org.jruby.exceptions.SignalException;
import org.jruby.exceptions.ThreadKill;
import org.jruby.main.DripMain;
import org.jruby.platform.Platform;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.SafePropertyAccessor;
import org.jruby.util.cli.OutputStrings;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
private static final Logger LOG = LoggerFactory.getLogger(Main.class);
public Main(RubyInstanceConfig config) {
this(config, false);
}
public Main(final InputStream in, final PrintStream out, final PrintStream err) {
this(new RubyInstanceConfig(in, out, err));
}
public Main() {
this(new RubyInstanceConfig());
}
private Main(RubyInstanceConfig config, boolean hardExit) {
this.config = config;
config.setHardExit(hardExit);
}
private Main(boolean hardExit) {
processDotfile();
this.config = new RubyInstanceConfig();
config.setHardExit(hardExit);
}
private static List<String> getDotfileDirectories() {
final ArrayList<String> searchList = new ArrayList<>(4);
for (String homeProp : new String[] {"user.dir", "user.home"}) {
String home = SafePropertyAccessor.getProperty(homeProp);
if (home != null) searchList.add(home);
}
if (Platform.IS_WINDOWS) {
String homeDrive = System.getenv("HOMEDRIVE");
String homePath = System.getenv("HOMEPATH");
if (homeDrive != null && homePath != null) {
searchList.add(1, (homeDrive + homePath).replace('\\', '/'));
}
}
return searchList;
}
public static void processDotfile() {
final StringBuilder path = new StringBuilder();
for (String home : getDotfileDirectories()) {
path.setLength(0);
path.append(home).append("/.jrubyrc");
final File dotfile = new File(path.toString());
if (dotfile.exists()) loadJRubyProperties(dotfile);
}
}
private static void loadJRubyProperties(File dotfile) {
FileInputStream fis = null;
try {
Properties sysProps = System.getProperties();
Properties newProps = new Properties();
fis = new FileInputStream(dotfile);
newProps.load(fis);
for (Map.Entry entry : newProps.entrySet()) {
sysProps.put("jruby." + entry.getKey(), entry.getValue());
}
}
catch (IOException|SecurityException ex) {
if (LOG.isDebugEnabled()) LOG.debug("exception loading properties from: " + dotfile, ex);
}
finally {
if (fis != null) try { fis.close(); } catch (Exception e) {}
}
}
public static class Status {
private boolean isExit = false;
private int status = 0;
Status(int status) {
this.isExit = true;
this.status = status;
}
Status() {}
public boolean isExit() { return isExit; }
public int getStatus() { return status; }
}
public static void main(String[] args) {
doGCJCheck();
Main main;
if (DripMain.DRIP_RUNTIME != null) {
main = new Main(DripMain.DRIP_CONFIG, true);
} else {
main = new Main(true);
}
try {
Status status = main.run(args);
if (status.isExit()) {
System.exit(status.getStatus());
}
}
catch (RaiseException ex) {
System.exit( handleRaiseException(ex) );
}
catch (JumpException ex) {
System.exit( handleUnexpectedJump(ex) );
}
catch (Throwable t) {
System.err.println("Unhandled Java exception: " + t);
System.err.println(ThreadContext.createRawBacktraceStringFromThrowable(t, false));
while ((t = t.getCause()) != null) {
System.err.println("Caused by:");
System.err.println(ThreadContext.createRawBacktraceStringFromThrowable(t, false));
}
System.exit(1);
}
}
public Status run(String[] args) {
try {
config.processArguments(args);
return internalRun();
} catch (MainExitException mee) {
return handleMainExit(mee);
} catch (OutOfMemoryError oome) {
return handleOutOfMemory(oome);
} catch (StackOverflowError soe) {
return handleStackOverflow(soe);
} catch (UnsupportedClassVersionError ucve) {
return handleUnsupportedClassVersion(ucve);
} catch (ThreadKill kill) {
return new Status();
}
}
@Deprecated
public Status run() {
return internalRun();
}
private Status internalRun() {
doShowVersion();
doShowCopyright();
doPrintProperties();
if (!config.getShouldRunInterpreter()) {
doPrintUsage(false);
return new Status();
}
InputStream in = config.getScriptSource();
String filename = config.displayedFileName();
Ruby _runtime;
if (DripMain.DRIP_RUNTIME != null) {
_runtime = DripMain.DRIP_RUNTIME;
_runtime.reinitialize(true);
} else {
_runtime = Ruby.newInstance(config);
}
final Ruby runtime = _runtime;
final AtomicBoolean didTeardown = new AtomicBoolean();
if (runtime != null && config.isHardExit()) {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
if (didTeardown.compareAndSet(false, true)) {
runtime.tearDown();
}
}
});
}
try {
if (runtime != null) {
doSetContextClassLoader(runtime);
}
if (in == null) {
return new Status();
} else if (config.getShouldCheckSyntax()) {
return doCheckSyntax(runtime, in, filename);
} else {
return doRunFromMain(runtime, in, filename);
}
} finally {
if (runtime != null && didTeardown.compareAndSet(false, true)) {
runtime.tearDown();
}
}
}
private Status handleUnsupportedClassVersion(UnsupportedClassVersionError ex) {
config.getError().println("Error: Some library (perhaps JRuby) was built with a later JVM version.");
config.getError().println("Please use libraries built with the version you intend to use or an earlier one.");
if (config.isVerbose()) {
ex.printStackTrace(config.getError());
} else {
config.getError().println("Specify -w for full " + ex + " stack trace");
}
return new Status(1);
}
private Status handleStackOverflow(StackOverflowError ex) {
String memoryMax = getRuntimeFlagValue("-Xss");
if (memoryMax != null) {
config.getError().println("Error: Your application used more stack memory than the safety cap of " + memoryMax + '.');
} else {
config.getError().println("Error: Your application used more stack memory than the default safety cap.");
}
config.getError().println("Specify -J-Xss####k to increase it (#### = cap size in KB).");
if (config.isVerbose()) {
ex.printStackTrace(config.getError());
} else {
config.getError().println("Specify -w for full " + ex + " stack trace");
}
return new Status(1);
}
private Status handleOutOfMemory(OutOfMemoryError ex) {
System.gc();
String oomeMessage = ex.getMessage();
boolean heapError = false;
if (oomeMessage != null) {
if (oomeMessage.contains("unable to create new native thread")) {
config.getError().println("Error: Your application demanded too many live threads, perhaps for Fiber or Enumerator.");
config.getError().println("Ensure your old Fibers and Enumerators are being cleaned up.");
} else {
heapError = true;
}
}
if (heapError) {
String memoryMax = getRuntimeFlagValue("-Xmx");
if (memoryMax != null) {
config.getError().println("Error: Your application used more memory than the safety cap of " + memoryMax + ".");
} else {
config.getError().println("Error: Your application used more memory than the automatic cap of " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB.");
}
config.getError().println("Specify -J-Xmx####M to increase it (#### = cap size in MB).");
}
if (config.isVerbose()) {
ex.printStackTrace(config.getError());
} else {
config.getError().println("Specify -w for full " + ex + " stack trace");
}
return new Status(1);
}
private String getRuntimeFlagValue(String prefix) {
RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
for (String param : runtime.getInputArguments()) {
if (param.startsWith(prefix)) {
return param.substring(prefix.length()).toUpperCase();
}
}
return null;
}
private Status handleMainExit(MainExitException mee) {
if (!mee.isAborted()) {
config.getError().println(mee.getMessage());
if (mee.isUsageError()) {
doPrintUsage(true);
}
}
return new Status(mee.getStatus());
}
private Status doRunFromMain(Ruby runtime, InputStream in, String filename) {
try {
doCheckSecurityManager();
runtime.runFromMain(in, filename);
} catch (RaiseException rj) {
return new Status(handleRaiseException(rj));
}
return new Status();
}
private Status doCheckSyntax(Ruby runtime, InputStream in, String filename) throws RaiseException {
boolean status = checkStreamSyntax(runtime, in, filename);
for (String arg : config.getArgv()) {
status = status && checkFileSyntax(runtime, arg);
}
return new Status(status ? 0 : -1);
}
private boolean checkFileSyntax(Ruby runtime, String filename) {
File file = new File(filename);
if (file.exists()) {
try {
return checkStreamSyntax(runtime, new FileInputStream(file), filename);
} catch (FileNotFoundException fnfe) {
config.getError().println("File not found: " + filename);
return false;
}
} else {
return false;
}
}
private boolean checkStreamSyntax(Ruby runtime, InputStream in, String filename) {
final ThreadContext context = runtime.getCurrentContext();
final IRubyObject $ex = context.getErrorInfo();
try {
runtime.parseFromMain(in, filename);
config.getOutput().println("Syntax OK");
return true;
} catch (RaiseException re) {
if (re.getException().getMetaClass().getBaseName().equals("SyntaxError")) {
context.setErrorInfo($ex);
config.getError().println("SyntaxError in " + re.getException().message(context));
return false;
}
throw re;
}
}
private void doSetContextClassLoader(Ruby runtime) {
try {
Thread.currentThread().setContextClassLoader(runtime.getJRubyClassLoader());
} catch (SecurityException se) {
if (runtime.getInstanceConfig().isVerbose()) {
config.getError().println("WARNING: Security restrictions disallowed setting context classloader for main thread.");
}
}
}
private void doPrintProperties() {
if (config.getShouldPrintProperties()) {
config.getOutput().print(OutputStrings.getPropertyHelp());
}
}
private void doPrintUsage(boolean force) {
if (config.getShouldPrintUsage() || force) {
config.getOutput().print(OutputStrings.getBasicUsageHelp());
config.getOutput().print(OutputStrings.getFeaturesHelp());
}
}
private void doShowCopyright() {
if (config.isShowCopyright()) {
config.getOutput().println(OutputStrings.getCopyrightString());
}
}
private void doShowVersion() {
if (config.isShowVersion()) {
config.getOutput().println(OutputStrings.getVersionString());
}
}
private static void doGCJCheck() {
if (Platform.IS_GCJ) {
System.err.println("Fatal: GCJ (GNU Compiler for Java) is not supported by JRuby.");
System.exit(1);
}
}
private void doCheckSecurityManager() {
if (Main.class.getClassLoader() == null && System.getSecurityManager() != null) {
System.err.println("Warning: security manager and JRuby running from boot classpath.\n" +
"Run from jruby.jar or set env VERIFY_JRUBY=true to enable security.");
}
}
protected static int handleRaiseException(final RaiseException ex) {
RubyException raisedException = ex.getException();
final Ruby runtime = raisedException.getRuntime();
if ( runtime.getSystemExit().isInstance(raisedException) ) {
IRubyObject status = raisedException.callMethod(runtime.getCurrentContext(), "status");
if (status != null && ! status.isNil()) {
return RubyNumeric.fix2int(status);
}
return 0;
} else if ( runtime.getSignalException().isInstance(raisedException) ) {
IRubyObject status = raisedException.callMethod(runtime.getCurrentContext(), "signo");
if (status != null && ! status.isNil()) {
return RubyNumeric.fix2int(status) + 128;
}
return 0;
}
System.err.print(runtime.getInstanceConfig().getTraceType().printBacktrace(raisedException, runtime.getPosix().isatty(FileDescriptor.err)));
return 1;
}
private static int handleUnexpectedJump(final JumpException ex) {
if ( ex instanceof JumpException.SpecialJump ) {
System.err.println("Unexpected break: " + ex);
}
else if ( ex instanceof JumpException.FlowControlException ) {
if ( Ruby.isGlobalRuntimeReady() ) {
final Ruby runtime = Ruby.getGlobalRuntime();
RaiseException raise = ((JumpException.FlowControlException) ex).buildException(runtime);
if ( raise != null ) handleRaiseException(raise);
}
else {
System.err.println("Unexpected jump: " + ex);
}
}
else {
System.err.println("Unexpected: " + ex);
}
final StackTraceElement[] trace = ex.getStackTrace();
if ( trace != null && trace.length > 0 ) {
System.err.println( ThreadContext.createRawBacktraceStringFromThrowable(ex, false) );
}
else {
System.err.println("HINT: to get backtrace for jump exceptions run with -Xjump.backtrace=true");
}
return 2;
}
private final RubyInstanceConfig config;
}