• 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             throw new CommandFailedException(args, outputLines);
135         }
136 
137         return outputLines;
138     }
139 
execute()140     public List<String> execute() {
141         try {
142             start();
143             return gatherOutput();
144         } catch (IOException e) {
145             throw new RuntimeException("Failed to execute process: " + args, e);
146         } catch (InterruptedException e) {
147             throw new RuntimeException("Interrupted while executing process: " + args, e);
148         }
149     }
150 
151     /**
152      * Executes a command with a specified timeout. If the process does not
153      * complete normally before the timeout has elapsed, it will be destroyed.
154      *
155      * @param timeoutSeconds how long to wait, or 0 to wait indefinitely
156      * @return the command's output, or null if the command timed out
157      */
executeWithTimeout(int timeoutSeconds)158     public List<String> executeWithTimeout(int timeoutSeconds) throws TimeoutException {
159         if (timeoutSeconds == 0) {
160             return execute();
161         }
162 
163         scheduleTimeout(timeoutSeconds);
164         return execute();
165     }
166 
167     /**
168      * Destroys the underlying process and closes its associated streams.
169      */
destroy()170     public void destroy() {
171         Process process = this.process;
172         if (process == null) {
173             throw new IllegalStateException();
174         }
175         if (destroyed) {
176             return;
177         }
178 
179         destroyed = true;
180         process.destroy();
181         try {
182             process.waitFor();
183             int exitValue = process.exitValue();
184             log.verbose("received exit value " + exitValue + " from destroyed command " + this);
185         } catch (IllegalThreadStateException | InterruptedException destroyUnsuccessful) {
186             log.warn("couldn't destroy " + this);
187         }
188     }
189 
toString()190     @Override public String toString() {
191         String envString = !env.isEmpty() ? (Strings.join(env.entrySet(), " ") + " ") : "";
192         return envString + Strings.join(args, " ");
193     }
194 
195     /**
196      * Sets the time at which this process will be killed. If a timeout has
197      * already been scheduled, it will be rescheduled.
198      */
scheduleTimeout(int timeoutSeconds)199     public void scheduleTimeout(int timeoutSeconds) {
200         timeoutNanoTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds);
201 
202         new TimeoutTask() {
203             @Override protected void onTimeout(Process process) {
204                 // send a quit signal immediately
205                 log.verbose("sending quit signal to command " + Command.this);
206                 sendQuitSignal(process);
207 
208                 // hard kill in 2 seconds
209                 timeoutNanoTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
210                 new TimeoutTask() {
211                     @Override protected void onTimeout(Process process) {
212                         log.verbose("killing timed out command " + Command.this);
213                         destroy();
214                     }
215                 }.schedule();
216             }
217         }.schedule();
218     }
219 
sendQuitSignal(Process process)220     private void sendQuitSignal(Process process) {
221         // TODO: 'adb shell kill' to kill on processes running on Androids
222         new Command(log, "kill", "-3", Long.toString(process.pid())).execute();
223     }
224 
timedOut()225     public boolean timedOut() {
226         return System.nanoTime() >= timeoutNanoTime;
227     }
228 
229     @VisibleForTesting
getArgs()230     public List<String> getArgs() {
231         return args;
232     }
233 
234     public static class Builder {
235         private final Log log;
236         private final List<String> args = new ArrayList<String>();
237         private final Map<String, String> env = new LinkedHashMap<String, String>();
238         private boolean permitNonZeroExitStatus = false;
239         private PrintStream tee = null;
240         private int maxLength = -1;
241         private File workingDir;
242 
Builder(Log log)243         public Builder(Log log) {
244             this.log = log;
245         }
246 
Builder(Builder other)247         public Builder(Builder other) {
248             this.log = other.log;
249             this.workingDir = other.workingDir;
250             this.args.addAll(other.args);
251             this.env.putAll(other.env);
252             this.permitNonZeroExitStatus = other.permitNonZeroExitStatus;
253             this.tee = other.tee;
254             this.maxLength = other.maxLength;
255         }
256 
args(Object... args)257         public Builder args(Object... args) {
258             return args(Arrays.asList(args));
259         }
260 
args(Collection<?> args)261         public Builder args(Collection<?> args) {
262             for (Object object : args) {
263                 this.args.add(object.toString());
264             }
265             return this;
266         }
267 
env(String key, String value)268         public Builder env(String key, String value) {
269             env.put(key, value);
270             return this;
271         }
272 
273         /**
274          * Controls whether execute() throws if the invoked process returns a
275          * nonzero exit code.
276          */
permitNonZeroExitStatus(boolean value)277         public Builder permitNonZeroExitStatus(boolean value) {
278             this.permitNonZeroExitStatus = value;
279             return this;
280         }
281 
tee(PrintStream printStream)282         public Builder tee(PrintStream printStream) {
283             tee = printStream;
284             return this;
285         }
286 
maxLength(int maxLength)287         public Builder maxLength(int maxLength) {
288             this.maxLength = maxLength;
289             return this;
290         }
291 
workingDir(File workingDir)292         public Builder workingDir(File workingDir) {
293             this.workingDir = workingDir;
294             return this;
295         }
296 
build()297         public Command build() {
298             return new Command(this);
299         }
300 
execute()301         public List<String> execute() {
302             return build().execute();
303         }
304     }
305 
306     /**
307      * Runs some code when the command times out.
308      */
309     private abstract class TimeoutTask implements Runnable {
schedule()310         public final void schedule() {
311             timer.schedule(this, System.nanoTime() - timeoutNanoTime, TimeUnit.NANOSECONDS);
312         }
313 
onTimeout(Process process)314         protected abstract void onTimeout(Process process);
315 
run()316         @Override public final void run() {
317             // don't destroy commands that have already been destroyed
318             Process process = Command.this.process;
319             if (destroyed) {
320                 return;
321             }
322 
323             if (timedOut()) {
324                 onTimeout(process);
325             } else {
326                 // if the kill time has been pushed back, reschedule
327                 timer.schedule(this, System.nanoTime() - timeoutNanoTime, TimeUnit.NANOSECONDS);
328             }
329         }
330     }
331 }
332