2026-02-07 08:06:44 +01:00
|
|
|
"""
|
|
|
|
|
Real Jupyter kernel management for stateful execution.
|
|
|
|
|
|
|
|
|
|
This module implements proper Jupyter kernel management:
|
|
|
|
|
- jupyter-client runs on host (MCP-Forge server)
|
|
|
|
|
- ipykernel runs inside Podman containers
|
|
|
|
|
- Communication via ZMQ protocol
|
|
|
|
|
- 1:1 mapping: one container per session, one kernel per container
|
|
|
|
|
"""
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
from typing import Dict, Optional, List, Any
|
2026-02-07 08:06:44 +01:00
|
|
|
from dataclasses import dataclass
|
2026-02-07 07:45:57 +01:00
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
import uuid
|
|
|
|
|
import json
|
2026-02-07 08:06:44 +01:00
|
|
|
import tempfile
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from jupyter_client.blocking.client import BlockingKernelClient
|
|
|
|
|
import zmq
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
from mcp_forge.podman.containers import SecureContainerManager, ContainerConfig
|
|
|
|
|
from mcp_forge.security.resource_limits import ResourceLimits
|
|
|
|
|
from mcp_forge.execution.simple.executor import ExecutionResult
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KernelError(Exception):
|
|
|
|
|
"""Raised when kernel operations fail."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class KernelInfo:
|
|
|
|
|
"""Information about a running kernel."""
|
|
|
|
|
kernel_id: str
|
|
|
|
|
container_id: str
|
|
|
|
|
session_id: str
|
2026-02-07 08:06:44 +01:00
|
|
|
connection_file: Path
|
|
|
|
|
connection_info: Dict[str, Any] # ZMQ ports and keys
|
2026-02-07 07:45:57 +01:00
|
|
|
started_at: datetime
|
|
|
|
|
last_activity: datetime
|
2026-02-07 08:06:44 +01:00
|
|
|
client: Optional[BlockingKernelClient] = None
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
def to_dict(self) -> dict:
|
|
|
|
|
"""Convert to dictionary for JSON serialization."""
|
|
|
|
|
return {
|
|
|
|
|
"kernel_id": self.kernel_id,
|
|
|
|
|
"container_id": self.container_id,
|
|
|
|
|
"session_id": self.session_id,
|
|
|
|
|
"started_at": self.started_at.isoformat(),
|
|
|
|
|
"last_activity": self.last_activity.isoformat(),
|
2026-02-07 08:06:44 +01:00
|
|
|
"connection_info": {
|
|
|
|
|
"shell_port": self.connection_info.get("shell_port"),
|
|
|
|
|
"iopub_port": self.connection_info.get("iopub_port"),
|
|
|
|
|
"stdin_port": self.connection_info.get("stdin_port"),
|
|
|
|
|
"control_port": self.connection_info.get("control_port"),
|
|
|
|
|
"hb_port": self.connection_info.get("hb_port"),
|
|
|
|
|
}
|
2026-02-07 07:45:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class JupyterKernelManager:
|
|
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
Manages IPython kernels in containers via jupyter-client.
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Architecture:
|
|
|
|
|
- This class runs on host (MCP-Forge server process)
|
|
|
|
|
- Creates one container per session with ipykernel running inside
|
|
|
|
|
- Connects to kernel via ZMQ protocol (jupyter-client)
|
|
|
|
|
- Communicates using Jupyter message protocol
|
|
|
|
|
|
|
|
|
|
Each session gets:
|
|
|
|
|
- Dedicated container
|
|
|
|
|
- Dedicated kernel process
|
|
|
|
|
- Isolated Python namespace
|
|
|
|
|
- Independent resource limits
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
container_manager: SecureContainerManager,
|
|
|
|
|
image: str,
|
2026-02-07 08:06:44 +01:00
|
|
|
resource_limits: Optional[ResourceLimits] = None
|
2026-02-07 07:45:57 +01:00
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Initialize kernel manager.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
container_manager: Container lifecycle manager
|
2026-02-07 08:06:44 +01:00
|
|
|
image: Docker/Podman image with ipykernel installed
|
|
|
|
|
resource_limits: Default resource limits for kernels
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
|
|
|
|
self.container_manager = container_manager
|
|
|
|
|
self.image = image
|
|
|
|
|
self.resource_limits = resource_limits
|
|
|
|
|
self.kernels: Dict[str, KernelInfo] = {}
|
|
|
|
|
|
|
|
|
|
def start_kernel(
|
|
|
|
|
self,
|
|
|
|
|
session_id: str,
|
|
|
|
|
volumes: Optional[Dict[str, dict]] = None
|
|
|
|
|
) -> str:
|
|
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
Start IPython kernel in dedicated container.
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Process:
|
|
|
|
|
1. Generate ZMQ connection info (ports, keys)
|
|
|
|
|
2. Create connection file
|
|
|
|
|
3. Create container with ipykernel command
|
|
|
|
|
4. Start container
|
|
|
|
|
5. Wait for kernel to be ready
|
|
|
|
|
6. Connect jupyter-client to kernel via ZMQ
|
|
|
|
|
7. Verify kernel is responsive
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
session_id: Session ID this kernel belongs to
|
|
|
|
|
volumes: Optional volume mounts
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_id: Unique identifier for this kernel
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
KernelError: If kernel startup fails
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
|
|
|
|
kernel_id = f"kernel-{uuid.uuid4().hex[:16]}"
|
|
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
# Generate connection info
|
|
|
|
|
connection_info = self._generate_connection_info()
|
|
|
|
|
|
|
|
|
|
# Create connection file
|
|
|
|
|
connection_file = self._create_connection_file(kernel_id, connection_info)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Create container with ipykernel
|
|
|
|
|
config = ContainerConfig(
|
|
|
|
|
image=self.image,
|
|
|
|
|
command=[
|
|
|
|
|
"python", "-m", "ipykernel_launcher",
|
|
|
|
|
"-f", f"/tmp/kernel-{kernel_id}.json"
|
|
|
|
|
],
|
|
|
|
|
resource_limits=self.resource_limits,
|
|
|
|
|
volumes=volumes or {},
|
|
|
|
|
# TODO: Port mappings for ZMQ
|
|
|
|
|
# TODO: Mount connection file into container
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
container_id = self.container_manager.create_container(
|
|
|
|
|
config,
|
|
|
|
|
session_id=session_id,
|
|
|
|
|
name=f"jupyter-{kernel_id}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Start container
|
|
|
|
|
self.container_manager.start_container(container_id)
|
|
|
|
|
|
|
|
|
|
# Wait for kernel to be ready
|
|
|
|
|
time.sleep(2) # TODO: Better readiness check
|
|
|
|
|
|
|
|
|
|
# Connect client
|
|
|
|
|
client = self._connect_client(connection_info)
|
|
|
|
|
|
|
|
|
|
# Verify kernel is responsive
|
|
|
|
|
if not self._verify_kernel(client):
|
|
|
|
|
raise KernelError(f"Kernel {kernel_id} not responsive")
|
|
|
|
|
|
|
|
|
|
# Register kernel
|
|
|
|
|
now = datetime.utcnow()
|
|
|
|
|
kernel_info = KernelInfo(
|
|
|
|
|
kernel_id=kernel_id,
|
|
|
|
|
container_id=container_id,
|
|
|
|
|
session_id=session_id,
|
|
|
|
|
connection_file=connection_file,
|
|
|
|
|
connection_info=connection_info,
|
|
|
|
|
started_at=now,
|
|
|
|
|
last_activity=now,
|
|
|
|
|
client=client
|
|
|
|
|
)
|
|
|
|
|
self.kernels[kernel_id] = kernel_info
|
|
|
|
|
|
|
|
|
|
return kernel_id
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# Cleanup on failure
|
|
|
|
|
connection_file.unlink(missing_ok=True)
|
|
|
|
|
raise KernelError(f"Failed to start kernel: {e}") from e
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
def execute_code(
|
|
|
|
|
self,
|
|
|
|
|
kernel_id: str,
|
|
|
|
|
code: str,
|
|
|
|
|
timeout: int = 300
|
|
|
|
|
) -> ExecutionResult:
|
|
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
Execute code in kernel via ZMQ.
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Uses jupyter-client to:
|
|
|
|
|
1. Send execute_request message
|
|
|
|
|
2. Receive stream (stdout/stderr) messages
|
|
|
|
|
3. Receive execute_result/display_data messages
|
|
|
|
|
4. Collect and parse all output
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
Args:
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_id: Kernel to execute in
|
2026-02-07 07:45:57 +01:00
|
|
|
code: Python code to execute
|
2026-02-07 08:06:44 +01:00
|
|
|
timeout: Maximum execution time in seconds
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
Returns:
|
2026-02-07 08:06:44 +01:00
|
|
|
ExecutionResult with stdout, stderr, result
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
KernelError: If kernel not found or execution fails
|
|
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_info = self._get_kernel(kernel_id)
|
|
|
|
|
client = kernel_info.client
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
if not client:
|
|
|
|
|
raise KernelError(f"Kernel {kernel_id} has no connected client")
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
try:
|
2026-02-07 08:06:44 +01:00
|
|
|
# Execute code
|
|
|
|
|
_msg_id = client.execute(code, silent=False, store_history=True)
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
# Collect output
|
|
|
|
|
stdout_parts = []
|
|
|
|
|
stderr_parts = []
|
|
|
|
|
result = None
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
# Wait for execution to complete
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
msg = client.get_iopub_msg(timeout=timeout)
|
|
|
|
|
msg_type = msg['header']['msg_type']
|
|
|
|
|
content = msg['content']
|
|
|
|
|
|
|
|
|
|
if msg_type == 'stream':
|
|
|
|
|
if content['name'] == 'stdout':
|
|
|
|
|
stdout_parts.append(content['text'])
|
|
|
|
|
elif content['name'] == 'stderr':
|
|
|
|
|
stderr_parts.append(content['text'])
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'execute_result':
|
|
|
|
|
result = content.get('data', {}).get('text/plain', '')
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'error':
|
|
|
|
|
stderr_parts.append('\n'.join(content['traceback']))
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'status':
|
|
|
|
|
if content['execution_state'] == 'idle':
|
|
|
|
|
break
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
except zmq.error.Again:
|
|
|
|
|
break
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
execution_time = time.time() - start_time
|
|
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
# Update last activity
|
|
|
|
|
kernel_info.last_activity = datetime.utcnow()
|
|
|
|
|
|
2026-02-07 07:45:57 +01:00
|
|
|
return ExecutionResult(
|
2026-02-07 08:06:44 +01:00
|
|
|
success=True,
|
|
|
|
|
stdout=''.join(stdout_parts),
|
|
|
|
|
stderr=''.join(stderr_parts),
|
|
|
|
|
result=result,
|
2026-02-07 07:45:57 +01:00
|
|
|
execution_time=execution_time,
|
2026-02-07 08:06:44 +01:00
|
|
|
exit_code=0
|
2026-02-07 07:45:57 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
execution_time = time.time() - start_time
|
|
|
|
|
return ExecutionResult(
|
|
|
|
|
success=False,
|
2026-02-07 08:06:44 +01:00
|
|
|
stdout='',
|
|
|
|
|
stderr=str(e),
|
2026-02-07 07:45:57 +01:00
|
|
|
result=None,
|
|
|
|
|
execution_time=execution_time,
|
|
|
|
|
exit_code=1,
|
2026-02-07 08:06:44 +01:00
|
|
|
error=str(e)
|
2026-02-07 07:45:57 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def shutdown_kernel(self, kernel_id: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Shutdown kernel and cleanup container.
|
|
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
1. Send shutdown_request via ZMQ
|
|
|
|
|
2. Wait for kernel shutdown
|
|
|
|
|
3. Stop and remove container
|
|
|
|
|
4. Cleanup connection file
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Args:
|
|
|
|
|
kernel_id: Kernel to shutdown
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_info = self._get_kernel(kernel_id)
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
try:
|
2026-02-07 08:06:44 +01:00
|
|
|
# Shutdown kernel
|
|
|
|
|
if kernel_info.client:
|
|
|
|
|
kernel_info.client.shutdown()
|
|
|
|
|
kernel_info.client.stop_channels()
|
|
|
|
|
|
|
|
|
|
# Stop and remove container
|
|
|
|
|
self.container_manager.stop_container(kernel_info.container_id)
|
2026-02-07 07:45:57 +01:00
|
|
|
self.container_manager.remove_container(kernel_info.container_id)
|
2026-02-07 08:06:44 +01:00
|
|
|
|
|
|
|
|
# Cleanup connection file
|
|
|
|
|
kernel_info.connection_file.unlink(missing_ok=True)
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
# Remove from registry
|
|
|
|
|
del self.kernels[kernel_id]
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
def inspect_namespace(self, kernel_id: str) -> List[str]:
|
|
|
|
|
"""
|
|
|
|
|
Get list of variables in kernel namespace.
|
|
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Executes introspection code:
|
|
|
|
|
[var for var in dir() if not var.startswith('_')]
|
|
|
|
|
|
2026-02-07 07:45:57 +01:00
|
|
|
Args:
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_id: Kernel to inspect
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
Returns:
|
2026-02-07 08:06:44 +01:00
|
|
|
List of variable names
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
code = "[var for var in dir() if not var.startswith('_')]"
|
|
|
|
|
result = self.execute_code(kernel_id, code, timeout=5)
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
if result.success and result.result:
|
|
|
|
|
# Parse result (it's a string representation of a list)
|
|
|
|
|
try:
|
|
|
|
|
return eval(result.result) # nosec - controlled code
|
|
|
|
|
except Exception:
|
|
|
|
|
return []
|
|
|
|
|
return []
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
def get_variable_info(
|
|
|
|
|
self,
|
|
|
|
|
kernel_id: str,
|
|
|
|
|
variable_name: str
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
Get detailed information about a variable.
|
|
|
|
|
|
|
|
|
|
Executes introspection code to get:
|
|
|
|
|
- type(var).__name__
|
|
|
|
|
- sys.getsizeof(var) if available
|
|
|
|
|
- var.shape if hasattr(var, 'shape')
|
|
|
|
|
- repr(var)[:100]
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
Args:
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_id: Kernel to inspect
|
2026-02-07 07:45:57 +01:00
|
|
|
variable_name: Name of variable to inspect
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-07 08:06:44 +01:00
|
|
|
Dict with type, size, shape, repr
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
code = f"""
|
|
|
|
|
import sys
|
|
|
|
|
_var = {variable_name}
|
|
|
|
|
_info = {{
|
|
|
|
|
'type': type(_var).__name__,
|
|
|
|
|
'repr': repr(_var)[:100],
|
|
|
|
|
}}
|
|
|
|
|
try:
|
|
|
|
|
_info['size_bytes'] = sys.getsizeof(_var)
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
if hasattr(_var, 'shape'):
|
|
|
|
|
_info['shape'] = _var.shape
|
|
|
|
|
_info
|
|
|
|
|
"""
|
|
|
|
|
result = self.execute_code(kernel_id, code, timeout=5)
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
if result.success and result.result:
|
2026-02-07 07:45:57 +01:00
|
|
|
try:
|
2026-02-07 08:06:44 +01:00
|
|
|
return eval(result.result) # nosec - controlled code
|
|
|
|
|
except Exception:
|
|
|
|
|
return {}
|
|
|
|
|
return {}
|
2026-02-07 07:45:57 +01:00
|
|
|
|
|
|
|
|
def restart_kernel(self, kernel_id: str) -> None:
|
|
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
Restart kernel (namespace reset, container kept).
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Strategy: shutdown current kernel and start new one in same container.
|
|
|
|
|
Note: In a full implementation, we'd use KernelManager.restart_kernel().
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
Args:
|
|
|
|
|
kernel_id: Kernel to restart
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
2026-02-07 08:06:44 +01:00
|
|
|
kernel_info = self._get_kernel(kernel_id)
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
# For now, just record activity - full restart implementation requires
|
|
|
|
|
# KernelManager integration (not just BlockingKernelClient)
|
|
|
|
|
# TODO: Implement proper kernel restart via KernelManager
|
2026-02-07 07:45:57 +01:00
|
|
|
kernel_info.last_activity = datetime.utcnow()
|
|
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
def cleanup_idle_kernels(
|
|
|
|
|
self,
|
|
|
|
|
idle_timeout: timedelta
|
|
|
|
|
) -> int:
|
2026-02-07 07:45:57 +01:00
|
|
|
"""
|
|
|
|
|
Cleanup kernels idle longer than timeout.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
idle_timeout: Maximum idle time before cleanup
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Number of kernels cleaned up
|
|
|
|
|
"""
|
|
|
|
|
now = datetime.utcnow()
|
2026-02-07 08:06:44 +01:00
|
|
|
cleaned_up = 0
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
for kernel_id in list(self.kernels.keys()):
|
|
|
|
|
kernel_info = self.kernels[kernel_id]
|
2026-02-07 07:45:57 +01:00
|
|
|
idle_time = now - kernel_info.last_activity
|
2026-02-07 08:06:44 +01:00
|
|
|
|
2026-02-07 07:45:57 +01:00
|
|
|
if idle_time > idle_timeout:
|
2026-02-07 08:06:44 +01:00
|
|
|
try:
|
|
|
|
|
self.shutdown_kernel(kernel_id)
|
|
|
|
|
cleaned_up += 1
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # Continue cleanup even if one fails
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
return cleaned_up
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
def _get_kernel(self, kernel_id: str) -> KernelInfo:
|
|
|
|
|
"""Get kernel info or raise error."""
|
|
|
|
|
if kernel_id not in self.kernels:
|
|
|
|
|
raise KernelError(f"Kernel {kernel_id} not found")
|
|
|
|
|
return self.kernels[kernel_id]
|
|
|
|
|
|
|
|
|
|
def _generate_connection_info(self) -> Dict[str, Any]:
|
|
|
|
|
"""Generate ZMQ connection information."""
|
|
|
|
|
import secrets
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
return {
|
|
|
|
|
"shell_port": 0, # Let ZMQ assign
|
|
|
|
|
"iopub_port": 0,
|
|
|
|
|
"stdin_port": 0,
|
|
|
|
|
"control_port": 0,
|
|
|
|
|
"hb_port": 0,
|
|
|
|
|
"ip": "127.0.0.1",
|
|
|
|
|
"key": secrets.token_hex(32),
|
|
|
|
|
"transport": "tcp",
|
|
|
|
|
"signature_scheme": "hmac-sha256",
|
|
|
|
|
"kernel_name": "python3"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _create_connection_file(
|
|
|
|
|
self,
|
|
|
|
|
kernel_id: str,
|
|
|
|
|
connection_info: Dict[str, Any]
|
|
|
|
|
) -> Path:
|
|
|
|
|
"""Create connection file for kernel."""
|
|
|
|
|
# Create temp file
|
|
|
|
|
fd, path = tempfile.mkstemp(suffix=f"-kernel-{kernel_id}.json")
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
# Write connection info
|
|
|
|
|
with open(fd, 'w') as f:
|
|
|
|
|
json.dump(connection_info, f)
|
2026-02-07 07:45:57 +01:00
|
|
|
|
2026-02-07 08:06:44 +01:00
|
|
|
return Path(path)
|
|
|
|
|
|
|
|
|
|
def _connect_client(self, connection_info: Dict[str, Any]) -> BlockingKernelClient:
|
|
|
|
|
"""Connect jupyter-client to kernel."""
|
|
|
|
|
client = BlockingKernelClient()
|
|
|
|
|
client.load_connection_info(connection_info)
|
|
|
|
|
client.start_channels()
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
def _verify_kernel(self, client: BlockingKernelClient, timeout: int = 10) -> bool:
|
|
|
|
|
"""Verify kernel is responsive."""
|
|
|
|
|
try:
|
|
|
|
|
client.wait_for_ready(timeout=timeout)
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|