Bug 551745 - [console] Revise IOConsolePartitioner output appending and
initialization

Remove, rename or rework some fields and simplify how stream output is
appended to document. (without losing existing functionality)

Second try. This time with deadlock protection.

Change-Id: I8d80ade8bb521895c97d6e08d2f520dd9b182a3b
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java
index dd20007..77e6172 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -24,6 +24,7 @@
 import java.util.List;
 
 import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.text.IDocument;
 import org.eclipse.ui.WorkbenchEncoding;
 import org.eclipse.ui.internal.console.IOConsolePage;
 import org.eclipse.ui.internal.console.IOConsolePartitioner;
@@ -42,7 +43,7 @@
 	/**
 	 * The document partitioner
 	 */
-	private IOConsolePartitioner partitioner;
+	private final IOConsolePartitioner partitioner;
 
 	/**
 	 * The stream from which user input may be read
@@ -52,12 +53,12 @@
 	/**
 	 * A collection of open streams connected to this console.
 	 */
-	private List<Closeable> openStreams = Collections.synchronizedList(new ArrayList<>());
+	private final List<Closeable> openStreams = Collections.synchronizedList(new ArrayList<>());
 
 	/**
 	 * The encoding used to for displaying console output.
 	 */
-	private Charset charset;
+	private final Charset charset;
 
 
 	/**
@@ -115,10 +116,11 @@
 			inputStream = new IOConsoleInputStream(this);
 			openStreams.add(inputStream);
 		}
-
-		if (inputStream instanceof IOConsoleInputStream) {
-			partitioner = new IOConsolePartitioner((IOConsoleInputStream) inputStream, this);
-			partitioner.connect(getDocument());
+		partitioner = new IOConsolePartitioner(this);
+		final IDocument document = getDocument();
+		if (document != null) {
+			partitioner.connect(document);
+			document.setDocumentPartitioner(partitioner);
 		}
 	}
 
@@ -174,6 +176,12 @@
 
 	/**
 	 * Returns the input stream connected to the keyboard.
+	 * <p>
+	 * Note: It returns the stream connected to keyboard. There is no guarantee to
+	 * get the stream last set with {@link #setInputStream(InputStream)}. The return
+	 * value might be <code>null</code> if the current input stream is not connected
+	 * to the keyboard.
+	 * </p>
 	 *
 	 * @return the input stream connected to the keyboard.
 	 */
