diff --git a/gosuv.go b/gosuv.go index 8fd56fd..29b72b6 100644 --- a/gosuv.go +++ b/gosuv.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "os/exec" + "path/filepath" "strconv" "time" @@ -64,7 +65,24 @@ func ServAction(ctx *cli.Context) { } func StatusAction(ctx *cli.Context) { - println("status todo") + programs := make([]*Program, 0) + res, err := goreq.Request{ + Method: "GET", + Uri: buildURI(ctx, "/api/programs"), + }.Do() + if err != nil { + log.Fatal(err) + } + if res.StatusCode != http.StatusOK { + log.Fatal(res.Body.ToString()) + } + if err = res.Body.FromJsonTo(&programs); err != nil { + log.Fatal(err) + } + fmt.Printf("%10s\t%s\n", "NAME", "STATUS") + for _, p := range programs { + fmt.Printf("%10s\t%s\n", p.Info.Name, p.Status) + } } func AddAction(ctx *cli.Context) { @@ -170,9 +188,10 @@ func init() { Action: wrapAction(VersionAction), }, { - Name: "status", - Usage: "show program status", - Action: StatusAction, + Name: "status", + Aliases: []string{"st"}, + Usage: "show program status", + Action: StatusAction, }, { Name: "add", @@ -207,13 +226,14 @@ func init() { } } -const ( - GOSUV_HOME = "$HOME/.gosuv" - GOSUV_CONFIG = "gosuv.json" +var ( + GOSUV_HOME = os.ExpandEnv("$HOME/.gosuv") + GOSUV_CONFIG = filepath.Join(GOSUV_HOME, "gosuv.json") GOSUV_VERSION = "0.0.1" ) func main() { MkdirIfNoExists(GOSUV_HOME) + app.HideHelp = false app.RunAndExitOnError() } diff --git a/program.go b/program.go new file mode 100644 index 0000000..ac51ce5 --- /dev/null +++ b/program.go @@ -0,0 +1,142 @@ +package main + +import ( + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "sync" + + "github.com/codeskyblue/kproc" + "github.com/qiniu/log" +) + +func GoFunc(f func() error) chan error { + ch := make(chan error) + go func() { + ch <- f() + }() + return ch +} + +const ( + ST_PENDING = "pending" + ST_RUNNING = "running" + ST_STOPPED = "stopped" + ST_FATAL = "fatal" +) + +type Program struct { + *kproc.Process `json:"-"` + Status string `json:"state"` + Sig chan os.Signal `json:"-"` + Info *ProgramInfo `json:"info"` +} + +func NewProgram(cmd *exec.Cmd, info *ProgramInfo) *Program { + return &Program{ + Process: kproc.ProcCommand(cmd), + Status: ST_PENDING, + Sig: make(chan os.Signal), + Info: info, + } +} + +func (p *Program) createLog() (*os.File, error) { + logDir := os.ExpandEnv("$HOME/.gosuv/logs") + os.MkdirAll(logDir, 0755) // just do it, err ignore it + logFile := filepath.Join(logDir, p.Info.Name+".output.log") + return os.Create(logFile) +} + +func (p *Program) Run() error { + if err := p.Start(); err != nil { + p.Status = ST_FATAL + return err + } + return p.Wait() +} + +func (p *Program) Start() error { + logFd, err := p.createLog() + if err != nil { + return err + } + p.Cmd.Stdout = logFd + p.Cmd.Stderr = logFd + return p.Cmd.Start() +} + +// wait func finish, also accept signal +func (p *Program) Wait() (err error) { + log.Println("Wait program to finish") + p.Status = ST_RUNNING + defer func() { + if out, ok := p.Cmd.Stdout.(io.Closer); ok { + out.Close() + } + if err != nil { + log.Warnf("program finish: %v", err) + p.Status = ST_FATAL + } else { + p.Status = ST_STOPPED + } + }() + ch := GoFunc(p.Cmd.Wait) + for { + select { + case err = <-ch: + return err + case sig := <-p.Sig: + p.Terminate(sig) + } + } +} + +type ProgramInfo struct { + Name string `json:"name"` + Command []string `json:"command"` + Dir string `json:"dir"` + Environ []string `json:"environ"` +} + +var programTable *ProgramTable + +func init() { + programTable = &ProgramTable{ + table: make(map[string]*Program, 10), + ch: make(chan string), + } +} + +type ProgramTable struct { + table map[string]*Program + ch chan string + mu sync.Mutex +} + +var ( + ErrProgramDuplicate = errors.New("program duplicate") +) + +func (pt *ProgramTable) AddProgram(p *Program) error { + pt.mu.Lock() + defer pt.mu.Unlock() + name := p.Info.Name + if _, exists := pt.table[name]; exists { + return ErrProgramDuplicate + } + pt.table[name] = p + return nil +} + +func (pt *ProgramTable) Programs() []*Program { + pt.mu.Lock() + defer pt.mu.Unlock() + ps := make([]*Program, 0, len(pt.table)) + for _, p := range pt.table { + ps = append(ps, p) + } + return ps +} diff --git a/server.go b/server.go index fbd6fdb..0ffc385 100644 --- a/server.go +++ b/server.go @@ -6,10 +6,8 @@ import ( "net/http" "os" "os/exec" - "path/filepath" "time" - "github.com/codeskyblue/kproc" "github.com/gorilla/mux" "github.com/qiniu/log" ) @@ -19,17 +17,6 @@ type JSONResponse struct { Message string `json:"message"` } -type ProgramInfo struct { - Name string `json:"name"` - Command []string `json:"command"` - Dir string `json:"dir"` - Environ []string `json:"environ"` -} - -var programTable struct { - table map[string]*Program -} - func renderJSON(w http.ResponseWriter, v interface{}) { w.Header().Add("Content-Type", "json") json.NewEncoder(w).Encode(v) @@ -42,6 +29,22 @@ func versionHandler(w http.ResponseWriter, r *http.Request) { }) } +func buildProgram(pinfo *ProgramInfo) (*Program, error) { + // init cmd + cmd := exec.Command(pinfo.Command[0], pinfo.Command[1:]...) + cmd.Dir = pinfo.Dir + cmd.Env = append(os.Environ(), pinfo.Environ...) + program := NewProgram(cmd, pinfo) + + // set output + return program, nil +} + +func statusHandler(w http.ResponseWriter, r *http.Request) { + prms := programTable.Programs() + renderJSON(w, prms) +} + func addHandler(w http.ResponseWriter, r *http.Request) { pinfo := new(ProgramInfo) err := json.NewDecoder(r.Body).Decode(pinfo) @@ -51,86 +54,31 @@ func addHandler(w http.ResponseWriter, r *http.Request) { } log.Printf("add: %#v", pinfo) - // init cmd - cmd := exec.Command(pinfo.Command[0], pinfo.Command[1:]...) - cmd.Dir = pinfo.Dir - cmd.Env = append(os.Environ(), pinfo.Environ...) - program := NewProgram(cmd, pinfo) - - // set output - logFd, err := program.createLog() + program, err := buildProgram(pinfo) if err != nil { - http.Error(w, err.Error(), 503) + http.Error(w, err.Error(), 502) return } - cmd.Stdout = logFd - cmd.Stderr = logFd - - if err = program.Start(); err != nil { + if err = programTable.AddProgram(program); err != nil { http.Error(w, err.Error(), 503) return } - program.Status = ST_RUNNING - - // wait func finish - go func() { - finish := false - ch := GoFunc(program.Wait) - for !finish { - select { - case err := <-ch: - if err != nil { - log.Warnf("program finish: %v", err) - } - finish = true - case sig := <-program.Sig: - program.Terminate(sig) - } + go program.Run() + /* + if err = program.Start(); err != nil { + http.Error(w, err.Error(), 503) + return } - }() + + go program.Wait() + */ + renderJSON(w, &JSONResponse{ Code: 200, Message: "program add success", }) } -func GoFunc(f func() error) chan error { - ch := make(chan error) - go func() { - ch <- f() - }() - return ch -} - -const ( - ST_PENDING = "pending" - ST_RUNNING = "running" - ST_STOPPED = "stopped" -) - -type Program struct { - *kproc.Process - Status string `json:"state"` - Sig chan os.Signal - info *ProgramInfo -} - -func NewProgram(cmd *exec.Cmd, info *ProgramInfo) *Program { - return &Program{ - Process: kproc.ProcCommand(cmd), - Status: ST_PENDING, - Sig: make(chan os.Signal), - info: info, - } -} - -func (p *Program) createLog() (*os.File, error) { - logDir := os.ExpandEnv("$HOME/.gosuv/logs") - os.MkdirAll(logDir, 0755) // just do it, err ignore it - logFile := filepath.Join(logDir, p.info.Name+".output.log") - return os.Create(logFile) -} - func shutdownHandler(w http.ResponseWriter, r *http.Request) { go func() { time.Sleep(50 * time.Millisecond) @@ -147,5 +95,6 @@ func ServeAddr(host string, port int) error { r.HandleFunc("/api/version", versionHandler) r.Methods("POST").Path("/api/shutdown").HandlerFunc(shutdownHandler) r.Methods("POST").Path("/api/programs").HandlerFunc(addHandler) + r.Methods("GET").Path("/api/programs").HandlerFunc(statusHandler) return http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), r) }