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

org.rx.core.ShellCommand Maven / Gradle / Ivy

There is a newer version: 3.0.0
Show newest version
package org.rx.core;

import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.rx.exception.InvalidException;
import org.rx.exception.TraceHandler;
import org.rx.io.Bytes;
import org.rx.io.FileStream;
import org.rx.io.Files;
import org.rx.util.function.TripleAction;

import java.io.File;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import static org.rx.core.Extends.newConcurrentList;
import static org.rx.core.Extends.tryClose;

@Slf4j
public class ShellCommand extends Disposable implements EventPublisher {
    @RequiredArgsConstructor
    @Getter
    public static class PrintOutEventArgs extends EventArgs {
        private static final long serialVersionUID = 4598104225029493537L;
        final int lineNumber;
        final String line;

        @Override
        public String toString() {
            return String.format("%s.\t%s\n", lineNumber, line);
        }
    }

    public static class FileOutHandler extends Disposable implements TripleAction {
        final FileStream fileStream;

        public FileOutHandler(String filePath) {
            Files.createDirectory(filePath);
            fileStream = new FileStream(filePath);
        }

        @Override
        protected void freeObjects() {
            fileStream.close();
        }

        @Override
        public void invoke(ShellCommand s, PrintOutEventArgs e) throws Throwable {
            ByteBuf buf = Bytes.directBuffer();
            buf.writeInt(e.lineNumber);
            buf.writeCharSequence(".\t", StandardCharsets.UTF_8);
            buf.writeCharSequence(e.line, StandardCharsets.UTF_8);
            buf.writeCharSequence("\n", StandardCharsets.UTF_8);
            try {
                fileStream.write(buf);
            } finally {
                buf.release();
            }
        }
    }

    @RequiredArgsConstructor
    @Getter
    public static class ExitedEventArgs extends EventArgs {
        private static final long serialVersionUID = 6563058539741657972L;
        final int exitValue;
    }

    public static final TripleAction CONSOLE_OUT_HANDLER = (s, e) -> System.out.print(e.toString());
    static final String LINUX_BASH = "bash -c ", WIN_CMD = "cmd /c ";
    static final List KILL_LIST = newConcurrentList(true);

    static {
        Tasks.addShutdownHook(() -> {
            for (ShellCommand executor : KILL_LIST) {
                executor.kill();
            }
        });
    }

    public static int exec(String shell, String workspace) {
        return exec(shell, workspace, Constants.TIMEOUT_INFINITE);
    }

    public static int exec(String shell, String workspace, long timeoutMillis) {
        return exec(shell, workspace, timeoutMillis, CONSOLE_OUT_HANDLER);
    }

    public static int exec(String shell, String workspace, long timeoutMillis, TripleAction outHandler) {
        try (ShellCommand cmd = new ShellCommand(shell, workspace)) {
            cmd.onPrintOut.combine(outHandler);
            cmd.start();
            if (timeoutMillis == Constants.TIMEOUT_INFINITE) {
                return cmd.waitFor();
            }

            if (!cmd.waitFor(timeoutMillis)) {
                cmd.kill();
            }
            return cmd.exitValue();
        }
    }

    /**
     * Crack a command line.
     *
     * @param toProcess the command line to process
     * @return the command line broken into strings. An empty or null toProcess parameter results in a zero sized array
     */
    static List translateCommandline(final String toProcess) {
        if (toProcess == null || toProcess.isEmpty()) {
            // no command? no string
            return Collections.emptyList();
        }

        // parse with a simple finite state machine

        final int normal = 0;
        final int inQuote = 1;
        final int inDoubleQuote = 2;
        int state = normal;
        final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
        final ArrayList list = new ArrayList<>();
        StringBuilder current = new StringBuilder();
        boolean lastTokenHasBeenQuoted = false;

        while (tok.hasMoreTokens()) {
            final String nextTok = tok.nextToken();
            switch (state) {
                case inQuote:
                    if ("\'".equals(nextTok)) {
                        lastTokenHasBeenQuoted = true;
                        state = normal;
                    } else {
                        current.append(nextTok);
                    }
                    break;
                case inDoubleQuote:
                    if ("\"".equals(nextTok)) {
                        lastTokenHasBeenQuoted = true;
                        state = normal;
                    } else {
                        current.append(nextTok);
                    }
                    break;
                default:
                    if ("\'".equals(nextTok)) {
                        state = inQuote;
                    } else if ("\"".equals(nextTok)) {
                        state = inDoubleQuote;
                    } else if (" ".equals(nextTok)) {
                        if (lastTokenHasBeenQuoted || current.length() != 0) {
                            list.add(current.toString());
                            current = new StringBuilder();
                        }
                    } else {
                        current.append(nextTok);
                    }
                    lastTokenHasBeenQuoted = false;
                    break;
            }
        }

        if (lastTokenHasBeenQuoted || current.length() != 0) {
            list.add(current.toString());
        }

        if (state == inQuote || state == inDoubleQuote) {
            throw new IllegalArgumentException("Unbalanced quotes in " + toProcess);
        }
        return list;
    }

