001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.common.util;
019
020import java.io.*;
021import org.photonvision.common.logging.LogGroup;
022import org.photonvision.common.logging.Logger;
023
024/** Execute external process and optionally read output buffer. */
025@SuppressWarnings({"unused"})
026public class ShellExec {
027    private static final Logger logger = new Logger(ShellExec.class, LogGroup.General);
028
029    private int exitCode;
030    private final boolean readOutput;
031    private final boolean readError;
032    private StreamGobbler errorGobbler, outputGobbler;
033
034    public ShellExec() {
035        this(false, false);
036    }
037
038    public ShellExec(boolean readOutput, boolean readError) {
039        this.readOutput = readOutput;
040        this.readError = readError;
041    }
042
043    /**
044     * Execute a bash command. We can handle complex bash commands including multiple executions (; |
045     * and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
046     *
047     * @param command Bash command to execute
048     * @return process exit code
049     */
050    public int executeBashCommand(String command) throws IOException {
051        return executeBashCommand(command, true, true);
052    }
053
054    /**
055     * Execute a bash command. We can handle complex bash commands including multiple executions (; |
056     * and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
057     *
058     * @param command Bash command to execute
059     * @param wait true if the command should wait for the proccess to complete
060     * @return process exit code
061     */
062    public int executeBashCommand(String command, boolean wait) throws IOException {
063        return executeBashCommand(command, true, true);
064    }
065
066    /**
067     * Execute a bash command. We can handle complex bash commands including multiple executions (; |
068     * and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
069     * This runs the commands with the default logging.
070     *
071     * @param command Bash command to execute
072     * @param wait true if the command should wait for the proccess to complete
073     * @param debug true if the command and return value should be logged
074     * @return process exit code
075     */
076    public int executeBashCommand(String command, boolean wait, boolean debug) throws IOException {
077        if (debug) logger.debug("Executing \"" + command + "\"");
078
079        boolean success = false;
080        Runtime r = Runtime.getRuntime();
081        // Use bash -c, so we can handle things like multi commands separated by ; and
082        // things like quotes, $, |, and \. My tests show that command comes as
083        // one argument to bash, so we do not need to quote it to make it one thing.
084        // Also, exec may object if it does not have an executable file as the first thing,
085        // so having bash here makes it happy provided bash is installed and in path.
086        String[] commands = {"bash", "-c", command};
087
088        Process process = r.exec(commands);
089
090        // Consume streams, older jvm's had a memory leak if streams were not read,
091        // some other jvm+OS combinations may block unless streams are consumed.
092        int retcode = doProcess(wait, process);
093        if (debug) logger.debug("Got exit code " + retcode);
094        return retcode;
095    }
096
097    /**
098     * Execute a command in current folder, and wait for process to end
099     *
100     * @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
101     * @param args 0..n command line arguments
102     * @return process exit code
103     */
104    public int execute(String command, String... args) throws IOException {
105        return execute(command, null, true, args);
106    }
107
108    /**
109     * Execute a command.
110     *
111     * @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
112     * @param workdir working directory or NULL to use command folder
113     * @param wait wait for process to end
114     * @param args 0..n command line arguments
115     * @return process exit code
116     */
117    public int execute(String command, String workdir, boolean wait, String... args)
118            throws IOException {
119        String[] cmdArr;
120        if (args != null && args.length > 0) {
121            cmdArr = new String[1 + args.length];
122            cmdArr[0] = command;
123            System.arraycopy(args, 0, cmdArr, 1, args.length);
124        } else {
125            cmdArr = new String[] {command};
126        }
127
128        ProcessBuilder pb = new ProcessBuilder(cmdArr);
129        File workingDir = (workdir == null ? new File(command).getParentFile() : new File(workdir));
130        pb.directory(workingDir);
131
132        Process process = pb.start();
133
134        // Consume streams, older jvm's had a memory leak if streams were not read,
135        // some other jvm+OS combinations may block unless streams are consumed.
136        return doProcess(wait, process);
137    }
138
139    private int doProcess(boolean wait, Process process) {
140        errorGobbler = new StreamGobbler(process.getErrorStream(), readError);
141        outputGobbler = new StreamGobbler(process.getInputStream(), readOutput);
142        errorGobbler.start();
143        outputGobbler.start();
144
145        exitCode = 0;
146        if (wait) {
147            try {
148                exitCode = process.waitFor();
149                errorGobbler.join();
150                outputGobbler.join();
151            } catch (InterruptedException ignored) {
152            }
153        }
154        return exitCode;
155    }
156
157    public int getExitCode() {
158        return exitCode;
159    }
160
161    public boolean isOutputCompleted() {
162        return (outputGobbler != null && outputGobbler.isCompleted());
163    }
164
165    public boolean isErrorCompleted() {
166        return (errorGobbler != null && errorGobbler.isCompleted());
167    }
168
169    public String getOutput() {
170        return (outputGobbler != null ? outputGobbler.getOutput() : null);
171    }
172
173    public String getError() {
174        return (errorGobbler != null ? errorGobbler.getOutput() : null);
175    }
176
177    // ********************************************
178    // ********************************************
179
180    /**
181     * StreamGobbler reads inputstream to "gobble" it. This is used by Executor class when running a
182     * commandline applications. Gobblers must read/purge INSTR and ERRSTR process streams.
183     * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html?page=4
184     */
185    @SuppressWarnings("WeakerAccess")
186    private static class StreamGobbler extends Thread {
187        private final InputStream is;
188        private final StringBuilder output;
189        private volatile boolean completed; // mark volatile to guarantee a thread safety
190
191        public StreamGobbler(InputStream is, boolean readStream) {
192            this.is = is;
193            this.output = (readStream ? new StringBuilder(256) : null);
194        }
195
196        public void run() {
197            completed = false;
198            try {
199                String NL = System.getProperty("line.separator", "\r\n");
200
201                InputStreamReader isr = new InputStreamReader(is);
202                BufferedReader br = new BufferedReader(isr);
203                String line;
204                while ((line = br.readLine()) != null) {
205                    if (output != null) output.append(line).append(NL);
206                }
207            } catch (IOException ex) {
208                // ex.printStackTrace();
209            }
210            completed = true;
211        }
212
213        /**
214         * Get inputstream buffer or null if stream was not consumed.
215         *
216         * @return Output stream
217         */
218        public String getOutput() {
219            return (output != null ? output.toString() : null);
220        }
221
222        /**
223         * Is input stream completed.
224         *
225         * @return if input stream is completed
226         */
227        public boolean isCompleted() {
228            return completed;
229        }
230    }
231}