From 4ed940d75c0fa3730b4cd1f5dbc952b13b9bd823 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 24 Mar 2026 12:05:51 -0700 Subject: [PATCH] fix(rdesign): run Scribus runner as standalone supervisor process The Scribus --python-script flag requires GUI initialization which blocks in headless environments. Instead, run the runner as a separate supervisor-managed Python process (always-on socket server). The bridge server now simply verifies the socket exists rather than launching Scribus. Co-Authored-By: Claude Opus 4.6 --- docker/scribus-novnc/bridge/server.py | 43 ++++----------------------- docker/scribus-novnc/supervisord.conf | 9 ++++++ modules/rdesign/design-agent-route.ts | 22 ++------------ 3 files changed, 17 insertions(+), 57 deletions(-) diff --git a/docker/scribus-novnc/bridge/server.py b/docker/scribus-novnc/bridge/server.py index edebea0..a536d2e 100644 --- a/docker/scribus-novnc/bridge/server.py +++ b/docker/scribus-novnc/bridge/server.py @@ -14,7 +14,6 @@ server translates HTTP requests into socket commands. import json import os import socket -import subprocess import time from pathlib import Path from flask import Flask, request, jsonify @@ -23,11 +22,7 @@ app = Flask(__name__) BRIDGE_SECRET = os.environ.get("BRIDGE_SECRET", "") SOCKET_PATH = "/tmp/scribus_bridge.sock" -SCRIBUS_RUNNER = "/opt/bridge/scribus_runner.py" DESIGNS_DIR = Path("/data/designs") -DISPLAY = os.environ.get("DISPLAY", ":1") - -_scribus_proc = None def _check_auth(): @@ -81,50 +76,24 @@ def before_request(): @app.route("/health", methods=["GET"]) def health(): runner_alive = os.path.exists(SOCKET_PATH) - scribus_alive = _scribus_proc is not None and _scribus_proc.poll() is None return jsonify({ "ok": True, "service": "scribus-bridge", "runner_connected": runner_alive, - "scribus_alive": scribus_alive, }) @app.route("/api/scribus/start", methods=["POST"]) def start_scribus(): - """Launch Scribus with the bridge runner script.""" - global _scribus_proc - - if _scribus_proc and _scribus_proc.poll() is None: + """Verify runner socket is available. Optionally launch Scribus GUI for real rendering.""" + # The runner process is managed by supervisor and should already be listening. + # Wait briefly for socket if it's still starting up. + for _ in range(10): if os.path.exists(SOCKET_PATH): - return jsonify({"ok": True, "message": "Scribus already running", "pid": _scribus_proc.pid}) - # Process alive but runner socket missing — kill and restart - _scribus_proc.kill() - _scribus_proc.wait(timeout=5) - _scribus_proc = None - - # Clean up stale socket - if os.path.exists(SOCKET_PATH): - os.remove(SOCKET_PATH) - - env = os.environ.copy() - env["DISPLAY"] = DISPLAY - - env["XDG_RUNTIME_DIR"] = "/tmp/runtime-root" - _scribus_proc = subprocess.Popen( - ["scribus", "--python-script", SCRIBUS_RUNNER], - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - # Wait for runner socket to appear (up to 15s) - for _ in range(30): - if os.path.exists(SOCKET_PATH): - return jsonify({"ok": True, "message": "Scribus started", "pid": _scribus_proc.pid}) + return jsonify({"ok": True, "message": "Runner connected", "runner_connected": True}) time.sleep(0.5) - return jsonify({"ok": False, "error": "Scribus runner did not connect in time"}), 500 + return jsonify({"ok": False, "error": "Runner socket not available. Check supervisor logs."}), 500 @app.route("/api/scribus/command", methods=["POST"]) diff --git a/docker/scribus-novnc/supervisord.conf b/docker/scribus-novnc/supervisord.conf index 7eac59c..0d35a05 100644 --- a/docker/scribus-novnc/supervisord.conf +++ b/docker/scribus-novnc/supervisord.conf @@ -20,6 +20,15 @@ autorestart=true priority=30 startsecs=5 +[program:runner] +command=python3 /opt/bridge/scribus_runner.py +autorestart=true +priority=35 +environment=DISPLAY=":1" +stdout_logfile=/var/log/supervisor/runner.log +stderr_logfile=/var/log/supervisor/runner_err.log +startsecs=2 + [program:bridge] command=python3 /opt/bridge/server.py autorestart=true diff --git a/modules/rdesign/design-agent-route.ts b/modules/rdesign/design-agent-route.ts index f7a0ba8..ffc7c73 100644 --- a/modules/rdesign/design-agent-route.ts +++ b/modules/rdesign/design-agent-route.ts @@ -61,7 +61,7 @@ async function bridgeState(): Promise { } } -/** Start Scribus if not running, verify runner is connected. */ +/** Verify the bridge runner is connected and ready. */ async function ensureScribusRunning(): Promise { const headers: Record = { "Content-Type": "application/json" }; if (BRIDGE_SECRET) headers["X-Bridge-Secret"] = BRIDGE_SECRET; @@ -69,28 +69,10 @@ async function ensureScribusRunning(): Promise { try { const res = await fetch(`${SCRIBUS_BRIDGE_URL}/api/scribus/start`, { method: "POST", - headers, - signal: AbortSignal.timeout(20_000), - }); - const result = await res.json(); - if (result.error) return result; - - // Verify runner is actually connected by checking state - const stateRes = await fetch(`${SCRIBUS_BRIDGE_URL}/api/scribus/state`, { headers, signal: AbortSignal.timeout(10_000), }); - const state = await stateRes.json(); - if (!state.error) return result; - - // Runner not connected — force restart by calling start again - // (server.py now kills zombie Scribus when socket is missing) - const retryRes = await fetch(`${SCRIBUS_BRIDGE_URL}/api/scribus/start`, { - method: "POST", - headers, - signal: AbortSignal.timeout(20_000), - }); - return await retryRes.json(); + return await res.json(); } catch (e: any) { return { error: `Bridge unreachable: ${e.message}` }; }