-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Description
Summary
JupyterCodeExecutor creates a temp directory via tempfile.mkdtemp() when no output_dir is provided, but never cleans it up in stop(). This is inconsistent with every other executor in the codebase (LocalCommandLineCodeExecutor, DockerCommandLineCodeExecutor, AzureContainerCodeExecutor) which all properly clean up their temp directories. Over time, leaked directories and their contents (generated images, HTML files) accumulate on disk.
Details
When a user creates a JupyterCodeExecutor without specifying output_dir, the constructor creates a temp directory using the low-level tempfile.mkdtemp():
# _jupyter_code_executor.py, line 148
self._output_dir: Path = Path(tempfile.mkdtemp()) if output_dir is None else Path(output_dir)The class even declares self._temp_dir: Optional[tempfile.TemporaryDirectory[str]] = None on line 151, suggesting the intention was to use tempfile.TemporaryDirectory() (which auto-cleans), but it's never actually used.
When stop() is called, only the kernel context is cleaned up — the output directory is left behind:
# _jupyter_code_executor.py, lines 297-309
async def stop(self) -> None:
if not self._started:
return
if self.kernel_context is not None:
await self.kernel_context.__aexit__(None, None, None)
self.kernel_context = None
self._client = None
self._started = False
# ← No cleanup of self._output_dirCompare this with how other executors handle it:
LocalCommandLineCodeExecutor (lines 494-503):
async def stop(self) -> None:
if self._temp_dir is not None:
self._temp_dir.cleanup() # ← Properly cleans up
self._temp_dir = None
self._started = FalseAzureContainerCodeExecutor (line 520):
self._temp_dir.cleanup() # ← Properly cleans upDockerCommandLineCodeExecutor (line 443):
self._temp_dir.cleanup() # ← Properly cleans upEach code execution that produces visual output (matplotlib charts, HTML reports) writes files into the leaked directory. These files persist indefinitely, and over many executor lifecycles, they accumulate.
PoC
import asyncio
from autogen_ext.code_executors.jupyter import JupyterCodeExecutor
from autogen_core.code_executor import CodeBlock
from autogen_core import CancellationToken
async def main():
leaked_dirs = []
for i in range(3):
executor = JupyterCodeExecutor()
leaked_dirs.append(str(executor.output_dir))
await executor.start()
result = await executor.execute_code_blocks(
[CodeBlock(code=f"print('run {i}')", language="python")],
CancellationToken()
)
await executor.stop()
# Verify: all directories still exist after stop()
import os
for d in leaked_dirs:
exists = os.path.exists(d)
print(f"{d} exists after stop(): {exists}")
# Expected: True (BUG - should be cleaned up)
asyncio.run(main())Output:
/tmp/tmpr1ax38gw exists after stop(): True
/tmp/tmp2i11247w exists after stop(): True
/tmp/tmpvt43odlp exists after stop(): True