From 49c9ea405a04d085fb01e83959db0a165cbe039e Mon Sep 17 00:00:00 2001 From: Martin Oberhuber < martin.oberhuber@windriver.com> Date: Tue, 30 May 2006 19:09:02 +0000 Subject: [PATCH] bug 143417 - Fix notifications for lost ssh sessions --- .../ssh/SshConnectorService.java | 282 +++++++++++++++++- .../org.eclipse.rse.services.ssh/readme.txt | 22 +- .../rse/services/ssh/ISshSessionProvider.java | 5 + .../services/ssh/files/SftpFileService.java | 31 +- .../rse/services/ssh/shell/SshHostShell.java | 17 +- .../ssh/shell/SshShellOutputReader.java | 6 + 6 files changed, 332 insertions(+), 31 deletions(-) diff --git a/rse/plugins/org.eclipse.rse.connectorservice.ssh/src/org/eclipse/rse/connectorservice/ssh/SshConnectorService.java b/rse/plugins/org.eclipse.rse.connectorservice.ssh/src/org/eclipse/rse/connectorservice/ssh/SshConnectorService.java index 0ec908c34b8..39fdc70f4d9 100644 --- a/rse/plugins/org.eclipse.rse.connectorservice.ssh/src/org/eclipse/rse/connectorservice/ssh/SshConnectorService.java +++ b/rse/plugins/org.eclipse.rse.connectorservice.ssh/src/org/eclipse/rse/connectorservice/ssh/SshConnectorService.java @@ -14,6 +14,7 @@ package org.eclipse.rse.connectorservice.ssh; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; import java.net.Socket; import java.net.UnknownHostException; @@ -22,17 +23,32 @@ import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.rse.core.SystemBasePlugin; import org.eclipse.rse.core.subsystems.AbstractConnectorService; +import org.eclipse.rse.core.subsystems.CommunicationsEvent; +import org.eclipse.rse.core.subsystems.IConnectorService; +import org.eclipse.rse.core.subsystems.SubSystemConfiguration; import org.eclipse.rse.model.IHost; +import org.eclipse.rse.model.ISystemRegistry; +import org.eclipse.rse.services.clientserver.messages.SystemMessage; import org.eclipse.rse.services.ssh.ISshSessionProvider; +import org.eclipse.rse.ui.ISystemMessages; +import org.eclipse.rse.ui.RSEUIPlugin; +import org.eclipse.rse.ui.messages.SystemMessageDialog; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; import org.eclipse.team.internal.ccvs.core.CVSProviderPlugin; import org.eclipse.team.internal.ccvs.core.util.Util; import org.eclipse.team.internal.ccvs.ssh2.CVSSSH2Plugin; import org.eclipse.team.internal.ccvs.ssh2.ISSHContants; import org.eclipse.team.internal.ccvs.ui.KeyboardInteractiveDialog; import org.eclipse.team.internal.ccvs.ui.UserValidationDialog; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; @@ -52,6 +68,7 @@ public class SshConnectorService extends AbstractConnectorService implements ISs private static final int SSH_DEFAULT_PORT = 22; private static JSch jsch=new JSch(); private Session session; + private SessionLostHandler fSessionLostHandler; public SshConnectorService(IHost host) { //TODO the port parameter doesnt really make sense here since @@ -59,6 +76,7 @@ public class SshConnectorService extends AbstractConnectorService implements ISs //setPort() on our base class -- I assume the port is meant to //be a local port. super("SSH Connector Service", "SSH Connector Service Description", host, 0); + fSessionLostHandler = null; } //---------------------------------------------------------------------- @@ -243,35 +261,61 @@ public class SshConnectorService extends AbstractConnectorService implements ISs session.setTimeout(getSshTimeoutInMillis()); String password = getPasswordInformation().getPassword(); session.setPassword(password); - MyUserInfo ui = new MyUserInfo(user, password); - session.setUserInfo(ui); + MyUserInfo userInfo = new MyUserInfo(user, password); + session.setUserInfo(userInfo); session.setSocketFactory(new ResponsiveSocketFacory(monitor)); //java.util.Hashtable config=new java.util.Hashtable(); //config.put("StrictHostKeyChecking", "no"); //session.setConfig(config); - ui.aboutToConnect(); + userInfo.aboutToConnect(); try { - Activator.trace("connecting..."); //$NON-NLS-1$ + Activator.trace("SshConnectorService.connecting..."); //$NON-NLS-1$ session.connect(); - Activator.trace("connected"); //$NON-NLS-1$ + Activator.trace("SshConnectorService.connected"); //$NON-NLS-1$ } catch (JSchException e) { - Activator.trace("connect failed: "+e.toString()); //$NON-NLS-1$ + Activator.trace("SshConnectorService.connect failed: "+e.toString()); //$NON-NLS-1$ if (session.isConnected()) session.disconnect(); throw e; } - ui.connectionMade(); + userInfo.connectionMade(); + fSessionLostHandler = new SessionLostHandler(this); + notifyConnection(); } - public void internalDisconnect(IProgressMonitor monitor) + public void internalDisconnect(IProgressMonitor monitor) throws Exception { - //TODO: Check, Is disconnect being called because the network (connection) went down? - //TODO: Fire communication event (aboutToDisconnect) -- see DStoreConnectorService.internalDisconnect() - //TODO: Wrap exception in an InvocationTargetException -- see DStoreConnectorService.internalDisconnect() - //Will services like the sftp service be disconnected too? Or notified? - Activator.trace("disconnect"); //$NON-NLS-1$ - session.disconnect(); + //TODO Will services like the sftp service be disconnected too? Or notified? + Activator.trace("SshConnectorService.disconnect"); //$NON-NLS-1$ + try + { + if (session != null) { + // Is disconnect being called because the network (connection) went down? + if (fSessionLostHandler != null && fSessionLostHandler.isSessionLost()) { + notifyError(); + } + else { + // Fire comm event to signal state about to change + fireCommunicationsEvent(CommunicationsEvent.BEFORE_DISCONNECT); + } + + session.disconnect(); + + // Fire comm event to signal state changed + notifyDisconnection(); + //TODO MOB - keep the session to avoid NPEs in services (disables gc for the session!) + // session = null; + fSessionLostHandler = null; + // DKM - no need to clear uid cache + clearPasswordCache(false); // clear in-memory password + //clearUserIdCache(); // Clear any cached local user IDs + } + } + catch (Exception exc) + { + throw new java.lang.reflect.InvocationTargetException(exc); + } } //TODO avoid having jsch type "Session" in the API. @@ -282,7 +326,206 @@ public class SshConnectorService extends AbstractConnectorService implements ISs return session; } - private static Display getStandardDisplay() { + /** + * Handle session-lost events. + * This is generic for any sort of connector service. + * Most of this is extracted from dstore's ConnectionStatusListener. + * + * TODO should be refactored to make it generally available, and allow + * dstore to derive from it. + */ + public static class SessionLostHandler implements Runnable, IRunnableWithProgress + { + private IConnectorService _connection; + private boolean fSessionLost; + + public SessionLostHandler(IConnectorService cs) + { + _connection = cs; + fSessionLost = false; + } + + /** + * Notify that the connection has been lost. This may be called + * multiple times from multiple subsystems. The SessionLostHandler + * ensures that actual user feedback and disconnect actions are + * done only once, on the first invocation. + */ + public void sessionLost() + { + //avoid duplicate execution of sessionLost + boolean showSessionLostDlg=false; + synchronized(this) { + if (!fSessionLost) { + fSessionLost = true; + showSessionLostDlg=true; + } + } + if (showSessionLostDlg) { + Display.getDefault().asyncExec(this); + } + } + + public synchronized boolean isSessionLost() { + return fSessionLost; + } + + public void run() + { + Shell shell = getShell(); + //TODO need a more correct message for "session lost" + //TODO allow users to reconnect from this dialog + //SystemMessage msg = RSEUIPlugin.getPluginMessage(ISystemMessages.MSG_CONNECT_UNKNOWNHOST); + SystemMessage msg = RSEUIPlugin.getPluginMessage(ISystemMessages.MSG_CONNECT_CANCELLED); + msg.makeSubstitution(_connection.getPrimarySubSystem().getHost().getAliasName()); + SystemMessageDialog dialog = new SystemMessageDialog(getShell(), msg); + dialog.open(); + try + { + IRunnableContext runnableContext = getRunnableContext(getShell()); + // will do this.run(IProgressMonitor mon) + runnableContext.run(false,true,this); // inthread, cancellable, IRunnableWithProgress + _connection.reset(); + ISystemRegistry sr = RSEUIPlugin.getDefault().getSystemRegistry(); + sr.connectedStatusChange(_connection.getPrimarySubSystem(), false, true, true); + } + catch (InterruptedException exc) // user cancelled + { + if (shell != null) + showDisconnectCancelledMessage(shell, _connection.getHostName(), _connection.getPort()); + } + catch (java.lang.reflect.InvocationTargetException invokeExc) // unexpected error + { + Exception exc = (Exception)invokeExc.getTargetException(); + if (shell != null) + showDisconnectErrorMessage(shell, _connection.getHostName(), _connection.getPort(), exc); + } + catch (Exception e) + { + SystemBasePlugin.logError("ConnectionStatusListener: Error disconnecting", e); + } + } + + public void run(IProgressMonitor monitor) + throws InvocationTargetException, InterruptedException + { + String message = null; + message = SubSystemConfiguration.getDisconnectingMessage( + _connection.getHostName(), _connection.getPort()); + monitor.beginTask(message, IProgressMonitor.UNKNOWN); + try { + _connection.disconnect(monitor); + } catch (Exception exc) { + if (exc instanceof java.lang.reflect.InvocationTargetException) + throw (java.lang.reflect.InvocationTargetException) exc; + if (exc instanceof java.lang.InterruptedException) + throw (java.lang.InterruptedException) exc; + throw new java.lang.reflect.InvocationTargetException(exc); + } finally { + monitor.done(); + } + } + + public Shell getShell() { + Shell activeShell = SystemBasePlugin.getActiveWorkbenchShell(); + if (activeShell != null) { + return activeShell; + } + + IWorkbenchWindow window = null; + try { + window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + } catch (Exception e) { + return null; + } + if (window == null) { + IWorkbenchWindow[] windows = PlatformUI.getWorkbench() + .getWorkbenchWindows(); + if (windows != null && windows.length > 0) { + return windows[0].getShell(); + } + } else { + return window.getShell(); + } + + return null; + } + + /** + * Get the progress monitor dialog for this operation. We try to use one + * for all phases of a single operation, such as connecting and + * resolving. + */ + protected IRunnableContext getRunnableContext(Shell rshell) { + Shell shell = getShell(); + // for other cases, use statusbar + IWorkbenchWindow win = SystemBasePlugin.getActiveWorkbenchWindow(); + if (win != null) { + Shell winShell = RSEUIPlugin.getDefault().getWorkbench() + .getActiveWorkbenchWindow().getShell(); + if (winShell != null && !winShell.isDisposed() + && winShell.isVisible()) { + SystemBasePlugin + .logInfo("Using active workbench window as runnable context"); + shell = winShell; + return win; + } else { + win = null; + } + } + if (shell == null || shell.isDisposed() || !shell.isVisible()) { + SystemBasePlugin + .logInfo("Using progress monitor dialog with given shell as parent"); + shell = rshell; + } + IRunnableContext dlg = new ProgressMonitorDialog(rshell); + return dlg; + } + + /** + * Show an error message when the disconnection fails. Shows a common + * message by default. Overridable. + */ + protected void showDisconnectErrorMessage(Shell shell, String hostName, int port, Exception exc) + { + //SystemMessage.displayMessage(SystemMessage.MSGTYPE_ERROR,shell,RSEUIPlugin.getResourceBundle(), + // ISystemMessages.MSG_DISCONNECT_FAILED, + // hostName, exc.getMessage()); + //RSEUIPlugin.logError("Disconnect failed",exc); // temporary + SystemMessageDialog msgDlg = new SystemMessageDialog(shell, + RSEUIPlugin.getPluginMessage(ISystemMessages.MSG_DISCONNECT_FAILED).makeSubstitution(hostName,exc)); + msgDlg.setException(exc); + msgDlg.open(); + } + + /** + * Show an error message when the user cancels the disconnection. + * Shows a common message by default. + * Overridable. + */ + protected void showDisconnectCancelledMessage(Shell shell, String hostName, int port) + { + //SystemMessage.displayMessage(SystemMessage.MSGTYPE_ERROR, shell, RSEUIPlugin.getResourceBundle(), + // ISystemMessages.MSG_DISCONNECT_CANCELLED, hostName); + SystemMessageDialog msgDlg = new SystemMessageDialog(shell, + RSEUIPlugin.getPluginMessage(ISystemMessages.MSG_DISCONNECT_CANCELLED).makeSubstitution(hostName)); + msgDlg.open(); + } + }; + + /** + * Notification from sub-services that our session was lost. + * Notify all subsystems properly. + * TODO allow user to try and reconnect? + */ + public void handleSessionLost() { + Activator.trace("SshConnectorService: handleSessionLost"); //$NON-NLS-1$ + if (fSessionLostHandler!=null) { + fSessionLostHandler.sessionLost(); + } + } + + private static Display getStandardDisplay() { Display display = Display.getCurrent(); if( display==null ) { display = Display.getDefault(); @@ -404,7 +647,14 @@ public class SshConnectorService extends AbstractConnectorService implements ISs } public boolean isConnected() { - return (session!=null && session.isConnected()); + if (session!=null) { + if (session.isConnected()) { + return true; + } else if (fSessionLostHandler!=null) { + fSessionLostHandler.sessionLost(); + } + } + return false; } public boolean hasRemoteServerLauncherProperties() { diff --git a/rse/plugins/org.eclipse.rse.services.ssh/readme.txt b/rse/plugins/org.eclipse.rse.services.ssh/readme.txt index 4335f17a100..cfee9682956 100644 --- a/rse/plugins/org.eclipse.rse.services.ssh/readme.txt +++ b/rse/plugins/org.eclipse.rse.services.ssh/readme.txt @@ -25,23 +25,26 @@ __Usage:__ Window > Preferences > Team > CVS > SSh2 Connection Method > General to set the ssh home directory, and private key types to be used. * Select the "Shells" node and choose Contextmenu > Launch Shell. -* Enter your username on the remote system. For the password, just - enter anything (this is not checked, since ssh has its own method - of acquiring password information). +* Enter your username and password on the remote system. If you want + to use private-key authentication, just enter any dummy password - + you will be asked for the passphrase of your keyring later, or + the connection will succeed without further prompting if your key + ring has an empty passphrase. * When asked to accept remote host authenticity, press OK. * Enter the correct password for ssh on the remote system (this is only necessary if you are not using a private key). __Known Limitations:__ * Symbolic Links are not resolved (readlink not supported by jsch-0.1.28) -* Ssh passwords can not be stored (no key ring; use private keys instead) * Ssh timeouts are not observed (no automatic reconnect) - - after auto-logout, shell just doesnt react to input any more + - after auto-logout, further actions will show a "connect cancel" + message and connection will go down * Password and passphrase internal handling has not been checked for security against malicious reading from other Eclipse plugins. __Known Issues:__ -* A dummy password must be entered on initial connect (can be saved) +* A dummy password must be entered on initial connect, empty + password should be allowed if private key authentication is used * After some time, the connection may freeze and need to be disconnected. * Command service should be provided in addition to the remote shell service. @@ -49,14 +52,15 @@ __Known Issues:__ due to memory exhaustion (ArrayIndexOutOfBoundsException) * "Break" can not be sent to the remote system in order to cancel long-running jobs on the remote side. -* Moving files with a space in the name doesn't currently work. - Renaming them works though. -* Copy&paste, or drag&drop of remote files remotely does'nt currently work. * The plugin currently uses some "internal" classes from the org.eclipse.team.cvs.ui plugin. This needs to be cleaned up. * For other internal coding issues, see TODO items in the code. __Changelog:__ +v0.3: +* support Keyboard Interactive Authentication. +* Fix interaction with RSE passwords. +* Fix connection lost notifications v0.2: * Re-use Team/CVS/ssh2 preferences for ssh2 home and private keys specification Allows to do the ssh login without password if private/public key are set up. diff --git a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/ISshSessionProvider.java b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/ISshSessionProvider.java index 9cfd1146f51..26af1625ac1 100644 --- a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/ISshSessionProvider.java +++ b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/ISshSessionProvider.java @@ -4,5 +4,10 @@ import com.jcraft.jsch.Session; public interface ISshSessionProvider { + /* Return an active SSH session from a ConnectorService. */ public Session getSession(); + + /* Inform the connectorService that a session has been lost. */ + public void handleSessionLost(); + } diff --git a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/files/SftpFileService.java b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/files/SftpFileService.java index 4ca55117096..797cbb991d9 100644 --- a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/files/SftpFileService.java +++ b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/files/SftpFileService.java @@ -62,6 +62,7 @@ public class SftpFileService extends AbstractFileService implements IFileService return "Access a remote file system via Ssh / Sftp protocol"; } + //TODO specify Exception more clearly public void connect() throws Exception { Activator.trace("SftpFileService.connecting..."); //$NON-NLS-1$ try { @@ -77,16 +78,35 @@ public class SftpFileService extends AbstractFileService implements IFileService } } - protected ChannelSftp getChannel(String task) { + //TODO specify Exception more clearly + protected ChannelSftp getChannel(String task) throws Exception + { Activator.trace(task); if (fChannelSftp==null || !fChannelSftp.isConnected()) { Activator.trace(task + ": channel not connected: "+fChannelSftp); //$NON-NLS-1$ + Session session = fSessionProvider.getSession(); + if (session!=null) { + if (!session.isConnected()) { + //notify of lost session. May reconnect asynchronously later. + fSessionProvider.handleSessionLost(); + //dont throw an exception here, expect jsch to throw something useful + } else { + //session connected but channel not: try to reconnect + //(may throw Exception) + connect(); + } + } + //TODO might throw NPE if session has been disconnected } return fChannelSftp; } public void disconnect() { - getChannel("SftpFileService.disconnect").disconnect(); //$NON-NLS-1$ + try { + getChannel("SftpFileService.disconnect").disconnect(); //$NON-NLS-1$ + } catch(Exception e) { + /*nothing to do*/ + } fChannelSftp = null; } @@ -113,7 +133,12 @@ public class SftpFileService extends AbstractFileService implements IFileService } public boolean isConnected() { - return getChannel("SftpFileService.isConnected()").isConnected(); //$NON-NLS-1$ + try { + return getChannel("SftpFileService.isConnected()").isConnected(); //$NON-NLS-1$ + } catch(Exception e) { + /*cannot be connected when we cannot get a channel*/ + } + return false; } protected IHostFile[] internalFetch(IProgressMonitor monitor, String parentPath, String fileFilter, int fileType) diff --git a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshHostShell.java b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshHostShell.java index 02121b9817b..fa4f9d048d3 100644 --- a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshHostShell.java +++ b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshHostShell.java @@ -25,6 +25,7 @@ import org.eclipse.rse.services.shells.IHostShellOutputReader; import org.eclipse.rse.services.ssh.ISshSessionProvider; import com.jcraft.jsch.Channel; +import com.jcraft.jsch.Session; /** * A Shell subsystem for SSH. @@ -62,12 +63,22 @@ public class SshHostShell extends AbstractHostShell { } public boolean isActive() { - return !fChannel.isEOF(); + if (fChannel!=null && !fChannel.isEOF()) { + return true; + } + // shell is not active: check for session lost + Session session = fSessionProvider.getSession(); + if (session!=null && !session.isConnected()) { + fSessionProvider.handleSessionLost(); + } + return false; } public void writeToShell(String command) { - fStdinHandler.println(command); - fStdinHandler.flush(); + if (isActive()) { + fStdinHandler.println(command); + fStdinHandler.flush(); + } } public IHostShellOutputReader getStandardOutputReader() { diff --git a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellOutputReader.java b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellOutputReader.java index 3c42c9eccfd..b882f7ce293 100644 --- a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellOutputReader.java +++ b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellOutputReader.java @@ -40,6 +40,12 @@ public class SshShellOutputReader extends AbstractHostShellOutputReader fReader = reader; } + public void dispose() { + super.dispose(); + //check for active session and notify lost session if necessary + getHostShell().isActive(); + } + protected Object internalReadLine() { if (fReader == null) { //Our workaround sets the stderr reader to null, so we never give any stderr output.