aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/shell/JavaSubprocessFactory.java
blob: c282d57ab62080bef6342e211ff1930c5008154e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Copyright 2016 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.build.lib.shell;

import com.google.devtools.build.lib.shell.SubprocessBuilder.StreamAction;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A subprocess factory that uses {@link java.lang.ProcessBuilder}.
 */
public class JavaSubprocessFactory implements SubprocessFactory {

  /**
   * A subprocess backed by a {@link java.lang.Process}.
   */
  private static class JavaSubprocess implements Subprocess {
    private final Process process;
    private final long deadlineMillis;
    private final AtomicBoolean deadlineExceeded = new AtomicBoolean();

    private JavaSubprocess(Process process, long deadlineMillis) {
      this.process = process;
      this.deadlineMillis = deadlineMillis;
    }

    @Override
    public boolean destroy() {
      process.destroy();
      return true;
    }

    @Override
    public int exitValue() {
      return process.exitValue();
    }

    @Override
    public boolean finished() {
      if (deadlineMillis > 0
          && System.currentTimeMillis() > deadlineMillis
          && deadlineExceeded.compareAndSet(false, true)) {
        // We use compareAndSet here to avoid calling destroy multiple times. Note that destroy
        // returns immediately, and we don't want to wait in this method.
        process.destroy();
      }
      // this seems to be the only non-blocking call for checking liveness
      return !process.isAlive();
    }

    @Override
    public boolean timedout() {
      return deadlineExceeded.get();
    }

    @Override
    public void waitFor() throws InterruptedException {
      if (deadlineMillis > 0) {
        // Careful: I originally used Long.MAX_VALUE if there's no timeout. This is safe with
        // Process, but not for the UNIXProcess subclass, which has an integer overflow for very
        // large timeouts. As of this writing, it converts the passed in value to nanos (which
        // saturates at Long.MAX_VALUE), then adds 999999 to round up (which overflows), converts
        // back to millis, and then calls Object.wait with a negative timeout, which throws.
        long waitTimeMillis = deadlineMillis - System.currentTimeMillis();
        boolean exitedInTime = process.waitFor(waitTimeMillis, TimeUnit.MILLISECONDS);
        if (!exitedInTime && deadlineExceeded.compareAndSet(false, true)) {
          process.destroy();
          // The destroy call returns immediately, so we still need to wait for the actual exit. The
          // sole caller assumes that waitFor only exits when the process is gone (or throws).
          process.waitFor();
        }
      } else {
        process.waitFor();
      }
    }

    @Override
    public OutputStream getOutputStream() {
      return process.getOutputStream();
    }

    @Override
    public InputStream getErrorStream() {
      return process.getErrorStream();
    }

    @Override
    public InputStream getInputStream() {
      return process.getInputStream();
    }

    @Override
    public void close() {
      // java.lang.Process doesn't give us a way to clean things up other than #destroy(), which was
      // already called by this point.
    }
  }

  public static final JavaSubprocessFactory INSTANCE = new JavaSubprocessFactory();

  private JavaSubprocessFactory() {
    // We are a singleton
  }

  // since we are a singleton, we represent an ideal global lock for
  // process invocations, which is required due to the following race condition:

  // Linux does not provide a safe API for a multi-threaded program to fork a subprocess.
  // Consider the case where two threads both write an executable file and then try to execute
  // it. It can happen that the first thread writes its executable file, with the file
  // descriptor still being open when the second thread forks, with the fork inheriting a copy
  // of the file descriptor. Then the first thread closes the original file descriptor, and
  // proceeds to execute the file. At that point Linux sees an open file descriptor to the file
  // and returns ETXTBSY (Text file busy) as an error. This race is inherent in the fork / exec
  // duality, with fork always inheriting a copy of the file descriptor table; if there was a
  // way to fork without copying the entire file descriptor table (e.g., only copy specific
  // entries), we could avoid this race.
  //
  // I was able to reproduce this problem reliably by running significantly more threads than
  // there are CPU cores on my workstation - the more threads the more likely it happens.
  //
  // As a workaround, we put a synchronized block around the fork.
  private synchronized Process start(ProcessBuilder builder) throws IOException {
    return builder.start();
  }

  @Override
  public Subprocess create(SubprocessBuilder params) throws IOException {
    ProcessBuilder builder = new ProcessBuilder();
    builder.command(params.getArgv());
    if (params.getEnv() != null) {
      builder.environment().clear();
      builder.environment().putAll(params.getEnv());
    }

    builder.redirectOutput(getRedirect(params.getStdout(), params.getStdoutFile()));
    builder.redirectError(getRedirect(params.getStderr(), params.getStderrFile()));
    builder.redirectErrorStream(params.redirectErrorStream());
    builder.directory(params.getWorkingDirectory());

    // Deadline is now + given timeout.
    long deadlineMillis = params.getTimeoutMillis() > 0
        ? Math.addExact(System.currentTimeMillis(), params.getTimeoutMillis())
        : 0;
    return new JavaSubprocess(start(builder), deadlineMillis);
  }

  /**
   * Returns a {@link java.lang.ProcessBuilder.Redirect} appropriate for the parameters. If a file
   * redirected to exists, deletes the file before redirecting to it.
   */
  private Redirect getRedirect(StreamAction action, File file) {
    switch (action) {
      case DISCARD:
        return Redirect.to(new File("/dev/null"));

      case REDIRECT:
        // We need to use Redirect.appendTo() here, because on older Linux kernels writes are
        // otherwise not atomic and might result in lost log messages:
        // https://lkml.org/lkml/2014/3/3/308
        if (file.exists()) {
          file.delete();
        }
        return Redirect.appendTo(file);

      case STREAM:
        return Redirect.PIPE;

      default:
        throw new IllegalStateException();
    }
  }
}