/*******************************************************************************
* Copyright (c) 2002, 2016 QNX Software Systems 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:
* QNX Software Systems - initial API and implementation
* Dmitry Kozlov (CodeSourcery) - Build error highlighting and navigation
* Andrew Gvozdev (Quoin Inc.) - Copy build log (bug 306222)
* Alex Collins (Broadcom Corp.) - Global console
* Sergey Prigogin (Google) - Performance improvements
*******************************************************************************/
package org.eclipse.cdt.internal.ui.buildconsole;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IDocumentPartitionerExtension;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.cdt.core.ConsoleOutputStream;
import org.eclipse.cdt.core.ProblemMarkerInfo;
import org.eclipse.cdt.core.resources.IConsole;
import org.eclipse.cdt.core.resources.ResourcesUtil;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.internal.ui.preferences.BuildConsolePreferencePage;
public class BuildConsolePartitioner
implements IDocumentPartitioner, IDocumentPartitionerExtension, IConsole, IPropertyChangeListener {
private static class SynchronizedDeque<E> implements Deque<E> {
private final Deque<E> deque;
public SynchronizedDeque(Deque<E> deque) {
this.deque = deque;
}
@Override
public synchronized boolean isEmpty() {
return deque.isEmpty();
}
@Override
public synchronized void addFirst(E e) {
deque.addFirst(e);
}
@Override
public synchronized void addLast(E e) {
deque.addLast(e);
}
@Override
public synchronized Object[] toArray() {
return deque.toArray();
}
@Override
public synchronized <T> T[] toArray(T[] a) {
return deque.toArray(a);
}
@Override
public synchronized boolean offerFirst(E e) {
return deque.offerFirst(e);
}
@Override
public synchronized boolean offerLast(E e) {
return deque.offerLast(e);
}
@Override
public synchronized E removeFirst() {
return deque.removeFirst();
}
@Override
public synchronized E removeLast() {
return deque.removeLast();
}
@Override
public synchronized E pollFirst() {
return deque.pollFirst();
}
@Override
public synchronized E pollLast() {
return deque.pollLast();
}
@Override
public synchronized E getFirst() {
return deque.getFirst();
}
@Override
public synchronized E getLast() {
return deque.getLast();
}
@Override
public synchronized E peekFirst() {
return deque.peekFirst();
}
@Override
public synchronized E peekLast() {
return deque.peekLast();
}
@Override
public synchronized boolean removeFirstOccurrence(Object o) {
return deque.removeFirstOccurrence(o);
}
@Override
public synchronized boolean containsAll(Collection<?> c) {
return deque.containsAll(c);
}
@Override
public synchronized boolean removeLastOccurrence(Object o) {
return deque.removeLastOccurrence(o);
}
@Override
public synchronized boolean addAll(Collection<? extends E> c) {
return deque.addAll(c);
}
@Override
public synchronized boolean add(E e) {
return deque.add(e);
}
@Override
public synchronized boolean removeAll(Collection<?> c) {
return deque.removeAll(c);
}
@Override
public synchronized boolean offer(E e) {
return deque.offer(e);
}
@Override
public synchronized boolean retainAll(Collection<?> c) {
return deque.retainAll(c);
}
@Override
public synchronized E remove() {
return deque.remove();
}
@Override
public synchronized E poll() {
return deque.poll();
}
@Override
public synchronized E element() {
return deque.element();
}
@Override
public synchronized void clear() {
deque.clear();
}
@Override
public synchronized boolean equals(Object o) {
return deque.equals(o);
}
@Override
public synchronized E peek() {
return deque.peek();
}
@Override
public synchronized void push(E e) {
deque.push(e);
}
@Override
public synchronized E pop() {
return deque.pop();
}
@Override
public synchronized int hashCode() {
return deque.hashCode();
}
@Override
public synchronized boolean remove(Object o) {
return deque.remove(o);
}
@Override
public synchronized boolean contains(Object o) {
return deque.contains(o);
}
@Override
public synchronized int size() {
return deque.size();
}
@Override
public synchronized Iterator<E> iterator() {
return deque.iterator();
}
@Override
public synchronized Iterator<E> descendingIterator() {
return deque.descendingIterator();
}
}
private IProject fProject;
private int openStreamCount = 0;
/**
* List of partitions
*/
List<ITypedRegion> fPartitions = new ArrayList<ITypedRegion>(5);
private int fMaxLines;
/**
* The stream that was last appended to
*/
BuildConsoleStreamDecorator fLastStream;
BuildConsoleDocument fDocument;
DocumentMarkerManager fDocumentMarkerManager;
boolean killed;
BuildConsoleManager fManager;
/**
* A queue of stream entries written to standard out and standard err.
* Entries appended to the end of the queue and removed from the front.
*/
private final Deque<StreamEntry> fQueue = new SynchronizedDeque<StreamEntry>(
new ArrayDeque<StreamEntry>());
private URI fLogURI;
private OutputStream fLogStream;
private class StreamEntry {
public static final int EVENT_APPEND = 0;
public static final int EVENT_OPEN_LOG = 1;
public static final int EVENT_CLOSE_LOG = 2;
public static final int EVENT_OPEN_APPEND_LOG = 3;
/** Identifier of the stream written to. */
private BuildConsoleStreamDecorator fStream;
/** The text written */
private StringBuilder fText = null;
/** Problem marker corresponding to the line of text */
private ProblemMarkerInfo fMarker;
/** Type of event **/
private int eventType;
public StreamEntry(String text, BuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) {
fText = new StringBuilder(text);
fStream = stream;
fMarker = marker;
eventType = EVENT_APPEND;
}
/**
* This constructor is used for special events such as clear console or
* close log.
*
* @param event
* - kind of event.
*/
public StreamEntry(int event) {
fText = null;
fStream = null;
fMarker = null;
eventType = event;
}
/**
* Returns the stream identifier
*/
public BuildConsoleStreamDecorator getStream() {
return fStream;
}
public void appendText(String text) {
fText.append(text);
}
public int size() {
return fText.length();
}
/**
* Returns the text written
*/
public String getText() {
return fText.toString();
}
/**
* Returns error marker
*/
public ProblemMarkerInfo getMarker() {
return fMarker;
}
/**
* Returns type of event
*/
public int getEventType() {
return eventType;
}
}
/**
* Construct a partitioner that is not associated with a specific project
*/
public BuildConsolePartitioner(BuildConsoleManager manager) {
this(null, manager);
}
public BuildConsolePartitioner(IProject project, BuildConsoleManager manager) {
fProject = project;
fManager = manager;
fMaxLines = BuildConsolePreferencePage.buildConsoleLines();
fDocument = new BuildConsoleDocument();
fDocument.setDocumentPartitioner(this);
fDocumentMarkerManager = new DocumentMarkerManager(fDocument, this);
connect(fDocument);
fLogURI = null;
fLogStream = null;
}
/**
* Sets the indicator that stream was opened so logging can be started.
* Should be called when opening the output stream.
*/
public void setStreamOpened() {
fQueue.add(new StreamEntry(StreamEntry.EVENT_OPEN_LOG));
asyncProcessQueue();
}
/**
* Open the stream for appending. Must be called after a call to
* setStreamOpened(). Can be used to reopen a stream for writing after it
* has been closed, without emptying the log file.
*/
public void setStreamAppend() {
fQueue.add(new StreamEntry(StreamEntry.EVENT_OPEN_APPEND_LOG));
asyncProcessQueue();
}
/**
* Sets the indicator that stream was closed so logging should be stopped.
* Should be called when build process has finished. Note that there could
* still be unprocessed console stream entries in the queue being worked on
* in the background.
*/
public void setStreamClosed() {
fQueue.add(new StreamEntry(StreamEntry.EVENT_CLOSE_LOG));
asyncProcessQueue();
}
/**
* Adds the new text to the document.
*
* @param text
* - the text to append.
* @param stream
* - the stream to append to.
*/
public void appendToDocument(String text, BuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) {
boolean addToQueue = true;
synchronized (fQueue) {
StreamEntry entry = fQueue.peekLast();
if (entry != null) {
// If last stream is the same and the size of the queued entry
// has not exceeded
// the batch size, avoid creating a new entry and append the new
// text to the last
// entry in the queue. The batch size is adaptive and grows with
// the length of
// the queue.
if (entry.getStream() == stream && entry.getEventType() == StreamEntry.EVENT_APPEND
&& entry.getMarker() == marker && entry.size() < 2000 * fQueue.size()) {
entry.appendText(text);
addToQueue = false;
}
}
if (addToQueue) {
fQueue.add(new StreamEntry(text, stream, marker));
}
}
if (addToQueue) {
asyncProcessQueue();
}
}
/**
* Asynchronous processing of stream entries to append to console. Note that
* all these are processed by the same thread - the user-interface thread as
* of {@link Display#asyncExec(Runnable)}.
*/
private void asyncProcessQueue() {
Runnable r = new Runnable() {
@Override
public void run() {
StreamEntry entry;
entry = fQueue.pollFirst();
if (entry == null)
return;
switch (entry.getEventType()) {
case StreamEntry.EVENT_OPEN_LOG:
openStreamCount++;
//$FALL-THROUGH$
case StreamEntry.EVENT_OPEN_APPEND_LOG:
logOpen(entry.getEventType() == StreamEntry.EVENT_OPEN_APPEND_LOG);
break;
case StreamEntry.EVENT_APPEND:
fLastStream = entry.getStream();
try {
warnOfContentChange(fLastStream);
if (fLastStream == null) {
// special case to empty document
fPartitions.clear();
fDocumentMarkerManager.clear();
fDocument.set(""); //$NON-NLS-1$
}
String text = entry.getText();
if (text.length() > 0) {
addStreamEntryToDocument(entry);
log(text);
boolean allowSlack = false;
entry = fQueue.peekFirst();
if (entry != null && entry.getEventType() == StreamEntry.EVENT_APPEND) {
// Buffer truncation is an expensive operation.
// Allow some slack
// if more data is coming and we will be
// truncating the buffer
// again soon.
allowSlack = true;
}
checkOverflow(allowSlack);
}
} catch (BadLocationException e) {
}
break;
case StreamEntry.EVENT_CLOSE_LOG:
openStreamCount--;
if (openStreamCount <= 0) {
openStreamCount = 0;
logClose();
}
break;
}
}
/**
* Open the log
*
* @param append
* Set to true if the log should be opened for appending,
* false for overwriting.
*/
private void logOpen(boolean append) {
fLogURI = fManager.getLogURI(fProject);
if (fLogURI != null) {
try {
IFileStore logStore = EFS.getStore(fLogURI);
// Ensure the directory exists before opening the file
IFileStore dir = logStore.getParent();
if (dir != null)
dir.mkdir(EFS.NONE, null);
int opts = append ? EFS.APPEND : EFS.NONE;
fLogStream = logStore.openOutputStream(opts, null);
} catch (CoreException e) {
CUIPlugin.log(e);
} finally {
ResourcesUtil.refreshWorkspaceFiles(fLogURI);
}
}
}
private void log(String text) {
if (fLogStream != null) {
try {
fLogStream.write(text.getBytes());
if (fQueue.isEmpty()) {
fLogStream.flush();
}
} catch (IOException e) {
CUIPlugin.log(e);
} finally {
ResourcesUtil.refreshWorkspaceFiles(fLogURI);
}
}
}
private void logClose() {
if (fLogStream != null) {
try {
fLogStream.close();
} catch (IOException e) {
CUIPlugin.log(e);
} finally {
ResourcesUtil.refreshWorkspaceFiles(fLogURI);
}
fLogStream = null;
}
}
};
Display display = CUIPlugin.getStandardDisplay();
if (display != null) {
display.asyncExec(r);
}
}
private void addStreamEntryToDocument(StreamEntry entry) throws BadLocationException {
ProblemMarkerInfo marker = entry.getMarker();
if (marker == null) {
// It is plain unmarkered console output
addPartition(new BuildConsolePartition(fLastStream, fDocument.getLength(),
entry.getText().length(), BuildConsolePartition.CONSOLE_PARTITION_TYPE));
} else {
// this text line in entry is markered with ProblemMarkerInfo,
// create special partition for it.
String errorPartitionType;
if (marker.severity == IMarker.SEVERITY_INFO) {
errorPartitionType = BuildConsolePartition.INFO_PARTITION_TYPE;
} else if (marker.severity == IMarker.SEVERITY_WARNING) {
errorPartitionType = BuildConsolePartition.WARNING_PARTITION_TYPE;
} else {
errorPartitionType = BuildConsolePartition.ERROR_PARTITION_TYPE;
}
addPartition(new BuildConsolePartition(fLastStream, fDocument.getLength(),
entry.getText().length(), errorPartitionType, marker));
}
fDocument.replace(fDocument.getLength(), 0, entry.getText());
}
void warnOfContentChange(BuildConsoleStreamDecorator stream) {
if (stream != null) {
ConsolePlugin.getDefault().getConsoleManager().warnOfContentChange(stream.getConsole());
}
fManager.showConsole();
}
public IDocument getDocument() {
return fDocument;
}
public void setDocumentSize(int nLines) {
fMaxLines = nLines;
checkOverflow(false);
}
@Override
public void connect(IDocument document) {
CUIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);
}
@Override
public void disconnect() {
fDocument.setDocumentPartitioner(null);
CUIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this);
killed = true;
}
@Override
public void documentAboutToBeChanged(DocumentEvent event) {
}
@Override
public boolean documentChanged(DocumentEvent event) {
return documentChanged2(event) != null;
}
/**
* @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
*/
@Override
public String[] getLegalContentTypes() {
return new String[] { BuildConsolePartition.CONSOLE_PARTITION_TYPE };
}
/**
* @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
*/
@Override
public String getContentType(int offset) {
ITypedRegion partition = getPartition(offset);
if (partition != null) {
return partition.getType();
}
return null;
}
/**
* @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int,
* int)
*/
@Override
public ITypedRegion[] computePartitioning(int offset, int length) {
if (offset == 0 && length == fDocument.getLength()) {
return fPartitions.toArray(new ITypedRegion[fPartitions.size()]);
}
int end = offset + length;
List<ITypedRegion> list = new ArrayList<ITypedRegion>();
for (int i = 0; i < fPartitions.size(); i++) {
ITypedRegion partition = fPartitions.get(i);
int partitionStart = partition.getOffset();
int partitionEnd = partitionStart + partition.getLength();
if ((offset >= partitionStart && offset <= partitionEnd)
|| (offset < partitionStart && end >= partitionStart)) {
list.add(partition);
}
}
return list.toArray(new ITypedRegion[list.size()]);
}
/**
* @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
*/
@Override
public ITypedRegion getPartition(int offset) {
for (int i = 0; i < fPartitions.size(); i++) {
ITypedRegion partition = fPartitions.get(i);
int start = partition.getOffset();
int end = start + partition.getLength();
if (offset >= start && offset < end) {
return partition;
}
}
return null;
}
@Override
public IRegion documentChanged2(DocumentEvent event) {
String text = event.getText();
if (getDocument().getLength() == 0) {
// cleared
fPartitions.clear();
return new Region(0, 0);
}
ITypedRegion[] affectedRegions = computePartitioning(event.getOffset(), text.length());
if (affectedRegions.length == 0) {
return null;
}
if (affectedRegions.length == 1) {
return affectedRegions[0];
}
int affectedLength = affectedRegions[0].getLength();
for (int i = 1; i < affectedRegions.length; i++) {
ITypedRegion region = affectedRegions[i];
affectedLength += region.getLength();
}
return new Region(affectedRegions[0].getOffset(), affectedLength);
}
/**
* Checks to see if the console buffer has overflowed, and empties the
* overflow if needed, updating partitions and hyperlink positions.
*/
protected void checkOverflow(boolean allowSlack) {
if (fMaxLines <= 0)
return;
int nLines = fDocument.getNumberOfLines();
if (nLines <= (allowSlack ? fMaxLines * 2 : fMaxLines) + 1)
return;
int overflow = 0;
try {
overflow = fDocument.getLineOffset(nLines - fMaxLines);
} catch (BadLocationException e) {
}
// Update partitions
List<ITypedRegion> newParitions = new ArrayList<ITypedRegion>(fPartitions.size());
Iterator<ITypedRegion> partitions = fPartitions.iterator();
while (partitions.hasNext()) {
ITypedRegion region = partitions.next();
if (region instanceof BuildConsolePartition) {
BuildConsolePartition messageConsolePartition = (BuildConsolePartition) region;
ITypedRegion newPartition = null;
int offset = region.getOffset();
String type = messageConsolePartition.getType();
if (offset < overflow) {
int endOffset = offset + region.getLength();
if (endOffset < overflow || BuildConsolePartition.isProblemPartitionType(type)) {
// Remove partition,
// partitions with problem markers can't be split -
// remove them too.
} else {
// Split partition
int length = endOffset - overflow;
newPartition = messageConsolePartition.createNewPartition(0, length, type);
}
} else {
// Modify partition offset
offset = messageConsolePartition.getOffset() - overflow;
newPartition = messageConsolePartition.createNewPartition(offset,
messageConsolePartition.getLength(), type);
}
if (newPartition != null) {
newParitions.add(newPartition);
}
}
}
fPartitions = newParitions;
fDocumentMarkerManager.moveToFirstError();
try {
fDocument.replace(0, overflow, ""); //$NON-NLS-1$
} catch (BadLocationException e) {
}
}
/**
* Adds a new partition, combining with the previous partition if possible.
*/
private BuildConsolePartition addPartition(BuildConsolePartition partition) {
if (fPartitions.isEmpty()) {
fPartitions.add(partition);
} else {
int index = fPartitions.size() - 1;
BuildConsolePartition last = (BuildConsolePartition) fPartitions.get(index);
if (last.canBeCombinedWith(partition)) {
// replace with a single partition
partition = last.combineWith(partition);
fPartitions.set(index, partition);
} else {
// different kinds - add a new parition
fPartitions.add(partition);
}
}
return partition;
}
public IConsole getConsole() {
return this;
}
@Override
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty() == BuildConsolePreferencePage.PREF_BUILDCONSOLE_LINES) {
setDocumentSize(BuildConsolePreferencePage.buildConsoleLines());
}
}
@Override
public void start(final IProject project) {
Display display = CUIPlugin.getStandardDisplay();
if (display != null) {
display.asyncExec(new Runnable() {
@Override
public void run() {
fLogStream = null;
fLogURI = null;
fManager.startConsoleActivity(project);
}
});
}
if (BuildConsolePreferencePage.isClearBuildConsole()) {
appendToDocument("", null, null); //$NON-NLS-1$
}
}
@Override
public ConsoleOutputStream getOutputStream() throws CoreException {
return new BuildOutputStream(this,
fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_OUTPUT));
}
@Override
public ConsoleOutputStream getInfoStream() throws CoreException {
return new BuildOutputStream(this,
fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_INFO));
}
@Override
public ConsoleOutputStream getErrorStream() throws CoreException {
return new BuildOutputStream(this,
fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_ERROR));
}
/** This method is useful for future debugging and bug-fixing */
@SuppressWarnings({ "unused", "nls" })
private void printDocumentPartitioning() {
System.out.println("Document partitioning: ");
for (ITypedRegion tr : fPartitions) {
BuildConsolePartition p = (BuildConsolePartition) tr;
int start = p.getOffset();
int end = p.getOffset() + p.getLength();
String text;
String isError = "U";
String type = p.getType();
if (type == BuildConsolePartition.ERROR_PARTITION_TYPE) {
isError = "E";
} else if (type == BuildConsolePartition.WARNING_PARTITION_TYPE) {
isError = "W";
} else if (type == BuildConsolePartition.INFO_PARTITION_TYPE) {
isError = "I";
} else if (type == BuildConsolePartition.CONSOLE_PARTITION_TYPE) {
isError = "C";
}
try {
text = fDocument.get(p.getOffset(), p.getLength());
} catch (BadLocationException e) {
text = "N/A";
}
if (text.endsWith("\n")) {
text = text.substring(0, text.length() - 1);
}
System.out.println(" " + isError + " " + start + "-" + end + ":[" + text + "]");
}
}
/**
* @return {@link URI} location of log file.
*/
public URI getLogURI() {
return fLogURI;
}
IProject getProject() {
return fProject;
}
}