Files
opensandbox-group--opensandbox/components/execd/pkg/web/controller/codeinterpreting.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

510 lines
13 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"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"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"
)
var codeRunner codeExecutionRunner
func InitCodeRunner() *runtime.Controller {
ctrl := runtime.NewController(flag.JupyterServerHost, flag.JupyterServerToken)
codeRunner = ctrl
return ctrl
}
// CodeInterpretingController handles code execution entrypoints.
type CodeInterpretingController struct {
*basicController
}
type codeExecutionRunner interface {
CreateContext(req *runtime.CreateContextRequest) (string, error)
Execute(request *runtime.ExecuteCodeRequest) error
GetContext(session string) (runtime.CodeContext, error)
GetCommandStatus(session string) (*runtime.CommandStatus, error)
ListContext(language string) ([]runtime.CodeContext, error)
DeleteLanguageContext(language runtime.Language) error
DeleteContext(session string) error
CreateBashSession(req *runtime.CreateContextRequest) (string, error)
RunInBashSession(ctx context.Context, req *runtime.ExecuteCodeRequest) error
SeekBackgroundCommandOutput(session string, cursor int64) ([]byte, int64, error)
DeleteBashSession(sessionID string) error
Interrupt(sessionID string) error
CreatePTYSession(id, cwd, command string) (runtime.PTYSession, error)
GetPTYSession(id string) runtime.PTYSession
DeletePTYSession(id string) error
GetPTYSessionStatus(id string) (bool, int64, error)
}
func NewCodeInterpretingController(ctx *gin.Context) *CodeInterpretingController {
return &CodeInterpretingController{
basicController: newBasicController(ctx),
}
}
// CreateContext creates a new code execution context.
func (c *CodeInterpretingController) CreateContext() {
var request model.CodeContextRequest
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
}
session, err := codeRunner.CreateContext(&runtime.CreateContextRequest{
Language: runtime.Language(request.Language),
Cwd: request.Cwd,
})
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error creating code context. %v", err),
)
return
}
resp := model.CodeContext{
ID: session,
CodeContextRequest: request,
}
c.RespondSuccess(resp)
}
// InterruptCode interrupts the execution of running code in a session.
func (c *CodeInterpretingController) InterruptCode() {
c.interrupt()
}
// RunCode executes code in a context and streams output via SSE.
func (c *CodeInterpretingController) RunCode() {
var request model.RunCodeRequest
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_code",
result,
float64(time.Since(execStart))/float64(time.Millisecond),
)
})
}
runCodeRequest := c.buildExecuteCodeRequest(request)
eventsHandler := c.setServerEventsHandler(ctx)
// completeCh is closed when OnExecuteComplete fires, meaning the final SSE
// event has been written and flushed. We only wait for this callback as a
// safety check and then return immediately to avoid fixed tail latency.
completeCh := make(chan struct{})
var completeOnce sync.Once
signalComplete := func() {
completeOnce.Do(func() {
close(completeCh)
})
}
origComplete := eventsHandler.OnExecuteComplete
eventsHandler.OnExecuteComplete = func(executionTime time.Duration) {
origComplete(executionTime)
recordExecution("success")
signalComplete()
}
origError := eventsHandler.OnExecuteError
eventsHandler.OnExecuteError = func(err *execute.ErrorOutput) {
origError(err)
recordExecution("failure")
signalComplete()
}
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 codes %v", err),
)
return
}
waitForExecutionComplete(ctx, completeCh)
}
// GetContext returns a specific code context by id.
func (c *CodeInterpretingController) GetContext() {
contextID := c.ctx.Param("contextId")
if contextID == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing path parameter 'contextId'",
)
return
}
codeContext, err := codeRunner.GetContext(contextID)
if err != nil {
if errors.Is(err, runtime.ErrContextNotFound) {
c.RespondError(
http.StatusNotFound,
model.ErrorCodeContextNotFound,
fmt.Sprintf("context %s not found", contextID),
)
return
}
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error getting code context %s. %v", contextID, err),
)
return
}
c.RespondSuccess(codeContext)
}
// ListContexts returns active code contexts, optionally filtered by language.
func (c *CodeInterpretingController) ListContexts() {
language := c.ctx.Query("language")
contexts, err := codeRunner.ListContext(language)
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
err.Error(),
)
return
}
c.RespondSuccess(contexts)
}
// DeleteContextsByLanguage deletes all contexts for a given language.
func (c *CodeInterpretingController) DeleteContextsByLanguage() {
language := c.ctx.Query("language")
if language == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing query parameter 'language'",
)
return
}
err := codeRunner.DeleteLanguageContext(runtime.Language(language))
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error deleting code context %s. %v", language, err),
)
return
}
c.RespondSuccess(nil)
}
// DeleteContext deletes a specific code context by id.
func (c *CodeInterpretingController) DeleteContext() {
contextID := c.ctx.Param("contextId")
if contextID == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing path parameter 'contextId'",
)
return
}
err := codeRunner.DeleteContext(contextID)
if err != nil {
if errors.Is(err, runtime.ErrContextNotFound) {
c.RespondError(
http.StatusNotFound,
model.ErrorCodeContextNotFound,
fmt.Sprintf("context %s not found", contextID),
)
return
} else {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error deleting code context %s. %v", contextID, err),
)
return
}
}
c.RespondSuccess(nil)
}
// CreateSession creates a new bash session (create_session API).
// An empty body is allowed and is treated as default options (no cwd override).
func (c *CodeInterpretingController) CreateSession() {
var request model.CreateSessionRequest
if err := c.bindJSON(&request); err != nil && !errors.Is(err, io.EOF) {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("error parsing request. %v", err),
)
return
}
sessionID, err := codeRunner.CreateBashSession(&runtime.CreateContextRequest{
Cwd: request.Cwd,
})
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error creating session. %v", err),
)
return
}
c.RespondSuccess(model.CreateSessionResponse{SessionID: sessionID})
}
// RunInSession runs a command in an existing bash session and streams output via SSE (run_in_session API).
func (c *CodeInterpretingController) RunInSession() {
sessionID := c.ctx.Param("sessionId")
if sessionID == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing path parameter 'sessionId'",
)
return
}
var request model.RunInSessionRequest
if err := c.bindJSON(&request); err != nil {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("error parsing request. %v", err),
)
return
}
if err := request.Validate(); err != nil {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("invalid request. %v", err),
)
return
}
timeout := time.Duration(request.Timeout) * time.Millisecond
runReq := &runtime.ExecuteCodeRequest{
Language: runtime.Bash,
Context: sessionID,
Code: request.Command,
Cwd: request.Cwd,
Timeout: timeout,
}
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_in_session",
result,
float64(time.Since(execStart))/float64(time.Millisecond),
)
})
}
// completeCh is closed when OnExecuteComplete fires, meaning the final SSE
// event has been written and flushed. We only wait for this callback as a
// safety check and then return immediately to avoid fixed tail latency.
completeCh := make(chan struct{})
var completeOnce sync.Once
signalComplete := func() {
completeOnce.Do(func() {
close(completeCh)
})
}
hooks := c.setServerEventsHandler(ctx)
origComplete := hooks.OnExecuteComplete
hooks.OnExecuteComplete = func(executionTime time.Duration) {
origComplete(executionTime)
recordExecution("success")
signalComplete()
}
origError := hooks.OnExecuteError
hooks.OnExecuteError = func(err *execute.ErrorOutput) {
origError(err)
recordExecution("failure")
signalComplete()
}
runReq.Hooks = hooks
// SSE headers are committed lazily on the first event write
// (see writeSingleEvent), so a synchronous error from
// RunInBashSession can still be surfaced as a structured JSON error.
err := codeRunner.RunInBashSession(ctx, runReq)
if err != nil {
recordExecution("failure")
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error running in session. %v", err),
)
return
}
waitForExecutionComplete(ctx, completeCh)
}
// DeleteSession deletes a bash session (delete_session API).
func (c *CodeInterpretingController) DeleteSession() {
sessionID := c.ctx.Param("sessionId")
if sessionID == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing path parameter 'sessionId'",
)
return
}
err := codeRunner.DeleteBashSession(sessionID)
if err != nil {
if errors.Is(err, runtime.ErrContextNotFound) {
c.RespondError(
http.StatusNotFound,
model.ErrorCodeContextNotFound,
fmt.Sprintf("session %s not found", sessionID),
)
return
}
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error deleting session %s. %v", sessionID, err),
)
return
}
c.RespondSuccess(nil)
}
// buildExecuteCodeRequest converts a RunCodeRequest to runtime format.
func (c *CodeInterpretingController) buildExecuteCodeRequest(request model.RunCodeRequest) *runtime.ExecuteCodeRequest {
req := &runtime.ExecuteCodeRequest{
Language: runtime.Language(request.Context.Language),
Code: request.Code,
Context: request.Context.ID,
}
if req.Language == "" {
req.Language = runtime.Command
}
return req
}
func waitForExecutionComplete(ctx context.Context, completeCh <-chan struct{}) {
timer := time.NewTimer(flag.ApiGracefulShutdownTimeout)
defer func() {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}()
select {
case <-completeCh:
case <-ctx.Done():
case <-timer.C:
}
}
func (c *CodeInterpretingController) interrupt() {
session := c.ctx.Query("id")
if session == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing query parameter 'id'",
)
return
}
err := codeRunner.Interrupt(session)
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error interruptting code context. %v", err),
)
return
}
c.RespondSuccess(nil)
}