go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/supervisor/subprocess_provider.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package supervisor
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"syscall"
    16  )
    17  
    18  type SubprocessProvider interface {
    19  	Exec(context.Context, *Service) (Subprocess, error)
    20  }
    21  
    22  // Subprocess is a forked process.
    23  type Subprocess interface {
    24  	Start() error
    25  	Pid() int
    26  	Signal(syscall.Signal) error
    27  	Wait() error
    28  }
    29  
    30  // ExecSubprocessProvider is the exec subprocess provider.
    31  type ExecSubprocessProvider struct{}
    32  
    33  func (e ExecSubprocessProvider) Exec(ctx context.Context, svc *Service) (Subprocess, error) {
    34  	esp := new(ExecSubprocess)
    35  	commandResolved, err := exec.LookPath(svc.Command)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	esp.handle = exec.CommandContext(ctx, commandResolved, svc.Args...)
    40  
    41  	var dir string
    42  	if svc.WorkDir != "" {
    43  		dir = filepath.Clean(svc.WorkDir)
    44  	}
    45  	// setting this SysProcAttr is required to be able to kill the process "group"
    46  	// that is spawned from our main process if the user sends a signal.
    47  	esp.handle.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
    48  	esp.handle.Dir = dir
    49  	esp.handle.Env = svc.Env
    50  	esp.handle.Stdin = svc.Stdin
    51  	esp.handle.Stdout = svc.Stdout
    52  	esp.handle.Stderr = svc.Stderr
    53  	return esp, nil
    54  }
    55  
    56  // ExecSubprocess is a subprocess that is implemented using `os/exec`.
    57  type ExecSubprocess struct {
    58  	handle *exec.Cmd
    59  }
    60  
    61  // Start invokes the subprocess.
    62  func (esp ExecSubprocess) Start() error {
    63  	return esp.handle.Start()
    64  }
    65  
    66  // Pid returns the underlying pid.
    67  func (esp ExecSubprocess) Pid() int {
    68  	if esp.handle != nil && esp.handle.Process != nil {
    69  		return esp.handle.Process.Pid
    70  	}
    71  	return 0
    72  }
    73  
    74  func (esp ExecSubprocess) Signal(sig syscall.Signal) error {
    75  	if esp.handle != nil && esp.handle.Process != nil {
    76  		if esp.handle.ProcessState != nil && esp.handle.ProcessState.Exited() {
    77  			return nil
    78  		}
    79  		return syscall.Kill(-esp.handle.Process.Pid, sig)
    80  	}
    81  	return nil
    82  }
    83  
    84  // Wait blocks on the subprocess.
    85  func (esp ExecSubprocess) Wait() error {
    86  	return esp.handle.Wait()
    87  }
    88  
    89  // MockSubprocessProvider is a provider for subprocesses that returns
    90  // a mocked subprocess.
    91  type MockSubprocessProvider struct{}
    92  
    93  // Exec returns a new mocked subprocess.
    94  func (m MockSubprocessProvider) Exec(ctx context.Context, svc *Service) (Subprocess, error) {
    95  	return &mockSubprocess{
    96  		ctx: ctx,
    97  		svc: svc,
    98  	}, nil
    99  }
   100  
   101  // MockSubprocess is a mocked subprocess.
   102  type MockSubprocess interface {
   103  	Subprocess
   104  	Exit(error)
   105  }
   106  
   107  type mockSubprocess struct {
   108  	ctx  context.Context
   109  	svc  *Service
   110  	wait chan error
   111  }
   112  
   113  func (msp *mockSubprocess) Start() error {
   114  	if msp.wait != nil {
   115  		return nil
   116  	}
   117  	msp.wait = make(chan error)
   118  	return nil
   119  }
   120  
   121  func (msp *mockSubprocess) Pid() int { return 123 }
   122  
   123  func (msp *mockSubprocess) Signal(sig syscall.Signal) error {
   124  	if msp.wait == nil {
   125  		return fmt.Errorf("no such process")
   126  	}
   127  	msp.wait <- fmt.Errorf("process exit 1")
   128  	return nil
   129  }
   130  
   131  func (msp *mockSubprocess) Exit(err error) {
   132  	if msp.wait != nil {
   133  		msp.wait <- err
   134  	}
   135  }
   136  
   137  func (msp *mockSubprocess) Wait() error {
   138  	select {
   139  	case <-msp.ctx.Done():
   140  		return context.Canceled
   141  	case err := <-msp.wait:
   142  		return err
   143  	}
   144  }