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 
Command(String... args)55     public Command(String... args) {
56         this(Arrays.asList(args));
57     }
58 
Command(List<String> args)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 
Command(Builder builder)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 
start()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 
isStarted()103     public boolean isStarted() {
104         return process != null;
105     }
106 
getInputStream()107     public InputStream getInputStream() {
108         if (!isStarted()) {
109             throw new IllegalStateException("Not started!");
110         }
111 
112         return process.getInputStream();
113     }
114 
gatherOutput()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 
execute()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      */
executeWithTimeout(int timeoutSeconds)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      */
executeLater()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      */
destroy()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 
toString()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 
args(Object... objects)234         public Builder args(Object... objects) {
235             for (Object object : objects) {
236                 args(object.toString());
237             }
238             return this;
239         }
240 
setNativeOutput(boolean nativeOutput)241         public Builder setNativeOutput(boolean nativeOutput) {
242             this.nativeOutput = nativeOutput;
243             return this;
244         }
245 
args(String... args)246         public Builder args(String... args) {
247             return args(Arrays.asList(args));
248         }
249 
args(Collection<String> args)250         public Builder args(Collection<String> args) {
251             this.args.addAll(args);
252             return this;
253         }
254 
env(String key, String value)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          */
workingDirectory(File workingDirectory)266         public Builder workingDirectory(File workingDirectory) {
267             this.workingDirectory = workingDirectory;
268             return this;
269         }
270 
tee(PrintStream printStream)271         public Builder tee(PrintStream printStream) {
272             tee = printStream;
273             return this;
274         }
275 
maxLength(int maxLength)276         public Builder maxLength(int maxLength) {
277             this.maxLength = maxLength;
278             return this;
279         }
280 
build()281         public Command build() {
282             return new Command(this);
283         }
284 
execute()285         public List<String> execute() {
286             return build().execute();
287         }
288     }
289 }
290