diff --git a/OliveTin.proto b/OliveTin.proto index 38fa1ab..49e0946 100644 --- a/OliveTin.proto +++ b/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" diff --git a/internal/acl/acl.go b/internal/acl/acl.go index 5640fa2..a91e399 100644 --- a/internal/acl/acl.go +++ b/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") diff --git a/internal/config/config.go b/internal/config/config.go index a2707d5..f69a653 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,6 +13,7 @@ type Action struct { ExecOnStartup bool ExecOnCron []string Arguments []ActionArgument + PopupOnStart bool } // ActionArgument objects appear on Actions. diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 92a0ce3..92e4847 100644 --- a/internal/executor/executor.go +++ b/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() - runerr := cmd.Run() + req.logEntry.ExecutionStarted = true + runerr := cmd.Start() + + cmd.Wait() + + //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() diff --git a/internal/grpcapi/grpcApi.go b/internal/grpcapi/grpcApi.go index d6f1353..65a5f6f 100644 --- a/internal/grpcapi/grpcApi.go +++ b/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, }) } diff --git a/internal/grpcapi/grpcApiActions.go b/internal/grpcapi/grpcApiActions.go index e70a75f..5fb1f4b 100644 --- a/internal/grpcapi/grpcApiActions.go +++ b/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 {