blob: b98119cec268819b2aa9e9df70ce345c81569eca [file] [log] [blame]
// Copyright 2019 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.sandbox;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.build.lib.shell.Subprocess;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* A sandboxfs implementation that uses an external sandboxfs binary to manage the mount point.
*
* <p>This implementation provides support for the reconfiguration protocol introduced in 0.2.0.
*/
final class RealSandboxfs02Process extends RealSandboxfsProcess {
private static final Logger log = Logger.getLogger(RealSandboxfsProcess.class.getName());
/**
* Writer with which to send data to the sandboxfs instance. Null only after {@link #destroy()}
* has been invoked.
*/
@GuardedBy("this")
private JsonWriter processStdIn;
/**
* Collection of active reconfiguration requests.
*
* <p>Each entry in this map is keyed by the identifier of the sandbox being affected by a
* reconfiguration request and points to a future that is set when the request completes.
*
* <p>New entries can be added to this map at any time, but only before {@link #destroy} is
* called. Once the sandboxfs instance has been destroyed, we do not expect any new requests to
* come in. However, existing requests will be drained by the {@link ResponsesReader} thread.
*/
private final ConcurrentMap<String, SettableFuture<Void>> inFlightRequests =
new ConcurrentHashMap<>();
/**
* Thread that reads responses from sandboxfs and dispatches them to the futures maintained by
* {@link #inFlightRequests}.
*/
private final Thread responsesReader;
/** Representation of a response returned by sandboxfs. */
private static class Response {
/** Identifier given in the request. Null if this carries a fatal error. */
@Nullable final String id;
/**
* Error message returned by sandboxfs if not null. If {@link #id} is not null, then this error
* corresponds to a specific request and is recoverable. Otherwise corresponds to a fatal
* condition, in which case sandboxfs will have stopped listening for requests.
*/
@Nullable final String error;
/** Constructs a new response with the given values. */
Response(@Nullable String id, @Nullable String error) {
this.id = id;
this.error = error;
}
}
/**
* A thread that reads responses from the sandboxfs output stream and dispatches them to the
* futures awaiting for them.
*/
private static class ResponsesReader extends Thread {
private final JsonReader reader;
private final ConcurrentMap<String, SettableFuture<Void>> inFlightRequests;
ResponsesReader(
JsonReader reader, ConcurrentMap<String, SettableFuture<Void>> inFlightRequests) {
this.reader = reader;
this.inFlightRequests = inFlightRequests;
}
/** Waits for responses and dispatches them. */
private void processResponses() throws IOException {
while (!Thread.interrupted() && reader.peek() != JsonToken.END_DOCUMENT) {
Response response = readResponse(reader);
if (response.id == null) {
// Non-recoverable error: abort.
throw new IOException(response.error != null ? response.error : "No error reported");
}
SettableFuture<Void> future = inFlightRequests.remove(response.id);
if (future == null) {
throw new IOException("sandboxfs returned response for unknown id " + response.id);
}
if (response.error == null) {
future.set(null);
} else {
future.setException(new IOException(response.error));
}
}
}
@Override
public void run() {
try {
processResponses();
} catch (EOFException e) {
// OK, nothing to do.
} catch (IOException e) {
log.log(Level.WARNING, "Failed to read responses from sandboxfs", e);
}
// sandboxfs has either replied with an unrecoverable error or has stopped providing
// responses. Either way, we have to clean up any pending in-flight requests to unblock the
// threads waiting for them.
//
// Given that we only get here once destroy() has been called, we do not expect any new
// requests to show up in the inFlightRequests map. This is why we do not synchronize
// accesses to the map during the iteration.
while (!inFlightRequests.isEmpty()) {
Iterator<Map.Entry<String, SettableFuture<Void>>> iter =
inFlightRequests.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, SettableFuture<Void>> entry = iter.next();
entry.getValue().cancel(true);
iter.remove();
}
}
}
}
/**
* Initializes a new sandboxfs process instance.
*
* @param process process handle for the already-running sandboxfs instance
*/
RealSandboxfs02Process(Path mountPoint, Subprocess process) {
super(mountPoint, process);
this.processStdIn =
new JsonWriter(
new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), UTF_8)));
JsonReader processStdOut =
new JsonReader(new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8)));
// Must use lenient writing and parsing to accept a stream of separate top-level JSON objects.
this.processStdIn.setLenient(true);
processStdOut.setLenient(true);
responsesReader = new ResponsesReader(processStdOut, inFlightRequests);
responsesReader.start();
}
@Override
public synchronized void destroy() {
super.destroy();
responsesReader.interrupt();
try {
responsesReader.join();
} catch (InterruptedException e) {
log.warning("Interrupted while waiting for responses processor thread");
Thread.currentThread().interrupt();
}
processStdIn = null;
}
/**
* Waits for a single response from sandboxfs and returns it.
*
* @param input the stream connected to sandboxfs's stdout
* @return the response obtained from the stream
* @throws IOException if sandboxfs fails to read from the stream for any reason, including EOF
*/
private static Response readResponse(JsonReader input) throws IOException {
input.beginObject();
String id = null;
String error = null;
while (input.hasNext()) {
String name = input.nextName();
switch (name) {
case "error":
if (input.peek() == JsonToken.NULL) {
input.nextNull();
} else {
checkState(error == null);
error = input.nextString();
}
break;
case "id":
if (input.peek() == JsonToken.NULL) {
input.nextNull();
} else {
checkState(id == null);
id = input.nextString();
}
break;
default:
throw new IOException("Invalid field name in response: " + name);
}
}
input.endObject();
return new Response(id, error);
}
/**
* Registers a new in-flight operation for the given sandbox identifier.
*
* <p>The caller must wait for the returned operation using {@link #waitForRequest}.
*
* @param id the identifier of the sandbox for which the request will be issued. There can only be
* one in-flight request per identifier.
* @return the future used to wait for the request's completion
*/
private SettableFuture<Void> newRequest(String id) {
SettableFuture<Void> future = SettableFuture.create();
SettableFuture<Void> other = inFlightRequests.put(id, future);
checkState(other == null, "Cannot have two in-flight requests for sandbox '%s'", id);
return future;
}
/**
* Waits for a request to complete and unregisters its in-flight operation.
*
* @param future the value returned by {@link #newRequest}.
* @throws IOException if the request cannot be waited for or if it raised an error
*/
private static void waitForRequest(SettableFuture<Void> future) throws IOException {
try {
future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new AssertionError("Unexpected exception type thrown by readResponse()", cause);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for sandboxfs response");
}
}
/** Encodes a mapping into JSON. */
@SuppressWarnings("UnnecessaryParentheses")
private static void writeMapping(JsonWriter writer, Mapping mapping) throws IOException {
writer.beginObject();
{
writer.name("path");
writer.value(mapping.path().getPathString());
writer.name("underlying_path");
writer.value(mapping.target().getPathString());
writer.name("writable");
writer.value(mapping.writable());
}
writer.endObject();
}
@Override
@SuppressWarnings("UnnecessaryParentheses")
public void createSandbox(String id, List<Mapping> mappings) throws IOException {
checkArgument(!PathFragment.containsSeparator(id));
SettableFuture<Void> future = newRequest(id);
synchronized (this) {
processStdIn.beginObject();
{
processStdIn.name("CreateSandbox");
processStdIn.beginObject();
{
processStdIn.name("id");
processStdIn.value(id);
processStdIn.name("mappings");
processStdIn.beginArray();
for (Mapping mapping : mappings) {
writeMapping(processStdIn, mapping);
}
processStdIn.endArray();
}
processStdIn.endObject();
}
processStdIn.endObject();
processStdIn.flush();
}
waitForRequest(future);
}
@Override
@SuppressWarnings("UnnecessaryParentheses")
public void destroySandbox(String id) throws IOException {
checkArgument(!PathFragment.containsSeparator(id));
SettableFuture<Void> future = newRequest(id);
synchronized (this) {
processStdIn.beginObject();
{
processStdIn.name("DestroySandbox");
processStdIn.value(id);
}
processStdIn.endObject();
processStdIn.flush();
}
waitForRequest(future);
}
}