/******************************************************************************* * Copyright (c) 2004 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Bjorn Freeman-Benson - initial API and implementation *******************************************************************************/ package com.aptana.ruby.internal.debug.core.launching; import java.io.File; import java.io.IOException; import java.net.ServerSocket; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; 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.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.LaunchConfigurationDelegate; import com.aptana.core.ShellExecutable; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.ResourceUtil; import com.aptana.core.util.StringUtil; import com.aptana.ruby.debug.core.RubyDebugCorePlugin; import com.aptana.ruby.debug.core.launching.IRubyLaunchConfigurationConstants; import com.aptana.ruby.internal.debug.core.RubyDebuggerProxy; import com.aptana.ruby.internal.debug.core.model.RubyDebugTarget; import com.aptana.ruby.internal.debug.core.model.RubyProcessingException; import com.aptana.ruby.launching.RubyLaunchingPlugin; /** * Launches Ruby program on a Ruby interpreter */ public class RubyDebuggerLaunchDelegate extends LaunchConfigurationDelegate { private static final String RDEBUG_IDE = "rdebug-ide"; //$NON-NLS-1$ private static final String DEBUGGER_PORT_SWITCH = "--port"; //$NON-NLS-1$ /** * Switch/arguments that tells ruby/debugger that we're done passing switches/arguments to it. */ private static final String END_OF_ARGUMENTS_DELIMETER = "--"; //$NON-NLS-1$ /* * (non-Javadoc) * @see * org.eclipse.debug.core.model.ILaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, * java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor) */ public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException { List<String> commandList = new ArrayList<String>(); // Ruby binary IPath rubyExecutablePath = rubyExecutable(configuration); commandList.add(rubyExecutablePath.toOSString()); // Arguments to ruby commandList.addAll(interpreterArguments(rubyExecutablePath, configuration)); // Set up debugger String host = configuration.getAttribute(IRubyLaunchConfigurationConstants.ATTR_REMOTE_HOST, IRubyLaunchConfigurationConstants.DEFAULT_REMOTE_HOST); int port = -1; if (mode.equals(ILaunchManager.DEBUG_MODE)) { // TODO Grab port from configuration? port = findFreePort(); if (port == -1) { abort(Messages.RubyDebuggerLaunchDelegate_0, null); } commandList.addAll(debugArguments(rubyExecutablePath, host, port, configuration)); } IPath workingDir = getWorkingDirectory(configuration); // File to run // if the file to launch is "Rakefile", we actually need to run "rake" on the parent String fileToLaunch = fileToLaunch(configuration); if (fileToLaunch.equals(RubyLaunchingPlugin.RAKEFILE) || fileToLaunch.endsWith(File.separator + RubyLaunchingPlugin.RAKEFILE)) { IPath rakeFilePath = Path.fromOSString(fileToLaunch); IPath parent = rakeFilePath.removeLastSegments(1); IPath rakePath = RubyLaunchingPlugin.getRakePath(parent); String rakePathString = (rakePath == null) ? RubyLaunchingPlugin.RAKE : rakePath.toOSString(); commandList.add(rakePathString); workingDir = parent; } else { commandList.add(fileToLaunch); } // Args to file commandList.addAll(programArguments(configuration)); // Now actually launch the process! Process process = DebugPlugin.exec(commandList.toArray(new String[commandList.size()]), (workingDir == null) ? null : workingDir.toFile(), getEnvironment(configuration)); // FIXME Build a label from args? String label = commandList.get(0); // Set process type to "ruby" so our linetracker hyperlink stuff works Map<String, String> map = new HashMap<String, String>(); map.put(IProcess.ATTR_PROCESS_TYPE, IRubyLaunchConfigurationConstants.PROCESS_TYPE); IProcess p = DebugPlugin.newProcess(launch, process, label, map); if (mode.equals(ILaunchManager.DEBUG_MODE)) { RubyDebugTarget target = new RubyDebugTarget(launch, host, port); target.setProcess(p); RubyDebuggerProxy proxy = new RubyDebuggerProxy(target, true); try { proxy.start(); launch.addDebugTarget(target); } catch (RubyProcessingException e) { RubyDebugCorePlugin.log(e); target.terminate(); } catch (IOException e) { RubyDebugCorePlugin.log(e); target.terminate(); } } } private Collection<? extends String> programArguments(ILaunchConfiguration configuration) throws CoreException { List<String> commandList = new ArrayList<String>(); String programArgs = configuration.getAttribute(IRubyLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, (String) null); if (programArgs != null) { for (String arg : DebugPlugin.parseArguments(programArgs)) { commandList.add(arg); } } return commandList; } private String fileToLaunch(ILaunchConfiguration configuration) throws CoreException { String program = configuration.getAttribute(IRubyLaunchConfigurationConstants.ATTR_FILE_NAME, (String) null); if (program == null) { abort(Messages.RubyDebuggerLaunchDelegate_1, null); } // check for absolute path File file = new File(program); if (file.exists()) return program; // check for relative to workspace root IFile iFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(program)); if (iFile == null || !iFile.exists()) { abort(MessageFormat.format(Messages.RubyDebuggerLaunchDelegate_2, program), null); } // TODO What about checking relative to working dir? return iFile.getLocation().toOSString(); } private Collection<? extends String> debugArguments(IPath rubyExecutablePath, String host, int port, ILaunchConfiguration configuration) throws CoreException { IPath workingDir = getWorkingDirectory(configuration); List<String> commandList = new ArrayList<String>(); IPath rdebug = RubyLaunchingPlugin.getBinaryScriptPath(RDEBUG_IDE, getRDebugIDELocations(rubyExecutablePath), workingDir); // FIXME What if user is using RVM? We need to respect which version of rdebug-ide we need to use! if (rdebug == null) { abort(Messages.RubyDebuggerLaunchDelegate_3, null); } commandList.add(rdebug.toOSString()); commandList.add(DEBUGGER_PORT_SWITCH); commandList.add(Integer.toString(port)); commandList.add(END_OF_ARGUMENTS_DELIMETER); return commandList; } private List<IPath> getRDebugIDELocations(IPath rubyExecutablePath) { List<IPath> locations = new ArrayList<IPath>(); // check in bin dir alongside where our ruby exe is! if (rubyExecutablePath != null) { locations.add(rubyExecutablePath.removeLastSegments(1)); } // TODO Check gem executable directory! (we need to get this from 'gem environment') locations.add(Path.fromOSString(System.getProperty("user.home")).append(".gem/ruby/1.8/bin")); //$NON-NLS-1$ //$NON-NLS-2$ locations.add(Path.fromOSString(System.getProperty("user.home")).append(".gem/ruby/1.9/bin")); //$NON-NLS-1$ //$NON-NLS-2$ locations.add(Path.fromOSString("/opt/local/bin")); //$NON-NLS-1$ locations.add(Path.fromOSString("/usr/local/bin")); //$NON-NLS-1$ locations.add(Path.fromOSString("/usr/bin")); //$NON-NLS-1$ locations.add(Path.fromOSString("/bin")); //$NON-NLS-1$ return locations; } private Collection<? extends String> interpreterArguments(IPath rubyExecutablePath, ILaunchConfiguration configuration) throws CoreException { List<String> arguments = new ArrayList<String>(); // Add special VM args if we're under jruby! String rubyVersion = RubyLaunchingPlugin.getRubyVersion(rubyExecutablePath); if (rubyVersion != null && (rubyVersion.contains("jruby") || rubyVersion.contains("java"))) //$NON-NLS-1$ //$NON-NLS-2$ { arguments.add("--debug"); //$NON-NLS-1$ arguments.add("-X+O"); //$NON-NLS-1$ } String interpreterArgs = configuration.getAttribute(IRubyLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, (String) null); if (interpreterArgs != null) { String[] raw = DebugPlugin.parseArguments(interpreterArgs); for (int i = 0; i < raw.length; i++) { String arg = raw[i]; if ((arg.equals("-e") || arg.equals("-X") || arg.equals("-F")) && (raw.length > (i + 1))) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ { arguments.add(arg + " " + raw[i + 1]); //$NON-NLS-1$ i++; } else { arguments.add(arg); } } } URL url = FileLocator.find(RubyLaunchingPlugin.getDefault().getBundle(), Path.fromPortableString("ruby/sync.rb"), null); //$NON-NLS-1$ try { File file = ResourceUtil.resourcePathToFile(url); String filePath = file.getParent(); arguments.add("-I"); //$NON-NLS-1$ arguments.add(filePath); arguments.add("-rsync"); //$NON-NLS-1$ } catch (Exception e) { IdeLog.logError(RubyLaunchingPlugin.getDefault(), e); } arguments.add(END_OF_ARGUMENTS_DELIMETER); return arguments; } protected IPath rubyExecutable(ILaunchConfiguration configuration) throws CoreException { IPath path = RubyLaunchingPlugin.rubyExecutablePath(getWorkingDirectory(configuration)); // TODO If we can't find one, should we just try plain "ruby"? if (path == null) { abort(Messages.RubyDebuggerLaunchDelegate_13, null); } if (!path.toFile().exists()) { abort(MessageFormat.format(Messages.RubyDebuggerLaunchDelegate_14, path), null); } return path; } private String[] getEnvironment(ILaunchConfiguration configuration) throws CoreException { Map<String, String> env = new HashMap<String, String>(); // Only grab shell environment if we're not on Windows. This isn't running inside cygwin! if (!Platform.OS_WIN32.equals(Platform.getOS())) { env.putAll(ShellExecutable.getEnvironment(getWorkingDirectory(configuration))); } String[] envp = DebugPlugin.getDefault().getLaunchManager().getEnvironment(configuration); if (envp != null) { for (String envstring : envp) { if (envstring.indexOf((int) '\u0000') != -1) { envstring = envstring.replaceFirst("\u0000.*", StringUtil.EMPTY); //$NON-NLS-1$ } int eqlsign = envstring.indexOf('='); if (eqlsign != -1) { env.put(envstring.substring(0, eqlsign), envstring.substring(eqlsign + 1)); } } } if (env.isEmpty()) { return null; } List<String> list = new ArrayList<String>(); for (Map.Entry<String, String> entry : env.entrySet()) { list.add(entry.getKey() + "=" + entry.getValue()); //$NON-NLS-1$ } return list.toArray(new String[list.size()]); } /** * Return a File pointing at the working directory for the launch. Return null if no value specified, or specified * location does not exist or is not a directory. * * @param configuration * @return * @throws CoreException */ protected IPath getWorkingDirectory(ILaunchConfiguration configuration) throws CoreException { // TODO Cache once we grab it once? String workingDirVal = configuration.getAttribute(IRubyLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, (String) null); if (workingDirVal == null) { return null; } IPath workingDirectory = Path.fromOSString(workingDirVal); if (!workingDirectory.toFile().isDirectory()) { IdeLog.logError(RubyDebugCorePlugin.getDefault(), "Specified working directory does not appear to be a valid directory: " //$NON-NLS-1$ + workingDirVal); return null; } return workingDirectory; } /** * Throws an exception with a new status containing the given message and optional exception. * * @param message * error message * @param e * underlying exception * @throws CoreException */ private void abort(String message, Throwable e) throws CoreException { throw new CoreException(new Status(IStatus.ERROR, IRubyLaunchConfigurationConstants.ID_RUBY_DEBUG_MODEL, 0, message, e)); } /** * Returns a free port number on localhost, or -1 if unable to find a free port. * * @return a free port number on localhost, or -1 if unable to find a free port */ private static int findFreePort() { ServerSocket socket = null; try { socket = new ServerSocket(0); return socket.getLocalPort(); } catch (IOException e) { } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { } } } return -1; } }