From f5ef107eaa43a9f9abc4c0f9e7f1a32cb3051374 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 16 Apr 2026 00:57:30 -0300 Subject: [PATCH] feat: implement asyncio exception handler and update entrypoint script for process management --- backend/app/main.py | 21 +++++++++++++++++++++ backend/requirements.txt | 1 + deploy/entrypoint.sh | 18 +++++++++++++++--- frontend/public/components-metadata.json | 2 +- scripts/generate-component-metadata.ts | 11 ++++++++++- wokwi-libs/ngspice-wasm | 1 + 6 files changed, 49 insertions(+), 5 deletions(-) create mode 160000 wokwi-libs/ngspice-wasm diff --git a/backend/app/main.py b/backend/app/main.py index 04d1724f..7cc255a2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -26,8 +26,29 @@ import app.models.user # noqa: F401 import app.models.project # noqa: F401 +logger = logging.getLogger(__name__) + + +def _asyncio_exception_handler(loop: asyncio.AbstractEventLoop, context: dict) -> None: + """Prevent unhandled asyncio task exceptions from killing the uvicorn process. + + Normally uvicorn re-raises unhandled task exceptions at the event-loop level, + which can crash the whole process. The main culprit is a race condition in + websockets <12.0 (legacy/protocol.py AssertionError during keepalive ping). + Upgrading websockets>=12.0 is the primary fix; this handler is a safety net. + """ + exc = context.get("exception") + msg = context.get("message", "") + if exc is not None: + logger.error("Unhandled asyncio task exception (swallowed): %s — %r", msg, exc) + else: + # No exception object — let default handler deal with it + loop.default_exception_handler(context) + + @asynccontextmanager async def lifespan(_app: FastAPI): + asyncio.get_event_loop().set_exception_handler(_asyncio_exception_handler) async with async_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) # Add is_admin column to existing databases that predate this feature diff --git a/backend/requirements.txt b/backend/requirements.txt index e22e6c8d..b3322098 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,6 @@ fastapi==0.115.0 uvicorn[standard]==0.32.0 +websockets>=12.0 sqlalchemy==2.0.36 aiosqlite==0.20.0 pydantic>=2.11.0 diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index db82836e..433108c0 100644 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -39,10 +39,22 @@ fi # Start FastAPI backend in the background on port 8001 echo "🚀 Starting Velxio Backend..." uvicorn app.main:app --host 127.0.0.1 --port 8001 & +UVICORN_PID=$! -# Wait for backend to be healthy (optional but good practice) +# Wait for backend to be healthy before starting nginx sleep 2 -# Start Nginx in the foreground to keep the container running +# Start Nginx in the background (not exec — we need to monitor both) echo "🌐 Starting Nginx Web Server on port 80..." -exec nginx -g "daemon off;" +nginx -g "daemon off;" & +NGINX_PID=$! + +# Exit as soon as either process dies so Docker can restart the container. +# wait -n requires bash 4.3+ (standard on Debian Bullseye / Ubuntu 20.04+). +wait -n $UVICORN_PID $NGINX_PID +EXIT_CODE=$? + +echo "⚠️ A process exited (code $EXIT_CODE) — shutting down container" +kill $UVICORN_PID $NGINX_PID 2>/dev/null || true +wait $UVICORN_PID $NGINX_PID 2>/dev/null || true +exit $EXIT_CODE diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 1e82397e..2ae8a80f 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "generatedAt": "2026-04-15T22:57:24.963Z", + "generatedAt": "2026-04-16T03:35:24.515Z", "components": [ { "thumbnail": "\n \n \n DIODE-1N4007\n \n ", diff --git a/scripts/generate-component-metadata.ts b/scripts/generate-component-metadata.ts index 4783b40d..e4cfb77c 100644 --- a/scripts/generate-component-metadata.ts +++ b/scripts/generate-component-metadata.ts @@ -332,10 +332,19 @@ class MetadataGenerator { const type = member.type?.getText() || 'any'; const defaultValue = member.initializer?.getText(); + let resolvedDefault: unknown; + if (defaultValue) { + try { + resolvedDefault = eval(defaultValue); + } catch { + // Initializer references an identifier not in scope (e.g. imported constant) + resolvedDefault = undefined; + } + } properties.push({ name, type, - defaultValue: defaultValue ? eval(defaultValue) : undefined, + defaultValue: resolvedDefault, }); } } diff --git a/wokwi-libs/ngspice-wasm b/wokwi-libs/ngspice-wasm new file mode 160000 index 00000000..a0c3b1ab --- /dev/null +++ b/wokwi-libs/ngspice-wasm @@ -0,0 +1 @@ +Subproject commit a0c3b1ab9a4e83ebeff4081b74ecef89bc9ff83c