Home | History | Annotate | Download | only in commands
      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