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 fatal events of ProxyServer. 30type ServerLogger interface { 31 Fatal(v ...interface{}) 32 Fatalf(format string, v ...interface{}) 33} 34 35// CmdRequest is a request to the Bazel Proxy server. 36type CmdRequest struct { 37 // Args to the Bazel command. 38 Argv []string 39 // Environment variables to pass to the Bazel invocation. Strings should be of 40 // the form "KEY=VALUE". 41 Env []string 42} 43 44// CmdResponse is a response from the Bazel Proxy server. 45type CmdResponse struct { 46 Stdout string 47 Stderr string 48 ErrorString string 49} 50 51// ProxyClient is a client which can issue Bazel commands to the Bazel 52// proxy server. Requests are issued (and responses received) via a unix socket. 53// See ProxyServer for more details. 54type ProxyClient struct { 55 outDir string 56} 57 58// ProxyServer is a server which runs as a background goroutine. Each 59// request to the server describes a Bazel command which the server should run. 60// The server then issues the Bazel command, and returns a response describing 61// the stdout/stderr of the command. 62// Client-server communication is done via a unix socket under the output 63// directory. 64// The server is intended to circumvent sandboxing for subprocesses of the 65// build. The build orchestrator (soong_ui) can launch a server to exist outside 66// of sandboxing, and sandboxed processes (such as soong_build) can issue 67// bazel commands through this socket tunnel. This allows a sandboxed process 68// to issue bazel requests to a bazel that resides outside of sandbox. This 69// is particularly useful to maintain a persistent Bazel server which lives 70// past the duration of a single build. 71// The ProxyServer will only live as long as soong_ui does; the 72// underlying Bazel server will live past the duration of the build. 73type ProxyServer struct { 74 logger ServerLogger 75 outDir string 76 workspaceDir string 77 // The server goroutine will listen on this channel and stop handling requests 78 // once it is written to. 79 done chan struct{} 80} 81 82// NewProxyClient is a constructor for a ProxyClient. 83func NewProxyClient(outDir string) *ProxyClient { 84 return &ProxyClient{ 85 outDir: outDir, 86 } 87} 88 89func unixSocketPath(outDir string) string { 90 return filepath.Join(outDir, "bazelsocket.sock") 91} 92 93// IssueCommand issues a request to the Bazel Proxy Server to issue a Bazel 94// request. Returns a response describing the output from the Bazel process 95// (if the Bazel process had an error, then the response will include an error). 96// Returns an error if there was an issue with the connection to the Bazel Proxy 97// server. 98func (b *ProxyClient) IssueCommand(req CmdRequest) (CmdResponse, error) { 99 var resp CmdResponse 100 var err error 101 // Check for connections every 1 second. This is chosen to be a relatively 102 // short timeout, because the proxy server should accept requests quite 103 // quickly. 104 d := net.Dialer{Timeout: 1 * time.Second} 105 var conn net.Conn 106 conn, err = d.Dial("unix", unixSocketPath(b.outDir)) 107 if err != nil { 108 return resp, err 109 } 110 defer conn.Close() 111 112 enc := gob.NewEncoder(conn) 113 if err = enc.Encode(req); err != nil { 114 return resp, err 115 } 116 dec := gob.NewDecoder(conn) 117 err = dec.Decode(&resp) 118 return resp, err 119} 120 121// NewProxyServer is a constructor for a ProxyServer. 122func NewProxyServer(logger ServerLogger, outDir string, workspaceDir string) *ProxyServer { 123 return &ProxyServer{ 124 logger: logger, 125 outDir: outDir, 126 workspaceDir: workspaceDir, 127 done: make(chan struct{}), 128 } 129} 130 131func (b *ProxyServer) handleRequest(conn net.Conn) error { 132 defer conn.Close() 133 134 dec := gob.NewDecoder(conn) 135 var req CmdRequest 136 if err := dec.Decode(&req); err != nil { 137 return fmt.Errorf("Error decoding request: %s", err) 138 } 139 140 bazelCmd := exec.Command("./build/bazel/bin/bazel", req.Argv...) 141 bazelCmd.Dir = b.workspaceDir 142 bazelCmd.Env = req.Env 143 144 stderr := &bytes.Buffer{} 145 bazelCmd.Stderr = stderr 146 var stdout string 147 var bazelErrString string 148 149 if output, err := bazelCmd.Output(); err != nil { 150 bazelErrString = fmt.Sprintf("bazel command failed: %s\n---command---\n%s\n---env---\n%s\n---stderr---\n%s---", 151 err, bazelCmd, strings.Join(bazelCmd.Env, "\n"), stderr) 152 } else { 153 stdout = string(output) 154 } 155 156 resp := CmdResponse{stdout, string(stderr.Bytes()), bazelErrString} 157 enc := gob.NewEncoder(conn) 158 if err := enc.Encode(&resp); err != nil { 159 return fmt.Errorf("Error encoding response: %s", err) 160 } 161 return nil 162} 163 164func (b *ProxyServer) listenUntilClosed(listener net.Listener) error { 165 for { 166 // Check for connections every 1 second. This is a blocking operation, so 167 // if the server is closed, the goroutine will not fully close until this 168 // deadline is reached. Thus, this deadline is short (but not too short 169 // so that the routine churns). 170 listener.(*net.UnixListener).SetDeadline(time.Now().Add(time.Second)) 171 conn, err := listener.Accept() 172 173 select { 174 case <-b.done: 175 return nil 176 default: 177 } 178 179 if err != nil { 180 if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { 181 // Timeout is normal and expected while waiting for client to establish 182 // a connection. 183 continue 184 } else { 185 b.logger.Fatalf("Listener error: %s", err) 186 } 187 } 188 189 err = b.handleRequest(conn) 190 if err != nil { 191 b.logger.Fatal(err) 192 } 193 } 194} 195 196// Start initializes the server unix socket and (in a separate goroutine) 197// handles requests on the socket until the server is closed. Returns an error 198// if a failure occurs during initialization. Will log any post-initialization 199// errors to the server's logger. 200func (b *ProxyServer) Start() error { 201 unixSocketAddr := unixSocketPath(b.outDir) 202 if err := os_lib.RemoveAll(unixSocketAddr); err != nil { 203 return fmt.Errorf("couldn't remove socket '%s': %s", unixSocketAddr, err) 204 } 205 listener, err := net.Listen("unix", unixSocketAddr) 206 207 if err != nil { 208 return fmt.Errorf("error listening on socket '%s': %s", unixSocketAddr, err) 209 } 210 211 go b.listenUntilClosed(listener) 212 return nil 213} 214 215// Close shuts down the server. This will stop the server from listening for 216// additional requests. 217func (b *ProxyServer) Close() { 218 b.done <- struct{}{} 219} 220