@@ -283,9 +291,7 @@
 
 	@Override
 	public void clearConsole() {
-		if (partitioner != null) {
-			partitioner.clearBuffer();
-		}
+		partitioner.clearBuffer();
 	}
 
 	/**
@@ -293,28 +299,38 @@
 	 */
 	@Override
 	protected void dispose() {
-		super.dispose();
-		partitioner.disconnect();
-		//make a copy of the open streams and close them all
-		//a copy is needed as close the streams results in a callback that
-		//removes the streams from the openStreams collection (bug 152794)
-		List<Closeable> list = new ArrayList<>(openStreams);
-		for (Closeable closable : list) {
-			try {
-				closable.close();
-			} catch (IOException e) {
-				// e.printStackTrace();
+		// Get lock on ourself before closing. Closing streams check for console finish.
+		// Finish check need lock on (this) console. Since closing also lock on stream
+		// this can deadlock with other threads (double) closing streams at same time
+		// but already own a lock on console. (bug 551902)
+		// Long story short. Before closing IOConsole...Stream get lock from associated
+		// console to prevent deadlocks.
+		synchronized (this) {
+			// make a copy of the open streams and close them all
+			// a copy is needed as close the streams results in a callback that
+			// removes the streams from the openStreams collection (bug 152794)
+			List<Closeable> list = new ArrayList<>(openStreams);
+			for (Closeable closable : list) {
+				try {
+					closable.close();
+				} catch (IOException e) {
+					// e.printStackTrace();
+				}
 			}
+			inputStream = null;
 		}
-		inputStream = null;
+
+		final IDocument document = partitioner.getDocument();
+		document.setDocumentPartitioner(null);
+		partitioner.disconnect();
+
+		super.dispose();
 	}
 
 	/**
-	 * Returns the encoding for this console, or <code>null</code> to indicate
-	 * default encoding.
+	 * Returns the encoding for this console.
 	 *
-	 * @return the encoding set for this console, or <code>null</code> to indicate
-	 * 	default encoding
+	 * @return the encoding set for this console
 	 * @since 3.3
 	 */
 	public String getEncoding() {
@@ -322,11 +338,9 @@
 	}
 
 	/**
-	 * Returns the Charset for this console, or <code>null</code> to indicate
-	 * default encoding.
+	 * Returns the Charset for this console.
 	 *
-	 * @return the Charset for this console, or <code>null</code> to indicate
-	 *         default encoding
+	 * @return the Charset for this console
 	 * @since 3.7
 	 */
 	public Charset getCharset() {
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartition.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartition.java
index 3f43137..34ffa88 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartition.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartition.java
@@ -160,7 +160,7 @@
 	 */
 	private int getFontStyle() {
 		if (type.equals(INPUT_PARTITION_TYPE)) {
-			return inputStream.getFontStyle();
+			return inputStream != null ? inputStream.getFontStyle() : 0;
 		}
 		return outputStream.getFontStyle();
 	}
@@ -174,7 +174,7 @@
 	 */
 	public Color getColor() {
 		if (type.equals(INPUT_PARTITION_TYPE)) {
-			return inputStream.getColor();
+			return inputStream != null ? inputStream.getColor() : null;
 		}
 		return outputStream.getColor();
 	}
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
index 79dd581..477e7a1 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
@@ -24,6 +24,7 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 
 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -59,6 +60,25 @@
 public class IOConsolePartitioner
 		implements IConsoleDocumentPartitioner, IConsoleDocumentPartitionerExtension, IDocumentPartitionerExtension {
 	/**
+	 * Enumeration used to distinct sources of document updates. (especially to
+	 * distinct updates triggered by this partitioner from other document changes)
+	 */
+	private enum DocUpdateType {
+		/**
+		 * Default if reason for document change is not known. Document change is
+		 * interpreted as user input.
+		 */
+		INPUT,
+		/**
+		 * Document update was triggered from this partitioner by appending content
+		 * received from output streams.
+		 */
+		OUTPUT,
+		/** Document update was triggered from this partitioner's {@link TrimJob}. */
+		TRIM,
+	}
+
+	/**
 	 * If true validate partitioning after changes and do other additional
 	 * assertions. Useful for developing/debugging.
 	 */
@@ -69,7 +89,6 @@
 	 */
 	private static final Comparator<IRegion> CMP_REGION_BY_OFFSET = Comparator.comparing(IRegion::getOffset);
 
-	private PendingPartition consoleClosedPartition;
 	/** The connected {@link IDocument} this partitioner manages. */
 	private IDocument document;
 	/**
@@ -77,59 +96,60 @@
 	 * {@link IRegion#getOffset()} and not contain <code>null</code> or 0-length
 	 * elements. (see also {@link #checkPartitions()})
 	 */
-	private ArrayList<IOConsolePartition> partitions;
+	private final ArrayList<IOConsolePartition> partitions = new ArrayList<>();
 	/** Blocks of data that have not yet been appended to the document. */
-	private ArrayList<PendingPartition> pendingPartitions;
-	/**
-	 * A list of PendingPartitions to be appended by the updateJob
-	 */
-	private ArrayList<PendingPartition> updatePartitions;
-	/**
-	 * Job that appends pending partitions to the document.
-	 */
-	private QueueProcessingJob queueJob;
+	private final ArrayList<PendingPartition> pendingPartitions = new ArrayList<>();
+	/** Total length of pending partitions content. */
+	private int pendingSize;
+	/** Job that appends pending partitions to the document. */
+	private final QueueProcessingJob queueJob = new QueueProcessingJob();
 	/** Job that trims console content if it exceeds {@link #highWaterMark}. */
-	private TrimJob trimJob = new TrimJob();
-	/** The input stream attached to this document. */
-	private IOConsoleInputStream inputStream;
+	private final TrimJob trimJob = new TrimJob();
 	/**
-	 * Flag to indicate that the updateJob is updating the document.
+	 * Reason for document update. Set before changing document inside this
+	 * partitioner to prevent that change is interpreted as user input.
+	 * <p>
+	 * Automatically reset to {@link DocUpdateType#INPUT} after every document
+	 * change.
+	 * </p>
 	 */
-	private boolean updateInProgress;
+	private DocUpdateType updateType = DocUpdateType.INPUT;
+	private IRegion changedRegion;
 	/**
 	 * A list of partitions containing input from the console, that have not been
 	 * appended to the input stream yet. No guarantees on element order.
 	 */
 	private ArrayList<IOConsolePartition> inputPartitions;
 	/**
-	 * offset used by updateJob
-	 */
-	private int firstOffset;
-	/**
 	 * A matcher to search for legal line delimiters in new input. Never
 	 * <code>null</code> but match nothing if no document connected.
 	 */
 	private MultiStringMatcher legalLineDelimiterMatcher;
+	/**
+	 * The high mark for console content trimming. If console content exceeds this
+	 * length trimming is scheduled. Trimming is disabled if value is negative.
+	 */
 	private int highWaterMark = -1;
 	private int lowWaterMark = -1;
-	private boolean connected = false;
 
 	/** The partitioned {@link IOConsole}. */
 	private IOConsole console;
 
+	/** Set after console signaled that all streams are closed. */
+	private volatile boolean streamsClosed;
+
 	/**
-	 * Lock for appending to and removing from the document - used
-	 * to synchronize addition of new text/partitions in the update
-	 * job and handling buffer overflow/clearing of the console.
+	 * Create new partitioner for an {@link IOConsole}.
+	 * <p>
+	 * The partitioner must be explicit {@link #connect(IDocument) connected} with
+	 * the consoles {@link IDocument}.
+	 * </p>
+	 *
+	 * @param console the partitioned console. Not <code>null</code>.
 	 */
-	private Object overflowLock = new Object();
-
-
-	private int fBuffer;
-
-	public IOConsolePartitioner(IOConsoleInputStream inputStream, IOConsole console) {
-		this.inputStream = inputStream;
-		this.console = console;
+	public IOConsolePartitioner(IOConsole console) {
+		this.console = Objects.requireNonNull(console);
+		queueJob.setRule(console.getSchedulingRule());
 		trimJob.setRule(console.getSchedulingRule());
 	}
 
@@ -144,27 +164,63 @@
 
 	@Override
 	public void connect(IDocument doc) {
-		document = doc;
-		document.setDocumentPartitioner(this);
-		legalLineDelimiterMatcher = MultiStringMatcher.create(document.getLegalLineDelimiters());
-		partitions = new ArrayList<>();
-		pendingPartitions = new ArrayList<>();
-		inputPartitions = new ArrayList<>();
-		queueJob = new QueueProcessingJob();
-		queueJob.setSystem(true);
-		queueJob.setPriority(Job.INTERACTIVE);
-		queueJob.setRule(console.getSchedulingRule());
-		connected = true;
+		if (doc == document) {
+			return;
+		}
+		disconnect();
+		if (doc != null) {
+			synchronized (partitions) {
+				inputPartitions = new ArrayList<>();
+				document = doc;
+				legalLineDelimiterMatcher = MultiStringMatcher.create(document.getLegalLineDelimiters());
+			}
+		}
 	}
 
+	@Override
+	public void disconnect() {
+		synchronized (pendingPartitions) {
+			pendingPartitions.clear();
+			pendingSize = 0;
+			pendingPartitions.notifyAll();
+		}
+		synchronized (partitions) {
+			trimJob.cancel();
+			queueJob.cancel();
+			legalLineDelimiterMatcher = null;
+			document = null;
+			inputPartitions = null;
+			partitions.clear();
+		}
+	}
+
+	/**
+	 * Get high water mark.
+	 *
+	 * @return the trim if exceeded mark
+	 * @see IOConsole#getHighWaterMark()
+	 */
 	public int getHighWaterMark() {
 		return highWaterMark;
 	}
 
+	/**
+	 * Get low water mark.
+	 *
+	 * @return the trim to this length mark
+	 * @see IOConsole#getLowWaterMark()
+	 */
 	public int getLowWaterMark() {
 		return lowWaterMark;
 	}
 
+	/**
+	 * Set low and high water marks.
+	 *
+	 * @param low  the trim to this length mark
+	 * @param high the trim if exceeded mark
+	 * @see IOConsole#setWaterMarks(int, int)
+	 */
 	public void setWaterMarks(int low, int high) {
 		lowWaterMark = low;
 		highWaterMark = high;
@@ -175,22 +231,30 @@
 	 * Notification from the console that all of its streams have been closed.
 	 */
 	public void streamsClosed() {
-		consoleClosedPartition = new PendingPartition(null, null);
-		synchronized (pendingPartitions) {
-			pendingPartitions.add(consoleClosedPartition);
+		if (streamsClosed) {
+			log(IStatus.ERROR, "Streams are already closed."); //$NON-NLS-1$
+			return;
 		}
-		queueJob.schedule(); //ensure that all pending partitions are processed.
+		streamsClosed = true;
+		checkFinished();
 	}
 
-	@Override
-	public void disconnect() {
-		synchronized (overflowLock) {
-			document = null;
-			partitions.clear();
-			connected = false;
-			try {
-				inputStream.close();
-			} catch (IOException e) {
+	/**
+	 * Check if partitioner is finished and does not expect any new data appended to
+	 * document.
+	 */
+	private void checkFinished() {
+		if (streamsClosed) {
+			// do not expect new data since all streams are closed
+			// check if pending data is queued
+			final boolean morePending;
+			synchronized (pendingPartitions) {
+				morePending = !pendingPartitions.isEmpty();
+			}
+			if (morePending) {
+				queueJob.schedule();
+			} else {
+				console.partitionerFinished();
 			}
 		}
 	}
@@ -320,15 +384,17 @@
 
 	/**
 	 * Enforces the buffer size.
+	 * <p>
 	 * When the number of lines in the document exceeds the high water mark, the
-	 * beginning of the document is trimmed until the number of lines equals the
-	 * low water mark.
+	 * beginning of the document is trimmed until the number of lines equals the low
+	 * water mark.
+	 * </p>
 	 */
 	private void checkBufferSize() {
 		if (document != null && highWaterMark > 0) {
 			int length = document.getLength();
 			if (length > highWaterMark) {
-				if (trimJob.getState() == Job.NONE) { //if the job isn't already running
+				if (trimJob.getState() == Job.NONE) { // if the job isn't already running
 					trimJob.setOffset(length - lowWaterMark);
 					trimJob.schedule();
 				}
@@ -337,57 +403,50 @@
 	}
 
 	/**
-	 * Clears the console
+	 * Clears the console content.
 	 */
 	public void clearBuffer() {
-		synchronized (overflowLock) {
-			trimJob.setOffset(-1);
-			trimJob.schedule();
-		}
+		trimJob.setOffset(-1);
+		trimJob.schedule();
 	}
 
 	@Override
 	public IRegion documentChanged2(DocumentEvent event) {
-		if (document == null) {
-			return null; //another thread disconnected the partitioner
-		}
-		if (document.getLength() == 0) { // document cleared
-			synchronized (partitions) {
-				partitions.clear();
-				inputPartitions.clear();
+		try {
+			if (document != event.getDocument()) {
+				log(IStatus.WARNING, "IOConsolePartitioner is connected to wrong document."); //$NON-NLS-1$
+				return null;
 			}
-			return new Region(0, 0);
-		}
+			if (document.getLength() == 0) { // document cleared
+				synchronized (partitions) {
+					partitions.clear();
+					inputPartitions.clear();
+				}
+				return new Region(0, 0);
+			}
 
-		if (updateInProgress) {
-			synchronized(partitions) {
-				if (updatePartitions != null) {
-					IOConsolePartition lastPartition = getPartitionByIndex(partitions.size() - 1);
-					for (PendingPartition pp : updatePartitions) {
-						if (pp == consoleClosedPartition) {
-							continue;
-						}
+			synchronized (partitions) {
+				switch (updateType) {
+				case INPUT:
+					return applyUserInput(event);
 
-						int ppLen = pp.text.length();
-						if (lastPartition != null && lastPartition.getOutputStream() == pp.stream) {
-							int len = lastPartition.getLength();
-							lastPartition.setLength(len + ppLen);
-						} else {
-							IOConsolePartition partition = new IOConsolePartition(firstOffset, pp.stream);
-							partition.setLength(ppLen);
-							lastPartition = partition;
-							partitions.add(partition);
-						}
-						firstOffset += ppLen;
-					}
+				// update and trim jobs are triggered by this partitioner and all partitioning
+				// changes are applied separately
+				case OUTPUT:
+					return changedRegion;
+				case TRIM:
+					return null; // trim does not change partition types
+
+				default:
+					log(IStatus.ERROR, "Invalid enum value " + updateType); //$NON-NLS-1$
+					return null;
 				}
 			}
-		} else {
-			synchronized (partitions) {
-				return applyUserInput(event);
-			}
+		} finally {
+			// always reset type since all change events not triggered by this partitioner
+			// are interpreted as user input
+			updateType = DocUpdateType.INPUT;
 		}
-		return new Region(event.fOffset, event.fText.length());
 	}
 
 	/**
@@ -404,10 +463,14 @@
 	 * @return the region of the document in which the partition type changed or
 	 *         <code>null</code>
 	 */
+	// Required for a false 'resource not closed' warning on inputStream.
+	// This input stream must not be closed by this method.
+	@SuppressWarnings("resource")
 	private IRegion applyUserInput(DocumentEvent event) {
 		final int eventTextLength = event.getText() != null ? event.getText().length() : 0;
 		final int offset = event.getOffset();
 		final int amountDeleted = event.getLength();
+		final IOConsoleInputStream inputStream = console.getInputStream(); // do not close this stream
 
 		if (amountDeleted == 0 && eventTextLength == 0) {
 			// event did not changed document
@@ -484,7 +547,9 @@
 					if (ASSERT) {
 						Assert.isTrue(inputLine.length() > 0);
 					}
-					inputStream.appendData(inputLine.toString());
+					if (inputStream != null) {
+						inputStream.appendData(inputLine.toString());
+					}
 				}
 				Assert.isTrue(newTextOffset > textOffset); // can prevent infinity loop
 				textOffset = newTextOffset;
@@ -558,10 +623,6 @@
 		return newPartition;
 	}
 
-	private void setUpdateInProgress(boolean b) {
-		updateInProgress = b;
-	}
-
 	/**
 	 * A stream has been appended, add to pendingPartions list and schedule
 	 * updateJob. updateJob is scheduled with a slight delay, this allows the
@@ -576,31 +637,34 @@
 		if (document == null) {
 			throw new IOException("Document is closed"); //$NON-NLS-1$
 		}
-		synchronized(pendingPartitions) {
-			PendingPartition last = pendingPartitions.size() > 0 ? pendingPartitions.get(pendingPartitions.size()-1) : null;
-			if (last != null && last.stream == stream) {
-				last.append(s);
+		if (s == null) {
+			return;
+		}
+		synchronized (pendingPartitions) {
+			final PendingPartition lastPending = pendingPartitions.size() > 0
+					? pendingPartitions.get(pendingPartitions.size() - 1)
+					: null;
+			if (lastPending != null && lastPending.stream == stream) {
+				lastPending.append(s);
 			} else {
 				pendingPartitions.add(new PendingPartition(stream, s));
-				if (fBuffer > 1000) {
-					queueJob.schedule();
-				} else {
-					queueJob.schedule(50);
-				}
 			}
 
-			if (fBuffer > 160000) {
-				if(Display.getCurrent() == null){
+			if (pendingSize > 1000) {
+				queueJob.schedule();
+			} else {
+				queueJob.schedule(50);
+			}
+
+			if (pendingSize > 160000) {
+				if (Display.getCurrent() == null) {
 					try {
 						pendingPartitions.wait();
 					} catch (InterruptedException e) {
 					}
 				} else {
-					/*
-					 * if we are in UI thread we cannot lock it, so process
-					 * queued output.
-					 */
-					processQueue();
+					// if we are in UI thread we cannot lock it, so process queued output.
+					processPendingPartitions();
 				}
 			}
 		}
@@ -615,30 +679,30 @@
 
 		PendingPartition(IOConsoleOutputStream stream, String text) {
 			this.stream = stream;
-			if (text != null) {
-				append(text);
-			}
+			append(text);
 		}
 
 		void append(String moreText) {
 			text.append(moreText);
-			fBuffer += moreText.length();
+			pendingSize += moreText.length();
 		}
 	}
 
 	/**
-	 * Updates the document. Will append everything that is available before
-	 * finishing.
+	 * Updates the document and partitioning structure. Will append everything
+	 * received from output streams that is available before finishing.
 	 */
 	private class QueueProcessingJob extends UIJob {
 
 		QueueProcessingJob() {
 			super("IOConsole Updater"); //$NON-NLS-1$
+			setSystem(true);
+			setPriority(Job.INTERACTIVE);
 		}
 
 		@Override
 		public IStatus runInUIThread(IProgressMonitor monitor) {
-			processQueue();
+			processPendingPartitions();
 			if (ASSERT) {
 				checkPartitions();
 			}
@@ -646,74 +710,67 @@
 		}
 
 		/*
-		 * Job will process as much as it can each time it's run, but it gets
-		 * scheduled everytime a PendingPartition is added to the list, meaning
-		 * that this job could get scheduled unnecessarily in cases of heavy output.
-		 * Note however, that schedule() will only reschedule a running/scheduled Job
-		 * once even if it's called many times.
+		 * Job will process as much as it can each time it's run, but it gets scheduled
+		 * everytime a PendingPartition is added to the list, meaning that this job
+		 * could get scheduled unnecessarily in cases of heavy output. Note however,
+		 * that schedule() will only reschedule a running/scheduled Job once even if
+		 * it's called many times.
 		 */
 		@Override
 		public boolean shouldRun() {
-			boolean shouldRun = connected && pendingPartitions != null && pendingPartitions.size() > 0;
-			return shouldRun;
+			synchronized (pendingPartitions) {
+				final boolean shouldRun = pendingPartitions.size() > 0;
+				return shouldRun;
+			}
 		}
 	}
 
-	void processQueue() {
-		synchronized (overflowLock) {
-			ArrayList<PendingPartition> pendingCopy = new ArrayList<>();
-			StringBuilder buffer = null;
-			boolean consoleClosed = false;
-			synchronized(pendingPartitions) {
-				pendingCopy.addAll(pendingPartitions);
-				pendingPartitions.clear();
-				fBuffer = 0;
-				pendingPartitions.notifyAll();
-			}
-			// determine buffer size
-			int size = 0;
-			for (PendingPartition pp : pendingCopy) {
-				if (pp != consoleClosedPartition) {
-					size+= pp.text.length();
-				}
-			}
-			buffer = new StringBuilder(size);
-			for (PendingPartition pp : pendingCopy) {
-				if (pp != consoleClosedPartition) {
-					buffer.append(pp.text);
-				} else {
-					consoleClosed = true;
-				}
-			}
-			if (connected) {
-				setUpdateInProgress(true);
-				updatePartitions = pendingCopy;
-				firstOffset = document.getLength();
-				try {
-					if (buffer != null) {
-						document.replace(firstOffset, 0, buffer.toString());
-					}
-				} catch (BadLocationException e) {
-				}
-				updatePartitions = null;
-				setUpdateInProgress(false);
-			}
-			if (consoleClosed) {
-				console.partitionerFinished();
-			}
-			checkBufferSize();
-		}
-
-	}
-
 	/**
-	 * Job to trim the console document, runs in the  UI thread.
+	 * Process {@link #pendingPartitions}, append their content to document and
+	 * update partitioning.
+	 */
+	private void processPendingPartitions() {
+		final List<PendingPartition> pendingCopy;
+		final int size;
+		synchronized (pendingPartitions) {
+			pendingCopy = new ArrayList<>(pendingPartitions);
+			size = pendingSize;
+			pendingPartitions.clear();
+			pendingSize = 0;
+			pendingPartitions.notifyAll();
+		}
+		synchronized (partitions) {
+			final StringBuilder addedContent = new StringBuilder(size);
+			IOConsolePartition lastPartition = getPartitionByIndex(partitions.size() - 1);
+			int nextOffset = document.getLength();
+			for (PendingPartition pendingPartition : pendingCopy) {
+				if (lastPartition == null || lastPartition.getOutputStream() != pendingPartition.stream) {
+					lastPartition = new IOConsolePartition(nextOffset, pendingPartition.stream);
+					partitions.add(lastPartition);
+				}
+				final int pendingLength = pendingPartition.text.length();
+				lastPartition.setLength(lastPartition.getLength() + pendingLength);
+				nextOffset += pendingLength;
+				addedContent.append(pendingPartition.text);
+			}
+			try {
+				updateType = DocUpdateType.OUTPUT;
+				document.replace(document.getLength(), 0, addedContent.toString());
+			} catch (BadLocationException e) {
+				log(e);
+			}
+		}
+		checkBufferSize();
+		checkFinished();
+	}
+
+	/**
+	 * Job to trim the console document, runs in the UI thread.
 	 */
 	private class TrimJob extends WorkbenchJob {
 
 		/**
-		 * trims output up to the line containing the given offset,
-		 * or all output if -1.
+		 * Trims output up to the line containing the given offset, or all output if -1.
 		 */
 		private int truncateOffset;
 
@@ -736,34 +793,31 @@
 
 		@Override
 		public IStatus runInUIThread(IProgressMonitor monitor) {
-			if (document == null) {
-				return Status.OK_STATUS;
-			}
+			synchronized (partitions) {
+				if (document == null) {
+					return Status.OK_STATUS;
+				}
 
-			int length = document.getLength();
-			if (truncateOffset < length) {
-				synchronized (overflowLock) {
+				int length = document.getLength();
+				if (truncateOffset < length) {
 					try {
 						if (truncateOffset < 0) {
 							// clear
-							setUpdateInProgress(true);
+							updateType = DocUpdateType.TRIM;
 							document.set(""); //$NON-NLS-1$
-							setUpdateInProgress(false);
 						} else {
 							// overflow
 							int cutoffLine = document.getLineOfOffset(truncateOffset);
 							int cutOffset = document.getLineOffset(cutoffLine);
 
-
 							// set the new length of the first partition
 							IOConsolePartition partition = (IOConsolePartition) getPartition(cutOffset);
 							partition.setLength(partition.getOffset() + partition.getLength() - cutOffset);
 
-							setUpdateInProgress(true);
+							updateType = DocUpdateType.TRIM;
 							document.replace(0, cutOffset, ""); //$NON-NLS-1$
-							setUpdateInProgress(false);
 
-							//remove partitions and reset Partition offsets
+							// remove partitions and reset Partition offsets
 							int index = partitions.indexOf(partition);
 							for (int i = 0; i < index; i++) {
 								partitions.remove(0);
@@ -794,9 +848,6 @@
 
 	@Override
 	public StyleRange[] getStyleRanges(int offset, int length) {
-		if (!connected) {
-			return new StyleRange[0];
-		}
 		final IOConsolePartition[] computedPartitions = computeIOPartitioning(offset, length);
 		final StyleRange[] styles = new StyleRange[computedPartitions.length];
 		for (int i = 0; i < computedPartitions.length; i++) {
@@ -869,7 +920,7 @@
 
 	@Override
 	public int getPreviousOffsetByState(int offset, boolean searchWritable) {
-		if (partitions != null) {
+		synchronized (partitions) {
 			int partitionIndex = findPartitionCandidate(offset);
 			for (; partitionIndex >= 0; partitionIndex--) {
 				final IOConsolePartition partition = partitions.get(partitionIndex);
@@ -883,7 +934,7 @@
 
 	@Override
 	public int getNextOffsetByState(int offset, boolean searchWritable) {
-		if (partitions != null) {
+		synchronized (partitions) {
 			int partitionIndex = findPartitionCandidate(offset);
 			if (partitionIndex >= 0) {
 				for (; partitionIndex < partitions.size(); partitionIndex++) {
@@ -921,27 +972,33 @@
 		ConsolePlugin.log(t);
 	}
 
+	private static void log(int status, String msg) {
+		ConsolePlugin.log(new Status(status, ConsolePlugin.getUniqueIdentifier(), msg));
+	}
+
 	/**
 	 * For debug purpose. Check if whole document is partitioned, partitions are
 	 * ordered by offset, every partition has length greater 0 and all writable
 	 * input partitions are listed in {@link #inputPartitions}.
 	 */
 	private void checkPartitions() {
-		if (!connected) {
+		if (document == null) {
 			return;
 		}
-		final List<IOConsolePartition> knownInputPartitions = new ArrayList<>(inputPartitions);
-		int offset = 0;
-		for (IOConsolePartition partition : partitions) {
-			Assert.isTrue(offset == partition.getOffset());
-			Assert.isTrue(partition.getLength() > 0);
-			offset += partition.getLength();
+		synchronized (partitions) {
+			final List<IOConsolePartition> knownInputPartitions = new ArrayList<>(inputPartitions);
+			int offset = 0;
+			for (IOConsolePartition partition : partitions) {
+				Assert.isTrue(offset == partition.getOffset());
+				Assert.isTrue(partition.getLength() > 0);
+				offset += partition.getLength();
 
-			if (isInputPartition(partition) && !partition.isReadOnly()) {
-				Assert.isTrue(knownInputPartitions.remove(partition));
+				if (isInputPartition(partition) && !partition.isReadOnly()) {
+					Assert.isTrue(knownInputPartitions.remove(partition));
+				}
 			}
+			Assert.isTrue(offset == document.getLength());
+			Assert.isTrue(knownInputPartitions.isEmpty());
 		}
-		Assert.isTrue(offset == document.getLength());
-		Assert.isTrue(knownInputPartitions.isEmpty());
 	}
 }