/******************************************************************************* * Copyright (c) 2000, 2006 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.rubypeople.rdt.internal.launching; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.text.DateFormat; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.model.IProcess; import org.osgi.framework.Version; import org.rubypeople.rdt.launching.AbstractVMRunner; import org.rubypeople.rdt.launching.IRubyLaunchConfigurationConstants; import org.rubypeople.rdt.launching.IVMInstall; import org.rubypeople.rdt.launching.VMRunnerConfiguration; public class StandardVMRunner extends AbstractVMRunner { private static final String SUDO_PROMPT = "Password:"; public static final String STREAM_FLUSH_SCRIPT = "rdt_stream_sync.rb"; private static final String LOADPATH_SWITCH = "-I"; protected static final String END_OF_OPTIONS_DELIMITER = "--"; protected boolean isVMArgs = true; protected String renderDebugTarget(String classToRun, int host) { String format = LaunchingMessages.StandardVMRunner__0__at_localhost__1__1; return MessageFormat.format(format, classToRun, String.valueOf(host)); } public static String renderProcessLabel(String[] commandLine) { String format = LaunchingMessages.StandardVMRunner__0____1___2; String timestamp = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format( new Date(System.currentTimeMillis())); return MessageFormat.format(format, commandLine[0], timestamp); } protected static String renderCommandLine(String[] commandLine) { if (commandLine == null || commandLine.length < 1) return ""; //$NON-NLS-1$ StringBuffer buf = new StringBuffer(); for (int i = 0; i < commandLine.length; i++) { if (commandLine[i] == null) continue; buf.append(' '); char[] characters = commandLine[i].toCharArray(); StringBuffer command = new StringBuffer(); boolean containsSpace = false; for (int j = 0; j < characters.length; j++) { char character = characters[j]; if (character == '\"') { command.append('\\'); } else if (character == ' ') { containsSpace = true; } command.append(character); } if (containsSpace) { buf.append('\"'); buf.append(command.toString()); buf.append('\"'); } else { buf.append(command.toString()); } } return buf.toString(); } protected void addArguments(String[] args, List<String> v) { if (args == null) { return; } for (int i = 0; i < args.length; i++) { v.add(args[i]); } } /** * Returns the working directory to use for the launched VM, or <code>null</code> if the working directory is to be * inherited from the current process. * * @return the working directory to use * @exception CoreException * if the working directory specified by the configuration does not exist or is not a directory */ protected File getWorkingDir(VMRunnerConfiguration config) throws CoreException { String path = config.getWorkingDirectory(); if (path == null) { return null; } File dir = new File(path); if (!dir.isDirectory()) { abort( MessageFormat .format( LaunchingMessages.StandardVMRunner_Specified_working_directory_does_not_exist_or_is_not_a_directory___0__3, path), null, IRubyLaunchConfigurationConstants.ERR_WORKING_DIRECTORY_DOES_NOT_EXIST); } return dir; } /** * @see VMRunner#getPluginIdentifier() */ protected String getPluginIdentifier() { return LaunchingPlugin.getUniqueIdentifier(); } /** * Construct and return a String containing the full path of a ruby executable command such as 'ruby' or * 'rubyw.exe'. 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 { List<String> string = new ArrayList<String>(); if (!Platform.getOS().equals(Platform.OS_WIN32) && config.isSudo()) { forceBackgroundSudoCommand(config, monitor); string.add("sudo"); } // Look for the user-specified ruby executable command String command = getCommand(config); // If no ruby command was specified, use default executable if (command == null) { File exe = fVMInstance.getVMInstallType().findExecutable(fVMInstance.getInstallLocation()); if (exe == null) { abort(MessageFormat.format(LaunchingMessages.StandardVMRunner_Unable_to_locate_executable_for__0__1, fVMInstance.getName()), null, IRubyLaunchConfigurationConstants.ERR_INTERNAL_ERROR); } string.add(exe.getAbsolutePath()); return string; } // Build the path to the ruby executable. String installLocation = fVMInstance.getInstallLocation().getAbsolutePath() + File.separatorChar; File originalExe = new File(installLocation + "bin" + File.separatorChar + command); //$NON-NLS-1$ File exe = originalExe; if (fileExists(exe)) { string.add(exe.getAbsolutePath()); return string; } exe = new File(exe.getAbsolutePath() + ".exe"); //$NON-NLS-1$ if (fileExists(exe)) { string.add(exe.getAbsolutePath()); return string; } // Also try the 1.8 or 1.9 suffix versions... String version = fVMInstance.getRubyVersion(); Version versionObj = new Version(version); exe = new File(originalExe.getAbsolutePath() + versionObj.getMajor() + "." + versionObj.getMinor()); //$NON-NLS-1$ if (fileExists(exe)) { string.add(exe.getAbsolutePath()); return string; } exe = new File(exe.getAbsolutePath() + ".exe"); //$NON-NLS-1$ if (fileExists(exe)) { string.add(exe.getAbsolutePath()); return string; } // not found abort(MessageFormat.format( LaunchingMessages.StandardVMRunner_Specified_executable__0__does_not_exist_for__1__4, command, fVMInstance.getName()), null, IRubyLaunchConfigurationConstants.ERR_INTERNAL_ERROR); // NOTE: an exception will be thrown - null cannot be returned return null; } protected void forceBackgroundSudoCommand(VMRunnerConfiguration config, IProgressMonitor monitor) throws CoreException { // Force a hidden launch under sudo and feed it the password! final Process p = exec(new String[] { "sudo", "-S", "-p", SUDO_PROMPT, "echo", "forced" }, null); final InputStream errorStream = p.getErrorStream(); final String sudoMsg = config.getSudoMessage(); final boolean[] doneWaiting = new boolean[1]; doneWaiting[0] = false; final int[] exitValue = new int[1]; exitValue[0] = 0; Job processWaiter = new Job("Wait on sudo process") { @Override protected IStatus run(IProgressMonitor monitor) { try { exitValue[0] = p.waitFor(); } catch (InterruptedException e) { LaunchingPlugin.log(e); } doneWaiting[0] = true; return Status.OK_STATUS; } }; processWaiter.setSystem(true); processWaiter.schedule(); Job job = new Job("Read error stream") { @Override protected IStatus run(IProgressMonitor monitor) { StringBuffer buffer = new StringBuffer(); String lineDelimeter = "\n"; while (true) { try { int value = errorStream.read(); if (value == -1) break; if (monitor.isCanceled()) return Status.CANCEL_STATUS; buffer.append((char) value); if (buffer.toString().contains(SUDO_PROMPT)) { buffer.delete(0, buffer.length()); String pw = Sudo.getPassword(sudoMsg); // TODO If user cancels prompt we should really kill the process, and abort trying to // launch! p.getOutputStream().write((pw + lineDelimeter).getBytes()); p.getOutputStream().flush(); } } catch (IOException e) { LaunchingPlugin.log(e); } } return Status.OK_STATUS; } }; job.setSystem(true); job.schedule(); while(true) { if (monitor != null && monitor.isCanceled()) return; Thread.yield(); if (doneWaiting[0]) break; } if (exitValue[0] != 0) { job.cancel(); // Yikes. Bail out entirely, user failed to provide a good sudo password and sudo already gave up on them (usually 3 tries) IStatus status = new Status(IStatus.ERROR, LaunchingPlugin.PLUGIN_ID, -1, "Failed to provide correct sudo password", null); throw new CoreException(status); } } protected String getCommand(VMRunnerConfiguration config) { String command = null; Map map = config.getVMSpecificAttributesMap(); if (map != null) { command = (String) map.get(IRubyLaunchConfigurationConstants.ATTR_RUBY_COMMAND); } return command; } protected boolean fileExists(File file) { return file.exists() && file.isFile(); } protected List<String> convertLoadPath(VMRunnerConfiguration config, String[] lp) { String working = null; try { File workingDir = getWorkingDir(config); if (workingDir != null) working = workingDir.getAbsolutePath(); } catch (CoreException e) { // ignore } List<String> strings = new ArrayList<String>(); for (int i = 0; i < lp.length; i++) { String path = lp[i]; // Don't add project to loadpath if project is working directory if (working != null && working.equals(path)) continue; strings.add(LOADPATH_SWITCH); //$NON-NLS-1$ strings.add(path); } return strings; } /* * (non-Javadoc) * @see org.rubypeople.rdt.launching.IVMRunner#run(org.rubypeople.rdt.launching. VMRunnerConfiguration, * org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor) */ public void run(VMRunnerConfiguration config, ILaunch launch, IProgressMonitor monitor) throws CoreException { if (monitor == null) { monitor = new NullProgressMonitor(); } IProgressMonitor subMonitor = new SubProgressMonitor(monitor, 1); subMonitor.beginTask(LaunchingMessages.StandardVMRunner_Launching_VM____1, 2); subMonitor.subTask(LaunchingMessages.StandardVMRunner_Constructing_command_line____2); List<String> arguments = constructProgramString(config, monitor); // VM args are the first thing after the ruby program so that users can // specify // options like '-client' & '-server' which are required to be the first // option String[] allVMArgs = combineVmArgs(config, fVMInstance); addArguments(allVMArgs, arguments); String[] lp = config.getLoadPath(); if (lp.length > 0) { arguments.addAll(convertLoadPath(config, lp)); } addStreamSync(arguments); arguments.add(END_OF_OPTIONS_DELIMITER); arguments.add(getFileToLaunch(config)); addArguments(config.getProgramArguments(), arguments); String[] cmdLine = new String[arguments.size()]; arguments.toArray(cmdLine); String[] envp = getEnvironment(config); subMonitor.worked(1); // check for cancellation if (monitor.isCanceled()) { return; } subMonitor.subTask(LaunchingMessages.StandardVMRunner_Starting_virtual_machine____3); Process p = null; File workingDir = getWorkingDir(config); if (envp != null && envp.length > 0) { p = exec(cmdLine, workingDir, envp); } else { p = exec(cmdLine, workingDir); } if (p == null) { return; } // check for cancellation if (monitor.isCanceled()) { p.destroy(); return; } IProcess process = newProcess(launch, p, renderProcessLabel(cmdLine), getDefaultProcessMap()); process.setAttribute(IProcess.ATTR_CMDLINE, renderCommandLine(cmdLine)); process.setAttribute(IRubyLaunchConfigurationConstants.ATTR_PROJECT_NAME, launch .getAttribute(IRubyLaunchConfigurationConstants.ATTR_PROJECT_NAME)); process.setAttribute(IRubyLaunchConfigurationConstants.ATTR_REQUIRES_REFRESH, launch .getAttribute(IRubyLaunchConfigurationConstants.ATTR_REQUIRES_REFRESH)); subMonitor.worked(1); subMonitor.done(); } /** * Grabs file to launch from config, but also will convert backward slashes to forward if we're running a cygwin * interpreter * * @param config * @return */ protected String getFileToLaunch(VMRunnerConfiguration config) { String file = config.getFileToLaunch(); if (fVMInstance.getPlatform().equals(IVMInstall.CYWGIN_PLATFORM)) { file = file.replace('\\', '/'); } return file; } protected String[] getEnvironment(VMRunnerConfiguration config) { String[] envp = config.getEnvironment(); if (Platform.getOS().equals(Platform.OS_WIN32)) { return envp; } List<String> newEnv = new ArrayList<String>(); Map<String, String> environment = System.getenv(); for (String key : environment.keySet()) { String value = environment.get(key); if (key.equalsIgnoreCase("PATH")) { File exe = fVMInstance.getVMInstallType().findExecutable(fVMInstance.getInstallLocation()); value = exe.getParent() + ":/opt/local/bin:/usr/local/bin:" + value; } newEnv.add(key + "=" + value); } int length = (envp == null) ? 0 : envp.length; for (int i = 0; i < length; i++) { newEnv.add(envp[i]); } return newEnv.toArray(new String[newEnv.size()]); } protected void addStreamSync(List<String> arguments) { File sync = LaunchingPlugin.getFileInPlugin(new Path("ruby").append("flush").append(STREAM_FLUSH_SCRIPT)); arguments.add(LOADPATH_SWITCH); arguments.add(sync.getParent()); arguments.add("-r" + STREAM_FLUSH_SCRIPT); } }