Compare commits
	
		
			7 Commits 
		
	
	
		
			master
			...
			feat/muzan
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 621c720767 | |
|  | 733dada3fe | |
|  | 556f673b3e | |
|  | f039033077 | |
|  | 2ecac19807 | |
|  | 56841cc8be | |
|  | 31c141b6f9 | 
							
								
								
									
										43
									
								
								app.go
								
								
								
								
							
							
						
						
									
										43
									
								
								app.go
								
								
								
								
							|  | @ -27,6 +27,7 @@ type Page struct { | |||
| 	BaseUrl      string | ||||
| 	Actions      []Action | ||||
| 	Integrations []*Integration | ||||
| 	Onitachi     map[string]*Oni | ||||
| 	Version      string | ||||
| 	Build        string | ||||
| 	Files        []ActiveFile | ||||
|  | @ -79,6 +80,7 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) { | |||
| 		}, | ||||
| 		Actions:      cui.Actions, | ||||
| 		Integrations: cui.Integrations, | ||||
| 		Onitachi:     cui.Onitachi, | ||||
| 		Static:       cui.NoReconfig, | ||||
| 		HideConfig:   cui.HideConfig, | ||||
| 		ResultBellow: cui.ResultBellow, | ||||
|  | @ -127,3 +129,44 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) { | |||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) Program(w http.ResponseWriter, r *http.Request) { | ||||
| 	Files := []ActiveFile{} | ||||
| 	for _, i := range cui.fileIndex { | ||||
| 		Files = append(Files, ActiveFile{ | ||||
| 			Name:  cui.Files[i].Name, | ||||
| 			Path:  cui.Files[i].Path, | ||||
| 			Order: cui.Files[i].Order, | ||||
| 		}) | ||||
| 	} | ||||
| 	sort.Slice(Files, func(i, j int) bool { | ||||
| 		if Files[i].Order == Files[j].Order { | ||||
| 			return Files[i].Name < Files[j].Name | ||||
| 		} | ||||
| 		return Files[i].Order < Files[j].Order | ||||
| 	}) | ||||
| 	plat := "unix" | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		plat = "windows" | ||||
| 	} | ||||
| 	data := Page{ | ||||
| 		AppName: cui.AppName, | ||||
| 		BaseUrl: cui.BaseUrl, | ||||
| 		File:    ActiveFile{}, | ||||
| 		Files:   Files, | ||||
| 		Editor: Editor{ | ||||
| 			Platform: plat, | ||||
| 		}, | ||||
| 		Actions:      cui.Actions, | ||||
| 		Integrations: cui.Integrations, | ||||
| 		Onitachi:     cui.Onitachi, | ||||
| 		Static:       cui.NoReconfig, | ||||
| 		HideConfig:   cui.HideConfig, | ||||
| 		Version:      version, | ||||
| 	} | ||||
| 
 | ||||
