Skip to content

Commit ab82a5e

Browse files
fix(tinytorch): tito module start/resume/view leak untracked Jupyter Lab processes
Bug Every tito module start (without --no-jupyter), resume, or view call launches a brand new jupyter lab subprocess.Popen with no PID tracking, no cleanup, and no check for whether one is already running. Over a normal working session where a student opens several modules, these accumulate indefinitely: confirmed 45 leaked processes after routine module-by-module testing, and separately confirmed that a build-up of ~38 live kernels was enough to make the Jupyter Lab UI itself sluggish (menu clicks timing out). Fix Track the launched Jupyter Lab process's PID in .tito/jupyter.pid. Before launching a new one, check whether that PID is still alive and is actually still a jupyter process (guards against a recycled PID being mistaken for a live server). If so, reuse it: tell the student which notebook to open in their existing tab instead of spawning another server. Only launch a new process if no tracked server is currently running. Testing Verified against a fresh dev install with all 20 modules completed: - First tito module view 01: launches Jupyter Lab as before, records its PID. - Second tito module view 01 (simulating a student opening another module in the same session): before the fix, this would spawn a second full Jupyter Lab server. After the fix, detects the existing PID, prints "Jupyter Lab is already running (pid N)" with the notebook to open, and spawns nothing. Confirmed via process listing that the jupyter.exe count stayed at 1 across both calls.
1 parent efefaf9 commit ab82a5e

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

tinytorch/tito/commands/module/workflow.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,40 @@ def resume_module(self, module_number: Optional[str] = None) -> int:
501501

502502
return self._open_jupyter(module_name)
503503

504+
def _jupyter_pid_file(self) -> Path:
505+
return self.config.project_root / ".tito" / "jupyter.pid"
506+
507+
def _running_jupyter_pid(self) -> Optional[int]:
508+
"""Return the PID of a tito-launched Jupyter Lab server still running, if any."""
509+
pid_file = self._jupyter_pid_file()
510+
if not pid_file.exists():
511+
return None
512+
try:
513+
pid = int(pid_file.read_text().strip())
514+
except (ValueError, OSError):
515+
return None
516+
517+
try:
518+
import psutil
519+
if not psutil.pid_exists(pid):
520+
return None
521+
proc = psutil.Process(pid)
522+
cmdline = " ".join(proc.cmdline()).lower()
523+
if "jupyter" not in cmdline:
524+
# PID was recycled by an unrelated process since we last launched
525+
return None
526+
return pid
527+
except ImportError:
528+
# No psutil available - fall back to os-level existence check only,
529+
# which can't verify it's still actually a Jupyter process.
530+
try:
531+
os.kill(pid, 0)
532+
return pid
533+
except OSError:
534+
return None
535+
except Exception:
536+
return None
537+
504538
def _open_jupyter(self, module_name: str) -> int:
505539
"""Open Jupyter Lab for a module."""
506540
import time
@@ -524,6 +558,20 @@ def _open_jupyter(self, module_name: str) -> int:
524558
else:
525559
notebook_path = None
526560

561+
# If a tito-launched Jupyter Lab server is already running, reuse it
562+
# instead of spawning another one. Without this, every tito module
563+
# start/resume/view call launches its own untracked jupyter lab
564+
# process that nothing ever stops, and they accumulate for the rest
565+
# of the session.
566+
existing_pid = self._running_jupyter_pid()
567+
if existing_pid is not None:
568+
self.console.print(f"\n[cyan]Jupyter Lab is already running (pid {existing_pid}).[/cyan]")
569+
if notebook_path:
570+
self.console.print(f"[dim]Open {notebook_path.name} from the existing Jupyter Lab tab in your browser.[/dim]")
571+
else:
572+
self.console.print("[dim]Switch to your existing Jupyter Lab browser tab.[/dim]")
573+
return 0
574+
527575
self.console.print(f"\n[cyan]🚀 Opening Jupyter Lab for module {module_name}...[/cyan]")
528576

529577
# Launch Jupyter Lab with the notebook file directly
@@ -541,6 +589,10 @@ def _open_jupyter(self, module_name: str) -> int:
541589
errors="replace"
542590
)
543591

592+
pid_file = self._jupyter_pid_file()
593+
pid_file.parent.mkdir(parents=True, exist_ok=True)
594+
pid_file.write_text(str(process.pid))
595+
544596
# Give Jupyter a moment to start and capture the URL
545597
time.sleep(2)
546598

0 commit comments

Comments
 (0)