/ components / execd / pkg / runtime / command_status.go
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  }