platform_build_soong/bazel/bazel_proxy.go

220 lines
6.6 KiB
Go
Raw Normal View History

// Copyright 2023 Google Inc. 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 bazel
import (
"bytes"
"encoding/gob"
"fmt"
"net"
os_lib "os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Logs fatal events of ProxyServer.
type ServerLogger interface {
Fatal(v ...interface{})
Fatalf(format string, v ...interface{})
}
// CmdRequest is a request to the Bazel Proxy server.
type CmdRequest struct {
// Args to the Bazel command.
Argv []string
// Environment variables to pass to the Bazel invocation. Strings should be of
// the form "KEY=VALUE".
Env []string
}
// CmdResponse is a response from the Bazel Proxy server.
type CmdResponse struct {
Stdout string
Stderr string
ErrorString string
}
// ProxyClient is a client which can issue Bazel commands to the Bazel
// proxy server. Requests are issued (and responses received) via a unix socket.
// See ProxyServer for more details.
type ProxyClient struct {
outDir string
}
// ProxyServer is a server which runs as a background goroutine. Each
// request to the server describes a Bazel command which the server should run.
// The server then issues the Bazel command, and returns a response describing
// the stdout/stderr of the command.
// Client-server communication is done via a unix socket under the output
// directory.
// The server is intended to circumvent sandboxing for subprocesses of the
// build. The build orchestrator (soong_ui) can launch a server to exist outside
// of sandboxing, and sandboxed processes (such as soong_build) can issue
// bazel commands through this socket tunnel. This allows a sandboxed process
// to issue bazel requests to a bazel that resides outside of sandbox. This
// is particularly useful to maintain a persistent Bazel server which lives
// past the duration of a single build.
// The ProxyServer will only live as long as soong_ui does; the
// underlying Bazel server will live past the duration of the build.
type ProxyServer struct {
logger ServerLogger
outDir string
workspaceDir string
// The server goroutine will listen on this channel and stop handling requests
// once it is written to.
done chan struct{}
}
// NewProxyClient is a constructor for a ProxyClient.
func NewProxyClient(outDir string) *ProxyClient {
return &ProxyClient{
outDir: outDir,
}
}
func unixSocketPath(outDir string) string {
return filepath.Join(outDir, "bazelsocket.sock")
}
// IssueCommand issues a request to the Bazel Proxy Server to issue a Bazel
// request. Returns a response describing the output from the Bazel process
// (if the Bazel process had an error, then the response will include an error).
// Returns an error if there was an issue with the connection to the Bazel Proxy
// server.
func (b *ProxyClient) IssueCommand(req CmdRequest) (CmdResponse, error) {
var resp CmdResponse
var err error
// Check for connections every 1 second. This is chosen to be a relatively
// short timeout, because the proxy server should accept requests quite
// quickly.
d := net.Dialer{Timeout: 1 * time.Second}
var conn net.Conn
conn, err = d.Dial("unix", unixSocketPath(b.outDir))
if err != nil {
return resp, err
}
defer conn.Close()
enc := gob.NewEncoder(conn)
if err = enc.Encode(req); err != nil {
return resp, err
}
dec := gob.NewDecoder(conn)
err = dec.Decode(&resp)
return resp, err
}
// NewProxyServer is a constructor for a ProxyServer.
func NewProxyServer(logger ServerLogger, outDir string, workspaceDir string) *ProxyServer {
return &ProxyServer{
logger: logger,
outDir: outDir,
workspaceDir: workspaceDir,
done: make(chan struct{}),
}
}
func (b *ProxyServer) handleRequest(conn net.Conn) error {
defer conn.Close()
dec := gob.NewDecoder(conn)
var req CmdRequest
if err := dec.Decode(&req); err != nil {
return fmt.Errorf("Error decoding request: %s", err)
}
bazelCmd := exec.Command("./build/bazel/bin/bazel", req.Argv...)
bazelCmd.Dir = b.workspaceDir
bazelCmd.Env = req.Env
stderr := &bytes.Buffer{}
bazelCmd.Stderr = stderr
var stdout string
var bazelErrString string
if output, err := bazelCmd.Output(); err != nil {
bazelErrString = fmt.Sprintf("bazel command failed: %s\n---command---\n%s\n---env---\n%s\n---stderr---\n%s---",
err, bazelCmd, strings.Join(bazelCmd.Env, "\n"), stderr)
} else {
stdout = string(output)
}
resp := CmdResponse{stdout, string(stderr.Bytes()), bazelErrString}
enc := gob.NewEncoder(conn)
if err := enc.Encode(&resp); err != nil {
return fmt.Errorf("Error encoding response: %s", err)
}
return nil
}
func (b *ProxyServer) listenUntilClosed(listener net.Listener) error {
for {
// Check for connections every 1 second. This is a blocking operation, so
// if the server is closed, the goroutine will not fully close until this
// deadline is reached. Thus, this deadline is short (but not too short
// so that the routine churns).
listener.(*net.UnixListener).SetDeadline(time.Now().Add(time.Second))
conn, err := listener.Accept()
select {
case <-b.done:
return nil
default:
}
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
// Timeout is normal and expected while waiting for client to establish
// a connection.
continue
} else {
b.logger.Fatalf("Listener error: %s", err)
}
}
err = b.handleRequest(conn)
if err != nil {
b.logger.Fatal(err)
}
}
}
// Start initializes the server unix socket and (in a separate goroutine)
// handles requests on the socket until the server is closed. Returns an error
// if a failure occurs during initialization. Will log any post-initialization
// errors to the server's logger.
func (b *ProxyServer) Start() error {
unixSocketAddr := unixSocketPath(b.outDir)
if err := os_lib.RemoveAll(unixSocketAddr); err != nil {
return fmt.Errorf("couldn't remove socket '%s': %s", unixSocketAddr, err)
}
listener, err := net.Listen("unix", unixSocketAddr)
if err != nil {
return fmt.Errorf("error listening on socket '%s': %s", unixSocketAddr, err)
}
go b.listenUntilClosed(listener)
return nil
}
// Close shuts down the server. This will stop the server from listening for
// additional requests.
func (b *ProxyServer) Close() {
b.done <- struct{}{}
}