blob: bd51c8b5a7a819e76f62414ebd32484375d3caf5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2009 Wind River Systems, Inc. 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:
* Martin Oberhuber (Wind River) - initial API and implementation
* David Dykstal (IBM) - [168977] refactoring IConnectorService and ServerLauncher hierarchies
* Sheldon D'souza (Celunite) - adapted from SshConnectorService
* Martin Oberhuber (Wind River) - apply refactorings for StandardConnectorService
* Martin Oberhuber (Wind River) - [178606] fix endless loop in readUntil()
* Sheldon D'souza (Celunite) - [186536] login and password should be configurable
* Sheldon D'souza (Celunite) - [186570] handle invalid user id and password more gracefully
* Martin Oberhuber (Wind River) - [187218] Fix error reporting for connect()
* Sheldon D'souza (Celunite) - [187301] support multiple telnet shells
* Sheldon D'souza (Celunite) - [194464] fix create multiple telnet shells quickly
* Martin Oberhuber (Wind River) - [186761] make the port setting configurable
* David McKnight (IBM) - [216252] [api][nls] Resource Strings specific to subsystems should be moved from rse.ui into files.ui / shells.ui / processes.ui where possible
* David McKnight (IBM) - [220547] [api][breaking] SimpleSystemMessage needs to specify a message id and some messages should be shared
* Anna Dushistova (MontaVista) - [240523] [rseterminals] Provide a generic adapter factory that adapts any ITerminalService to an IShellService
* Anna Dushistova (MontaVista) - [198819] [telnet] TelnetConnectorService does not send CommunicationsEvent.BEFORE_CONNECT
*******************************************************************************/
package org.eclipse.rse.internal.connectorservice.telnet;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.net.telnet.TelnetClient;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.osgi.util.NLS;
import org.eclipse.rse.core.RSECorePlugin;
import org.eclipse.rse.core.model.IHost;
import org.eclipse.rse.core.model.IPropertySet;
import org.eclipse.rse.core.model.ISystemRegistry;
import org.eclipse.rse.core.model.PropertyType;
import org.eclipse.rse.core.model.SystemSignonInformation;
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.internal.services.telnet.ITelnetSessionProvider;
import org.eclipse.rse.services.clientserver.messages.CommonMessages;
import org.eclipse.rse.services.clientserver.messages.ICommonMessageIds;
import org.eclipse.rse.services.clientserver.messages.SimpleSystemMessage;
import org.eclipse.rse.services.clientserver.messages.SystemMessage;
import org.eclipse.rse.services.clientserver.messages.SystemMessageException;
import org.eclipse.rse.ui.RSEUIPlugin;
import org.eclipse.rse.ui.SystemBasePlugin;
import org.eclipse.rse.ui.messages.SystemMessageDialog;
import org.eclipse.rse.ui.subsystems.StandardConnectorService;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
public class TelnetConnectorService extends StandardConnectorService implements
ITelnetSessionProvider {
public static final String PROPERTY_SET_NAME = "Telnet Settings"; //$NON-NLS-1$
public static final String PROPERTY_LOGIN_REQUIRED = "Login.Required"; //$NON-NLS-1$
public static final String PROPERTY_LOGIN_PROMPT = "Login.Prompt"; //$NON-NLS-1$
public static final String PROPERTY_PASSWORD_PROMPT = "Password.Prompt"; //$NON-NLS-1$
public static final String PROPERTY_COMMAND_PROMPT = "Command.Prompt"; //$NON-NLS-1$
private static final int TELNET_DEFAULT_PORT = 23; // TODO Make configurable
private static final int TELNET_CONNECT_TIMEOUT = 60; //seconds - TODO: Make configurable
private List fTelnetClients = new ArrayList();
private SessionLostHandler fSessionLostHandler;
private IPropertySet telnetPropertySet = null;
private static final int ERROR_CODE = 100; // filed error code
private static final int SUCCESS_CODE = 150; // login pass code
private static final int CONNECT_CLOSED = 200; // code for end of login attempts
private static final int CONNECT_CANCELLED = 250; // code for cancel progress
public TelnetConnectorService(IHost host) {
super(TelnetConnectorResources.TelnetConnectorService_Name,
TelnetConnectorResources.TelnetConnectorService_Description,
host, TELNET_DEFAULT_PORT);
fSessionLostHandler = null;
telnetPropertySet = getTelnetPropertySet();
}
/**
* Return the telnet property set, and fill it with default values if it has
* not been created yet. Extenders may override in order to set different
* default values.
*
* @return a property set holding properties understood by the telnet
* connector service.
*/
protected IPropertySet getTelnetPropertySet() {
IPropertySet telnetSet = getPropertySet(PROPERTY_SET_NAME);
if (telnetSet == null) {
telnetSet = createPropertySet(PROPERTY_SET_NAME,
TelnetConnectorResources.PropertySet_Description);
telnetSet.addProperty(PROPERTY_LOGIN_REQUIRED,
"true", PropertyType.getEnumPropertyType(new String[] { "true", "false" })); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
telnetSet.addProperty(PROPERTY_LOGIN_PROMPT,
"ogin: ", PropertyType.getStringPropertyType()); //$NON-NLS-1$
telnetSet.addProperty(PROPERTY_PASSWORD_PROMPT,
"assword: ", PropertyType.getStringPropertyType()); //$NON-NLS-1$
telnetSet.addProperty(PROPERTY_COMMAND_PROMPT,
"$", PropertyType.getStringPropertyType()); //$NON-NLS-1$
}
return telnetSet;
}
public static void checkCancelled(IProgressMonitor monitor) {
if (monitor.isCanceled())
throw new OperationCanceledException();
}
protected void internalConnect(IProgressMonitor monitor) throws Exception {
try {
// Fire comm event to signal state about to change
fireCommunicationsEvent(CommunicationsEvent.BEFORE_CONNECT);
TelnetClient client = makeNewTelnetClient(monitor);
if( client != null ) {
synchronized(this) {
fTelnetClients.add(client);
if (fSessionLostHandler==null) {
fSessionLostHandler = new SessionLostHandler(this);
}
}
notifyConnection();
}
}catch( Exception e) {
if( e instanceof SystemMessageException ) {
internalDisconnect( null );
throw e;
}
}
}
protected int getTelnetPort() {
int port = getPort();
if (port<=0) {
//Legacy "default port" setting
port = TELNET_DEFAULT_PORT;
}
return port;
}
public TelnetClient makeNewTelnetClient(IProgressMonitor monitor ) throws Exception {
TelnetClient client = new TelnetClient();
return loginTelnetClient(client, monitor);
}
public TelnetClient loginTelnetClient(TelnetClient client, IProgressMonitor monitor) throws SystemMessageException {
String host = getHostName();
String user = getUserId();
String password = ""; //$NON-NLS-1$
int status = ERROR_CODE;
Exception nestedException = null;
try {
Activator.trace("Telnet Service: Connecting....."); //$NON-NLS-1$
client.connect(host, getTelnetPort());
SystemSignonInformation ssi = getSignonInformation();
if (ssi != null) {
password = ssi.getPassword();
}
long millisToEnd = System.currentTimeMillis() + TELNET_CONNECT_TIMEOUT*1000;
LoginThread checkLogin = new LoginThread(user, password,client.getInputStream(),new PrintStream( client.getOutputStream() ));
checkLogin.start();
while (checkLogin.isAlive() && System.currentTimeMillis()<millisToEnd) {
if (monitor!=null) {
monitor.worked(1);
if (monitor.isCanceled()) {
status = CONNECT_CANCELLED;
//Thread will be interrupted by sessionDisconnect()
//checkLogin.interrupt();
break;
}
}
Display d = Display.getCurrent();
if (d!=null) {
while(d.readAndDispatch()) {
//get next event if on dispatch thread
}
}
checkLogin.join(500);
}
if (status != CONNECT_CANCELLED) {
status = checkLogin.getLoginStatus();
checkLogin.join();
}
} catch (Exception e) {
Activator.trace("Telnet Service failed: " + e.toString()); //$NON-NLS-1$
nestedException = e;
} finally {
if (status == CONNECT_CANCELLED) {
Activator.trace("Telnet Service: Cancelled"); //$NON-NLS-1$
try {
client.disconnect(); //will eventually destroy the LoginThread
} catch(Exception e) {
/*ignore on forced disconnect*/
}
client = null;
} else if (status == SUCCESS_CODE) {
Activator.trace("Telnet Service: Connected"); //$NON-NLS-1$
} else {
Activator.trace("Telnet Service: Connect failed"); //$NON-NLS-1$
//TODO pass the nested exception as well as original prompts
//from the remote side with the SystemMessageException for user diagnostics
SystemMessage msg;
if (nestedException!=null) {
msg = new SimpleSystemMessage(Activator.PLUGIN_ID,
ICommonMessageIds.MSG_EXCEPTION_OCCURRED,
IStatus.ERROR,
CommonMessages.MSG_EXCEPTION_OCCURRED, nestedException);
} else {
msg = new SimpleSystemMessage(Activator.PLUGIN_ID,
ICommonMessageIds.MSG_COMM_AUTH_FAILED,
IStatus.ERROR,
CommonMessages.MSG_COMM_AUTH_FAILED,
NLS.bind(CommonMessages.MSG_COMM_AUTH_FAILED_DETAILS, getHost().getAliasName()));
msg.makeSubstitution(getHost().getAliasName());
}
throw new SystemMessageException(msg);
}
}
return client;
}
/**
* Disconnect the telnet session. Synchronized in order to avoid NPE's from
* commons.net when called quickly in succession.
*/
private synchronized void sessionDisconnect() {
Activator.trace("TelnetConnectorService.sessionDisconnect"); //$NON-NLS-1$
Iterator it = fTelnetClients.iterator();
while (it.hasNext()) {
TelnetClient client = (TelnetClient)it.next();
if (client.isConnected()) {
try {
client.disconnect();
} catch(IOException e) {
// Avoid NPE on disconnect shown in UI
// This is a non-critical exception so print only in debug mode
if (Activator.isTracingOn())
e.printStackTrace();
}
}
it.remove();
}
}
public int readUntil(String pattern,InputStream in) {
try {
char lastChar = pattern.charAt(pattern.length() - 1);
StringBuffer sb = new StringBuffer();
int ch = in.read();
while (ch >= 0) {
char tch = (char) ch;
if (Activator.isTracingOn())
System.out.print(tch);
sb.append(tch);
if (tch=='t' && sb.indexOf("incorrect") >= 0) { //$NON-NLS-1$
return ERROR_CODE;
}
if (tch=='d' && sb.indexOf("closed") >= 0) { //$NON-NLS-1$
return CONNECT_CLOSED;
}
if (tch == lastChar) {
if (sb.toString().endsWith(pattern)) {
return SUCCESS_CODE;
}
}
ch = in.read();
}
} catch (Exception e) {
SystemBasePlugin.logError(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
}
return CONNECT_CLOSED;
}
public void write(String value,PrintStream out) {
try {
out.println(value);
out.flush();
if (Activator.isTracingOn()) {
// Avoid printing password to stdout
// Activator.trace("write: "+value ); //$NON-NLS-1$
int len = value.length() + 6;
Activator.trace("write: ******************".substring(0, len <= 24 ? len : 24)); //$NON-NLS-1$
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected void internalDisconnect(IProgressMonitor monitor)
throws Exception {
Activator.trace("Telnet Service: Disconnecting ....."); //$NON-NLS-1$
boolean sessionLost = (fSessionLostHandler != null && fSessionLostHandler.isSessionLost());
// no more interested in handling session-lost, since we are
// disconnecting anyway
fSessionLostHandler = null;
// handle events
if (sessionLost) {
notifyError();
} else {
// Fire comm event to signal state about to change
fireCommunicationsEvent(CommunicationsEvent.BEFORE_DISCONNECT);
}
sessionDisconnect();
// Fire comm event to signal state changed
notifyDisconnection();
}
/**
* 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) {
// invokes this.run() on dispatch thread
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 = new SimpleSystemMessage(Activator.PLUGIN_ID,
ICommonMessageIds.MSG_CONNECT_CANCELLED,
IStatus.CANCEL,
NLS.bind(CommonMessages.MSG_CONNECT_CANCELLED, _connection.getHost().getAliasName()));
SystemMessageDialog dialog = new SystemMessageDialog(getShell(), msg);
dialog.open();
try {
// TODO I think we should better use a Job for disconnecting?
// But what about error messages?
IRunnableContext runnableContext = getRunnableContext(getShell());
// will do this.run(IProgressMonitor mon)
// runnableContext.run(false,true,this); // inthread,
// cancellable, IRunnableWithProgress
runnableContext.run(true, true, this); // fork, cancellable,
// IRunnableWithProgress
_connection.reset();
ISystemRegistry sr = RSECorePlugin.getTheSystemRegistry();
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(TelnetConnectorResources.TelnetConnectorService_ErrorDisconnecting, 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"); //$NON-NLS-1$
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"); //$NON-NLS-1$
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 msg = new SimpleSystemMessage(Activator.PLUGIN_ID,
ICommonMessageIds.MSG_DISCONNECT_FAILED,
IStatus.ERROR,
NLS.bind(CommonMessages.MSG_DISCONNECT_FAILED, hostName), exc);
SystemMessageDialog msgDlg = new SystemMessageDialog(shell,msg);
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 msg = new SimpleSystemMessage(Activator.PLUGIN_ID,
ICommonMessageIds.MSG_DISCONNECT_CANCELLED,
IStatus.CANCEL,
NLS.bind(CommonMessages.MSG_DISCONNECT_CANCELLED, hostName));
SystemMessageDialog msgDlg = new SystemMessageDialog(shell,msg);
msgDlg.open();
}
}
/*
* A Login Thread to catch errors during login into telnet session
*/
private class LoginThread extends Thread {
private String username;
private String password;
private int status = SUCCESS_CODE;
private InputStream in;
private PrintStream out;
public LoginThread(String username, String password,InputStream in,PrintStream out) {
this.username = username;
this.password = password;
this.in = in;
this.out = out;
}
public void run() {
telnetPropertySet = getTelnetPropertySet();
String login_required = telnetPropertySet
.getPropertyValue(PROPERTY_LOGIN_REQUIRED);
String login_prompt = telnetPropertySet
.getPropertyValue(PROPERTY_LOGIN_PROMPT);
String password_prompt = telnetPropertySet
.getPropertyValue(PROPERTY_PASSWORD_PROMPT);
String command_prompt = telnetPropertySet
.getPropertyValue(PROPERTY_COMMAND_PROMPT);
if (Boolean.valueOf(login_required).booleanValue()) {
status = SUCCESS_CODE;
if (login_prompt != null && login_prompt.length() > 0) {
status = readUntil(login_prompt,this.in);
write(username,this.out);
}
if (status == SUCCESS_CODE && password_prompt != null && password_prompt.length() > 0) {
status = readUntil(password_prompt,this.in);
write(password,this.out);
}
if (status == SUCCESS_CODE && command_prompt != null && command_prompt.length() > 0) {
status = readUntil(command_prompt,this.in);
}
} else {
if (command_prompt != null && command_prompt.length() > 0) {
status = readUntil(command_prompt,this.in);
}
}
}
public int getLoginStatus() {
return this.status;
}
}
protected static Display getStandardDisplay() {
Display display = Display.getCurrent();
if (display == null) {
display = Display.getDefault();
}
return display;
}
public boolean isConnected() {
boolean anyConnected = false;
synchronized(this) {
Iterator it = fTelnetClients.iterator();
while (it.hasNext()) {
TelnetClient client = (TelnetClient)it.next();
if (client.isConnected()) {
anyConnected = true;
} else {
it.remove();
}
}
}
if (!anyConnected && fSessionLostHandler != null) {
Activator.trace("TelnetConnectorService.isConnected: false -> sessionLost"); //$NON-NLS-1$
fSessionLostHandler.sessionLost();
}
return anyConnected;
}
/**
* Test if this connector service requires a password. Telnet connector
* service returns false since a password is not necessarily required, i.e.
* the corresponding password field may be empty.
*
* @see org.eclipse.rse.core.subsystems.AbstractConnectorService#requiresPassword()
* @return false
*/
public boolean requiresPassword() {
return false;
}
/**
* Test if this connector service requires a user id. Telnet connector
* service returns false since a user id is not necessarily required, i.e.
* the corresponding user id field may be empty.
*
* @see org.eclipse.rse.core.subsystems.AbstractConnectorService#requiresPassword()
* @return false
*/
public boolean requiresUserId() {
return false;
}
/**
* Test if this connector service requires logging in.
*
* @return false if the Property {@link #PROPERTY_LOGIN_REQUIRED} is set and
* false. Returns true otherwise.
*/
protected boolean supportsLogin() {
boolean result = true;
if (telnetPropertySet != null) {
String login_required = telnetPropertySet.getPropertyValue(PROPERTY_LOGIN_REQUIRED);
if (login_required != null && login_required.equalsIgnoreCase("false")) { //$NON-NLS-1$
result = false;
}
}
return result;
}
/*
* (non-Javadoc)
* @see org.eclipse.rse.ui.subsystems.StandardConnectorService#supportsPassword()
*/
public boolean supportsPassword() {
return supportsLogin();
}
/*
* (non-Javadoc)
* @see org.eclipse.rse.ui.subsystems.StandardConnectorService#supportsUserId()
*/
public boolean supportsUserId() {
return supportsLogin();
}
}