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
507 lines
12 KiB
Go
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)
|
|
}
|