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 json
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
|
|
@ -23,11 +22,7 @@ app = Flask(__name__)
|
||||||
|
|
||||||
BRIDGE_SECRET = os.environ.get("BRIDGE_SECRET", "")
|
BRIDGE_SECRET = os.environ.get("BRIDGE_SECRET", "")
|
||||||
SOCKET_PATH = "/tmp/scribus_bridge.sock"
|
SOCKET_PATH = "/tmp/scribus_bridge.sock"
|
||||||
SCRIBUS_RUNNER = "/opt/bridge/scribus_runner.py"
|
|
||||||
DESIGNS_DIR = Path("/data/designs")
|
DESIGNS_DIR = Path("/data/designs")
|
||||||
DISPLAY = os.environ.get("DISPLAY", ":1")
|
|
||||||
|
|
||||||
_scribus_proc = None
|
|
||||||
|
|
||||||
|
|
||||||
def _check_auth():
|
def _check_auth():
|
||||||
|
|
@ -81,50 +76,24 @@ def before_request():
|
||||||
@app.route("/health", methods=["GET"])
|
@app.route("/health", methods=["GET"])
|
||||||
def health():
|
def health():
|
||||||
runner_alive = os.path.exists(SOCKET_PATH)
|
runner_alive = os.path.exists(SOCKET_PATH)
|
||||||
scribus_alive = _scribus_proc is not None and _scribus_proc.poll() is None
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"service": "scribus-bridge",
|
"service": "scribus-bridge",
|
||||||
"runner_connected": runner_alive,
|
"runner_connected": runner_alive,
|
||||||
"scribus_alive": scribus_alive,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/scribus/start", methods=["POST"])
|
@app.route("/api/scribus/start", methods=["POST"])
|
||||||
def start_scribus():
|
def start_scribus():
|
||||||
"""Launch Scribus with the bridge runner script."""
|
"""Verify runner socket is available. Optionally launch Scribus GUI for real rendering."""
|
||||||
global _scribus_proc
|
# The runner process is managed by supervisor and should already be listening.
|
||||||
|
# Wait briefly for socket if it's still starting up.
|
||||||
if _scribus_proc and _scribus_proc.poll() is None:
|
for _ in range(10):
|
||||||
if os.path.exists(SOCKET_PATH):
|
if os.path.exists(SOCKET_PATH):
|
||||||
return jsonify({"ok": True, "message": "Scribus already running", "pid": _scribus_proc.pid})
|
return jsonify({"ok": True, "message": "Runner connected", "runner_connected": True})
|
||||||
# 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})
|
|
||||||
time.sleep(0.5)
|
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"])
|
@app.route("/api/scribus/command", methods=["POST"])
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ autorestart=true
|
||||||
priority=30
|
priority=30
|
||||||
startsecs=5
|
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]
|
[program:bridge]
|
||||||
command=python3 /opt/bridge/server.py
|
command=python3 /opt/bridge/server.py
|
||||||
autorestart=true
|
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> {
|
async function ensureScribusRunning(): Promise<any> {
|
||||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||||
if (BRIDGE_SECRET) headers["X-Bridge-Secret"] = BRIDGE_SECRET;
|
if (BRIDGE_SECRET) headers["X-Bridge-Secret"] = BRIDGE_SECRET;
|
||||||
|
|
@ -69,28 +69,10 @@ async function ensureScribusRunning(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${SCRIBUS_BRIDGE_URL}/api/scribus/start`, {
|
const res = await fetch(`${SCRIBUS_BRIDGE_URL}/api/scribus/start`, {
|
||||||
method: "POST",
|
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,
|
headers,
|
||||||
signal: AbortSignal.timeout(10_000),
|
signal: AbortSignal.timeout(10_000),
|
||||||
});
|
});
|
||||||
const state = await stateRes.json();
|
return await res.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();
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { error: `Bridge unreachable: ${e.message}` };
|
return { error: `Bridge unreachable: ${e.message}` };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue