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