mirror of
https://github.com/gwoo/goforever.git
synced 2025-09-26 19:41:10 +08:00
Initial Commit.
TODO: log rotation
This commit is contained in:
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## Goforever
|
||||
|
||||
Config based process manager. Goforever could be used in place of supervisor, upstart, launchctl, etc.
|
||||
Goforever will start an http server on the specified port.
|
||||
|
||||
Usage of ./goforever:
|
||||
-conf="goforever.toml": Path to config file.
|
||||
-d=false: Daemonize goforever. Must be first flag
|
||||
-password="test": Password for basic auth.
|
||||
-port=8080: Port for the server.
|
||||
-username="demo": Username for basic auth.
|
||||
|
||||
## CLI
|
||||
list List processes.
|
||||
show <process> Show a process.
|
||||
start <process> Start a process.
|
||||
stop <process> Stop a process.
|
||||
restart <process> Restart a process.
|
||||
|
||||
|
||||
## HTTP API
|
||||
|
||||
Return a list of managed processes
|
||||
|
||||
GET host:port/
|
||||
|
||||
Start the process
|
||||
|
||||
POST host:port/:name
|
||||
|
||||
Restart the process
|
||||
|
||||
PUT host:port/:name
|
||||
|
||||
Stop the process
|
||||
|
||||
DELETE host:port/:name
|
49
config.go
Normal file
49
config.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Username string
|
||||
Password string
|
||||
Processes []*Process `toml:"process"`
|
||||
}
|
||||
|
||||
func (c Config) Keys() []string {
|
||||
keys := []string{}
|
||||
for _, p := range c.Processes {
|
||||
keys = append(keys, p.Name)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (c Config) Get(key string) *Process {
|
||||
for _, p := range c.Processes {
|
||||
if p.Name == key {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadConfig(file string) (*Config, error) {
|
||||
if string(file[0]) != "/" {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file = filepath.Join(wd, file)
|
||||
}
|
||||
var c *Config
|
||||
if _, err := toml.DecodeFile(file, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
38
config_test.go
Normal file
38
config_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
r, err := LoadConfig("goforever.toml")
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error creating config %s.", err)
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
t.Errorf("Expected %#v. Result %#v\n", r, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigGet(t *testing.T) {
|
||||
c, _ := LoadConfig("goforever.toml")
|
||||
ex := "example/example.pid"
|
||||
r := string(c.Get("example").Pidfile)
|
||||
if ex != r {
|
||||
t.Errorf("Expected %#v. Result %#v\n", ex, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigKeys(t *testing.T) {
|
||||
c, _ := LoadConfig("goforever.toml")
|
||||
ex := []string{"example", "example-panic"}
|
||||
r := c.Keys()
|
||||
if len(ex) != len(r) {
|
||||
t.Errorf("Expected %#v. Result %#v\n", ex, r)
|
||||
}
|
||||
}
|
125
goforever.go
Normal file
125
goforever.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gwoo/greq"
|
||||
)
|
||||
|
||||
var d = flag.Bool("d", false, "Daemonize goforever. Must be first flag")
|
||||
var conf = flag.String("conf", "goforever.toml", "Path to config file.")
|
||||
var port = flag.Int("port", 8080, "Port for the server.")
|
||||
var username = flag.String("username", "demo", "Username for basic auth.")
|
||||
var password = flag.String("password", "test", "Password for basic auth.")
|
||||
var server string
|
||||
var config *Config
|
||||
|
||||
var Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
usage := `
|
||||
Process subcommands
|
||||
list List processes.
|
||||
show <process> Show a process.
|
||||
start <process> Start a process.
|
||||
stop <process> Stop a process.
|
||||
restart <process> Restart a process.
|
||||
`
|
||||
fmt.Fprintln(os.Stderr, usage)
|
||||
}
|
||||
|
||||
func init() {
|
||||
setConfig()
|
||||
setHost()
|
||||
daemon = &Process{
|
||||
Name: "goforever",
|
||||
Args: []string{"./goforever"},
|
||||
Command: "goforever",
|
||||
Pidfile: "goforever.pid",
|
||||
Logfile: "goforever.debug.log",
|
||||
Errfile: "goforever.errors.log",
|
||||
Respawn: 1,
|
||||
}
|
||||
flag.Usage = Usage
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
daemon.Name = "goforever"
|
||||
if *d == true {
|
||||
daemon.Args = append(daemon.Args, os.Args[2:]...)
|
||||
daemon.start(daemon.Name)
|
||||
return
|
||||
}
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Printf("%s\n", Cli())
|
||||
return
|
||||
}
|
||||
if len(flag.Args()) == 0 {
|
||||
RunDaemon()
|
||||
HttpServer()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func Cli() string {
|
||||
sub := flag.Arg(0)
|
||||
name := flag.Arg(1)
|
||||
var o []byte
|
||||
|
||||
if sub == "list" {
|
||||
o, _ = greq.Get("/")
|
||||
}
|
||||
if name != "" {
|
||||
switch sub {
|
||||
case "show":
|
||||
o, _ = greq.Get("/" + name)
|
||||
case "start":
|
||||
o, _ = greq.Post("/"+name, nil)
|
||||
case "stop":
|
||||
o, _ = greq.Delete("/" + name)
|
||||
case "restart":
|
||||
o, _ = greq.Put("/"+name, nil)
|
||||
}
|
||||
}
|
||||
return string(o)
|
||||
}
|
||||
|
||||
func RunDaemon() {
|
||||
fmt.Printf("Running %s.\n", daemon.Name)
|
||||
daemon.children = make(map[string]*Process, 0)
|
||||
for _, name := range config.Keys() {
|
||||
daemon.children[name] = config.Get(name)
|
||||
}
|
||||
daemon.run()
|
||||
}
|
||||
|
||||
func setConfig() {
|
||||
c, err := LoadConfig(*conf)
|
||||
if err != nil {
|
||||
log.Fatalf("Config error: %s", err)
|
||||
return
|
||||
}
|
||||
config = c
|
||||
|
||||
if config.Username != "" {
|
||||
username = &config.Username
|
||||
}
|
||||
if config.Password != "" {
|
||||
password = &config.Password
|
||||
}
|
||||
}
|
||||
|
||||
func setHost() {
|
||||
scheme := "https"
|
||||
if isHttps() == false {
|
||||
scheme = "http"
|
||||
}
|
||||
greq.Host = fmt.Sprintf("%s://%s:%s@0.0:%d", scheme, *username, *password, *port)
|
||||
}
|
21
goforever.toml
Normal file
21
goforever.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
username = "go"
|
||||
password = "forever"
|
||||
|
||||
|
||||
[[process]]
|
||||
name = "example-panic"
|
||||
command = "./example/example-panic"
|
||||
pidfile = "example/example-panic.pid"
|
||||
logfile = "example/logs/example-panic.debug.log"
|
||||
errfile = "example/logs/example-panic.errors.log"
|
||||
respawn = 1
|
||||
ping = "30s"
|
||||
|
||||
[[process]]
|
||||
name = "example"
|
||||
command = "./example/example"
|
||||
pidfile = "example/example.pid"
|
||||
logfile = "example/logs/example.debug.log"
|
||||
errfile = "example/logs/example.errors.log"
|
||||
respawn = 1
|
||||
|
156
http.go
Normal file
156
http.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var daemon *Process
|
||||
|
||||
func HttpServer() {
|
||||
http.HandleFunc("/favicon.ico", http.NotFound)
|
||||
http.HandleFunc("/", AuthHandler(Handler))
|
||||
fmt.Printf("goforever serving port %d\n", *port)
|
||||
|
||||
if isHttps() == false {
|
||||
http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)
|
||||
return
|
||||
}
|
||||
log.Printf("SSL enabled.\n")
|
||||
http.ListenAndServeTLS(fmt.Sprintf(":%d", *port), "cert.pem", "key.pem", nil)
|
||||
}
|
||||
|
||||
func isHttps() bool {
|
||||
_, cerr := os.Open("cert.pem")
|
||||
_, kerr := os.Open("key.pem")
|
||||
|
||||
if os.IsNotExist(cerr) || os.IsNotExist(kerr) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "DELETE":
|
||||
DeleteHandler(w, r)
|
||||
return
|
||||
case "POST":
|
||||
PostHandler(w, r)
|
||||
return
|
||||
case "PUT":
|
||||
PutHandler(w, r)
|
||||
return
|
||||
case "GET":
|
||||
GetHandler(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func GetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var output []byte
|
||||
var err error
|
||||
switch r.URL.Path[1:] {
|
||||
case "":
|
||||
output, err = json.Marshal(daemon.children.keys())
|
||||
default:
|
||||
output, err = json.Marshal(daemon.children.get(r.URL.Path[1:]))
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Get Error: %#v", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "%s", output)
|
||||
}
|
||||
|
||||
func PostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Path[1:]
|
||||
p := daemon.children.get(name)
|
||||
if p == nil {
|
||||
fmt.Fprintf(w, "%s does not exist.", name)
|
||||
return
|
||||
}
|
||||
cp, _ := p.find()
|
||||
if cp != nil {
|
||||
fmt.Fprintf(w, "%s already running.", name)
|
||||
return
|
||||
}
|
||||
ch := RunProcess(name, p)
|
||||
fmt.Fprintf(w, "%s", <-ch)
|
||||
}
|
||||
|
||||
func PutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Path[1:]
|
||||
p := daemon.children.get(name)
|
||||
if p == nil {
|
||||
fmt.Fprintf(w, "%s does not exist.", name)
|
||||
return
|
||||
}
|
||||
p.find()
|
||||
ch := p.restart()
|
||||
fmt.Fprintf(w, "%s", <-ch)
|
||||
}
|
||||
|
||||
func DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Path[1:]
|
||||
p := daemon.children.get(name)
|
||||
if p == nil {
|
||||
fmt.Fprintf(w, "%s does not exist.", name)
|
||||
return
|
||||
}
|
||||
p.find()
|
||||
p.stop()
|
||||
fmt.Fprintf(w, "%s stopped.", name)
|
||||
}
|
||||
|
||||
func AuthHandler(fn func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL
|
||||
for k, v := range r.Header {
|
||||
fmt.Printf(" %s = %s\n", k, v[0])
|
||||
}
|
||||
auth, ok := r.Header["Authorization"]
|
||||
if !ok {
|
||||
log.Printf("Unauthorized access to %s", url)
|
||||
w.Header().Add("WWW-Authenticate", "basic realm=\"host\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, "Not Authorized.")
|
||||
return
|
||||
}
|
||||
encoded := strings.Split(auth[0], " ")
|
||||
if len(encoded) != 2 || encoded[0] != "Basic" {
|
||||
log.Printf("Strange Authorization %q", auth)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded[1])
|
||||
if err != nil {
|
||||
log.Printf("Cannot decode %q: %s", auth, err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
parts := strings.Split(string(decoded), ":")
|
||||
if len(parts) != 2 {
|
||||
log.Printf("Unknown format for credentials %q", decoded)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if parts[0] == *username && parts[1] == *password {
|
||||
fn(w, r)
|
||||
return
|
||||
}
|
||||
log.Printf("Unauthorized access to %s", url)
|
||||
w.Header().Add("WWW-Authenticate", "basic realm=\"host\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, "Not Authorized.")
|
||||
return
|
||||
}
|
||||
}
|
59
http_test.go
Normal file
59
http_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gwoo/greq"
|
||||
)
|
||||
|
||||
func TestListHandler(t *testing.T) {
|
||||
daemon.children = children{
|
||||
"test": &Process{Name: "test"},
|
||||
}
|
||||
body, _ := newTestResponse("GET", "/", nil)
|
||||
ex := fmt.Sprintf("%s", string([]byte(`["test"]`)))
|
||||
r := fmt.Sprintf("%s", string(body))
|
||||
if ex != r {
|
||||
t.Errorf("\nExpected = %v\nResult = %v\n", ex, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowHandler(t *testing.T) {
|
||||
daemon.children = children{
|
||||
"test": &Process{Name: "test"},
|
||||
}
|
||||
body, _ := newTestResponse("GET", "/test", nil)
|
||||
e := []byte(`{"Name":"test","Command":"","Args":null,"Pidfile":"","Logfile":"","Errfile":"","Path":"","Respawn":0,"Ping":"","Pid":0,"Status":""}`)
|
||||
ex := fmt.Sprintf("%s", e)
|
||||
r := fmt.Sprintf("%s", body)
|
||||
if ex != r {
|
||||
t.Errorf("\nExpected = %v\nResult = %v\n", ex, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostHandler(t *testing.T) {
|
||||
daemon.children = children{
|
||||
"test": &Process{Name: "test", Command: "/bin/echo", Args: []string{"woohoo"}},
|
||||
}
|
||||
body, _ := newTestResponse("POST", "/test", nil)
|
||||
e := []byte(`{"Name":"test","Command":"/bin/echo","Args":["woohoo"],"Pidfile":"","Logfile":"","Errfile":"","Path":"","Respawn":0,"Ping":"","Pid":0,"Status":"stopped"}`)
|
||||
ex := fmt.Sprintf("%s", e)
|
||||
r := fmt.Sprintf("%s", body)
|
||||
if ex != r {
|
||||
t.Errorf("\nExpected = %v\nResult = %v\n", ex, r)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestResponse(method string, path string, body io.Reader) ([]byte, *http.Response) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(Handler))
|
||||
defer ts.Close()
|
||||
url := ts.URL + path
|
||||
return greq.Request(method, url, body)
|
||||
}
|
299
process.go
Normal file
299
process.go
Normal file
@@ -0,0 +1,299 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ping = "1m"
|
||||
|
||||
//Run the process
|
||||
func RunProcess(name string, p *Process) chan *Process {
|
||||
ch := make(chan *Process)
|
||||
go func() {
|
||||
p.start(name)
|
||||
p.ping(ping, func(time time.Duration, p *Process) {
|
||||
if p.Pid > 0 {
|
||||
p.respawns = 0
|
||||
fmt.Printf("%s refreshed after %s.\n", p.Name, time)
|
||||
p.Status = "running"
|
||||
}
|
||||
})
|
||||
go p.watch()
|
||||
ch <- p
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Name string
|
||||
Command string
|
||||
Args []string
|
||||
Pidfile Pidfile
|
||||
Logfile string
|
||||
Errfile string
|
||||
Path string
|
||||
Respawn int
|
||||
Ping string
|
||||
Pid int
|
||||
Status string
|
||||
x *os.Process
|
||||
respawns int
|
||||
children children
|
||||
}
|
||||
|
||||
func (p *Process) String() string {
|
||||
js, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
return string(js)
|
||||
}
|
||||
|
||||
//Find a process by name
|
||||
func (p *Process) find() (*os.Process, error) {
|
||||
if p.Pidfile == "" {
|
||||
return nil, errors.New("Pidfile is empty.")
|
||||
}
|
||||
if pid := p.Pidfile.read(); pid > 0 {
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.x = process
|
||||
p.Pid = process.Pid
|
||||
fmt.Printf("%s is %#v\n", p.Name, process.Pid)
|
||||
p.Status = "running"
|
||||
return process, nil
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("Could not find process %s.", p.Name))
|
||||
}
|
||||
|
||||
//Start the process
|
||||
func (p *Process) start(name string) {
|
||||
p.Name = name
|
||||
wd, _ := os.Getwd()
|
||||
proc := &os.ProcAttr{
|
||||
Dir: wd,
|
||||
Env: os.Environ(),
|
||||
Files: []*os.File{
|
||||
os.Stdin,
|
||||
NewLog(p.Logfile),
|
||||
NewLog(p.Errfile),
|
||||
},
|
||||
}
|
||||
process, err := os.StartProcess(p.Command, p.Args, proc)
|
||||
if err != nil {
|
||||
log.Fatalf("%s failed. %s", p.Name, err)
|
||||
return
|
||||
}
|
||||
err = p.Pidfile.write(process.Pid)
|
||||
if err != nil {
|
||||
log.Printf("%s pidfile error: %s", p.Name, err)
|
||||
return
|
||||
}
|
||||
p.x = process
|
||||
p.Pid = process.Pid
|
||||
fmt.Printf("%s is %#v\n", p.Name, process.Pid)
|
||||
p.Status = "started"
|
||||
}
|
||||
|
||||
//Stop the process
|
||||
func (p *Process) stop() {
|
||||
if p.x != nil {
|
||||
// p.x.Kill() this seems to cause trouble
|
||||
cmd := exec.Command("kill", fmt.Sprintf("%d", p.x.Pid))
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
p.children.stop("all")
|
||||
}
|
||||
p.release("stopped")
|
||||
fmt.Printf("%s stopped.\n", p.Name)
|
||||
|
||||
}
|
||||
|
||||
//Release process and remove pidfile
|
||||
func (p *Process) release(status string) {
|
||||
if p.x != nil {
|
||||
p.x.Release()
|
||||
}
|
||||
p.Pid = 0
|
||||
p.Pidfile.delete()
|
||||
p.Status = status
|
||||
}
|
||||
|
||||
//Restart the process
|
||||
func (p *Process) restart() chan *Process {
|
||||
p.stop()
|
||||
fmt.Fprintf(os.Stderr, "%s restarted.\n", p.Name)
|
||||
return RunProcess(p.Name, p)
|
||||
}
|
||||
|
||||
//Run callback on the process after given duration.
|
||||
func (p *Process) ping(duration string, f func(t time.Duration, p *Process)) {
|
||||
if p.Ping != "" {
|
||||
duration = p.Ping
|
||||
}
|
||||
t, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
t, _ = time.ParseDuration(ping)
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(t):
|
||||
f(t, p)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//Watch the process
|
||||
func (p *Process) watch() {
|
||||
if p.x == nil {
|
||||
p.release("stopped")
|
||||
return
|
||||
}
|
||||
status := make(chan *os.ProcessState)
|
||||
died := make(chan error)
|
||||
go func() {
|
||||
state, err := p.x.Wait()
|
||||
if err != nil {
|
||||
died <- err
|
||||
return
|
||||
}
|
||||
status <- state
|
||||
}()
|
||||
select {
|
||||
case s := <-status:
|
||||
if p.Status == "stopped" {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", p.Name, s)
|
||||
fmt.Fprintf(os.Stderr, "%s success = %#v\n", p.Name, s.Success())
|
||||
fmt.Fprintf(os.Stderr, "%s exited = %#v\n", p.Name, s.Exited())
|
||||
p.respawns++
|
||||
if p.respawns > p.Respawn {
|
||||
p.release("exited")
|
||||
log.Printf("%s respawn limit reached.\n", p.Name)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s respawns = %#v\n", p.Name, p.respawns)
|
||||
p.restart()
|
||||
p.Status = "restarted"
|
||||
case err := <-died:
|
||||
p.release("killed")
|
||||
log.Printf("%d %s killed = %#v", p.x.Pid, p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
//Run child processes
|
||||
func (p *Process) run() {
|
||||
for name, p := range p.children {
|
||||
RunProcess(name, p)
|
||||
}
|
||||
}
|
||||
|
||||
//Child processes.
|
||||
type children map[string]*Process
|
||||
|
||||
//Stringify
|
||||
func (c children) String() string {
|
||||
js, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return ""
|
||||
}
|
||||
return string(js)
|
||||
}
|
||||
|
||||
//Get child processes names.
|
||||
func (c children) keys() []string {
|
||||
keys := []string{}
|
||||
for k, _ := range c {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
//Get child process.
|
||||
func (c children) get(key string) *Process {
|
||||
if v, ok := c[key]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c children) stop(name string) {
|
||||
if name == "all" {
|
||||
for name, p := range c {
|
||||
p.stop()
|
||||
delete(c, name)
|
||||
}
|
||||
return
|
||||
}
|
||||
p := c.get(name)
|
||||
p.stop()
|
||||
delete(c, name)
|
||||
}
|
||||
|
||||
type Pidfile string
|
||||
|
||||
//Read the pidfile.
|
||||
func (f *Pidfile) read() int {
|
||||
data, err := ioutil.ReadFile(string(*f))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
pid, err := strconv.ParseInt(string(data), 0, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int(pid)
|
||||
}
|
||||
|
||||
//Write the pidfile.
|
||||
func (f *Pidfile) write(data int) error {
|
||||
err := ioutil.WriteFile(string(*f), []byte(strconv.Itoa(data)), 0660)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Delete the pidfile
|
||||
func (f *Pidfile) delete() bool {
|
||||
_, err := os.Stat(string(*f))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
err = os.Remove(string(*f))
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//Create a new file for logging
|
||||
func NewLog(path string) *os.File {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660)
|
||||
if err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
return nil
|
||||
}
|
||||
return file
|
||||
}
|
56
process_test.go
Normal file
56
process_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// goforever - processes management
|
||||
// Copyright (c) 2013 Garrett Woodworth (https://github.com/gwoo).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPidfile(t *testing.T) {
|
||||
c := &Config{"", "",
|
||||
[]*Process{&Process{
|
||||
Name: "test",
|
||||
Pidfile: "test.pid",
|
||||
}},
|
||||
}
|
||||
p := c.Get("test")
|
||||
err := p.Pidfile.write(100)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s.", err)
|
||||
return
|
||||
}
|
||||
ex := 100
|
||||
r := p.Pidfile.read()
|
||||
if ex != r {
|
||||
t.Errorf("Expected %#v. Result %#v\n", ex, r)
|
||||
}
|
||||
|
||||
s := p.Pidfile.delete()
|
||||
if s != true {
|
||||
t.Error("Failed to remove pidfile.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessStart(t *testing.T) {
|
||||
c := &Config{
|
||||
"", "",
|
||||
[]*Process{&Process{
|
||||
Name: "example",
|
||||
Command: "example/example",
|
||||
Pidfile: "example/example.pid",
|
||||
Logfile: "example/logs/example.debug.log",
|
||||
Errfile: "example/logs/example.errors.log",
|
||||
Respawn: 3,
|
||||
}},
|
||||
}
|
||||
p := c.Get("example")
|
||||
p.start("example")
|
||||
ex := 0
|
||||
r := p.x.Pid
|
||||
if ex >= r {
|
||||
t.Errorf("Expected %#v < %#v\n", ex, r)
|
||||
}
|
||||
p.stop()
|
||||
}
|
Reference in New Issue
Block a user