package com.trilead.ssh2.channel; import com.trilead.ssh2.log.Logger; import com.trilead.ssh2.packets.PacketSignal; import com.trilead.ssh2.packets.PacketWindowChange; import com.trilead.ssh2.packets.Packets; import com.trilead.ssh2.transport.TransportManager; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import static com.trilead.ssh2.util.IOUtils.closeQuietly; /** * Channel. * * @author Christian Plattner, plattner@trilead.com * @version $Id: Channel.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ */ public class Channel { /* * OK. Here is an important part of the JVM Specification: * (http://java.sun.com/docs/books/vmspec/2nd-edition/html/Threads.doc.html#22214) * * Any association between locks and variables is purely conventional. * Locking any lock conceptually flushes all variables from a thread's * working memory, and unlocking any lock forces the writing out to main * memory of all variables that the thread has assigned. That a lock may be * associated with a particular object or a class is purely a convention. * (...) * * If a thread uses a particular shared variable only after locking a * particular lock and before the corresponding unlocking of that same lock, * then the thread will read the shared value of that variable from main * memory after the lock operation, if necessary, and will copy back to main * memory the value most recently assigned to that variable before the * unlock operation. * * This, in conjunction with the mutual exclusion rules for locks, suffices * to guarantee that values are correctly transmitted from one thread to * another through shared variables. * * ====> Always keep that in mind when modifying the Channel/ChannelManger * code. * */ static final int STATE_OPENING = 1; static final int STATE_OPEN = 2; static final int STATE_CLOSED = 4; private static final int CHANNEL_BUFFER_SIZE = Integer.getInteger( Channel.class.getName()+".bufferSize", 1024*1024 + 16*1024).intValue(); /** * This channel's session size. */ // @GuarydedBy("this") int channelBufferSize = CHANNEL_BUFFER_SIZE; /* * To achieve correctness, the following rules have to be respected when * accessing this object: */ // These fields can always be read final ChannelManager cm; final ChannelOutputStream stdinStream; /** * One stream. * * Either {@link #stream} and {@link #buffer} is set, or the {@link #sink} is set, but those * are mutually exclusive. The former is used when we are buffering data and let the application * read it via {@link InputStream}, and the latter is used when we are passing through the data * to another {@link OutputStream}. * * The synchronization is done by {@link Channel} */ class Output { ChannelInputStream stream; FifoBuffer buffer = new FifoBuffer(Channel.this, 2048, channelBufferSize); OutputStream sink; public void write(byte[] buf, int start, int len) throws IOException { if (buffer!=null) { try { buffer.write(buf,start,len); } catch (InterruptedException e) { throw new InterruptedIOException(); } } else { sink.write(buf,start,len); freeupWindow(len, true); } } /** * How many bytes can be read from the buffer? */ public int readable() { if (buffer!=null) return buffer.readable(); else return 0; } /** * See {@link InputStream#available()} */ public int available() { if (buffer==null) throw new IllegalStateException("Output is being piped to "+sink); int sz = buffer.readable(); if (sz>0) return sz; return isEOF() ? -1 : 0; } /** * Read from the buffer. */ public int read(byte[] buf, int start, int len) throws InterruptedException { return buffer.read(buf,start,len); } /** * Called when there will be no more data arriving to this output any more. * Not that buffer might still have some more data that needs to be drained. */ public void eof() { if (buffer!=null) buffer.close(); else closeQuietly(sink); } /** * Instead of spooling data, let our I/O thread write to the given {@link OutputStream}. */ public void pipeTo(OutputStream os) throws IOException { sink = os; if (buffer.readable()!=0) { freeupWindow(buffer.writeTo(os)); } buffer = null; stream = null; } } final Output stdout = new Output(); final Output stderr = new Output(); // These two fields will only be written while the Channel is in state // STATE_OPENING. // The code makes sure that the two fields are written out when the state is // changing to STATE_OPEN. // Therefore, if you know that the Channel is in state STATE_OPEN, then you // can read these two fields without synchronizing on the Channel. However, make // sure that you get the latest values (e.g., flush caches by synchronizing on any // object). However, to be on the safe side, you can lock the channel. int localID = -1; int remoteID = -1; /* * Make sure that we never send a data/EOF/WindowChange msg after a CLOSE * msg. * * This is a little bit complicated, but we have to do it in that way, since * we cannot keep a lock on the Channel during the send operation (this * would block sometimes the receiver thread, and, in extreme cases, can * lead to a deadlock on both sides of the connection (senders are blocked * since the receive buffers on the other side are full, and receiver * threads wait for the senders to finish). It all depends on the * implementation on the other side. But we cannot make any assumptions, we * have to assume the worst case. Confused? Just believe me. */ /* * If you send a message on a channel, then you have to aquire the * "channelSendLock" and check the "closeMessageSent" flag (this variable * may only be accessed while holding the "channelSendLock" !!! * * BTW: NEVER EVER SEND MESSAGES FROM THE RECEIVE THREAD - see explanation * above. */ final Object channelSendLock = new Object(); boolean closeMessageSent = false; /* * Stop memory fragmentation by allocating this often used buffer. * May only be used while holding the channelSendLock */ final byte[] msgWindowAdjust = new byte[9]; // If you access (read or write) any of the following fields, then you have // to synchronize on the channel. int state = STATE_OPENING; boolean closeMessageRecv = false; /* This is a stupid implementation. At the moment we can only wait * for one pending request per channel. */ int successCounter = 0; int failedCounter = 0; int localWindow = 0; /* locally, we use a small window, < 2^31 */ long remoteWindow = 0; /* long for readable 2^32 - 1 window support */ int localMaxPacketSize = -1; int remoteMaxPacketSize = -1; private boolean eof = false; synchronized void eof() { stdout.eof(); stderr.eof(); eof = true; } boolean isEOF() { return eof; } Integer exit_status; String exit_signal; // we keep the x11 cookie so that this channel can be closed when this // specific x11 forwarding gets stopped String hexX11FakeCookie; // reasonClosed is special, since we sometimes need to access it // while holding the channelSendLock. // We protect it with a private short term lock. private final Object reasonClosedLock = new Object(); private Throwable reasonClosed = null; public Channel(ChannelManager cm) { this.cm = cm; this.localWindow = channelBufferSize; this.localMaxPacketSize = TransportManager.MAX_PACKET_SIZE - 1024; // leave enough slack this.stdinStream = new ChannelOutputStream(this); this.stdout.stream = new ChannelInputStream(this, false); this.stderr.stream = new ChannelInputStream(this, true); } /* Methods to allow access from classes outside of this package */ public synchronized void setWindowSize(int newSize) { if (newSize<=0) throw new IllegalArgumentException("Invalid value: "+newSize); this.channelBufferSize = newSize; // next time when the other side sends us something, we'll issue SSH_MSG_CHANNEL_WINDOW_ADJUST } public ChannelInputStream getStderrStream() { return stderr.stream; } public ChannelOutputStream getStdinStream() { return stdinStream; } public ChannelInputStream getStdoutStream() { return stdout.stream; } public synchronized void pipeStdoutStream(OutputStream os) throws IOException { stdout.pipeTo(os); } public synchronized void pipeStderrStream(OutputStream os) throws IOException { stderr.pipeTo(os); } public String getExitSignal() { synchronized (this) { return exit_signal; } } public Integer getExitStatus() { synchronized (this) { return exit_status; } } /** * @deprecated * Use {@link #getReasonClosedCause()} */ public String getReasonClosed() { synchronized (reasonClosedLock) { return reasonClosed!=null ? reasonClosed.getMessage() : null; } } public Throwable getReasonClosedCause() { synchronized (reasonClosedLock) { return reasonClosed; } } public void setReasonClosed(String reasonClosed) { setReasonClosed(new IOException(reasonClosed)); } public void setReasonClosed(Throwable reasonClosed) { synchronized (reasonClosedLock) { if (this.reasonClosed == null) this.reasonClosed = reasonClosed; } } /** * Update the flow control couner and if necessary, sends ACK to the other end to * let it send more data. */ void freeupWindow(int copylen) throws IOException { freeupWindow(copylen, false); } /** * Update the flow control couner and if necessary, sends ACK to the other end to * let it send more data. */ void freeupWindow(int copylen, boolean sendAsync) throws IOException { if (copylen <= 0) return; int increment = 0; int remoteID; int localID; synchronized (this) { if (localWindow <= ((channelBufferSize * 3) / 4)) { // have enough local window been consumed? if so, we'll send Ack // the window control is on the combined bytes of stdout & stderr int space = channelBufferSize - stdout.readable() - stderr.readable(); increment = space - localWindow; if (increment > 0) // increment<0 can't happen, but be defensive localWindow += increment; } remoteID = this.remoteID; /* read while holding the lock */ localID = this.localID; /* read while holding the lock */ } /* * If a consumer reads stdout and stdin in parallel, we may end up with * sending two msgWindowAdjust messages. Luckily, it * does not matter in which order they arrive at the server. */ if (increment > 0) { if (log.isEnabled()) log.log(80, "Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + localID + ", " + increment + ")"); synchronized (channelSendLock) { byte[] msg = msgWindowAdjust; msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST; msg[1] = (byte) (remoteID >> 24); msg[2] = (byte) (remoteID >> 16); msg[3] = (byte) (remoteID >> 8); msg[4] = (byte) (remoteID); msg[5] = (byte) (increment >> 24); msg[6] = (byte) (increment >> 16); msg[7] = (byte) (increment >> 8); msg[8] = (byte) (increment); if (closeMessageSent == false) { if (sendAsync) { cm.tm.sendAsynchronousMessage(msg); } else { cm.tm.sendMessage(msg); } } } } } public void requestWindowChange(int term_width_characters, int term_height_characters, int term_width_pixels, int term_height_pixels) throws IOException { PacketWindowChange pwc; synchronized (this) { if (state != Channel.STATE_OPEN) throw (IOException)new IOException("Cannot request window-change on this channel").initCause(getReasonClosedCause()); pwc = new PacketWindowChange(remoteID, term_width_characters, term_height_characters, term_width_pixels, term_height_pixels); } synchronized (channelSendLock) { if (closeMessageSent) throw (IOException)new IOException("Cannot request window-change on this channel").initCause(getReasonClosedCause()); cm.tm.sendMessage(pwc.getPayload()); } } public void signal(String name) throws IOException { PacketSignal p; synchronized (this) { if (state != Channel.STATE_OPEN) throw (IOException)new IOException("Cannot send signal on this channel").initCause(getReasonClosedCause()); p = new PacketSignal(remoteID, name); } synchronized (channelSendLock) { if (closeMessageSent) throw (IOException)new IOException("Cannot request window-change on this channel").initCause(getReasonClosedCause()); cm.tm.sendMessage(p.getPayload()); } } private static final Logger log = Logger.getLogger(Channel.class); }