com.google.devtools.build.lib.sandbox.RealSandboxfsProcess.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.lib.sandbox.RealSandboxfsProcess.java

Source

// Copyright 2018 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.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.shell.Subprocess;
import com.google.devtools.build.lib.shell.SubprocessBuilder;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;

/** A sandboxfs implementation that uses an external sandboxfs binary to manage the mount point. */
final class RealSandboxfsProcess implements SandboxfsProcess {
    private static final Logger log = Logger.getLogger(RealSandboxfsProcess.class.getName());

    /** Directory on which the sandboxfs is serving. */
    private final Path mountPoint;

    /**
     * Process handle to the sandboxfs instance.  Null only after {@link #destroy()} has been invoked.
     */
    private @Nullable Subprocess process;

    /**
     * Writer with which to send data to the sandboxfs instance.  Null only after {@link #destroy()}
     * has been invoked.
     */
    private @Nullable BufferedWriter processStdIn;

    /**
     * Reader with which to receive data from the sandboxfs instance.  Null only after
     * {@link #destroy()} has been invoked.
     */
    private @Nullable BufferedReader processStdOut;

    /**
     * Shutdown hook to stop the sandboxfs instance on abrupt termination.  Null only after
     * {@link #destroy()} has been invoked.
     */
    private @Nullable Thread shutdownHook;

