All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.yamcs.ProcessRunner Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.yamcs.Spec.OptionType;

import com.google.common.base.CharMatcher;

/**
 * Global service that launches and supervises a configured program or script. The primary purpose is to run non-java
 * code, or to decouple java code that uses a fragile or untested JNI layer.
 */
public class ProcessRunner extends AbstractYamcsService {

    private static enum RestartMode {
        ALWAYS("always"),
        ON_SUCCESS("on-success"),
        ON_FAILURE("on-failure"),
        NEVER("never");

        String userOption;

        RestartMode(String userOption) {
            this.userOption = userOption;
        }

        static RestartMode fromUserOption(String userOption) {
            for (RestartMode value : values()) {
                if (value.userOption.equals(userOption)) {
                    return value;
                }
            }
            throw new IllegalArgumentException("Unexpected restart mode: '" + userOption + "'");
        }
    }

    private ProcessBuilder pb;
    private String logLevel;
    private String logPrefix;

    private Process process;
    private ScheduledFuture watchdog;

    private RestartMode restartMode;
    private List successExitCodes;

    @Override
    public Spec getSpec() {
        Spec spec = new Spec();
        spec.addOption("command", OptionType.LIST_OR_ELEMENT)
                .withElementType(OptionType.STRING)
                .withRequired(true);
        spec.addOption("directory", OptionType.STRING);
        spec.addOption("logLevel", OptionType.STRING).withDefault("INFO");
        spec.addOption("logPrefix", OptionType.STRING);
        spec.addOption("restart", OptionType.STRING)
                .withChoices("always", "on-success", "on-failure", "never")
                .withDefault("never");
        spec.addOption("successExitCode", OptionType.LIST_OR_ELEMENT)
                .withElementType(OptionType.INTEGER)
                .withDefault(0);
        spec.addOption("environment", OptionType.MAP).withSpec(Spec.ANY);
        return spec;
    }

    @Override
    public void init(String yamcsInstance, String serviceName, YConfiguration config) throws InitException {
        super.init(yamcsInstance, serviceName, config);

        restartMode = RestartMode.fromUserOption(config.getString("restart"));
        successExitCodes = config.getList("successExitCode");

        List command = config.getList("command");
        pb = new ProcessBuilder(command);

        pb.redirectErrorStream(true);
        pb.environment().put("YAMCS", "1");

        if (config.containsKey("environment")) {
            Map map = config.getMap("environment");
            for (var entry : map.entrySet()) {
                pb.environment().put(entry.getKey(), "" + entry.getValue());
            }
        }

        if (config.containsKey("directory")) {
            pb.directory(new File(config.getString("directory")));
        }

        logLevel = config.getString("logLevel");
        logPrefix = config.getString("logPrefix", "[" + pb.command().get(0) + "] ");
    }

    @Override
    protected void doStart() {
        try {
            startProcess();
            notifyStarted();
        } catch (IOException e) {
            log.error("Failed to start process", e);
            notifyFailed(e);
            return;
        }

        YamcsServer yamcs = YamcsServer.getServer();
        ScheduledExecutorService exec = yamcs.getThreadPoolExecutor();
        watchdog = exec.scheduleWithFixedDelay(() -> {
            if (!process.isAlive() && isRunning() && !yamcs.isShuttingDown()) {
                int code = process.exitValue();

                boolean restart = false;
                if (successExitCodes.contains(code)) {
                    if (restartMode == RestartMode.ALWAYS || restartMode == RestartMode.ON_SUCCESS) {
                        log.info("Process exited with code {}. Starting new process", code);
                        restart = true;
                    } else {
                        log.info("Process exited with code {}. Stopping service", code);
                        stopAsync();
                    }
                } else {
                    if (restartMode == RestartMode.ALWAYS || restartMode == RestartMode.ON_FAILURE) {
                        log.warn("Process exited with code {}. Starting new process", code);
                        restart = true;
                    } else {
                        log.warn("Process exited with code {}. Stopping service", code);
                        stopAsync();
                    }
                }

                if (restart) {
                    try {
                        startProcess();
                    } catch (IOException e) {
                        log.error("Failed to start process", e);
                    }
                }
            }
        }, 5, 5, TimeUnit.SECONDS);
    }

    private void startProcess() throws IOException {
        process = pb.start();

        // Start a thread for reading process output. The thread lifecycle is linked to the process.
        new Thread(() -> {
            try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                reader.lines().forEach(line -> {
                    line = CharMatcher.whitespace().trimTrailingFrom(line);
                    switch (logLevel) {
                    case "DEBUG":
                        log.debug("{}{}", logPrefix, line);
                        break;
                    case "TRACE":
                        log.trace("{}{}", logPrefix, line);
                        break;
                    case "WARN":
                        log.warn("{}{}", logPrefix, line);
                        break;
                    case "ERROR":
                        log.error("{}{}", logPrefix, line);
                        break;
                    default:
                        log.info("{}{}", logPrefix, line);
                    }
                    onProcessOutput(line);
                });
            } catch (IOException e) {
                log.error("Exception while gobbling process output", e);
            }
        }, getClass().getSimpleName() + " Gobbler").start();
    }

    protected void onProcessOutput(String line) {
        // NOP by default
    }

    @Override
    protected void doStop() {
        watchdog.cancel(true);
        process.destroy();

        // Give the process some time to stop before reporting success. During
        // shutdown, this reduces the chance of subprocess to be momentarily
        // alive after the main Yamcs process has already stopped.
        try {
            boolean exited = process.waitFor(1000, TimeUnit.MILLISECONDS);
            if (!exited) {
                // This is also no "guarantee", but we did our best.
                process.destroyForcibly();
            }
            notifyStopped();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy