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
180 lines
5.1 KiB
Go
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,
|
|
}
|
|
}
|
|
}
|