package com.intellij.javascript.karma.server;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.javascript.karma.util.ArchivedOutputListener;
import com.intellij.javascript.karma.util.StreamEventListener;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class KarmaProcessOutputManager {
private static final int MAX_ARCHIVED_TEXTS_LENGTH = 1024 * 16;
private static final char NEW_LINE = '\n';
private static final String PREFIX = "##intellij-event[";
private static final String SUFFIX = "]\n";
private final ProcessHandler myProcessHandler;
private final Deque<Pair<String, Key>> myArchivedTexts = new ArrayDeque<>();
private int myArchivedTextsLength = 0;
private boolean myArchiveTextsTruncated = false;
private final List<ArchivedOutputListener> myOutputListeners = new CopyOnWriteArrayList<>();
private final List<StreamEventListener> myStdOutStreamEventListeners = new CopyOnWriteArrayList<>();
private final List<Pair<String, Key>> myStdOutCurrentLineChunks = ContainerUtil.newArrayList();
private final Consumer<String> myStdOutLineConsumer;
public KarmaProcessOutputManager(@NotNull ProcessHandler processHandler, @NotNull Consumer<String> stdOutLineConsumer) {
myProcessHandler = processHandler;
myStdOutLineConsumer = stdOutLineConsumer;
}
public void startNotify() {
myProcessHandler.addProcessListener(new ProcessAdapter() {
@Override
public void onTextAvailable(ProcessEvent event, Key outputType) {
String text = event.getText();
if (outputType != ProcessOutputTypes.SYSTEM && outputType != ProcessOutputTypes.STDERR) {
processStandardOutput(text, outputType);
}
else {
addText(text, outputType);
}
}
});
myProcessHandler.startNotify();
}
private void processStandardOutput(@NotNull String text, @NotNull Key type) {
int lineStartInd = 0;
int newLineInd = text.indexOf(NEW_LINE, lineStartInd);
while (newLineInd != -1) {
String line = text.substring(lineStartInd, newLineInd + 1); // always not empty
if (!myStdOutCurrentLineChunks.isEmpty()) {
myStdOutCurrentLineChunks.add(Pair.create(line, type));
line = concatCurrentLineChunks();
}
if (!handleLineAsEvent(line)) {
onStandardOutputLineAvailable(line);
if (!myStdOutCurrentLineChunks.isEmpty()) {
for (Pair<String, Key> chunk : myStdOutCurrentLineChunks) {
addText(chunk.getFirst(), chunk.getSecond());
}
}
else {
addText(line, type);
}
}
myStdOutCurrentLineChunks.clear();
lineStartInd = newLineInd + 1;
newLineInd = text.indexOf(NEW_LINE, lineStartInd);
}
if (lineStartInd < text.length()) {
myStdOutCurrentLineChunks.add(Pair.create(text.substring(lineStartInd), type));
}
}
@NotNull
private String concatCurrentLineChunks() {
int size = 0;
for (Pair<String, Key> chunk : myStdOutCurrentLineChunks) {
size += chunk.getFirst().length();
}
StringBuilder result = new StringBuilder(size);
for (Pair<String, Key> chunk : myStdOutCurrentLineChunks) {
result.append(chunk.getFirst());
}
return result.toString();
}
private void addText(@NotNull String text, @NotNull Key outputType) {
synchronized (myArchivedTexts) {
myArchivedTexts.addLast(Pair.create(text, outputType));
myArchivedTextsLength += text.length();
while (myArchivedTextsLength > MAX_ARCHIVED_TEXTS_LENGTH) {
Pair<String, Key> pair = myArchivedTexts.removeFirst();
myArchivedTextsLength -= pair.getFirst().length();
myArchiveTextsTruncated = true;
}
for (ArchivedOutputListener listener : myOutputListeners) {
listener.onOutputAvailable(text, outputType, false);
}
}
}
private void onStandardOutputLineAvailable(@NotNull String line) {
myStdOutLineConsumer.consume(line);
}
private boolean handleLineAsEvent(@NotNull String line) {
if (line.startsWith(PREFIX) && line.endsWith(SUFFIX)) {
int colonInd = line.indexOf(':');
if (colonInd == -1) {
return false;
}
String eventType = line.substring(PREFIX.length(), colonInd);
String eventBody = line.substring(colonInd + 1, line.length() - SUFFIX.length());
for (StreamEventListener listener : myStdOutStreamEventListeners) {
listener.on(eventType, eventBody);
}
return true;
}
return false;
}
@NotNull
public ProcessHandler getProcessHandler() {
return myProcessHandler;
}
public void addOutputListener(@NotNull final ArchivedOutputListener outputListener) {
ApplicationManager.getApplication().executeOnPooledThread(() -> {
synchronized (myArchivedTexts) {
if (myArchiveTextsTruncated) {
outputListener.onOutputAvailable("... too much output to process, truncated\n", ProcessOutputTypes.SYSTEM, true);
}
for (Pair<String, Key> text : myArchivedTexts) {
outputListener.onOutputAvailable(text.getFirst(), text.getSecond(), true);
}
myOutputListeners.add(outputListener);
}
});
}
public void removeOutputListener(@NotNull ArchivedOutputListener outputListener) {
myOutputListeners.remove(outputListener);
}
void addStreamEventListener(@NotNull StreamEventListener listener) {
myStdOutStreamEventListeners.add(listener);
}
}