191 lines
4.1 KiB
Go
191 lines
4.1 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"dappco.re/go/core"
|
|
)
|
|
|
|
// DaemonOptions configures daemon mode execution.
|
|
//
|
|
// opts := process.DaemonOptions{PIDFile: "/tmp/process.pid", HealthAddr: "127.0.0.1:0"}
|
|
type DaemonOptions struct {
|
|
// PIDFile path for single-instance enforcement.
|
|
// Leave empty to skip PID file management.
|
|
PIDFile string
|
|
|
|
// ShutdownTimeout is the maximum time to wait for graceful shutdown.
|
|
// Default: 30 seconds.
|
|
ShutdownTimeout time.Duration
|
|
|
|
// HealthAddr is the address for health check endpoints.
|
|
// Example: ":8080", "127.0.0.1:9000"
|
|
// Leave empty to disable health checks.
|
|
HealthAddr string
|
|
|
|
// HealthChecks are additional health check functions.
|
|
HealthChecks []HealthCheck
|
|
|
|
// Registry for tracking this daemon. Leave nil to skip registration.
|
|
Registry *Registry
|
|
|
|
// RegistryEntry provides the code and daemon name for registration.
|
|
// PID, Health, and Started are filled automatically.
|
|
RegistryEntry DaemonEntry
|
|
}
|
|
|
|
// Daemon manages daemon lifecycle: PID file, health server, graceful shutdown.
|
|
//
|
|
// daemon := process.NewDaemon(process.DaemonOptions{HealthAddr: "127.0.0.1:0"})
|
|
type Daemon struct {
|
|
opts DaemonOptions
|
|
pid *PIDFile
|
|
health *HealthServer
|
|
running bool
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewDaemon creates a daemon runner with the given options.
|
|
//
|
|
// daemon := process.NewDaemon(process.DaemonOptions{PIDFile: "/tmp/process.pid"})
|
|
func NewDaemon(opts DaemonOptions) *Daemon {
|
|
if opts.ShutdownTimeout == 0 {
|
|
opts.ShutdownTimeout = 30 * time.Second
|
|
}
|
|
|
|
d := &Daemon{opts: opts}
|
|
|
|
if opts.PIDFile != "" {
|
|
d.pid = NewPIDFile(opts.PIDFile)
|
|
}
|
|
|
|
if opts.HealthAddr != "" {
|
|
d.health = NewHealthServer(opts.HealthAddr)
|
|
for _, check := range opts.HealthChecks {
|
|
d.health.AddCheck(check)
|
|
}
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// Start initialises the daemon (PID file, health server).
|
|
func (d *Daemon) Start() error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if d.running {
|
|
return core.E("daemon.start", "daemon already running", nil)
|
|
}
|
|
|
|
if d.pid != nil {
|
|
if err := d.pid.Acquire(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.health != nil {
|
|
if err := d.health.Start(); err != nil {
|
|
if d.pid != nil {
|
|
_ = d.pid.Release()
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
d.running = true
|
|
|
|
// Auto-register if registry is set
|
|
if d.opts.Registry != nil {
|
|
entry := d.opts.RegistryEntry
|
|
entry.PID = currentPID()
|
|
if d.health != nil {
|
|
entry.Health = d.health.Addr()
|
|
}
|
|
if err := d.opts.Registry.Register(entry); err != nil {
|
|
if d.health != nil {
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), d.opts.ShutdownTimeout)
|
|
_ = d.health.Stop(shutdownCtx)
|
|
cancel()
|
|
}
|
|
if d.pid != nil {
|
|
_ = d.pid.Release()
|
|
}
|
|
d.running = false
|
|
return core.E("daemon.start", "registry", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Run blocks until the context is cancelled.
|
|
func (d *Daemon) Run(ctx context.Context) error {
|
|
d.mu.Lock()
|
|
if !d.running {
|
|
d.mu.Unlock()
|
|
return core.E("daemon.run", "daemon not started - call Start() first", nil)
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
<-ctx.Done()
|
|
|
|
return d.Stop()
|
|
}
|
|
|
|
// Stop performs graceful shutdown.
|
|
func (d *Daemon) Stop() error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if !d.running {
|
|
return nil
|
|
}
|
|
|
|
var errs []error
|
|
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), d.opts.ShutdownTimeout)
|
|
defer cancel()
|
|
|
|
if d.health != nil {
|
|
d.health.SetReady(false)
|
|
if err := d.health.Stop(shutdownCtx); err != nil {
|
|
errs = append(errs, core.E("daemon.stop", "health server", err))
|
|
}
|
|
}
|
|
|
|
if d.pid != nil {
|
|
if err := d.pid.Release(); err != nil && !isNotExist(err) {
|
|
errs = append(errs, core.E("daemon.stop", "pid file", err))
|
|
}
|
|
}
|
|
|
|
// Auto-unregister
|
|
if d.opts.Registry != nil {
|
|
_ = d.opts.Registry.Unregister(d.opts.RegistryEntry.Code, d.opts.RegistryEntry.Daemon)
|
|
}
|
|
|
|
d.running = false
|
|
|
|
if len(errs) > 0 {
|
|
return core.ErrorJoin(errs...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetReady sets the daemon readiness status for health checks.
|
|
func (d *Daemon) SetReady(ready bool) {
|
|
if d.health != nil {
|
|
d.health.SetReady(ready)
|
|
}
|
|
}
|
|
|
|
// HealthAddr returns the health server address, or empty if disabled.
|
|
func (d *Daemon) HealthAddr() string {
|
|
if d.health != nil {
|
|
return d.health.Addr()
|
|
}
|
|
return ""
|
|
}
|