| 	err := cui.tmpl.ExecuteTemplate(w, "muzan", data) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ func init() { | |||
| 	flag.StringVar(&flagAction, "c", "", "cmd to apply") | ||||
| 	flag.StringVar(&flagLogFile, "log", "", "log to file") | ||||
| 	flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all") | ||||
| 	flag.StringVar(&flagBind, "bind", "0.0.0.0:8000", "address to bind") | ||||
| 	flag.StringVar(&flagBind, "bind", "127.0.0.1:8000", "address to bind") | ||||
| 	flag.BoolVar(&flagNoReconfig, "static", false, "disable config api") | ||||
| 	flag.BoolVar(&flagVer, "v", false, "show version") | ||||
| 	flag.Usage = func() { | ||||
|  |  | |||
							
								
								
									
										112
									
								
								configui.go
								
								
								
								
							
							
						
						
									
										112
									
								
								configui.go
								
								
								
								
							|  | @ -7,6 +7,8 @@ import ( | |||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | @ -16,8 +18,9 @@ import ( | |||
| 	"kumoly.io/tools/configui/public" | ||||
| ) | ||||
| 
 | ||||
| var UNIX_SHELL = "/usr/bin/sh" | ||||
| var WIN_SHELL = "C:\\Windows\\System32\\cmd" | ||||
| const UNIX_SHELL = "/usr/bin/sh" | ||||
| const WIN_SHELL = "C:\\Windows\\System32\\cmd" | ||||
| const DARWIN_SHELL = "/bin/bash" | ||||
| 
 | ||||
| const version = "v0.1.12" | ||||
| 
 | ||||
|  | @ -34,10 +37,12 @@ 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"` | ||||
| 
 | ||||
| 	run chan struct{} `json:"-"` | ||||
| 	pid int | ||||
| } | ||||
| 
 | ||||
| type Integration struct { | ||||
|  | @ -52,6 +57,7 @@ type ConfigUI struct { | |||
| 	Prod       bool   `json:"production"` | ||||
| 	BaseUrl    string `json:"base_url"` | ||||
| 	ConfigPath string `json:"config_path"` | ||||
| 	SHELL      string `json:"shell"` | ||||
| 
 | ||||
| 	NoReconfig bool   `json:"no_reconfig"` | ||||
| 	AllowIP    string `json:"allow_ip"` | ||||
|  | @ -70,6 +76,9 @@ type ConfigUI struct { | |||
| 	LogPath  string      `json:"log_path"` | ||||
| 	LogLevel klog.Llevel `json:"log_level"` | ||||
| 
 | ||||
| 	// Running commands
 | ||||
| 	Onitachi map[string]*Oni `json:"onitachi"` | ||||
| 
 | ||||
| 	TmplFS     embed.FS `json:"-"` | ||||
| 	tmpl       *engine.Engine | ||||
| 	PublicFS   embed.FS `json:"-"` | ||||
|  | @ -91,9 +100,43 @@ func New() *ConfigUI { | |||
| 		"normal": func(name string) string { | ||||
| 			return strings.ReplaceAll(name, " ", "-") | ||||
| 		}, | ||||
| 		"state_class": func(state State) string { | ||||
| 			switch state { | ||||
| 			case STARTED: | ||||
| 				return "is-success" | ||||
| 			case ERROR: | ||||
| 				return "is-danger" | ||||
| 			case ENDED: | ||||
| 				return "has-background-grey-light" | ||||
| 			default: | ||||
| 				return "" | ||||
| 			} | ||||
| 		}, | ||||
| 		"state_icon": func(state State) string { | ||||
| 			switch state { | ||||
| 			case STARTED: | ||||
| 				return "play_arrow" | ||||
| 			case ERROR: | ||||
| 				return "error" | ||||
| 			case ENDED: | ||||
| 				return "stop" | ||||
| 			default: | ||||
| 				return "pending" | ||||
| 			} | ||||
| 		}, | ||||
| 	}).ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl")) | ||||
| 	sh := "" | ||||
| 	switch runtime.GOOS { | ||||
| 	case "darwin": | ||||
| 		sh = DARWIN_SHELL | ||||
| 	case "windows": | ||||
| 		sh = WIN_SHELL | ||||
| 	default: | ||||
| 		sh = UNIX_SHELL | ||||
| 	} | ||||
| 	return &ConfigUI{ | ||||
| 		fileIndex:  map[string]int{}, | ||||
| 		SHELL:      sh, | ||||
| 		Prod:       true, | ||||
| 		AppName:    "ConfigUI", | ||||
| 		BaseUrl:    "/", | ||||
|  | @ -105,6 +148,7 @@ func New() *ConfigUI { | |||
| 		cmdTimeout: time.Second * 10, | ||||
| 		LogLevel:   klog.Lerror | klog.Linfo, | ||||
| 		log:        klog.Sub("ConfigUI"), | ||||
| 		Onitachi:   make(map[string]*Oni), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -125,6 +169,8 @@ func (cui *ConfigUI) File(file_name string) (*File, error) { | |||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) LoadConfig(confstr string) error { | ||||
| 	cui.configLock.Lock() | ||||
| 	defer cui.configLock.Unlock() | ||||
| 	tmpConf := &ConfigUI{} | ||||
| 	err := json.Unmarshal([]byte(confstr), tmpConf) | ||||
| 	if err != nil { | ||||
|  | @ -147,9 +193,15 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// del oni dry run
 | ||||
| 	for k, v := range cui.Onitachi { | ||||
| 		_, ok := tmpConf.Onitachi[k] | ||||
| 		if !ok && v.State == STARTED { | ||||
| 			return ErrorNoDeleteRunning.New(k) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// copy
 | ||||
| 	cui.configLock.Lock() | ||||
| 	defer cui.configLock.Unlock() | ||||
| 	cui.fileIndex = tmpIndex | ||||
| 	cui.Files = tmpConf.Files | ||||
| 	cui.AllowIP = tmpConf.AllowIP | ||||
|  | @ -159,6 +211,40 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 	cui.HideConfig = tmpConf.HideConfig | ||||
| 	cui.NoReconfig = tmpConf.NoReconfig | ||||
| 	cui.ResultBellow = tmpConf.ResultBellow | ||||
| 	cui.SHELL = tmpConf.SHELL | ||||
| 
 | ||||
| 	cui.log = klog.Sub(cui.AppName) | ||||
| 	cui.LogLevel = tmpConf.LogLevel | ||||
| 	if cui.LogLevel == 0 { | ||||
| 		cui.LogLevel = klog.Lerror | klog.Linfo | ||||
| 	} | ||||
| 	klog.LEVEL = cui.LogLevel | ||||
| 	cui.LogPath = tmpConf.LogPath | ||||
| 	cui.setLog() | ||||
| 
 | ||||
| 	// construct oni - update existing, del
 | ||||
| 	for k, v := range cui.Onitachi { | ||||
| 		n, ok := tmpConf.Onitachi[k] | ||||
| 		if !ok { | ||||
| 			delete(cui.Onitachi, k) | ||||
| 		} else { | ||||
| 			v.Name = k | ||||
| 			v.Cmd = n.Cmd | ||||
| 			v.Args = n.Args | ||||
| 			v.Envs = n.Envs | ||||
| 			v.Dir = n.Dir | ||||
| 			v.Policy = n.Policy | ||||
| 		} | ||||
| 	} | ||||
| 	// construct oni - append new
 | ||||
| 	for k, v := range tmpConf.Onitachi { | ||||
| 		_, ok := cui.Onitachi[k] | ||||
| 		if !ok { | ||||
| 			v.Name = k | ||||
| 			v.Init(cui) | ||||
| 			cui.Onitachi[k] = v | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	cui.Actions = tmpConf.Actions | ||||
| 	for i := range cui.Actions { | ||||
|  | @ -186,15 +272,6 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | |||
| 		cui.cmdTimeout = ct | ||||
| 	} | ||||
| 
 | ||||
| 	cui.log = klog.Sub(cui.AppName) | ||||
| 
 | ||||
| 	cui.LogLevel = tmpConf.LogLevel | ||||
| 	if cui.LogLevel == 0 { | ||||
| 		cui.LogLevel = klog.Lerror | klog.Linfo | ||||
| 	} | ||||
| 	klog.LEVEL = cui.LogLevel | ||||
| 	cui.LogPath = tmpConf.LogPath | ||||
| 	cui.setLog() | ||||
| 	// fmt.Printf("%+v", cui)
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -205,6 +282,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) | ||||
|  |  | |||
|  | @ -0,0 +1,55 @@ | |||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"kumoly.io/lib/ksrv" | ||||
| ) | ||||
| 
 | ||||
| var ErrorServerReloading = ksrv.Error{ | ||||
| 	Code:    http.StatusServiceUnavailable, | ||||
| 	ID:      "ErrorServerReloading", | ||||
| 	Message: "server is reloading", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniHasNoPID = ksrv.Error{ | ||||
| 	Code:    http.StatusBadRequest, | ||||
| 	ID:      "ErrorOniHasNoPID", | ||||
| 	Message: "oni has no pid", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNotStarted = ksrv.Error{ | ||||
| 	Code: http.StatusConflict, | ||||
| 	ID:   "ErrorOniNotStarted", | ||||
| 	Tmpl: "%s hasn't start", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniHasStarted = ksrv.Error{ | ||||
| 	Code: http.StatusConflict, | ||||
| 	ID:   "ErrorOniHasStarted", | ||||
| 	Tmpl: "%s has started", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNotFound = ksrv.Error{ | ||||
| 	Code: http.StatusNotFound, | ||||
| 	ID:   "ErrorOniNotFound", | ||||
| 	Tmpl: "%s not found", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNotValid = ksrv.Error{ | ||||
| 	Code:    http.StatusBadRequest, | ||||
| 	ID:      "ErrorOniNotValid", | ||||
| 	Message: "oni no name or cmd", | ||||
| } | ||||
| 
 | ||||
| var ErrorNoDeleteRunning = ksrv.Error{ | ||||
| 	Code: http.StatusBadRequest, | ||||
| 	ID:   "ErrorNoDeleteRunning", | ||||
| 	Tmpl: "cannot delete running: %s", | ||||
| } | ||||
| 
 | ||||
| var ErrorOniNoLog = ksrv.Error{ | ||||
| 	Code: http.StatusNotFound, | ||||
| 	ID:   "ErrorOniNoLog", | ||||
| 	Tmpl: "%s has no logs", | ||||
| } | ||||
							
								
								
									
										7
									
								
								file.go
								
								
								
								
							
							
						
						
									
										7
									
								
								file.go
								
								
								
								
							|  | @ -58,7 +58,7 @@ func (f *File) Write(data []byte) error { | |||
| 	return os.WriteFile(f.Path, data, info.Mode()) | ||||
| } | ||||
| 
 | ||||
| func (f *File) Do(CmdTimeout time.Duration) (string, error) { | ||||
| func (f *File) Do(CmdTimeout time.Duration, report chan int) (string, error) { | ||||
| 	if f.Cmd == "" { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | @ -80,9 +80,9 @@ func (f *File) Do(CmdTimeout time.Duration) (string, error) { | |||
| 	// prepare cmd
 | ||||
| 	cmd := &exec.Cmd{} | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		cmd = exec.Command(WIN_SHELL, "/c", f.Cmd) | ||||
| 		cmd = exec.Command(f.owner.SHELL, "/c", f.Cmd) | ||||
| 	} else { | ||||
| 		cmd = exec.Command(UNIX_SHELL, "-c", f.Cmd) | ||||
| 		cmd = exec.Command(f.owner.SHELL, "-c", f.Cmd) | ||||
| 	} | ||||
| 	f.owner.log.Info("DO: ", f.Cmd) | ||||
| 	done := make(chan string, 1) | ||||
|  | @ -96,6 +96,7 @@ func (f *File) Do(CmdTimeout time.Duration) (string, error) { | |||
| 	} | ||||
| 	go func() { | ||||
| 		f.pid = cmd.Process.Pid | ||||
| 		report <- cmd.Process.Pid | ||||
| 		cmd.Wait() | ||||
| 		done <- b.String() | ||||
| 	}() | ||||
|  |  | |||
							
								
								
									
										3
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										3
									
								
								go.mod
								
								
								
								
							|  | @ -4,10 +4,11 @@ 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 ( | ||||
| 	github.com/mattn/go-isatty v0.0.14 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect | ||||
| 	kumoly.io/lib/stat v0.0.1 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										4
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										4
									
								
								go.sum
								
								
								
								
							|  | @ -8,3 +8,7 @@ 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= | ||||
| kumoly.io/lib/stat v0.0.1 h1:Ck596El7Ixk7GZyzQq/86F1YCl7iYffHmzEdFx1sSRM= | ||||
| kumoly.io/lib/stat v0.0.1/go.mod h1:zqMV9q4TC94VGbpDn/mGBTwRNWBVWlVg1taLlCBAWc8= | ||||
|  |  | |||
							
								
								
									
										110
									
								
								handler.go
								
								
								
								
							
							
						
						
									
										110
									
								
								handler.go
								
								
								
								
							|  | @ -8,7 +8,6 @@ import ( | |||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"kumoly.io/lib/ksrv" | ||||
| ) | ||||
|  | @ -122,7 +121,7 @@ func (cui *ConfigUI) Apply(w http.ResponseWriter, r *http.Request) { | |||
| 		ksrv.Response(w, 404, []byte("file not found")) | ||||
| 		return | ||||
| 	} | ||||
| 	result, err := file.Do(cui.cmdTimeout) | ||||
| 	result, err := file.Do(cui.cmdTimeout, make(chan int)) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | @ -146,15 +145,16 @@ func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) { | |||
| 			} | ||||
| 
 | ||||
| 			file := &File{Name: name, Cmd: v.Cmd, owner: cui} | ||||
| 			pid := make(chan int) | ||||
| 			go func() { | ||||
| 				<-time.After(time.Millisecond * 10) | ||||
| 				cui.Actions[i].pid = file.pid | ||||
| 				cui.Actions[i].pid = <-pid | ||||
| 			}() | ||||
| 			result, err := file.Do(cui.cmdTimeout) | ||||
| 			result, err := file.Do(cui.cmdTimeout, pid) | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 			w.Write([]byte(result)) | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | @ -196,8 +196,8 @@ func (cui *ConfigUI) Download(w http.ResponseWriter, r *http.Request) { | |||
| 				panic(err) | ||||
| 			} | ||||
| 			w.Header().Set( | ||||
| 				"Content-Disposition", ` | ||||
| 				attachment; filename="`+cui.AppName+`.json"`, | ||||
| 				"Content-Disposition", | ||||
| 				`attachment; filename="`+cui.AppName+`.json"`, | ||||
| 			) | ||||
| 			w.Write(data) | ||||
| 			return | ||||
|  | @ -259,3 +259,99 @@ func (cui *ConfigUI) GetConfig(w http.ResponseWriter, r *http.Request) { | |||
| 	} | ||||
| 	w.Write(data) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) GetOni(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	if name == "" { | ||||
| 		ksrv.JSON(w, cui.Onitachi) | ||||
| 		return | ||||
| 	} | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	ksrv.JSON(w, oni) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniStart(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	switch oni.State { | ||||
| 	case "": | ||||
| 		oni.Init(cui) | ||||
| 	case STARTED: | ||||
| 		panic(ErrorOniHasStarted.New(name)) | ||||
| 	} | ||||
| 	err := oni.Start() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	w.Write([]byte("ok")) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniStop(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	switch oni.State { | ||||
| 	case STARTED: | ||||
| 		err := oni.Stop() | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	default: | ||||
| 		panic(ErrorOniNotStarted.New(name)) | ||||
| 	} | ||||
| 	w.Write([]byte("ok")) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniLog(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	if oni.LogFile == "" { | ||||
| 		panic(ErrorOniNoLog.New(name)) | ||||
| 	} | ||||
| 	stat, err := os.Stat(oni.LogFile) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	data, err := os.ReadFile(oni.LogFile) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	ksrv.JSON(w, map[string]string{ | ||||
| 		"name":  name, | ||||
| 		"data":  string(data), | ||||
| 		"delta": strconv.Itoa(int(stat.Size())), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniKill(w http.ResponseWriter, r *http.Request) { | ||||
| 	name := r.URL.Query().Get("name") | ||||
| 	oni, ok := cui.Onitachi[name] | ||||
| 	if !ok { | ||||
| 		panic(ErrorOniNotFound.New(name)) | ||||
| 	} | ||||
| 	switch oni.State { | ||||
| 	case STARTED: | ||||
| 		err := oni.Kill() | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	default: | ||||
| 		panic(ErrorOniNotStarted.New(name)) | ||||
| 	} | ||||
| 	w.Write([]byte("ok")) | ||||
| } | ||||
| 
 | ||||
| func (cui *ConfigUI) OniAttach(w http.ResponseWriter, r *http.Request) { | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,172 @@ | |||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
| 	"syscall" | ||||
| ) | ||||
| 
 | ||||
| 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:"-"` | ||||
| 	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 | ||||
| 	running chan struct{} | ||||
| 	buff    bytes.Buffer | ||||
| 	log     io.Writer | ||||
| 
 | ||||
| 	listeners []io.Writer | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Init(cui *ConfigUI) { | ||||
| 	oni.parent = cui | ||||
| 	oni.running = make(chan struct{}, 1) | ||||
| 	oni.listeners = make([]io.Writer, 0) | ||||
| 	oni.StateChange(READY) | ||||
| 	oni.SetLog() | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Start() error { | ||||
| 	if oni.Cmd == "" { | ||||
| 		return ErrorOniNotValid | ||||
| 	} | ||||
| 	select { | ||||
| 	case oni.running <- struct{}{}: | ||||
| 		defer func() { | ||||
| 			err := recover() | ||||
| 			if err != nil { | ||||
| 				oni.end(err) | ||||
| 			} | ||||
| 		}() | ||||
| 	default: | ||||
| 		return ErrorOniHasStarted | ||||
| 	} | ||||
| 	var err error | ||||
| 	cmd := exec.Command(oni.Cmd, oni.Args...) | ||||
| 	setgpid(cmd) | ||||
| 	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.StateChange(STARTED) | ||||
| 	go func() { | ||||
| 		oni.end(cmd.Wait()) | ||||
| 	}() | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Stop() error { | ||||
| 	if oni.cmd == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return oni.cmd.Process.Signal(syscall.SIGTERM) | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) end(v interface{}) { | ||||
| 	if v == nil { | ||||
| 		oni.StateChange(ENDED) | ||||
| 	} else { | ||||
| 		oni.Error = fmt.Sprint(v) | ||||
| 		oni.StateChange(ERROR) | ||||
| 	} | ||||
| 	if oni.log != nil { | ||||
| 		file, ok := oni.log.(*os.File) | ||||
| 		if ok { | ||||
| 			file.Close() | ||||
| 		} | ||||
| 	} | ||||
| 	if len(oni.running) == 1 { | ||||
| 		<-oni.running | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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())) | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) StateChange(state State) { | ||||
| 	oni.State = state | ||||
| 	// for i := range oni.parent.Actions {
 | ||||
| 	// 	if oni.parent.Actions[i].Name == oni.Name {
 | ||||
| 	// 		oni.parent.Actions[i].State = state
 | ||||
| 	// 	}
 | ||||
| 	// }
 | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) SetLog() error { | ||||
| 	var err error | ||||
| 	if oni.log != nil { | ||||
| 		file, ok := oni.log.(*os.File) | ||||
| 		if ok { | ||||
| 			file.Close() | ||||
| 		} | ||||
| 	} | ||||
| 	if oni.parent.LogPath != "" { | ||||
| 		logpath := filepath.Join(filepath.Dir(oni.parent.LogPath), oni.Name+".log") | ||||
| 		oni.LogFile = logpath | ||||
| 		oni.log, err = os.OpenFile(logpath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) | ||||
| 		if err != nil { | ||||
| 			oni.end(err) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| //go:build !windows
 | ||||
| // +build !windows
 | ||||
| 
 | ||||
| package configui | ||||
| 
 | ||||
| import ( | ||||
| 	"os/exec" | ||||
| 	"syscall" | ||||
| ) | ||||
| 
 | ||||
| func setgpid(cmd *exec.Cmd) { | ||||
| 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Kill() error { | ||||
| 	if oni.cmd == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	pgid, err := syscall.Getpgid(oni.cmd.Process.Pid) | ||||
| 	if err == nil { | ||||
| 		return syscall.Kill(-pgid, 15) // note the minus sign
 | ||||
| 	} | ||||
| 	return oni.cmd.Process.Kill() | ||||
| } | ||||
|  | @ -0,0 +1,13 @@ | |||
| package configui | ||||
| 
 | ||||
| import "os/exec" | ||||
| 
 | ||||
| func setgpid(cmd *exec.Cmd) { | ||||
| } | ||||
| 
 | ||||
| func (oni *Oni) Kill() error { | ||||
| 	if oni.cmd == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return oni.cmd.Process.Kill() | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -17,10 +17,6 @@ | |||
|   "dependencies": { | ||||
|     "@creativebulma/bulma-tooltip": "^1.2.0", | ||||
|     "bulma": "^0.9.3", | ||||
|     "prismjs": "^1.25.0", | ||||
|     "sass": "^1.43.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "parcel": "^2.0.0" | ||||
|   } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										54
									
								
								server.go
								
								
								
								
							
							
						
						
									
										54
									
								
								server.go
								
								
								
								
							|  | @ -5,6 +5,7 @@ import ( | |||
| 	"strings" | ||||
| 
 | ||||
| 	"kumoly.io/lib/ksrv" | ||||
| 	"kumoly.io/lib/stat" | ||||
| ) | ||||
| 
 | ||||
| func (cui *ConfigUI) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
|  | @ -30,6 +31,9 @@ func (cui *ConfigUI) middleware(next http.Handler) http.Handler { | |||
| 					panic("permission denied") | ||||
| 				} | ||||
| 			} | ||||
| 			if MutexLocked(&cui.configLock) { | ||||
| 				panic(ErrorServerReloading) | ||||
| 			} | ||||
| 			next.ServeHTTP(rw, r) | ||||
| 		}), | ||||
| 	) | ||||
|  | @ -98,7 +102,57 @@ func (cui *ConfigUI) mux() *http.ServeMux { | |||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "GET" { | ||||
| 			ksrv.JSON(w, stat.GetProfile()) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "GET" { | ||||
| 			cui.GetOni(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/start", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "POST" { | ||||
| 			cui.OniStart(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/stop", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "POST" { | ||||
| 			cui.OniStop(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/kill", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == "POST" { | ||||
| 			cui.OniKill(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/log", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == http.MethodGet { | ||||
| 			cui.OniLog(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.HandleFunc("/api/oni/attach", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if r.Method == http.MethodGet { | ||||
| 			cui.OniAttach(w, r) | ||||
| 		} else { | ||||
| 			w.WriteHeader(404) | ||||
| 		} | ||||
| 	}) | ||||
| 	cuiR.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(cui.PublicFS)))) | ||||
| 	cuiR.HandleFunc("/program", cui.Program) | ||||
| 	cuiR.HandleFunc("/", cui.App) | ||||
| 	return cuiR | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| 
 | ||||
| $modal-content-width: 90vw; | ||||
| $footer-padding: 0.5rem 1.5rem 0.5rem; | ||||
| // $navbar-z: 0; | ||||
| 
 | ||||
| @import "../node_modules/bulma/bulma.sass"; | ||||
| @import "../node_modules/@creativebulma/bulma-tooltip/src/sass/index.sass"; | ||||
|  |  | |||
|  | @ -6,11 +6,6 @@ | |||
|       </p> | ||||
|     </div> | ||||
|   </footer> | ||||
|   <script src="{{.BaseUrl}}public/ace/js/ace.js" type="text/javascript" charset="utf-8"></script> | ||||
|   <script src="{{.BaseUrl}}public/ace/js/ext-beautify.js" type="text/javascript" charset="utf-8"></script> | ||||
|   <script src="{{.BaseUrl}}public/ace/js/ext-searchbox.js" type="text/javascript" charset="utf-8"></script> | ||||
|   {{template "base/script" .}} | ||||
|   <!-- <script src="/public/js/main.js"></script> --> | ||||
|   </body> | ||||
| </html> | ||||
| {{end}} | ||||
|  | @ -8,4 +8,6 @@ | |||
|     <link rel="stylesheet" href="{{.BaseUrl}}public/css/main.css"> | ||||
|   </head> | ||||
|   <body> | ||||
|    | ||||
| {{template "base/nav" .}} | ||||
| {{end}} | ||||
|  | @ -0,0 +1,76 @@ | |||
| 
 | ||||
| {{define "base/nav"}} | ||||
| <nav class="navbar" role="navigation" aria-label="main navigation"> | ||||
|   <div class="navbar-brand"> | ||||
|     <a class="navbar-item" href="{{.BaseUrl}}"> | ||||
|       <p><strong>{{.AppName}}</strong></p> | ||||
|     </a> | ||||
| 
 | ||||
|     <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"> | ||||
|       <span aria-hidden="true"></span> | ||||
|       <span aria-hidden="true"></span> | ||||
|       <span aria-hidden="true"></span> | ||||
|     </a> | ||||
|   </div> | ||||
| 
 | ||||
|   <div id="navbarBasicExample" class="navbar-menu"> | ||||
|     {{/* | ||||
|     <div class="navbar-start"> | ||||
|       {{if .Actions}} | ||||
|       <a class="navbar-item"> | ||||
|         Actions | ||||
|       </a> | ||||
|       {{end}} | ||||
|       {{if .Integrations}} | ||||
|       <a class="navbar-item"> | ||||
|         Integrations | ||||
|       </a> | ||||
|       {{end}} | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="navbar-end"> | ||||
|       <div class="navbar-item"> | ||||
|         <div class="buttons"> | ||||
|           <a class="button is-primary"> | ||||
|             <strong>Sign up</strong> | ||||
|           </a> | ||||
|           <a class="button is-light"> | ||||
|             Log in | ||||
|           </a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     */}} | ||||
|   </div> | ||||
| </nav> | ||||
| 
 | ||||
| {{/* not having pages | ||||
| <script> | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
| 
 | ||||
|   // Get all "navbar-burger" elements | ||||
|   const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); | ||||
| 
 | ||||
|   // Check if there are any navbar burgers | ||||
|   if ($navbarBurgers.length > 0) { | ||||
| 
 | ||||
|     // Add a click event on each of them | ||||
|     $navbarBurgers.forEach( el => { | ||||
|       el.addEventListener('click', () => { | ||||
| 
 | ||||
|         // Get the target from the "data-target" attribute | ||||
|         const target = el.dataset.target; | ||||
|         const $target = document.getElementById(target); | ||||
| 
 | ||||
|         // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" | ||||
|         el.classList.toggle('is-active'); | ||||
|         $target.classList.toggle('is-active'); | ||||
| 
 | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| }); | ||||
| */}} | ||||
| </script> | ||||
| {{end}} | ||||
|  | @ -1,4 +1,8 @@ | |||
| {{define "base/script"}} | ||||
| 
 | ||||
| <script src="{{.BaseUrl}}public/ace/js/ace.js" type="text/javascript" charset="utf-8"></script> | ||||
| <script src="{{.BaseUrl}}public/ace/js/ext-beautify.js" type="text/javascript" charset="utf-8"></script> | ||||
| <script src="{{.BaseUrl}}public/ace/js/ext-searchbox.js" type="text/javascript" charset="utf-8"></script> | ||||
| <script> | ||||
| window.ToolIsFollow = false; | ||||
| window.LastDelta = 0; | ||||
|  |  | |||
|  | @ -14,6 +14,26 @@ | |||
|       </li> | ||||
|       {{ end }} | ||||
|     </ul> | ||||
|     {{if .Onitachi}} | ||||
|     <p class="menu-label"> | ||||
|       Programs | ||||
|     </p> | ||||
|     <ul class="menu-list"> | ||||
|       {{- range $key, $value := .Onitachi -}} | ||||
|       <li> | ||||
|         <a class="has-tooltip-arrow" data-tooltip="{{$value.Cmd}}" href="{{$.BaseUrl}}program?name={{.Name}}"> | ||||
|           <div class="level"> | ||||
|           <span>{{$key}}</span> | ||||
|             <div class="tags has-addons"> | ||||
|               <span class="tag">State</span> | ||||
|               <span class="tag {{$value.State|state_class}}">{{$value.State}}</span> | ||||
|             </div> | ||||
|           </div> | ||||
|         </a> | ||||
|       </li> | ||||
|       {{- end -}} | ||||
|     </ul> | ||||
|     {{end}} | ||||
|     {{if not .HideConfig}} | ||||
|     <p class="menu-label"> | ||||
|       System | ||||
|  |  | |||
|  | @ -5,14 +5,6 @@ | |||
| var Active = "{{.File.Name}}"; | ||||
| </script> | ||||
| 
 | ||||
| <section class="hero is-small is-primary"> | ||||
|   <div class="hero-body" id="title"> | ||||
|     <p class="title"> | ||||
|       {{.AppName}} | ||||
|     </p> | ||||
|   </div> | ||||
| </section> | ||||
| 
 | ||||
| <div class="columns"> | ||||
|   <div class="column is-one-quarter"> | ||||
|     <div class="box"> | ||||
|  | @ -88,5 +80,6 @@ async function toolDoIntegration(name){ | |||
| {{if not .ResultBellow}} | ||||
| {{template "components/result" .}} | ||||
| {{end}} | ||||
| {{template "base/script" .}} | ||||
| {{template "base/footer" .}} | ||||
| {{end}} | ||||
|  | @ -0,0 +1,73 @@ | |||
| {{define "muzan"}} | ||||
| {{template "base/header" .}} | ||||
| {{template "components/error" .}} | ||||
| <script> | ||||
| var Active = "{{.File.Name}}"; | ||||
| </script> | ||||
| 
 | ||||
| <div class="columns"> | ||||
|   <div class="column is-one-quarter"> | ||||
|     <div class="box"> | ||||
|       {{template "components/menu" .}} | ||||
|     </div> | ||||
|     {{if .Actions}} | ||||
|     <div class="box"> | ||||
|       <p class="menu-label has-text-left">Actions</p> | ||||
|       <div class="buttons are-small"> | ||||
|       {{- range .Actions -}} | ||||
|         <button class="button has-tooltip-arrow" id="actbtn-{{.Name|normal}}" | ||||
|           data-tooltip="{{.Cmd}}" onclick="toolDoAction('{{.Name}}')"> | ||||
|           <span>{{- .Name -}}</span> | ||||
|         </button> | ||||
|       {{- end -}} | ||||
|       </div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|     {{if .Integrations}} | ||||
|     <div class="box"> | ||||
|       <p class="menu-label has-text-left">Integrations</p> | ||||
|       <div class="buttons are-small"> | ||||
|       {{- range .Integrations -}} | ||||
|         <button class="button has-tooltip-arrow" id="itgbtn-{{.Name|normal}}" | ||||
|           data-tooltip="{{.Description}}" onclick="toolDoIntegration('{{.Name}}')"> | ||||
|           <span>{{- .Name -}}</span> | ||||
|         </button> | ||||
|       {{- end -}} | ||||
|       </div> | ||||
|     </div> | ||||
|     {{end}} | ||||
|     <div class="box has-text-centered"> | ||||
|       <a href="{{.BaseUrl}}api/export" class="button is-small">Export Files</a> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="column"> | ||||
|     <div class="box"> | ||||
|       Home | ||||
|       {{/* {{template "components/editor" .}} */}} | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <script> | ||||
| async function toolDoAction(name){ | ||||
|   let el = document.getElementById('actbtn-'+name.replaceAll(" ","-")); | ||||
|   el.classList.add("is-loading") | ||||
|   el.classList.remove("has-tooltip-arrow") | ||||
|   await DoAction('action',name) | ||||
|   el.classList.remove("is-loading") | ||||
|   el.classList.add("has-tooltip-arrow") | ||||
|   {{if not .ResultBellow}}ResultViewTog(){{end}} | ||||
| } | ||||
| 
 | ||||
| async function toolDoIntegration(name){ | ||||
|   let el = document.getElementById('itgbtn-'+name.replaceAll(" ","-")); | ||||
|   el.classList.add("is-loading") | ||||
|   el.classList.remove("has-tooltip-arrow") | ||||
|   await DoAction('integration',name) | ||||
|   el.classList.remove("is-loading") | ||||
|   el.classList.add("has-tooltip-arrow") | ||||
|   {{if not .ResultBellow}}ResultViewTog(){{end}} | ||||
| } | ||||
| </script> | ||||
| {{template "base/footer" .}} | ||||
| {{end}} | ||||
							
								
								
									
										25
									
								
								util.go
								
								
								
								
							
							
						
						
									
										25
									
								
								util.go
								
								
								
								
							|  | @ -5,7 +5,10 @@ import ( | |||
| 	"compress/gzip" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error { | ||||
|  | @ -65,3 +68,25 @@ 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) | ||||
| } | ||||
| 
 | ||||
| const mutexLocked = 1 | ||||
| 
 | ||||
| func MutexLocked(m *sync.Mutex) bool { | ||||
| 	state := reflect.ValueOf(m).Elem().FieldByName("state") | ||||
| 	return state.Int()&mutexLocked == mutexLocked | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue