// Package idleshut provides a lifecycle and idletime management tool. package idleshut import ( "sync" "time" ) // Config are the options for creating a new process type Config struct { // Start is called by Process.Start. If an error is returned, // starting the process is aborted. Start func() error // Stop is called by Process.Stop. If an error is returned, // stopping the process is aborted. Stop func() error // IdleTick is called by Process for every iteration of Tick. If // an error is returned, it is sent to IdleProcessError. IdleTick // and IdleProcessError must both be set or unset; mixed set-edness // will result in a panic. IdleTick func() error IdleProcessError func(error) Tick time.Duration // MaxIdleTicks, if nonzero and ticking is configured, will cause // the process to stop once the maximum number of ticks has been // reached. MaxIdleTicks uint } func (cfg Config) validate() { allSet := cfg.IdleTick == nil && cfg.IdleProcessError == nil && cfg.Tick == 0 noneSet := cfg.IdleTick != nil && cfg.IdleProcessError != nil && cfg.Tick != 0 if !allSet && !noneSet { panic("idleshut: Config.IdleTick, config.IdleProcessError, and config.Tick must all be set or all be unset") } } // New returns a new Process with the given configuration. func New(cfg Config) *Process { cfg.validate() return &Process{cfg: cfg} } // Process is a lifecycle and idle process monitor. type Process struct { cfg Config mtx sync.Mutex started time.Time stopped time.Time running bool touched bool idleTicks uint currentGeneration uint } // Touch resets the idle timer to zero. func (p *Process) Touch() bool { p.mtx.Lock() defer p.mtx.Unlock() p.idleTicks = 0 p.touched = true return p.running } // Start initiates the process. func (p *Process) Start() error { p.mtx.Lock() defer p.mtx.Unlock() if !p.running { var err error if p.cfg.Start != nil { err = p.cfg.Start() } if err != nil { return err } p.running = true p.started = time.Now() p.currentGeneration++ go p.startTicker() } return nil } // Running returns true if the process is currently running. func (p *Process) Running() bool { p.mtx.Lock() defer p.mtx.Unlock() return p.running } func (p *Process) generation() uint { p.mtx.Lock() defer p.mtx.Unlock() return p.currentGeneration } func (p *Process) startTicker() { if p.cfg.Tick == 0 || p.cfg.IdleTick == nil { return } startingGen := p.generation() tk := time.NewTicker(p.cfg.Tick) for { <-tk.C // if we stopped running, or we started again and thus the geneneration has advanced, return if !p.Running() || p.generation() != startingGen { return } func() { p.mtx.Lock() defer p.mtx.Unlock() if p.touched { p.idleTicks = 0 p.touched = false //reset } else { p.idleTicks++ if p.cfg.IdleTick != nil { p.idleProcessError(p.cfg.IdleTick()) } if p.idleTicks >= p.cfg.MaxIdleTicks { p.idleProcessError(p.stopWithLock()) } } }() } } func (p *Process) idleProcessError(err error) { if err != nil && p.cfg.IdleProcessError != nil { go p.cfg.IdleProcessError(err) } } // Stop halts the process. func (p *Process) Stop() error { p.mtx.Lock() defer p.mtx.Unlock() if p.running { return p.stopWithLock() } return nil } func (p *Process) stopWithLock() error { var err error if p.cfg.Stop != nil { err = p.cfg.Stop() } if err != nil { return err } p.idleTicks = 0 p.running = false p.stopped = time.Now() return nil }