Files
opensandbox-group--opensandbox/components/execd/pkg/web/controller/command.go
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

180 lines
5.1 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 controller
import (
"context"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/alibaba/opensandbox/execd/pkg/flag"
"github.com/alibaba/opensandbox/execd/pkg/jupyter/execute"
"github.com/alibaba/opensandbox/execd/pkg/runtime"
"github.com/alibaba/opensandbox/execd/pkg/telemetry"
"github.com/alibaba/opensandbox/execd/pkg/web/model"
)
// RunCommand executes a shell command and streams the output via SSE.
func (c *CodeInterpretingController) RunCommand() {
var request model.RunCommandRequest
if err := c.bindJSON(&request); err != nil {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("error parsing request, MAYBE invalid body format. %v", err),
)
return
}
err := request.Validate()
if err != nil {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("invalid request, validation error %v", err),
)
return
}
ctx, cancel := context.WithCancel(c.ctx.Request.Context())
defer cancel()
execStart := time.Now()
var recordOnce sync.Once
recordExecution := func(result string) {
recordOnce.Do(func() {
telemetry.RecordExecutionDuration(
ctx,
"run_command",
result,
float64(time.Since(execStart))/float64(time.Millisecond),
)
})
}
runCodeRequest := c.buildExecuteCommandRequest(request)
eventsHandler := c.setServerEventsHandler(ctx)
origComplete := eventsHandler.OnExecuteComplete
eventsHandler.OnExecuteComplete = func(executionTime time.Duration) {
origComplete(executionTime)
recordExecution("success")
}
origError := eventsHandler.OnExecuteError
eventsHandler.OnExecuteError = func(err *execute.ErrorOutput) {
origError(err)
recordExecution("failure")
}
runCodeRequest.Hooks = eventsHandler
// SSE headers are committed lazily on the first event write
// (see writeSingleEvent), so a synchronous error from Execute below can
// still be surfaced as a structured JSON error response.
err = codeRunner.Execute(runCodeRequest)
if err != nil {
recordExecution("failure")
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error running commands %v", err),
)
return
}
time.Sleep(flag.ApiGracefulShutdownTimeout)
}
// InterruptCommand stops a running shell command session.
func (c *CodeInterpretingController) InterruptCommand() {
c.interrupt()
}
// GetCommandStatus returns command status by id.
func (c *CodeInterpretingController) GetCommandStatus() {
commandID := c.ctx.Param("id")
if commandID == "" {
c.RespondError(http.StatusBadRequest, model.ErrorCodeInvalidRequest, "missing command execution id")
return
}
status, err := codeRunner.GetCommandStatus(commandID)
if err != nil {
c.RespondError(http.StatusNotFound, model.ErrorCodeInvalidRequest, err.Error())
return
}
resp := model.CommandStatusResponse{
ID: status.Session,
Running: status.Running,
ExitCode: status.ExitCode,
Error: status.Error,
Content: status.Content,
}
if !status.StartedAt.IsZero() {
resp.StartedAt = status.StartedAt
}
if status.FinishedAt != nil {
resp.FinishedAt = status.FinishedAt
}
c.RespondSuccess(resp)
}
// GetBackgroundCommandOutput returns accumulated stdout/stderr for a command session as plain text.
func (c *CodeInterpretingController) GetBackgroundCommandOutput() {
id := c.ctx.Param("id")
if id == "" {
c.RespondError(http.StatusBadRequest, model.ErrorCodeMissingQuery, "missing command execution id")
return
}
cursor := c.QueryInt64(c.ctx.Query("cursor"), 0)
output, lastCursor, err := codeRunner.SeekBackgroundCommandOutput(id, cursor)
if err != nil {
c.RespondError(http.StatusBadRequest, model.ErrorCodeInvalidRequest, err.Error())
return
}
c.ctx.Header("EXECD-COMMANDS-TAIL-CURSOR", strconv.FormatInt(lastCursor, 10))
c.ctx.Header("Content-Type", "text/plain; charset=utf-8")
c.ctx.String(http.StatusOK, "%s", output)
}
func (c *CodeInterpretingController) buildExecuteCommandRequest(request model.RunCommandRequest) *runtime.ExecuteCodeRequest {
timeout := time.Duration(request.TimeoutMs) * time.Millisecond
if request.Background {
return &runtime.ExecuteCodeRequest{
Language: runtime.BackgroundCommand,
Code: request.Command,
Cwd: request.Cwd,
Timeout: timeout,
Gid: request.Gid,
Uid: request.Uid,
Envs: request.Envs,
}
} else {
return &runtime.ExecuteCodeRequest{
Language: runtime.Command,
Code: request.Command,
Cwd: request.Cwd,
Timeout: timeout,
Gid: request.Gid,
Uid: request.Uid,
Envs: request.Envs,
}
}
}