blob: 229818da00927228145891a3d0b90d8b416a5ae1 [file] [log] [blame]
Chris Parsons9402ca82023-02-23 17:28:06 -05001// 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
Chris Parsonsc83398f2023-05-31 18:41:41 +000029// Logs events of ProxyServer.
Chris Parsons9402ca82023-02-23 17:28:06 -050030type ServerLogger interface {
31 Fatal(v ...interface{})
32 Fatalf(format string, v ...interface{})
Chris Parsonsc83398f2023-05-31 18:41:41 +000033 Println(v ...interface{})
Chris Parsons9402ca82023-02-23 17:28:06 -050034}
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 {
Chris Parsonsc83398f2023-05-31 18:41:41 +000075 logger ServerLogger
76 outDir string
77 workspaceDir string
78 bazeliskVersion string
Chris Parsons9402ca82023-02-23 17:28:06 -050079 // 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.
Chris Parsonsc83398f2023-05-31 18:41:41 +0000124func 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 Parsons9402ca82023-02-23 17:28:06 -0500129 return &ProxyServer{
Chris Parsonsc83398f2023-05-31 18:41:41 +0000130 logger: logger,
131 outDir: outDir,
132 workspaceDir: workspaceDir,
133 done: make(chan struct{}),
134 bazeliskVersion: bazeliskVersion,
Chris Parsons9402ca82023-02-23 17:28:06 -0500135 }
136}
137
Chris Parsonsc9089dc2023-04-24 16:21:27 +0000138func 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 Parsons9402ca82023-02-23 17:28:06 -0500156func (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 Parsonsc83398f2023-05-31 18:41:41 +0000165 if len(b.bazeliskVersion) > 0 {
166 req.Env = append(req.Env, "USE_BAZEL_VERSION="+b.bazeliskVersion)
167 }
Chris Parsonsc9089dc2023-04-24 16:21:27 +0000168 stdout, stderr, cmdErr := ExecBazel("./build/bazel/bin/bazel", b.workspaceDir, req)
169 errorString := ""
170 if cmdErr != nil {
171 errorString = cmdErr.Error()
Chris Parsons9402ca82023-02-23 17:28:06 -0500172 }
173
Chris Parsonsc9089dc2023-04-24 16:21:27 +0000174 resp := CmdResponse{string(stdout), string(stderr), errorString}
Chris Parsons9402ca82023-02-23 17:28:06 -0500175 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}