command_status.go
1 // Copyright 2025 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package runtime 16 17 import ( 18 "fmt" 19 "io" 20 "os" 21 "time" 22 ) 23 24 // CommandStatus describes the lifecycle state of a command. 25 type CommandStatus struct { 26 Session string `json:"session"` 27 Running bool `json:"running"` 28 ExitCode *int `json:"exit_code,omitempty"` 29 Error string `json:"error,omitempty"` 30 StartedAt time.Time `json:"started_at,omitempty"` 31 FinishedAt *time.Time `json:"finished_at,omitempty"` 32 Content string `json:"content,omitempty"` 33 } 34 35 // CommandOutput contains non-streamed stdout/stderr plus status. 36 type CommandOutput struct { 37 CommandStatus 38 Stdout string `json:"stdout"` 39 Stderr string `json:"stderr"` 40 } 41 42 func (c *Controller) commandSnapshot(session string) *commandKernel { 43 var kernel *commandKernel 44 if v, ok := c.commandClientMap.Load(session); ok { 45 kernel, _ = v.(*commandKernel) 46 } 47 if kernel == nil { 48 return nil 49 } 50 51 cp := *kernel 52 return &cp 53 } 54 55 // GetCommandStatus returns the execution status for a command session. 56 func (c *Controller) GetCommandStatus(session string) (*CommandStatus, error) { 57 kernel := c.commandSnapshot(session) 58 if kernel == nil { 59 return nil, fmt.Errorf("command not found: %s", session) 60 } 61 62 status := &CommandStatus{ 63 Session: session, 64 Running: kernel.running, 65 ExitCode: kernel.exitCode, 66 Error: kernel.errMsg, 67 StartedAt: kernel.startedAt, 68 FinishedAt: kernel.finishedAt, 69 Content: kernel.content, 70 } 71 return status, nil 72 } 73 74 // SeekBackgroundCommandOutput returns accumulated stdout/stderr and status for a session. 75 func (c *Controller) SeekBackgroundCommandOutput(session string, cursor int64) ([]byte, int64, error) { 76 kernel := c.commandSnapshot(session) 77 if kernel == nil { 78 return nil, -1, fmt.Errorf("command not found: %s", session) 79 } 80 81 if !kernel.isBackground { 82 return nil, -1, fmt.Errorf("command %s is not running in background", session) 83 } 84 85 file, err := os.Open(kernel.stdoutPath) 86 if err != nil { 87 return nil, -1, fmt.Errorf("error open combined output file for command %s: %w", session, err) 88 } 89 defer file.Close() 90 91 // Seek to the cursor position 92 _, err = file.Seek(cursor, 0) 93 if err != nil { 94 return nil, -1, fmt.Errorf("error seek file: %w", err) 95 } 96 97 // Read all content from cursor to end 98 data, err := io.ReadAll(file) 99 if err != nil { 100 return nil, -1, fmt.Errorf("error read file: %w", err) 101 } 102 103 // Get current file position (end of file) 104 currentPos, err := file.Seek(0, 1) 105 if err != nil { 106 return nil, -1, fmt.Errorf("error get current position: %w", err) 107 } 108 109 return data, currentPos, nil 110 } 111 112 // markCommandFinished updates bookkeeping when a command exits. 113 func (c *Controller) markCommandFinished(session string, exitCode int, errMsg string) { 114 now := time.Now() 115 116 c.mu.Lock() 117 defer c.mu.Unlock() 118 119 var kernel *commandKernel 120 if v, ok := c.commandClientMap.Load(session); ok { 121 kernel, _ = v.(*commandKernel) 122 } 123 if kernel == nil { 124 return 125 } 126 127 kernel.exitCode = &exitCode 128 kernel.errMsg = errMsg 129 kernel.running = false 130 kernel.finishedAt = &now 131 }