tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/engine/engine.go (about)

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"log"
     6  	"log/slog"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  
    11  	"tractor.dev/toolkit-go/engine/cli"
    12  	"tractor.dev/toolkit-go/engine/daemon"
    13  )
    14  
    15  var (
    16  	Identifier string
    17  )
    18  
    19  // Initializer provides an initialization hook after assembly.
    20  type Initializer interface {
    21  	Initialize()
    22  }
    23  
    24  // PostInitializer provides a hook after initialization.
    25  type PostInitializer interface {
    26  	PostInitialize()
    27  }
    28  
    29  // Service is a long-running process managed by daemon.Framework.
    30  type Service interface {
    31  	Serve(ctx context.Context)
    32  }
    33  
    34  // Runner is a unit that can take over the program entrypoint.
    35  type Runner interface {
    36  	Run(ctx context.Context) error
    37  }
    38  
    39  type Terminator interface {
    40  	Terminate()
    41  }
    42  
    43  type Depender interface {
    44  	Assembly() []Unit
    45  }
    46  
    47  var (
    48  	// Main is a global reference to the top-level main unit.
    49  	Main Unit
    50  
    51  	defaultAssembly *Assembly
    52  )
    53  
    54  func typeExists(units []Unit, unit Unit) bool {
    55  	for _, u := range units {
    56  		a := reflect.ValueOf(unitFrom(unit))
    57  		b := reflect.ValueOf(unitFrom(u))
    58  		if a.Type() == b.Type() {
    59  			return true
    60  		}
    61  	}
    62  	return false
    63  }
    64  
    65  func Dependencies(units ...Unit) []Unit {
    66  	var deps []Unit
    67  	for _, unit := range units {
    68  		if d, ok := unit.(Depender); ok {
    69  			for _, dep := range d.Assembly() {
    70  				if !typeExists(deps, dep) && !typeExists(units, dep) {
    71  					deps = append(deps, dep)
    72  				}
    73  			}
    74  		}
    75  	}
    76  	return append(units, deps...)
    77  }
    78  
    79  func Assemble(units ...Unit) (asm *Assembly, err error) {
    80  	if asm, err = New(Dependencies(units...)...); err != nil {
    81  		return
    82  	}
    83  
    84  	// dependency injection
    85  	if err = asm.SelfAssemble(); err != nil {
    86  		return
    87  	}
    88  
    89  	// initialize units after DI, in reverse order (main last)
    90  	for i := len(asm.Units()) - 1; i >= 0; i-- {
    91  		u := asm.Units()[i]
    92  		i, ok := u.(Initializer)
    93  		if ok {
    94  			i.Initialize()
    95  		}
    96  	}
    97  
    98  	// postinitialize units, for units depending on main initialization
    99  	for _, u := range asm.Units() {
   100  		pi, ok := u.(PostInitializer)
   101  		if ok {
   102  			pi.PostInitialize()
   103  		}
   104  	}
   105  
   106  	return
   107  }
   108  
   109  // Init only needs to be explicitly called if you
   110  // need to run code before calling Run
   111  func Init() {
   112  	if Identifier == "" {
   113  		Identifier = filepath.Base(os.Args[0])
   114  	}
   115  }
   116  
   117  // Run assembles units and starts the program.
   118  func Run(units ...Unit) {
   119  	Init()
   120  
   121  	asm, err := New(units...)
   122  	if err != nil {
   123  		log.Fatal(err)
   124  	}
   125  
   126  	// add assembly
   127  	if err := asm.Add(asm); err != nil {
   128  		panic(err)
   129  	}
   130  
   131  	// add daemon framework
   132  	d := &daemon.Framework{}
   133  	if err := asm.Add(d); err != nil {
   134  		panic(err)
   135  	}
   136  
   137  	// add cli framework
   138  	c := &cli.Framework{}
   139  	if err := asm.Add(c); err != nil {
   140  		panic(err)
   141  	}
   142  
   143  	// add logger
   144  	if err := asm.Add(slog.Default()); err != nil {
   145  		panic(err)
   146  	}
   147  
   148  	// re-assemble
   149  	asm, err = Assemble(asm.Units()...)
   150  	if err != nil {
   151  		panic(err)
   152  	}
   153  	defaultAssembly = asm
   154  
   155  	// make main unit global accessible
   156  	Main = asm.Main()
   157  
   158  	// if main has Run use it
   159  	if r, ok := asm.Main().(Runner); ok {
   160  		if err := r.Run(context.Background()); err != nil {
   161  			log.Fatal(err)
   162  		}
   163  		return
   164  	}
   165  
   166  	// find a runner; should always be the cli.Framework
   167  	for i := len(asm.Units()) - 1; i >= 0; i-- {
   168  		u := asm.Units()[i]
   169  		r, ok := u.(Runner)
   170  		if ok {
   171  			if err := r.Run(context.Background()); err != nil {
   172  				log.Fatal(err)
   173  			}
   174  			return
   175  		}
   176  	}
   177  
   178  	panic("nothing to run")
   179  }
   180  
   181  func Terminate() {
   182  	if defaultAssembly == nil {
   183  		panic("no active default assembly")
   184  	}
   185  	for i := len(defaultAssembly.Units()) - 1; i >= 0; i-- {
   186  		u := defaultAssembly.Units()[i]
   187  		if t, ok := u.(Terminator); ok {
   188  			t.Terminate()
   189  			return
   190  		}
   191  	}
   192  }