blob: 5fd62c86e4ff6c08826076271771446ee7f6ef8f [file] [log] [blame]
// Copyright 2015 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.worker;
import com.google.common.hash.HashCode;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.Path;
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.atomic.AtomicInteger;
/**
* Interface to a worker process running as a child process.
*
* <p>A worker process must follow this protocol to be usable via this class: The worker process is
* spawned on demand. The worker process is free to exit whenever necessary, as new instances will
* be relaunched automatically. Communication happens via the WorkerProtocol protobuf, sent to and
* received from the worker process via stdin / stdout.
*
* <p>Other code in Blaze can talk to the worker process via input / output streams provided by this
* class.
*/
final class Worker {
private static final AtomicInteger pidCounter = new AtomicInteger();
private final int workerId;
private final Process process;
private final Thread shutdownHook;
private final HashCode workerFilesHash;
private Worker(Process process, Thread shutdownHook, int pid, HashCode workerFilesHash) {
this.process = process;
this.shutdownHook = shutdownHook;
this.workerId = pid;
this.workerFilesHash = workerFilesHash;
}
static Worker create(WorkerKey key, Path logDir, Reporter reporter, boolean verbose)
throws IOException {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(logDir);
int workerId = pidCounter.getAndIncrement();
Path logFile = logDir.getRelative("worker-" + workerId + "-" + key.getMnemonic() + ".log");
String[] command = key.getArgs().toArray(new String[0]);
// Follows the logic of {@link com.google.devtools.build.lib.shell.Command}.
File executable = new File(command[0]);
if (!executable.isAbsolute() && executable.getParent() != null) {
command[0] = new File(key.getWorkDir().getPathFile(), command[0]).getAbsolutePath();
}
ProcessBuilder processBuilder =
new ProcessBuilder(command)
.directory(key.getWorkDir().getPathFile())
.redirectError(Redirect.appendTo(logFile.getPathFile()));
processBuilder.environment().clear();
processBuilder.environment().putAll(key.getEnv());
final Process process = processBuilder.start();
Thread shutdownHook =
new Thread() {
@Override
public void run() {
destroyProcess(process);
}
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
if (verbose) {
reporter.handle(
Event.info(
"Created new "
+ key.getMnemonic()
+ " worker (id "
+ workerId
+ "), logging to "
+ logFile));
}
return new Worker(process, shutdownHook, workerId, key.getWorkerFilesHash());
}
void destroy() {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
destroyProcess(process);
}
/**
* Destroys a process and waits for it to exit. This is necessary for the child to not become a
* zombie.
*
* @param process the process to destroy.
*/
private static void destroyProcess(Process process) {
boolean wasInterrupted = false;
try {
process.destroy();
while (true) {
try {
process.waitFor();
return;
} catch (InterruptedException ie) {
wasInterrupted = true;
}
}
} finally {
// Read this for detailed explanation: http://www.ibm.com/developerworks/library/j-jtp05236/
if (wasInterrupted) {
Thread.currentThread().interrupt(); // preserve interrupted status
}
}
}
/**
* Returns a unique id for this worker. This is used to distinguish different worker processes in
* logs and messages.
*/
int getWorkerId() {
return this.workerId;
}
HashCode getWorkerFilesHash() {
return workerFilesHash;
}
boolean isAlive() {
// This is horrible, but Process.isAlive() is only available from Java 8 on and this is the
// best we can do prior to that.
try {
process.exitValue();
return false;
} catch (IllegalThreadStateException e) {
return true;
}
}
InputStream getInputStream() {
return process.getInputStream();
}
OutputStream getOutputStream() {
return process.getOutputStream();
}
}