camfish/phases.go
2025-12-29 15:57:39 -05:00

364 lines
10 KiB
Go

package camfish
import "os"
import "fmt"
import "log"
import "io/fs"
import "errors"
import "context"
import "runtime"
import "strings"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/go-cli"
import "git.tebibyte.media/sashakoshka/go-service/daemon"
import "git.tebibyte.media/sashakoshka/go-service/rotate"
func (this *environment) phase10FlagParsing() bool {
// create flag set and specify built-in flags
set := flagSet {
name: this.name,
description: this.description,
}
flagHelp := set.Flag('h', "help", "Display usage information and exit", nil)
flagPidFile := set.Flag('p', "pid-file", "Write the PID to the specified file", cli.ValString)
flagUser := set.Flag('u', "user", "The user:group to run as", cli.ValString)
flagLogDirectory := set.Flag('l', "log-directory", "Write logs to the specified directory", cli.ValString)
flagConfigFile := set.Flag('c', "config-file", "Use this configuration file", cli.ValString)
flagVerbose := set.Flag('v', "verbose", "(debug) Enable verbose output/logging", nil)
flagCrash := set.Flag(0, "crash", "(debug) Crash when an actor panics", nil)
flagCrashOnError := set.Flag(0, "crash-on-error", "(debug) Crash when an actor experiences any error", nil)
flagFastTiming := set.Flag(0, "fast-timing", "(debug) Make timed things happen faster/more often", nil)
// ask actors to add flags
actors, done := this.actors.RBorrow()
defer done()
for _, actor := range sortActors(actors, actors.flagAdder.all()) {
actor.AddFlags(&set)
}
// parse flags
err := set.parse(os.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
set.usage()
return false
}
// handle built-in flags
if _, ok := flagHelp.First(); ok {
set.usage()
return false
}
if pidFile, ok := flagPidFile.First(); ok {
this.flags.pidFile = pidFile
}
if user, ok := flagUser.First(); ok {
this.flags.user = user
}
if logDirectory, ok := flagLogDirectory.First(); ok {
this.flags.logDirectory = logDirectory
}
if configFile, ok := flagConfigFile.First(); ok {
this.flags.configFile = configFile
}
if _, ok := flagVerbose.First(); ok {
this.flags.verbose = true
}
if _, ok := flagCrash.First(); ok {
this.flags.crash = true
}
if _, ok := flagCrashOnError.First(); ok {
this.flags.crash = true
this.flags.crashOnError = true
}
if _, ok := flagFastTiming.First(); ok {
this.flags.fastTiming = true
this.cron.fastTiming = true
}
return true
}
func (this *environment) phase13PidFileCreation() bool {
if this.flags.pidFile != "" {
err := daemon.PidFile(this.flags.pidFile).Start()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
return false
}
}
return true
}
func (this *environment) phase17PrivilegeDropping() bool {
if this.flags.user != "" {
if this.Verb() {
fmt.Fprintf(os.Stderr,
"%s: dropping privilege to %s\n",
os.Args[0], this.flags.user)
}
user, group, _ := strings.Cut(this.flags.user, ":")
err := dropPrivelege(user, group)
fmt.Fprintf(os.Stderr, "%s: could not drop privilege %v\n", os.Args[0], err)
}
return true
}
func (this *environment) phase20LogSwitching() bool {
if this.flags.logDirectory != "" {
directory := this.flags.logDirectory
if this.Verb() {
fmt.Fprintf(os.Stderr,
"%s: logging to %s\n",
os.Args[0], directory)
}
directory, err := filepath.Abs(directory)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: could not rotate logs: %v\n", os.Args[0], err)
return false
}
logger, err := rotate.New(directory)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: could not rotate logs: %v\n", os.Args[0], err)
return false
}
defer logger.Close()
log.SetOutput(logger)
}
log.Printf("====== [%s] START =======", this.name)
log.Printf("(i) (20) CAMFISH environment %s", version)
logActors(All())
return true
}
func (this *environment) phase30ConfigurationParsing() bool {
if this.Verb() { log.Println("... (30) parsing configuration") }
// blank config if nothing happens
this.conf = make(iniConfig)
// get list of config files
paths, err := configFiles(this.name)
if err != nil {
log.Println("!!! (30) could not determine location of file(s):", err)
return true
}
if this.flags.configFile != "" {
paths = append(paths, this.flags.configFile)
}
if this.Verb() {
log.Println("(i) (30) have configuration files:")
for _, paths := range paths {
log.Println("(i) (30) -", paths)
}
}
// parse every config and merge them all
configs := make([]iniConfig, 0, len(paths))
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
log.Println("!!! (30) file present but inaccessible:", err)
} else if path == this.flags.configFile {
log.Println("!!! (30)", err)
}
continue
}
defer file.Close()
config, err := DecodeINI(path, file)
if err != nil {
log.Println("!!! (30) could not parse:", err)
continue
}
configs = append(configs, config.(iniConfig))
}
this.conf = mergeINI(configs...)
if this.Verb() { log.Println(".// (30) parsed configuration") }
return true
}
func (this *environment) phase40ConfigurationProcessing() bool {
if this.Verb() { log.Println("... (40) processing configuration") }
actors, done := this.actors.RBorrow()
defer done()
for _, actor := range sortActors(actors, actors.configProcessor.all()) {
err := actor.ProcessConfig(this.conf)
if err != nil {
log.Println("XXX (50) could not process configuration:", err)
return false
}
}
if this.Verb() { log.Println(".// (40) processed configuration") }
return true
}
func (this *environment) phase50ConfigurationApplication() bool {
if this.Verb() { log.Println("... (50) applying configuration") }
err := this.applyConfig()
if err != nil {
log.Println("XXX (50) could not apply configuration:", err)
return false
}
actors, done := this.actors.RBorrow()
defer done()
for _, actor := range sortActors(actors, actors.configurable.all()) {
if !actors.info(actor.(Actor)).initial { continue }
if this.Verb() { log.Printf ("... (50) applying configuration to %s", actor.(Actor).Type())}
err := actor.Configure(this.conf)
if err != nil {
log.Printf (
"XXX (50) could not apply configuration to %s: %v",
actor.(Actor).Type(), err)
return false
}
}
if this.Verb() { log.Println(".// (50) applied configuration") }
return true
}
func (this *environment) phase60Initialization() bool {
if this.Verb() { log.Println("... (60) initializing") }
// this fucking sucks in sorry
var initializable []Initializable
func() {
actors, done := this.actors.RBorrow()
defer done()
initializable = actors.initializable.all()
}()
// filter out non-initial actors
initializableInitial := initializable
index := 0
for _, actor := range initializable {
if !this.info(actor.(Actor)).initial { continue }
initializableInitial = initializable[:index + 1]
initializableInitial[index] = actor
index ++
}
if err := this.initializeActors(this.ctx, initializableInitial...); err != nil {
log.Println("XXX (60) failed to initialize:", err)
return false
}
if this.Verb() { log.Println(".// (60) initialized") }
return true
}
func (this *environment) phase70Running() bool {
for actor := range this.All() {
if actor, ok := actor.(MainRunnable); ok {
this.main = actor
}
}
bodyReturn := make(chan bool)
go func() {
bodyReturn <- this.phase70RunningBody()
if this.main != nil {
shutdownCtx, done := context.WithTimeout(
context.Background(),
defaul(this.timing.shutdownTimeout.Load(),
defaultShutdownTimeout))
defer done()
this.main.ShutdownMain(shutdownCtx)
}
}()
mainReturn := false
if this.main != nil {
mainReturn = this.phase70_2MainBind()
}
return <- bodyReturn && mainReturn
}
func (this *environment) phase70RunningBody() bool {
defer this.Done(nil)
if this.Verb() { log.Println("... (70) starting up") }
this.running.Store(true)
defer this.running.Store(false)
func() {
actors, done := this.actors.RBorrow()
defer done()
for _, actor := range actors.runnable.all() {
if !actors.info(actor.(Actor)).initial { continue }
this.start(actor.(Actor))
}
for _, actor := range actors.runShutdownable.all() {
if !actors.info(actor.(Actor)).initial { continue }
this.start(actor.(Actor))
}
}()
log.Println(".// (70) startup sequence complete")
// await context cancellation or waitgroup completion
go func() {
this.group.Wait()
close(this.noneLeft)
}()
select {
case <- this.ctx.Done():
if this.Verb() { log.Println("(i) (70) canceled") }
case <- this.noneLeft:
if this.Verb() { log.Println("(i) (70) all actors have finished") }
}
return true
}
func (this *environment) phase70_2MainBind() bool{
mainActor := this.main.(Actor)
if this.Verb() { log.Printf("... (70.2) binding %s to main thread", mainActor.Type()) }
runtime.LockOSThread()
if this.Verb() { log.Printf(".// (70.2) main thread bind complete") }
defer runtime.UnlockOSThread()
err := panicWrap(this.main.RunMain)
if err != nil {
log.Printf("XXX [%s] main thread failed: %v", mainActor.Type(), err)
return false
}
if this.Verb() { log.Printf("(i) (70.2) main thread exited") }
return true
}
func (this *environment) phase70_5Trimming() bool {
if this.Verb() { log.Println("... (70.5) trimming") }
var trimmable []Trimmable
func() {
actors, done := this.actors.RBorrow()
defer done()
trimmable = actors.trimmable.all()
}()
if err := this.trimActors(this.ctx, trimmable...); err != nil {
log.Println(".// (70.5) failed to trim:", err)
if this.flags.crashOnError {
panic(err)
}
return false
}
if this.Verb() { log.Println(".// (70.5) trimmed") }
return true
}
func (this *environment) phase80Shutdown() bool {
logActors(All())
ctx, done := context.WithTimeout(
context.Background(),
defaul(this.timing.shutdownTimeout.Load(), defaultShutdownTimeout))
defer done()
go func() {
<- ctx.Done()
if errors.Is(context.Cause(ctx), context.DeadlineExceeded) {
this.emergencyHalt("shutdown timeout expired")
}
}()
cause := context.Cause(this.ctx)
if cause != nil {
log.Println("XXX (80) shutting down because:", cause)
}
log.Println("... (80) waiting for actors to shut down")
defer func() {
log.Println(".// (80) shutdown succeeded, goodbye")
log.Printf("====== [%s] END =======", this.name)
}()
// wait for all actors to shut down
<- this.noneLeft
return cause == nil
}