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
diff --git a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
index 6c5df1b..04da210 100644
--- a/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/standalone/StandaloneSpawnStrategy.java
@@ -77,11 +77,11 @@
         .getEventBus()
         .post(ActionStatusMessage.runningStrategy(spawn.getResourceOwner(), "standalone"));
 
-    int timeout = -1;
+    int timeoutSeconds = -1;
     String timeoutStr = spawn.getExecutionInfo().get("timeout");
     if (timeoutStr != null) {
       try {
-        timeout = Integer.parseInt(timeoutStr);
+        timeoutSeconds = Integer.parseInt(timeoutStr);
       } catch (NumberFormatException e) {
         throw new UserExecException("could not parse timeout: ", e);
       }
@@ -97,7 +97,7 @@
       // Disable it for now to make the setup easier and to avoid further PATH hacks.
       // Ideally we should have a native implementation of process-wrapper for Windows.
       args.add(processWrapper.getPathString());
-      args.add(Integer.toString(timeout));
+      args.add(Integer.toString(timeoutSeconds));
       args.add("5"); /* kill delay: give some time to print stacktraces and whatnot. */
 
       // TODO(bazel-team): use process-wrapper redirection so we don't have to
@@ -109,7 +109,8 @@
 
     String cwd = executor.getExecRoot().getPathString();
     Command cmd = new Command(args.toArray(new String[]{}),
-        locallyDeterminedEnv(spawn.getEnvironment()), new File(cwd));
+        locallyDeterminedEnv(spawn.getEnvironment()), new File(cwd),
+        OS.getCurrent() == OS.WINDOWS && timeoutSeconds >= 0 ? timeoutSeconds * 1000 : -1);
 
     FileOutErr outErr = actionExecutionContext.getFileOutErr();
     try {
@@ -121,7 +122,8 @@
           /*killSubprocessOnInterrupt*/ true);
     } catch (AbnormalTerminationException e) {
       TerminationStatus status = e.getResult().getTerminationStatus();
-      boolean timedOut = !status.exited() && (status.getTerminatingSignal() == 14 /* SIGALRM */);
+      boolean timedOut = !status.exited() && (
+          status.timedout() || status.getTerminatingSignal() == 14 /* SIGALRM */);
       String message =
           CommandFailureUtils.describeCommandFailure(
               verboseFailures, spawn.getArguments(), spawn.getEnvironment(), cwd);
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java
index 2188068..5774f0f 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java
@@ -76,11 +76,15 @@
   static native int nativeReadStream(long stream, byte[] bytes, int offset, int length);
 
   /**
-   * Waits until the given process terminates.
+   * Waits until the given process terminates. If timeout is non-negative, it indicates the number
+   * of milliseconds before the call times out.
    *
-   * <p>Returns true if the process terminated or false if something went wrong.
+   * <p>Return values:
+   * <li>0: Process finished</li>
+   * <li>1: Timeout</li>
+   * <li>2: Something went wrong</li>
    */
-  static native boolean nativeWaitFor(long process);
+  static native int nativeWaitFor(long process, long timeout);
 
   /**
    * Returns the exit code of the process. Throws {@code IllegalStateException} if something
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocess.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocess.java
index 2457749..e0009d5 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocess.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocess.java
@@ -15,7 +15,6 @@
 package com.google.devtools.build.lib.windows;
 
 import com.google.devtools.build.lib.shell.Subprocess;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -23,6 +22,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -124,12 +124,14 @@
   private final ProcessInputStream stdoutStream;
   private final ProcessInputStream stderrStream;
   private final CountDownLatch waitLatch;
+  private final long timeoutMillis;
+  private final AtomicBoolean timedout = new AtomicBoolean(false);
 
-
-  WindowsSubprocess(
-      long nativeProcess, String commandLine, boolean stdoutRedirected, boolean stderrRedirected) {
+  WindowsSubprocess(long nativeProcess, String commandLine, boolean stdoutRedirected,
+      boolean stderrRedirected, long timeoutMillis) {
     this.commandLine = commandLine;
     this.nativeProcess = nativeProcess;
+    this.timeoutMillis = timeoutMillis;
     stdoutStream =
         stdoutRedirected
             ? null
@@ -150,11 +152,24 @@
   }
 
   private void waiterThreadFunc() {
-    if (!WindowsProcesses.nativeWaitFor(nativeProcess)) {
-      // There isn't a lot we can do -- the process is still alive but WaitForMultipleObjects()
-      // failed for some odd reason. We'll pretend it terminated and log a message to jvm.out .
-      System.err.println("Waiting for process "
-          + WindowsProcesses.nativeGetProcessPid(nativeProcess) + " failed");
+    switch (WindowsProcesses.nativeWaitFor(nativeProcess, timeoutMillis)) {
+      case 0:
+        // Excellent, process finished in time.
+        break;
+
+      case 1:
+        // Timeout. Terminate the process if we can.
+        timedout.set(true);
+        WindowsProcesses.nativeTerminate(nativeProcess);
+        break;
+
+      case 2:
+        // Error. There isn't a lot we can do -- the process is still alive but
+        // WaitForMultipleObjects() failed for some odd reason. We'll pretend it terminated and
+        // log a message to jvm.out .
+        System.err.println("Waiting for process "
+            + WindowsProcesses.nativeGetProcessPid(nativeProcess) + " failed");
+        break;
     }
 
     waitLatch.countDown();
@@ -198,6 +213,11 @@
   }
 
   @Override
+  public boolean timedout() {
+    return timedout.get();
+  }
+
+  @Override
   public void waitFor() throws InterruptedException {
     waitLatch.await();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java
index 10759a9..fbd0cdd 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java
@@ -52,8 +52,8 @@
       throw new IOException(error);
     }
 
-    return new WindowsSubprocess(
-        nativeProcess, commandLine, stdoutPath != null, stderrPath != null);
+    return new WindowsSubprocess(nativeProcess, commandLine, stdoutPath != null,
+        stderrPath != null, builder.getTimeoutMillis());
   }
 
   private String getRedirectPath(StreamAction action, File file) {
diff --git a/src/main/native/windows_processes.cc b/src/main/native/windows_processes.cc
index 5167857..3231c97 100644
--- a/src/main/native/windows_processes.cc
+++ b/src/main/native/windows_processes.cc
@@ -412,21 +412,29 @@
   return exit_code;
 }
 
-extern "C" JNIEXPORT jboolean JNICALL
+// return values:
+// 0: Wait completed successfully
+// 1: Timeout
+// 2: Wait returned with an error
+extern "C" JNIEXPORT jint JNICALL
 Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWaitFor(
-    JNIEnv *env, jclass clazz, jlong process_long) {
+    JNIEnv *env, jclass clazz, jlong process_long, jlong java_timeout) {
   NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
   HANDLE handles[1] = { process->process_ };
-  switch (WaitForMultipleObjects(1, handles, FALSE, INFINITE)) {
+  DWORD win32_timeout = java_timeout < 0 ? INFINITE : java_timeout;
+  switch (WaitForMultipleObjects(1, handles, FALSE, win32_timeout)) {
     case 0:
-      return true;
+      return 0;
+
+    case WAIT_TIMEOUT:
+      return 1;
 
     case WAIT_FAILED:
-      return false;
+      return 2;
 
     default:
       process->error_ = "WaitForMultipleObjects() returned unknown result";
-      return false;
+      return 2;
   }
 }
 
diff --git a/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java b/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java
index 1562532..540162c 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java
@@ -34,6 +34,7 @@
  *   <li><code>O&lt;register&gt;</code>: Write the contents of a register to stdout</li>
  *   <li><code>E&lt;register&gt;</code>: Write the contents of a register to stderr</li>
  *   <li><code>X&lt;exit code%gt;</code>: Exit with the specified exit code</li>
+ *   <li><code>S&lt;seconds&gt;</code>: Wait the specified number of seconds</li>
  * </ul>
  *
  * <p>Registers are single characters. Each command line argument is interpreted as a single
@@ -95,6 +96,15 @@
           writeBytes(System.out, arg);
           break;
 
+        case 'W':
+          try {
+            Thread.sleep(Integer.parseInt(arg.substring(1)) * 1000);
+          } catch (InterruptedException e) {
+            // This is good enough for a mock process
+            throw new IllegalStateException(e);
+          }
+          break;
+
         case 'X':
           System.exit(Integer.parseInt(arg.substring(1)));
       }
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
index 074c81d..0b09216 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
@@ -133,7 +133,7 @@
   @Test
   public void testExitCode() throws Exception {
     process = WindowsProcesses.nativeCreateProcess(mockArgs("X42"), null, null, null, null);
-    assertThat(WindowsProcesses.nativeWaitFor(process)).isTrue();
+    assertThat(WindowsProcesses.nativeWaitFor(process, -1)).isEqualTo(0);
     assertThat(WindowsProcesses.nativeGetExitCode(process)).isEqualTo(42);
     assertNoProcessError();
   }
@@ -302,7 +302,7 @@
         null, null, stdoutFile, stderrFile);
     assertThat(process).isGreaterThan(0L);
     assertNoProcessError();
-    assertThat(WindowsProcesses.nativeWaitFor(process)).isTrue();
+    assertThat(WindowsProcesses.nativeWaitFor(process, -1)).isEqualTo(0);
     WindowsProcesses.nativeGetExitCode(process);
     assertNoProcessError();
     byte[] stdout = Files.readAllBytes(Paths.get(stdoutFile));
@@ -319,7 +319,7 @@
         null, null, file, file);
     assertThat(process).isGreaterThan(0L);
     assertNoProcessError();
-    assertThat(WindowsProcesses.nativeWaitFor(process)).isTrue();
+    assertThat(WindowsProcesses.nativeWaitFor(process, -1)).isEqualTo(0);
     WindowsProcesses.nativeGetExitCode(process);
     assertNoProcessError();
     byte[] bytes = Files.readAllBytes(Paths.get(file));
@@ -337,7 +337,7 @@
     byte[] buf = new byte[1];
     assertThat(readStdout(buf, 0, 1)).isEqualTo(-1);
     assertThat(readStderr(buf, 0, 1)).isEqualTo(-1);
-    WindowsProcesses.nativeWaitFor(process);
+    WindowsProcesses.nativeWaitFor(process, -1);
   }
 
   @Test
@@ -352,7 +352,7 @@
     process = WindowsProcesses.nativeCreateProcess(mockArgs("O-out2", "E-err2"), null,
         null, stdoutFile, stderrFile);
     assertNoProcessError();
-    WindowsProcesses.nativeWaitFor(process);
+    WindowsProcesses.nativeWaitFor(process, -1);
     WindowsProcesses.nativeGetExitCode(process);
     assertNoProcessError();
     byte[] stdoutBytes = Files.readAllBytes(Paths.get(stdoutFile));
@@ -372,6 +372,11 @@
     int len = readStdout(buf, 0, 1024);
     assertNoProcessError();
     assertThat(new String(buf, 0, len, UTF8).replace("\\", "/")).isEqualTo(dir1);
+  }
 
+  @Test
+  public void testTimeout() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("W5", "X0"), null, null, null, null);
+    assertThat(WindowsProcesses.nativeWaitFor(process, 1000)).isEqualTo(1);
   }
 }