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  }