/* ***** BEGIN LICENSE BLOCK ***** * Version: EPL 1.0/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Eclipse Public * License Version 1.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.eclipse.org/legal/epl-v10.html * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * Copyright (C) 2001 Alan Moore * Copyright (C) 2001-2002 Benoit Cerrina * Copyright (C) 2001-2004 Jan Arne Petersen * Copyright (C) 2002-2004 Anders Bengtsson * Copyright (C) 2004 Thomas E Enebo * Copyright (C) 2004-2006 Charles O Nutter * Copyright (C) 2004 Stefan Matthias Aust * Copyright (C) 2005 Kiel Hodges * Copyright (C) 2005 Jason Voegele * Copyright (C) 2005 Tim Azzopardi * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the EPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the EPL, the GPL or the LGPL. ***** END LICENSE BLOCK *****/ package org.jruby; import java.io.InputStream; import java.io.PrintStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; import org.jruby.exceptions.MainExitException; import org.jruby.exceptions.RaiseException; 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.SimpleSampler; import org.jruby.util.cli.OutputStrings; import org.jruby.util.log.Logger; import org.jruby.util.log.LoggerFactory; /** * Class used to launch the interpreter. * This is the main class as defined in the jruby.mf manifest. * It is very basic and does not support yet the same array of switches * as the C interpreter. * Usage: java -jar jruby.jar [switches] [rubyfile.rb] [arguments] * -e 'command' one line of script. Several -e's allowed. Omit [programfile] * @author jpetersen */ public class Main { private static final Logger LOG = LoggerFactory.getLogger("Main"); public Main(RubyInstanceConfig config) { this(config, false); } public Main(final InputStream in, final PrintStream out, final PrintStream err) { this(new RubyInstanceConfig(){{ setInput(in); setOutput(out); setError(err); }}); } public Main() { this(new RubyInstanceConfig()); } private Main(RubyInstanceConfig config, boolean hardExit) { this.config = config; config.setHardExit(hardExit); } private Main(boolean hardExit) { // used only from main(String[]), so we process dotfile here processDotfile(); this.config = new RubyInstanceConfig(); config.setHardExit(hardExit); } private static List getDotfileDirectories() { ArrayList searchList = new ArrayList(); for (String homeProp : new String[] {"user.dir", "user.home"}) { String home = SafePropertyAccessor.getProperty(homeProp); if (home != null) searchList.add(home); } // JVM sometimes picks odd location for user.home based on a registry entry // (see http://bugs.sun.com/view_bug.do?bug_id=4787931). Add extra check in // case that entry is wrong. Add before user.home in search list. 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() { for (String home : getDotfileDirectories()) { File dotfile = new File(home + "/.jrubyrc"); if (dotfile.exists()) loadJRubyProperties(dotfile); } } private static void loadJRubyProperties(File dotfile) { FileInputStream fis = null; try { // update system properties with long form jruby properties from .jrubyrc Properties sysProps = System.getProperties(); Properties newProps = new Properties(); // load properties and re-set as jruby.* fis = new FileInputStream(dotfile); newProps.load(fis); for (Map.Entry entry : newProps.entrySet()) { sysProps.put("jruby." + entry.getKey(), entry.getValue()); } } catch (IOException ioe) { LOG.debug("exception loading " + dotfile, ioe); } catch (SecurityException se) { LOG.debug("exception loading " + dotfile, se); } finally { if (fis != null) try {fis.close();} catch (Exception e) {} } } public static class Status { private boolean isExit = false; private int status = 0; /** * Creates a status object with the specified value and with explicit * exit flag. An exit flag means that Kernel.exit() has been explicitly * invoked during the run. * * @param status The status value. */ Status(int status) { this.isExit = true; this.status = status; } /** * Creates a status object with 0 value and no explicit exit flag. */ Status() {} public boolean isExit() { return isExit; } public int getStatus() { return status; } } /** * This is the command-line entry point for JRuby, and should ONLY be used by * Java when starting up JRuby from a command-line. Use other mechanisms when * embedding JRuby into another application. * * @param args command-line args, provided by the JVM. */ 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 rj) { System.exit(handleRaiseException(rj)); } catch (Throwable t) { // print out as a nice Ruby backtrace System.err.println(ThreadContext.createRawBacktraceStringFromThrowable(t)); while ((t = t.getCause()) != null) { System.err.println("Caused by:"); System.err.println(ThreadContext.createRawBacktraceStringFromThrowable(t)); } 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(); if (!config.getShouldRunInterpreter() ) { doPrintUsage(false); doPrintProperties(); return new Status(); } InputStream in = config.getScriptSource(); String filename = config.displayedFileName(); doProcessArguments(in); Ruby runtime; if (DripMain.DRIP_RUNTIME != null) { // use drip's runtime, reinitializing config runtime = DripMain.DRIP_RUNTIME; runtime.reinitialize(true); } else { runtime = Ruby.newInstance(config); } try { doSetContextClassLoader(runtime); if (in == null) { // no script to run, return success return new Status(); } else if (config.isXFlag() && !config.hasShebangLine()) { // no shebang was found and x option is set throw new MainExitException(1, "jruby: no Ruby script found in input (LoadError)"); } else if (config.getShouldCheckSyntax()) { // check syntax only and exit return doCheckSyntax(runtime, in, filename); } else { // proceed to run the script return doRunFromMain(runtime, in, filename); } } finally { runtime.tearDown(); } } private Status handleUnsupportedClassVersion(UnsupportedClassVersionError ucve) { 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()) { config.getError().println("Exception trace follows:"); ucve.printStackTrace(); } else { config.getError().println("Specify -w for full UnsupportedClassVersionError stack trace"); } return new Status(1); } /** * Print a nicer stack size error since Rubyists aren't used to seeing this. */ private Status handleStackOverflow(StackOverflowError soe) { 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()) { config.getError().println("Exception trace follows:"); soe.printStackTrace(config.getError()); } else { config.getError().println("Specify -w for full StackOverflowError stack trace"); } return new Status(1); } /** * Print a nicer memory error since Rubyists aren't used to seeing this. */ private Status handleOutOfMemory(OutOfMemoryError oome) { System.gc(); // try to clean up a bit of space, hopefully, so we can report this error String oomeMessage = oome.getMessage(); if (oomeMessage.contains("PermGen")) { // report permgen memory error config.getError().println("Error: Your application exhausted PermGen area of the heap."); config.getError().println("Specify -J-XX:MaxPermSize=###M to increase it (### = PermGen size in MB)."); } else { // report heap memory error 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 default safety cap."); } config.getError().println("Specify -J-Xmx####m to increase it (#### = cap size in MB)."); } if (config.isVerbose()) { config.getError().println("Exception trace follows:"); oome.printStackTrace(config.getError()); } else { config.getError().println("Specify -w for full OutOfMemoryError 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.getOutput().println(mee.getMessage()); if (mee.isUsageError()) { doPrintUsage(true); } } return new Status(mee.getStatus()); } private Status doRunFromMain(Ruby runtime, InputStream in, String filename) { long now = -1; try { if (config.isBenchmarking()) { now = System.currentTimeMillis(); } if (config.isSamplingEnabled()) { SimpleSampler.startSampleThread(); } doCheckSecurityManager(); try { runtime.runFromMain(in, filename); } finally { if (config.isBenchmarking()) { config.getOutput().println("Runtime: " + (System.currentTimeMillis() - now) + " ms"); } if (config.isSamplingEnabled()) { org.jruby.util.SimpleSampler.report(); } } } catch (RaiseException rj) { return new Status(handleRaiseException(rj)); } return new Status(); } private Status doCheckSyntax(Ruby runtime, InputStream in, String filename) throws RaiseException { // check primary script boolean status = checkStreamSyntax(runtime, in, filename); // check other scripts specified on argv 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) { PrintStream error = config.getError(); try { runtime.parseFromMain(in, filename); error.println("Syntax OK for " + filename); return true; } catch (RaiseException re) { if (re.getException().getMetaClass().getBaseName().equals("SyntaxError")) { error.println("SyntaxError in " + re.getException().message(runtime.getCurrentContext())); } else { throw re; } return false; } } private void doSetContextClassLoader(Ruby runtime) { // set thread context JRuby classloader here, for the main thread try { Thread.currentThread().setContextClassLoader(runtime.getJRubyClassLoader()); } catch (SecurityException se) { // can't set TC classloader if (runtime.getInstanceConfig().isVerbose()) { config.getError().println("WARNING: Security restrictions disallowed setting context classloader for main thread."); } } } private void doProcessArguments(InputStream in) { config.processArguments(config.parseShebangOptions(in)); } 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()); } } private void doShowCopyright() { if (config.isShowCopyright()) { config.getOutput().println(OutputStrings.getCopyrightString()); } } private void doShowVersion() { if (config.isShowVersion()) { config.getOutput().println(OutputStrings.getVersionString(config.getCompatVersion())); } } private static void doGCJCheck() { // Ensure we're not running on GCJ, since it's not supported and leads to weird errors 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."); } } private static int handleRaiseException(RaiseException rj) { RubyException raisedException = rj.getException(); 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); } else { return 0; } } else { System.err.print(runtime.getInstanceConfig().getTraceType().printBacktrace(raisedException, runtime.getPosix().isatty(FileDescriptor.err))); return 1; } } private final RubyInstanceConfig config; }