Files
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

507 lines
12 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 execute provides functionality for executing Jupyter kernel code via WebSocket
package execute
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/alibaba/opensandbox/internal/safego"
"github.com/google/uuid"
"github.com/gorilla/websocket"
execdflag "github.com/alibaba/opensandbox/execd/pkg/flag"
)
// HTTPClient defines the HTTP client interface
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// Client is the client for code execution
type Client struct {
// Internal HTTP client for sending HTTP requests
httpClient HTTPClient
// WebSocket connection
conn *websocket.Conn
// Message handler mappings
handlers map[MessageType]func(*Message)
// Session ID
session string
// Message ID counter
msgCounter int
// Mutex for protecting concurrent access
mu sync.Mutex
// WebSocket URL for kernel connection
wsURL string
}
// NewClient creates a new code execution client
func NewClient(baseURL string, httpClient HTTPClient) *Client {
return &Client{
httpClient: httpClient,
handlers: make(map[MessageType]func(*Message)),
session: uuid.New().String(),
msgCounter: 0,
}
}
// Connect connects to the WebSocket of the specified kernel
func (c *Client) Connect(wsURL string) error {
c.mu.Lock()
defer c.mu.Unlock()
// Save WebSocket URL
c.wsURL = wsURL
// Connect to WebSocket
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
if resp != nil && err != nil {
resp.Body.Close()
}
if err != nil {
return fmt.Errorf("failed to connect to kernel: %w", err)
}
c.conn = conn
// Register default message handlers
c.registerDefaultHandlers()
// Start message receiving goroutine
safego.Go(func() { c.receiveMessages() })
return nil
}
// Disconnect disconnects the WebSocket connection to the kernel
func (c *Client) Disconnect() {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
}
// IsConnected checks if connected to the kernel
func (c *Client) IsConnected() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.conn != nil
}
type streamExecutionState struct {
startTime time.Time
result *ExecutionResult
executeDone bool
executeMutex sync.Mutex
resultMutex sync.Mutex
}
func newStreamExecutionState(startTime time.Time) *streamExecutionState {
return &streamExecutionState{
startTime: startTime,
result: &ExecutionResult{
Status: "ok",
Stream: make([]*StreamOutput, 0),
ExecutionTime: 0,
},
}
}
// ExecuteCodeStream executes code in streaming mode, sending results to the provided channel
func (c *Client) ExecuteCodeStream(code string, resultChan chan *ExecutionResult) error {
if !c.IsConnected() {
return errors.New("not connected to kernel, please call Connect method")
}
msg, err := c.buildExecuteMessage(code)
if err != nil {
return err
}
state := newStreamExecutionState(time.Now())
// Clear temporary handlers
c.clearTemporaryHandlers()
c.registerExecuteCodeStreamHandlers(state, resultChan)
if err := c.writeMessage(msg); err != nil {
return fmt.Errorf("failed to send execution request: %w", err)
}
return nil
}
func (c *Client) buildExecuteMessage(code string) (*Message, error) {
msgID := c.nextMessageID()
request := &ExecuteRequest{
Code: code,
Silent: false,
StoreHistory: true,
UserExpressions: make(map[string]string),
AllowStdin: false,
StopOnError: true,
}
content, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to serialize request: %w", err)
}
msg := &Message{
Header: Header{
MessageID: msgID,
Username: "go-client",
Session: c.session,
Date: time.Now().Format(time.RFC3339),
MessageType: string(MsgExecuteRequest),
Version: "5.3",
},
ParentHeader: Header{},
Metadata: make(map[string]interface{}),
Content: content,
Channel: "shell",
}
return msg, nil
}
func (c *Client) registerExecuteCodeStreamHandlers(state *streamExecutionState, resultChan chan *ExecutionResult) {
c.registerHandler(MsgExecuteReply, func(msg *Message) {
c.handleExecuteReply(msg, state)
})
c.registerHandler(MsgExecuteResult, func(msg *Message) {
c.handleExecuteResult(msg, state, resultChan)
})
c.registerHandler(MsgStream, func(msg *Message) {
c.handleStreamOutput(msg, state, resultChan)
})
c.registerHandler(MsgError, func(msg *Message) {
c.handleExecutionError(msg, state, resultChan)
})
c.registerHandler(MsgStatus, func(msg *Message) {
c.handleExecutionStatus(msg, state, resultChan)
})
}
func (c *Client) handleExecuteReply(msg *Message, state *streamExecutionState) {
var execReply ExecuteReply
if err := json.Unmarshal(msg.Content, &execReply); err != nil {
return
}
state.resultMutex.Lock()
defer state.resultMutex.Unlock()
state.result.ExecutionCount = execReply.ExecutionCount
if execReply.EName != "" {
state.result.Error = &execReply.ErrorOutput
}
}
func (c *Client) handleExecuteResult(msg *Message, state *streamExecutionState, resultChan chan *ExecutionResult) {
var execResult ExecuteResult
if err := json.Unmarshal(msg.Content, &execResult); err != nil {
return
}
state.resultMutex.Lock()
defer state.resultMutex.Unlock()
state.result.ExecutionCount = execResult.ExecutionCount
notify := &ExecutionResult{
ExecutionCount: execResult.ExecutionCount,
ExecutionData: execResult.Data,
}
resultChan <- notify
}
func (c *Client) handleStreamOutput(msg *Message, state *streamExecutionState, resultChan chan *ExecutionResult) {
var stream StreamOutput
if err := json.Unmarshal(msg.Content, &stream); err != nil {
return
}
state.resultMutex.Lock()
defer state.resultMutex.Unlock()
state.result.Stream = append(state.result.Stream, &stream)
notify := &ExecutionResult{
Stream: []*StreamOutput{&stream},
}
resultChan <- notify
}
func (c *Client) handleExecutionError(msg *Message, state *streamExecutionState, resultChan chan *ExecutionResult) {
var errOutput ErrorOutput
if err := json.Unmarshal(msg.Content, &errOutput); err != nil {
return
}
state.resultMutex.Lock()
defer state.resultMutex.Unlock()
state.result.Status = "error"
state.result.Error = &errOutput
notify := &ExecutionResult{
Error: &errOutput,
Status: "error",
}
resultChan <- notify
}
func (c *Client) handleExecutionStatus(msg *Message, state *streamExecutionState, resultChan chan *ExecutionResult) {
var status StatusUpdate
if err := json.Unmarshal(msg.Content, &status); err != nil {
return
}
if status.ExecutionState != StateIdle {
return
}
state.executeMutex.Lock()
defer state.executeMutex.Unlock()
if state.executeDone {
return
}
state.executeDone = true
safego.Go(func() { c.finalizeExecution(state, resultChan) })
}
func (c *Client) finalizeExecution(state *streamExecutionState, resultChan chan *ExecutionResult) {
state.resultMutex.Lock()
state.result.ExecutionTime = time.Since(state.startTime)
notify := &ExecutionResult{
ExecutionTime: state.result.ExecutionTime,
}
resultChan <- notify
state.resultMutex.Unlock()
pollInterval := execdflag.JupyterIdlePollInterval
if pollInterval <= 0 {
pollInterval = 100 * time.Millisecond
}
for {
state.resultMutex.Lock()
done := state.result.ExecutionCount > 0 || state.result.Error != nil
state.resultMutex.Unlock()
if done {
break
}
time.Sleep(pollInterval)
}
close(resultChan)
}
func (c *Client) writeMessage(msg *Message) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.conn.WriteJSON(msg)
}
// ExecuteCodeWithCallback executes code using callback functions
func (c *Client) ExecuteCodeWithCallback(code string, handler CallbackHandler) error {
if !c.IsConnected() {
return errors.New("not connected to kernel, please call Connect method")
}
// prepare execution request
msgID := c.nextMessageID()
request := &ExecuteRequest{
Code: code,
Silent: false,
StoreHistory: true,
UserExpressions: make(map[string]string),
AllowStdin: false,
StopOnError: true,
}
// serialize request content
content, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("failed to serialize request: %w", err)
}
// create message
msg := &Message{
Header: Header{
MessageID: msgID,
Username: "go-client",
Session: c.session,
Date: time.Now().Format(time.RFC3339),
MessageType: string(MsgExecuteRequest),
Version: "5.3",
},
ParentHeader: Header{},
Metadata: make(map[string]interface{}),
Content: content,
Channel: "shell",
}
// register execution result handler
if handler.OnExecuteResult != nil {
c.registerHandler(MsgExecuteResult, func(msg *Message) {
var execResult ExecuteResult
if err := json.Unmarshal(msg.Content, &execResult); err != nil {
return
}
// calls callback functions
handler.OnExecuteResult(&execResult)
})
}
// Register stream output handler
if handler.OnStream != nil {
c.registerHandler(MsgStream, func(msg *Message) {
var stream StreamOutput
if err := json.Unmarshal(msg.Content, &stream); err != nil {
return
}
// calls callback functions
handler.OnStream(&stream)
})
}
// Register display data handler
if handler.OnDisplayData != nil {
c.registerHandler(MsgDisplayData, func(msg *Message) {
var display DisplayData
if err := json.Unmarshal(msg.Content, &display); err != nil {
return
}
// calls callback functions
handler.OnDisplayData(&display)
})
}
// register error handler
if handler.OnError != nil {
c.registerHandler(MsgError, func(msg *Message) {
var errOutput ErrorOutput
if err := json.Unmarshal(msg.Content, &errOutput); err != nil {
return
}
// calls callback functions
handler.OnError(&errOutput)
})
}
// register status handler
if handler.OnStatus != nil {
c.registerHandler(MsgStatus, func(msg *Message) {
var status StatusUpdate
if err := json.Unmarshal(msg.Content, &status); err != nil {
return
}
// calls callback functions
handler.OnStatus(&status)
})
}
// send execution request
c.mu.Lock()
err = c.conn.WriteJSON(msg)
c.mu.Unlock()
if err != nil {
return fmt.Errorf("failed to send execution request: %w", err)
}
return nil
}
// Register default message handlers
func (c *Client) registerDefaultHandlers() {
// default message handlers can be registered here
}
// Register temporary message handler
func (c *Client) registerHandler(msgType MessageType, handler func(*Message)) {
c.mu.Lock()
defer c.mu.Unlock()
c.handlers[msgType] = handler
}
// Clear temporary message handlers
func (c *Client) clearTemporaryHandlers() {
c.mu.Lock()
defer c.mu.Unlock()
c.handlers = make(map[MessageType]func(*Message))
c.registerDefaultHandlers()
}
// Receive WebSocket messages
func (c *Client) receiveMessages() {
for {
c.mu.Lock()
conn := c.conn
c.mu.Unlock()
if conn == nil {
break
}
// Receive message
var msg Message
err := conn.ReadJSON(&msg)
if err != nil {
// connection may already be closed
break
}
// Process message
c.handleMessage(&msg)
}
}
// Handle received messages
func (c *Client) handleMessage(msg *Message) {
// Extract message type
msgType := MessageType(msg.Header.MessageType)
// call the corresponding handler
c.mu.Lock()
handler, ok := c.handlers[msgType]
c.mu.Unlock()
if ok && handler != nil {
handler(msg)
}
}
// generate next messageID
func (c *Client) nextMessageID() string {
c.mu.Lock()
defer c.mu.Unlock()
c.msgCounter++
return fmt.Sprintf("%s-%d", c.session, c.msgCounter)
}