|
@@ -0,0 +1,219 @@
|
|
|
+// 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{}{}
|
|
|
+}
|