go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/buildproxywrap/relay.go (about) 1 // Copyright 2023 The Fuchsia Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can 3 // found in the LICENSE file. 4 5 // 'relay.go' provides a simple interface for running a socket relay 6 // to a remote server, using the 'socat' tool. 7 package main 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "io/fs" 14 "net" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "sync" 19 "time" 20 21 "github.com/golang/glog" 22 ) 23 24 // socketRelay contains the parameters for operating a single socket relay. 25 type socketRelay struct { 26 // Identifier for this relay, used for logging. 27 Name string `json:"name"` 28 // Name of socket file (to be created inside a temporary directory). 29 SocketFileName string `json:"socket_file_name"` 30 // Name of environment variable to export, pointing to the socket file. 31 SocketPathEnvVar string `json:"socket_path_env_var"` 32 // Remote address of server to connect. 33 ServerAddr string `json:"server_address"` 34 35 // Internal fields follow: 36 37 // Full path to socket file. 38 socketFileFullPath string 39 40 // Signal that relay process is ready to accept connections. 41 ready chan struct{} 42 } 43 44 const ( 45 socketPollInterval = 1 * time.Second 46 socketCreateTimeout = 5 * time.Second 47 socketConnectTimeout = 5 * time.Second 48 ) 49 50 // initSocketFile establishes a full path to a socket file. 51 // The directory 'tempdir' should already exist. 52 func (r *socketRelay) initSocketFile(tempDir string) (string, error) { 53 if _, err := os.Stat(tempDir); errors.Is(err, fs.ErrNotExist) { 54 return "", err 55 } 56 r.socketFileFullPath = filepath.Join(tempDir, r.SocketFileName) 57 glog.V(1).Infof("[%s] using socket file %s", r.Name, r.socketFileFullPath) 58 // remove existing socket first 59 err := os.RemoveAll(r.socketFileFullPath) 60 r.ready = make(chan struct{}) 61 return r.socketFileFullPath, err 62 } 63 64 // env returns the environment variable string X=Y for the socket file. 65 func (r *socketRelay) env() (string, error) { 66 if r.socketFileFullPath == "" { 67 return "", fmt.Errorf("must call initSocketFile() before env().") 68 } 69 env := fmt.Sprintf("%s=%s", r.SocketPathEnvVar, r.socketFileFullPath) 70 glog.V(1).Infof("[%s] env %s", r.Name, env) 71 return env, nil 72 } 73 74 // runInterruptOnCancel runs a command that is expected to be terminated 75 // only by an interrupt signal (Ctrl-C). 76 // `waitAfterLaunch` is a function to wait for a certain condition before 77 // resuming, for example, waiting for a socket to bind. If no wait is needed, 78 // pass a function that just returns nil right away. 79 func runInterruptOnCancel(ctx context.Context, logPrefix string, waitAfterLaunch func() error, command ...string) error { 80 cmd := exec.CommandContext(ctx, command[0], command[1:]...) 81 glog.V(0).Infof("%sSpawning process... %v", logPrefix, command) 82 83 interrupted := false 84 cmd.Cancel = func() error { // .Cancel requires go-1.20 85 // Send SIGINT (instead of the default SIGKILL) to allow socat to 86 // clean-up before gracefully exiting. 87 interrupted = true 88 if cmd.Process != nil { 89 glog.V(0).Infof("%sShutting down with SIGINT", logPrefix) 90 return cmd.Process.Signal(os.Interrupt) 91 } 92 return nil 93 } 94 95 // Run process in the background. 96 if err := cmd.Start(); err != nil { 97 return err 98 } 99 glog.V(0).Infof("%sProcess has started.", logPrefix) 100 101 // Wait for a condition before signaling back to the caller 102 // that the relay process is ready to accept connections. 103 if waitErr := waitAfterLaunch(); waitErr != nil { 104 return waitErr 105 } 106 107 err := cmd.Wait() 108 glog.V(0).Infof("%sProcess finished.", logPrefix) 109 110 // Cancellation is expected. 111 if interrupted { 112 glog.V(1).Infof("%sInterrupted after work was completed.", logPrefix) 113 return nil 114 } 115 if err == nil { 116 // Program exited before interrupt. 117 glog.V(1).Infof("%sProgram exited itself.", logPrefix) 118 return nil 119 } 120 if errors.Is(err, context.Canceled) { 121 glog.V(1).Infof("%sProgram's context was canceled.", logPrefix) 122 return nil 123 } 124 if exitErr, ok := err.(*exec.ExitError); ok { 125 if !exitErr.Exited() { // process was terminated 126 glog.V(1).Infof("%sProgram was terminated.", logPrefix) 127 return nil 128 } 129 } 130 return fmt.Errorf("%sprocess error: %v", logPrefix, err) 131 } 132 133 // waitForSocketBind() waits for a socket to appear and be connect-able. 134 // `name` is just an identifier for diagnostics. 135 // `socketPath` is the path of the socket to wait for. 136 // An alternative would be to use something like inotify. 137 func waitForSocketBind(name string, socketPath string) error { 138 // Wait for socket to be connect-able before returning. 139 glog.V(1).Infof("[%s] waiting for socket to bind: %s", name, socketPath) 140 c := make(chan struct{}) 141 go func() { 142 for { 143 if _, err := os.Stat(socketPath); err == nil { 144 glog.V(1).Infof("[%s] socket now exists", name) 145 break 146 } else { 147 glog.V(2).Infof("[%s] waiting for socket to bind...", name) 148 time.Sleep(socketPollInterval) 149 } 150 } 151 c <- struct{}{} 152 }() 153 154 // Wait for socket with timeout. 155 select { 156 case <-c: 157 break 158 case <-time.After(socketCreateTimeout): 159 return fmt.Errorf("Socket file %s not found after %v", socketPath, socketCreateTimeout) 160 } 161 162 // Ensure that connecting to the socket works. 163 glog.V(1).Infof("[%s] Dialing socket: %s", name, socketPath) 164 conn, err := net.DialTimeout("unix", socketPath, socketConnectTimeout) 165 if err != nil { 166 return err 167 } 168 glog.V(1).Infof("[%s] Closing connection on socket: %s", name, socketPath) 169 return conn.Close() 170 } 171 172 // Run launches a relay. Caller should invoke this in a go-routine to run it in the background. 173 // 'socatPath' is the path to the 'socat' tool for the relay subprocess. 174 func (r *socketRelay) run(ctx context.Context, socatPath string) error { 175 if r.socketFileFullPath == "" { 176 return fmt.Errorf("Must call initSocketFile() before run().") 177 } 178 command := []string{ 179 socatPath, 180 // For verbose debugging, pass repeated -d options. 181 fmt.Sprintf("UNIX-LISTEN:%s,unlink-early,fork", r.socketFileFullPath), 182 fmt.Sprintf("TCP:%s", r.ServerAddr), 183 } 184 glog.V(1).Infof("[%s] launching: %v", r.Name, command) 185 return runInterruptOnCancel(ctx, fmt.Sprintf("[%s](relay) ", r.Name), 186 func() error { 187 err := waitForSocketBind(r.Name, r.socketFileFullPath) 188 if err == nil { 189 glog.V(1).Infof("[%s] After launching, relay is now ready to accept connections", r.Name) 190 r.ready <- struct{}{} 191 glog.V(1).Infof("[%s] Sent relay ready signal", r.Name) 192 } 193 return err 194 }, 195 command...) 196 } 197 198 // multiRelayWrap sets up multiple socket relay processes, sets up an 199 // environment, and invokes a function 'f', which usually invokes 200 // a wrapped subprocess using the new environment, and then shuts down 201 // the relay processes. 202 // 'relays' are the set of connections to relay through sockets. 203 // 'tempDir' is a writeable directory where socket files will be created. 204 // 'socatPath' is the location of the `socat` tool. 205 // 'f' is any function or subprocesses that operate while the relays are run. 206 // Returns the exit code of the wrapped function. 207 func multiRelayWrap(ctx context.Context, relays []*socketRelay, tempDir string, socatPath string, f func(env []string) error) error { 208 // Initialize socket files. 209 for _, r := range relays { 210 if _, err := r.initSocketFile(tempDir); err != nil { 211 return err 212 } 213 } 214 215 // Setup wait for all relays to be ready to accept connections. 216 var readyWg sync.WaitGroup 217 for _, r := range relays { 218 readyWg.Add(1) 219 go func() { 220 defer readyWg.Done() 221 glog.V(2).Infof("[%s] Waiting for relay to be ready...", r.Name) 222 <-r.ready 223 glog.V(2).Infof("[%s] Received relay ready signal.", r.Name) 224 }() 225 } 226 227 // Launch relays, running them in the background. 228 // Stop relays before returning. 229 glog.V(1).Infof("Starting relays in the background.") 230 var wg sync.WaitGroup 231 ctx, cancel := context.WithCancel(ctx) 232 for _, r := range relays { 233 wg.Add(1) 234 go func() { 235 defer wg.Done() 236 if err := r.run(ctx, socatPath); err != nil { 237 glog.Errorf("[%s] error running relay: %v", r.Name, err) 238 } 239 }() 240 } 241 242 defer func() { 243 cancel() 244 wg.Wait() // Wait for all relay go-routines to finish exiting. 245 }() 246 247 // Setup a modified environment for the function (usually subprocess). 248 glog.V(1).Infof("Preparing environment for wrapped command") 249 var env []string 250 for _, r := range relays { 251 v, err := r.env() 252 if err != nil { 253 return err 254 } 255 env = append(env, v) 256 } 257 glog.V(1).Infof("environment: %v ", env) 258 259 // Wait for all relays to be ready to accept connections through sockets. 260 glog.V(1).Infof("Waiting for all relays to be ready.") 261 readyWg.Wait() 262 glog.V(1).Infof("All relays are ready.") 263 264 // Call the wrapped function/subprocesses. 265 return f(env) 266 }