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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-24 12:05:51 -07:00
parent 3a443a0d09
commit 4ed940d75c
3 changed files with 17 additions and 57 deletions

View File

@ -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"])

View File

@ -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

View File

@ -61,7 +61,7 @@ async function bridgeState(): Promise<any> {
}
}
/** Start Scribus if not running, verify runner is connected. */
/** Verify the bridge runner is connected and ready. */
async function ensureScribusRunning(): Promise<any> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (BRIDGE_SECRET) headers["X-Bridge-Secret"] = BRIDGE_SECRET;
@ -69,28 +69,10 @@ async function ensureScribusRunning(): Promise<any> {
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}` };
}