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 15package bazel 16 17import ( 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 29// Logs events of ProxyServer. 30type ServerLogger interface { 31 Fatal(v ...interface{}) 32 Fatalf(format string, v ...interface{}) 33 Println(v ...interface{}) 34} 35 36// CmdRequest is a request to the Bazel Proxy server. 37type 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. 46type 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. 55type 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. 74type ProxyServer struct { 75 logger ServerLogger 76 outDir string 77 workspaceDir string 78 bazeliskVersion string 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. 85func NewProxyClient(outDir string) *ProxyClient { 86 return &ProxyClient{ 87 outDir: outDir, 88 } 89} 90 91func 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. 100func (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. 124func 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 129 return &ProxyServer{ 130 logger: logger, 131 outDir: outDir, 132 workspaceDir: workspaceDir, 133 done: make(chan struct{}), 134 bazeliskVersion: bazeliskVersion, 135 } 136} 137 138func 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 156func (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 165 if len(b.bazeliskVersion) > 0 { 166 req.Env = append(req.Env, "USE_BAZEL_VERSION="+b.bazeliskVersion) 167 } 168 stdout, stderr, cmdErr := ExecBazel("./build/bazel/bin/bazel", b.workspaceDir, req) 169 errorString := "" 170 if cmdErr != nil { 171 errorString = cmdErr.Error() 172 } 173 174 resp := CmdResponse{string(stdout), string(stderr), errorString} 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 182func (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. 218func (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. 235func (b *ProxyServer) Close() { 236 b.done <- struct{}{} 237} 238