/******************************************************************************* * Copyright (c) 2011 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: * Wind River Systems - initial API and implementation *******************************************************************************/ package org.eclipse.tm.te.ui.terminals.streams; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.osgi.util.NLS; import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl; import org.eclipse.tm.te.runtime.services.interfaces.constants.ILineSeparatorConstants; import org.eclipse.tm.te.ui.terminals.activator.UIPlugin; import org.eclipse.tm.te.ui.terminals.nls.Messages; import org.eclipse.ui.services.IDisposable; /** * Input stream monitor implementation. * <p> * <b>Note:</b> The input is coming <i>from</i> the terminal. Therefore, the input * stream monitor is attached to the stdin stream of the monitored (remote) process. */ @SuppressWarnings("restriction") public class InputStreamMonitor extends OutputStream implements IDisposable { // Reference to the parent terminal control @SuppressWarnings("unused") private final ITerminalControl terminalControl; // Reference to the monitored (output) stream private final OutputStream stream; // Reference to the thread writing the stream private Thread thread; // Flag to mark the monitor disposed. When disposed, // no further data is written from the monitored stream. private boolean disposed; // A list of object to dispose if this monitor is disposed private final List<IDisposable> disposables = new ArrayList<IDisposable>(); // Queue to buffer the data to write to the output stream private final Queue<byte[]> queue = new LinkedList<byte[]>(); // ***** Line separator replacement logic ***** // ***** Adapted from org.eclipse.tm.internal.terminal.local.LocalTerminalOutputStream ***** private final static int TERMINAL_SENDS_CR = 0; private final static int TERMINAL_SENDS_CRLF = 1; private final static int PROGRAM_EXPECTS_LF = 0; private final static int PROGRAM_EXPECTS_CRLF = 1; private final static int PROGRAM_EXPECTS_CR = 2; private final static int NO_CHANGE = 0; private final static int CHANGE_CR_TO_LF = 1; private final static int INSERT_LF_AFTER_CR = 2; private final static int REMOVE_CR = 3; private final static int REMOVE_LF = 4; // CRLF conversion table: // // Expected line separator --> | LF | CRLF | CR | // ------------------------------------+-----------------+--------------------+----------------+ // Local echo off - control sends CR | change CR to LF | insert LF after CR | no change | // ------------------------------------+-----------------+--------------------+----------------+ // Local echo on - control sends CRLF | remove CR | no change | remove LF | // private final static int[][] CRLF_REPLACEMENT = { {CHANGE_CR_TO_LF, INSERT_LF_AFTER_CR, NO_CHANGE}, {REMOVE_CR, NO_CHANGE, REMOVE_LF} }; private int replacement; /** * Constructor. * * @param terminalControl The parent terminal control. Must not be <code>null</code>. * @param stream The stream. Must not be <code>null</code>. * @param localEcho Local echo on or off. * @param lineSeparator The line separator used by the stream. */ public InputStreamMonitor(ITerminalControl terminalControl, OutputStream stream, boolean localEcho, String lineSeparator) { super(); Assert.isNotNull(terminalControl); this.terminalControl = terminalControl; Assert.isNotNull(stream); this.stream = stream; // Determine the line separator replacement setting int terminalSends = localEcho ? TERMINAL_SENDS_CRLF : TERMINAL_SENDS_CR; if (lineSeparator == null) { replacement = NO_CHANGE; } else { int programExpects; if (lineSeparator.equals(ILineSeparatorConstants.LINE_SEPARATOR_LF)) { programExpects = PROGRAM_EXPECTS_LF; } else if (lineSeparator.equals(ILineSeparatorConstants.LINE_SEPARATOR_CR)) { programExpects = PROGRAM_EXPECTS_CR; } else { programExpects = PROGRAM_EXPECTS_CRLF; } replacement = CRLF_REPLACEMENT[terminalSends][programExpects]; } } /** * Adds the given disposable object to the list. The method will do nothing * if either the disposable object is already part of the list or the monitor * is disposed. * * @param disposable The disposable object. Must not be <code>null</code>. */ public final void addDisposable(IDisposable disposable) { Assert.isNotNull(disposable); if (!disposed && !disposables.contains(disposable)) disposables.add(disposable); } /** * Removes the disposable object from the list. * * @param disposable The disposable object. Must not be <code>null</code>. */ public final void removeDisposable(IDisposable disposable) { Assert.isNotNull(disposable); disposables.remove(disposable); } /* (non-Javadoc) * @see org.eclipse.ui.services.IDisposable#dispose() */ @Override public void dispose() { // If already disposed --> return immediately if (disposed) return; // Mark the monitor disposed disposed = true; // Close the stream (ignore exceptions on close) try { stream.close(); } catch (IOException e) { /* ignored on purpose */ } // And interrupt the thread close(); // Dispose all registered disposable objects for (IDisposable disposable : disposables) disposable.dispose(); // Clear the list disposables.clear(); } /** * Close the terminal input stream monitor. */ @Override public void close() { // Not initialized -> return immediately if (thread == null) return; // Copy the reference final Thread oldThread = thread; // Unlink the monitor from the thread thread = null; // And interrupt the writer thread oldThread.interrupt(); } /** * Starts the terminal output stream monitor. */ public void startMonitoring() { // If already initialized -> return immediately if (thread != null) return; // Create a new runnable which is constantly reading from the stream Runnable runnable = new Runnable() { @Override public void run() { writeStream(); } }; // Create the writer thread thread = new Thread(runnable, "Terminal Input Stream Monitor Thread"); //$NON-NLS-1$ // Configure the writer thread thread.setDaemon(true); // Start the processing thread.start(); } /** * Reads from the queue and writes the read content to the stream. */ protected void writeStream() { // Read from the queue and write to the stream until disposed while (thread != null && !disposed) { // If the queue is empty, wait until notified if (queue.isEmpty()) { synchronized(queue) { try { queue.wait(); } catch (InterruptedException e) { /* ignored on purpose */ } } } // If the queue is not empty, take the first element // and write the data to the stream while (!queue.isEmpty() && !disposed) { // Retrieves the queue head (is null if queue is empty (should never happen)) byte[] data = queue.poll(); if (data != null) { try { // Write the data to the stream stream.write(data); // Flush the stream immediately stream.flush(); } catch (IOException e) { // IOException received. If this is happening when already disposed -> ignore if (!disposed) { IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(), NLS.bind(Messages.InputStreamMonitor_error_writingToStream, e.getLocalizedMessage()), e); UIPlugin.getDefault().getLog().log(status); } } } } } // Dispose the stream dispose(); } /* (non-Javadoc) * @see java.io.OutputStream#write(int) */ @Override public void write(int b) throws IOException { synchronized(queue) { queue.add(new byte[] { (byte)b }); queue.notifyAll(); } } /* (non-Javadoc) * @see java.io.OutputStream#write(byte[], int, int) */ @Override public void write(byte[] b, int off, int len) throws IOException { // Write the whole block to the queue to avoid synchronization // to happen for every byte. To do so, we have to avoid calling // the super method. Therefore we have to do the same checking // here as the base class does. // Null check. See the implementation in OutputStream. if (b == null) throw new NullPointerException(); // Boundary check. See the implementation in OutputStream. if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } // Make sure that the written block is not interlaced with other input. synchronized(queue) { // Preprocess the block to be written byte[] processedBytes = onWriteContentToStream(b, off, len); // If the returned array is not the original one, adjust offset and length if (processedBytes != b) { off = 0; len = processedBytes.length; } // Get the content from the byte buffer specified by offset and length byte[] bytes = new byte[len]; int j = 0; for (int i = 0 ; i < len ; i++) { bytes[j++] = b[off + i]; } queue.add(bytes); queue.notifyAll(); } } /** * Allow for processing of data from byte stream from the terminal before * it is written to the output stream. If the returned byte array is different * than the one that was passed in with the bytes argument, then the * length value will be adapted. * * @param bytes The byte stream. Must not be <code>null</code>. * @param off The offset. * @param len the length. * * @return The processed byte stream. * */ protected byte[] onWriteContentToStream(byte[] bytes, int off, int len) { Assert.isNotNull(bytes); if (replacement != NO_CHANGE && len > 0) { String text = new String(bytes, off, len); // // TODO: check whether this is correct! new String(byte[], int, int) always uses the default // encoding! if (replacement == CHANGE_CR_TO_LF) { text = text.replace('\r', '\n'); } else if (replacement == INSERT_LF_AFTER_CR) { text = text.replaceAll(ILineSeparatorConstants.LINE_SEPARATOR_CR, "\r\n"); //$NON-NLS-1$ } else if (replacement == REMOVE_CR) { text = text.replaceAll(ILineSeparatorConstants.LINE_SEPARATOR_CR, ""); //$NON-NLS-1$ } else if (replacement == REMOVE_LF) { text = text.replaceAll(ILineSeparatorConstants.LINE_SEPARATOR_LF, ""); //$NON-NLS-1$ } if (text.length() > 0) { bytes = text.getBytes(); } } return bytes; } }