    /**
     * Initializes a new sandboxfs process instance.
     *
     * @param process process handle for the already-running sandboxfs instance
     */
    private RealSandboxfsProcess(Path mountPoint, Subprocess process) {
        this.mountPoint = mountPoint;

        this.process = process;
        this.processStdIn = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), UTF_8));
        this.processStdOut = new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8));

        this.shutdownHook = new Thread(() -> {
            try {
                this.destroy();
            } catch (Exception e) {
                log.warning("Failed to destroy running sandboxfs instance; mount point may have "
                        + "been left behind: " + e);
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    /**
     * Mounts a new sandboxfs instance.
     *
     * <p>The root of the file system instance is left unmapped which means that it remains as
     * read-only throughout the lifetime of this instance.  Writable subdirectories can later be
     * mapped via {@link #map(List)}.
     *
     * @param binary path to the sandboxfs binary.  This is a {@link PathFragment} and not a
     *     {@link Path} because we want to support "bare" (non-absolute) names for the location of
     *     the sandboxfs binary; such names are automatically looked for in the {@code PATH}.
     * @param mountPoint directory on which to mount the sandboxfs instance
     * @param logFile path to the file that will receive all sandboxfs logging output
     * @return a new handle that represents the running process
     * @throws IOException if there is a problem starting the process
     */
    static SandboxfsProcess mount(PathFragment binary, Path mountPoint, Path logFile) throws IOException {
        log.info("Mounting sandboxfs (" + binary + ") onto " + mountPoint);

        // TODO(jmmv): Before starting a sandboxfs serving instance, we must query the current version
        // of sandboxfs and check if we support its communication protocol.

        ImmutableList.Builder<String> argvBuilder = ImmutableList.builder();

        argvBuilder.add(binary.getPathString());

        // On macOS, we need to allow users other than self to access the sandboxfs instance.  This is
        // necessary because macOS's amfid, which runs as root, has to have access to the binaries
        // within the sandbox in order to validate signatures. See:
        // http://julio.meroh.net/2017/10/fighting-execs-sandboxfs-macos.html
        argvBuilder.add(OS.getCurrent() == OS.DARWIN ? "--allow=other" : "--allow=self");

        // TODO(jmmv): Pass flags to enable sandboxfs' debugging support (--listen_address and --debug)
        // when requested by the user via --sandbox_debug.  Tricky because we have to figure out how to
        // deal with port numbers (which sandboxfs can autoassign, but doesn't currently promise a way
        // to tell us back what it picked).

        argvBuilder.add(mountPoint.getPathString());

        SubprocessBuilder processBuilder = new SubprocessBuilder();
        processBuilder.setArgv(argvBuilder.build());
        processBuilder.setStderr(logFile.getPathFile());
        processBuilder.setEnv(ImmutableMap.of(
                // sandboxfs may need to locate fusermount depending on the FUSE implementation so pass the
                // PATH to the subprocess (which we assume is sufficient).
                "PATH", System.getenv("PATH")));

        Subprocess process = processBuilder.start();
        RealSandboxfsProcess sandboxfs = new RealSandboxfsProcess(mountPoint, process);
        // TODO(jmmv): We should have a better mechanism to wait for sandboxfs to start successfully but
        // sandboxfs currently provides no interface to do so.  Just try to push an empty configuration
        // and see if it works.
        try {
            sandboxfs.reconfigure("[]\n\n");
        } catch (IOException e) {
            destroyProcess(process);
            throw new IOException("sandboxfs failed to start", e);
        }
        return sandboxfs;
    }

    @Override
    public Path getMountPoint() {
        return mountPoint;
    }

    @Override
    public boolean isAlive() {
        return process != null && !process.finished();
    }

    /**
     * Destroys a process and waits for it to exit.
     *
     * @param process the process to destroy.
     */
    // TODO(jmmv): This is adapted from Worker.java. Should probably replace both with a new variant
    // of Uninterruptibles.callUninterruptibly that takes a lambda instead of a callable.
    private static void destroyProcess(Subprocess process) {
        process.destroy();

        boolean interrupted = false;
        try {
            while (true) {
                try {
                    process.waitFor();
                    return;
                } catch (InterruptedException ie) {
                    interrupted = true;
                }
            }
        } finally {
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public synchronized void destroy() {
        if (shutdownHook != null) {
            Runtime.getRuntime().removeShutdownHook(shutdownHook);
            shutdownHook = null;
        }

        if (processStdIn != null) {
            try {
                processStdIn.close();
            } catch (IOException e) {
                log.warning("Failed to close sandboxfs's stdin pipe: " + e);
            }
            processStdIn = null;
        }

        if (processStdOut != null) {
            try {
                processStdOut.close();
            } catch (IOException e) {
                log.warning("Failed to close sandboxfs's stdout pipe: " + e);
            }
            processStdOut = null;
        }

        if (process != null) {
            destroyProcess(process);
            process = null;
        }
    }

    /**
     * Pushes a new configuration to sandboxfs and waits for acceptance.
     *
     * @param config the configuration chunk to push to sandboxfs
     * @throws IOException if sandboxfs cannot be reconfigured either because of an error in the
     *     configuration or because we failed to communicate with the subprocess
     */
    private synchronized void reconfigure(String config) throws IOException {
        checkNotNull(processStdIn, "sandboxfs already has been destroyed");
        processStdIn.write(config);
        processStdIn.flush();

        checkNotNull(processStdOut, "sandboxfs has already been destroyed");
        String done = processStdOut.readLine();
        if (done == null) {
            throw new IOException("premature end of output from sandboxfs");
        }
        if (!done.equals("Done")) {
            throw new IOException("received unknown string from sandboxfs: " + done + "; expected Done");
        }
    }

    @Override
    public void map(List<Mapping> mappings) throws IOException {
        Function<Mapping, String> formatMapping = (mapping) -> String.format(
                "{\"Map\": {\"Mapping\": \"%s\", \"Target\": \"%s\", \"Writable\": %s}}", mapping.path(),
                mapping.target(), mapping.writable() ? "true" : "false");

        StringBuilder sb = new StringBuilder();
        sb.append("[\n");
        sb.append(mappings.stream().map(formatMapping).collect(Collectors.joining(",\n")));
        sb.append("]\n\n");
        reconfigure(sb.toString());
    }

    @Override
    public void unmap(PathFragment mapping) throws IOException {
        reconfigure(String.format("[{\"Unmap\": \"%s\"}]\n\n", mapping));
    }
}