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  }