package org.rubypeople.rdt.internal.launching; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.IStatusHandler; import org.osgi.framework.Bundle; import org.rubypeople.rdt.launching.IRubyLaunchConfigurationConstants; import org.rubypeople.rdt.launching.IVMInstall; import org.rubypeople.rdt.launching.IVMRunner; import org.rubypeople.rdt.launching.VMRunnerConfiguration; public class JRubyVMRunner extends StandardVMRunner implements IVMRunner { private static final String GEM_MEMORY_HACK = "-J-Xmx512M"; @Override protected String[] combineVmArgs(VMRunnerConfiguration configuration, IVMInstall vmInstall) { String[] result = tryGemMemoryHack(configuration, super.combineVmArgs(configuration, vmInstall)); result = tryRailsRubyPlatformHack(configuration, result); return result; } /** * Hack to set RUBY_PLATFORM to "jruby mswin" on win32 under jruby for rails scripts. * * @param configuration * @param old * @return */ private String[] tryRailsRubyPlatformHack(VMRunnerConfiguration configuration, String[] old) { if (!isWindows()) return old; String file = configuration.getFileToLaunch(); if (file.endsWith("script/server")) { // when we run rails server, we already have the -e load(ARGV.shift) arg if (old.length == 0) { return new String[] { "-e", "RUBY_PLATFORM='java mswin'", "-e", "load(ARGV.shift)" }; } String newArray[] = new String[old.length + 2]; System.arraycopy(old, 0, newArray, 0, old.length); newArray[old.length - 2] = "-e"; newArray[old.length - 1] = "RUBY_PLATFORM='java mswin'"; newArray[old.length] = "-e"; newArray[old.length + 1] = "load(ARGV.shift)"; return newArray; } if (!(file.endsWith("rake") || file.endsWith("script/generate") || file.endsWith("script/plugin") || file.endsWith("script/console") || file.endsWith("script/destroy"))) return old; String newArray[] = new String[old.length + 4]; System.arraycopy(old, 0, newArray, 0, old.length); newArray[old.length] = "-e"; newArray[old.length + 1] = "RUBY_PLATFORM='java mswin'"; newArray[old.length + 2] = "-e"; newArray[old.length + 3] = "load(ARGV.shift)"; return newArray; } /** * This is a hack to bump up max memory to 512M when running gem commands on JRuby. Otherwise it will usually fail * with out of memory errors. * * @param configuration * @param vmInstall * @return */ private String[] tryGemMemoryHack(VMRunnerConfiguration configuration, String[] old) { if (isWindows()) return old; // return since we set this down in constructProgramString on Windows String file = configuration.getFileToLaunch(); if (!file.endsWith("gem")) return old; String newArray[] = new String[old.length + 1]; System.arraycopy(old, 0, newArray, 1, old.length); newArray[0] = GEM_MEMORY_HACK; return newArray; } @Override protected String getCommand(VMRunnerConfiguration config) { String command = super.getCommand(config); if (command == null) return null; // in jruby they preface these commands with a j... if (command.equals("ruby") || command.equals("rubyw") || command.equals("irb")) return "j" + command; return command; } /** * Construct and return a String containing the full path of a ruby executable command such as 'jruby' or 'jrubyw'. * If the configuration specifies an explicit executable, that is used. * * @return full path to ruby executable * @exception CoreException * if unable to locate an executable */ protected List<String> constructProgramString(VMRunnerConfiguration config, IProgressMonitor monitor) throws CoreException { if (!isWindows()) { String installLocation = fVMInstance.getInstallLocation().getAbsolutePath(); File exe = new File(installLocation + File.separatorChar + "bin" + File.separatorChar + "jruby"); //$NON-NLS-1$ if (fileExists(exe)) { // force it to be executable on non windows platforms? try { Process p = setExecutableBit(exe.getAbsolutePath()); p.waitFor(); } catch (InterruptedException e) { LaunchingPlugin.log(e); } } // FIXME Hack to add derby.jar symbolic link in lib folder for non-windows platforms try { String link = installLocation + File.separator + "lib" + File.separator + "derbyclient.jar"; File linkFile = new File(link); if (!linkFile.exists()) { Bundle bundle = Platform.getBundle("com.aptana.ide.libraries"); if (bundle != null) { URL url = FileLocator.find(bundle, new Path("derbyclient.jar"), null); url = FileLocator.toFileURL(url); String path = url.getFile(); File file = new File(path); Process p = createSymbolicLink(file.getAbsolutePath(), link); p.waitFor(); } } } catch (Exception e) { LaunchingPlugin.log(e); } return super.constructProgramString(config, monitor); } List<String> string = new ArrayList<String>(); // On windows we need to launch via "java" string.add("java"); // Build the path to the jruby jar String installLocation = fVMInstance.getInstallLocation().getAbsolutePath(); File exe = new File(installLocation + File.separatorChar + "bin" + File.separatorChar + "jruby.bat"); //$NON-NLS-1$ if (fileExists(exe)) { File lib = new File(fVMInstance.getInstallLocation(), "lib"); String[] jars = lib.list(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); String jarString = ""; for (int i = 0; i < jars.length; i++) { if (i != 0) jarString += File.pathSeparator; jarString += lib.getAbsolutePath() + File.separator + jars[i]; } // FIXME This is a hack for using derby with JRuby. adds derby from aptana plugin to classpath on Windows try { Bundle bundle = Platform.getBundle("com.aptana.ide.libraries"); if (bundle != null) { URL url = FileLocator.find(bundle, new Path("derbyclient.jar"), null); url = FileLocator.toFileURL(url); String path = url.getFile(); File file = new File(path.substring(1)); jarString += File.pathSeparator + file.getAbsolutePath(); } } catch (IOException e) { LaunchingPlugin.log(e); } string.add("-Xverify:none"); if (config.getFileToLaunch().endsWith("gem")) { string.add("-Xmx512m"); } else { string.add("-Xmx256m"); } string.add("-Xss1024k"); string.add("-cp"); string.add("\"" + jarString + "\""); string.add("-Djruby.base=\"" + installLocation + "\""); string.add("-Djruby.home=\"" + installLocation + "\""); string.add("-Djruby.lib=\"" + lib.getAbsolutePath() + "\""); if (isWindows()) string.add("-Djruby.shell=\"cmd.exe\""); else string.add("-Djruby.shell=/bin/sh"); string.add("-Djruby.script=\"" + exe.getName() + "\""); string.add("org.jruby.Main"); return string; } abort(MessageFormat.format( LaunchingMessages.StandardVMRunner_Specified_executable__0__does_not_exist_for__1__4, "jruby.jar", fVMInstance.getName()), null, IRubyLaunchConfigurationConstants.ERR_INTERNAL_ERROR); // NOTE: an exception will be thrown - null cannot be returned return null; } private boolean isWindows() { return Platform.getOS().equals(Platform.OS_WIN32); } @Override protected String[] getEnvironment(VMRunnerConfiguration config) { String[] env = super.getEnvironment(config); if (env == null) env = new String[0]; int itemsToAdd = 4; if (isWindows()) itemsToAdd++; String[] special = new String[env.length + itemsToAdd]; System.arraycopy(env, 0, special, 0, env.length); special[env.length + 0] = "CLASSPATH=."; special[env.length + 1] = "JRUBY_BASE=" + fVMInstance.getInstallLocation().getAbsolutePath(); special[env.length + 2] = "JRUBY_HOME=" + fVMInstance.getInstallLocation().getAbsolutePath(); special[env.length + 3] = "JAVA_HOME=" + System.getProperty("java.home"); String root = System.getenv("SYSTEMROOT"); if (root == null || root.trim().length() == 0) { root = "C:\\Windows"; } if (isWindows()) special[env.length + 4] = "SystemRoot=" + root; // It is absolutely essential that this is set for JRuby to // work properly! Otherwise you will get problems running // rails server "initialize: name or service unknown"! return special; } @Override protected void addStreamSync(List<String> arguments) { // do nothing } /** * @since 0.9.0 * @see DebugPlugin#exec(String[], File, String[]) */ protected Process exec(String[] cmdLine, File workingDirectory, String[] envp) throws CoreException { String cmd = getCmdLineAsString(cmdLine); LaunchingPlugin.info("Starting: " + cmd); Process p = null; try { if (isWindows()) { // have to use quoted strings on windows when paths contain spaces if (workingDirectory == null) { p = Runtime.getRuntime().exec(cmd, envp); } else { p = Runtime.getRuntime().exec(cmd, envp, workingDirectory); } } else { // on mac (and hopefully otehr OSes), have to use array of command line strings when parts contain spaces if (workingDirectory == null) { p = Runtime.getRuntime().exec(cmdLine, envp); } else { p = Runtime.getRuntime().exec(cmdLine, envp, workingDirectory); } } } catch (IOException e) { Status status = new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugPlugin.INTERNAL_ERROR, LaunchingMessages.DebugPlugin_Exception_occurred_executing_command_line__1, e); throw new CoreException(status); } catch (NoSuchMethodError e) { // attempting launches on 1.2.* - no ability to set working directory IStatus status = new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugPlugin.ERR_WORKING_DIRECTORY_NOT_SUPPORTED, LaunchingMessages.DebugPlugin_Eclipse_runtime_does_not_support_working_directory_2, e); IStatusHandler handler = DebugPlugin.getDefault().getStatusHandler(status); if (handler != null) { Object result = handler.handleStatus(status, null); if (result instanceof Boolean && ((Boolean) result).booleanValue()) { p = exec(cmdLine, null); } } } return p; } private Process setExecutableBit(String filePath) { LaunchingPlugin.log("Setting executable bit for: " + filePath); if (filePath == null) return null; try { Process pr = Runtime.getRuntime().exec(new String[] { "chmod", "a+x", filePath }); //$NON-NLS-1$ //$NON-NLS-2$ Thread chmodOutput = new StreamConsumer(pr.getInputStream()); chmodOutput.setName("chmod output reader"); //$NON-NLS-1$ chmodOutput.start(); Thread chmodError = new StreamConsumer(pr.getErrorStream()); chmodError.setName("chmod error reader"); //$NON-NLS-1$ chmodError.start(); return pr; } catch (IOException ioe) { LaunchingPlugin.log(ioe); } return null; } private Process createSymbolicLink(String target, String linkLocation) { LaunchingPlugin.log("Creating symbolic link from: " + linkLocation + " to: " + target); if (target == null || linkLocation == null) return null; try { Process pr = Runtime.getRuntime().exec(new String[] { "ln", "-s", target, linkLocation }); //$NON-NLS-1$ //$NON-NLS-2$ Thread chmodOutput = new StreamConsumer(pr.getInputStream()); chmodOutput.setName("ln -s output reader"); //$NON-NLS-1$ chmodOutput.start(); Thread chmodError = new StreamConsumer(pr.getErrorStream()); chmodError.setName("ln - error reader"); //$NON-NLS-1$ chmodError.start(); return pr; } catch (IOException ioe) { LaunchingPlugin.log(ioe); } return null; } public static class StreamConsumer extends Thread { InputStream is; byte[] buf; public StreamConsumer(InputStream inputStream) { super(); this.setDaemon(true); this.is = inputStream; buf = new byte[512]; } public void run() { try { int n = 0; while (n >= 0) n = is.read(buf); } catch (IOException ioe) { } } } }