| /******************************************************************************* |
| * 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; |
| } |
| } |