diff --git a/main.go b/main.go index 3b317cf..f374023 100644 --- a/main.go +++ b/main.go @@ -28,8 +28,6 @@ var ( //go:embed static staticFS embed.FS - - scriptScheduler ScriptScheduler ) func initCmdline() { @@ -65,9 +63,6 @@ func main() { os.Exit(1) } - scriptScheduler = NewScriptScheduler() - go scriptScheduler.Loop() - err = initWebserver() if err != nil { logger.Error("webserver", "error", err) diff --git a/script.go b/script.go index e8f98ab..fa24d38 100644 --- a/script.go +++ b/script.go @@ -27,7 +27,6 @@ type Hook struct { Node Node Script Script SSH string - Env map[string]string } func GetScript(scriptID int) (script Script, err error) { // {{{ @@ -162,7 +161,7 @@ func SearchScripts(search string) (scripts []Script, err error) { // {{{ return } // }}} func HookScript(nodeID, scriptID int) (err error) { // {{{ - _, err = db.Exec(`INSERT INTO hook(node_id, script_id, ssh, env) VALUES($1, $2, '')`, nodeID, scriptID) + _, err = db.Exec(`INSERT INTO hook(node_id, script_id, ssh) VALUES($1, $2, '')`, nodeID, scriptID) return } // }}} @@ -174,7 +173,6 @@ func GetHook(hookID int) (hook Hook, err error) { // {{{ SELECT h.id, h.ssh, - h.env, (SELECT to_json(node) FROM node WHERE id = h.node_id) AS node, (SELECT to_json(script) FROM script WHERE id = h.script_id) AS script FROM hook h @@ -233,7 +231,7 @@ func ScheduleHook(hookID int) (err error) { // {{{ // The node tree data is retrieved and sent as input to the script. var topNode *Node - topNode, err = GetNodeTree(hook.Node.ID, 8, true) + topNode, err = GetNodeTree(hook.Node.ID, 0, true) if err != nil { err = werr.Wrap(err) return @@ -245,17 +243,11 @@ func ScheduleHook(hookID int) (err error) { // {{{ return } - j, _ := json.Marshal(hook.Env) - _, err = db.Exec(`INSERT INTO execution(script_log_id, data, ssh, env) VALUES($1, $2, $3, $4)`, scriptLogID, nodeData, hook.SSH, j) + _, err = db.Exec(`INSERT INTO execution(script_log_id, data, ssh) VALUES($1, $2, $3)`, scriptLogID, nodeData, hook.SSH) if err != nil { err = werr.Wrap(err) return } - - go func() { - scriptScheduler.EventQueue <- "SCRIPT_SCHEDULED" - }() - return } // }}} diff --git a/script_scheduler.go b/script_scheduler.go deleted file mode 100644 index 96fc913..0000000 --- a/script_scheduler.go +++ /dev/null @@ -1,251 +0,0 @@ -package main - -import ( - // External - werr "git.gibonuddevalla.se/go/wrappederror" - - // Standard - "bytes" - "database/sql" - "encoding/json" - "fmt" - "os/exec" - "strings" - "time" -) - -const ENV_NAME = 0 -const SCRIPT_NAME = 1 - -type ScriptScheduler struct { - EventQueue chan string -} - -type ScriptExecution struct { - ID int - TimeStart sql.NullTime `db:"time_start"` - TimeEnd sql.NullTime `db:"time_end"` - Source []byte - Data []byte - SSH string - Env []byte - OutputStdout sql.NullString `db:"output_stdout"` - OutputStderr sql.NullString `db:"output_stderr"` - ExitCode sql.NullInt16 -} - -func NewScriptScheduler() (sched ScriptScheduler) { - sched.EventQueue = make(chan string, 64) - return -} - -func (self ScriptScheduler) Loop() { // {{{ - - // Lets check for somehow missed executions every minute. - // An event SHOULD be received for each new created, but let's be sure. - tick := time.NewTicker(time.Second * 60) - - var event string - for { - select { - case <-tick.C: - self.HandleNextExecution() - - case event = <-self.EventQueue: - if event == "SCRIPT_SCHEDULED" { - self.HandleNextExecution() - } - } - } -} // }}} -func (self ScriptScheduler) HandleNextExecution() { // {{{ - se, err := self.GetNextExecution() - if err != nil { - logger.Error("script_scheduler", "error", err) - return - } - - if se.ID == 0 { - return - } - - // Setting the time_start value on the database row makes sure it doesn't get handled again. - se.TimeStart.Time = time.Now() - se.TimeStart.Valid = true - se.Update() - - logger.Info("script_scheduler", "op", "execute", "id", se.ID) - - var fnames []string - fnames, err = se.UploadScript() - if err != nil { - err = werr.Wrap(err) - logger.Error("script_execution", "op", "upload_script", "id", se.ID, "error", err) - return - } - - err = se.UploadEnv(fnames[ENV_NAME], fnames[SCRIPT_NAME]) - if err != nil { - err = werr.Wrap(err) - logger.Error("script_execution", "op", "upload_env", "id", se.ID, "error", err) - return - } - - err = se.RunScript(fnames[ENV_NAME]) - if err != nil { - err = werr.Wrap(err) - logger.Error("script_execution", "op", "run_script", "id", se.ID, "error", err) - return - } - - se.SSHCommand([]byte{}, false, fmt.Sprintf("rm %s %s", fnames[ENV_NAME], fnames[SCRIPT_NAME])) - - logger.Info("script_scheduler", "op", "handled", "script", fnames[SCRIPT_NAME]) -} // }}} -func (self ScriptScheduler) GetNextExecution() (e ScriptExecution, err error) { // {{{ - row := db.QueryRowx(` - SELECT - e.id, - time_start, - time_end, - data, - ssh, - env, - output_stdout, - output_stderr, - exitcode, - sl.source - FROM execution e - INNER JOIN script_log sl ON e.script_log_id = sl.id - WHERE - time_start IS NULL - ORDER BY - id ASC - `) - err = row.StructScan(&e) - - // Returned ScriptExecution is having an ID of 0 if none was returned - if err == sql.ErrNoRows { - err = nil - return - } - - if err != nil { - err = werr.Wrap(err) - return - } - - return -} // }}} - -func (se *ScriptExecution) Update() (err error) { // {{{ - _, err = db.Exec(` - UPDATE public.execution - SET - time_start = $2, - time_end = $3, - output_stdout = $4, - output_stderr = $5, - exitcode = $6 - - WHERE - id=$1`, - se.ID, - se.TimeStart, - se.TimeEnd, - se.OutputStdout, - se.OutputStderr, - se.ExitCode, - ) - if err != nil { - err = werr.Wrap(err) - logger.Error("script_execution", "op", "execute", "id", se.ID, "error", err) - return - } - return -} // }}} -func (se *ScriptExecution) SSHCommand(stdin []byte, log bool, args ...string) (stdoutString string, err error) { // {{{ - params := []string{se.SSH} - params = append(params, args...) - cmd := exec.Command("ssh", params...) - - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - cmd.Stdin = bytes.NewReader(stdin) - cmd.Stdout = stdout - cmd.Stderr = stderr - err = cmd.Run() - - // A cleanup command is run after the script. This shouldn't overwrite the output from the actual script. - if log { - se.OutputStdout.String = stdout.String() - se.OutputStderr.String = stderr.String() - se.ExitCode.Int16 = int16(cmd.ProcessState.ExitCode()) - se.OutputStdout.Valid = true - se.OutputStderr.Valid = true - se.ExitCode.Valid = true - } - - se.TimeEnd.Time = time.Now() - se.TimeEnd.Valid = true - se.Update() - - if err != nil { - err = werr.Wrap(err) - return - } - - return stdout.String(), nil -} // }}} -func (se *ScriptExecution) UploadScript() (fnames []string, err error) { // {{{ - var filenames string - filenames, err = se.SSHCommand( - se.Source, - true, - `sh -c 'RUNENV=$(mktemp -t datagraph.XXXXXX) && SCRIPT=$(mktemp -t datagraph.XXXXXX) && touch $RUNENV $SCRIPT && chmod 700 $RUNENV $SCRIPT && cat >$SCRIPT && echo $RUNENV $SCRIPT'`, - ) - if err != nil { - err = werr.Wrap(err) - } - - fnames = strings.Split(strings.TrimSpace(filenames), " ") - - if len(fnames) != 2 { - err = werr.New("Invalid temp filename count: %d", len(fnames)) - return - } - - return fnames[:2], nil -} // }}} -func (se *ScriptExecution) UploadEnv(envFname, scriptFname string) (err error) { // {{{ - env := make(map[string]string) - err = json.Unmarshal(se.Env, &env) - if err != nil { - err = werr.Wrap(err) - return - } - - var script = "#!/bin/sh\n\n" - for key, val := range env { - script = script + fmt.Sprintf("export %s=\"%s\"\n", key, strings.ReplaceAll(val, `"`, `\"`)) - } - script = script + "\n" + scriptFname + "\n" - - _, err = se.SSHCommand( - []byte(script), - true, - fmt.Sprintf(`sh -c 'cat >%s'`, envFname), - ) - if err != nil { - err = werr.Wrap(err) - } - - return -} // }}} -func (se *ScriptExecution) RunScript(fname string) (err error) { // {{{ - _, err = se.SSHCommand(se.Data, true, fname) - if err != nil { - err = werr.Wrap(err) - } - return -} // }}} diff --git a/sql/0014.sql b/sql/0014.sql deleted file mode 100644 index 43af267..0000000 --- a/sql/0014.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE INDEX execution_time_start_idx ON public.execution (time_start); diff --git a/sql/0015.sql b/sql/0015.sql deleted file mode 100644 index dedf69f..0000000 --- a/sql/0015.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.hook ADD env jsonb DEFAULT '{}' NOT NULL; diff --git a/static/css/main.css b/static/css/main.css index ed55cbb..3c45bcf 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -62,14 +62,13 @@ button { padding: 16px 32px; } #menu .item { - font-size: 1.1em; cursor: pointer; } #menu .item.selected { font-weight: bold; } #logo img { - height: 64px; + height: 96px; margin-right: 32px; } #nodes { @@ -246,8 +245,6 @@ select:focus { grid-template-columns: 24px 1fr; grid-gap: 8px; align-items: center; - background-color: #f0f0f0; - padding: 16px; } #connected-nodes .connected-nodes .type-group .type-name { font-weight: bold; @@ -262,10 +259,10 @@ select:focus { height: 24px; } #script-hooks .scripts-grid { - display: flex; - align-items: start; - flex-flow: row wrap; - gap: 32px; + display: grid; + grid-template-columns: repeat(4, min-content); + align-items: center; + grid-gap: 2px 0px; } #script-hooks .scripts-grid .header { font-weight: bold; @@ -275,19 +272,12 @@ select:focus { white-space: nowrap; } #script-hooks .scripts-grid .script-group { - display: grid; - grid-template-columns: repeat(4, min-content); - align-items: center; - grid-gap: 2px 0px; - padding: 16px; - background-color: #f0f0f0; -} -#script-hooks .scripts-grid .script-group-title { grid-column: 1 / -1; font-weight: bold; + margin-top: 8px; } #script-hooks .scripts-grid .script-unhook img, -#script-hooks .scripts-grid .script-schedule img { +#script-hooks .scripts-grid .script-run img { display: block; height: 24px; cursor: pointer; @@ -298,10 +288,6 @@ select:focus { } #script-hooks .scripts-grid .script-ssh { cursor: pointer; - color: #555; -} -#script-hooks .scripts-grid .script-schedule.disabled { - filter: invert(50%); } #script-hooks > .label { display: grid; diff --git a/static/js/app.mjs b/static/js/app.mjs index 9eaf8a7..d6d5294 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -1048,29 +1048,19 @@ class ScriptHooks extends Component { renderHooks() {// {{{ this.scriptGrid.innerHTML = '' - let curGroupName = null - let group = document.createElement('div') - group.classList.add('script-group') - + let prevGroup = null for (const hook of this.hooks) { - if (hook.Script.Group !== curGroupName) { + if (hook.Script.Group !== prevGroup) { const g = document.createElement('div') - g.classList.add('script-group-title') + g.classList.add('script-group') g.innerText = hook.Script.Group - - group = document.createElement('div') - group.classList.add('script-group') - group.append(g) - this.scriptGrid.append(group) - curGroupName = hook.Script.Group + this.scriptGrid.append(g) + prevGroup = hook.Script.Group } const h = new ScriptHook(hook, this) - group.append(h.render()) + this.scriptGrid.append(h.render()) } - - if (group.children.length > 1) - this.scriptGrid.append(group) }// }}} hookScript(script) {// {{{ _app.query(`/nodes/hook`, { @@ -1087,9 +1077,7 @@ class ScriptHook extends Component { super() this.hook = hook this.parentList = parentList - this.elementSSH = null - this.elementSchedule = null - this.scheduleDisable = false + this.element_ssh = null }// }}} renderComponent() {// {{{ const tmpl = document.createElement('template') @@ -1097,15 +1085,14 @@ class ScriptHook extends Component {
${this.hook.Script.Name}
-
+
` - this.elementSchedule = tmpl.content.querySelector('.script-schedule') - this.elementSSH = tmpl.content.querySelector('.script-ssh') - this.elementSSH.innerText = `[ ${this.hook.SSH} ]` + this.element_ssh = tmpl.content.querySelector('.script-ssh') + this.element_ssh.innerText = this.hook.SSH tmpl.content.querySelector('.script-ssh').addEventListener('click', () => this.update()) tmpl.content.querySelector('.script-unhook').addEventListener('click', () => this.delete()) - tmpl.content.querySelector('.script-schedule').addEventListener('click', () => this.run()) + tmpl.content.querySelector('.script-run').addEventListener('click', () => this.run()) return tmpl.content }// }}} @@ -1134,7 +1121,7 @@ class ScriptHook extends Component { return } this.hook.SSH = ssh - this.elementSSH.innerText = this.hook.SSH + this.element_ssh.innerText = this.hook.SSH }) .catch(err => showError(err)) }// }}} @@ -1154,22 +1141,8 @@ class ScriptHook extends Component { .catch(err => showError(err)) }// }}} run() {// {{{ - if (this.scheduleDisable) - return - - this.scheduleDisable = true - this.elementSchedule.classList.add('disabled') - const start = Date.now() - _app.query(`/hooks/schedule/${this.hook.ID}`) .catch(err => showError(err)) - .finally(() => { - const duration = Date.now() - start - setTimeout(() => { - this.scheduleDisable = false - this.elementSchedule.classList.remove('disabled') - }, 250 - duration) - }) }// }}} } diff --git a/static/less/main.less b/static/less/main.less index c9a29bc..8254d66 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -79,7 +79,6 @@ button { } .item { - font-size: 1.1em; cursor: pointer; &.selected { @@ -90,7 +89,7 @@ button { #logo { img { - height: 64px; + height: 96px; margin-right: 32px; } } @@ -325,8 +324,6 @@ select:focus { grid-template-columns: 24px 1fr; grid-gap: 8px; align-items: center; - background-color: #f0f0f0; - padding: 16px; .type-name { font-weight: bold; @@ -349,35 +346,27 @@ select:focus { #script-hooks { .scripts-grid { - display: flex; - align-items: start; - flex-flow: row wrap; - gap: 32px; - .header { font-weight: bold; margin-right: 8px; } + display: grid; + grid-template-columns: repeat(4, min-content); + align-items: center; + grid-gap: 2px 0px; + div { white-space: nowrap; } .script-group { - display: grid; - grid-template-columns: repeat(4, min-content); - align-items: center; - grid-gap: 2px 0px; - padding: 16px; - background-color: #f0f0f0; - } - - .script-group-title { grid-column: 1 / -1; font-weight: bold; + margin-top: 8px; } - .script-unhook, .script-schedule { + .script-unhook, .script-run { img { display: block; height: 24px; @@ -391,11 +380,6 @@ select:focus { .script-ssh { cursor: pointer; - color: #555; - } - - .script-schedule.disabled { - filter: invert(50%); } }