Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 1 | // Copyright 2023 Google Inc. All rights reserved. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package bazel |
| 16 | |
| 17 | import ( |
| 18 | "bytes" |
| 19 | "encoding/gob" |
| 20 | "fmt" |
| 21 | "net" |
| 22 | os_lib "os" |
| 23 | "os/exec" |
| 24 | "path/filepath" |
| 25 | "strings" |
| 26 | "time" |
| 27 | ) |
| 28 | |
Chris Parsons | c83398f | 2023-05-31 18:41:41 +0000 | [diff] [blame] | 29 | // Logs events of ProxyServer. |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 30 | type ServerLogger interface { |
| 31 | Fatal(v ...interface{}) |
| 32 | Fatalf(format string, v ...interface{}) |
Chris Parsons | c83398f | 2023-05-31 18:41:41 +0000 | [diff] [blame] | 33 | Println(v ...interface{}) |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 34 | } |
| 35 | |
| 36 | // CmdRequest is a request to the Bazel Proxy server. |
| 37 | type CmdRequest struct { |
| 38 | // Args to the Bazel command. |
| 39 | Argv []string |
| 40 | // Environment variables to pass to the Bazel invocation. Strings should be of |
| 41 | // the form "KEY=VALUE". |
| 42 | Env []string |
| 43 | } |
| 44 | |
| 45 | // CmdResponse is a response from the Bazel Proxy server. |
| 46 | type CmdResponse struct { |
| 47 | Stdout string |
| 48 | Stderr string |
| 49 | ErrorString string |
| 50 | } |
| 51 | |
| 52 | // ProxyClient is a client which can issue Bazel commands to the Bazel |
| 53 | // proxy server. Requests are issued (and responses received) via a unix socket. |
| 54 | // See ProxyServer for more details. |
| 55 | type ProxyClient struct { |
| 56 | outDir string |
| 57 | } |
| 58 | |
| 59 | // ProxyServer is a server which runs as a background goroutine. Each |
| 60 | // request to the server describes a Bazel command which the server should run. |
| 61 | // The server then issues the Bazel command, and returns a response describing |
| 62 | // the stdout/stderr of the command. |
| 63 | // Client-server communication is done via a unix socket under the output |
| 64 | // directory. |
| 65 | // The server is intended to circumvent sandboxing for subprocesses of the |
| 66 | // build. The build orchestrator (soong_ui) can launch a server to exist outside |
| 67 | // of sandboxing, and sandboxed processes (such as soong_build) can issue |
| 68 | // bazel commands through this socket tunnel. This allows a sandboxed process |
| 69 | // to issue bazel requests to a bazel that resides outside of sandbox. This |
| 70 | // is particularly useful to maintain a persistent Bazel server which lives |
| 71 | // past the duration of a single build. |
| 72 | // The ProxyServer will only live as long as soong_ui does; the |
| 73 | // underlying Bazel server will live past the duration of the build. |
| 74 | type ProxyServer struct { |
Chris Parsons | c83398f | 2023-05-31 18:41:41 +0000 | [diff] [blame] | 75 | logger ServerLogger |
| 76 | outDir string |
| 77 | workspaceDir string |
| 78 | bazeliskVersion string |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 79 | // The server goroutine will listen on this channel and stop handling requests |
| 80 | // once it is written to. |
| 81 | done chan struct{} |
| 82 | } |
| 83 | |
| 84 | // NewProxyClient is a constructor for a ProxyClient. |
| 85 | func NewProxyClient(outDir string) *ProxyClient { |
| 86 | return &ProxyClient{ |
| 87 | outDir: outDir, |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | func unixSocketPath(outDir string) string { |
| 92 | return filepath.Join(outDir, "bazelsocket.sock") |
| 93 | } |
| 94 | |
| 95 | // IssueCommand issues a request to the Bazel Proxy Server to issue a Bazel |
| 96 | // request. Returns a response describing the output from the Bazel process |
| 97 | // (if the Bazel process had an error, then the response will include an error). |
| 98 | // Returns an error if there was an issue with the connection to the Bazel Proxy |
| 99 | // server. |
| 100 | func (b *ProxyClient) IssueCommand(req CmdRequest) (CmdResponse, error) { |
| 101 | var resp CmdResponse |
| 102 | var err error |
| 103 | // Check for connections every 1 second. This is chosen to be a relatively |
| 104 | // short timeout, because the proxy server should accept requests quite |
| 105 | // quickly. |
| 106 | d := net.Dialer{Timeout: 1 * time.Second} |
| 107 | var conn net.Conn |
| 108 | conn, err = d.Dial("unix", unixSocketPath(b.outDir)) |
| 109 | if err != nil { |
| 110 | return resp, err |
| 111 | } |
| 112 | defer conn.Close() |
| 113 | |
| 114 | enc := gob.NewEncoder(conn) |
| 115 | if err = enc.Encode(req); err != nil { |
| 116 | return resp, err |
| 117 | } |
| 118 | dec := gob.NewDecoder(conn) |
| 119 | err = dec.Decode(&resp) |
| 120 | return resp, err |
| 121 | } |
| 122 | |
| 123 | // NewProxyServer is a constructor for a ProxyServer. |
Chris Parsons | c83398f | 2023-05-31 18:41:41 +0000 | [diff] [blame] | 124 | func NewProxyServer(logger ServerLogger, outDir string, workspaceDir string, bazeliskVersion string) *ProxyServer { |
| 125 | if len(bazeliskVersion) > 0 { |
| 126 | logger.Println("** Using Bazelisk for this build, due to env var USE_BAZEL_VERSION=" + bazeliskVersion + " **") |
| 127 | } |
| 128 | |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 129 | return &ProxyServer{ |
Chris Parsons | c83398f | 2023-05-31 18:41:41 +0000 | [diff] [blame] | 130 | logger: logger, |
| 131 | outDir: outDir, |
| 132 | workspaceDir: workspaceDir, |
| 133 | done: make(chan struct{}), |
| 134 | bazeliskVersion: bazeliskVersion, |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 135 | } |
| 136 | } |
| 137 | |
Chris Parsons | c9089dc | 2023-04-24 16:21:27 +0000 | [diff] [blame] | 138 | func ExecBazel(bazelPath string, workspaceDir string, request CmdRequest) (stdout []byte, stderr []byte, cmdErr error) { |
| 139 | bazelCmd := exec.Command(bazelPath, request.Argv...) |
| 140 | bazelCmd.Dir = workspaceDir |
| 141 | bazelCmd.Env = request.Env |
| 142 | |
| 143 | stderrBuffer := &bytes.Buffer{} |
| 144 | bazelCmd.Stderr = stderrBuffer |
| 145 | |
| 146 | if output, err := bazelCmd.Output(); err != nil { |
| 147 | cmdErr = fmt.Errorf("bazel command failed: %s\n---command---\n%s\n---env---\n%s\n---stderr---\n%s---", |
| 148 | err, bazelCmd, strings.Join(bazelCmd.Env, "\n"), stderrBuffer) |
| 149 | } else { |
| 150 | stdout = output |
| 151 | } |
| 152 | stderr = stderrBuffer.Bytes() |
| 153 | return |
| 154 | } |
| 155 | |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 156 | func (b *ProxyServer) handleRequest(conn net.Conn) error { |
| 157 | defer conn.Close() |
| 158 | |
| 159 | dec := gob.NewDecoder(conn) |
| 160 | var req CmdRequest |
| 161 | if err := dec.Decode(&req); err != nil { |
| 162 | return fmt.Errorf("Error decoding request: %s", err) |
| 163 | } |
| 164 | |
Chris Parsons | c83398f | 2023-05-31 18:41:41 +0000 | [diff] [blame] | 165 | if len(b.bazeliskVersion) > 0 { |
| 166 | req.Env = append(req.Env, "USE_BAZEL_VERSION="+b.bazeliskVersion) |
| 167 | } |
Chris Parsons | c9089dc | 2023-04-24 16:21:27 +0000 | [diff] [blame] | 168 | stdout, stderr, cmdErr := ExecBazel("./build/bazel/bin/bazel", b.workspaceDir, req) |
| 169 | errorString := "" |
| 170 | if cmdErr != nil { |
| 171 | errorString = cmdErr.Error() |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 172 | } |
| 173 | |
Chris Parsons | c9089dc | 2023-04-24 16:21:27 +0000 | [diff] [blame] | 174 | resp := CmdResponse{string(stdout), string(stderr), errorString} |
Chris Parsons | 9402ca8 | 2023-02-23 17:28:06 -0500 | [diff] [blame] | 175 | enc := gob.NewEncoder(conn) |
| 176 | if err := enc.Encode(&resp); err != nil { |
| 177 | return fmt.Errorf("Error encoding response: %s", err) |
| 178 | } |
| 179 | return nil |
| 180 | } |
| 181 | |
| 182 | func (b *ProxyServer) listenUntilClosed(listener net.Listener) error { |
| 183 | for { |
| 184 | // Check for connections every 1 second. This is a blocking operation, so |
| 185 | // if the server is closed, the goroutine will not fully close until this |
| 186 | // deadline is reached. Thus, this deadline is short (but not too short |
| 187 | // so that the routine churns). |
| 188 | listener.(*net.UnixListener).SetDeadline(time.Now().Add(time.Second)) |
| 189 | conn, err := listener.Accept() |
| 190 | |
| 191 | select { |
| 192 | case <-b.done: |
| 193 | return nil |
| 194 | default: |
| 195 | } |
| 196 | |
| 197 | if err != nil { |
| 198 | if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { |
| 199 | // Timeout is normal and expected while waiting for client to establish |
| 200 | // a connection. |
| 201 | continue |
| 202 | } else { |
| 203 | b.logger.Fatalf("Listener error: %s", err) |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | err = b.handleRequest(conn) |
| 208 | if err != nil { |
| 209 | b.logger.Fatal(err) |
| 210 | } |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | // Start initializes the server unix socket and (in a separate goroutine) |
| 215 | // handles requests on the socket until the server is closed. Returns an error |
| 216 | // if a failure occurs during initialization. Will log any post-initialization |
| 217 | // errors to the server's logger. |
| 218 | func (b *ProxyServer) Start() error { |
| 219 | unixSocketAddr := unixSocketPath(b.outDir) |
| 220 | if err := os_lib.RemoveAll(unixSocketAddr); err != nil { |
| 221 | return fmt.Errorf("couldn't remove socket '%s': %s", unixSocketAddr, err) |
| 222 | } |
| 223 | listener, err := net.Listen("unix", unixSocketAddr) |
| 224 | |
| 225 | if err != nil { |
| 226 | return fmt.Errorf("error listening on socket '%s': %s", unixSocketAddr, err) |
| 227 | } |
| 228 | |
| 229 | go b.listenUntilClosed(listener) |
| 230 | return nil |
| 231 | } |
| 232 | |
| 233 | // Close shuts down the server. This will stop the server from listening for |
| 234 | // additional requests. |
| 235 | func (b *ProxyServer) Close() { |
| 236 | b.done <- struct{}{} |
| 237 | } |