mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-07-25 18:05:33 +02:00
Bug 303808: Add a GDB CLI Console history buffer
Change-Id: Ide52d3223b65323f3e9476d2c280d0ebe6640ba8
This commit is contained in:
parent
b4cc24e31e
commit
36fd126919
2 changed files with 335 additions and 94 deletions
|
@ -7,23 +7,12 @@
|
|||
*******************************************************************************/
|
||||
package org.eclipse.cdt.dsf.gdb.internal.ui.console;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.cdt.debug.ui.debuggerconsole.IDebuggerConsoleView;
|
||||
import org.eclipse.cdt.utils.pty.PTY;
|
||||
import org.eclipse.core.runtime.CoreException;
|
||||
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.debug.core.ILaunch;
|
||||
import org.eclipse.debug.core.ILaunchConfiguration;
|
||||
import org.eclipse.debug.ui.DebugUITools;
|
||||
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
|
||||
import org.eclipse.ui.PlatformUI;
|
||||
import org.eclipse.ui.console.AbstractConsole;
|
||||
import org.eclipse.ui.console.IConsoleView;
|
||||
|
@ -62,89 +51,6 @@ public class GdbFullCliConsole extends AbstractConsole implements IGDBDebuggerCo
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private final class GdbTerminalConnector implements IGdbTerminalControlConnector {
|
||||
private final Set<ITerminalControl> fTerminalPageControls = new HashSet<>();
|
||||
private final Process fProcess;
|
||||
private final Job fOutputStreamJob;
|
||||
private final Job fErrorStreamJob;
|
||||
|
||||
public GdbTerminalConnector(Process process) {
|
||||
fProcess = process;
|
||||
|
||||
fOutputStreamJob = new OutputReadJob(process.getInputStream());
|
||||
fOutputStreamJob.schedule();
|
||||
fErrorStreamJob = new OutputReadJob(process.getErrorStream());
|
||||
fErrorStreamJob.schedule();
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
fOutputStreamJob.cancel();
|
||||
fErrorStreamJob.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPageTerminalControl(ITerminalControl terminalControl) {
|
||||
fTerminalPageControls.add(terminalControl);
|
||||
}
|
||||
|
||||
@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 inputStream) {
|
||||
super("GDB CLI output Job"); //$NON-NLS-1$
|
||||
fInputStream = inputStream;
|
||||
}
|
||||
|
||||
@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) {
|
||||
for (ITerminalControl control : fTerminalPageControls) {
|
||||
control.getRemoteToTerminalOutputStream().write(b, 0, read);
|
||||
}
|
||||
}
|
||||
} while (read >= 0);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
|
||||
return Status.OK_STATUS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ILaunch getLaunch() { return fLaunch; }
|
||||
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
/*******************************************************************************
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue