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