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}