Implement timeouts on Windows.

Makes #1664 much less acute.

--
MOS_MIGRATED_REVID=130750731
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Command.java b/src/main/java/com/google/devtools/build/lib/shell/Command.java
index 79b850a..9f35d69 100644
--- a/src/main/java/com/google/devtools/build/lib/shell/Command.java
+++ b/src/main/java/com/google/devtools/build/lib/shell/Command.java
@@ -163,6 +163,16 @@
   }
 
   /**
+   * Just like {@link Command(String, Map<String, String>, File, long), but without a timeout.
+   */
+  public Command(
+      String[] commandLineElements,
+      Map<String, String> environmentVariables,
+      File workingDirectory) {
+    this(commandLineElements, environmentVariables, workingDirectory, -1);
+  }
+
+  /**
    * Creates a new {@link Command} for the given command line elements. The
    * command line is executed without a shell.
    *
@@ -180,15 +190,17 @@
    *
    * @param commandLineElements elements of raw command line to execute
    * @param environmentVariables environment variables to replace JVM's
-   *  environment variables; may be null
+   *    environment variables; may be null
    * @param workingDirectory working directory for execution; if null, current
-   * working directory is used
+   *    working directory is used
+   * @param timeoutMillis timeout in milliseconds. Only supported on Windows.
    * @throws IllegalArgumentException if commandLine is null or empty
    */
   public Command(
       String[] commandLineElements,
-      final Map<String, String> environmentVariables,
-      final File workingDirectory) {
+      Map<String, String> environmentVariables,
+      File workingDirectory,
+      long timeoutMillis) {
     if (commandLineElements == null || commandLineElements.length == 0) {
       throw new IllegalArgumentException("command line is null or empty");
     }
@@ -203,6 +215,7 @@
     subprocessBuilder.setArgv(ImmutableList.copyOf(commandLineElements));
     subprocessBuilder.setEnv(environmentVariables);
     subprocessBuilder.setWorkingDirectory(workingDirectory);
+    subprocessBuilder.setTimeoutMillis(timeoutMillis);
   }
 
   /**
@@ -676,7 +689,7 @@
       final Consumers.OutErrConsumers outErrConsumers,
       final boolean killSubprocessOnInterrupt,
       final boolean closeOutputStreams)
-    throws CommandException {
+      throws CommandException {
 
     logCommand();
 
@@ -872,7 +885,7 @@
       while (true) {
         try {
           process.waitFor();
-          return new TerminationStatus(process.exitValue());
+          return new TerminationStatus(process.exitValue(), process.timedout());
         } catch (InterruptedException ie) {
           wasInterrupted = true;
           if (killSubprocessOnInterrupt) {
diff --git a/src/main/java/com/google/devtools/build/lib/shell/JavaSubprocessFactory.java b/src/main/java/com/google/devtools/build/lib/shell/JavaSubprocessFactory.java
index 7c85cec..5218490 100644
--- a/src/main/java/com/google/devtools/build/lib/shell/JavaSubprocessFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/shell/JavaSubprocessFactory.java
@@ -60,6 +60,12 @@
     }
 
     @Override
+    public boolean timedout() {
+      // Not supported.
+      return false;
+    }
+
+    @Override
     public void waitFor() throws InterruptedException {
       process.waitFor();
     }
@@ -93,6 +99,9 @@
 
   @Override
   public Subprocess create(SubprocessBuilder params) throws IOException {
+    if (params.getTimeoutMillis() >= 0) {
+      throw new UnsupportedOperationException("Timeouts are not supported");
+    }
     ProcessBuilder builder = new ProcessBuilder();
     builder.command(params.getArgv());
     if (params.getEnv() != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/shell/Subprocess.java b/src/main/java/com/google/devtools/build/lib/shell/Subprocess.java
index e5710e7..769b6b5 100644
--- a/src/main/java/com/google/devtools/build/lib/shell/Subprocess.java
+++ b/src/main/java/com/google/devtools/build/lib/shell/Subprocess.java
@@ -53,6 +53,11 @@
   boolean finished();
 
   /**
+   * Returns if the process timed out.
+   */
+  boolean timedout();
+
+  /**
    * Waits for the process to finish.
    */
   void waitFor() throws InterruptedException;
diff --git a/src/main/java/com/google/devtools/build/lib/shell/SubprocessBuilder.java b/src/main/java/com/google/devtools/build/lib/shell/SubprocessBuilder.java
index b89cf96..0b70e05 100644
--- a/src/main/java/com/google/devtools/build/lib/shell/SubprocessBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/shell/SubprocessBuilder.java
@@ -45,6 +45,7 @@
   private StreamAction stderrAction;
   private File stderrFile;
   private File workingDirectory;
+  private long timeoutMillis = -1;
 
   private static Subprocess.Factory factory = JavaSubprocessFactory.INSTANCE;
 
@@ -115,6 +116,15 @@
     return this;
   }
 
+  public SubprocessBuilder setTimeoutMillis(long timeoutMillis) {
+    this.timeoutMillis = timeoutMillis;
+    return this;
+  }
+
+  public long getTimeoutMillis() {
+    return timeoutMillis;
+  }
+
   public StreamAction getStderr() {
     return stderrAction;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
index c8ee015..2f75b5e 100644
--- a/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
+++ b/src/main/java/com/google/devtools/build/lib/shell/TerminationStatus.java
@@ -29,6 +29,7 @@
 public final class TerminationStatus {
 
   private final int waitResult;
+  private final boolean timedout;
 
   /**
    * Values taken from the glibc strsignal(3) function.
@@ -79,8 +80,9 @@
    *
    * @param waitResult the value returned by {@link java.lang.Process#waitFor}.
    */
-  public TerminationStatus(int waitResult) {
+  public TerminationStatus(int waitResult, boolean timedout) {
     this.waitResult = waitResult;
+    this.timedout = timedout;
   }
 
   /**
@@ -110,7 +112,14 @@
    * Returns true iff the process exited normally.
    */
   public boolean exited() {
-    return waitResult < SIGNAL_1 || waitResult > SIGNAL_63;
+    return !timedout && (waitResult < SIGNAL_1 || waitResult > SIGNAL_63);
+  }
+
+  /**
+   * Returns true if the process timed out.
+   */
+  public boolean timedout() {
+    return timedout;
   }
 
   /**
@@ -128,7 +137,7 @@
    * if exited() returns true.
    */
   public int getTerminatingSignal() {
-    if (exited()) {
+    if (exited() || timedout) {
       throw new IllegalStateException("getTerminatingSignal() not defined");
     }
     return waitResult - SIGNAL_1 + 1;
@@ -139,16 +148,20 @@
    * e.g. "Exit 1" or "Hangup".
    */
   public String toShortString() {
-    return exited()
-      ? ("Exit " + getExitCode())
-      : (getSignalString(getTerminatingSignal()));
+    return exited() ? "Exit " + getExitCode()
+      : timedout ? "Timeout"
+      : getSignalString(getTerminatingSignal());
   }
 
   @Override
   public String toString() {
-    return exited()
-      ? ("Process exited with status " + getExitCode())
-      : ("Process terminated by signal " + getTerminatingSignal());
+    if (exited()) {
+      return "Process exited with status " + getExitCode();
+    } else if (timedout) {
+      return "Timed out";
+    } else {
+      return "Process terminated by signal " + getTerminatingSignal();
+    }
   }
 
   @Override