mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-09 19:27:45 +02:00
[eric] electron: the packaged backend runs as backend.serve, so an agent's pkill -f uvicorn aimed at its own app cannot take ours down
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
08f4c03a88
commit
92b8f461ab
@@ -0,0 +1,37 @@
|
|||||||
|
"""The packaged backend's entry point: `python -m backend.serve --port N`.
|
||||||
|
|
||||||
|
Why not `python -m uvicorn backend.main:app`: agents building FastAPI apps restart their own dev
|
||||||
|
server with `pkill -f "uvicorn backend.main"` (the app template is uvicorn + backend/main.py too),
|
||||||
|
and that pattern matched the OpenSwarm backend's own command line. Measured 2026-09-01 on a packaged
|
||||||
|
soak: one agent's restart command took down the host backend, a second instance's backend and the
|
||||||
|
user's production app backend, twelve seconds apart, twice. A command line that contains neither
|
||||||
|
"uvicorn" nor "backend.main" cannot be caught by the patterns an app-building agent reaches for.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import uvicorn
|
||||||
|
from typeguard import typechecked
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyServer(uvicorn.Server):
|
||||||
|
"""Prints a machine-readable READY line once the socket is bound, like the dev runner does."""
|
||||||
|
async def startup(self, sockets=None):
|
||||||
|
await super().startup(sockets)
|
||||||
|
print(f"READY:PORT={self.config.port}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="OpenSwarm backend server")
|
||||||
|
parser.add_argument("--port", type=int, default=int(os.environ.get("OPENSWARM_PORT", "8324")))
|
||||||
|
parser.add_argument("--host", default=os.environ.get("OPENSWARM_HOST", "127.0.0.1"))
|
||||||
|
parser.add_argument("--timeout-graceful-shutdown", type=int, default=8)
|
||||||
|
args = parser.parse_args()
|
||||||
|
os.environ["OPENSWARM_PORT"] = str(args.port)
|
||||||
|
config = uvicorn.Config("backend.main:app", host=args.host, port=args.port, timeout_graceful_shutdown=args.timeout_graceful_shutdown)
|
||||||
|
asyncio.run(ReadyServer(config).serve())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// The packaged backend's command line must not look like the app an agent is building. Agents restart
|
||||||
|
// their FastAPI dev servers with `pkill -f "uvicorn backend.main"` (the app template is uvicorn +
|
||||||
|
// backend/main.py), and on 2026-09-01 that command, run by ONE agent, SIGTERMed the host backend, a
|
||||||
|
// second instance's backend and the user's production app backend, twice. Sessions mid-turn were
|
||||||
|
// flushed as "stopped" with nothing saying why: silent work loss caused by a name collision.
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SRC = fs.readFileSync(path.join(__dirname, 'main.js'), 'utf8');
|
||||||
|
|
||||||
|
function backendSpawnArgs() {
|
||||||
|
const m = SRC.match(/backendProcess = spawn\(\s*pythonPath,\s*\[([^\]]*)\]/);
|
||||||
|
assert.ok(m, 'the backend spawn call must be findable');
|
||||||
|
return m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the backend runs as backend.serve, never as the uvicorn CLI on backend.main', () => {
|
||||||
|
const args = backendSpawnArgs();
|
||||||
|
assert.match(args, /'-m',\s*'backend\.serve'/);
|
||||||
|
assert.doesNotMatch(args, /uvicorn/, 'an agent pkill -f uvicorn would catch the host backend');
|
||||||
|
assert.doesNotMatch(args, /backend\.main/, 'an agent pkill -f backend.main would catch the host backend');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('backend/serve.py exists and is the only module spelled in the spawn', () => {
|
||||||
|
const serve = fs.readFileSync(path.join(__dirname, '..', 'backend', 'serve.py'), 'utf8');
|
||||||
|
assert.match(serve, /uvicorn\.Config\("backend\.main:app"/, 'serve.py hands the app to uvicorn programmatically, keeping the name off argv');
|
||||||
|
assert.match(serve, /READY:PORT=/, 'the readiness line the shell may wait on');
|
||||||
|
});
|
||||||
+2
-1
@@ -1287,9 +1287,10 @@ async function startBackend() {
|
|||||||
console.log(`Starting backend: ${pythonPath} (exists=${pythonExists}) on port ${backendPort}`);
|
console.log(`Starting backend: ${pythonPath} (exists=${pythonExists}) on port ${backendPort}`);
|
||||||
console.log(`Project root: ${projectRoot}`);
|
console.log(`Project root: ${projectRoot}`);
|
||||||
|
|
||||||
|
// backend.serve, not `-m uvicorn backend.main:app`: an agent's `pkill -f "uvicorn backend.main"` aimed at its own app must not match ours.
|
||||||
backendProcess = spawn(
|
backendProcess = spawn(
|
||||||
pythonPath,
|
pythonPath,
|
||||||
['-m', 'uvicorn', 'backend.main:app', '--host', '127.0.0.1', '--port', String(backendPort), '--timeout-graceful-shutdown', '8'],
|
['-m', 'backend.serve', '--host', '127.0.0.1', '--port', String(backendPort), '--timeout-graceful-shutdown', '8'],
|
||||||
{
|
{
|
||||||
cwd: projectRoot,
|
cwd: projectRoot,
|
||||||
env,
|
env,
|
||||||
|
|||||||
Reference in New Issue
Block a user