From 31c141b6f9860978950a421e8d591425944f4f80 Mon Sep 17 00:00:00 2001 From: Evan Chen Date: Fri, 12 Nov 2021 17:25:44 +0800 Subject: [PATCH] add muzan --- configui.go | 17 ++++-- errors.go | 37 +++++++++++++ go.mod | 2 +- go.sum | 2 + handler.go | 25 +++++---- muzan.go | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++ server.go | 7 +++ util.go | 16 ++++++ 8 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 errors.go create mode 100644 muzan.go diff --git a/configui.go b/configui.go index 3f9bdcd..6115856 100644 --- a/configui.go +++ b/configui.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "os" + "path/filepath" "strings" "sync" "time" @@ -34,10 +35,13 @@ var Ext2Mode map[string]string = map[string]string{ } type Action struct { - Name string `json:"name"` - Cmd string `json:"cmd"` - run chan struct{} `json:"-"` - pid int `json:"-"` + Name string `json:"name"` + Cmd string `json:"cmd"` + Dir string `json:"dir"` + Args []string `json:"args"` + Envs []string `json:"envs"` + + run chan struct{} `json:"-"` } type Integration struct { @@ -70,6 +74,9 @@ type ConfigUI struct { LogPath string `json:"log_path"` LogLevel klog.Llevel `json:"log_level"` + // Running commands + Onitachi map[string]*Oni `json:"-"` + TmplFS embed.FS `json:"-"` tmpl *engine.Engine PublicFS embed.FS `json:"-"` @@ -105,6 +112,7 @@ func New() *ConfigUI { cmdTimeout: time.Second * 10, LogLevel: klog.Lerror | klog.Linfo, log: klog.Sub("ConfigUI"), + Onitachi: make(map[string]*Oni), } } @@ -205,6 +213,7 @@ func (cui *ConfigUI) setLog() { cui.f.Close() } if cui.LogPath != "" { + mkdir(filepath.Dir(cui.LogPath)) cui.f, err = os.OpenFile(cui.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { cui.log.Error(err) diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..26d1fcb --- /dev/null +++ b/errors.go @@ -0,0 +1,37 @@ +package configui + +import ( + "net/http" + + "kumoly.io/lib/ksrv" +) + +var ErrorOniHasNoPID = ksrv.Error{ + Code: http.StatusBadRequest, + ID: "ErrorOniHasNoPID", + Message: "oni has no pid", +} + +var ErrorOniNotStarted = ksrv.Error{ + Code: http.StatusConflict, + ID: "ErrorOniNotStarted", + Message: "oni hasn't start", +} + +var ErrorOniHasStarted = ksrv.Error{ + Code: http.StatusConflict, + ID: "ErrorOniHasStarted", + Message: "oni has started", +} + +var ErrorOniNotFound = ksrv.Error{ + Code: http.StatusNotFound, + ID: "ErrorOniNotFound", + Message: "oni not found", +} + +var ErrorOniNotValid = ksrv.Error{ + Code: http.StatusBadRequest, + ID: "ErrorOniNotValid", + Message: "oni no name or cmd", +} diff --git a/go.mod b/go.mod index 7771cf6..788ec5c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( kumoly.io/lib/klog v0.0.8 - kumoly.io/lib/ksrv v0.0.1 + kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298 ) require ( diff --git a/go.sum b/go.sum index 0f85473..ac4f6bf 100644 --- a/go.sum +++ b/go.sum @@ -8,3 +8,5 @@ kumoly.io/lib/klog v0.0.8 h1:6hTfDlZh7KGnPrd2tUrauCKRImSnyyN9DHXpey3Czn8= kumoly.io/lib/klog v0.0.8/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M= kumoly.io/lib/ksrv v0.0.1 h1:JfWwJ9GeiTtDfGoeG7YxJwsckralbhsLKEPLQb20Uzo= kumoly.io/lib/ksrv v0.0.1/go.mod h1:ykHXeAPjNvA5jEZo5rp32edzkugLf0e+2pspct3FOFQ= +kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298 h1:0raqoIXmNpD6s1SrJbieAyIIkDyhe+aqfaXvx8wenrI= +kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298/go.mod h1:pwd+NspxnoxPJAETRY2V4i2qZc+orKLxvWzGUBiqBW8= diff --git a/handler.go b/handler.go index d2e0c49..0d4b1b3 100644 --- a/handler.go +++ b/handler.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "strconv" - "time" "kumoly.io/lib/ksrv" ) @@ -142,19 +141,23 @@ func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) { case cui.Actions[i].run <- struct{}{}: defer func() { <-cui.Actions[i].run }() default: - panic(fmt.Errorf("another task of %s is running with pid: %d", name, v.pid)) + panic(fmt.Errorf("another task of %s is running with", name)) } - file := &File{Name: name, Cmd: v.Cmd, owner: cui} - go func() { - <-time.After(time.Millisecond * 10) - cui.Actions[i].pid = file.pid - }() - result, err := file.Do(cui.cmdTimeout) + oni, ok := cui.Onitachi[name] + if !ok { + oni = NewOni(name, cui) + oni.Cmd = cui.Actions[i].Cmd + oni.Args = cui.Actions[i].Args + oni.Envs = cui.Actions[i].Envs + oni.Dir = cui.Actions[i].Dir + cui.Onitachi[name] = oni + } + err := oni.Start() if err != nil { panic(err) } - w.Write([]byte(result)) + w.Write([]byte("ok")) return } } @@ -259,3 +262,7 @@ func (cui *ConfigUI) GetConfig(w http.ResponseWriter, r *http.Request) { } w.Write(data) } + +func (cui *ConfigUI) GetRunning(w http.ResponseWriter, r *http.Request) { + ksrv.JSON(w, cui.Onitachi) +} diff --git a/muzan.go b/muzan.go new file mode 100644 index 0000000..fdb238e --- /dev/null +++ b/muzan.go @@ -0,0 +1,146 @@ +package configui + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" +) + +type State string + +const ( + READY State = "ready" + ERROR State = "error" + STARTED State = "started" + ENDED State = "ended" +) + +type Policy string + +const ( + NO Policy = "no" + ONFAIL Policy = "on-failure" + ALWAYS Policy = "always" + UNLESSSTOP Policy = "unless-stopped" +) + +type Oni struct { + Name string `json:"name"` + Cmd string `json:"cmd"` + Dir string `json:"dir"` + Args []string `json:"args"` + Envs []string `json:"envs"` + Policy Policy `json:"policy"` + + PID int `json:"pid"` + LogFile string `json:"log_file"` + State State `json:"state"` + Error string `json:"error"` + ManStop bool `json:"manual_stop"` + + parent *ConfigUI + cmd *exec.Cmd + start chan struct{} + buff bytes.Buffer + log io.Writer + + listeners []io.Writer +} + +func NewOni(name string, cui *ConfigUI) *Oni { + var err error + oni := &Oni{ + Policy: NO, + State: READY, + parent: cui, + start: make(chan struct{}, 1), + listeners: make([]io.Writer, 0), + } + if cui.LogPath != "" { + logpath := filepath.Join(filepath.Dir(cui.LogPath), name+".log") + oni.LogFile = logpath + oni.log, err = os.OpenFile(logpath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + } + return oni +} + +func (oni *Oni) Start() error { + if oni.Cmd == "" { + return ErrorOniNotValid + } + select { + case oni.start <- struct{}{}: + defer func() { + err := recover() + if err != nil { + oni.end(err) + } + }() + default: + return ErrorOniHasStarted + } + cmd := exec.Command(oni.Cmd, oni.Args...) + cmd.Env = oni.Envs + cmd.Dir = oni.Dir + var out io.Writer + if oni.LogFile != "" { + out = io.MultiWriter(oni.log, &oni.buff) + } else { + out = &oni.buff + } + cmd.Stderr = out + cmd.Stdout = out + oni.cmd = cmd + err := oni.cmd.Start() + if err != nil { + oni.end(err) + return err + } + go oni.read() + oni.PID = cmd.Process.Pid + oni.State = STARTED + go func() { + oni.end(cmd.Wait()) + }() + + return nil +} + +func (oni *Oni) Stop() error { + if oni.cmd == nil { + return nil + } + return oni.cmd.Process.Kill() +} + +func (oni *Oni) end(v interface{}) { + if v == nil { + oni.State = ENDED + } else { + oni.Error = fmt.Sprint(v) + oni.State = ERROR + } + if len(oni.start) == 1 { + <-oni.start + } +} + +func (oni *Oni) read() { + go func() { + scanner := bufio.NewScanner(&oni.buff) + for scanner.Scan() { + if len(oni.listeners) == 0 { + continue + } + w := io.MultiWriter(oni.listeners...) + w.Write([]byte(scanner.Text())) + } + }() +} diff --git a/server.go b/server.go index 5d7a19d..62f7793 100644 --- a/server.go +++ b/server.go @@ -98,6 +98,13 @@ func (cui *ConfigUI) mux() *http.ServeMux { w.WriteHeader(404) } }) + cuiR.HandleFunc("/api/running", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + cui.GetRunning(w, r) + } else { + w.WriteHeader(404) + } + }) cuiR.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(cui.PublicFS)))) cuiR.HandleFunc("/", cui.App) return cuiR diff --git a/util.go b/util.go index d4f5ff8..36cf0cc 100644 --- a/util.go +++ b/util.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "io" "os" + "path/filepath" "strings" ) @@ -65,3 +66,18 @@ func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error { return nil } + +func mkdir(args ...interface{}) error { + var path string + var mode os.FileMode + mode = 0755 + for _, arg := range args { + switch arg := arg.(type) { + case string: + path = filepath.Join(path, arg) + case os.FileMode: + mode = arg + } + } + return os.MkdirAll(path, mode) +}