/******************************************************************************* * Copyright (c) 2012 Sierra Wireless 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: * Sierra Wireless - initial API and implementation *******************************************************************************/ package org.eclipse.koneki.ldt.remote.debug.core.internal.sshprocess; import java.io.IOException; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IStreamsProxy; import org.eclipse.koneki.ldt.remote.debug.core.internal.Activator; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; /** * Implementation of {@link org.eclipse.debug.core.model.IProcess} based on ssh connection */ public class SshProcess implements IProcess { public static final char ARGUMENT_SEPARATOR = ' '; private ILaunch launch; private ChannelExec channelExec; private StreamsProxy sshStreamProxy; private String label; private Session currentSession; private String workingDir; /** * Create and launch a process corresponding to the given command throw an ssh connection<br> * <b>/!\ Session must be connected !</b> * * A .PID file will be create in the working directory for this process.(which contains the PID of this process)<br> * This file is used to be able to stop (kill) the process when needed.<br> * So, User must have the write to create file in the working directory and so 2 process must not be run at the same time in the same working * directory * * @throws CoreException * if exec channel can not be created, or session is down */ public SshProcess(Session session, ILaunch launch, String workingDirectoryPath, String command, Map<String, String> envVars) throws CoreException { this.launch = launch; this.currentSession = session; this.workingDir = workingDirectoryPath; // open exec channel Channel channel; try { channel = session.openChannel("exec"); //$NON-NLS-1$ } catch (JSchException e) { throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to create SShProcess", e)); //$NON-NLS-1$ } if (!(channel instanceof ChannelExec)) throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to create SShProcess")); //$NON-NLS-1$ channelExec = (ChannelExec) channel; this.label = command; // create composed command String composedCommand = createLaunchCommand(workingDirectoryPath, command, envVars); channelExec.setCommand(composedCommand); } /** * Escapes a string so it became a valid Shell token. * * @param s * @return */ public static String escapeShell(String s) { // TODO: there is still some sequences to escape return "\"" + s.replaceAll("\"", Matcher.quoteReplacement("\\\"")) + "\""; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } /** * start the process <br> * don't forget to call Terminate if an error is raised. * * @throws CoreException * if an error occured (session is down, channel is not open,IO Problem on channel's stream) */ public void start() throws CoreException { // start stream monitoring try { sshStreamProxy = new StreamsProxy(channelExec.getInputStream(), channelExec.getErrStream(), channelExec.getOutputStream(), null, null); } catch (IOException e) { this.terminate(); throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to start SShProcess", e)); //$NON-NLS-1$ } // start connection try { channelExec.connect(); } catch (JSchException e) { this.terminate(); throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to start SShProcess", e)); //$NON-NLS-1$ } fireCreationEvent(); // monitor channel state new Thread(new Runnable() { @Override public void run() { boolean connected; do { connected = channelExec.isConnected(); try { Thread.sleep(100); // CHECKSTYLE:OFF } catch (InterruptedException e) { // nothing to do // CHECKSTYLE:ON } } while (connected); sshStreamProxy.kill(); fireTerminateEvent(); } }).start(); } /** * create one command from workingdir, envpath and command */ private String createLaunchCommand(String workingDirectoryPath, String command, Map<String, String> envVars) { // TODO : should works only on linux... StringBuilder composedCommand = new StringBuilder(); // add : move to the working directory composedCommand.append("cd "); //$NON-NLS-1$ composedCommand.append(escapeShell(workingDirectoryPath)); composedCommand.append(" && "); //$NON-NLS-1$ // add : set env path for (Entry<String, String> entrySet : envVars.entrySet()) { composedCommand.append("export "); //$NON-NLS-1$ composedCommand.append(entrySet.getKey()); composedCommand.append("="); //$NON-NLS-1$ composedCommand.append(escapeShell(entrySet.getValue())); composedCommand.append(" && "); //$NON-NLS-1$ } // if a .PID file already exist, we try to kill the corresponding process // and remove the old .PID file // run this command in silent mode (redirect all the output stream in /dev/null) composedCommand.append("("); //$NON-NLS-1$ composedCommand.append("cat .PID > /dev/null 2>&1"); //$NON-NLS-1$ composedCommand.append(" && "); //$NON-NLS-1$ composedCommand.append("kill `cat .PID` > /dev/null 2>&1"); //$NON-NLS-1$ composedCommand.append(" && "); //$NON-NLS-1$ composedCommand.append("rm .PID > /dev/null 2>&1"); //$NON-NLS-1$ composedCommand.append(")"); //$NON-NLS-1$ composedCommand.append("; "); //$NON-NLS-1$ // launch command in background composedCommand.append("{ "); //$NON-NLS-1$ composedCommand.append(command); composedCommand.append(" & }"); //$NON-NLS-1$ composedCommand.append(" && "); //$NON-NLS-1$ // store the PID of the last background task (so the command below) composedCommand.append("echo $! > .PID"); //$NON-NLS-1$ composedCommand.append(" && "); //$NON-NLS-1$ // wait the end of the command execution composedCommand.append("wait $!"); //$NON-NLS-1$ composedCommand.append(" && "); //$NON-NLS-1$ // remove the PID file composedCommand.append("rm .PID"); //$NON-NLS-1$ return composedCommand.toString(); } /** * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) */ @Override public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) { return null; } /** * @see org.eclipse.debug.core.model.ITerminate#canTerminate() */ @Override public boolean canTerminate() { return !isTerminated(); } /** * @see org.eclipse.debug.core.model.ITerminate#isTerminated() */ @Override public boolean isTerminated() { return channelExec.isClosed(); } /** * @see org.eclipse.debug.core.model.ITerminate#terminate() */ @Override public void terminate() throws DebugException { if (!isTerminated()) { SshProcess.killProcess(currentSession, workingDir); } else { sshStreamProxy.kill(); fireTerminateEvent(); } } /** * create the kill command for the current process */ private static String createKillCommand(String pidContainerFolder) { // TODO : should works only on linux... StringBuilder composedCommand = new StringBuilder(); // add : move to the working directory composedCommand.append("cd "); //$NON-NLS-1$ composedCommand.append(escapeShell(pidContainerFolder)); composedCommand.append(" && "); //$NON-NLS-1$ // kill process composedCommand.append("kill `cat .PID`"); //$NON-NLS-1$ composedCommand.append(" && "); //$NON-NLS-1$ composedCommand.append(" rm .PID"); //$NON-NLS-1$ return composedCommand.toString(); } /** * @see org.eclipse.debug.core.model.IProcess#getLabel() */ @Override public String getLabel() { return label; } /** * @see org.eclipse.debug.core.model.IProcess#getLaunch() */ @Override public ILaunch getLaunch() { return launch; } /** * @see org.eclipse.debug.core.model.IProcess#getStreamsProxy() */ @Override public IStreamsProxy getStreamsProxy() { return sshStreamProxy; } /** * @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String, java.lang.String) */ @Override public void setAttribute(String key, String value) { } /** * @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String) */ @Override public String getAttribute(String key) { return null; } /** * @see org.eclipse.debug.core.model.IProcess#getExitValue() */ @Override public int getExitValue() throws DebugException { return channelExec.getExitStatus(); } /** * Fires a creation event. */ protected void fireCreationEvent() { fireEvent(new DebugEvent(this, DebugEvent.CREATE)); } /** * Fires the given debug event. * * @param event * debug event to fire */ protected void fireEvent(DebugEvent event) { DebugPlugin manager = DebugPlugin.getDefault(); if (manager != null) { manager.fireDebugEventSet(new DebugEvent[] { event }); } } /** * Fires a terminate event. */ protected void fireTerminateEvent() { fireEvent(new DebugEvent(this, DebugEvent.TERMINATE)); } /** * Fires a change event. */ protected void fireChangeEvent() { fireEvent(new DebugEvent(this, DebugEvent.CHANGE)); } /** * try to kill the process with the PID equal to the value of the .PID file contained in pidContainerFolder */ public static void killProcess(Session session, String pidContainerFolder) throws DebugException { try { // create a new channel Channel channel = session.openChannel("exec"); //$NON-NLS-1$ if (!(channel instanceof ChannelExec)) throw new JSchException("Unable to create exec channel"); //$NON-NLS-1$ ChannelExec killChannel = (ChannelExec) channel; // create kill command String killCommand = createKillCommand(pidContainerFolder); killChannel.setCommand(killCommand); // execute command killChannel.connect(); // wait the end of command execution int timeout = 5000; // in ms int period = 100; // in ms int i = 0; // counter while (!channel.isClosed() && i * period < timeout) { try { Thread.sleep(period); // CHECKSTYLE:OFF } catch (InterruptedException e) { // nothing to do // CHECKSTYLE:ON } finally { i++; } } // CHECKSTYLE:OFF } catch (Exception e) { // CHECKSTYLE:ON throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "An exception occurred when trying to stop the application.", e)); //$NON-NLS-1$ } } }