From a219176dd192f9335d76da512ad6f7ff45c8c373 Mon Sep 17 00:00:00 2001 From: Martin Oberhuber < martin.oberhuber@windriver.com> Date: Tue, 8 Aug 2006 14:14:31 +0000 Subject: [PATCH] Fix ssh shell session timeouts by using a separate Thread. Improve ssh terminal handling (backspace) --- .../rse/services/ssh/shell/SshHostShell.java | 44 +++++-- .../ssh/shell/SshShellOutputReader.java | 72 +++++++----- .../ssh/shell/SshShellWriterThread.java | 109 ++++++++++++++++++ 3 files changed, 190 insertions(+), 35 deletions(-) create mode 100644 rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellWriterThread.java 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 5478f6361f2..8c6c9f14768 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 @@ -18,25 +18,29 @@ package org.eclipse.rse.services.ssh.shell; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; +import org.eclipse.core.runtime.IProgressMonitor; + import com.jcraft.jsch.Channel; import com.jcraft.jsch.Session; import org.eclipse.rse.services.shells.AbstractHostShell; +import org.eclipse.rse.services.shells.IHostShell; import org.eclipse.rse.services.shells.IHostShellOutputReader; import org.eclipse.rse.services.ssh.ISshSessionProvider; /** * A Shell subsystem for SSH. */ -public class SshHostShell extends AbstractHostShell { +public class SshHostShell extends AbstractHostShell implements IHostShell { private ISshSessionProvider fSessionProvider; private Channel fChannel; private SshShellOutputReader fStdoutHandler; private SshShellOutputReader fStderrHandler; - private PrintWriter fStdinHandler; + private SshShellWriterThread fShellWriter; public SshHostShell(ISshSessionProvider sessionProvider, String initialWorkingDirectory, String commandToRun, String encoding, String[] environment) { try { @@ -53,21 +57,43 @@ public class SshHostShell extends AbstractHostShell { fStdoutHandler = new SshShellOutputReader(this, new BufferedReader(new InputStreamReader(fChannel.getInputStream())), false); fStderrHandler = new SshShellOutputReader(this, null,true); - fStdinHandler = new PrintWriter(fChannel.getOutputStream()); - + OutputStream outputStream = fChannel.getOutputStream(); + //TODO check if encoding or command to execute needs to be considered + //If a command is given, it might be possible to do without a Thread + //Charset cs = Charset.forName(encoding); + //PrintWriter outputWriter = new PrintWriter( + // new BufferedWriter(new OutputStreamWriter(outputStream,cs))); + PrintWriter outputWriter = new PrintWriter(outputStream); + fShellWriter = new SshShellWriterThread(outputWriter); fChannel.connect(); - writeToShell("cd "+initialWorkingDirectory); //$NON-NLS-1$ + if (initialWorkingDirectory!=null && initialWorkingDirectory.length()>0 && !initialWorkingDirectory.equals(".")) { //$NON-NLS-1$ + writeToShell("cd "+initialWorkingDirectory); //$NON-NLS-1$ + } } catch(Exception e) { //TODO Forward exception to RSE properly e.printStackTrace(); + if (fShellWriter!=null) { + fShellWriter.stopThread(); + fShellWriter = null; + } } } + /** + * Connect to remote system and launch Threads for the + * shell as needed. + * @param monitor + */ + protected void start(IProgressMonitor monitor) + { + } + public boolean isActive() { if (fChannel!=null && !fChannel.isEOF()) { return true; } // shell is not active: check for session lost + exit(); Session session = fSessionProvider.getSession(); if (session!=null && !session.isConnected()) { fSessionProvider.handleSessionLost(); @@ -77,8 +103,11 @@ public class SshHostShell extends AbstractHostShell { public void writeToShell(String command) { if (isActive()) { - fStdinHandler.println(command); - fStdinHandler.flush(); + if (!fShellWriter.sendCommand(command)) { + //exception occurred: terminate writer thread, cancel connection + exit(); + isActive(); + } } } @@ -91,6 +120,7 @@ public class SshHostShell extends AbstractHostShell { } public void exit() { + fShellWriter.stopThread(); fChannel.disconnect(); } 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 70ad3fd5770..0632a25cbbf 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 @@ -37,11 +37,12 @@ public class SshShellOutputReader extends AbstractHostShellOutputReader implements IHostShellOutputReader { protected BufferedReader fReader; + private String fPromptChars = ">$%#]"; //Characters we accept as the end of a prompt //$NON-NLS-1$; public SshShellOutputReader(IHostShell hostShell, BufferedReader reader, boolean isErrorReader) { super(hostShell, isErrorReader); - setName("SshShellOutputReader-"+getName()); //$NON-NLS-1$ + setName("Ssh ShellOutputReader"+getName()); //$NON-NLS-1$ fReader = reader; } @@ -58,73 +59,83 @@ public class SshShellOutputReader extends AbstractHostShellOutputReader return null; } StringBuffer theLine = new StringBuffer(); + StringBuffer theDebugLine = null; + theDebugLine = new StringBuffer(); int ch; - int lastch = 0; boolean done = false; while (!done && !isFinished()) { try { ch = fReader.read(); switch (ch) { case -1: + case 65535: if (theLine.length() == 0) // End of Reader return null; done = true; break; - case 65535: - if (theLine.length() == 0) // Check why I keep getting this!!! - return null; - done = true; - break; + case '\b': //backspace + if(theDebugLine!=null) theDebugLine.append((char)ch); + int len = theLine.length()-1; + if (len>=0) theLine.deleteCharAt(len); + case 13: + if(theDebugLine!=null) theDebugLine.append((char)ch); + break; // Carriage Return: dont append to the buffer case 10: + if(theDebugLine!=null) theDebugLine.append((char)ch); done = true; // Newline break; case 9: - //TODO Count characters and insert as many as needed to do a real tab - theLine.append(" "); // Tab //$NON-NLS-1$ + //Tab: we count tabs at column 8 + if(theDebugLine!=null) theDebugLine.append((char)ch); + int tabIndex = theLine.length() % 8; + while (tabIndex < 8) { + theLine.append(' '); + tabIndex++; + } break; - case 13: - break; // Carriage Return default: char tch = (char) ch; + if(theDebugLine!=null) theDebugLine.append(tch); if (!Character.isISOControl(tch)) { theLine.append(tch); // Any other character } else if (ch == 27) { // Escape: ignore next char too - int nch = (char)fReader.read(); + int nch = fReader.read(); + if (theDebugLine!=null) theDebugLine.append((char)nch); if (nch == 91) { //vt100 escape sequence: read until end-of-command (skip digits and semicolon) //e.g. \x1b;13;m --> ignore the entire command, including the trailing m do { nch = fReader.read(); + if (theDebugLine!=null) theDebugLine.append((char)nch); } while (Character.isDigit((char)nch) || nch == ';'); } } } - boolean ready = fReader.ready(); - //TODO Get rid of this to support UNIX and Mac? -- It appears that - //due to vt100 emulation we get CRLF even for UNIX connections. - if (ch == 10 && lastch == 13) { - return theLine.toString(); - } - lastch = ch; - // Check to see if the BufferedReader is still ready which means // there are more characters // in the Buffer...If not, then we assume it is waiting for // input. - if (!ready) { - // wait to make sure + if (!done && !fReader.ready()) { + // wait to make sure -- max. 500 msec to wait for new chars + // if we are not at a CRLF seems to be appropriate for the + // Pipes and Threads in ssh. + long waitIncrement = 500; + // Check if we think we are at a prompt + int len = theLine.length()-1; + while (len>0 && Character.isSpaceChar(theLine.charAt(len))) { + len--; + } + if (len>=0 && fPromptChars.indexOf(theLine.charAt(len))>=0) { + waitIncrement = 5; //wait only 5 msec if we think it's a prompt + } try { - Thread.sleep(_waitIncrement); + Thread.sleep(waitIncrement); } catch (InterruptedException e) { } if (!fReader.ready()) { - if (done) { - return theLine.toString().trim(); - } else { - done = true; - } + done = true; } } } catch (IOException e) { @@ -132,9 +143,14 @@ public class SshShellOutputReader extends AbstractHostShellOutputReader //our reader thread completely... the exception could just be //temporary, and we should keep running! Activator.getDefault().getLog().log(new Status(IStatus.WARNING, Activator.getDefault().getBundle().getSymbolicName(), 0, "IOException in SshShellOutputReader", e)); + //e.printStackTrace(); return null; } } + if (theDebugLine!=null) { + String debugLine = theDebugLine.toString(); + debugLine.compareTo(""); //$NON-NLS-1$ + } return theLine.toString(); } diff --git a/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellWriterThread.java b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellWriterThread.java new file mode 100644 index 00000000000..71d814cbb45 --- /dev/null +++ b/rse/plugins/org.eclipse.rse.services.ssh/src/org/eclipse/rse/services/ssh/shell/SshShellWriterThread.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2006 Wind River Systems, Inc. + * 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 + *******************************************************************************/ + +package org.eclipse.rse.services.ssh.shell; + +import java.io.PrintWriter; + +/** + * The SshShellWriterThread is a Thread used to print commands into + * a running ssh shell channel. + * + * A separate Thread is needed because the PipedInputStream + * used by ssh requirs that the writing end of the Pipe be + * a Thread that remains alive during the entire lifetime + * of the shell. + */ +public class SshShellWriterThread extends Thread +{ + private PrintWriter fOutputWriter; + private String fNextCommand; + private boolean fIsCancelled; + + + /** + * constructor for ssh shell writer thread + * @param outputStream Stream to write to in separate Thread + */ + public SshShellWriterThread(PrintWriter outputWriter) + { + super(); + fOutputWriter = outputWriter; + setName("Ssh ShellWriter"+getName()); //$NON-NLS-1$ + start(); + } + + public synchronized boolean isDone() + { + return fIsCancelled; + } + + public synchronized void stopThread() + { + fIsCancelled = true; + notifyAll(); + } + + /** + * Write command to remote side. Wait until the + * thread takes the command (no queueing). + * @param command to send + * @return boolean true if command was sent ok + */ + public synchronized boolean sendCommand(String command) + { + try { + //In case multiple commands try to send: + //wait until it's our turn + while (!fIsCancelled && fNextCommand!=null) { + wait(); + } + if (!fIsCancelled) { + //Now it's our turn + fNextCommand = command; + notifyAll(); + //Wait until our command is processed + while (!fIsCancelled && fNextCommand!=null) { + wait(); + } + } + } catch(InterruptedException e) { + stopThread(); + } + return !fIsCancelled; + } + + public synchronized void run() + { + try { + while (!fIsCancelled) { + while (fNextCommand==null && !fIsCancelled) { + wait(); + } + if (!fIsCancelled) { + fOutputWriter.println(fNextCommand); + fNextCommand=null; + notifyAll(); + if (fOutputWriter.checkError()) { //flush AND get error + stopThread(); + } + } + } + } catch(InterruptedException e) { + /* no special handling -> close stream */ + } finally { + stopThread(); + fOutputWriter.close(); + fOutputWriter = null; + } + } + +} \ No newline at end of file