11import docker
22from loguru import logger
33from pathlib import Path
4- from typing import Dict , List , Optional , Any
4+ from typing import Dict , List , Optional , Any , Literal
55import time
66from docker .errors import APIError , ImageNotFound
77import asyncio
@@ -35,14 +35,29 @@ class DockerExecutor:
3535
3636 WORK_DIR = "/mnt/data" # Working directory will be the same as data mount point
3737 DATA_MOUNT = "/mnt/data" # Mount point for session data
38- MAX_CONCURRENT_CONTAINERS = 10
38+
39+ # Language-specific execution commands
40+ LANGUAGE_EXECUTORS = {
41+ "py" : ["python" , "-c" ],
42+ "r" : ["Rscript" , "-e" ],
43+ }
44+
45+ # Language-specific messages
46+ LANGUAGE_SPECIFIC_MESSAGES = {
47+ "py" : {
48+ "empty_output" : "Empty. Make sure to explicitly print() the results in Python"
49+ },
50+ "r" : {
51+ "empty_output" : "Empty. Make sure to use print() or cat() to display results in R"
52+ }
53+ }
3954
4055 def __init__ (self ):
41- self ._container_semaphore = asyncio .Semaphore (self .MAX_CONCURRENT_CONTAINERS )
56+ self ._container_semaphore = asyncio .Semaphore (settings .MAX_CONCURRENT_CONTAINERS )
4257 self ._active_containers : Dict [str , ContainerMetrics ] = {}
4358 self ._lock = asyncio .Lock ()
4459 self ._docker = None # Will be initialized in initialize()
45- self ._image_pull_locks : Dict [str , asyncio .Lock ] = {} # Locks for image pulling
60+ self ._image_pull_locks : Dict [str , asyncio .Lock ] = {}
4661
4762 async def initialize (self ):
4863 """Initialize the Docker client."""
@@ -55,7 +70,7 @@ async def initialize(self):
5570 # Reinitialize if there was an error
5671 await self .close ()
5772 self ._docker = aiodocker .Docker ()
58-
73+
5974 logger .info ("Docker client initialized successfully" )
6075 return self
6176 except Exception as e :
@@ -155,7 +170,12 @@ def _clean_output(self, raw_output: bytes) -> str:
155170 return b"" .join (output_parts ).decode ("utf-8" ).strip ()
156171
157172 async def execute (
158- self , code : str , session_id : str , files : Optional [List [Dict [str , Any ]]] = None , timeout : int = 30
173+ self ,
174+ code : str ,
175+ session_id : str ,
176+ lang : Literal ["py" , "r" ],
177+ files : Optional [List [Dict [str , Any ]]] = None ,
178+ timeout : int = 30 ,
159179 ) -> Dict [str , Any ]:
160180 """Execute code in a Docker container with file management."""
161181 container = None
@@ -183,9 +203,9 @@ async def execute(
183203 async with self ._container_semaphore :
184204 try :
185205 # Ensure the image is available
186- image_name = settings .PYTHON_CONTAINER_IMAGE
206+ image_name = settings .LANGUAGE_CONTAINERS . get ( lang )
187207 logger .info (f"Using container image: { image_name } " )
188-
208+
189209 try :
190210 # Check if image exists
191211 await self ._docker .images .inspect (image_name )
@@ -196,15 +216,18 @@ async def execute(
196216 # Get or create a lock for this specific image
197217 if image_name not in self ._image_pull_locks :
198218 self ._image_pull_locks [image_name ] = asyncio .Lock ()
199-
219+
200220 # Acquire the lock for this image to prevent multiple pulls
201221 async with self ._image_pull_locks [image_name ]:
202222 # Check again if the image exists (another request might have pulled it while we were waiting)
203223 try :
204224 await self ._docker .images .inspect (image_name )
205225 logger .info (f"Image { image_name } is now available (pulled by another request)" )
206226 except Exception as check_again_error :
207- if isinstance (check_again_error , aiodocker .exceptions .DockerError ) and check_again_error .status == 404 :
227+ if (
228+ isinstance (check_again_error , aiodocker .exceptions .DockerError )
229+ and check_again_error .status == 404
230+ ):
208231 # Pull the image if not available
209232 logger .info (f"Image { image_name } not found, pulling..." )
210233 try :
@@ -227,7 +250,7 @@ async def execute(
227250 # Re-raise if it's not a 404 error
228251 logger .error (f"Error checking for image { image_name } : { str (e )} " )
229252 raise
230-
253+
231254 # Create container config
232255 config = {
233256 "Image" : image_name ,
@@ -284,8 +307,16 @@ async def execute(
284307 output = await response .read ()
285308 output_text = self ._clean_output (output )
286309
287- # Execute the Python code as jovyan user
288- exec = await container .exec (cmd = ["python" , "-c" , code ], user = "jovyan" , stdout = True , stderr = True )
310+ # Execute the code with the appropriate interpreter
311+ logger .info (f"Code to execute: { code } " )
312+ logger .info (f"Language: { lang } " )
313+
314+ # Get the execution command for the specified language
315+ exec_cmd = self .LANGUAGE_EXECUTORS .get (lang , self .LANGUAGE_EXECUTORS ["py" ])
316+ logger .info (f"Using execution command: { exec_cmd } " )
317+
318+ # Execute the code with the appropriate interpreter
319+ exec = await container .exec (cmd = [* exec_cmd , code ], user = "jovyan" , stdout = True , stderr = True )
289320 # Use raw API call to get output
290321 exec_url = f"exec/{ exec ._id } /start"
291322 async with self ._docker ._query (
0 commit comments