| // 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(); |
| } |
| } |
| } |