Skip to content

Commit

Permalink
CHE-4144: Recognize when nodejs is froze and stop process (#4263)
Browse files Browse the repository at this point in the history
CHE-4144: Recognize when nodejs is froze and stop process
  • Loading branch information
Anatoliy Bazko committed Mar 1, 2017
1 parent 4325d38 commit c76d7bd
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 161 deletions.
Expand Up @@ -12,18 +12,16 @@

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.lang.IoUtil;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.plugin.nodejsdbg.server.exception.NodeJsDebuggerException;
import org.eclipse.che.plugin.nodejsdbg.server.exception.NodeJsDebuggerTerminatedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -32,190 +30,155 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static java.lang.Math.min;

/**
* Wrapper over NodeJs process is being run.
* Communication is performed through standard input/output streams.
*
* @author Anatoliy Bazko
*/
public class NodeJsDebugProcess implements NodeJsProcessObservable {
private static final Logger LOG = LoggerFactory.getLogger(NodeJsDebugProcess.class);
private static final int MAX_OUTPUT = 4096;

private static final Logger LOG = LoggerFactory.getLogger(NodeJsDebugProcess.class);
private static final String NODEJS_COMMAND = detectNodeJsCommand();

private final Process process;
private final String outputSeparator;
private final ScheduledExecutorService executor;
private final BufferedWriter processWriter;

private final Process process;
private final String outputSeparator;
private final ScheduledExecutorService executor;
private final BufferedWriter processWriter;
private final List<NodeJsProcessObserver> observers;

private NodeJsDebugProcess(String outputSeparator, String... options) throws NodeJsDebuggerException {
this.observers = new CopyOnWriteArrayList<>();
this.outputSeparator = outputSeparator;

List<String> commands = new ArrayList<>(1 + options.length);
commands.add(NODEJS_COMMAND);
commands.addAll(Arrays.asList(options));

ProcessBuilder processBuilder = new ProcessBuilder(commands);
try {
process = processBuilder.start();
} catch (IOException e) {
throw new NodeJsDebuggerException("NodeJs process failed.", e);
}

process = initializeNodeJsDebugProcess(options);
processWriter = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));

executor = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder().setNameFormat("nodejs-debugger-%d")
.setUncaughtExceptionHandler(
LoggingUncaughtExceptionHandler.getInstance())
.setDaemon(true)
.build());
executor.scheduleWithFixedDelay(new OutputReader(), 0, 100, TimeUnit.MILLISECONDS);
executor = Executors
.newScheduledThreadPool(1, new ThreadFactoryBuilder()
.setNameFormat("nodejs-debugger-%d")
.setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance())
.setDaemon(true)
.build());

OutputReader outputReader = new OutputReader(process, outputSeparator, this::notifyObservers);
executor.scheduleWithFixedDelay(outputReader, 0, 100, TimeUnit.MILLISECONDS);
}

public static NodeJsDebugProcess start(String file) throws NodeJsDebuggerException {
return new NodeJsDebugProcess("debug> ", "debug", "--debug-brk", file);
}

@Override
public void addObserver(NodeJsProcessObserver observer) {
observers.add(observer);
}
private Process initializeNodeJsDebugProcess(String[] options) throws NodeJsDebuggerException {
List<String> commands = new ArrayList<>(1 + options.length);
commands.add(NODEJS_COMMAND);
commands.addAll(Arrays.asList(options));

@Override
public void removeObserver(NodeJsProcessObserver observer) {
observers.remove(observer);
ProcessBuilder processBuilder = new ProcessBuilder(commands);
try {
return processBuilder.start();
} catch (IOException e) {
throw new NodeJsDebuggerException("NodeJs process initialization failed.", e);
}
}


/**
* Stops process.
* Stops nodejs process.
*/
void stop() {
public void stop() {
boolean interrupted = false;
observers.clear();

try {
send("quit");
} catch (NodeJsDebuggerException e) {
LOG.warn(e.getMessage());
LOG.warn("Failed to execute 'quit' command. " + e.getMessage());
}

try {
processWriter.close();
} catch (IOException ignored) {
// ignore
}

executor.shutdown();
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
LOG.warn("Unable to terminate main pool");
LOG.error("Unable to terminate pool of NodeJs debugger tasks.");
}
}
} catch (InterruptedException e) {
LOG.warn(e.getMessage());
interrupted = true;
if (!executor.isShutdown()) {
LOG.error("Unable to terminate pool of NodeJs debugger tasks.");
}
}


process.destroy();
try {
if (!process.waitFor(10, TimeUnit.SECONDS)) {
LOG.error("Unable to terminate NodeJs");
LOG.error("Unable to terminate NodeJs process.");
}
} catch (InterruptedException e) {
LOG.warn(e.getMessage());
interrupted = true;
if (!process.isAlive()) {
LOG.error("Unable to terminate NodeJs process.");
}
}

