| 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 | } |