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 }