github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/pkg/ctl/commander.go (about)

     1  package ctl
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/benchkram/errz"
     9  
    10  	"github.com/benchkram/bob/pkg/boblog"
    11  )
    12  
    13  var ErrInProgress = fmt.Errorf("in progress")
    14  var ErrDone = fmt.Errorf("commander is done and can't be started")
    15  
    16  type Commander interface {
    17  	CommandTree
    18  }
    19  
    20  // commander allows to manage mutliple controls
    21  type commander struct {
    22  	// ctx to listen for execution interruption
    23  	ctx context.Context
    24  
    25  	// builder can trigger a rebuild.
    26  	builder Builder
    27  
    28  	// control is used to control the commander.
    29  	control Control
    30  
    31  	// commands are the children the commander controls.
    32  	commands []Command
    33  
    34  	// starting  blocks subssequent starting requests.
    35  	starting *Flag
    36  	// stopping blocks subssequent stopping requests.
    37  	stopping *Flag
    38  
    39  	// done indicates that the commander becomes noop.
    40  	done bool
    41  
    42  	// doneChan emits when the commander becomes noop.
    43  	doneChan chan struct{}
    44  }
    45  
    46  type Builder interface {
    47  	Build(context.Context) error
    48  }
    49  
    50  // NewCommander creates a commander object which can be started and stopped
    51  // until shutdown is called, then it becomes noop.
    52  //
    53  // The commander allows it to control multiple commands while taking
    54  // orders from a higher level instance like a TUI.
    55  //
    56  // TODO: Could be benficial for a TUI to directly control the commands.
    57  //       That needs somehow blocking of a starting/stopping of the whole commander
    58  //       while a child is doing some work. This is currently not implemented.
    59  //       It is possible to control the underlying commands directly through
    60  //       `Subcommands()` but that could probably lead to nasty start/stop loops.
    61  //  ___________             ___________             ___________
    62  // |           | Command() |           | Command() |           |
    63  // | n*command | *-------1 | commander |1--------1 |    tui    |
    64  // |           |           |           |           |           |
    65  // |___________|           |___________|           |___________|
    66  //
    67  func NewCommander(ctx context.Context, builder Builder, ctls ...Command) Commander {
    68  
    69  	c := &commander{
    70  		ctx: ctx,
    71  
    72  		builder: builder,
    73  
    74  		control:  New("commander", 0, nil, nil, nil),
    75  		commands: ctls,
    76  
    77  		starting: &Flag{},
    78  		stopping: &Flag{},
    79  
    80  		doneChan: make(chan struct{}),
    81  	}
    82  
    83  	// Listen on the control for external cmds
    84  	go func() {
    85  		restarting := Flag{}
    86  
    87  		for {
    88  			select {
    89  			case <-ctx.Done():
    90  				// wait till all cmds are done
    91  				<-c.Done()
    92  				c.control.EmitDone()
    93  				return
    94  			case s := <-c.control.Control():
    95  				switch s {
    96  				case Restart:
    97  					// Prevent a restart to happen multiple times.
    98  					// Blocks till the first restart request is finished.
    99  					done, err := restarting.InProgress()
   100  					if err != nil {
   101  						continue
   102  					}
   103  
   104  					go func() {
   105  						defer done()
   106  
   107  						err := c.Stop()
   108  						boblog.Log.Error(err, "Error on stopping comander")
   109  
   110  						err = c.Start()
   111  						boblog.Log.Error(err, "Error during comander run")
   112  
   113  						c.control.EmitRestarted()
   114  					}()
   115  				}
   116  			}
   117  		}
   118  	}()
   119  
   120  	// Shutdown each control
   121  	// on a canceled context
   122  	go func() {
   123  		<-ctx.Done()
   124  		c.shutdown()
   125  	}()
   126  
   127  	return c
   128  }
   129  
   130  // Subcommands allows direct access to the underlying commands.
   131  // !!!Should used with care!!!
   132  // See the comment from `NewCommander()`
   133  func (c *commander) Subcommands() []Command {
   134  	return c.commands
   135  }
   136  
   137  // Start cmds in inverse order.
   138  // Blocks subsequent calls until the first one is completed.
   139  func (c *commander) Start() (err error) {
   140  	defer errz.Recover(&err)
   141  
   142  	err = c.builder.Build(c.ctx)
   143  	errz.Fatal(err)
   144  
   145  	return c.start()
   146  }
   147  
   148  func (c *commander) start() (err error) {
   149  	if c.done {
   150  		return ErrDone
   151  	}
   152  
   153  	done, err := c.starting.InProgress()
   154  	if err != nil {
   155  		return err
   156  	}
   157  	defer done()
   158  
   159  	for i := len(c.commands) - 1; i >= 0; i-- {
   160  		ctl := c.commands[i]
   161  		err = ctl.Start()
   162  		if err != nil {
   163  			return err
   164  		}
   165  	}
   166  
   167  	return err
   168  }
   169  
   170  // Stop children from top to bottom.
   171  // Blocks subsquent calls until the first one is completed.
   172  func (c *commander) Stop() (err error) {
   173  	return c.stop()
   174  }
   175  
   176  // stop children, starting from top.
   177  func (c *commander) stop() (err error) {
   178  
   179  	done, err := c.stopping.InProgress()
   180  	if err != nil {
   181  		return err
   182  	}
   183  	defer done()
   184  
   185  	for _, v := range c.commands {
   186  		if e := v.Stop(); err != nil {
   187  			err = stackErrors(err, e)
   188  		}
   189  	}
   190  
   191  	return err
   192  }
   193  
   194  func (c *commander) Done() <-chan struct{} {
   195  	return c.doneChan
   196  }
   197  
   198  // shutdown forwards the signal to the children.
   199  func (c *commander) shutdown() {
   200  	for _, v := range c.commands {
   201  		_ = v.Shutdown()
   202  	}
   203  	c.done = true
   204  	close(c.doneChan)
   205  }
   206  
   207  func (c *commander) Name() string {
   208  	return c.control.Name()
   209  }
   210  func (c *commander) Restart() error {
   211  	return c.control.Restart()
   212  }
   213  func (c *commander) Running() bool {
   214  	return c.control.Running()
   215  }
   216  func (c *commander) Shutdown() error {
   217  	return c.control.Shutdown()
   218  }
   219  func (c *commander) Stdout() io.Reader {
   220  	return c.control.Stdout()
   221  }
   222  func (c *commander) Stderr() io.Reader {
   223  	return c.control.Stderr()
   224  }
   225  func (c *commander) Stdin() io.Writer {
   226  	return c.control.Stdin()
   227  }