    public final Delegate onPrintOut = Delegate.create();
    public final Delegate onExited = Delegate.create();

    final long daemonPeriod;
    @Getter
    String shell;
    File workspace;
    Process process;
    Future daemonFuture;

    public synchronized boolean isRunning() {
        return process != null && process.isAlive();
    }

    public synchronized ShellCommand withCloseFlag() {
        shell = (Sys.IS_OS_WINDOWS ? WIN_CMD : LINUX_BASH) + shell;
        return this;
    }

    public synchronized ShellCommand withAutoRestart() {
        onExited.last((s, e) -> restart());
        return this;
    }

    public ShellCommand(String shell) {
        this(shell, null);
    }

    public ShellCommand(String shell, String workspace) {
        this(shell, workspace, Constants.DEFAULT_INTERVAL, true);
    }

    public ShellCommand(@NonNull String shell, String workspace, long daemonPeriod, boolean killOnExited) {
        this.shell = shell.trim();
        this.workspace = workspace == null ? null : new File(workspace);

        this.daemonPeriod = Math.max(1, daemonPeriod);
        if (killOnExited) {
            KILL_LIST.add(this);
        }
    }

    @Override
    protected void freeObjects() {
        onPrintOut.close();
        kill();
        KILL_LIST.remove(this);
    }

    @SneakyThrows
    public synchronized ShellCommand start() {
        if (isRunning()) {
            throw new InvalidException("Already started");
        }

        log.debug("start {}", shell);
        List command = translateCommandline(shell);
        if (!command.isEmpty() && Files.isPath(command.get(0))) {
            workspace = new File(Files.getFullPathNoEndSeparator(command.get(0)));
        }
        Process tmp = process = new ProcessBuilder(command).directory(workspace).redirectErrorStream(true)  //combine inputStream and errorStream
                .start();

        if (daemonFuture != null) {
            daemonFuture.cancel(true);
        }
        daemonFuture = Tasks.run(() -> {
            LineNumberReader reader = null;
            try {
                if (!onPrintOut.isEmpty()) {
                    reader = new LineNumberReader(new InputStreamReader(tmp.getInputStream(), StandardCharsets.UTF_8));
                }

                while (tmp.isAlive()) {
                    try {
                        if (reader != null) {
                            String line;
                            while (
//                                    tmp.isAlive() &&
                                    (line = reader.readLine()) != null) {
                                raiseEvent(onPrintOut, new PrintOutEventArgs(reader.getLineNumber(), line));
                            }
                        }
                    } catch (Throwable e) {
                        TraceHandler.INSTANCE.log("onPrintOut", e);
                    }
                    if (!tmp.isAlive()) {
                        break;
                    }
                    Thread.sleep(daemonPeriod);
                }
            } finally {
                tryClose(reader);
                synchronized (this) {
                    int exitValue = tmp.exitValue();
                    log.debug("exit={} {}", exitValue, shell);
                    raiseEvent(onExited, new ExitedEventArgs(exitValue));
                }
            }
        });
        return this;
    }

    public synchronized int exitValue() {
        if (process == null) {
            throw new InvalidException("Not start");
        }
        return process.exitValue();
    }

    @SneakyThrows
    public synchronized int waitFor() {
        if (!isRunning()) {
            return exitValue();
        }
        return process.waitFor();
    }

    @SneakyThrows
    public synchronized boolean waitFor(long timeoutMillis) {
        if (!isRunning()) {
            return true;
        }
        return process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS);
    }

    @SneakyThrows
    public synchronized void inputKeys(String keys) {
        if (!isRunning()) {
            throw new InvalidException("Not start");
        }

        OutputStream out = process.getOutputStream();
        out.write(keys.getBytes(StandardCharsets.UTF_8));
        out.flush();
    }

    public synchronized void kill() {
        if (!isRunning()) {
            return;
        }

        log.debug("kill {}", shell);
        process.destroyForcibly();
        daemonFuture.cancel(true);
        raiseEvent(onExited, new ExitedEventArgs(process.exitValue()));
    }

    public synchronized void restart() {
        kill();
        start();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy