Bladeren bron

feature: log-popup

jamesread 3 jaren geleden
bovenliggende
commit
89f08ae6c7
6 gewijzigde bestanden met toevoegingen van 128 en 57 verwijderingen
  1. 21 4
      OliveTin.proto
  2. 4 1
      internal/acl/acl.go
  3. 1 0
      internal/config/config.go
  4. 59 39
      internal/executor/executor.go
  5. 38 9
      internal/grpcapi/grpcApi.go
  6. 5 4
      internal/grpcapi/grpcApiActions.go

+ 21 - 4
OliveTin.proto

@@ -9,13 +9,13 @@ message Action {
 	string title = 2;
 	string icon = 3;
 	bool canExec = 4;
-
 	repeated ActionArgument arguments = 5;
+	bool popupOnStart = 6;
 }
 
 message ActionArgument {
 	string name = 1;
-	string title = 2; 
+	string title = 2;
 	string type = 3;
 	string defaultValue = 4;
 
@@ -55,13 +55,13 @@ message StartActionArgument {
 }
 
 message StartActionResponse {
-	LogEntry logEntry = 1;
+	string executionUuid = 2;
 }
 
 message GetLogsRequest{};
 
 message LogEntry {
-	string datetime = 1;
+	string datetimeStarted = 1;
 	string actionTitle = 2;
 	string stdout = 3;
 	string stderr = 4;
@@ -71,6 +71,8 @@ message LogEntry {
 	string userClass = 8;
 	string actionIcon = 9;
 	repeated string tags = 10;
+	string executionUuid = 11;
+	string datetimeFinished = 12;
 }
 
 message GetLogsResponse {
@@ -87,6 +89,14 @@ message ValidateArgumentTypeResponse {
 	string description = 2;
 }
 
+message WatchExecutionRequest {
+	string executionUuid = 1;
+}
+
+message WatchExecutionUpdate {
+	string update = 1;
+}
+
 message WhoAmIRequest {}
 
 message WhoAmIResponse {
@@ -113,6 +123,13 @@ service OliveTinApi {
 		};
 	}
 
+	rpc WatchExecution(WatchExecutionRequest) returns (stream WatchExecutionUpdate) {
+		option (google.api.http) = {
+			post: "/api/WatchExecution"
+			body: "*"
+		};
+	}
+
 	rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
 		option (google.api.http) = {
 			get: "/api/GetLogs"

+ 4 - 1
internal/acl/acl.go

@@ -75,7 +75,10 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
 func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
 	md, ok := metadata.FromIncomingContext(ctx)
 
-	ret := &AuthenticatedUser{}
+	ret := &AuthenticatedUser{
+		Username:  "guest",
+		Usergroup: "guest",
+	}
 
 	if ok {
 		ret.Username = getMetdataKeyOrEmpty(md, "username")

+ 1 - 0
internal/config/config.go

@@ -13,6 +13,7 @@ type Action struct {
 	ExecOnStartup bool
 	ExecOnCron    []string
 	Arguments     []ActionArgument
+	PopupOnStart  bool
 }
 
 // ActionArgument objects appear on Actions.

+ 59 - 39
internal/executor/executor.go

@@ -4,10 +4,12 @@ import (
 	pb "github.com/OliveTin/OliveTin/gen/grpc"
 	acl "github.com/OliveTin/OliveTin/internal/acl"
 	config "github.com/OliveTin/OliveTin/internal/config"
+	"github.com/google/uuid"
 	log "github.com/sirupsen/logrus"
 
 	"bytes"
 	"context"
+	"io"
 	"os/exec"
 	"runtime"
 	"time"
@@ -24,18 +26,22 @@ type ExecutionRequest struct {
 	AuthenticatedUser  *acl.AuthenticatedUser
 	logEntry           *InternalLogEntry
 	finalParsedCommand string
+	uuid               string
 }
 
 // InternalLogEntry objects are created by an Executor, and represent the final
 // state of execution (even if the command is not executed). It's designed to be
 // easily serializable.
 type InternalLogEntry struct {
-	Datetime string
-	Stdout   string
-	Stderr   string
-	TimedOut bool
-	ExitCode int32
-	Tags     []string
+	DatetimeStarted  string
+	DatetimeFinished string
+	Stdout           string
+	Stderr           string
+	StdoutBuffer     io.ReadCloser
+	StderrBuffer     io.ReadCloser
+	TimedOut         bool
+	ExitCode         int32
+	Tags             []string
 
 	/*
 		The following two properties are obviously on Action normally, but it's useful
@@ -44,6 +50,9 @@ type InternalLogEntry struct {
 	*/
 	ActionTitle string
 	ActionIcon  string
+
+	ExecutionStarted   bool
+	ExecutionCompleted bool
 }
 
 type executorStepFunc func(*ExecutionRequest) bool
@@ -51,46 +60,17 @@ type executorStepFunc func(*ExecutionRequest) bool
 // Executor represents a helper class for executing commands. It's main method
 // is ExecRequest
 type Executor struct {
-	Logs []InternalLogEntry
+	Logs map[string]*InternalLogEntry
 
 	chainOfCommand []executorStepFunc
 }
 
-// ExecRequest processes an ExecutionRequest
-func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
-	req.logEntry = &InternalLogEntry{
-		Datetime:    time.Now().Format("2006-01-02 15:04:05"),
-		ActionTitle: req.ActionName,
-		Stdout:      "",
-		Stderr:      "",
-		ExitCode:    -1337, // If an Action is not actually executed, this is the default exit code.
-	}
-
-	for _, step := range e.chainOfCommand {
-		if !step(req) {
-			break
-		}
-	}
-
-	e.Logs = append(e.Logs, *req.logEntry)
-
-	return &pb.StartActionResponse{
-		LogEntry: &pb.LogEntry{
-			ActionTitle: req.logEntry.ActionTitle,
-			ActionIcon:  req.logEntry.ActionIcon,
-			Datetime:    req.logEntry.Datetime,
-			Stderr:      req.logEntry.Stderr,
-			Stdout:      req.logEntry.Stdout,
-			TimedOut:    req.logEntry.TimedOut,
-			ExitCode:    req.logEntry.ExitCode,
-		},
-	}
-}
-
 // DefaultExecutor returns an Executor, with a sensible "chain of command" for
 // executing actions.
 func DefaultExecutor() *Executor {
 	e := Executor{}
+	e.Logs = make(map[string]*InternalLogEntry)
+
 	e.chainOfCommand = []executorStepFunc{
 		stepFindAction,
 		stepACLCheck,
@@ -103,6 +83,36 @@ func DefaultExecutor() *Executor {
 	return &e
 }
 
+// ExecRequest processes an ExecutionRequest
+func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
+	req.uuid = uuid.New().String()
+	req.logEntry = &InternalLogEntry{
+		DatetimeStarted:    time.Now().Format("2006-01-02 15:04:05"),
+		ActionTitle:        req.ActionName,
+		Stdout:             "",
+		Stderr:             "",
+		ExitCode:           -1337, // If an Action is not actually executed, this is the default exit code.
+		ExecutionStarted:   false,
+		ExecutionCompleted: false,
+	}
+
+	e.Logs[req.uuid] = req.logEntry
+
+	go e.execChain(req)
+
+	return &pb.StartActionResponse{
+		ExecutionUuid: req.uuid,
+	}
+}
+
+func (e *Executor) execChain(req *ExecutionRequest) {
+	for _, step := range e.chainOfCommand {
+		if !step(req) {
+			break
+		}
+	}
+}
+
 func stepFindAction(req *ExecutionRequest) bool {
 	actualAction := req.Cfg.FindAction(req.ActionName)
 
@@ -181,9 +191,19 @@ func stepExec(req *ExecutionRequest) bool {
 	cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
 	cmd.Stdout = &stdout
 	cmd.Stderr = &stderr
+	req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
+	req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
+
+	req.logEntry.ExecutionStarted = true
+
+	runerr := cmd.Start()
+
+	cmd.Wait()
 
-	runerr := cmd.Run()
+	//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()

+ 38 - 9
internal/grpcapi/grpcApi.go

@@ -6,6 +6,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
 
+	"io"
 	"net"
 
 	acl "github.com/OliveTin/OliveTin/internal/acl"
@@ -43,6 +44,32 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
 	return api.executor.ExecRequest(&execReq), nil
 }
 
+func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
+	log.Infof("Watch")
+
+	if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
+		log.Errorf("Execution not found: %v", req.ExecutionUuid)
+
+		return nil
+	} else {
+		if logEntry.ExecutionStarted {
+			for !logEntry.ExecutionCompleted {
+				tmp := make([]byte, 256)
+
+				red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
+
+				log.Infof("%v %v", red, err)
+
+				srv.Send(&pb.WatchExecutionUpdate{
+					Update: string(tmp),
+				})
+			}
+		}
+
+		return nil
+	}
+}
+
 func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
 	user := acl.UserFromContext(ctx, cfg)
 
@@ -62,16 +89,18 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
 
 	// TODO Limit to 10 entries or something to prevent browser lag.
 
-	for _, logEntry := range api.executor.Logs {
+	for uuid, logEntry := range api.executor.Logs {
 		ret.Logs = append(ret.Logs, &pb.LogEntry{
-			ActionTitle: logEntry.ActionTitle,
-			ActionIcon:  logEntry.ActionIcon,
-			Datetime:    logEntry.Datetime,
-			Stdout:      logEntry.Stdout,
-			Stderr:      logEntry.Stderr,
-			TimedOut:    logEntry.TimedOut,
-			ExitCode:    logEntry.ExitCode,
-			Tags:        logEntry.Tags,
+			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,
 		})
 	}
 

+ 5 - 4
internal/grpcapi/grpcApiActions.go

@@ -25,10 +25,11 @@ func actionsCfgToPb(cfgActions []config.Action, user *acl.AuthenticatedUser) *pb
 
 func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action {
 	btn := pb.Action{
-		Id:      fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
-		Title:   action.Title,
-		Icon:    action.Icon,
-		CanExec: acl.IsAllowedExec(cfg, user, &action),
+		Id:           fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
+		Title:        action.Title,
+		Icon:         action.Icon,
+		CanExec:      acl.IsAllowedExec(cfg, user, &action),
+		PopupOnStart: action.PopupOnStart,
 	}
 
 	for _, cfgArg := range action.Arguments {