1package modd 2 3import ( 4 "os" 5 "os/exec" 6 "sync" 7 "time" 8 9 "github.com/cortesi/modd/conf" 10 "github.com/cortesi/modd/shell" 11 "github.com/cortesi/modd/varcmd" 12 "github.com/cortesi/termlog" 13) 14 15const ( 16 // MinRestart is the minimum amount of time between daemon restarts 17 MinRestart = 500 * time.Millisecond 18 // MulRestart is the exponential backoff multiplier applied when the daemon exits uncleanly 19 MulRestart = 2 20 // MaxRestart is the maximum amount of time between daemon restarts 21 MaxRestart = 8 * time.Second 22) 23 24// A single daemon 25type daemon struct { 26 conf conf.Daemon 27 indir string 28 29 ex *shell.Executor 30 log termlog.Stream 31 shell string 32 stop bool 33 sync.Mutex 34} 35 36func (d *daemon) Run() { 37 var lastStart time.Time 38 delay := MinRestart 39 for d.stop != true { 40 if delay > MinRestart { 41 d.log.Notice(">> restart backoff... %dms", delay/time.Millisecond) 42 } 43 if !lastStart.IsZero() { 44 time.Sleep(delay) 45 } 46 d.log.Notice(">> starting...") 47 lastStart = time.Now() 48 err, pstate := d.ex.Run(d.log, false) 49 50 if err != nil { 51 d.log.Shout("execution error: %s", err) 52 } else if pstate.Error != nil { 53 if _, ok := pstate.Error.(*exec.ExitError); ok { 54 d.log.Warn("exited: %s", pstate.ProcState) 55 } else { 56 d.log.Shout("exited: %s", err) 57 } 58 } else { 59 d.log.Warn("exited: %s", pstate.ProcState) 60 } 61 62 // If we exited cleanly, or the process ran for > MaxRestart, we reset 63 // the delay timer 64 if time.Now().Sub(lastStart) > MaxRestart { 65 delay = MinRestart 66 } else { 67 delay *= MulRestart 68 if delay > MaxRestart { 69 delay = MaxRestart 70 } 71 } 72 } 73} 74 75// Restart the daemon, or start it if it's not yet running 76func (d *daemon) Restart() { 77 d.Lock() 78 defer d.Unlock() 79 if d.ex == nil { 80 ex, err := shell.NewExecutor(d.shell, d.conf.Command, d.indir) 81 if err != nil { 82 d.log.Shout("Could not create executor: %s", err) 83 } 84 d.ex = ex 85 go d.Run() 86 } else { 87 d.log.Notice(">> sending signal %s", d.conf.RestartSignal) 88 err := d.ex.Signal(d.conf.RestartSignal) 89 if err != nil { 90 d.log.Warn( 91 "failed to send %s signal to %s: %v", d.conf.RestartSignal, d.conf.Command, err, 92 ) 93 } 94 } 95} 96 97func (d *daemon) Shutdown(sig os.Signal) error { 98 d.log.Notice(">> stopping") 99 d.stop = true 100 if d.ex != nil { 101 return d.ex.Stop() 102 } 103 return nil 104} 105 106// DaemonPen is a group of daemons in a single block, managed as a unit. 107type DaemonPen struct { 108 daemons []*daemon 109 sync.Mutex 110} 111 112// NewDaemonPen creates a new DaemonPen 113func NewDaemonPen(block conf.Block, vars map[string]string, log termlog.TermLog) (*DaemonPen, error) { 114 d := make([]*daemon, len(block.Daemons)) 115 for i, dmn := range block.Daemons { 116 vcmd := varcmd.VarCmd{Block: nil, Modified: nil, Vars: vars} 117 finalcmd, err := vcmd.Render(dmn.Command) 118 if err != nil { 119 return nil, err 120 } 121 dmn.Command = finalcmd 122 var indir string 123 if block.InDir != "" { 124 indir = block.InDir 125 } else { 126 indir, err = os.Getwd() 127 if err != nil { 128 return nil, err 129 } 130 } 131 sh, err := shell.GetShellName(vars[shellVarName]) 132 if err != nil { 133 return nil, err 134 } 135 136 d[i] = &daemon{ 137 conf: dmn, 138 log: log.Stream(niceHeader("daemon: ", dmn.Command)), 139 shell: sh, 140 indir: indir, 141 } 142 } 143 return &DaemonPen{daemons: d}, nil 144} 145 146// Restart all daemons in the pen, or start them if they're not running yet. 147func (dp *DaemonPen) Restart() { 148 dp.Lock() 149 defer dp.Unlock() 150 if dp.daemons != nil { 151 for _, d := range dp.daemons { 152 d.Restart() 153 } 154 } 155} 156 157// Shutdown all daemons in the pen 158func (dp *DaemonPen) Shutdown(sig os.Signal) { 159 dp.Lock() 160 defer dp.Unlock() 161 if dp.daemons != nil { 162 for _, d := range dp.daemons { 163 d.Shutdown(sig) 164 } 165 } 166} 167 168// DaemonWorld represents the entire world of daemons 169type DaemonWorld struct { 170 DaemonPens []*DaemonPen 171} 172 173// NewDaemonWorld creates a DaemonWorld 174func NewDaemonWorld(cnf *conf.Config, log termlog.TermLog) (*DaemonWorld, error) { 175 daemonPens := make([]*DaemonPen, len(cnf.Blocks)) 176 for i, b := range cnf.Blocks { 177 d, err := NewDaemonPen(b, cnf.GetVariables(), log) 178 if err != nil { 179 return nil, err 180 } 181 daemonPens[i] = d 182 183 } 184 return &DaemonWorld{daemonPens}, nil 185} 186 187// Shutdown all daemons with signal s 188func (dw *DaemonWorld) Shutdown(s os.Signal) { 189 for _, dp := range dw.DaemonPens { 190 dp.Shutdown(s) 191 } 192} 193