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 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 
     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 
     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 
     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 
    102     public boolean isStarted() {
    103         return process != null;
    104     }
    105 
    106     public InputStream getInputStream() {
    107         if (!isStarted()) {
    108             throw new IllegalStateException("Not started!");
    109         }
    110 
    111         return process.getInputStream();
    112     }
    113 
    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 
    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      */
    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      */
    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 
    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      */
    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 
    220     private void sendQuitSignal(Process process) {
    221         // TODO: 'adb shell kill' to kill on processes running on Androids
    222         new Command(log, "kill", "-3", Integer.toString(getPid(process))).execute();
    223     }
    224 
    225     /**
    226      * Return the PID of this command's process.
    227      */
    228     private int getPid(Process process) {
    229         try {
    230             // See org.openqa.selenium.ProcessUtils.getProcessId()
    231             Field field = process.getClass().getDeclaredField("pid");
    232             field.setAccessible(true);
    233             return (Integer) field.get(process);
    234         } catch (Exception e) {
    235             throw new RuntimeException(e);
    236         }
    237     }
    238 
    239     public boolean timedOut() {
    240         return System.nanoTime() >= timeoutNanoTime;
    241     }
    242 
    243     @VisibleForTesting
    244     public List<String> getArgs() {
    245         return args;
    246     }
    247 
    248     public static class Builder {
    249         private final Log log;
    250         private final List<String> args = new ArrayList<String>();
    251         private final Map<String, String> env = new LinkedHashMap<String, String>();
    252         private boolean permitNonZeroExitStatus = false;
    253         private PrintStream tee = null;
    254         private int maxLength = -1;
    255         private File workingDir;
    256 
    257         public Builder(Log log) {
    258             this.log = log;
    259         }
    260 
    261         public Builder(Builder other) {
    262             this.log = other.log;
    263             this.workingDir = other.workingDir;
    264             this.args.addAll(other.args);
    265             this.env.putAll(other.env);
    266             this.permitNonZeroExitStatus = other.permitNonZeroExitStatus;
    267             this.tee = other.tee;
    268             this.maxLength = other.maxLength;
    269         }
    270 
    271         public Builder args(Object... args) {
    272             return args(Arrays.asList(args));
    273         }
    274 
    275         public Builder args(Collection<?> args) {
    276             for (Object object : args) {
    277                 this.args.add(object.toString());
    278             }
    279             return this;
    280         }
    281 
    282         public Builder env(String key, String value) {
    283             env.put(key, value);
    284             return this;
    285         }
    286 
    287         /**
    288          * Controls whether execute() throws if the invoked process returns a
    289          * nonzero exit code.
    290          */
    291         public Builder permitNonZeroExitStatus(boolean value) {
    292             this.permitNonZeroExitStatus = value;
    293             return this;
    294         }
    295 
    296         public Builder tee(PrintStream printStream) {
    297             tee = printStream;
    298             return this;
    299         }
    300 
    301         public Builder maxLength(int maxLength) {
    302             this.maxLength = maxLength;
    303             return this;
    304         }
    305 
    306         public Builder workingDir(File workingDir) {
    307             this.workingDir = workingDir;
    308             return this;
    309         }
    310 
    311         public Command build() {
    312             return new Command(this);
    313         }
    314 
    315         public List<String> execute() {
    316             return build().execute();
    317         }
    318     }
    319 
    320     /**
    321      * Runs some code when the command times out.
    322      */
    323     private abstract class TimeoutTask implements Runnable {
    324         public final void schedule() {
    325             timer.schedule(this, System.nanoTime() - timeoutNanoTime, TimeUnit.NANOSECONDS);
    326         }
    327 
    328         protected abstract void onTimeout(Process process);
    329 
    330         @Override public final void run() {
    331             // don't destroy commands that have already been destroyed
    332             Process process = Command.this.process;
    333             if (destroyed) {
    334                 return;
    335             }
    336 
    337             if (timedOut()) {
    338                 onTimeout(process);
    339             } else {
    340                 // if the kill time has been pushed back, reschedule
    341                 timer.schedule(this, System.nanoTime() - timeoutNanoTime, TimeUnit.NANOSECONDS);
    342             }
    343         }
    344     }
    345 }
    346