Spaces:
Running on Zero
Running on Zero
File size: 4,935 Bytes
ba9ed6e 71ea0d0 ba9ed6e 71ea0d0 ba9ed6e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """User-space Node.js runtime bootstrap for the Respite backend.
Runs the existing Next.js backend inside the Gradio Space container without
root: downloads an official self-contained Node tarball to a local cache,
extracts it, and launches the Next.js standalone server as a child process
bound to localhost. The FastAPI layer reverse-proxies /api/* to it.
Env:
NODE_VERSION Node version to fetch (default 22.14.0 LTS)
NEXT_PORT localhost port for the Node server (default 3210)
NEXT_DIR directory of the standalone build (default ./respite-backend)
NODE_PROXY_OFF set to "1" to skip the download/proxy entirely
"""
import os
import shutil
import subprocess
import tarfile
import threading
import urllib.request
BACKEND_DIR = os.environ.get("NEXT_DIR", os.path.join(os.path.dirname(os.path.abspath(__file__)), "respite-backend"))
NODE_VERSION = os.environ.get("NODE_VERSION", "22.14.0")
NODE_PORT = int(os.environ.get("NEXT_PORT", "3210"))
NODE_TARBALL = f"https://nodejs.org/dist/v{NODE_VERSION}/node-v{NODE_VERSION}-linux-x64.tar.xz"
RUNTIME_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".node-runtime")
NODE_BIN = os.path.join(RUNTIME_DIR, f"node-v{NODE_VERSION}-linux-x64", "bin", "node")
_node_proc = None
_node_lock = threading.Lock()
_node_ready = threading.Event()
def _log(msg):
print(f"[node-runtime] {msg}", flush=True)
def ensure_node() -> str:
"""Download + extract Node into the local cache once. Returns node path."""
if os.path.exists(NODE_BIN):
return NODE_BIN
os.makedirs(RUNTIME_DIR, exist_ok=True)
tgz = os.path.join(RUNTIME_DIR, "node.tar.xz")
_log(f"downloading {NODE_TARBALL}")
urllib.request.urlretrieve(NODE_TARBALL, tgz)
_log("extracting")
with tarfile.open(tgz, "r:xz") as tf:
tf.extractall(RUNTIME_DIR)
os.remove(tgz)
os.chmod(NODE_BIN, 0o755)
_log(f"node ready at {NODE_BIN}")
return NODE_BIN
def _wait_ready(proc: subprocess.Popen, timeout: float = 90.0) -> bool:
"""Wait until the Node server answers on its localhost port."""
import socket
import time
deadline = time.time() + timeout
while time.time() < deadline:
if proc.poll() is not None:
_log(f"node exited early with code {proc.returncode}")
return False
try:
with socket.create_connection(("127.0.0.1", NODE_PORT), timeout=1):
return True
except OSError:
time.sleep(0.5)
return False
def _ensure_backend_extracted() -> bool:
"""Extract backend-bundle/respite-backend.tar.xz if the backend dir is absent."""
if os.path.isdir(BACKEND_DIR) and os.path.exists(os.path.join(BACKEND_DIR, "server.js")):
return True
bundle = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backend-bundle", "respite-backend.tar.xz")
if not os.path.exists(bundle):
_log(f"no bundle at {bundle}")
return False
_log(f"extracting {bundle} -> {BACKEND_DIR}")
os.makedirs(BACKEND_DIR, exist_ok=True)
with tarfile.open(bundle, "r:xz") as tf:
tf.extractall(BACKEND_DIR)
_log("backend extracted")
return True
def start_node_backend() -> bool:
"""Launch the Next.js standalone server. Safe to call once at startup."""
global _node_proc
if os.environ.get("NODE_PROXY_OFF") == "1":
_log("disabled via NODE_PROXY_OFF")
return False
if not _ensure_backend_extracted():
return False
with _node_lock:
if _node_proc and _node_proc.poll() is None:
return True
try:
node = ensure_node()
except Exception as e:
_log(f"node bootstrap failed: {e}")
return False
server_js = os.path.join(BACKEND_DIR, "server.js")
if not os.path.exists(server_js):
_log(f"no server.js in {BACKEND_DIR}")
return False
env = os.environ.copy()
env.update({
"PORT": str(NODE_PORT),
"HOSTNAME": "127.0.0.1",
"NODE_ENV": "production",
})
_log(f"starting node server on 127.0.0.1:{NODE_PORT}")
_node_proc = subprocess.Popen(
[node, server_js],
cwd=BACKEND_DIR,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
threading.Thread(target=_pump_logs, daemon=True).start()
ok = _wait_ready(_node_proc)
if ok:
_log("node backend is READY")
_node_ready.set()
else:
_log("node backend failed to become ready")
return ok
def _pump_logs():
assert _node_proc and _node_proc.stdout
for line in _node_proc.stdout:
_log(f"[next] {line.rstrip()}")
def node_ready() -> bool:
return _node_ready.is_set() and _node_proc is not None and _node_proc.poll() is None
|