Skip to content

Commit 02c72c2

Browse files
Add isolated container functionality for fresh instances
- Modified CodeRunner constructor to support isolated=True parameter (default) - Added ContainerManager.create_isolated_container() with unique port allocation - Implemented cleanup methods for isolated containers with context manager support - Added comprehensive tests for isolated vs shared container modes - Users now get fresh isolated containers by default instead of shared ones - Maintains backward compatibility with isolated=False for shared containers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 432444c commit 02c72c2

File tree

3 files changed

+389
-12
lines changed

3 files changed

+389
-12
lines changed

__init__.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
44
Zero-configuration local code execution with seamless cloud migration.
55
6-
Basic Usage:
6+
Basic Usage (Isolated Fresh Container):
77
from coderunner import CodeRunner
88
9-
runner = CodeRunner() # Auto-starts container
9+
runner = CodeRunner() # Creates fresh isolated container
1010
result = runner.execute("print('Hello World!')")
1111
print(result['stdout']) # "Hello World!"
1212
13+
Shared Container (for backward compatibility):
14+
runner = CodeRunner(isolated=False) # Reuses shared container
15+
1316
Cloud Migration:
1417
from coderunner.cloud import InstaVM as CodeRunner
1518
@@ -32,26 +35,40 @@
3235
class CodeRunner:
3336
"""Local code execution without API keys or cloud setup"""
3437

35-
def __init__(self, auto_start: bool = True, base_url: str = None):
38+
def __init__(self, auto_start: bool = True, base_url: str = None, isolated: bool = True):
3639
"""
3740
Initialize CodeRunner client
3841
3942
Args:
4043
auto_start: Automatically start container if not running
4144
base_url: Custom REST API base URL (for testing)
45+
isolated: Create isolated fresh container (True) or reuse shared (False)
4246
"""
43-
self.base_url = base_url or "http://localhost:8223"
44-
self.session_id: Optional[str] = None
47+
self.isolated = isolated
48+
self.container_id: Optional[str] = None
4549
self._session = requests.Session()
4650

4751
# Set reasonable timeouts
4852
self._session.timeout = 30
4953

50-
if auto_start:
54+
if isolated and auto_start:
55+
# Create isolated fresh container
5156
try:
52-
ContainerManager.ensure_running()
57+
self.container_id, ports = ContainerManager.create_isolated_container()
58+
self.base_url = f"http://localhost:{ports['rest']}"
59+
logger.info(f"Created isolated container {self.container_id} on port {ports['rest']}")
5360
except Exception as e:
54-
raise CodeRunnerError(f"Failed to start CodeRunner: {e}")
61+
raise CodeRunnerError(f"Failed to create isolated container: {e}")
62+
else:
63+
# Use shared container (backward compatibility)
64+
self.base_url = base_url or "http://localhost:8223"
65+
if auto_start:
66+
try:
67+
ContainerManager.ensure_running()
68+
except Exception as e:
69+
raise CodeRunnerError(f"Failed to start CodeRunner: {e}")
70+
71+
self.session_id: Optional[str] = None
5572

5673
def execute(self, code: str, language: str = "python", timeout: int = 30) -> Dict[str, Any]:
5774
"""
@@ -207,6 +224,19 @@ def close_session(self):
207224
finally:
208225
self.session_id = None
209226

227+
def cleanup(self):
228+
"""Cleanup resources including isolated container if created"""
229+
self.close_session()
230+
231+
if self.isolated and self.container_id:
232+
try:
233+
ContainerManager.remove_isolated_container(self.container_id)
234+
logger.info(f"Cleaned up isolated container {self.container_id}")
235+
except Exception as e:
236+
logger.warning(f"Error cleaning up container {self.container_id}: {e}")
237+
finally:
238+
self.container_id = None
239+
210240
def is_session_active(self) -> bool:
211241
"""
212242
Check if current session is active (InstaVM compatible)
@@ -331,14 +361,39 @@ def stop_container(self) -> bool:
331361

332362
def get_container_status(self) -> Dict[str, Any]:
333363
"""Get container status information"""
334-
return ContainerManager.get_container_status()
364+
if self.isolated and self.container_id:
365+
# For isolated containers, get specific status
366+
containers = ContainerManager.list_isolated_containers()
367+
container_info = containers.get(self.container_id, {})
368+
return {
369+
"isolated": True,
370+
"container_id": self.container_id,
371+
"base_url": self.base_url,
372+
**container_info
373+
}
374+
else:
375+
# For shared container, get standard status
376+
status = ContainerManager.get_container_status()
377+
status["isolated"] = False
378+
return status
379+
380+
def is_isolated(self) -> bool:
381+
"""Check if this instance uses an isolated container"""
382+
return self.isolated
383+
384+
def get_container_id(self) -> Optional[str]:
385+
"""Get the container ID (for isolated containers)"""
386+
return self.container_id
335387

336388
# Context manager support (InstaVM compatible)
337389
def __enter__(self):
338390
return self
339391

340392
def __exit__(self, exc_type, exc_val, exc_tb):
341-
self.close_session()
393+
if self.isolated:
394+
self.cleanup() # Cleanup isolated container
395+
else:
396+
self.close_session() # Just close session for shared container
342397

343398

344399
# Import cloud migration support

container_manager.py

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import os
77
import platform
88
import logging
9+
import uuid
10+
import socket
911
from pathlib import Path
10-
from typing import Optional
12+
from typing import Optional, Dict, Tuple
1113

1214
logger = logging.getLogger(__name__)
1315

@@ -332,4 +334,159 @@ def get_container_status(cls) -> dict:
332334
"healthy": False,
333335
"error": str(e),
334336
"docker_available": cls.check_docker()
335-
}
337+
}
338+
339+
# Isolated container methods for fresh instances
340+
341+
@classmethod
342+
def create_isolated_container(cls) -> Tuple[str, Dict[str, int]]:
343+
"""
344+
Create a fresh isolated container with unique ports
345+
346+
Returns:
347+
Tuple of (container_id, port_mapping)
348+
349+
Raises:
350+
RuntimeError: If container creation fails
351+
"""
352+
if not cls.check_docker():
353+
raise RuntimeError("Docker not available")
354+
355+
# Generate unique container name
356+
container_name = f"coderunner-{uuid.uuid4().hex[:8]}"
357+
358+
# Find available ports
359+
ports = cls._find_available_ports()
360+
361+
try:
362+
# Pull image if needed
363+
cls._pull_image_if_needed()
364+
365+
# Create isolated container
366+
result = subprocess.run([
367+
"docker", "run", "-d",
368+
"--name", container_name,
369+
"-p", f"{ports['rest']}:8223",
370+
"-p", f"{ports['mcp']}:8222",
371+
"-p", f"{ports['jupyter']}:8888",
372+
"-p", f"{ports['playwright']}:3000",
373+
"--rm", # Auto-remove when stopped
374+
cls.DOCKER_IMAGE
375+
], capture_output=True, text=True, check=True)
376+
377+
container_id = result.stdout.strip()
378+
379+
# Wait for container to be healthy
380+
cls._wait_for_isolated_health(ports['rest'])
381+
382+
logger.info(f"Created isolated container {container_id} ({container_name})")
383+
return container_id, ports
384+
385+
except subprocess.CalledProcessError as e:
386+
logger.error(f"Failed to create isolated container: {e}")
387+
raise RuntimeError(f"Failed to create isolated container: {e.stderr}")
388+
389+
@classmethod
390+
def _find_available_ports(cls) -> Dict[str, int]:
391+
"""Find available ports for isolated container"""
392+
def is_port_available(port: int) -> bool:
393+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
394+
try:
395+
s.bind(('localhost', port))
396+
return True
397+
except OSError:
398+
return False
399+
400+
# Start from base ports and find available ones
401+
base_ports = {
402+
'rest': 8223,
403+
'mcp': 8222,
404+
'jupyter': 8888,
405+
'playwright': 3000
406+
}
407+
408+
ports = {}
409+
for service, base_port in base_ports.items():
410+
# Try ports starting from base + 100 to avoid conflicts
411+
for port in range(base_port + 100, base_port + 200):
412+
if is_port_available(port):
413+
ports[service] = port
414+
break
415+
else:
416+
raise RuntimeError(f"No available port found for {service}")
417+
418+
return ports
419+
420+
@classmethod
421+
def _wait_for_isolated_health(cls, rest_port: int, timeout: int = 120):
422+
"""Wait for isolated container to be healthy"""
423+
start_time = time.time()
424+
425+
while time.time() - start_time < timeout:
426+
try:
427+
response = requests.get(
428+
f"http://localhost:{rest_port}/health",
429+
timeout=3
430+
)
431+
if response.status_code == 200:
432+
return
433+
except:
434+
pass
435+
436+
time.sleep(2)
437+
438+
raise TimeoutError(f"Isolated container on port {rest_port} failed to become healthy")
439+
440+
@classmethod
441+
def remove_isolated_container(cls, container_id: str) -> bool:
442+
"""
443+
Remove isolated container
444+
445+
Args:
446+
container_id: Container ID to remove
447+
448+
Returns:
449+
True if removed successfully
450+
"""
451+
try:
452+
# Stop container (will auto-remove due to --rm flag)
453+
subprocess.run(
454+
["docker", "stop", container_id],
455+
capture_output=True,
456+
check=True
457+
)
458+
logger.info(f"Removed isolated container {container_id}")
459+
return True
460+
461+
except subprocess.CalledProcessError as e:
462+
logger.warning(f"Failed to remove container {container_id}: {e}")
463+
return False
464+
465+
@classmethod
466+
def list_isolated_containers(cls) -> Dict[str, Dict]:
467+
"""List all isolated CodeRunner containers"""
468+
try:
469+
result = subprocess.run([
470+
"docker", "ps", "-a",
471+
"--filter", "name=coderunner-",
472+
"--format", "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"
473+
], capture_output=True, text=True, check=True)
474+
475+
containers = {}
476+
lines = result.stdout.strip().split('\n')[1:] # Skip header
477+
478+
for line in lines:
479+
if line.strip():
480+
parts = line.split('\t')
481+
if len(parts) >= 4:
482+
containers[parts[0]] = {
483+
"id": parts[0],
484+
"name": parts[1],
485+
"status": parts[2],
486+
"ports": parts[3]
487+
}
488+
489+
return containers
490+
491+
except subprocess.CalledProcessError:
492+
return {}

0 commit comments

Comments
 (0)