// Package idleshut provides a lifecycle and idletime management tool. package idleshut import ( "sync" "time" ) // Config contains the options for creating a new process. type Config struct { // Start, if non-nil, is called by Process.Start. If an error is returned, // starting the process is aborted. Start func() error // Stop, if non-nil, is called by Process.Stop. If an error is returned, // stopping the process is aborted. Stop func() error // If TickDuration is non-zero, the process will count ticks where // the process is active or idle. Process are kept active by the user // calling the Process.Touch() method. When the process has been idle for // more than MaxIdleTicks, it will be stopped. If ticking is enabled, // TickError must be set or operations will result in a panic. TickDuration time.Duration TickError func(error) // Tick is called by Process for every iteration of Tick. If // an error is returned, it is sent to TickError. Additionally, // IdleTick and ActiveTick are called for idle and active ticks // respectively. Tick func() error IdleTick func() error ActiveTick func() error // 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.TickError == nil && cfg.TickDuration == 0 noneSet := cfg.TickError != nil && cfg.TickDuration != 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.TickDuration == 0 { return } startingGen := p.generation() tk := time.NewTicker(p.cfg.TickDuration) 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.cfg.Tick != nil { p.idleProcessError(p.cfg.Tick()) } if p.touched { p.idleTicks = 0 p.touched = false //reset if p.cfg.ActiveTick != nil { p.idleProcessError(p.cfg.ActiveTick()) } } 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.TickError != nil { go p.cfg.TickError(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 }