From 4c60901c65ec6e03fc6e2e7f377f51fa5a626f9d Mon Sep 17 00:00:00 2001 From: codeskyblue Date: Wed, 6 Apr 2016 14:43:34 +0800 Subject: [PATCH] put different file to different folder --- README.md | 5 +- build.sh | 2 +- config/config.go | 14 +++ gosuv.go | 13 +-- program.go | 198 +--------------------------------------- program/program.go | 178 ++++++++++++++++++++++++++++++++++++ program/program_test.go | 7 ++ serv.go | 1 + serv_cmds.go | 4 +- tests/normal-program.sh | 5 +- utils/utils.go | 33 +++++++ 11 files changed, 250 insertions(+), 210 deletions(-) mode change 100644 => 100755 build.sh create mode 100644 config/config.go create mode 100644 program/program.go create mode 100644 program/program_test.go create mode 100644 utils/utils.go diff --git a/README.md b/README.md index f3cc645..38f7a68 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,13 @@ For binary version, just click $ gosuv stop timetest program "timetest" stopped - $ gosuv tail -n 2 -f timetest + $ gosuv tail -n 2 timetest line 1 line 2 line ... + + $ gosuv remove timetest + # remove program which named timetest # see more usage $ gosuv help diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index ec159a4..a957589 --- a/build.sh +++ b/build.sh @@ -17,4 +17,4 @@ then fi SHA=$(git rev-parse HEAD) -exec go build -ldflags "-X main.GOSUV_VERSION=$VERSION" +exec go build -ldflags "-X main.GOSUV_VERSION=$VERSION" "$@" diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..f38cad6 --- /dev/null +++ b/config/config.go @@ -0,0 +1,14 @@ +package config + +import ( + "os" + "path/filepath" +) + +var ( + GOSUV_HOME = os.ExpandEnv("$HOME/.gosuv") + GOSUV_SOCK_PATH = filepath.Join(GOSUV_HOME, "gosuv.sock") + GOSUV_CONFIG = filepath.Join(GOSUV_HOME, "gosuv.json") + GOSUV_PROGRAM_CONFIG = filepath.Join(GOSUV_HOME, "programs.json") + CMDPLUGIN_DIR = filepath.Join(GOSUV_HOME, "cmdplugin") +) diff --git a/gosuv.go b/gosuv.go index df560a4..069e4ef 100644 --- a/gosuv.go +++ b/gosuv.go @@ -14,7 +14,9 @@ import ( "github.com/codegangsta/cli" "github.com/codegangsta/inject" + . "github.com/codeskyblue/gosuv/config" pb "github.com/codeskyblue/gosuv/gosuvpb" + . "github.com/codeskyblue/gosuv/utils" "github.com/qiniu/log" "golang.org/x/net/context" "google.golang.org/grpc" @@ -22,17 +24,6 @@ import ( var GOSUV_VERSION = "UNKNOWN" -var ( - GOSUV_HOME = os.ExpandEnv("$HOME/.gosuv") - GOSUV_SOCK_PATH = filepath.Join(GOSUV_HOME, "gosuv.sock") - GOSUV_CONFIG = filepath.Join(GOSUV_HOME, "gosuv.json") - GOSUV_PROGRAM_CONFIG = filepath.Join(GOSUV_HOME, "programs.json") -) - -var ( - CMDPLUGIN_DIR = filepath.Join(GOSUV_HOME, "cmdplugin") -) - func MkdirIfNoExists(dir string) error { dir = os.ExpandEnv(dir) if _, err := os.Stat(dir); err != nil { diff --git a/program.go b/program.go index c2fc18b..b17e1b2 100644 --- a/program.go +++ b/program.go @@ -3,206 +3,15 @@ package main import ( "encoding/json" "errors" - "io" "io/ioutil" "os" - "path/filepath" "sort" "sync" - "syscall" - "time" - "github.com/codeskyblue/kexec" - "github.com/qiniu/log" + . "github.com/codeskyblue/gosuv/config" + . "github.com/codeskyblue/gosuv/program" ) -var ErrGoTimeout = errors.New("GoTimeoutFunc") - -func GoFunc(f func() error) chan error { - ch := make(chan error) - go func() { - ch <- f() - }() - return ch -} - -func GoTimeoutFunc(timeout time.Duration, f func() error) chan error { - ch := make(chan error) - go func() { - var err error - select { - case err = <-GoFunc(f): - ch <- err - case <-time.After(timeout): - log.Debugf("timeout: %v", f) - ch <- ErrGoTimeout - } - }() - return ch -} - -const ( - ST_RUNNING = "RUNNING" - ST_STOPPED = "STOPPED" - ST_FATAL = "FATAL" - ST_RETRYWAIT = "RETRYWAIT" // some like python-supervisor EXITED -) - -type Event int - -const ( - EVENT_START = Event(iota) - EVENT_STOP -) - -type ProgramInfo struct { - Name string `json:"name"` - Command []string `json:"command"` - Environ []string `json:"environ"` - Dir string `json:"directory"` - AutoStart bool `json:"autostart"` // change to *bool, which support unexpected - StartRetries int `json:"startretries"` - StartSeconds int `json:"startsecs"` -} - -func (p *ProgramInfo) buildCmd() *kexec.KCommand { - cmd := kexec.Command(p.Command[0], p.Command[1:]...) - cmd.Dir = p.Dir - cmd.Env = append(os.Environ(), p.Environ...) - return cmd -} - -type Program struct { - *kexec.KCommand `json:"-"` - Status string `json:"state"` - Sig chan os.Signal `json:"-"` - Info *ProgramInfo `json:"info"` - - retry int - stopc chan bool -} - -func NewProgram(info *ProgramInfo) *Program { - // set default values - if info.StartRetries == 0 { - info.StartRetries = 3 - } - if info.StartSeconds == 0 { - info.StartSeconds = 3 - } - return &Program{ - Status: ST_STOPPED, - Sig: make(chan os.Signal), - Info: info, - stopc: make(chan bool), - } -} - -func (p *Program) setStatus(st string) { - // TODO: status change hook - p.Status = st -} - -func (p *Program) InputData(event Event) { - switch event { - case EVENT_START: - if p.Status == ST_STOPPED || p.Status == ST_FATAL { - go p.RunWithRetry() - } - case EVENT_STOP: - if p.Status == ST_RUNNING || p.Status == ST_RETRYWAIT { - p.Stop() - } - } -} - -func (p *Program) createLog() (*os.File, error) { - logDir := filepath.Join(GOSUV_HOME, "logs") - os.MkdirAll(logDir, 0755) // just do it, err ignore it - logFile := p.logFilePath() - return os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) -} - -func (p *Program) sleep(d time.Duration) { - select { - case <-p.stopc: - return - case <-time.After(time.Second * 2): - } -} - -func (p *Program) logFilePath() string { - logDir := filepath.Join(GOSUV_HOME, "logs") - return filepath.Join(logDir, p.Info.Name+".log") -} - -func (p *Program) RunWithRetry() { - for p.retry = 0; p.retry < p.Info.StartRetries+1; p.retry += 1 { - // wait program to exit - errc := GoFunc(p.Run) - var err error - - PROGRAM_WAIT: - // Here is RUNNING State - select { - case err = <-errc: - log.Info(p.Info.Name, err) - case <-time.After(time.Second * time.Duration(p.Info.StartSeconds)): // reset retry - p.retry = 0 - goto PROGRAM_WAIT - case <-p.stopc: - return - } - - // Enter RETRY_WAIT State - if p.retry < p.Info.StartRetries { - p.setStatus(ST_RETRYWAIT) - select { - case <-p.stopc: - return - case <-time.After(time.Second * 2): - } - } - } - p.setStatus(ST_FATAL) -} - -func (p *Program) Run() (err error) { - if err = p.Start(); err != nil { - return - } - p.setStatus(ST_RUNNING) - defer func() { - if out, ok := p.Cmd.Stdout.(io.Closer); ok { - out.Close() - } - log.Warnf("program finish: %v", err) - }() - err = p.Wait() - return -} - -func (p *Program) Stop() error { - select { - case p.stopc <- true: // stopc may not recevied - case <-time.After(time.Millisecond * 50): - } - p.Terminate(syscall.SIGKILL) - p.setStatus(ST_STOPPED) - return nil -} - -func (p *Program) Start() error { - p.KCommand = p.Info.buildCmd() - logFd, err := p.createLog() - if err != nil { - return err - } - p.Cmd.Stdout = logFd - p.Cmd.Stderr = logFd - return p.Cmd.Start() -} - var programTable *ProgramTable func initProgramTable() { @@ -220,7 +29,7 @@ type ProgramTable struct { } var ( - ErrProgramDuplicate = errors.New("program duplicate") + ErrProgramDuplicate = errors.New("program duplicated") ErrProgramNotExists = errors.New("program not exists") ) @@ -244,6 +53,7 @@ func (pt *ProgramTable) loadConfig() error { return err } defer cfgFd.Close() + table := make(map[string]*ProgramInfo) if err = json.NewDecoder(cfgFd).Decode(&table); err != nil { return err diff --git a/program/program.go b/program/program.go new file mode 100644 index 0000000..504df1c --- /dev/null +++ b/program/program.go @@ -0,0 +1,178 @@ +package program + +import ( + "io" + "os" + "path/filepath" + "sync" + "syscall" + "time" + + . "github.com/codeskyblue/gosuv/config" + . "github.com/codeskyblue/gosuv/utils" + "github.com/codeskyblue/kexec" + "github.com/qiniu/log" +) + +const ( + ST_RUNNING = "RUNNING" + ST_STOPPED = "STOPPED" + ST_FATAL = "FATAL" + ST_RETRYWAIT = "RETRYWAIT" // some like python-supervisor EXITED +) + +type Event int + +const ( + EVENT_START = Event(iota) + EVENT_STOP +) + +type ProgramInfo struct { + Name string `json:"name"` + Command []string `json:"command"` + Environ []string `json:"environ"` + Dir string `json:"directory"` + AutoStart bool `json:"autostart"` // change to *bool, which support unexpected + StartRetries int `json:"startretries"` + StartSeconds int `json:"startsecs"` + LogDir string `json:"logdir"` +} + +func (p *ProgramInfo) buildCmd() *kexec.KCommand { + cmd := kexec.Command(p.Command[0], p.Command[1:]...) + cmd.Dir = p.Dir + cmd.Env = append(os.Environ(), p.Environ...) + return cmd +} + +type Program struct { + *kexec.KCommand `json:"-"` + Status string `json:"state"` + Sig chan os.Signal `json:"-"` + Info *ProgramInfo `json:"info"` + mu sync.Mutex + + retry int + stopC chan bool +} + +func NewProgram(info *ProgramInfo) *Program { + // set default values + if info.StartRetries == 0 { + info.StartRetries = 3 + } + if info.StartSeconds == 0 { + info.StartSeconds = 3 + } + return &Program{ + Status: ST_STOPPED, + Sig: make(chan os.Signal), + Info: info, + stopC: make(chan bool), + } +} + +// TODO: status change hook +func (p *Program) setStatus(st string) { + p.Status = st +} + +// STOP and START Should not run parallel +func (p *Program) InputData(event Event) { + p.mu.Lock() + defer p.mu.Unlock() + + switch event { + case EVENT_START: + if p.Status == ST_STOPPED || p.Status == ST_FATAL { + go p.RunWithRetry() + } + case EVENT_STOP: + if p.Status == ST_RUNNING || p.Status == ST_RETRYWAIT { + p.stop() + } + } +} + +func (p *Program) createLog() (*os.File, error) { + logDir := filepath.Join(p.Info.LogDir, "logs") + os.MkdirAll(logDir, 0755) // just do it, err ignore it + logFile := p.LogFilePath() + return os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) +} + +func (p *Program) LogFilePath() string { + logDir := p.Info.LogDir + if logDir == "" { + logDir = filepath.Join(GOSUV_HOME, "logs") + } + return filepath.Join(logDir, p.Info.Name+".log") +} + +func (p *Program) RunWithRetry() { + for p.retry = 0; p.retry < p.Info.StartRetries+1; p.retry += 1 { + // wait program to exit + errc := GoFunc(p.run) + var err error + + PROGRAM_WAIT: + // Here is RUNNING State + select { + case err = <-errc: + log.Info(p.Info.Name, err) + case <-time.After(time.Second * time.Duration(p.Info.StartSeconds)): // reset retry + p.retry = 0 + goto PROGRAM_WAIT + case <-p.stopC: + return + } + + // Enter RETRY_WAIT State + if p.retry < p.Info.StartRetries { + p.setStatus(ST_RETRYWAIT) + select { + case <-p.stopC: + return + case <-time.After(time.Second * 2): + } + } + } + p.setStatus(ST_FATAL) +} + +func (p *Program) start() error { + p.KCommand = p.Info.buildCmd() + logFd, err := p.createLog() + if err != nil { + return err + } + p.Cmd.Stdout = logFd + p.Cmd.Stderr = logFd + return p.Cmd.Start() +} + +func (p *Program) run() (err error) { + if err = p.start(); err != nil { + return + } + p.setStatus(ST_RUNNING) + defer func() { + if out, ok := p.Cmd.Stdout.(io.Closer); ok { + out.Close() + } + log.Warnf("program finish: %v", err) + }() + err = p.Wait() + return +} + +func (p *Program) stop() error { + select { + case p.stopC <- true: // stopC may not recevied, when in FATAL, in a very low probality + case <-time.After(time.Millisecond * 200): // 0.2s + } + p.Terminate(syscall.SIGKILL) + p.setStatus(ST_STOPPED) + return nil +} diff --git a/program/program_test.go b/program/program_test.go new file mode 100644 index 0000000..8eeebb2 --- /dev/null +++ b/program/program_test.go @@ -0,0 +1,7 @@ +package program + +import "testing" + +func TestProgramCreate(t *testing.T) { + +} diff --git a/serv.go b/serv.go index 50af8ae..a804fa0 100644 --- a/serv.go +++ b/serv.go @@ -7,6 +7,7 @@ import ( "os/signal" "syscall" + . "github.com/codeskyblue/gosuv/config" pb "github.com/codeskyblue/gosuv/gosuvpb" "github.com/qiniu/log" "google.golang.org/grpc" diff --git a/serv_cmds.go b/serv_cmds.go index f9e1b32..0cde7c8 100644 --- a/serv_cmds.go +++ b/serv_cmds.go @@ -7,7 +7,9 @@ import ( "os/exec" "time" + . "github.com/codeskyblue/gosuv/config" pb "github.com/codeskyblue/gosuv/gosuvpb" + . "github.com/codeskyblue/gosuv/program" "golang.org/x/net/context" ) @@ -103,7 +105,7 @@ func (c *PbProgram) Tail(in *pb.TailRequest, stream pb.Program_TailServer) (err if in.Follow { args = append(args, "-f") } - cmd := exec.Command("tail", append(args, program.logFilePath())...) + cmd := exec.Command("tail", append(args, program.LogFilePath())...) rd, err := cmd.StdoutPipe() go cmd.Run() defer func() { diff --git a/tests/normal-program.sh b/tests/normal-program.sh index d6f38fc..35941bf 100755 --- a/tests/normal-program.sh +++ b/tests/normal-program.sh @@ -5,6 +5,7 @@ N=1 while true do - sleep 5 - exit 1 + echo "Hello $N" + sleep 2 + N=$(expr $N + 1) done diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..f0b5135 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,33 @@ +package utils + +import ( + "errors" + "time" + + "github.com/qiniu/log" +) + +var ErrGoTimeout = errors.New("GoTimeoutFunc") + +func GoFunc(f func() error) chan error { + ch := make(chan error) + go func() { + ch <- f() + }() + return ch +} + +func GoTimeoutFunc(timeout time.Duration, f func() error) chan error { + ch := make(chan error) + go func() { + var err error + select { + case err = <-GoFunc(f): + ch <- err + case <-time.After(timeout): + log.Debugf("timeout: %v", f) + ch <- ErrGoTimeout + } + }() + return ch +}