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 | 	BaseUrl      string | ||||||
| 	Actions      []Action | 	Actions      []Action | ||||||
| 	Integrations []*Integration | 	Integrations []*Integration | ||||||
|  | 	Onitachi     map[string]*Oni | ||||||
| 	Version      string | 	Version      string | ||||||
| 	Build        string | 	Build        string | ||||||
| 	Files        []ActiveFile | 	Files        []ActiveFile | ||||||
|  | @ -79,6 +80,7 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) { | ||||||
| 		}, | 		}, | ||||||
| 		Actions:      cui.Actions, | 		Actions:      cui.Actions, | ||||||
| 		Integrations: cui.Integrations, | 		Integrations: cui.Integrations, | ||||||
|  | 		Onitachi:     cui.Onitachi, | ||||||
| 		Static:       cui.NoReconfig, | 		Static:       cui.NoReconfig, | ||||||
| 		HideConfig:   cui.HideConfig, | 		HideConfig:   cui.HideConfig, | ||||||
| 		ResultBellow: cui.ResultBellow, | 		ResultBellow: cui.ResultBellow, | ||||||
|  | @ -127,3 +129,44 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) { | ||||||
| 		panic(err) | 		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(&flagAction, "c", "", "cmd to apply") | ||||||
| 	flag.StringVar(&flagLogFile, "log", "", "log to file") | 	flag.StringVar(&flagLogFile, "log", "", "log to file") | ||||||
| 	flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all") | 	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(&flagNoReconfig, "static", false, "disable config api") | ||||||
| 	flag.BoolVar(&flagVer, "v", false, "show version") | 	flag.BoolVar(&flagVer, "v", false, "show version") | ||||||
| 	flag.Usage = func() { | 	flag.Usage = func() { | ||||||
|  |  | ||||||
							
								
								
									
										112
									
								
								configui.go
								
								
								
								
							
							
						
						
									
										112
									
								
								configui.go
								
								
								
								
							|  | @ -7,6 +7,8 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  | @ -16,8 +18,9 @@ import ( | ||||||
| 	"kumoly.io/tools/configui/public" | 	"kumoly.io/tools/configui/public" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var UNIX_SHELL = "/usr/bin/sh" | const UNIX_SHELL = "/usr/bin/sh" | ||||||
| var WIN_SHELL = "C:\\Windows\\System32\\cmd" | const WIN_SHELL = "C:\\Windows\\System32\\cmd" | ||||||
|  | const DARWIN_SHELL = "/bin/bash" | ||||||
| 
 | 
 | ||||||
| const version = "v0.1.12" | const version = "v0.1.12" | ||||||
| 
 | 
 | ||||||
|  | @ -34,10 +37,12 @@ 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:"-"` | 
 | ||||||
|  | 	run chan struct{} `json:"-"` | ||||||
|  | 	pid int | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Integration struct { | type Integration struct { | ||||||
|  | @ -52,6 +57,7 @@ type ConfigUI struct { | ||||||
| 	Prod       bool   `json:"production"` | 	Prod       bool   `json:"production"` | ||||||
| 	BaseUrl    string `json:"base_url"` | 	BaseUrl    string `json:"base_url"` | ||||||
| 	ConfigPath string `json:"config_path"` | 	ConfigPath string `json:"config_path"` | ||||||
|  | 	SHELL      string `json:"shell"` | ||||||
| 
 | 
 | ||||||
| 	NoReconfig bool   `json:"no_reconfig"` | 	NoReconfig bool   `json:"no_reconfig"` | ||||||
| 	AllowIP    string `json:"allow_ip"` | 	AllowIP    string `json:"allow_ip"` | ||||||
|  | @ -70,6 +76,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:"onitachi"` | ||||||
|  | 
 | ||||||
| 	TmplFS     embed.FS `json:"-"` | 	TmplFS     embed.FS `json:"-"` | ||||||
| 	tmpl       *engine.Engine | 	tmpl       *engine.Engine | ||||||
| 	PublicFS   embed.FS `json:"-"` | 	PublicFS   embed.FS `json:"-"` | ||||||
|  | @ -91,9 +100,43 @@ func New() *ConfigUI { | ||||||
| 		"normal": func(name string) string { | 		"normal": func(name string) string { | ||||||
| 			return strings.ReplaceAll(name, " ", "-") | 			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")) | 	}).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{ | 	return &ConfigUI{ | ||||||
| 		fileIndex:  map[string]int{}, | 		fileIndex:  map[string]int{}, | ||||||
|  | 		SHELL:      sh, | ||||||
| 		Prod:       true, | 		Prod:       true, | ||||||
| 		AppName:    "ConfigUI", | 		AppName:    "ConfigUI", | ||||||
| 		BaseUrl:    "/", | 		BaseUrl:    "/", | ||||||
|  | @ -105,6 +148,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), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -125,6 +169,8 @@ func (cui *ConfigUI) File(file_name string) (*File, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (cui *ConfigUI) LoadConfig(confstr string) error { | func (cui *ConfigUI) LoadConfig(confstr string) error { | ||||||
|  | 	cui.configLock.Lock() | ||||||
|  | 	defer cui.configLock.Unlock() | ||||||
| 	tmpConf := &ConfigUI{} | 	tmpConf := &ConfigUI{} | ||||||
| 	err := json.Unmarshal([]byte(confstr), tmpConf) | 	err := json.Unmarshal([]byte(confstr), tmpConf) | ||||||
| 	if err != nil { | 	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
 | 	// copy
 | ||||||
| 	cui.configLock.Lock() |  | ||||||
| 	defer cui.configLock.Unlock() |  | ||||||
| 	cui.fileIndex = tmpIndex | 	cui.fileIndex = tmpIndex | ||||||
| 	cui.Files = tmpConf.Files | 	cui.Files = tmpConf.Files | ||||||
| 	cui.AllowIP = tmpConf.AllowIP | 	cui.AllowIP = tmpConf.AllowIP | ||||||
|  | @ -159,6 +211,40 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | ||||||
| 	cui.HideConfig = tmpConf.HideConfig | 	cui.HideConfig = tmpConf.HideConfig | ||||||
| 	cui.NoReconfig = tmpConf.NoReconfig | 	cui.NoReconfig = tmpConf.NoReconfig | ||||||
| 	cui.ResultBellow = tmpConf.ResultBellow | 	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 | 	cui.Actions = tmpConf.Actions | ||||||
| 	for i := range cui.Actions { | 	for i := range cui.Actions { | ||||||
|  | @ -186,15 +272,6 @@ func (cui *ConfigUI) LoadConfig(confstr string) error { | ||||||
| 		cui.cmdTimeout = ct | 		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)
 | 	// fmt.Printf("%+v", cui)
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | @ -205,6 +282,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) | ||||||
|  |  | ||||||
|  | @ -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()) | 	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 == "" { | 	if f.Cmd == "" { | ||||||
| 		return "", nil | 		return "", nil | ||||||
| 	} | 	} | ||||||
|  | @ -80,9 +80,9 @@ func (f *File) Do(CmdTimeout time.Duration) (string, error) { | ||||||
| 	// prepare cmd
 | 	// prepare cmd
 | ||||||
| 	cmd := &exec.Cmd{} | 	cmd := &exec.Cmd{} | ||||||
| 	if runtime.GOOS == "windows" { | 	if runtime.GOOS == "windows" { | ||||||
| 		cmd = exec.Command(WIN_SHELL, "/c", f.Cmd) | 		cmd = exec.Command(f.owner.SHELL, "/c", f.Cmd) | ||||||
| 	} else { | 	} 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) | 	f.owner.log.Info("DO: ", f.Cmd) | ||||||
| 	done := make(chan string, 1) | 	done := make(chan string, 1) | ||||||
|  | @ -96,6 +96,7 @@ func (f *File) Do(CmdTimeout time.Duration) (string, error) { | ||||||
| 	} | 	} | ||||||
| 	go func() { | 	go func() { | ||||||
| 		f.pid = cmd.Process.Pid | 		f.pid = cmd.Process.Pid | ||||||
|  | 		report <- cmd.Process.Pid | ||||||
| 		cmd.Wait() | 		cmd.Wait() | ||||||
| 		done <- b.String() | 		done <- b.String() | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										3
									
								
								go.mod
								
								
								
								
							|  | @ -4,10 +4,11 @@ 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 ( | ||||||
| 	github.com/mattn/go-isatty v0.0.14 // indirect | 	github.com/mattn/go-isatty v0.0.14 // indirect | ||||||
| 	golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // 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/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= | ||||||
|  | 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" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	"kumoly.io/lib/ksrv" | 	"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")) | 		ksrv.Response(w, 404, []byte("file not found")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	result, err := file.Do(cui.cmdTimeout) | 	result, err := file.Do(cui.cmdTimeout, make(chan int)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		panic(err) | 		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} | 			file := &File{Name: name, Cmd: v.Cmd, owner: cui} | ||||||
|  | 			pid := make(chan int) | ||||||
| 			go func() { | 			go func() { | ||||||
| 				<-time.After(time.Millisecond * 10) | 				cui.Actions[i].pid = <-pid | ||||||
| 				cui.Actions[i].pid = file.pid |  | ||||||
| 			}() | 			}() | ||||||
| 			result, err := file.Do(cui.cmdTimeout) | 			result, err := file.Do(cui.cmdTimeout, pid) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				panic(err) | 				panic(err) | ||||||
| 			} | 			} | ||||||
| 			w.Write([]byte(result)) | 			w.Write([]byte(result)) | ||||||
|  | 
 | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -196,8 +196,8 @@ func (cui *ConfigUI) Download(w http.ResponseWriter, r *http.Request) { | ||||||
| 				panic(err) | 				panic(err) | ||||||
| 			} | 			} | ||||||
| 			w.Header().Set( | 			w.Header().Set( | ||||||
| 				"Content-Disposition", ` | 				"Content-Disposition", | ||||||
| 				attachment; filename="`+cui.AppName+`.json"`, | 				`attachment; filename="`+cui.AppName+`.json"`, | ||||||
| 			) | 			) | ||||||
| 			w.Write(data) | 			w.Write(data) | ||||||
| 			return | 			return | ||||||
|  | @ -259,3 +259,99 @@ func (cui *ConfigUI) GetConfig(w http.ResponseWriter, r *http.Request) { | ||||||
| 	} | 	} | ||||||
| 	w.Write(data) | 	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": { |   "dependencies": { | ||||||
|     "@creativebulma/bulma-tooltip": "^1.2.0", |     "@creativebulma/bulma-tooltip": "^1.2.0", | ||||||
|     "bulma": "^0.9.3", |     "bulma": "^0.9.3", | ||||||
|     "prismjs": "^1.25.0", |  | ||||||
|     "sass": "^1.43.2" |     "sass": "^1.43.2" | ||||||
|   }, |  | ||||||
|   "devDependencies": { |  | ||||||
|     "parcel": "^2.0.0" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										54
									
								
								server.go
								
								
								
								
							
							
						
						
									
										54
									
								
								server.go
								
								
								
								
							|  | @ -5,6 +5,7 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"kumoly.io/lib/ksrv" | 	"kumoly.io/lib/ksrv" | ||||||
|  | 	"kumoly.io/lib/stat" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (cui *ConfigUI) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 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") | 					panic("permission denied") | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 			if MutexLocked(&cui.configLock) { | ||||||
|  | 				panic(ErrorServerReloading) | ||||||
|  | 			} | ||||||
| 			next.ServeHTTP(rw, r) | 			next.ServeHTTP(rw, r) | ||||||
| 		}), | 		}), | ||||||
| 	) | 	) | ||||||
|  | @ -98,7 +102,57 @@ func (cui *ConfigUI) mux() *http.ServeMux { | ||||||
| 			w.WriteHeader(404) | 			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.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(cui.PublicFS)))) | ||||||
|  | 	cuiR.HandleFunc("/program", cui.Program) | ||||||
| 	cuiR.HandleFunc("/", cui.App) | 	cuiR.HandleFunc("/", cui.App) | ||||||
| 	return cuiR | 	return cuiR | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
| 
 | 
 | ||||||
