e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
139 lines
3.8 KiB
Go
139 lines
3.8 KiB
Go
// Copyright 2025 Alibaba Group Holding Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package runtime
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// CommandStatus describes the lifecycle state of a command.
|
|
type CommandStatus struct {
|
|
Session string `json:"session"`
|
|
Running bool `json:"running"`
|
|
ExitCode *int `json:"exit_code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
StartedAt time.Time `json:"started_at,omitempty"`
|
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
}
|
|
|
|
// CommandOutput contains non-streamed stdout/stderr plus status.
|
|
type CommandOutput struct {
|
|
CommandStatus
|
|
Stdout string `json:"stdout"`
|
|
Stderr string `json:"stderr"`
|
|
}
|
|
|
|
func (c *Controller) commandSnapshot(session string) *commandKernel {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
var kernel *commandKernel
|
|
if v, ok := c.commandClientMap.Load(session); ok {
|
|
kernel, _ = v.(*commandKernel)
|
|
}
|
|
if kernel == nil {
|
|
return nil
|
|
}
|
|
|
|
cp := *kernel
|
|
return &cp
|
|
}
|
|
|
|
// GetCommandStatus returns the execution status for a command session.
|
|
func (c *Controller) GetCommandStatus(session string) (*CommandStatus, error) {
|
|
kernel := c.commandSnapshot(session)
|
|
if kernel == nil {
|
|
return nil, fmt.Errorf("command not found: %s", session)
|
|
}
|
|
|
|
status := &CommandStatus{
|
|
Session: session,
|
|
Running: kernel.running,
|
|
ExitCode: kernel.exitCode,
|
|
Error: kernel.errMsg,
|
|
StartedAt: kernel.startedAt,
|
|
FinishedAt: kernel.finishedAt,
|
|
Content: kernel.content,
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
// SeekBackgroundCommandOutput returns accumulated stdout/stderr and status for a session.
|
|
func (c *Controller) SeekBackgroundCommandOutput(session string, cursor int64) ([]byte, int64, error) {
|
|
kernel := c.commandSnapshot(session)
|
|
if kernel == nil {
|
|
return nil, -1, fmt.Errorf("command not found: %s", session)
|
|
}
|
|
|
|
if !kernel.isBackground {
|
|
return nil, -1, fmt.Errorf("command %s is not running in background", session)
|
|
}
|
|
|
|
file, err := os.Open(kernel.stdoutPath)
|
|
if err != nil {
|
|
return nil, -1, fmt.Errorf("error open combined output file for command %s: %w", session, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Seek to the cursor position
|
|
_, err = file.Seek(cursor, 0)
|
|
if err != nil {
|
|
return nil, -1, fmt.Errorf("error seek file: %w", err)
|
|
}
|
|
|
|
// Read all content from cursor to end
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, -1, fmt.Errorf("error read file: %w", err)
|
|
}
|
|
|
|
// Get current file position (end of file)
|
|
currentPos, err := file.Seek(0, 1)
|
|
if err != nil {
|
|
return nil, -1, fmt.Errorf("error get current position: %w", err)
|
|
}
|
|
|
|
return data, currentPos, nil
|
|
}
|
|
|
|
// markCommandFinished updates bookkeeping when a command exits.
|
|
func (c *Controller) markCommandFinished(session string, exitCode int, errMsg string) {
|
|
now := time.Now()
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
var kernel *commandKernel
|
|
if v, ok := c.commandClientMap.Load(session); ok {
|
|
kernel, _ = v.(*commandKernel)
|
|
}
|
|
if kernel == nil {
|
|
return
|
|
}
|
|
|
|
kernel.exitCode = &exitCode
|
|
kernel.errMsg = errMsg
|
|
kernel.running = false
|
|
kernel.finishedAt = &now
|
|
// Clear the PID so a late or retried Interrupt cannot signal a recycled
|
|
// process. Group-wide kill would otherwise amplify the impact of a
|
|
// stale-PID hit to every process in the unrelated process group.
|
|
kernel.pid = 0
|
|
}
|