├── .travis.yml ├── goforever_test.go ├── goforever.toml ├── config_test.go ├── LICENSE ├── README.md ├── config.go ├── process_test.go ├── http_test.go ├── goforever.go ├── http.go └── process.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.2 3 | 4 | before_script: 5 | - go build -a 6 | 7 | script: 8 | - go test -v -------------------------------------------------------------------------------- /goforever_test.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | //"fmt" 8 | "testing" 9 | ) 10 | 11 | func Test_main(t *testing.T) { 12 | if daemon.Name != "goforever" { 13 | t.Error("Daemon name is not goforever") 14 | } 15 | daemon.Args = []string{"foo"} 16 | daemon.start(daemon.Name) 17 | if daemon.Args[0] != "foo" { 18 | t.Error("First arg not foo") 19 | } 20 | daemon.find() 21 | daemon.stop() 22 | } 23 | -------------------------------------------------------------------------------- /goforever.toml: -------------------------------------------------------------------------------- 1 | ip = "127.0.0.1" 2 | port = "2224" 3 | username = "go" 4 | password = "forever" 5 | pidfile = "goforever.pid" 6 | logfile = "goforever.log" 7 | errfile = "goforever.log" 8 | 9 | [[process]] 10 | name = "example-panic" 11 | command = "./example/example-panic" 12 | pidfile = "example/example-panic.pid" 13 | logfile = "example/logs/example-panic.debug.log" 14 | errfile = "example/logs/example-panic.errors.log" 15 | respawn = 1 16 | delay = "1m" 17 | ping = "30s" 18 | 19 | [[process]] 20 | name = "example" 21 | command = "./example/example" 22 | args = ["-name=foo"] 23 | pidfile = "example/example.pid" 24 | logfile = "example/logs/example.debug.log" 25 | errfile = "example/logs/example.errors.log" 26 | respawn = 1 27 | 28 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestNewConfig(t *testing.T) { 11 | r, err := LoadConfig("goforever.toml") 12 | 13 | if err != nil { 14 | t.Errorf("Error creating config %s.", err) 15 | return 16 | } 17 | if r == nil { 18 | t.Errorf("Expected %#v. Result %#v\n", r, nil) 19 | } 20 | } 21 | 22 | func TestConfigGet(t *testing.T) { 23 | c, _ := LoadConfig("goforever.toml") 24 | ex := "example/example.pid" 25 | r := string(c.Get("example").Pidfile) 26 | if ex != r { 27 | t.Errorf("Expected %#v. Result %#v\n", ex, r) 28 | } 29 | } 30 | 31 | func TestConfigKeys(t *testing.T) { 32 | c, _ := LoadConfig("goforever.toml") 33 | ex := []string{"example", "example-panic"} 34 | r := c.Keys() 35 | if len(ex) != len(r) { 36 | t.Errorf("Expected %#v. Result %#v\n", ex, r) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goforever [![Build Status](https://travis-ci.org/gwoo/goforever.png)](https://travis-ci.org/gwoo/goforever) 2 | 3 | Config based process manager. Goforever could be used in place of supervisor, runit, node-forever, etc. 4 | Goforever will start an http server on the specified port. 5 | 6 | Usage of ./goforever: 7 | -conf="goforever.toml": Path to config file. 8 | 9 | ## Running 10 | Help. 11 | 12 | ./goforever -h 13 | 14 | Daemonize main process. 15 | 16 | ./goforever start 17 | 18 | Run main process and output to current session. 19 | 20 | ./goforever 21 | 22 | ## CLI 23 | list List processes. 24 | show [process] Show a main proccess or named process. 25 | start [process] Start a main proccess or named process. 26 | stop [process] Stop a main proccess or named process. 27 | restart [process] Restart a main proccess or named process. 28 | 29 | ## HTTP API 30 | 31 | Return a list of managed processes 32 | 33 | GET host:port/ 34 | 35 | Start the process 36 | 37 | POST host:port/:name 38 | 39 | Restart the process 40 | 41 | PUT host:port/:name 42 | 43 | Stop the process 44 | 45 | DELETE host:port/:name 46 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/BurntSushi/toml" 11 | ) 12 | 13 | type Config struct { 14 | IP string 15 | Port string 16 | Username string 17 | Password string 18 | Daemonize bool 19 | Pidfile Pidfile 20 | Logfile string 21 | Errfile string 22 | Processes []*Process `toml:"process"` 23 | } 24 | 25 | func (c Config) Keys() []string { 26 | keys := []string{} 27 | for _, p := range c.Processes { 28 | keys = append(keys, p.Name) 29 | } 30 | return keys 31 | } 32 | 33 | func (c Config) Get(key string) *Process { 34 | for _, p := range c.Processes { 35 | if p.Name == key { 36 | return p 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func LoadConfig(file string) (*Config, error) { 43 | if string(file[0]) != "/" { 44 | wd, err := os.Getwd() 45 | if err != nil { 46 | return nil, err 47 | } 48 | file = filepath.Join(wd, file) 49 | } 50 | var c *Config 51 | if _, err := toml.DecodeFile(file, &c); err != nil { 52 | return nil, err 53 | } 54 | return c, nil 55 | } 56 | -------------------------------------------------------------------------------- /process_test.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestPidfile(t *testing.T) { 11 | c := &Config{Processes: []*Process{&Process{ 12 | Name: "test", 13 | Pidfile: "test.pid", 14 | }}, 15 | } 16 | p := c.Get("test") 17 | err := p.Pidfile.write(100) 18 | if err != nil { 19 | t.Errorf("Error: %s.", err) 20 | return 21 | } 22 | ex := 100 23 | r := p.Pidfile.read() 24 | if ex != r { 25 | t.Errorf("Expected %#v. Result %#v\n", ex, r) 26 | } 27 | 28 | s := p.Pidfile.delete() 29 | if s != true { 30 | t.Error("Failed to remove pidfile.") 31 | return 32 | } 33 | } 34 | 35 | func TestProcessStart(t *testing.T) { 36 | c := &Config{Processes: []*Process{&Process{ 37 | Name: "bash", 38 | Command: "/bin/bash", 39 | Args: []string{"foo", "bar"}, 40 | Pidfile: "echo.pid", 41 | Logfile: "debug.log", 42 | Errfile: "error.log", 43 | Respawn: 3, 44 | }}, 45 | } 46 | p := c.Get("bash") 47 | p.start("bash") 48 | ex := 0 49 | r := p.x.Pid 50 | if ex >= r { 51 | t.Errorf("Expected %#v < %#v\n", ex, r) 52 | } 53 | p.stop() 54 | } 55 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "github.com/gwoo/greq" 14 | ) 15 | 16 | func TestListHandler(t *testing.T) { 17 | daemon.children = children{ 18 | "test": &Process{Name: "test"}, 19 | } 20 | body, _ := newTestResponse("GET", "/", nil) 21 | ex := fmt.Sprintf("%s", string([]byte(`["test"]`))) 22 | r := fmt.Sprintf("%s", string(body)) 23 | if ex != r { 24 | t.Errorf("\nExpected = %v\nResult = %v\n", ex, r) 25 | } 26 | } 27 | 28 | func TestShowHandler(t *testing.T) { 29 | daemon.children = children{ 30 | "test": &Process{Name: "test"}, 31 | } 32 | body, _ := newTestResponse("GET", "/test", nil) 33 | e := []byte(`{"Name":"test","Command":"","Args":null,"Pidfile":"","Logfile":"","Errfile":"","Path":"","Respawn":0,"Delay":"","Ping":"","Pid":0,"Status":""}`) 34 | ex := fmt.Sprintf("%s", e) 35 | r := fmt.Sprintf("%s", body) 36 | if ex != r { 37 | t.Errorf("\nExpected = %v\nResult = %v\n", ex, r) 38 | } 39 | } 40 | 41 | func TestPostHandler(t *testing.T) { 42 | daemon.children = children{ 43 | "test": &Process{Name: "test", Command: "/bin/echo", Args: []string{"woohoo"}}, 44 | } 45 | body, _ := newTestResponse("POST", "/test", nil) 46 | e := []byte(`{"Name":"test","Command":"/bin/echo","Args":["woohoo"],"Pidfile":"","Logfile":"","Errfile":"","Path":"","Respawn":0,"Delay":"","Ping":"","Pid":0,"Status":"stopped"}`) 47 | ex := fmt.Sprintf("%s", e) 48 | r := fmt.Sprintf("%s", body) 49 | if ex != r { 50 | t.Errorf("\nExpected = %v\nResult = %v\n", ex, r) 51 | } 52 | } 53 | 54 | func newTestResponse(method string, path string, body io.Reader) ([]byte, *http.Response) { 55 | ts := httptest.NewServer(http.HandlerFunc(Handler)) 56 | defer ts.Close() 57 | url := ts.URL + path 58 | b, r, _ := greq.Do(method, url, nil, body) 59 | return b, r 60 | } 61 | -------------------------------------------------------------------------------- /goforever.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "github.com/gwoo/greq" 13 | ) 14 | 15 | var conf = flag.String("conf", "goforever.toml", "Path to config file.") 16 | var config *Config 17 | var daemon *Process 18 | 19 | var Usage = func() { 20 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 21 | flag.PrintDefaults() 22 | usage := ` 23 | Commands 24 | list List processes. 25 | show [name] Show main proccess or named process. 26 | start [name] Start main proccess or named process. 27 | stop [name] Stop main proccess or named process. 28 | restart [name] Restart main proccess or named process. 29 | ` 30 | fmt.Fprintln(os.Stderr, usage) 31 | } 32 | 33 | func init() { 34 | flag.Usage = Usage 35 | flag.Parse() 36 | setConfig() 37 | daemon = &Process{ 38 | Name: "goforever", 39 | Args: []string{}, 40 | Command: "goforever", 41 | Pidfile: config.Pidfile, 42 | Logfile: config.Logfile, 43 | Errfile: config.Errfile, 44 | Respawn: 1, 45 | } 46 | } 47 | 48 | func main() { 49 | if len(flag.Args()) > 0 { 50 | fmt.Printf("%s", Cli()) 51 | return 52 | } 53 | if len(flag.Args()) == 0 { 54 | RunDaemon() 55 | HttpServer() 56 | return 57 | } 58 | } 59 | 60 | func Cli() string { 61 | var o []byte 62 | var err error 63 | sub := flag.Arg(0) 64 | name := flag.Arg(1) 65 | req := greq.New(host(), true) 66 | if sub == "list" { 67 | o, _, err = req.Get("/") 68 | } 69 | if name == "" { 70 | if sub == "start" { 71 | daemon.Args = append(daemon.Args, os.Args[2:]...) 72 | return daemon.start(daemon.Name) 73 | } 74 | _, _, err = daemon.find() 75 | if err != nil { 76 | return fmt.Sprintf("Error: %s.\n", err) 77 | } 78 | if sub == "show" { 79 | return fmt.Sprintf("%s.\n", daemon.String()) 80 | } 81 | if sub == "stop" { 82 | message := daemon.stop() 83 | return message 84 | } 85 | if sub == "restart" { 86 | ch, message := daemon.restart() 87 | fmt.Print(message) 88 | return fmt.Sprintf("%s\n", <-ch) 89 | } 90 | } 91 | if name != "" { 92 | path := fmt.Sprintf("/%s", name) 93 | switch sub { 94 | case "show": 95 | o, _, err = req.Get(path) 96 | case "start": 97 | o, _, err = req.Post(path, nil) 98 | case "stop": 99 | o, _, err = req.Delete(path) 100 | case "restart": 101 | o, _, err = req.Put(path, nil) 102 | } 103 | } 104 | if err != nil { 105 | fmt.Printf("Process error: %s", err) 106 | } 107 | return fmt.Sprintf("%s\n", o) 108 | } 109 | 110 | func RunDaemon() { 111 | daemon.children = make(map[string]*Process, 0) 112 | for _, name := range config.Keys() { 113 | daemon.children[name] = config.Get(name) 114 | } 115 | daemon.run() 116 | } 117 | 118 | func setConfig() { 119 | var err error 120 | config, err = LoadConfig(*conf) 121 | if err != nil { 122 | log.Fatalf("%s", err) 123 | return 124 | } 125 | if config.Username == "" { 126 | log.Fatalf("Config error: %s", "Please provide a username.") 127 | return 128 | } 129 | if config.Password == "" { 130 | log.Fatalf("Config error: %s", "Please provide a password.") 131 | return 132 | } 133 | if config.Port == "" { 134 | config.Port = "2224" 135 | } 136 | if config.IP == "" { 137 | config.IP = "0.0.0.0" 138 | } 139 | } 140 | 141 | func host() string { 142 | scheme := "https" 143 | if isHttps() == false { 144 | scheme = "http" 145 | } 146 | return fmt.Sprintf("%s://%s:%s@0.0.0.0:%s", 147 | scheme, config.Username, config.Password, config.Port, 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | func HttpServer() { 17 | http.HandleFunc("/favicon.ico", http.NotFound) 18 | http.HandleFunc("/", AuthHandler(Handler)) 19 | fmt.Printf("goforever serving port %s\n", config.Port) 20 | fmt.Printf("goforever serving IP %s\n", config.IP) 21 | bindAddress := fmt.Sprintf("%s:%s", config.IP, config.Port) 22 | if isHttps() == false { 23 | if err := http.ListenAndServe(bindAddress, nil); err != nil { 24 | log.Fatal("ListenAndServe: ", err) 25 | } 26 | return 27 | } 28 | log.Printf("SSL enabled.\n") 29 | if err := http.ListenAndServeTLS(bindAddress, "cert.pem", "key.pem", nil); err != nil { 30 | log.Fatal("ListenAndServeTLS: ", err) 31 | } 32 | } 33 | 34 | func isHttps() bool { 35 | _, cerr := os.Open("cert.pem") 36 | _, kerr := os.Open("key.pem") 37 | 38 | if os.IsNotExist(cerr) || os.IsNotExist(kerr) { 39 | return false 40 | } 41 | return true 42 | } 43 | 44 | func Handler(w http.ResponseWriter, r *http.Request) { 45 | switch r.Method { 46 | case "DELETE": 47 | DeleteHandler(w, r) 48 | return 49 | case "POST": 50 | PostHandler(w, r) 51 | return 52 | case "PUT": 53 | PutHandler(w, r) 54 | return 55 | case "GET": 56 | GetHandler(w, r) 57 | return 58 | } 59 | } 60 | 61 | func GetHandler(w http.ResponseWriter, r *http.Request) { 62 | var output []byte 63 | var err error 64 | switch r.URL.Path[1:] { 65 | case "": 66 | output, err = json.Marshal(daemon.children.keys()) 67 | default: 68 | output, err = json.Marshal(daemon.children.get(r.URL.Path[1:])) 69 | } 70 | if err != nil { 71 | log.Printf("Get Error: %#v", err) 72 | return 73 | } 74 | fmt.Fprintf(w, "%s", output) 75 | } 76 | 77 | func PostHandler(w http.ResponseWriter, r *http.Request) { 78 | name := r.URL.Path[1:] 79 | p := daemon.children.get(name) 80 | if p == nil { 81 | fmt.Fprintf(w, "%s does not exist.", name) 82 | return 83 | } 84 | cp, _, _ := p.find() 85 | if cp != nil { 86 | fmt.Fprintf(w, "%s already running.", name) 87 | return 88 | } 89 | ch := RunProcess(name, p) 90 | fmt.Fprintf(w, "%s", <-ch) 91 | } 92 | 93 | func PutHandler(w http.ResponseWriter, r *http.Request) { 94 | name := r.URL.Path[1:] 95 | p := daemon.children.get(name) 96 | if p == nil { 97 | fmt.Fprintf(w, "%s does not exist.", name) 98 | return 99 | } 100 | p.find() 101 | ch, _ := p.restart() 102 | fmt.Fprintf(w, "%s", <-ch) 103 | } 104 | 105 | func DeleteHandler(w http.ResponseWriter, r *http.Request) { 106 | name := r.URL.Path[1:] 107 | p := daemon.children.get(name) 108 | if p == nil { 109 | fmt.Fprintf(w, "%s does not exist.", name) 110 | return 111 | } 112 | p.find() 113 | p.stop() 114 | fmt.Fprintf(w, "%s stopped.", name) 115 | } 116 | 117 | func AuthHandler(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 118 | return func(w http.ResponseWriter, r *http.Request) { 119 | url := r.URL 120 | for k, v := range r.Header { 121 | fmt.Printf(" %s = %s\n", k, v[0]) 122 | } 123 | auth, ok := r.Header["Authorization"] 124 | if !ok { 125 | log.Printf("Unauthorized access to %s", url) 126 | w.Header().Add("WWW-Authenticate", "basic realm=\"host\"") 127 | w.WriteHeader(http.StatusUnauthorized) 128 | fmt.Fprintf(w, "Not Authorized.") 129 | return 130 | } 131 | encoded := strings.Split(auth[0], " ") 132 | if len(encoded) != 2 || encoded[0] != "Basic" { 133 | log.Printf("Strange Authorization %q", auth) 134 | w.WriteHeader(http.StatusBadRequest) 135 | return 136 | } 137 | decoded, err := base64.StdEncoding.DecodeString(encoded[1]) 138 | if err != nil { 139 | log.Printf("Cannot decode %q: %s", auth, err) 140 | w.WriteHeader(http.StatusBadRequest) 141 | return 142 | } 143 | parts := strings.Split(string(decoded), ":") 144 | if len(parts) != 2 { 145 | log.Printf("Unknown format for credentials %q", decoded) 146 | w.WriteHeader(http.StatusBadRequest) 147 | return 148 | } 149 | if parts[0] == config.Username && parts[1] == config.Password { 150 | fn(w, r) 151 | return 152 | } 153 | log.Printf("Unauthorized access to %s", url) 154 | w.Header().Add("WWW-Authenticate", "basic realm=\"host\"") 155 | w.WriteHeader(http.StatusUnauthorized) 156 | fmt.Fprintf(w, "Not Authorized.") 157 | return 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | // goforever - processes management 2 | // Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo). 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | var ping = "1m" 19 | 20 | //Run the process 21 | func RunProcess(name string, p *Process) chan *Process { 22 | ch := make(chan *Process) 23 | go func() { 24 | p.start(name) 25 | p.ping(ping, func(time time.Duration, p *Process) { 26 | if p.Pid > 0 { 27 | p.respawns = 0 28 | fmt.Printf("%s refreshed after %s.\n", p.Name, time) 29 | p.Status = "running" 30 | } 31 | }) 32 | go p.watch() 33 | ch <- p 34 | }() 35 | return ch 36 | } 37 | 38 | type Process struct { 39 | Name string 40 | Command string 41 | Args []string 42 | Pidfile Pidfile 43 | Logfile string 44 | Errfile string 45 | Path string 46 | Respawn int 47 | Delay string 48 | Ping string 49 | Pid int 50 | Status string 51 | x *os.Process 52 | respawns int 53 | children children 54 | } 55 | 56 | func (p *Process) String() string { 57 | js, err := json.Marshal(p) 58 | if err != nil { 59 | log.Print(err) 60 | return "" 61 | } 62 | return string(js) 63 | } 64 | 65 | //Find a process by name 66 | func (p *Process) find() (*os.Process, string, error) { 67 | if p.Pidfile == "" { 68 | return nil, "", errors.New("Pidfile is empty.") 69 | } 70 | if pid := p.Pidfile.read(); pid > 0 { 71 | process, err := os.FindProcess(pid) 72 | if err != nil { 73 | return nil, "", err 74 | } 75 | p.x = process 76 | p.Pid = process.Pid 77 | p.Status = "running" 78 | message := fmt.Sprintf("%s is %#v\n", p.Name, process.Pid) 79 | return process, message, nil 80 | } 81 | message := fmt.Sprintf("%s not running.\n", p.Name) 82 | return nil, message, errors.New(fmt.Sprintf("Could not find process %s.", p.Name)) 83 | } 84 | 85 | //Start the process 86 | func (p *Process) start(name string) string { 87 | p.Name = name 88 | wd, _ := os.Getwd() 89 | proc := &os.ProcAttr{ 90 | Dir: wd, 91 | Env: os.Environ(), 92 | Files: []*os.File{ 93 | os.Stdin, 94 | NewLog(p.Logfile), 95 | NewLog(p.Errfile), 96 | }, 97 | } 98 | args := append([]string{p.Name}, p.Args...) 99 | process, err := os.StartProcess(p.Command, args, proc) 100 | if err != nil { 101 | log.Fatalf("%s failed. %s\n", p.Name, err) 102 | return "" 103 | } 104 | err = p.Pidfile.write(process.Pid) 105 | if err != nil { 106 | log.Printf("%s pidfile error: %s\n", p.Name, err) 107 | return "" 108 | } 109 | p.x = process 110 | p.Pid = process.Pid 111 | p.Status = "started" 112 | return fmt.Sprintf("%s is %#v\n", p.Name, process.Pid) 113 | } 114 | 115 | //Stop the process 116 | func (p *Process) stop() string { 117 | if p.x != nil { 118 | // p.x.Kill() this seems to cause trouble 119 | cmd := exec.Command("kill", fmt.Sprintf("%d", p.x.Pid)) 120 | _, err := cmd.CombinedOutput() 121 | if err != nil { 122 | log.Println(err) 123 | } 124 | p.children.stop("all") 125 | } 126 | p.release("stopped") 127 | message := fmt.Sprintf("%s stopped.\n", p.Name) 128 | return message 129 | } 130 | 131 | //Release process and remove pidfile 132 | func (p *Process) release(status string) { 133 | if p.x != nil { 134 | p.x.Release() 135 | } 136 | p.Pid = 0 137 | p.Pidfile.delete() 138 | p.Status = status 139 | } 140 | 141 | //Restart the process 142 | func (p *Process) restart() (chan *Process, string) { 143 | p.stop() 144 | message := fmt.Sprintf("%s restarted.\n", p.Name) 145 | ch := RunProcess(p.Name, p) 146 | return ch, message 147 | } 148 | 149 | //Run callback on the process after given duration. 150 | func (p *Process) ping(duration string, f func(t time.Duration, p *Process)) { 151 | if p.Ping != "" { 152 | duration = p.Ping 153 | } 154 | t, err := time.ParseDuration(duration) 155 | if err != nil { 156 | t, _ = time.ParseDuration(ping) 157 | } 158 | go func() { 159 | select { 160 | case <-time.After(t): 161 | f(t, p) 162 | } 163 | }() 164 | } 165 | 166 | //Watch the process 167 | func (p *Process) watch() { 168 | if p.x == nil { 169 | p.release("stopped") 170 | return 171 | } 172 | status := make(chan *os.ProcessState) 173 | died := make(chan error) 174 | go func() { 175 | state, err := p.x.Wait() 176 | if err != nil { 177 | died <- err 178 | return 179 | } 180 | status <- state 181 | }() 182 | select { 183 | case s := <-status: 184 | if p.Status == "stopped" { 185 | return 186 | } 187 | fmt.Fprintf(os.Stderr, "%s %s\n", p.Name, s) 188 | fmt.Fprintf(os.Stderr, "%s success = %#v\n", p.Name, s.Success()) 189 | fmt.Fprintf(os.Stderr, "%s exited = %#v\n", p.Name, s.Exited()) 190 | p.respawns++ 191 | if p.respawns > p.Respawn { 192 | p.release("exited") 193 | log.Printf("%s respawn limit reached.\n", p.Name) 194 | return 195 | } 196 | fmt.Fprintf(os.Stderr, "%s respawns = %#v\n", p.Name, p.respawns) 197 | if p.Delay != "" { 198 | t, _ := time.ParseDuration(p.Delay) 199 | time.Sleep(t) 200 | } 201 | p.restart() 202 | p.Status = "restarted" 203 | case err := <-died: 204 | p.release("killed") 205 | log.Printf("%d %s killed = %#v", p.x.Pid, p.Name, err) 206 | } 207 | } 208 | 209 | //Run child processes 210 | func (p *Process) run() { 211 | for name, p := range p.children { 212 | RunProcess(name, p) 213 | } 214 | } 215 | 216 | //Child processes. 217 | type children map[string]*Process 218 | 219 | //Stringify 220 | func (c children) String() string { 221 | js, err := json.Marshal(c) 222 | if err != nil { 223 | log.Print(err) 224 | return "" 225 | } 226 | return string(js) 227 | } 228 | 229 | //Get child processes names. 230 | func (c children) keys() []string { 231 | keys := []string{} 232 | for k, _ := range c { 233 | keys = append(keys, k) 234 | } 235 | return keys 236 | } 237 | 238 | //Get child process. 239 | func (c children) get(key string) *Process { 240 | if v, ok := c[key]; ok { 241 | return v 242 | } 243 | return nil 244 | } 245 | 246 | func (c children) stop(name string) { 247 | if name == "all" { 248 | for name, p := range c { 249 | p.stop() 250 | delete(c, name) 251 | } 252 | return 253 | } 254 | p := c.get(name) 255 | p.stop() 256 | delete(c, name) 257 | } 258 | 259 | type Pidfile string 260 | 261 | //Read the pidfile. 262 | func (f *Pidfile) read() int { 263 | data, err := ioutil.ReadFile(string(*f)) 264 | if err != nil { 265 | return 0 266 | } 267 | pid, err := strconv.ParseInt(string(data), 0, 32) 268 | if err != nil { 269 | return 0 270 | } 271 | return int(pid) 272 | } 273 | 274 | //Write the pidfile. 275 | func (f *Pidfile) write(data int) error { 276 | err := ioutil.WriteFile(string(*f), []byte(strconv.Itoa(data)), 0660) 277 | if err != nil { 278 | return err 279 | } 280 | return nil 281 | } 282 | 283 | //Delete the pidfile 284 | func (f *Pidfile) delete() bool { 285 | _, err := os.Stat(string(*f)) 286 | if err != nil { 287 | return true 288 | } 289 | err = os.Remove(string(*f)) 290 | if err == nil { 291 | return true 292 | } 293 | return false 294 | } 295 | 296 | //Create a new file for logging 297 | func NewLog(path string) *os.File { 298 | if path == "" { 299 | return nil 300 | } 301 | file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660) 302 | if err != nil { 303 | log.Fatalf("%s", err) 304 | return nil 305 | } 306 | return file 307 | } 308 | --------------------------------------------------------------------------------