| $modal-content-width: 90vw; | $modal-content-width: 90vw; | ||||||
| $footer-padding: 0.5rem 1.5rem 0.5rem; | $footer-padding: 0.5rem 1.5rem 0.5rem; | ||||||
|  | // $navbar-z: 0; | ||||||
| 
 | 
 | ||||||
| @import "../node_modules/bulma/bulma.sass"; | @import "../node_modules/bulma/bulma.sass"; | ||||||
| @import "../node_modules/@creativebulma/bulma-tooltip/src/sass/index.sass"; | @import "../node_modules/@creativebulma/bulma-tooltip/src/sass/index.sass"; | ||||||
|  |  | ||||||
|  | @ -6,11 +6,6 @@ | ||||||
|       </p> |       </p> | ||||||
|     </div> |     </div> | ||||||
|   </footer> |   </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> |   </body> | ||||||
| </html> | </html> | ||||||
| {{end}} | {{end}} | ||||||
|  | @ -8,4 +8,6 @@ | ||||||
|     <link rel="stylesheet" href="{{.BaseUrl}}public/css/main.css"> |     <link rel="stylesheet" href="{{.BaseUrl}}public/css/main.css"> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|  |    | ||||||
|  | {{template "base/nav" .}} | ||||||
| {{end}} | {{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"}} | {{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> | <script> | ||||||
| window.ToolIsFollow = false; | window.ToolIsFollow = false; | ||||||
| window.LastDelta = 0; | window.LastDelta = 0; | ||||||
|  |  | ||||||
|  | @ -14,6 +14,26 @@ | ||||||
|       </li> |       </li> | ||||||
|       {{ end }} |       {{ end }} | ||||||
|     </ul> |     </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}} |     {{if not .HideConfig}} | ||||||
|     <p class="menu-label"> |     <p class="menu-label"> | ||||||
|       System |       System | ||||||
|  |  | ||||||
|  | @ -5,14 +5,6 @@ | ||||||
| var Active = "{{.File.Name}}"; | var Active = "{{.File.Name}}"; | ||||||
| </script> | </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="columns"> | ||||||
|   <div class="column is-one-quarter"> |   <div class="column is-one-quarter"> | ||||||
|     <div class="box"> |     <div class="box"> | ||||||
|  | @ -88,5 +80,6 @@ async function toolDoIntegration(name){ | ||||||
| {{if not .ResultBellow}} | {{if not .ResultBellow}} | ||||||
| {{template "components/result" .}} | {{template "components/result" .}} | ||||||
| {{end}} | {{end}} | ||||||
|  | {{template "base/script" .}} | ||||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||||
| {{end}} | {{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" | 	"compress/gzip" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error { | 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 | 	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