1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package vogar.commands; 18 19 import java.io.BufferedReader; 20 import java.io.File; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.InputStreamReader; 24 import java.io.PrintStream; 25 import java.util.ArrayList; 26 import java.util.Arrays; 27 import java.util.Collection; 28 import java.util.Collections; 29 import java.util.LinkedHashMap; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.concurrent.Callable; 33 import java.util.concurrent.ExecutionException; 34 import java.util.concurrent.ExecutorService; 35 import java.util.concurrent.Future; 36 import java.util.concurrent.TimeUnit; 37 import java.util.concurrent.TimeoutException; 38 39 import vogar.util.Log; 40 import vogar.util.Strings; 41 import vogar.util.Threads; 42 43 /** 44 * An out of process executable. 45 */ 46 public final class Command { 47 private final List<String> args; 48 private final Map<String, String> env; 49 private final File workingDirectory; 50 private final boolean permitNonZeroExitStatus; 51 private final PrintStream tee; 52 private final boolean nativeOutput; 53 private volatile Process process; 54 55 public Command(String... args) { 56 this(Arrays.asList(args)); 57 } 58 59 public Command(List<String> args) { 60 this.args = new ArrayList<String>(args); 61 this.env = Collections.emptyMap(); 62 this.workingDirectory = null; 63 this.permitNonZeroExitStatus = false; 64 this.tee = null; 65 this.nativeOutput = false; 66 } 67 68 private Command(Builder builder) { 69 this.args = new ArrayList<String>(builder.args); 70 this.env = builder.env; 71 this.workingDirectory = builder.workingDirectory; 72 this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus; 73 this.tee = builder.tee; 74 if (builder.maxLength != -1) { 75 String string = toString(); 76 if (string.length() > builder.maxLength) { 77 throw new IllegalStateException("Maximum command length " + builder.maxLength 78 + " exceeded by: " + string); 79 } 80 } 81 this.nativeOutput = builder.nativeOutput; 82 } 83 84 public void start() throws IOException { 85 if (isStarted()) { 86 throw new IllegalStateException("Already started!"); 87 } 88 89 Log.verbose("executing " + this); 90 91 ProcessBuilder processBuilder = new ProcessBuilder() 92 .command(args) 93 .redirectErrorStream(true); 94 if (workingDirectory != null) { 95 processBuilder.directory(workingDirectory); 96 } 97 98 processBuilder.environment().putAll(env); 99 100 process = processBuilder.start(); 101 } 102 103 public boolean isStarted() { 104 return process != null; 105 } 106 107 public InputStream getInputStream() { 108 if (!isStarted()) { 109 throw new IllegalStateException("Not started!"); 110 } 111 112 return process.getInputStream(); 113 } 114 115 public List<String> gatherOutput() 116 throws IOException, InterruptedException { 117 if (!isStarted()) { 118 throw new IllegalStateException("Not started!"); 119 } 120 121 BufferedReader in = new BufferedReader( 122 new InputStreamReader(getInputStream(), "UTF-8")); 123 List<String> outputLines = new ArrayList<String>(); 124 String outputLine; 125 while ((outputLine = in.readLine()) != null) { 126 if (tee != null) { 127 tee.println(outputLine); 128 } 129 if (nativeOutput) { 130 Log.nativeOutput(outputLine); 131 } 132 outputLines.add(outputLine); 133 } 134 135 if (process.waitFor() != 0 && !permitNonZeroExitStatus) { 136 StringBuilder message = new StringBuilder(); 137 for (String line : outputLines) { 138 message.append("\n").append(line); 139 } 140 throw new CommandFailedException(args, outputLines); 141 } 142 143 return outputLines; 144 } 145 146 public List<String> execute() { 147 try { 148 start(); 149 return gatherOutput(); 150 } catch (IOException e) { 151 throw new RuntimeException("Failed to execute process: " + args, e); 152 } catch (InterruptedException e) { 153 throw new RuntimeException("Interrupted while executing process: " + args, e); 154 } 155 } 156 157 /** 158 * Executes a command with a specified timeout. If the process does not 159 * complete normally before the timeout has elapsed, it will be destroyed. 160 * 161 * @param timeoutSeconds how long to wait, or 0 to wait indefinitely 162 * @return the command's output, or null if the command timed out 163 */ 164 public List<String> executeWithTimeout(int timeoutSeconds) 165 throws TimeoutException { 166 if (timeoutSeconds == 0) { 167 return execute(); 168 } 169 170 try { 171 return executeLater().get(timeoutSeconds, TimeUnit.SECONDS); 172 } catch (InterruptedException e) { 173 throw new RuntimeException("Interrupted while executing process: " + args, e); 174 } catch (ExecutionException e) { 175 throw new RuntimeException(e); 176 } finally { 177 destroy(); 178 } 179 } 180 181 /** 182 * Executes the command on a new background thread. This method returns 183 * immediately. 184 * 185 * @return a future to retrieve the command's output. 186 */ 187 public Future<List<String>> executeLater() { 188 ExecutorService executor = Threads.fixedThreadsExecutor("command", 1); 189 Future<List<String>> result = executor.submit(new Callable<List<String>>() { 190 public List<String> call() throws Exception { 191 start(); 192 return gatherOutput(); 193 } 194 }); 195 executor.shutdown(); 196 return result; 197 } 198 199 /** 200 * Destroys the underlying process and closes its associated streams. 201 */ 202 public void destroy() { 203 if (process == null) { 204 return; 205 } 206 207 process.destroy(); 208 try { 209 process.waitFor(); 210 int exitValue = process.exitValue(); 211 Log.verbose("received exit value " + exitValue 212 + " from destroyed command " + this); 213 } catch (IllegalThreadStateException destroyUnsuccessful) { 214 Log.warn("couldn't destroy " + this); 215 } catch (InterruptedException e) { 216 Log.warn("couldn't destroy " + this); 217 } 218 } 219 220 @Override public String toString() { 221 String envString = !env.isEmpty() ? (Strings.join(env.entrySet(), " ") + " ") : ""; 222 return envString + Strings.join(args, " "); 223 } 224 225 public static class Builder { 226 private final List<String> args = new ArrayList<String>(); 227 private final Map<String, String> env = new LinkedHashMap<String, String>(); 228 private File workingDirectory; 229 private boolean permitNonZeroExitStatus = false; 230 private PrintStream tee = null; 231 private boolean nativeOutput; 232 private int maxLength = -1; 233 234 public Builder args(Object... objects) { 235 for (Object object : objects) { 236 args(object.toString()); 237 } 238 return this; 239 } 240 241 public Builder setNativeOutput(boolean nativeOutput) { 242 this.nativeOutput = nativeOutput; 243 return this; 244 } 245 246 public Builder args(String... args) { 247 return args(Arrays.asList(args)); 248 } 249 250 public Builder args(Collection<String> args) { 251 this.args.addAll(args); 252 return this; 253 } 254 255 public Builder env(String key, String value) { 256 env.put(key, value); 257 return this; 258 } 259 260 /** 261 * Sets the working directory from which the command will be executed. 262 * This must be a <strong>local</strong> directory; Commands run on 263 * remote devices (ie. via {@code adb shell}) require a local working 264 * directory. 265 */ 266 public Builder workingDirectory(File workingDirectory) { 267 this.workingDirectory = workingDirectory; 268 return this; 269 } 270 271 public Builder tee(PrintStream printStream) { 272 tee = printStream; 273 return this; 274 } 275 276 public Builder maxLength(int maxLength) { 277 this.maxLength = maxLength; 278 return this; 279 } 280 281 public Command build() { 282 return new Command(this); 283 } 284 285 public List<String> execute() { 286 return build().execute(); 287 } 288 } 289 } 290