/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.nodejsdbg.server; import com.google.common.util.concurrent.ThreadFactoryBuilder; 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.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * 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 String NODEJS_COMMAND = detectNodeJsCommand(); 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; 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()); 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); } private Process initializeNodeJsDebugProcess(String[] options) throws NodeJsDebuggerException { List<String> commands = new ArrayList<>(1 + options.length); commands.add(NODEJS_COMMAND); commands.addAll(Arrays.asList(options)); ProcessBuilder processBuilder = new ProcessBuilder(commands); try { return processBuilder.start(); } catch (IOException e) { throw new NodeJsDebuggerException("NodeJs process initialization failed.", e); } } /** * Stops nodejs process. */ public void stop() { boolean interrupted = false; observers.clear(); try { send("quit"); } catch (NodeJsDebuggerException e) { 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.error("Unable to terminate pool of NodeJs debugger tasks."); } } } catch (InterruptedException e) { 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 process."); } } catch (InterruptedException e) { interrupted = true; if (!process.isAlive()) { LOG.error("Unable to terminate NodeJs process."); } } if (interrupted) { Thread.currentThread().interrupt(); } } /** * Synchronizes sending commands. */ public synchronized void send(String command) throws NodeJsDebuggerException { LOG.debug("Execute: {}", command); if (!process.isAlive()) { throw new NodeJsDebuggerTerminatedException("NodeJs process has been terminated."); } try { processWriter.write(command); processWriter.newLine(); processWriter.flush(); } catch (IOException e) { throw new NodeJsDebuggerException(e.getMessage(), e); } } /** * 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); 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); } } 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)) { break; } } catch (NodeJsDebuggerException e) { LOG.error(e.getMessage(), e); } } } private void testConnectivity() { try { send("version"); } catch (NodeJsDebuggerException ignored) { // ignore } } @Override public void addObserver(NodeJsProcessObserver observer) { observers.add(observer); } @Override public void removeObserver(NodeJsProcessObserver observer) { observers.remove(observer); } }