Initial Commit.

TODO: log rotation
This commit is contained in:
gwoo
2013-12-31 11:02:48 -08:00
commit ecd770dcaf
10 changed files with 859 additions and 0 deletions

19
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}