1// Copyright 2017 syzkaller project authors. All rights reserved.
2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3
4package main
5
6import (
7	"os"
8	"os/exec"
9	"syscall"
10	"time"
11
12	"github.com/google/syzkaller/pkg/log"
13	"github.com/google/syzkaller/pkg/osutil"
14)
15
16// ManagerCmd encapsulates a single instance of syz-manager process.
17// It automatically restarts syz-manager if it exits unexpectedly,
18// and supports graceful shutdown via SIGINT.
19type ManagerCmd struct {
20	name    string
21	log     string
22	errorf  Errorf
23	bin     string
24	args    []string
25	closing chan bool
26}
27
28type Errorf func(msg string, args ...interface{})
29
30// NewManagerCmd starts new syz-manager process.
31// name - name for logging.
32// log - manager log file with stdout/stderr.
33// bin/args - process binary/args.
34func NewManagerCmd(name, log string, errorf Errorf, bin string, args ...string) *ManagerCmd {
35	mc := &ManagerCmd{
36		name:    name,
37		log:     log,
38		errorf:  errorf,
39		bin:     bin,
40		args:    args,
41		closing: make(chan bool),
42	}
43	go mc.loop()
44	return mc
45}
46
47// Close gracefully shutdowns the process and waits for its termination.
48func (mc *ManagerCmd) Close() {
49	mc.closing <- true
50	<-mc.closing
51}
52
53func (mc *ManagerCmd) loop() {
54	const (
55		restartPeriod    = 10 * time.Minute // don't restart crashing manager more frequently than that
56		interruptTimeout = time.Minute      // give manager that much time to react to SIGINT
57	)
58	var (
59		cmd         *exec.Cmd
60		started     time.Time
61		interrupted time.Time
62		stopped     = make(chan error, 1)
63		closing     = mc.closing
64		ticker1     = time.NewTicker(restartPeriod)
65		ticker2     = time.NewTicker(interruptTimeout)
66	)
67	defer func() {
68		ticker1.Stop()
69		ticker2.Stop()
70	}()
71	for closing != nil || cmd != nil {
72		if cmd == nil {
73			// cmd is not running
74			// don't restart too frequently (in case it instantly exits with an error)
75			if time.Since(started) > restartPeriod {
76				started = time.Now()
77				os.Rename(mc.log, mc.log+".old")
78				logfile, err := os.Create(mc.log)
79				if err != nil {
80					mc.errorf("failed to create manager log: %v", err)
81				} else {
82					cmd = osutil.Command(mc.bin, mc.args...)
83					cmd.Stdout = logfile
84					cmd.Stderr = logfile
85					err := cmd.Start()
86					logfile.Close()
87					if err != nil {
88						mc.errorf("failed to start manager: %v", err)
89						cmd = nil
90					} else {
91						log.Logf(1, "%v: started manager", mc.name)
92						go func() {
93							stopped <- cmd.Wait()
94						}()
95					}
96				}
97			}
98		} else {
99			// cmd is running
100			if closing == nil && time.Since(interrupted) > interruptTimeout {
101				log.Logf(1, "%v: killing manager", mc.name)
102				cmd.Process.Kill()
103				interrupted = time.Now()
104			}
105		}
106
107		select {
108		case <-closing:
109			closing = nil
110			if cmd != nil {
111				log.Logf(1, "%v: stopping manager", mc.name)
112				cmd.Process.Signal(syscall.SIGINT)
113				interrupted = time.Now()
114			}
115		case err := <-stopped:
116			if cmd == nil {
117				mc.errorf("spurious stop signal: %v", err)
118			}
119			if closing != nil {
120				mc.errorf("manager exited unexpectedly: %v", err)
121			}
122			cmd = nil
123			log.Logf(1, "%v: manager exited with %v", mc.name, err)
124		case <-ticker1.C:
125		case <-ticker2.C:
126		}
127	}
128	close(mc.closing)
129}
130