blob: 92f7a6b0a8c89c7752df763328bcf6b2bc4b8c33 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2018 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 2.0 which accompanies this distribution, and is
* available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tm.terminal.view.ui.streams;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
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.terminal.view.core.interfaces.ITerminalServiceOutputStreamMonitorListener;
import org.eclipse.tm.terminal.view.core.interfaces.constants.ILineSeparatorConstants;
import org.eclipse.tm.terminal.view.ui.activator.UIPlugin;
import org.eclipse.tm.terminal.view.ui.interfaces.tracing.ITraceIds;
import org.eclipse.tm.terminal.view.ui.nls.Messages;
import org.eclipse.ui.services.IDisposable;
/**
* Output stream monitor implementation.
* <p>
* <b>Note:</b> The output is going <i>to</i> the terminal. Therefore, the output
* stream monitor is attached to the stdout and/or stderr stream of the monitored
* (remote) process.
*/
public class OutputStreamMonitor implements IDisposable {
// The default buffer size to use
private static final int BUFFER_SIZE = 8192;
// Reference to the parent terminal control
private final ITerminalControl terminalControl;
// Reference to the monitored (input) stream
private final InputStream stream;
// The line separator used by the monitored (input) stream
private final String lineSeparator;
// Reference to the thread reading the stream
private Thread thread;
// Flag to mark the monitor disposed. When disposed,
// no further data is read 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>();
// The list of registered listener
private final ListenerList listeners;
/**
* Constructor.
*
* @param terminalControl The parent terminal control. Must not be <code>null</code>.
* @param stream The stream. Must not be <code>null</code>.
* @param lineSeparator The line separator used by the stream.
*/
public OutputStreamMonitor(ITerminalControl terminalControl, InputStream stream, String lineSeparator) {
super();
Assert.isNotNull(terminalControl);
this.terminalControl = terminalControl;
Assert.isNotNull(stream);
this.stream = new BufferedInputStream(stream, BUFFER_SIZE);
this.lineSeparator = lineSeparator;
this.listeners = new ListenerList();
}
/**
* Register a streams data receiver listener.
*
* @param listener The listener. Must not be <code>null</code>.
*/
public final void addListener(ITerminalServiceOutputStreamMonitorListener listener) {
Assert.isNotNull(listener);
listeners.add(listener);
}
/**
* Unregister a streams data receiver listener.
*
* @param listener The listener. Must not be <code>null</code>.
*/
public final void removeListener(ITerminalServiceOutputStreamMonitorListener listener) {
Assert.isNotNull(listener);
listeners.remove(listener);
}
/**
* 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 */ }
// Dispose all registered disposable objects
for (IDisposable disposable : disposables) disposable.dispose();
// Clear the list
disposables.clear();
}
/**
* Starts the terminal output stream monitor.
*/
protected 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() {
readStream();
}
};
// Create the reader thread
thread = new Thread(runnable, "Terminal Output Stream Monitor Thread"); //$NON-NLS-1$
// Configure the reader thread
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
// Start the processing
thread.start();
}
/**
* Returns the terminal control that this stream monitor is associated with.
*/
protected ITerminalControl getTerminalControl() {
return terminalControl;
}
/**
* Reads from the output stream and write the read content
* to the terminal control output stream.
*/
void readStream() {
// Creates the read buffer
byte[] readBuffer = new byte[BUFFER_SIZE];
// We need to maintain UI responsiveness but still stream the content
// to the terminal control fast. Put the thread to a short sleep each second.
long sleepMarker = System.currentTimeMillis();
// Read from the stream until EOS is reached or the
// monitor is marked disposed.
int read = 0;
while (read >= 0 && !disposed) {
try {
// Read from the stream
read = stream.read(readBuffer);
// If some data has been read, append to the terminal
// control output stream
if (read > 0) {
// Allow for post processing the read content before appending
byte[] processedReadBuffer = onContentReadFromStream(readBuffer, read);
if (processedReadBuffer != readBuffer) {
read = processedReadBuffer.length;
}
terminalControl.getRemoteToTerminalOutputStream().write(processedReadBuffer, 0, read);
}
} 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.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status);
}
break;
} catch (NullPointerException e) {
// killing the stream monitor while reading can cause an NPE
// when reading from the stream
if (!disposed && thread != null) {
IStatus status = new Status(IStatus.ERROR, UIPlugin.getUniqueIdentifier(),
NLS.bind(Messages.OutputStreamMonitor_error_readingFromStream, e.getLocalizedMessage()), e);
UIPlugin.getDefault().getLog().log(status);
}
break;
}
// See above -> Thread will go to sleep each second
if (System.currentTimeMillis() - sleepMarker > 1000) {
sleepMarker = System.currentTimeMillis();
try { Thread.sleep(1); } catch (InterruptedException e) { /* ignored on purpose */ }
}
}
// Dispose ourself
dispose();
}
/**
* Allow for processing of data from byte stream after it is read from
* client but before it is appended to the terminal. If the returned byte
* array is different than the one that was passed in with the byteBuffer
* argument, then the bytesRead value will be ignored and the full
* returned array will be written out.
*
* @param byteBuffer The byte stream. Must not be <code>null</code>.
* @param bytesRead The number of bytes that were read into the read buffer.
* @return The processed byte stream.
*
*/
protected byte[] onContentReadFromStream(byte[] byteBuffer, int bytesRead) {
Assert.isNotNull(byteBuffer);
// If tracing is enabled, print out the decimal byte values read
if (UIPlugin.getTraceHandler().isSlotEnabled(0, ITraceIds.TRACE_OUTPUT_STREAM_MONITOR)) {
StringBuilder debug = new StringBuilder("byteBuffer [decimal, " + bytesRead + " bytes] : "); //$NON-NLS-1$ //$NON-NLS-2$
for (int i = 0; i < bytesRead; i++) {
debug.append(Byte.valueOf(byteBuffer[i]).intValue());
debug.append(' ');
}
System.out.println(debug.toString());
}
// Remember if the text got changed.
boolean changed = false;
// How can me make sure that we don't mess with the encoding here?
String text = new String(byteBuffer, 0, bytesRead);
// Shift-In (14) and Shift-Out(15) confuses the terminal widget
if (text.indexOf(14) != -1 || text.indexOf(15) != -1) {
text = text.replaceAll("\\x0e", "").replaceAll("\\x0f", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
changed = true;
}
// Check on the line separator setting
if (lineSeparator != null
&& !ILineSeparatorConstants.LINE_SEPARATOR_CRLF.equals(lineSeparator)) {
String separator = ILineSeparatorConstants.LINE_SEPARATOR_LF.equals(lineSeparator) ? "\n" : "\r"; //$NON-NLS-1$ //$NON-NLS-2$
String separator2 = ILineSeparatorConstants.LINE_SEPARATOR_LF.equals(lineSeparator) ? "\r" : "\n"; //$NON-NLS-1$ //$NON-NLS-2$
if (text.indexOf(separator) != -1) {
String[] fragments = text.split(separator);
StringBuilder b = new StringBuilder();
for (int i = 0; i < fragments.length; i++) {
String fragment = fragments[i];
String nextFragment = i + 1 < fragments.length ? fragments[i + 1] : null;
b.append(fragment);
if (fragment.endsWith(separator2) || (nextFragment != null && nextFragment.startsWith(separator2))) {
// Both separators are found, just add the original separator
b.append(separator);
} else {
b.append("\n\r"); //$NON-NLS-1$
}
}
if (!text.equals(b.toString())) {
text = b.toString();
changed = true;
}
}
}
// If changed, get the new bytes array
if (changed) {
byteBuffer = text.getBytes();
bytesRead = byteBuffer.length;
}
// If listeners are registered, invoke the listeners now.
if (listeners.size() > 0) {
for (Object candidate : listeners.getListeners()) {
if (!(candidate instanceof ITerminalServiceOutputStreamMonitorListener)) continue;
((ITerminalServiceOutputStreamMonitorListener)candidate).onContentReadFromStream(byteBuffer, bytesRead);
}
}
return byteBuffer;
}
}