Spaces:
Running
Running
feat: add JupyterLab terminal at /terminal/ (DEV_MODE=true)
Browse filesInstall JupyterLab in the venv. When DEV_MODE=true and JUPYTER_TOKEN
is set (strong token required β full shell access), start JupyterLab
on port 8888 with /terminal/ base URL. health-server proxies HTTP
and WebSocket (required for kernels/terminals) to JupyterLab.
Usage: set DEV_MODE=true and JUPYTER_TOKEN=<openssl rand -hex 32>
in HF Space secrets, then visit /terminal/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile +1 -1
- health-server.js +42 -0
- start.sh +45 -0
Dockerfile
CHANGED
|
@@ -30,7 +30,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 30 |
fonts-liberation \
|
| 31 |
fonts-noto-color-emoji \
|
| 32 |
&& rm -rf /var/lib/apt/lists/* \
|
| 33 |
-
&& uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir huggingface_hub hf_transfer
|
| 34 |
|
| 35 |
COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
|
| 36 |
COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
|
|
|
|
| 30 |
fonts-liberation \
|
| 31 |
fonts-noto-color-emoji \
|
| 32 |
&& rm -rf /var/lib/apt/lists/* \
|
| 33 |
+
&& uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir huggingface_hub hf_transfer jupyterlab
|
| 34 |
|
| 35 |
COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
|
| 36 |
COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
|
health-server.js
CHANGED
|
@@ -9,7 +9,9 @@ const PORT = Number(process.env.PORT || 7861);
|
|
| 9 |
const GATEWAY_PORT = Number(process.env.API_SERVER_PORT || 8642);
|
| 10 |
const DASHBOARD_PORT = Number(process.env.DASHBOARD_PORT || 9119);
|
| 11 |
const TELEGRAM_WEBHOOK_PORT = Number(process.env.TELEGRAM_WEBHOOK_PORT || 8765);
|
|
|
|
| 12 |
const GATEWAY_HOST = "127.0.0.1";
|
|
|
|
| 13 |
const startTime = Date.now();
|
| 14 |
const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
|
| 15 |
const APP_BASE = "/app";
|
|
@@ -662,10 +664,50 @@ const server = http.createServer(async (req, res) => {
|
|
| 662 |
return;
|
| 663 |
}
|
| 664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
| 666 |
res.end("Not found");
|
| 667 |
});
|
| 668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
server.listen(PORT, "0.0.0.0", () => {
|
| 670 |
console.log(`HuggingMes dashboard listening on 0.0.0.0:${PORT}`);
|
| 671 |
});
|
|
|
|
| 9 |
const GATEWAY_PORT = Number(process.env.API_SERVER_PORT || 8642);
|
| 10 |
const DASHBOARD_PORT = Number(process.env.DASHBOARD_PORT || 9119);
|
| 11 |
const TELEGRAM_WEBHOOK_PORT = Number(process.env.TELEGRAM_WEBHOOK_PORT || 8765);
|
| 12 |
+
const JUPYTER_PORT = 8888;
|
| 13 |
const GATEWAY_HOST = "127.0.0.1";
|
| 14 |
+
const TERMINAL_BASE = "/terminal";
|
| 15 |
const startTime = Date.now();
|
| 16 |
const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
|
| 17 |
const APP_BASE = "/app";
|
|
|
|
| 664 |
return;
|
| 665 |
}
|
| 666 |
|
| 667 |
+
if (path === TERMINAL_BASE || path.startsWith(`${TERMINAL_BASE}/`)) {
|
| 668 |
+
if (!requireAuth(req, res)) return;
|
| 669 |
+
canConnect(JUPYTER_PORT).then((up) => {
|
| 670 |
+
if (!up) {
|
| 671 |
+
res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
|
| 672 |
+
res.end("JupyterLab is not running. Set DEV_MODE=true and JUPYTER_TOKEN in Space secrets to enable /terminal/.");
|
| 673 |
+
return;
|
| 674 |
+
}
|
| 675 |
+
proxyRequest(req, res, JUPYTER_PORT);
|
| 676 |
+
});
|
| 677 |
+
return;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
| 681 |
res.end("Not found");
|
| 682 |
});
|
| 683 |
|
| 684 |
+
// ββ WebSocket upgrade (JupyterLab terminals + kernels need this) ββ
|
| 685 |
+
server.on("upgrade", (req, socket, head) => {
|
| 686 |
+
const { pathname } = new URL(req.url, "http://localhost");
|
| 687 |
+
const isJupyter = pathname === TERMINAL_BASE || pathname.startsWith(`${TERMINAL_BASE}/`);
|
| 688 |
+
const targetPort = isJupyter ? JUPYTER_PORT : GATEWAY_PORT;
|
| 689 |
+
const ps = net.createConnection(targetPort, GATEWAY_HOST, () => {
|
| 690 |
+
ps.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`);
|
| 691 |
+
ps.write(`Host: ${GATEWAY_HOST}:${targetPort}\r\n`);
|
| 692 |
+
ps.write(`X-Forwarded-Host: ${req.headers.host || ""}\r\n`);
|
| 693 |
+
ps.write("X-Forwarded-Proto: https\r\n");
|
| 694 |
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 695 |
+
const lower = req.rawHeaders[i].toLowerCase();
|
| 696 |
+
if (["host", "x-forwarded-host", "x-forwarded-proto"].includes(lower)) continue;
|
| 697 |
+
ps.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`);
|
| 698 |
+
}
|
| 699 |
+
ps.write("\r\n");
|
| 700 |
+
if (head && head.length) ps.write(head);
|
| 701 |
+
ps.pipe(socket).pipe(ps);
|
| 702 |
+
});
|
| 703 |
+
ps.on("error", () => socket.destroy());
|
| 704 |
+
ps.on("close", () => socket.destroy());
|
| 705 |
+
socket.on("error", () => ps.destroy());
|
| 706 |
+
socket.on("close", () => ps.destroy());
|
| 707 |
+
});
|
| 708 |
+
|
| 709 |
+
server.timeout = 0;
|
| 710 |
+
server.keepAliveTimeout = 65000;
|
| 711 |
server.listen(PORT, "0.0.0.0", () => {
|
| 712 |
console.log(`HuggingMes dashboard listening on 0.0.0.0:${PORT}`);
|
| 713 |
});
|
start.sh
CHANGED
|
@@ -308,6 +308,49 @@ echo "Dashboard : http://127.0.0.1:${DASHBOARD_PORT}"
|
|
| 308 |
echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}"
|
| 309 |
echo ""
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
# ββ Trap SIGTERM for graceful shutdown ββ
|
| 312 |
graceful_shutdown() {
|
| 313 |
echo "Shutting down HuggingMes..."
|
|
@@ -371,6 +414,8 @@ if [ -n "${HF_TOKEN:-}" ]; then
|
|
| 371 |
python3 -u "$APP_DIR/hermes-sync.py" loop &
|
| 372 |
fi
|
| 373 |
|
|
|
|
|
|
|
| 374 |
wait "$GATEWAY_PID"
|
| 375 |
|
| 376 |
# Gateway exited (e.g. user restarted from Hermes UI). Sync before container dies.
|
|
|
|
| 308 |
echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}"
|
| 309 |
echo ""
|
| 310 |
|
| 311 |
+
# ββ JupyterLab terminal (DEV_MODE=true + JUPYTER_TOKEN required) ββ
|
| 312 |
+
start_jupyter() {
|
| 313 |
+
if [ "${DEV_MODE:-false}" != "true" ]; then
|
| 314 |
+
echo "JupyterLab disabled (set DEV_MODE=true to enable /terminal/)."
|
| 315 |
+
return 0
|
| 316 |
+
fi
|
| 317 |
+
if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then
|
| 318 |
+
echo "ERROR: JUPYTER_TOKEN unset or insecure default. JupyterLab grants full shell β set a strong token." >&2
|
| 319 |
+
echo " Hint: openssl rand -hex 32" >&2
|
| 320 |
+
return 1
|
| 321 |
+
fi
|
| 322 |
+
if ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
|
| 323 |
+
echo "WARNING: jupyterlab not installed; skipping terminal." >&2
|
| 324 |
+
return 1
|
| 325 |
+
fi
|
| 326 |
+
local root_dir="${JUPYTER_ROOT_DIR:-$HERMES_HOME/workspace}"
|
| 327 |
+
mkdir -p "$root_dir"
|
| 328 |
+
ln -sfn "$HERMES_HOME" "$root_dir/HuggingMes" 2>/dev/null || true
|
| 329 |
+
echo "Starting JupyterLab terminal on port 8888 (path: /terminal/) root: $root_dir"
|
| 330 |
+
python3 -m jupyterlab \
|
| 331 |
+
--ip 127.0.0.1 \
|
| 332 |
+
--port 8888 \
|
| 333 |
+
--no-browser \
|
| 334 |
+
--IdentityProvider.token="$JUPYTER_TOKEN" \
|
| 335 |
+
--ServerApp.base_url=/terminal/ \
|
| 336 |
+
--ServerApp.terminals_enabled=True \
|
| 337 |
+
--ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \
|
| 338 |
+
--ServerApp.allow_origin='*' \
|
| 339 |
+
--ServerApp.allow_remote_access=True \
|
| 340 |
+
--ServerApp.trust_xheaders=True \
|
| 341 |
+
--ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \
|
| 342 |
+
--IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \
|
| 343 |
+
--ServerApp.disable_check_xsrf=True \
|
| 344 |
+
--LabApp.news_url=None \
|
| 345 |
+
--LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \
|
| 346 |
+
--ServerApp.log_level=WARN \
|
| 347 |
+
--ServerApp.root_dir="$root_dir" \
|
| 348 |
+
>> "$HERMES_HOME/logs/jupyter.log" 2>&1 &
|
| 349 |
+
JUPYTER_PID=$!
|
| 350 |
+
export JUPYTER_PID
|
| 351 |
+
echo "JupyterLab started (PID: $JUPYTER_PID)"
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
# ββ Trap SIGTERM for graceful shutdown ββ
|
| 355 |
graceful_shutdown() {
|
| 356 |
echo "Shutting down HuggingMes..."
|
|
|
|
| 414 |
python3 -u "$APP_DIR/hermes-sync.py" loop &
|
| 415 |
fi
|
| 416 |
|
| 417 |
+
start_jupyter
|
| 418 |
+
|
| 419 |
wait "$GATEWAY_PID"
|
| 420 |
|
| 421 |
# Gateway exited (e.g. user restarted from Hermes UI). Sync before container dies.
|