JupyterManager/docs/LIFECYCLE_SPEC.md
saymrwulf d191344eb5 Initial commit: jupyter-hub cross-project Jupyter manager
CLI tool that discovers and manages JupyterLab instances across
multiple projects. Prevents port conflicts, finds orphan processes,
provides unified status and stop-all functionality.

Includes:
- bin/jupyter-hub: the CLI (status, ports, stop-all, orphans, which, config)
- config/defaults.sh: default scan dirs and port range
- docs/LIFECYCLE_SPEC.md: client project requirements
- docs/AGENT_PROMPT.md: prompt for coding agents to align projects
- install.sh: symlink installer to ~/bin
- tests/test_jupyter_hub.sh: 15 functional tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 09:07:43 +02:00

166 lines
4.5 KiB
Markdown

# Jupyter Lifecycle Specification for Client Projects
**Version:** 1.0
**Purpose:** Ensure all Jupyter-based projects use aligned lifecycle management, preventing port conflicts, orphan processes, and cross-project interference.
---
## The Problem
Multiple Jupyter-based projects coexist on the same machine. Without alignment:
- Projects fight over port 8888
- Killing a terminal leaves orphan Jupyter processes
- Orphan processes block ports for other projects
- Jupyter config/runtime state leaks between projects
## Required Conventions
### 1. Isolated Jupyter Directories
Every project MUST isolate its Jupyter state into project-local directories. These directories prevent cross-project config leakage and make cleanup deterministic.
```bash
# These environment variables MUST be set before any jupyter command
export JUPYTER_CONFIG_DIR="$PROJECT_ROOT/.jupyter_config"
export JUPYTER_DATA_DIR="$PROJECT_ROOT/.jupyter_data"
export JUPYTER_RUNTIME_DIR="$PROJECT_ROOT/.jupyter_runtime"
export IPYTHONDIR="$PROJECT_ROOT/.ipython"
export MPLCONFIGDIR="$PROJECT_ROOT/.cache/matplotlib"
```
These directories MUST be listed in `.gitignore`.
### 2. Kernel Registration
Kernels MUST be installed into the venv (`--sys-prefix`), NOT into the user's global kernel directory (`--user`). This avoids kernel name collisions between projects.
```bash
python -m ipykernel install \
--sys-prefix \
--name "$PROJECT_NAME" \
--display-name "$DISPLAY_NAME" \
--env IPYTHONDIR "$PROJECT_ROOT/.ipython" \
--env MPLCONFIGDIR "$PROJECT_ROOT/.cache/matplotlib"
```
### 3. app.sh Interface Contract
Every project MUST provide `scripts/app.sh` with at minimum these subcommands:
| Command | Behavior |
|---------|----------|
| `bootstrap` | Create venv, install deps, register kernel, create isolation dirs |
| `start` | Launch JupyterLab in background, write PID file, auto-find free port |
| `start --foreground` | Launch in foreground (Ctrl-C stops cleanly) |
| `start --no-open` | Launch without opening browser |
| `start --port PORT` | Use a specific port |
| `stop` | Send SIGTERM to PID, wait for graceful shutdown, clean PID file |
| `restart` | stop + start |
| `status` | Show venv, server, port, URL, orphan detection |
| `logs [-f]` | Show or follow log output |
### 4. PID File Location
PID files MUST be at `$PROJECT_ROOT/.logs/jupyter.pid` (preferred) or `$PROJECT_ROOT/.run/jupyter.pid` (acceptable). The cross-project manager (`jupyter-hub`) checks both locations.
### 5. Port Allocation
Projects MUST auto-detect free ports, not hardcode 8888:
```bash
local port=8888
while lsof -i :"$port" &>/dev/null; do
port=$((port + 1))
if (( port > 8899 )); then
echo "No free port in range 8888-8899"
exit 1
fi
done
```
### 6. Background Process Management
When starting in background mode:
- Use `nohup` so the process survives shell close
- Write PID to the PID file immediately after `nohup ... &`
- Poll until the server responds (check port or API endpoint)
- Detect early exit (process dies during startup)
```bash
nohup "$JUPYTER" lab \
--port="$port" \
--no-browser \
--ip=127.0.0.1 \
--notebook-dir="$PROJECT_ROOT/notebooks" \
> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
# Wait for startup
while ! curl -s "http://localhost:$port/api" &>/dev/null; do
sleep 0.5
if ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "Server died during startup"
exit 1
fi
done
```
### 7. Stop Protocol
```bash
pid=$(cat "$PID_FILE")
# 1. Send SIGTERM (graceful)
kill -TERM "$pid"
# 2. Wait up to 10 seconds for graceful shutdown
for i in $(seq 1 20); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.5
done
# 3. If still alive, SIGKILL
if kill -0 "$pid" 2>/dev/null; then
kill -KILL "$pid"
fi
# 4. Clean up
rm -f "$PID_FILE"
```
### 8. Orphan Detection
The `status` command SHOULD detect orphan Jupyter processes belonging to this project but not tracked by the PID file:
```bash
ps aux | grep "[j]upyter.*--notebook-dir.*$PROJECT_ROOT" | awk '{print $2}'
```
### 9. Stale Runtime Cleanup
On every `start` and `status`, clean up stale Jupyter runtime JSON files (where the PID is no longer alive):
```bash
for json_file in "$JUPYTER_RUNTIME_DIR"/jpserver-*.json; do
pid=$(python3 -c "import json; print(json.load(open('$json_file')).get('pid',''))")
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$json_file"
fi
done
```
### 10. .gitignore Entries
```gitignore
# Jupyter isolation directories
.jupyter_config/
.jupyter_data/
.jupyter_runtime/
.ipython/
.cache/
# Process management
.logs/
.run/
```