Compare commits

..

4 Commits

Author SHA1 Message Date
Evan Chen ef76eff228 v0.1.11
continuous-integration/drone/tag Build is passing Details
2021-11-11 10:37:43 +08:00
Evan Chen 4415ffe2ee fix: deprecate 'action' and use 'cmd' in file 2021-11-11 10:21:10 +08:00
Evan Chen acec4a6af9 fix: error togging unexpectedly 2021-11-11 10:05:55 +08:00
Evan Chen eddd813846 limit run command to single instance 2021-11-11 09:58:36 +08:00
10 changed files with 110 additions and 42 deletions

4
app.go
View File

@ -12,7 +12,7 @@ type ActiveFile struct {
RO bool RO bool
Path string Path string
Name string Name string
Action string Cmd string
Content string Content string
Order int Order int
} }
@ -100,7 +100,7 @@ func (cui *ConfigUI) App(w http.ResponseWriter, r *http.Request) {
} }
} else { } else {
tmp, err = file.Read() tmp, err = file.Read()
data.File.Action = file.Action data.File.Cmd = file.Cmd
data.File.Name = file.Name data.File.Name = file.Name
if file.Lang != "" { if file.Lang != "" {
data.Editor.Lang = file.Lang data.Editor.Lang = file.Lang

View File

@ -19,7 +19,7 @@ import (
var UNIX_SHELL = "sh" var UNIX_SHELL = "sh"
var WIN_SHELL = "cmd" var WIN_SHELL = "cmd"
const version = "v0.1.10" const version = "v0.1.11"
//go:embed templates //go:embed templates
var tmplFS embed.FS var tmplFS embed.FS
@ -34,14 +34,17 @@ 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:"-"`
pid int `json:"-"`
} }
type Integration struct { type Integration struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Cmd func() (string, error) `json:"-"` Cmd func() (string, error) `json:"-"`
run chan struct{} `json:"-"`
} }
type ConfigUI struct { type ConfigUI struct {
@ -136,6 +139,12 @@ func (cui *ConfigUI) LoadConfig(confstr string) error {
} }
f.owner = cui f.owner = cui
tmpIndex[f.Name] = i tmpIndex[f.Name] = i
// deprecated fix
if f.Cmd == "" && f.Action != "" {
f.Cmd = f.Action
f.Action = ""
}
} }
// copy // copy

53
file.go
View File

@ -1,7 +1,9 @@
package configui package configui
import ( import (
"bytes"
"errors" "errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
@ -10,9 +12,9 @@ import (
) )
type File struct { type File struct {
Path string `json:"path"` Path string `json:"path"`
Name string `json:"name"` Name string `json:"name"`
Action string `json:"action"` Cmd string `json:"cmd"`
// RO is readonly // RO is readonly
RO bool `json:"ro"` RO bool `json:"ro"`
Lang string `json:"lang"` Lang string `json:"lang"`
@ -23,8 +25,13 @@ type File struct {
// used for parsing post data // used for parsing post data
Data string `json:"data,omitempty"` Data string `json:"data,omitempty"`
lock sync.RWMutex `json:"-"` lock sync.RWMutex `json:"-"`
owner *ConfigUI `json:"-"` owner *ConfigUI `json:"-"`
run chan struct{} `json:"-"`
pid int `json:"-"`
// deprecated
Action string `json:"action,omitempty"`
} }
func (f *File) Read() ([]byte, error) { func (f *File) Read() ([]byte, error) {
@ -52,26 +59,42 @@ func (f *File) Write(data []byte) error {
} }
func (f *File) Do(CmdTimeout time.Duration) (string, error) { func (f *File) Do(CmdTimeout time.Duration) (string, error) {
if f.Action == "" { if f.Cmd == "" {
return "", nil return "", nil
} }
f.lock.RLock() f.lock.RLock()
defer f.lock.RUnlock() defer f.lock.RUnlock()
// limit running instance to 1
if cap(f.run) == 0 {
f.run = make(chan struct{}, 1)
}
select {
case f.run <- struct{}{}:
defer func() { <-f.run }()
default:
f.owner.log.Error("task is running: ", f.Name)
return "", fmt.Errorf("another task of %s is running with pid: %d", f.Name, f.pid)
}
// prepare cmd
cmd := &exec.Cmd{} cmd := &exec.Cmd{}
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
cmd = exec.Command(WIN_SHELL, "/c", f.Action) cmd = exec.Command(WIN_SHELL, "/c", f.Cmd)
} else { } else {
cmd = exec.Command(UNIX_SHELL, "-c", f.Action) cmd = exec.Command(UNIX_SHELL, "-c", f.Cmd)
} }
f.owner.log.Info("DO: ", f.Action) f.owner.log.Info("DO: ", f.Cmd)
done := make(chan string, 1) done := make(chan string, 1)
go func() { go func() {
out, _ := cmd.CombinedOutput()
// real cmd err is unhandled, but passed to client var b bytes.Buffer
// if err != nil { cmd.Stdout = &b
// return string(out), err cmd.Stderr = &b
// } cmd.Start()
done <- string(out) f.pid = cmd.Process.Pid
cmd.Wait()
done <- b.String()
}() }()
select { select {
case <-time.After(CmdTimeout): case <-time.After(CmdTimeout):

2
go.mod
View File

@ -3,7 +3,7 @@ module kumoly.io/tools/configui
go 1.17 go 1.17
require ( require (
kumoly.io/lib/klog v0.0.5 kumoly.io/lib/klog v0.0.8
kumoly.io/lib/ksrv v0.0.1 kumoly.io/lib/ksrv v0.0.1
) )

5
go.sum
View File

@ -3,9 +3,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
kumoly.io/lib/klog v0.0.4 h1:Ev9G/zvLt/C8Q1yWfYoUcXVJWgPMjpqHPat2WKyOPIM=
kumoly.io/lib/klog v0.0.4/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M= kumoly.io/lib/klog v0.0.4/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M=
kumoly.io/lib/klog v0.0.5 h1:8Z2FYpW01gxt2gbRJNag8f8KSsfd1pRleQTPJvoR8Zc= kumoly.io/lib/klog v0.0.8 h1:6hTfDlZh7KGnPrd2tUrauCKRImSnyyN9DHXpey3Czn8=
kumoly.io/lib/klog v0.0.5/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=

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"time"
"kumoly.io/lib/ksrv" "kumoly.io/lib/ksrv"
) )
@ -36,11 +37,11 @@ func (cui *ConfigUI) GetFile(w http.ResponseWriter, r *http.Request) {
panic(err) panic(err)
} }
response, err := json.Marshal(map[string]string{ response, err := json.Marshal(map[string]string{
"path": file.Path, "path": file.Path,
"name": file.Name, "name": file.Name,
"action": file.Action, "cmd": file.Cmd,
"data": string(data), "data": string(data),
"delta": strconv.Itoa(int(stat.Size())), "delta": strconv.Itoa(int(stat.Size())),
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@ -80,11 +81,11 @@ func (cui *ConfigUI) GetDelta(w http.ResponseWriter, r *http.Request) {
} }
response, err := json.Marshal(map[string]string{ response, err := json.Marshal(map[string]string{
"path": file.Path, "path": file.Path,
"name": file.Name, "name": file.Name,
"action": file.Action, "cmd": file.Cmd,
"data": string(buf), "data": string(buf),
"delta": strconv.Itoa(int(stat.Size())), "delta": strconv.Itoa(int(stat.Size())),
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@ -130,9 +131,25 @@ func (cui *ConfigUI) Apply(w http.ResponseWriter, r *http.Request) {
func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) { func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
for _, v := range cui.Actions { for i, v := range cui.Actions {
if v.Name == name { if v.Name == name {
// limit running instance to one
if cap(cui.Actions[i].run) != 1 {
cui.Actions[i].run = make(chan struct{}, 1)
}
select {
case cui.Actions[i].run <- struct{}{}:
defer func() { <-cui.Actions[i].run }()
default:
panic(fmt.Errorf("another task of %s is running with pid: %d", name, v.pid))
}
file := &File{Name: name, Action: v.Cmd, owner: cui} file := &File{Name: name, Action: v.Cmd, owner: cui}
go func() {
<-time.After(time.Millisecond * 10)
cui.Actions[i].pid = file.pid
}()
result, err := file.Do(cui.cmdTimeout) result, err := file.Do(cui.cmdTimeout)
if err != nil { if err != nil {
panic(err) panic(err)
@ -146,8 +163,20 @@ func (cui *ConfigUI) DoAction(w http.ResponseWriter, r *http.Request) {
func (cui *ConfigUI) DoIntegration(w http.ResponseWriter, r *http.Request) { func (cui *ConfigUI) DoIntegration(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
for _, v := range cui.Integrations { for i, v := range cui.Integrations {
if v.Name == name { if v.Name == name {
// limit running instance to one
if cap(cui.Integrations[i].run) != 1 {
cui.Integrations[i].run = make(chan struct{}, 1)
}
select {
case cui.Integrations[i].run <- struct{}{}:
defer func() { <-cui.Integrations[i].run }()
default:
panic(fmt.Errorf("another task of %s is running", name))
}
result, err := v.Cmd() result, err := v.Cmd()
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -1,6 +1,6 @@
{{define "components/error"}} {{define "components/error"}}
<div class="notification is-danger {{if not .Error}}is-hidden{{end}}" id="error_view"> <div class="notification is-danger {{if not .Error}}is-hidden{{end}}" id="error_view">
<button class="delete" onclick="ErrorTog()"></button> <button class="delete" onclick="ErrorClose()"></button>
<p id="err_msg">{{.Error}}</p> <p id="err_msg">{{.Error}}</p>
</div> </div>
<script> <script>
@ -8,10 +8,18 @@ function ErrorTog(){
let el = document.getElementById('error_view'); let el = document.getElementById('error_view');
el.classList.toggle('is-hidden'); el.classList.toggle('is-hidden');
} }
function ErrorOpen(){
let el = document.getElementById('error_view');
el.classList.remove('is-hidden');
}
function ErrorClose(){
let el = document.getElementById('error_view');
el.classList.add('is-hidden');
}
function ShowError(msg){ function ShowError(msg){
let el = document.getElementById('err_msg'); let el = document.getElementById('err_msg');
el.innerHTML = msg; el.innerHTML = msg;
ErrorTog(); ErrorOpen();
} }
</script> </script>
{{end}} {{end}}

View File

@ -21,7 +21,7 @@
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a class="has-tooltip-arrow {{if eq .File.Name .AppName }}is-active{{end}}" <a class="has-tooltip-arrow {{if eq .File.Name .AppName }}is-active{{end}}"
data-tooltip="{{$.File.Path}}" data-tooltip="Configuration"
href="{{.BaseUrl}}" href="{{.BaseUrl}}"
>Config</a></li> >Config</a></li>
</ul> </ul>

View File

@ -7,7 +7,7 @@
<button class="delete" aria-label="close" onclick="ResultViewTog()"></button> <button class="delete" aria-label="close" onclick="ResultViewTog()"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<p class="title">{{.File.Action}}</p> <p class="title">{{.File.Cmd}}</p>
<p class="subtitle">{{.File.Path}}</p> <p class="subtitle">{{.File.Path}}</p>
<div id="result_editor"></div> <div id="result_editor"></div>
</section> </section>

View File

@ -91,10 +91,10 @@
{{end}} {{end}}
</p> </p>
{{end}} {{end}}
{{if .File.Action}} {{if .File.Cmd}}
<p class="control"> <p class="control">
<button class="button is-small has-tooltip-arrow" <button class="button is-small has-tooltip-arrow"
data-tooltip="{{.File.Action}}" data-tooltip="{{.File.Cmd}}"
onclick="toolApply()"> onclick="toolApply()">
<span class="icon is-small"><span class="material-icons"id="toolApplyIco"> <span class="icon is-small"><span class="material-icons"id="toolApplyIco">
play_arrow play_arrow