try {
processWriter.close();
} catch (IOException e) {
LOG.warn("Failed to close NodeJs process output stream", e);
if (interrupted) {
Thread.currentThread().interrupt();
}
observers.clear();
}

/**
* Synchronizes sending commands.
*/
public synchronized void send(String command) throws NodeJsDebuggerException {
LOG.debug("Execute: {}", command);

if (!process.isAlive()) {
throw new NodeJsDebuggerTerminatedException("NodeJs process is terminated.");
throw new NodeJsDebuggerTerminatedException("NodeJs process has been terminated.");
}

try {
processWriter.write(command);
processWriter.newLine();
processWriter.flush();
} catch (IOException e) {
LOG.error(String.format("Command execution <%s> failed", command), e);
throw new NodeJsDebuggerException(e.getMessage(), e);
}
}


/**
* Continuously reads process output and store in the {@code #outputs}.
* Returns NodeJs command to run: either {@code nodejs} or {@code node}.
*/
private class OutputReader implements Runnable {
private final StringBuffer outputBuffer;

public OutputReader() {
this.outputBuffer = new StringBuffer();
}

@Override
public void run() {
try {
InputStream in = getInput();
if (in != null) {
String outputData = read(in);
if (!outputData.isEmpty()) {
outputBuffer.append(outputData);
if (outputBuffer.length() > MAX_OUTPUT) {
outputBuffer.delete(0, outputBuffer.length() - MAX_OUTPUT);
}

extractOutputs();
}
}
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}

private InputStream getInput() throws IOException {
return hasError() ? process.getErrorStream()
: (hasInput() ? (hasError() ? process.getErrorStream()
: process.getInputStream())
: null);
}

private void extractOutputs() {
int indexOf;
while ((indexOf = outputBuffer.indexOf(outputSeparator)) >= 0) {
NodeJsOutput nodeJsOutput = NodeJsOutput.of(outputBuffer.substring(0, indexOf));
outputBuffer.delete(0, indexOf + outputSeparator.length());
private static String detectNodeJsCommand() {
String detectionCommand = "if command -v nodejs >/dev/null 2>&1; then echo -n 'nodejs'; else echo -n 'node'; fi";
ProcessBuilder builder = new ProcessBuilder("sh", "-c", detectionCommand);

notifyObservers(nodeJsOutput);
try {
Process process = builder.start();
int resultCode = process.waitFor();
if (resultCode != 0) {
String errMsg = IoUtil.readAndCloseQuietly(process.getErrorStream());
throw new IllegalStateException("NodeJs not found. " + errMsg);
}
}

private boolean hasError() throws IOException {
return process.getErrorStream().available() != 0;
}

private boolean hasInput() throws IOException {
return process.getInputStream().available() != 0;
}

private String read(InputStream in) throws IOException {
int available = min(in.available(), MAX_OUTPUT);
byte[] buf = new byte[available];
int read = in.read(buf, 0, available);

return new String(buf, 0, read, StandardCharsets.UTF_8);
return IoUtil.readAndCloseQuietly(process.getInputStream());
} catch (IOException | InterruptedException e) {
throw new IllegalStateException("NodeJs not found", e);
}
}

private void notifyObservers(NodeJsOutput nodeJsOutput) {
LOG.debug("{}{}", outputSeparator, nodeJsOutput.getOutput());

if (OutputReader.CONNECTIVITY_TEST_NEEDED_MSG.equals(nodeJsOutput.getOutput())) {
testConnectivity();
return;
}

for (NodeJsProcessObserver observer : observers) {
try {
if (observer.onOutputProduced(nodeJsOutput)) {
Expand All @@ -227,25 +190,23 @@ private void notifyObservers(NodeJsOutput nodeJsOutput) {
}
}

/**
* Returns NodeJs command to run: either {@code nodejs} or {@code node}.
*/
private static String detectNodeJsCommand() {
String detectionCommand = "if command -v nodejs >/dev/null 2>&1; then echo -n 'nodejs'; else echo -n 'node'; fi";
ProcessBuilder builder = new ProcessBuilder("sh", "-c", detectionCommand);

private void testConnectivity() {
try {
Process process = builder.start();
int resultCode = process.waitFor();
if (resultCode != 0) {
String errMsg = IoUtil.readAndCloseQuietly(process.getErrorStream());
throw new IllegalStateException("NodeJs not found. " + errMsg);
}

return IoUtil.readAndCloseQuietly(process.getInputStream());
} catch (IOException | InterruptedException e) {
throw new IllegalStateException("NodeJs not found", e);
send("version");
} catch (NodeJsDebuggerException ignored) {
// ignore
}
}


@Override
public void addObserver(NodeJsProcessObserver observer) {
observers.add(observer);
}

@Override
public void removeObserver(NodeJsProcessObserver observer) {
observers.remove(observer);
}

}

0 comments on commit c76d7bd

Please sign in to comment.