Răsfoiți Sursa

feature: #146 Support for maxConcurrent (#156)

James Read 2 ani în urmă
părinte
comite
e8cb661938

+ 1 - 0
.gitignore

@@ -9,3 +9,4 @@ reports
 releases/
 dist/
 installation-id.txt
+tmp/

+ 3 - 0
OliveTin.proto

@@ -76,6 +76,9 @@ message LogEntry {
 	string execution_uuid = 11;
 	string datetime_finished = 12;
 	string uuid = 13;
+	bool execution_started = 14;
+	bool execution_finished = 15;
+	bool blocked = 16;
 }
 
 message GetLogsResponse {

+ 5 - 0
config.yaml

@@ -13,13 +13,18 @@ actions:
   # This will run a simple script that you create.
 - title: Run backup script
   shell: /opt/backupScript.sh
+  maxConcurrent: 1
   icon: backup
 
+- title: date
+  shell: date
+
   # This will send 1 ping (-c 1)
   # Docs: https://docs.olivetin.app/action-ping.html
 - title: Ping host
   shell: ping {{ host }} -c {{ count }}
   icon: ping
+  timeout: 100
   arguments:
     - name: host
       title: host

+ 1 - 0
internal/config/config.go

@@ -12,6 +12,7 @@ type Action struct {
 	Acls          []string
 	ExecOnStartup bool
 	ExecOnCron    []string
+	MaxConcurrent int
 	Arguments     []ActionArgument
 	PopupOnStart  bool
 }

+ 4 - 0
internal/config/sanitize.go

@@ -30,6 +30,10 @@ func (action *Action) sanitize() {
 
 	action.Icon = lookupHTMLIcon(action.Icon)
 
+	if action.MaxConcurrent < 1 {
+		action.MaxConcurrent = 1
+	}
+
 	for idx := range action.Arguments {
 		action.Arguments[idx].sanitize()
 	}

+ 58 - 24
internal/executor/executor.go

@@ -8,6 +8,7 @@ import (
 
 	"bytes"
 	"context"
+	"fmt"
 	"io"
 	"os/exec"
 	"runtime"
@@ -43,17 +44,18 @@ type ExecutionRequest struct {
 // state of execution (even if the command is not executed). It's designed to be
 // easily serializable.
 type InternalLogEntry struct {
-	DatetimeStarted    string
-	DatetimeFinished   string
-	Stdout             string
-	Stderr             string
-	StdoutBuffer       io.ReadCloser
-	StderrBuffer       io.ReadCloser
-	TimedOut           bool
-	ExitCode           int32
-	Tags               []string
-	ExecutionStarted   bool
-	ExecutionCompleted bool
+	DatetimeStarted   string
+	DatetimeFinished  string
+	Stdout            string
+	Stderr            string
+	StdoutBuffer      io.ReadCloser
+	StderrBuffer      io.ReadCloser
+	TimedOut          bool
+	Blocked           bool
+	ExitCode          int32
+	Tags              []string
+	ExecutionStarted  bool
+	ExecutionFinished bool
 
 	/*
 		The following 3 properties are obviously on Action normally, but it's useful
@@ -76,11 +78,11 @@ func DefaultExecutor() *Executor {
 	e.chainOfCommand = []executorStepFunc{
 		stepLogRequested,
 		stepFindAction,
+		stepConcurrencyCheck,
 		stepACLCheck,
 		stepParseArgs,
 		stepLogStart,
 		stepExec,
-		stepNotifyListeners,
 		stepLogFinish,
 	}
 
@@ -103,14 +105,14 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
 	// duplicate UUIDs (or just random strings), but this is the only way.
 	req.executor = e
 	req.logEntry = &InternalLogEntry{
-		DatetimeStarted:    time.Now().Format("2006-01-02 15:04:05"),
-		ActionTitle:        req.ActionName,
-		UUID:               req.UUID,
-		Stdout:             "",
-		Stderr:             "",
-		ExitCode:           -1337, // If an Action is not actually executed, this is the default exit code.
-		ExecutionStarted:   false,
-		ExecutionCompleted: false,
+		DatetimeStarted:   time.Now().Format("2006-01-02 15:04:05"),
+		ActionTitle:       req.ActionName,
+		UUID:              req.UUID,
+		Stdout:            "",
+		Stderr:            "",
+		ExitCode:          -1337, // If an Action is not actually executed, this is the default exit code.
+		ExecutionStarted:  false,
+		ExecutionFinished: false,
 	}
 
 	e.Logs[req.UUID] = req.logEntry
@@ -132,6 +134,41 @@ func (e *Executor) execChain(req *ExecutionRequest) {
 			break
 		}
 	}
+
+	req.logEntry.ExecutionFinished = true
+
+	// This isn't a step, because we want to notify all listeners, irrespective
+	// of how many steps were actually executed.
+	notifyListeners(req)
+}
+
+func getConcurrentCount(req *ExecutionRequest) int {
+	concurrentCount := 0
+
+	for _, log := range req.executor.Logs {
+		if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
+			concurrentCount += 1
+		}
+	}
+
+	return concurrentCount
+}
+
+func stepConcurrencyCheck(req *ExecutionRequest) bool {
+	concurrentCount := getConcurrentCount(req)
+
+	// Note that the current execution is counted int the logs, so when checking we +1
+	if concurrentCount >= (req.action.MaxConcurrent + 1) {
+		msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.action.MaxConcurrent)
+
+		log.Warnf(msg)
+
+		req.logEntry.Stdout = msg
+		req.logEntry.Blocked = true
+		return false
+	}
+
+	return true
 }
 
 func stepFindAction(req *ExecutionRequest) bool {
@@ -202,12 +239,10 @@ func stepLogFinish(req *ExecutionRequest) bool {
 	return true
 }
 
-func stepNotifyListeners(req *ExecutionRequest) bool {
+func notifyListeners(req *ExecutionRequest) {
 	for _, listener := range req.executor.listeners {
 		listener.OnExecutionFinished(req.logEntry)
 	}
-
-	return true
 }
 
 func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
@@ -240,7 +275,6 @@ func stepExec(req *ExecutionRequest) bool {
 	// req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
 	// req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
 
-	req.logEntry.ExecutionCompleted = true
 	req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
 	req.logEntry.Stdout = stdout.String()
 	req.logEntry.Stderr = stderr.String()

+ 27 - 21
internal/grpcapi/grpcApi.go

@@ -55,16 +55,19 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
 	}
 
 	res.LogEntry = &pb.LogEntry{
-		ActionTitle:      logEntry.ActionTitle,
-		ActionIcon:       logEntry.ActionIcon,
-		DatetimeStarted:  logEntry.DatetimeStarted,
-		DatetimeFinished: logEntry.DatetimeFinished,
-		Stdout:           logEntry.Stdout,
-		Stderr:           logEntry.Stderr,
-		TimedOut:         logEntry.TimedOut,
-		ExitCode:         logEntry.ExitCode,
-		Tags:             logEntry.Tags,
-		ExecutionUuid:    logEntry.UUID,
+		ActionTitle:       logEntry.ActionTitle,
+		ActionIcon:        logEntry.ActionIcon,
+		DatetimeStarted:   logEntry.DatetimeStarted,
+		DatetimeFinished:  logEntry.DatetimeFinished,
+		Stdout:            logEntry.Stdout,
+		Stderr:            logEntry.Stderr,
+		TimedOut:          logEntry.TimedOut,
+		Blocked:           logEntry.Blocked,
+		ExitCode:          logEntry.ExitCode,
+		Tags:              logEntry.Tags,
+		ExecutionUuid:     logEntry.UUID,
+		ExecutionStarted:  logEntry.ExecutionStarted,
+		ExecutionFinished: logEntry.ExecutionFinished,
 	}
 
 	return res, nil
@@ -119,16 +122,19 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
 
 	for uuid, logEntry := range api.executor.Logs {
 		ret.Logs = append(ret.Logs, &pb.LogEntry{
-			ActionTitle:      logEntry.ActionTitle,
-			ActionIcon:       logEntry.ActionIcon,
-			DatetimeStarted:  logEntry.DatetimeStarted,
-			DatetimeFinished: logEntry.DatetimeFinished,
-			Stdout:           logEntry.Stdout,
-			Stderr:           logEntry.Stderr,
-			TimedOut:         logEntry.TimedOut,
-			ExitCode:         logEntry.ExitCode,
-			Tags:             logEntry.Tags,
-			ExecutionUuid:    uuid,
+			ActionTitle:       logEntry.ActionTitle,
+			ActionIcon:        logEntry.ActionIcon,
+			DatetimeStarted:   logEntry.DatetimeStarted,
+			DatetimeFinished:  logEntry.DatetimeFinished,
+			Stdout:            logEntry.Stdout,
+			Stderr:            logEntry.Stderr,
+			TimedOut:          logEntry.TimedOut,
+			Blocked:           logEntry.Blocked,
+			ExitCode:          logEntry.ExitCode,
+			Tags:              logEntry.Tags,
+			ExecutionUuid:     uuid,
+			ExecutionStarted:  logEntry.ExecutionStarted,
+			ExecutionFinished: logEntry.ExecutionFinished,
 		})
 	}
 
@@ -136,7 +142,7 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
 		return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
 	}
 
-	sort.Slice(ret.Logs, sorter);
+	sort.Slice(ret.Logs, sorter)
 
 	return ret, nil
 }

+ 13 - 10
internal/websocket/websocket.go

@@ -37,16 +37,19 @@ func (WebsocketExecutionListener) OnExecutionStarted(title string) {
 
 func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
 	le := &pb.LogEntry{
-		ActionTitle:      logEntry.ActionTitle,
-		ActionIcon:       logEntry.ActionIcon,
-		DatetimeStarted:  logEntry.DatetimeStarted,
-		DatetimeFinished: logEntry.DatetimeFinished,
-		Stdout:           logEntry.Stdout,
-		Stderr:           logEntry.Stderr,
-		TimedOut:         logEntry.TimedOut,
-		ExitCode:         logEntry.ExitCode,
-		Tags:             logEntry.Tags,
-		Uuid:             logEntry.UUID,
+		ActionTitle:       logEntry.ActionTitle,
+		ActionIcon:        logEntry.ActionIcon,
+		DatetimeStarted:   logEntry.DatetimeStarted,
+		DatetimeFinished:  logEntry.DatetimeFinished,
+		Stdout:            logEntry.Stdout,
+		Stderr:            logEntry.Stderr,
+		TimedOut:          logEntry.TimedOut,
+		Blocked:           logEntry.Blocked,
+		ExitCode:          logEntry.ExitCode,
+		Tags:              logEntry.Tags,
+		Uuid:              logEntry.UUID,
+		ExecutionStarted:  logEntry.ExecutionStarted,
+		ExecutionFinished: logEntry.ExecutionFinished,
 	}
 
 	broadcast("ExecutionFinished", le)

+ 5 - 1
webui/index.html

@@ -83,12 +83,16 @@
 				</h2>
 			</div>
 			<p>
-				<strong>Started: </strong><span class = "datetimeStarted">unknown</span>
+				<strong>Started: </strong><span class = "datetimeStarted">unknown</span>.
 				<strong>Finished: </strong><span class = "datetimeFinished">unknown</span>
 			</p>
 			<p>
 				<strong>Exit Code: </strong><span class = "exitCode">unknown</span>
 			</p>
+			<p>
+				<strong>Status: </strong><span class = "status">unknown</span>
+			</p>
+
 
 			<details>
 				<summary>stdout</summary>

+ 2 - 3
webui/js/ExecutionButton.js

@@ -51,8 +51,8 @@ class ExecutionButton extends window.HTMLElement {
   onFinished (LogEntry) {
     if (LogEntry.timedOut) {
       this.onActionResult('action-timeout', 'Timed out')
-    } else if (LogEntry.exitCode === -1337) {
-      this.onActionError('Error')
+    } else if (LogEntry.blocked) {
+      this.onActionResult('action-blocked', 'Blocked!')
     } else if (LogEntry.exitCode !== 0) {
       this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
     } else {
@@ -61,7 +61,6 @@ class ExecutionButton extends window.HTMLElement {
   }
 
   onActionResult (cssClass, temporaryStatusMessage) {
-    this.btn.disabled = false
     this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
     this.updateDom()
     this.btn.classList.add(cssClass)

+ 14 - 6
webui/js/ExecutionDialog.js

@@ -14,6 +14,7 @@ export class ExecutionDialog {
     this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
     this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
     this.domExitCode = this.dlg.querySelector('.exitCode')
+    this.domStatus = this.dlg.querySelector('.status')
   }
 
   show () {
@@ -23,17 +24,24 @@ export class ExecutionDialog {
   renderResult (res) {
     this.executionUuid = res.logEntry.executionUuid
 
-    if (res.logEntry.datetimeFinished === '') {
-      this.domExitCode.innerText = 'Still running...'
-      this.domDatetimeFinished.innerText = 'Still running...'
-    } else {
+    if (res.logEntry.executionFinished) {
+      this.domStatus.innerText = 'Completed'
+      this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
+
+      if (res.logEntry.blocked) {
+        this.domStatus.innerText = 'Blocked'
+      }
+
       if (res.logEntry.timedOut) {
         this.domExitCode.innerText = 'Timed out'
+        this.domStatus.innerText = 'Timed out'
       } else {
         this.domExitCode.innerText = res.logEntry.exitCode
       }
-
-      this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
+    } else {
+      this.domDatetimeFinished.innerText = 'Still running...'
+      this.domExitCode.innerText = 'Still running...'
+      this.domStatus.innerText = 'Still running...'
     }
 
     this.domIcon.innerHTML = res.logEntry.actionIcon

+ 8 - 0
webui/style.css

@@ -270,6 +270,14 @@ button.active-section {
   20% { background-color: cyan; }
 }
 
+.action-blocked {
+  animation: kf-action-blocked 1s;
+}
+
+@keyframes kf-action-blocked {
+  20% { background-color: purple; }
+}
+
 footer,
 footer a {
   color: black;