• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.google.common.annotations.VisibleForTesting;
20 import com.google.common.collect.ImmutableList;
21 import java.io.BufferedReader;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.io.PrintStream;
27 import java.lang.reflect.Field;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.LinkedHashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.concurrent.Executors;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.TimeoutException;
39 import vogar.Log;
40 import vogar.util.Strings;
41 
42 /**
43  * An out of process executable.
44  */
45 public final class Command {
46     private static final ScheduledExecutorService timer
47             = Executors.newSingleThreadScheduledExecutor();
48 
49     private final Log log;
50     private final File workingDir;
51     private final List<String> args;
52     private final Map<String, String> env;
53     private final boolean permitNonZeroExitStatus;
54     private final PrintStream tee;
55 
56     private volatile Process process;
57     private volatile boolean destroyed;
58     private volatile long timeoutNanoTime;
59 
Command(Log log, String... args)60     public Command(Log log, String... args) {
61         this.log = log;
62         this.workingDir = null;
63         this.args = ImmutableList.copyOf(args);
64         this.env = Collections.emptyMap();
65         this.permitNonZeroExitStatus = false;
66         this.tee = null;
67     }
68 
Command(Builder builder)69     private Command(Builder builder) {
70         this.log = builder.log;
71         this.workingDir = builder.workingDir;
72         this.args = ImmutableList.copyOf(builder.args);
73         this.env = builder.env;
74         this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
75         this.tee = builder.tee;
76         if (builder.maxLength != -1) {
77             String string = toString();
78             if (string.length() > builder.maxLength) {
79                 throw new IllegalStateException("Maximum command length " + builder.maxLength
80                                                 + " exceeded by: " + string);
81             }
82         }
83     }
84 
start()85     public void start() throws IOException {
86         if (isStarted()) {
87             throw new IllegalStateException("Already started!");
88         }
89 
90         log.verbose("executing " + args + (workingDir != null ? " in " + workingDir : ""));
91 
92         ProcessBuilder processBuilder = new ProcessBuilder()
93                 .directory(workingDir)
94                 .command(args)
95                 .redirectErrorStream(true);
96 
97         processBuilder.environment().putAll(env);
98 
99         process = processBuilder.start();
100     }
101 
isStarted()102     public boolean isStarted() {
103         return process != null;
104     }
105 
getInputStream()106     public InputStream getInputStream() {
107         if (!isStarted()) {
108             throw new IllegalStateException("Not started!");
109         }
110 
111         return process.getInputStream();
112     }
113 
gatherOutput()114     public List<String> gatherOutput()
115             throws IOException, InterruptedException {
116         if (!isStarted()) {
117             throw new IllegalStateException("Not started!");
118         }
119 
120         BufferedReader in = new BufferedReader(
121                 new InputStreamReader(getInputStream(), "UTF-8"));
122         List<String> outputLines = new ArrayList<String>();
123         String outputLine;
124         while ((outputLine = in.readLine()) != null) {
125             if (tee != null) {
126                 tee.println(outputLine);
127             }
128             outputLines.add(outputLine);
129         }
130 
131         int exitValue = process.waitFor();
132         destroyed = true;
133         if (exitValue != 0 && !permitNonZeroExitStatus) {
134             outputLines.add("Command exited with code: " + exitValue);
135             throw new CommandFailedException(args, outputLines);
136         }
137 
138         return outputLines;
139     }
140 
execute()141     public List<String> execute() {
142         try {
143             start();
144             return gatherOutput();
145         } catch (IOException e) {
146             throw new RuntimeException("Failed to execute process: " + args, e);
147         } catch (InterruptedException e) {
148             throw new RuntimeException("Interrupted while executing process: " + args, e);
149         }
150     }
151 
152     /**
153      * Executes a command with a specified timeout. If the process does not
154      * complete normally before the timeout has elapsed, it will be destroyed.
155      *
156      * @param timeoutSeconds how long to wait, or 0 to wait indefinitely
157      * @return the command's output, or null if the command timed out
158      */
executeWithTimeout(int timeoutSeconds)159     public List<String> executeWithTimeout(int timeoutSeconds) throws TimeoutException {
160         if (timeoutSeconds == 0) {
161             return execute();
162         }
163 
164         scheduleTimeout(timeoutSeconds);
165         return execute();
166     }
167 
168     /**
169      * Destroys the underlying process and closes its associated streams.
170      */
destroy()171     public void destroy() {
172         Process process = this.process;
173         if (process == null) {
174             throw new IllegalStateException();
175         }
176         if (destroyed) {
177             return;
178         }
179 
180         destroyed = true;
181         process.destroy();
182         try {
183             process.waitFor();
184             int exitValue = process.exitValue();
185             log.verbose("received exit value " + exitValue + " from destroyed command " + this);
186         } catch (IllegalThreadStateException | InterruptedException destroyUnsuccessful) {
187             log.warn("couldn't destroy " + this);
188         }
189     }
190 
toString()191     @Override public String toString() {
192         String envString = !env.isEmpty() ? (Strings.join(env.entrySet(), " ") + " ") : "";
193         return envString + Strings.join(args, " ");
194     }
195 
196     /**
197      * Sets the time at which this process will be killed. If a timeout has
198      * already been scheduled, it will be rescheduled.
199      */
scheduleTimeout(int timeoutSeconds)200     public void scheduleTimeout(int timeoutSeconds) {
201         timeoutNanoTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds);
202 
203         new TimeoutTask() {
204             @Override protected void onTimeout(Process process) {
205                 // send a quit signal immediately
206                 log.verbose("sending quit signal to command " + Command.this);
207                 sendQuitSignal(process);
208 
209                 // hard kill in 1 minute
210                 timeoutNanoTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
211                 new TimeoutTask() {
212                     @Override protected void onTimeout(Process process) {
213                         log.verbose("killing timed out command " + Command.this);
214                         destroy();
215                     }
216                 }.schedule();
217             }
218         }.schedule();
219     }
220 
sendQuitSignal(Process process)221     private void sendQuitSignal(Process process) {
222         // TODO: 'adb shell kill' to kill on processes running on Androids
223         new Command(log, "kill", "-3", Long.toString(process.pid())).execute();
224     }
225 
timedOut()226     public boolean timedOut() {
227         return System.nanoTime() >= timeoutNanoTime;
228     }
229 
230     @VisibleForTesting
getArgs()231     public List<String> getArgs() {
232         return args;
233     }
234 
235     public static class Builder {
236         private final Log log;
237         private final List<String> args = new ArrayList<String>();
238         private final Map<String, String> env = new LinkedHashMap<String, String>();
239         private boolean permitNonZeroExitStatus = false;
240         private PrintStream tee = null;
241         private int maxLength = -1;
242         private File workingDir;
243 
Builder(Log log)244         public Builder(Log log) {
245             this.log = log;
246         }
247 
Builder(Builder other)248         public Builder(Builder other) {
249             this.log = other.log;
250             this.workingDir = other.workingDir;
251             this.args.addAll(other.args);
252             this.env.putAll(other.env);
253             this.permitNonZeroExitStatus = other.permitNonZeroExitStatus;
254             this.tee = other.tee;
255             this.maxLength = other.maxLength;
256         }
257 
args(Object... args)258         public Builder args(Object... args) {
259             return args(Arrays.asList(args));
260         }
261 
args(Collection<?> args)262         public Builder args(Collection<?> args) {
263             for (Object object : args) {
264                 this.args.add(object.toString());
265             }
266             return this;
267         }
268 
env(String key, String value)269         public Builder env(String key, String value) {
270             env.put(key, value);
271             return this;
272         }
273 
274         /**
275          * Controls whether execute() throws if the invoked process returns a
276          * nonzero exit code.
277          */
permitNonZeroExitStatus(boolean value)278         public Builder permitNonZeroExitStatus(boolean value) {
279             this.permitNonZeroExitStatus = value;
280             return this;
281         }
282 
tee(PrintStream printStream)283         public Builder tee(PrintStream printStream) {
284             tee = printStream;
285             return this;
286         }
287 
maxLength(int maxLength)288         public Builder maxLength(int maxLength) {
289             this.maxLength = maxLength;
290             return this;
291         }
292 
workingDir(File workingDir)293         public Builder workingDir(File workingDir) {
294             this.workingDir = workingDir;
295             return this;
296         }
297 
build()298         public Command build() {
299             return new Command(this);
300         }
301 
execute()302         public List<String> execute() {
303             return build().execute();
304         }
305     }
306 
307     /**
308      * Runs some code when the command times out.
309      */
310     private abstract class TimeoutTask implements Runnable {
schedule()311         public final void schedule() {
312             timer.schedule(this, timeoutNanoTime - System.nanoTime(), TimeUnit.NANOSECONDS);
313         }
314 
onTimeout(Process process)315         protected abstract void onTimeout(Process process);
316 
run()317         @Override public final void run() {
318             // don't destroy commands that have already been destroyed
319             Process process = Command.this.process;
320             if (destroyed) {
321                 return;
322             }
323 
324             if (timedOut()) {
325                 onTimeout(process);
326             } else {
327                 // if the kill time has been pushed back, reschedule
328                 schedule();
329             }
330         }
331     }
332 }
333