initial commit
This commit is contained in:
commit
af40e0890f
97
App.go
Normal file
97
App.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
cfg AppConfig
|
||||||
|
|
||||||
|
rxTaskInfo *regexp.Regexp
|
||||||
|
|
||||||
|
tasksMtx sync.RWMutex
|
||||||
|
tasks map[string]Task
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(configPath string) (*App, error) {
|
||||||
|
|
||||||
|
confBytes, err := ioutil.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Couldn't open configuration file '%s': %s", configPath, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := AppConfig{}
|
||||||
|
err = json.Unmarshal(confBytes, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid configuration file: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewAppFromConfig(cfg), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAppFromConfig(cfg AppConfig) *App {
|
||||||
|
return &App{
|
||||||
|
cfg: cfg,
|
||||||
|
rxTaskInfo: regexp.MustCompile(`^/task/([A-Z0-9]+)$`),
|
||||||
|
tasks: make(map[string]Task),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Server", "webcmd/1.0")
|
||||||
|
|
||||||
|
if r.Method == "GET" {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
this.Serve_Homepage(w)
|
||||||
|
|
||||||
|
} else if r.URL.Path == "/style.css" {
|
||||||
|
this.Serve_StyleCSS(w)
|
||||||
|
|
||||||
|
} else if r.URL.Path == "/tasks" {
|
||||||
|
this.Serve_Tasks(w)
|
||||||
|
|
||||||
|
} else if matches := this.rxTaskInfo.FindStringSubmatch(r.URL.Path); len(matches) == 2 {
|
||||||
|
this.Serve_TaskInfo(w, matches[1])
|
||||||
|
|
||||||
|
} else {
|
||||||
|
http.Error(w, "No matching route for request", 404)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if r.Method == "POST" {
|
||||||
|
if r.URL.Path == "/x-new-task" {
|
||||||
|
this.Action_NewTask(w, r)
|
||||||
|
|
||||||
|
} else if r.URL.Path == "/x-abandon-task" {
|
||||||
|
this.Action_AbandonTask(w, r)
|
||||||
|
|
||||||
|
} else if r.URL.Path == "/x-clear-completed-tasks" {
|
||||||
|
this.Action_ClearCompleted(w, r)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
http.Error(w, "No matching route for request", 404)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Invalid method", 400)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *App) Run() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/", this)
|
||||||
|
|
||||||
|
log.Printf("Listening on '%s'...", this.cfg.ListenAddress)
|
||||||
|
err := http.ListenAndServe(this.cfg.ListenAddress, mux)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Network error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
24
Makefile
Normal file
24
Makefile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
OBJS := $(addprefix webcmd.,linux64 linux32 win64 win32)
|
||||||
|
|
||||||
|
all: $(OBJS)
|
||||||
|
|
||||||
|
define compile
|
||||||
|
/bin/bash -c 'GOARCH=$(1) GOOS=$(2) go build -o ./webcmd.$(3) -ldflags "-s -w" ./cmd/webcmd'
|
||||||
|
endef
|
||||||
|
|
||||||
|
webcmd.linux64:
|
||||||
|
$(call compile,amd64,linux,linux64)
|
||||||
|
|
||||||
|
webcmd.linux32:
|
||||||
|
$(call compile,386,linux,linux32)
|
||||||
|
|
||||||
|
webcmd.win64:
|
||||||
|
$(call compile,amd64,windows,win64)
|
||||||
|
|
||||||
|
webcmd.win32:
|
||||||
|
$(call compile,386,windows,win32)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm $(OBJS)
|
121
Task.go
Normal file
121
Task.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutputLine struct {
|
||||||
|
isError bool
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
output []OutputLine
|
||||||
|
started int64
|
||||||
|
stopped int64
|
||||||
|
exitCode int
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) Finished() bool {
|
||||||
|
return t.stopped != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func uuid() string {
|
||||||
|
buff := make([]byte, 15) // multiples of five are best for base32
|
||||||
|
_, err := rand.Read(buff)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base32.StdEncoding.EncodeToString(buff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LaunchTask creates a new task from the given command parameters.
|
||||||
|
func (this *App) LaunchTask(params []string) (taskRef string, err error) {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return "", errors.New("No parameters for task")
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := uuid()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cmd := exec.CommandContext(ctx, params[0], params[1:]...)
|
||||||
|
|
||||||
|
errPipe, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
outPipe, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tasksMtx.Lock()
|
||||||
|
this.tasks[ref] = Task{
|
||||||
|
cmd: cmd,
|
||||||
|
output: make([]OutputLine, 0),
|
||||||
|
started: time.Now().Unix(),
|
||||||
|
stopped: 0,
|
||||||
|
exitCode: 0,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
this.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
writeline := func(text string, isError bool) {
|
||||||
|
this.tasksMtx.Lock()
|
||||||
|
defer this.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
task := this.tasks[ref]
|
||||||
|
task.output = append(task.output, OutputLine{isError: isError, text: text})
|
||||||
|
this.tasks[ref] = task
|
||||||
|
}
|
||||||
|
|
||||||
|
pipe2line := func(rc io.ReadCloser, isError bool) {
|
||||||
|
defer wg.Done()
|
||||||
|
sc := bufio.NewScanner(rc)
|
||||||
|
for sc.Scan() {
|
||||||
|
writeline(sc.Text(), isError)
|
||||||
|
}
|
||||||
|
rc.Close()
|
||||||
|
}
|
||||||
|
go pipe2line(errPipe, true)
|
||||||
|
go pipe2line(outPipe, false)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
err := cmd.Wait()
|
||||||
|
stopTime := time.Now().Unix()
|
||||||
|
exitCode := 0
|
||||||
|
if err != nil {
|
||||||
|
writeline(err.Error(), true)
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tasksMtx.Lock()
|
||||||
|
defer this.tasksMtx.Unlock()
|
||||||
|
task := this.tasks[ref]
|
||||||
|
task.stopped = stopTime
|
||||||
|
task.exitCode = exitCode
|
||||||
|
this.tasks[ref] = task
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ref, nil
|
||||||
|
}
|
20
cmd/webcmd/main.go
Normal file
20
cmd/webcmd/main.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"code.ivysaur.me/webcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
confPath := flag.String("config", "webcmd.conf", "Path to configuration file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
app, err := webcmd.NewApp(*confPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run()
|
||||||
|
}
|
20
cmd/webcmd/webcmd.conf
Normal file
20
cmd/webcmd/webcmd.conf
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"AppTitle": "Looking Glass",
|
||||||
|
|
||||||
|
"ListenAddress": ":8192",
|
||||||
|
|
||||||
|
"Commands": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"Title": "Ping",
|
||||||
|
"Execution": [
|
||||||
|
{"ParamType": 0, "Value": "/bin/ping"},
|
||||||
|
{"ParamType": 0, "Value": "-c"},
|
||||||
|
{"ParamType": 0, "Value": "4"},
|
||||||
|
{"ParamType": 0, "Value": "--"},
|
||||||
|
{"ParamType": 1, "Value": "example.com", "Description": "Target host"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
33
config.go
Normal file
33
config.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
type ParamType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PARAMTYPE_CONST ParamType = 0
|
||||||
|
PARAMTYPE_STRING ParamType = 1
|
||||||
|
PARAMTYPE_OPTIONAL ParamType = 2
|
||||||
|
// bool 1/0
|
||||||
|
// list
|
||||||
|
// k/v list
|
||||||
|
// file upload (temporary path passed to binary)
|
||||||
|
// nested parse subgroup (e.g. ffmpeg filters)
|
||||||
|
// one optional to control a whole subgroup (e.g. --timeout 4)
|
||||||
|
// String validations (regexp, min-length, ...)
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputParam struct {
|
||||||
|
Description string `json:",omitempty"` // only use for editable parameters
|
||||||
|
ParamType ParamType
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandConfig struct {
|
||||||
|
Title string
|
||||||
|
Execution []InputParam // TODO allow plain strings as a shorthand for PARAMTYPE_CONST
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
ListenAddress string
|
||||||
|
AppTitle string
|
||||||
|
Commands []CommandConfig
|
||||||
|
}
|
16
html.go
Normal file
16
html.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hesc(in string) string {
|
||||||
|
return html.EscapeString(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(w http.ResponseWriter, r *http.Request, s string) {
|
||||||
|
log.Printf("[%s] %s", r.RemoteAddr, s)
|
||||||
|
http.Error(w, "Malformed request", 400)
|
||||||
|
}
|
42
tpl_Home.go
Normal file
42
tpl_Home.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Serve_Homepage(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("Content-Type", "text/html;charset=UTF-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
this.ServePartial_Header(w, "/")
|
||||||
|
|
||||||
|
for i, t := range this.cfg.Commands {
|
||||||
|
fmt.Fprint(w,
|
||||||
|
`<h3>`+hesc(t.Title)+`</h3>
|
||||||
|
<form method="POST" action="/x-new-task">
|
||||||
|
<input type="hidden" name="task_id" value="`+fmt.Sprintf("%d", i)+`">
|
||||||
|
`)
|
||||||
|
for i, param := range t.Execution {
|
||||||
|
switch param.ParamType {
|
||||||
|
case PARAMTYPE_CONST:
|
||||||
|
// not configurable parameter
|
||||||
|
case PARAMTYPE_STRING:
|
||||||
|
fmt.Fprintf(w, `<input type="text" name="param[%d]" placeholder="%s" title="%s" value="%s"><br>`,
|
||||||
|
i, hesc(param.Description), hesc(param.Description), hesc(param.Value))
|
||||||
|
case PARAMTYPE_OPTIONAL:
|
||||||
|
fmt.Fprintf(w, `<input type="hidden" name="param[%d]" value="off"><label><input type="checkbox" name="param[%d]" value="on">%s</label><br>`,
|
||||||
|
i, i, hesc(param.Description))
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown PARAMTYPE(%d)", param.ParamType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
<input type="submit" value="Run »">
|
||||||
|
</form>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ServePartial_Footer(w)
|
||||||
|
}
|
50
tpl_Style.go
Normal file
50
tpl_Style.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Serve_StyleCSS(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("Content-Type", "text/css")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
/* Global styles */
|
||||||
|
html,body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color:green;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width:100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top:1em;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding:2px;
|
||||||
|
}
|
||||||
|
tr:hover td {
|
||||||
|
background:lightyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific elements */
|
||||||
|
.task-state-finished {
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
.task-state-running {
|
||||||
|
color:darkgreen;
|
||||||
|
}
|
||||||
|
#task-output tbody {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
#task-output .stdout {
|
||||||
|
}
|
||||||
|
#task-output .stderr {
|
||||||
|
color:red;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
81
tpl_Taskinfo.go
Normal file
81
tpl_Taskinfo.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Serve_TaskInfo(w http.ResponseWriter, task_ref string) {
|
||||||
|
|
||||||
|
this.tasksMtx.RLock()
|
||||||
|
taskinfo, ok := this.tasks[task_ref]
|
||||||
|
this.tasksMtx.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Unknown task.", 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html;charset=UTF-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
this.ServePartial_Header(w, "/taskinfo/...")
|
||||||
|
|
||||||
|
fmt.Fprintf(w, `
|
||||||
|
<ul>
|
||||||
|
<li>Type: %s</li>
|
||||||
|
<li>Start time: %s</li>
|
||||||
|
`,
|
||||||
|
"???",
|
||||||
|
hesc(time.Unix(taskinfo.started, 0).Format(time.RFC822Z)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if taskinfo.Finished() {
|
||||||
|
fmt.Fprintf(w, `
|
||||||
|
<li>Completed at: %s</li>
|
||||||
|
<li>Process exit code: %d</li>
|
||||||
|
`,
|
||||||
|
hesc(time.Unix(taskinfo.stopped, 0).Format(time.RFC822Z)),
|
||||||
|
taskinfo.exitCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<table id="task-output">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`)
|
||||||
|
|
||||||
|
for _, line := range taskinfo.output {
|
||||||
|
messageClass := "stdout"
|
||||||
|
if line.isError {
|
||||||
|
messageClass = "stderr"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w,
|
||||||
|
`<tr class="%s">
|
||||||
|
<td>%s</td>
|
||||||
|
</tr>
|
||||||
|
`, messageClass, hesc(line.text),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<form method="POST" action="/x-abandon-task">
|
||||||
|
<input type="hidden" name="task_ref" value="%s">
|
||||||
|
<input type="submit" value="Cancel »">
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
hesc(task_ref),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.ServePartial_Footer(w)
|
||||||
|
}
|
61
tpl_Tasks.go
Normal file
61
tpl_Tasks.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Serve_Tasks(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("Content-Type", "text/html;charset=UTF-8")
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
this.ServePartial_Header(w, "/tasks")
|
||||||
|
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Task</th>
|
||||||
|
<th>Started</th>
|
||||||
|
<th>State</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`)
|
||||||
|
|
||||||
|
this.tasksMtx.RLock()
|
||||||
|
defer this.tasksMtx.RUnlock()
|
||||||
|
for ref, t := range this.tasks {
|
||||||
|
fmt.Fprintf(w,
|
||||||
|
`<tr>
|
||||||
|
<td><a href="/task/%s">%s</td>
|
||||||
|
<td>%s</td>
|
||||||
|
<td>
|
||||||
|
`,
|
||||||
|
hesc(ref), hesc(ref),
|
||||||
|
hesc(time.Unix(t.started, 0).Format(time.RFC822Z)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if t.Finished() {
|
||||||
|
fmt.Fprint(w, `<span class="task-state-finished">Finished</span>`)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, `<span class="task-state-running">Running</span>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form method="POST" action="/x-clear-completed-tasks">
|
||||||
|
<input type="submit" value="Clear completed tasks »">
|
||||||
|
</form>
|
||||||
|
`)
|
||||||
|
this.ServePartial_Footer(w)
|
||||||
|
}
|
41
tpt_Header.go
Normal file
41
tpt_Header.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) ServePartial_Header(w http.ResponseWriter, slug string) {
|
||||||
|
|
||||||
|
fmt.Fprint(w, `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>`+hesc(this.cfg.AppTitle)+`</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>`+hesc(this.cfg.AppTitle)+`</h2>
|
||||||
|
`)
|
||||||
|
|
||||||
|
if slug == "/" {
|
||||||
|
fmt.Fprint(w, "New task")
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, `<a href="/">New task</a>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, ` | `)
|
||||||
|
|
||||||
|
if slug == "/tasks" {
|
||||||
|
fmt.Fprint(w, "Current tasks")
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, `<a href="/tasks">Current tasks</a>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *App) ServePartial_Footer(w http.ResponseWriter) {
|
||||||
|
fmt.Fprint(w, `
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
}
|
27
twa_AbandonTask.go
Normal file
27
twa_AbandonTask.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Action_AbandonTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
fail(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskRef := r.Form.Get("task_ref")
|
||||||
|
|
||||||
|
this.tasksMtx.RLock()
|
||||||
|
task, ok := this.tasks[taskRef]
|
||||||
|
this.tasksMtx.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Invalid task specified", 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.cancel()
|
||||||
|
http.Redirect(w, r, "/task/"+taskRef, 302)
|
||||||
|
}
|
19
twa_ClearCompleted.go
Normal file
19
twa_ClearCompleted.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Action_ClearCompleted(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
this.tasksMtx.Lock()
|
||||||
|
defer this.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
for k, v := range this.tasks {
|
||||||
|
if v.Finished() {
|
||||||
|
delete(this.tasks, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/tasks", 302)
|
||||||
|
}
|
68
twa_NewTask.go
Normal file
68
twa_NewTask.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package webcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (this *App) Action_NewTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
fail(w, r, fmt.Sprintf("Bad form data: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskIdStr := r.Form.Get("task_id")
|
||||||
|
if len(taskIdStr) == 0 {
|
||||||
|
fail(w, r, "Missing task ID in request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskId, err := strconv.Atoi(taskIdStr)
|
||||||
|
if err != nil {
|
||||||
|
fail(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if taskId < 0 || taskId >= len(this.cfg.Commands) {
|
||||||
|
fail(w, r, fmt.Sprintf("Invalid task ID %d", taskId))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskInfo := this.cfg.Commands[taskId]
|
||||||
|
|
||||||
|
params := make([]string, 0, len(taskInfo.Execution))
|
||||||
|
for i, prop := range taskInfo.Execution {
|
||||||
|
switch prop.ParamType {
|
||||||
|
case PARAMTYPE_CONST:
|
||||||
|
params = append(params, prop.Value)
|
||||||
|
|
||||||
|
case PARAMTYPE_STRING:
|
||||||
|
val := r.Form.Get(fmt.Sprintf("param[%d]", i))
|
||||||
|
params = append(params, val)
|
||||||
|
|
||||||
|
case PARAMTYPE_OPTIONAL:
|
||||||
|
val := r.Form.Get(fmt.Sprintf("param[%d]", i))
|
||||||
|
if val == "on" {
|
||||||
|
params = append(params, prop.Value)
|
||||||
|
} else if val == "off" {
|
||||||
|
// nothing
|
||||||
|
} else {
|
||||||
|
fail(w, r, "Unexpected value for parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new command from supplied values
|
||||||
|
taskRef, err := this.LaunchTask(params)
|
||||||
|
if err != nil {
|
||||||
|
fail(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/task/"+taskRef, 302)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user