blob: 2940b999550c521a6a4501e1ec6aac12fd1d513c [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
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
Chris Parsonsc9089dc2023-04-24 16:21:27 +0000131func ExecBazel(bazelPath string, workspaceDir string, request CmdRequest) (stdout []byte, stderr []byte, cmdErr error) {
132 bazelCmd := exec.Command(bazelPath, request.Argv...)
133 bazelCmd.Dir = workspaceDir
134 bazelCmd.Env = request.Env
135
136 stderrBuffer := &bytes.Buffer{}
137 bazelCmd.Stderr = stderrBuffer
138
139 if output, err := bazelCmd.Output(); err != nil {
140 cmdErr = fmt.Errorf("bazel command failed: %s\n---command---\n%s\n---env---\n%s\n---stderr---\n%s---",
141 err, bazelCmd, strings.Join(bazelCmd.Env, "\n"), stderrBuffer)
142 } else {
143 stdout = output
144 }
145 stderr = stderrBuffer.Bytes()
146 return
147}
148
Chris Parsons9402ca82023-02-23 17:28:06 -0500149func (b *ProxyServer) handleRequest(conn net.Conn) error {
150 defer conn.Close()
151
152 dec := gob.NewDecoder(conn)
153 var req CmdRequest
154 if err := dec.Decode(&req); err != nil {
155 return fmt.Errorf("Error decoding request: %s", err)
156 }
157
Chris Parsonsc9089dc2023-04-24 16:21:27 +0000158 stdout, stderr, cmdErr := ExecBazel("./build/bazel/bin/bazel", b.workspaceDir, req)
159 errorString := ""
160 if cmdErr != nil {
161 errorString = cmdErr.Error()
Chris Parsons9402ca82023-02-23 17:28:06 -0500162 }
163
Chris Parsonsc9089dc2023-04-24 16:21:27 +0000164 resp := CmdResponse{string(stdout), string(stderr), errorString}
Chris Parsons9402ca82023-02-23 17:28:06 -0500165 enc := gob.NewEncoder(conn)
166 if err := enc.Encode(&resp); err != nil {
167 return fmt.Errorf("Error encoding response: %s", err)
168 }
169 return nil
170}
171
172func (b *ProxyServer) listenUntilClosed(listener net.Listener) error {
173 for {
174 // Check for connections every 1 second. This is a blocking operation, so
175 // if the server is closed, the goroutine will not fully close until this
176 // deadline is reached. Thus, this deadline is short (but not too short
177 // so that the routine churns).
178 listener.(*net.UnixListener).SetDeadline(time.Now().Add(time.Second))
179 conn, err := listener.Accept()
180
181 select {
182 case <-b.done:
183 return nil
184 default:
185 }
186
187 if err != nil {
188 if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
189 // Timeout is normal and expected while waiting for client to establish
190 // a connection.
191 continue
192 } else {
193 b.logger.Fatalf("Listener error: %s", err)
194 }
195 }
196
197 err = b.handleRequest(conn)
198 if err != nil {
199 b.logger.Fatal(err)
200 }
201 }
202}
203
204// Start initializes the server unix socket and (in a separate goroutine)
205// handles requests on the socket until the server is closed. Returns an error
206// if a failure occurs during initialization. Will log any post-initialization
207// errors to the server's logger.
208func (b *ProxyServer) Start() error {
209 unixSocketAddr := unixSocketPath(b.outDir)
210 if err := os_lib.RemoveAll(unixSocketAddr); err != nil {
211 return fmt.Errorf("couldn't remove socket '%s': %s", unixSocketAddr, err)
212 }
213 listener, err := net.Listen("unix", unixSocketAddr)
214
215 if err != nil {
216 return fmt.Errorf("error listening on socket '%s': %s", unixSocketAddr, err)
217 }
218
219 go b.listenUntilClosed(listener)
220 return nil
221}
222
223// Close shuts down the server. This will stop the server from listening for
224// additional requests.
225func (b *ProxyServer) Close() {
226 b.done <- struct{}{}
227}