github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/controller/processes/processes.go (about) 1 package processes 2 3 import ( 4 "context" 5 "sync" 6 "sync/atomic" 7 8 "github.com/docker/buildx/build" 9 "github.com/docker/buildx/controller/pb" 10 "github.com/docker/buildx/util/ioset" 11 "github.com/pkg/errors" 12 "github.com/sirupsen/logrus" 13 ) 14 15 // Process provides methods to control a process. 16 type Process struct { 17 inEnd *ioset.Forwarder 18 invokeConfig *pb.InvokeConfig 19 errCh chan error 20 processCancel func() 21 serveIOCancel func() 22 } 23 24 // ForwardIO forwards process's io to the specified reader/writer. 25 // Optionally specify ioCancelCallback which will be called when 26 // the process closes the specified IO. This will be useful for additional cleanup. 27 func (p *Process) ForwardIO(in *ioset.In, ioCancelCallback func()) { 28 p.inEnd.SetIn(in) 29 if f := p.serveIOCancel; f != nil { 30 f() 31 } 32 p.serveIOCancel = ioCancelCallback 33 } 34 35 // Done returns a channel where error or nil will be sent 36 // when the process exits. 37 // TODO: change this to Wait() 38 func (p *Process) Done() <-chan error { 39 return p.errCh 40 } 41 42 // Manager manages a set of proceses. 43 type Manager struct { 44 container atomic.Value 45 processes sync.Map 46 } 47 48 // NewManager creates and returns a Manager. 49 func NewManager() *Manager { 50 return &Manager{} 51 } 52 53 // Get returns the specified process. 54 func (m *Manager) Get(id string) (*Process, bool) { 55 v, ok := m.processes.Load(id) 56 if !ok { 57 return nil, false 58 } 59 return v.(*Process), true 60 } 61 62 // CancelRunningProcesses cancels execution of all running processes. 63 func (m *Manager) CancelRunningProcesses() { 64 var funcs []func() 65 m.processes.Range(func(key, value any) bool { 66 funcs = append(funcs, value.(*Process).processCancel) 67 m.processes.Delete(key) 68 return true 69 }) 70 for _, f := range funcs { 71 f() 72 } 73 } 74 75 // ListProcesses lists all running processes. 76 func (m *Manager) ListProcesses() (res []*pb.ProcessInfo) { 77 m.processes.Range(func(key, value any) bool { 78 res = append(res, &pb.ProcessInfo{ 79 ProcessID: key.(string), 80 InvokeConfig: value.(*Process).invokeConfig, 81 }) 82 return true 83 }) 84 return res 85 } 86 87 // DeleteProcess deletes the specified process. 88 func (m *Manager) DeleteProcess(id string) error { 89 p, ok := m.processes.LoadAndDelete(id) 90 if !ok { 91 return errors.Errorf("unknown process %q", id) 92 } 93 p.(*Process).processCancel() 94 return nil 95 } 96 97 // StartProcess starts a process in the container. 98 // When a container isn't available (i.e. first time invoking or the container has exited) or cfg.Rollback is set, 99 // this method will start a new container and run the process in it. Otherwise, this method starts a new process in the 100 // existing container. 101 func (m *Manager) StartProcess(pid string, resultCtx *build.ResultHandle, cfg *pb.InvokeConfig) (*Process, error) { 102 // Get the target result to invoke a container from 103 var ctr *build.Container 104 if a := m.container.Load(); a != nil { 105 ctr = a.(*build.Container) 106 } 107 if cfg.Rollback || ctr == nil || ctr.IsUnavailable() { 108 go m.CancelRunningProcesses() 109 // (Re)create a new container if this is rollback or first time to invoke a process. 110 if ctr != nil { 111 go ctr.Cancel() // Finish the existing container 112 } 113 var err error 114 ctr, err = build.NewContainer(context.TODO(), resultCtx, cfg) 115 if err != nil { 116 return nil, errors.Errorf("failed to create container %v", err) 117 } 118 m.container.Store(ctr) 119 } 120 // [client(ForwardIO)] <-forwarder(switchable)-> [out] <-pipe-> [in] <- [process] 121 in, out := ioset.Pipe() 122 f := ioset.NewForwarder() 123 f.PropagateStdinClose = false 124 f.SetOut(&out) 125 126 // Register process 127 ctx, cancel := context.WithCancel(context.TODO()) 128 var cancelOnce sync.Once 129 processCancelFunc := func() { cancelOnce.Do(func() { cancel(); f.Close(); in.Close(); out.Close() }) } 130 p := &Process{ 131 inEnd: f, 132 invokeConfig: cfg, 133 processCancel: processCancelFunc, 134 errCh: make(chan error), 135 } 136 m.processes.Store(pid, p) 137 go func() { 138 var err error 139 if err = ctr.Exec(ctx, cfg, in.Stdin, in.Stdout, in.Stderr); err != nil { 140 logrus.Debugf("process error: %v", err) 141 } 142 logrus.Debugf("finished process %s %v", pid, cfg.Entrypoint) 143 m.processes.Delete(pid) 144 processCancelFunc() 145 p.errCh <- err 146 }() 147 148 return p, nil 149 }