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