Spaces:
Running on Zero
Running on Zero
File size: 6,588 Bytes
42f6abb 79019c5 42f6abb 1df1e19 accae83 363d9ef 42f6abb 363d9ef 42f6abb 363d9ef 42f6abb 363d9ef 42f6abb 79019c5 42f6abb 79019c5 42f6abb 79019c5 42f6abb 79019c5 42f6abb 79019c5 | 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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """User-space SearXNG bootstrap for the Respite search backend.
SearXNG is not on PyPI (the `searxng` package there is the abandoned
pre-NG searx). Instead we fetch the upstream source tarball into a local
cache, install it into a userspace virtualenv on first boot (no root),
and launch the built-in development server bound to 127.0.0.1:8888.
The instance is configured by searxng_settings.yml in the repo root
(SEARXNG_SETTINGS_PATH points there), with the DDG + Google engines
enabled and JSON output on.
Env:
SEARXNG_VERSION source tag/branch to fetch (default: master)
SEARXNG_PORT localhost port (default 8888)
SEARXNG_OFF set to "1" to skip the bootstrap entirely
"""
import os
import shutil
import subprocess
import tarfile
import threading
import time
import urllib.request
SEARXNG_VERSION = os.environ.get("SEARXNG_VERSION", "master")
SEARXNG_PORT = int(os.environ.get("SEARXNG_PORT", "8888"))
SEARXNG_URL = f"https://github.com/searxng/searxng/archive/refs/heads/{SEARXNG_VERSION}.tar.gz"
ROOT = os.path.dirname(os.path.abspath(__file__))
RUNTIME_DIR = os.path.join(ROOT, ".searxng-runtime")
SRC_DIR = os.path.join(RUNTIME_DIR, f"searxng-{SEARXNG_VERSION}")
VENV_DIR = os.path.join(RUNTIME_DIR, "venv")
VENV_PIP = os.path.join(VENV_DIR, "bin", "pip")
SETTINGS_FILE = os.path.join(ROOT, "searxng_settings.yml")
_proc = None
_lock = threading.Lock()
_ready = threading.Event()
_boot_error = ""
def _log(msg):
print(f"[searxng] {msg}", flush=True)
def _fetch_source() -> bool:
if os.path.isdir(SRC_DIR) and os.path.exists(os.path.join(SRC_DIR, "searx", "webapp.py")):
return True
os.makedirs(RUNTIME_DIR, exist_ok=True)
tgz = os.path.join(RUNTIME_DIR, "searxng.tar.gz")
_log(f"downloading {SEARXNG_URL}")
urllib.request.urlretrieve(SEARXNG_URL, tgz)
_log("extracting")
with tarfile.open(tgz, "r:gz") as tf:
tf.extractall(RUNTIME_DIR)
os.remove(tgz)
return os.path.isdir(SRC_DIR)
def _venv_python() -> str | None:
"""Create the venv and install searxng + deps (idempotent)."""
py = os.path.join(VENV_DIR, "bin", "python")
if os.path.exists(py):
# A stale venv from a different interpreter breaks imports; recreate.
r = subprocess.run([py, "-c", "import tomllib"], capture_output=True)
if r.returncode != 0:
_log("stale venv (python<3.11) — recreating")
shutil.rmtree(VENV_DIR, ignore_errors=True)
else:
return py
# Prefer the newest interpreter available (SearXNG master needs 3.11+;
# the container base image ships 3.10).
base = None
for cand in ("python3.13", "python3.12", "python3.11", "python3"):
if shutil.which(cand):
base = cand
break
_log(f"creating venv with {base}")
if not base:
return None
# --without-pip: many slim images ship the interpreter without the
# venv/ensurepip pieces; get-pip.py bootstraps pip inside afterwards.
r = subprocess.run([base, "-m", "venv", "--without-pip", VENV_DIR], capture_output=True)
if r.returncode != 0:
r = subprocess.run([base, "-m", "venv", VENV_DIR], capture_output=True)
if r.returncode != 0:
_log(f"venv creation failed: {r.stderr[-500:]}")
return None
vpy = os.path.join(VENV_DIR, "bin", "python")
if not os.path.exists(os.path.join(VENV_DIR, "bin", "pip")):
_log("bootstrapping pip via get-pip.py")
gp = os.path.join(RUNTIME_DIR, "get-pip.py")
urllib.request.urlretrieve("https://bootstrap.pypa.io/get-pip.py", gp)
if subprocess.run([vpy, gp], capture_output=True).returncode != 0:
_log("get-pip failed")
return None
_log("installing searxng deps (this takes a few minutes on first boot)")
r1 = subprocess.run(
[vpy, "-m", "pip", "install", "--no-input", "-r", os.path.join(SRC_DIR, "requirements.txt")],
capture_output=True, text=True, timeout=900,
)
if r1.returncode != 0:
_log(f"deps install failed: {r1.stderr[-2000:]}")
return None
r2 = subprocess.run(
[vpy, "-m", "pip", "install", "--no-input", "--no-deps", "-e", SRC_DIR],
capture_output=True, text=True, timeout=300,
)
if r2.returncode != 0:
_log(f"searxng install failed: {r2.stderr[-2000:]}")
return None
_log("searxng installed")
return py
def _wait_ready(proc: subprocess.Popen, timeout: float = 120.0) -> bool:
import socket
deadline = time.time() + timeout
while time.time() < deadline:
if proc.poll() is not None:
_log(f"searxng exited early with code {proc.returncode}")
return False
try:
with socket.create_connection(("127.0.0.1", SEARXNG_PORT), timeout=1):
return True
except OSError:
time.sleep(0.5)
return False
def start_searxng() -> bool:
"""Launch SearXNG bound to localhost. Safe to call once at startup."""
global _proc
if os.environ.get("SEARXNG_OFF") == "1":
_log("disabled via SEARXNG_OFF")
return False
with _lock:
if _proc and _proc.poll() is None:
return True
global _boot_error
try:
if not _fetch_source():
_boot_error = "source fetch failed"
_log(_boot_error)
return False
py = _venv_python()
if not py:
_boot_error = "venv/pip install failed (see logs)"
return False
except Exception as e:
_boot_error = f"bootstrap failed: {e}"
_log(_boot_error)
return False
env = os.environ.copy()
env["SEARXNG_SETTINGS_PATH"] = SETTINGS_FILE
env["PYTHONPATH"] = SRC_DIR
_log(f"starting searxng on 127.0.0.1:{SEARXNG_PORT}")
_proc = subprocess.Popen(
[py, "-m", "searx.webapp"],
cwd=SRC_DIR,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
threading.Thread(target=_pump_logs, daemon=True).start()
if _wait_ready(_proc):
_log("searxng is READY")
_ready.set()
return True
_log("searxng failed to become ready")
return False
def _pump_logs():
assert _proc and _proc.stdout
for line in _proc.stdout:
_log(line.rstrip())
def searxng_ready() -> bool:
return _ready.is_set()
def boot_error() -> str:
return _boot_error
|