mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-28 01:52:28 +02:00
9ef02dd06f
Reported by @f3n3k on Windows native install path. Symptom:
C:\001\backend\venv\Scripts\python.exe: No module named uvicorn
[backend] exited with 1
ShadowBroker has stopped. Exit code: 1
Root cause
----------
The Windows Start.bat flow chains:
Start.bat
└─ scripts\run-windows-runtime.ps1
└─ frontend\scripts\dev-all.cjs
└─ start-backend.js
└─ backend\venv\Scripts\python.exe -m uvicorn main:app
`start-backend.js` decided whether an existing `backend\venv` was usable
by calling `canRun(candidate, ["-V"])`. That only checks whether Python
itself can run — it does NOT check whether the backend's actual runtime
dependencies are installed.
When the venv exists but `pip install` never finished (partial install,
failed network, interrupted bootstrap, etc.), the launcher happily
accepted that broken venv, then died with the exact error f3n3k
reported.
Fix
---
New `canRunBackendPython()` helper that requires BOTH:
python -V # Python is runnable
python -c "import fastapi, uvicorn" # backend deps are installed
Used in two call sites:
* `ensureBackendVenv()` — when iterating candidate venvs on first
launch, reject any venv whose Python can't import the backend's
real entry-point deps. The launcher then falls through to its
existing rebuild path (`rebuildBackendVenv`) which reinstalls deps
before declaring the venv healthy.
* `rebuildBackendVenv()` — after a rebuild attempt, verify the deps
are present before returning the new interpreter path. Catches
silent partial rebuilds.
The check is the import that uvicorn itself would do at startup, so a
green return here genuinely means "uvicorn will start". Cost is one
extra `python -c` per venv candidate on launcher startup — milliseconds.
Verified locally with `node --check start-backend.js`.
Credit: @f3n3k for the original report.
216 lines
6.1 KiB
JavaScript
216 lines
6.1 KiB
JavaScript
const { spawn, spawnSync } = require("child_process");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
|
|
const backendDir = path.resolve(__dirname, "backend");
|
|
const isWindows = process.platform === "win32";
|
|
const configuredBasePython = String(process.env.BACKEND_BASE_PYTHON || process.env.PYTHON || "").trim();
|
|
const configuredVenvDir = String(process.env.BACKEND_VENV_DIR || "").trim();
|
|
const canonicalVenvDir = path.join(backendDir, "venv");
|
|
const venvMarkerPath = path.join(backendDir, ".venv-dir");
|
|
|
|
function venvPythonPath(dir) {
|
|
return isWindows
|
|
? path.join(dir, "Scripts", "python.exe")
|
|
: path.join(dir, "bin", "python3");
|
|
}
|
|
|
|
function readPersistedVenvDir() {
|
|
try {
|
|
const value = fs.readFileSync(venvMarkerPath, "utf8").trim();
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
return path.isAbsolute(value) ? value : path.join(backendDir, value);
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function persistSelectedVenv(pythonBin) {
|
|
const envDir = path.dirname(path.dirname(pythonBin));
|
|
const relativeDir = path.relative(backendDir, envDir);
|
|
if (!relativeDir || relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) {
|
|
return;
|
|
}
|
|
try {
|
|
fs.writeFileSync(venvMarkerPath, `${relativeDir}\n`, "utf8");
|
|
} catch {
|
|
// Best effort only. Startup should still succeed if the marker cannot be updated.
|
|
}
|
|
}
|
|
|
|
const explicitVenvCandidate = configuredVenvDir
|
|
? venvPythonPath(path.isAbsolute(configuredVenvDir) ? configuredVenvDir : path.join(backendDir, configuredVenvDir))
|
|
: "";
|
|
const persistedVenvDir = readPersistedVenvDir();
|
|
const persistedVenvCandidate = persistedVenvDir ? venvPythonPath(persistedVenvDir) : "";
|
|
|
|
const venvCandidates = [
|
|
explicitVenvCandidate,
|
|
persistedVenvCandidate,
|
|
...(isWindows
|
|
? [
|
|
path.join(backendDir, "venv", "Scripts", "python.exe"),
|
|
path.join(backendDir, "venv-repair", "Scripts", "python.exe"),
|
|
path.join(backendDir, ".venv", "Scripts", "python.exe"),
|
|
path.join(backendDir, ".venv-repair", "Scripts", "python.exe"),
|
|
]
|
|
: [
|
|
path.join(backendDir, "venv", "bin", "python3"),
|
|
path.join(backendDir, "venv-repair", "bin", "python3"),
|
|
path.join(backendDir, ".venv", "bin", "python3"),
|
|
path.join(backendDir, ".venv-repair", "bin", "python3"),
|
|
]),
|
|
].filter(Boolean);
|
|
const repairTargetDir = isWindows
|
|
? path.join(backendDir, "venv-repair")
|
|
: path.join(backendDir, "venv-repair");
|
|
|
|
function canRun(command, args) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: backendDir,
|
|
env: process.env,
|
|
stdio: "ignore",
|
|
});
|
|
return !result.error && result.status === 0;
|
|
}
|
|
|
|
function canRunBackendPython(pythonBin) {
|
|
return (
|
|
canRun(pythonBin, ["-V"]) &&
|
|
canRun(pythonBin, ["-c", "import fastapi, uvicorn"])
|
|
);
|
|
}
|
|
|
|
function findBasePython() {
|
|
const candidates = isWindows
|
|
? [
|
|
[configuredBasePython, []],
|
|
["python", []],
|
|
["py", ["-3.11"]],
|
|
["py", ["-3"]],
|
|
]
|
|
: [
|
|
[configuredBasePython, []],
|
|
["python3", []],
|
|
["python", []],
|
|
];
|
|
|
|
for (const [command, prefixArgs] of candidates) {
|
|
if (!command) {
|
|
continue;
|
|
}
|
|
if (canRun(command, [...prefixArgs, "-V"])) {
|
|
return { command, prefixArgs };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function rebuildBackendVenv(targetDir, basePython) {
|
|
console.log(`[*] Preparing backend Python environment at ${targetDir}...`);
|
|
try {
|
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
} catch (error) {
|
|
console.warn(`[*] Could not clear ${targetDir} cleanly (${error.code || error.message}). Trying a fresh repair path...`);
|
|
targetDir = `${targetDir}-${Date.now()}`;
|
|
}
|
|
|
|
let result = spawnSync(
|
|
basePython.command,
|
|
[...basePython.prefixArgs, "-m", "venv", targetDir],
|
|
{
|
|
cwd: backendDir,
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
}
|
|
);
|
|
if (result.error || result.status !== 0) {
|
|
return null;
|
|
}
|
|
|
|
const repairedBin = isWindows
|
|
? path.join(targetDir, "Scripts", "python.exe")
|
|
: path.join(targetDir, "bin", "python3");
|
|
|
|
result = spawnSync(repairedBin, ["-m", "pip", "install", "-q", "."], {
|
|
cwd: backendDir,
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
return null;
|
|
}
|
|
return canRunBackendPython(repairedBin) ? repairedBin : null;
|
|
}
|
|
|
|
function ensureBackendVenv() {
|
|
for (const candidate of venvCandidates) {
|
|
if (fs.existsSync(candidate) && canRunBackendPython(candidate)) {
|
|
persistSelectedVenv(candidate);
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
const hadExisting = venvCandidates.some((candidate) => fs.existsSync(candidate));
|
|
console.log(
|
|
hadExisting
|
|
? "[*] Backend venv exists but is stale. Rebuilding it automatically..."
|
|
: "[*] Backend venv is missing. Creating it automatically..."
|
|
);
|
|
|
|
const basePython = findBasePython();
|
|
if (!basePython) {
|
|
return null;
|
|
}
|
|
|
|
const preferredRebuildDir = persistedVenvDir || canonicalVenvDir;
|
|
const rebuilt = rebuildBackendVenv(hadExisting ? preferredRebuildDir : canonicalVenvDir, basePython);
|
|
if (rebuilt) {
|
|
persistSelectedVenv(rebuilt);
|
|
}
|
|
return rebuilt;
|
|
}
|
|
|
|
const venvBin = ensureBackendVenv();
|
|
|
|
if (!venvBin) {
|
|
console.error(`[!] Unable to prepare backend Python venv. Checked: ${venvCandidates.join(", ")}`);
|
|
console.error("[!] Install Python 3.10-3.12 and rerun start.sh/start.bat if the repair could not complete.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const backendArgs = ["-m", "uvicorn", "main:app", "--timeout-keep-alive", "120"];
|
|
if (["1", "true", "yes"].includes(String(process.env.BACKEND_RELOAD || "").toLowerCase())) {
|
|
backendArgs.push("--reload");
|
|
}
|
|
|
|
console.log(`[*] Starting backend with: ${venvBin} ${backendArgs.join(" ")}`);
|
|
const backendProc = spawn(venvBin, backendArgs, {
|
|
cwd: backendDir,
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
});
|
|
|
|
const cleanupAll = () => {
|
|
if (backendProc && !backendProc.killed) {
|
|
backendProc.kill();
|
|
}
|
|
};
|
|
|
|
process.on("exit", cleanupAll);
|
|
process.on("SIGINT", () => {
|
|
cleanupAll();
|
|
process.exit(0);
|
|
});
|
|
process.on("SIGTERM", () => {
|
|
cleanupAll();
|
|
process.exit(0);
|
|
});
|
|
|
|
backendProc.on("exit", (code) => {
|
|
cleanupAll();
|
|
process.exit(code ?? 0);
|
|
});
|