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:
parent
3a443a0d09
commit
4ed940d75c
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}` };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue