/*******************************************************************************
* Copyright (c) 2016 Ericsson 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
*******************************************************************************/
package org.eclipse.cdt.dsf.gdb.internal.ui.console;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.cdt.dsf.gdb.IGdbDebugPreferenceConstants;
import org.eclipse.cdt.dsf.gdb.internal.ui.GdbUIPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
/**
* This class will read from the GDB process output and error streams and will write it to any registered
* ITerminalControl. It must continue reading from the streams, even if there are no ITerminalControl to write
* to. This is important to prevent GDB's output buffer from getting full and then completely stopping.
*
* In addition this class manages a history buffer which will be used to populate a new console with history
* information already collected for the same session. Used for example when closing an re-opening a console.
*/
public class GdbTerminalConnector implements IGdbTerminalControlConnector {
/**
* The maximum number of lines the internal history buffer can hold
*/
private static final int HIST_BUFFER_MAX_SIZE = 1000; /* lines */
/**
* The History buffer is written out in chunks of lines, this chunks are taken from the total history buffer
* and written out sequentially.
* This constant determines the writing size in number of lines (i.e. chunk size)
*/
private static final int HIST_BUFFER_WRITE_SIZE = 100; /* lines */
private final Process fProcess;
private final Set<ITerminalControl> fTerminalPageControls = Collections.synchronizedSet(new HashSet<>());
private final Job fOutputStreamJob;
private final Job fErrorStreamJob;
private final ConsoleHistoryLinesBuffer fHistoryBuffer;
public GdbTerminalConnector(Process process) {
fProcess = process;
// Using a history buffer size aligned with the preferences for console buffering
// but not exceeding the internal maximum
// We cap the history buffer to an internal maximum in order to prevent excessive use
// of memory, the preference value applies to the console (not the history buffer) and can be specified
// to billions of lines.
// Handling billion of lines for the history buffer would require a completely different approach
// to this implementation, possibly making use of the hard disk instead of in memory.
IPreferenceStore store = GdbUIPlugin.getDefault().getPreferenceStore();
int prefBufferLines = store.getInt(IGdbDebugPreferenceConstants.PREF_CONSOLE_BUFFERLINES);
int history_buffer_size = prefBufferLines < HIST_BUFFER_MAX_SIZE ? prefBufferLines
: HIST_BUFFER_MAX_SIZE;
fHistoryBuffer = new ConsoleHistoryLinesBuffer(history_buffer_size);
// Start the jobs that read the GDB process output streams
String jobSuffix = ""; //$NON-NLS-1$
fOutputStreamJob = new OutputReadJob(process.getInputStream(), jobSuffix);
fOutputStreamJob.schedule();
jobSuffix = "-Error"; //$NON-NLS-1$
fErrorStreamJob = new OutputReadJob(process.getErrorStream(), jobSuffix);
fErrorStreamJob.schedule();
}
/**
* This class will hold a buffer of history lines, it uses a queue to easily pop out the oldest lines once
* the maximum is being exceeded.</br>
* It also keeps track of partial text at the end of the receiving input i.e. not yet forming a complete
* line, once it forms a complete line it gets integrated in the queue
*
* In addition the API used in this implementation are synchronized to allow consistent information among
* the Jobs using it
*/
private class ConsoleHistoryLinesBuffer extends ArrayDeque<String> {
private static final long serialVersionUID = 1L;
/**
* Holds the last characters received but not yet forming a complete line, The HistoryBuffer contains
* complete lines to be able to keep a proper line count that can be then be dimensioned by e.g.
* preferences
*/
private final StringBuilder fHistoryRemainder = new StringBuilder();
public ConsoleHistoryLinesBuffer(int size) {
super(size);
}
/**
* A simple container holding consistent information of the history lines and accumulated remainder
* at a particular point in time
*/
private class HistorySnapShot {
private final String[] fHistoryLinesSnapShot;
private final String fHistoryRemainderSnapShot;
private HistorySnapShot(String[] historyLines, String historyRemainder) {
fHistoryLinesSnapShot = historyLines;
fHistoryRemainderSnapShot = historyRemainder;
}
}
@Override
public synchronized int size() {
return super.size();
}
/**
* @param text
* Accumulate the text not yet forming a line
*/
private synchronized void appendRemainder(String text) {
fHistoryRemainder.append(text);
}
/**
* @return Returns the accumulated text and clears its internal value
*/
private synchronized String popRemainder() {
String remainder = fHistoryRemainder.toString();
fHistoryRemainder.setLength(0);
return remainder;
}
/**
* @return The history information at a specific point in time
*/
private synchronized HistorySnapShot getHistorySnapShot() {
return new HistorySnapShot(toArray(), fHistoryRemainder.toString());
}
/**
* Writes complete lines to the history buffer, and accumulates incomplete lines "remainder" until
* they form a full line.
*
* Adding complete lines to the buffer is needed to respect a specified maximum number of buffered
* lines
*/
public synchronized void appendHistory(byte[] b, int read) {
// Read this new input
StringBuilder info = new StringBuilder(new String(b, StandardCharsets.UTF_8));
info.setLength(read);
// Separate by lines but keep the separator character
String regEx = "(?<=\\n)"; //$NON-NLS-1$
String[] chunks = info.toString().split(regEx);
for (int i = 0; i < chunks.length; i++) {
StringBuilder lineBuilder = new StringBuilder();
if (i == 0) {
// Add the previous incomplete line info ("remainder") first
lineBuilder.append(popRemainder());
}
lineBuilder.append(chunks[i]);
String line = lineBuilder.toString();
if (line.endsWith("\n")) { //$NON-NLS-1$
// We have build a complete line, So lets add it to the history
// Make sure we don't exceed the maximum buffer size
while (this.size() >= HIST_BUFFER_MAX_SIZE) {
this.remove();
}
this.offer(line);
} else {
// The only line with no separator shall be the last one
// otherwise it should have been split
assert i == (chunks.length - 1);
appendRemainder(line);
}
}
}
@Override
public synchronized String[] toArray() {
return super.toArray(new String[size()]);
}
}
public void dispose() {
fOutputStreamJob.cancel();
fErrorStreamJob.cancel();
}
@Override
public void addPageTerminalControl(ITerminalControl terminalControl) {
// write the currently available buffered history to this new terminal
new WriteHistoryJob(terminalControl).schedule();
}
@Override
public void removePageTerminalControl(ITerminalControl terminalControl) {
if (terminalControl != null) {
fTerminalPageControls.remove(terminalControl);
}
}
@Override
public OutputStream getTerminalToRemoteStream() {
// When the user writes to the terminal, it should be sent
// directly to GDB
return fProcess.getOutputStream();
}
private class OutputReadJob extends Job {
{
setSystem(true);
}
private InputStream fInputStream;
private OutputReadJob(InputStream procStream, String nameSuffix) {
super("GDB CLI output Job" + nameSuffix); //$NON-NLS-1$
fInputStream = procStream;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
byte[] b = new byte[1024];
int read = 0;
do {
if (monitor.isCanceled()) {
break;
}
read = fInputStream.read(b);
if (read > 0) {
// Write fresh output to the existing consoles
synchronized (fTerminalPageControls) {
for (ITerminalControl control : fTerminalPageControls) {
control.getRemoteToTerminalOutputStream().write(b, 0, read);
}
// Add this input to the history buffer
fHistoryBuffer.appendHistory(b, read);
}
}
} while (read >= 0);
} catch (IOException e) {
}
return Status.OK_STATUS;
}
}
private class WriteHistoryJob extends Job {
{
setSystem(true);
}
private final ITerminalControl fTerminalControl;
public WriteHistoryJob(ITerminalControl terminalControl) {
super("GDB CLI write history job"); //$NON-NLS-1$
fTerminalControl = terminalControl;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
OutputStream terminalOutputStream = fTerminalControl.getRemoteToTerminalOutputStream();
if (terminalOutputStream == null) {
return Status.OK_STATUS;
}
// Append the buffered lines to the terminal control instance
synchronized (fTerminalPageControls) {
// First get a snapshot of the current information in the history buffer
ConsoleHistoryLinesBuffer.HistorySnapShot history = fHistoryBuffer.getHistorySnapShot();
String[] buffLines = history.fHistoryLinesSnapShot;
// Writing the current buffer in chunks of data
// Calculate the initial limits
// The position pointed by 'end' is not written out on the iteration, but used as the limit
int start = 0;
int end = buffLines.length <= HIST_BUFFER_WRITE_SIZE ? buffLines.length : HIST_BUFFER_WRITE_SIZE;
// Write the history in chunks of lines
StringBuilder sb = new StringBuilder(HIST_BUFFER_WRITE_SIZE);
while (start < buffLines.length) {
// Prepare the data chunk to write
String[] chunk = Arrays.copyOfRange(buffLines, start, end);
for (String line : chunk) {
sb.append(line);
}
// Calculate limits for next iteration
start = end;
int linesLeft = buffLines.length - end;
end = start + (linesLeft <= HIST_BUFFER_WRITE_SIZE ? linesLeft : HIST_BUFFER_WRITE_SIZE);
// if this is the last write,
if (!(start < buffLines.length)) {
// Add the accumulated remainder value (i.e. not yet a complete line) as the last line
sb.append(history.fHistoryRemainderSnapShot);
}
// Write to Output Stream
if (sb.length() > 0) {
byte[] bytes = sb.toString().getBytes();
try {
terminalOutputStream.write(bytes, 0, bytes.length);
} catch (IOException e) {
}
sb.setLength(0);
}
}
// Add it to the list so it can now receive new input
fTerminalPageControls.add(fTerminalControl);
}
return Status.OK_STATUS;
}
}
}