add muzan

feat/muzan
Evan Chen 2021-11-12 17:25:44 +08:00
parent 4b1ad5b791
commit 31c141b6f9
8 changed files with 238 additions and 14 deletions

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -34,10 +35,13 @@ var Ext2Mode map[string]string = map[string]string{
} }
type Action struct { type Action struct {
Name string `json:"name"` Name string `json:"name"`
Cmd string `json:"cmd"` Cmd string `json:"cmd"`
run chan struct{} `json:"-"` Dir string `json:"dir"`
pid int `json:"-"` Args []string `json:"args"`
Envs []string `json:"envs"`
run chan struct{} `json:"-"`
} }
type Integration struct { type Integration struct {
@ -70,6 +74,9 @@ type ConfigUI struct {
LogPath string `json:"log_path"` LogPath string `json:"log_path"`
LogLevel klog.Llevel `json:"log_level"` LogLevel klog.Llevel `json:"log_level"`
// Running commands
Onitachi map[string]*Oni `json:"-"`
TmplFS embed.FS `json:"-"` TmplFS embed.FS `json:"-"`
tmpl *engine.Engine tmpl *engine.Engine
PublicFS embed.FS `json:"-"` PublicFS embed.FS `json:"-"`
@ -105,6 +112,7 @@ func New() *ConfigUI {
cmdTimeout: time.Second * 10, cmdTimeout: time.Second * 10,
LogLevel: klog.Lerror | klog.Linfo, LogLevel: klog.Lerror | klog.Linfo,
log: klog.Sub("ConfigUI"), log: klog.Sub("ConfigUI"),
Onitachi: make(map[string]*Oni),
} }
} }
@ -205,6 +213,7 @@ func (cui *ConfigUI) setLog() {
cui.f.Close() cui.f.Close()
} }
if cui.LogPath != "" { 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) cui.f, err = os.OpenFile(cui.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil { if err != nil {
cui.log.Error(err) cui.log.Error(err)

37
errors.go Normal file
View File

@ -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",
}

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.17
require ( require (
kumoly.io/lib/klog v0.0.8 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 ( require (

2
go.sum
View File

@ -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/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 h1:JfWwJ9GeiTtDfGoeG7YxJwsckralbhsLKEPLQb20Uzo=
kumoly.io/lib/ksrv v0.0.1/go.mod h1:ykHXeAPjNvA5jEZo5rp32edzkugLf0e+2pspct3FOFQ= 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=

View File

@ -8,7 +8,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"time"
"kumoly.io/lib/ksrv" "kumoly.io/lib/ksrv"
) )
@ -142,19 +141,23 @@ func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) {
case cui.Actions[i].run <- struct{}{}: case cui.Actions[i].run <- struct{}{}:
defer func() { <-cui.Actions[i].run }() defer func() { <-cui.Actions[i].run }()
default: 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} oni, ok := cui.Onitachi[name]
go func() { if !ok {
<-time.After(time.Millisecond * 10) oni = NewOni(name, cui)
cui.Actions[i].pid = file.pid oni.Cmd = cui.Actions[i].Cmd
}() oni.Args = cui.Actions[i].Args
result, err := file.Do(cui.cmdTimeout) oni.Envs = cui.Actions[i].Envs
oni.Dir = cui.Actions[i].Dir
cui.Onitachi[name] = oni
}
err := oni.Start()
if err != nil { if err != nil {
panic(err) panic(err)
} }
w.Write([]byte(result)) w.Write([]byte("ok"))
return return
} }
} }
@ -259,3 +262,7 @@ func (cui *ConfigUI) GetConfig(w http.ResponseWriter, r *http.Request) {
} }
w.Write(data) w.Write(data)
} }
func (cui *ConfigUI) GetRunning(w http.ResponseWriter, r *http.Request) {
ksrv.JSON(w, cui.Onitachi)
}

146
muzan.go Normal file
View File

@ -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()))
}
}()
}

View File

@ -98,6 +98,13 @@ func (cui *ConfigUI) mux() *http.ServeMux {
w.WriteHeader(404) 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.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(cui.PublicFS))))
cuiR.HandleFunc("/", cui.App) cuiR.HandleFunc("/", cui.App)
return cuiR return cuiR

16
util.go
View File

@ -5,6 +5,7 @@ import (
"compress/gzip" "compress/gzip"
"io" "io"
"os" "os"
"path/filepath"
"strings" "strings"
) )
@ -65,3 +66,18 @@ func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error {
return nil 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)
}