Commit d9fdbea1 authored by vertighel's avatar vertighel
Browse files

Fix uvicorn ws-backend fallback: only trigger on Config.load() failure



The previous fallback caught any exception from the whole server.run()
call, including ones raised late during Ctrl-C shutdown (the same
websocket close-race, this time from inside uvicorn's own teardown code).
That misread a shutdown-time error as "backend unavailable" and silently
restarted the entire server on the other ws implementation — re-running
every device's startup — right when the operator was trying to stop it.

Now the ws implementation is resolved via Config.load() (which only
imports/resolves protocol classes, no socket bound, no before_serving
hooks touched) before Server(config).run() is ever called. Only a
Config.load() failure triggers the next backend in the list; anything
after that point behaves as before (KeyboardInterrupt handled, anything
else propagates).

Also includes minor UI tweaks (control.html/init.html widget ordering
and defaults).

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent f89b4c0b
Loading
Loading
Loading
Loading
+28 −14
Original line number Diff line number Diff line
@@ -54,15 +54,6 @@ class _WebSocketCloseRaceMiddleware:
app = _WebSocketCloseRaceMiddleware(app)


def _serve(ws):
    """Run uvicorn with the given websocket implementation until Ctrl-C."""

    try:
        uvicorn.run(app, host="0.0.0.0", port=5533, ws=ws)
    except KeyboardInterrupt as e:
        print(f"Shutting down: {e}")


def run():
    """
    Server run using uvicorn.
@@ -76,6 +67,18 @@ def run():
    cleanly (e.g. a page reload) can wedge the connection into an
    unrecoverable state instead of raising — see
    https://github.com/Kludex/uvicorn/issues/997.

    The fallback decision is made by calling Config.load() ourselves —
    which is where the ws implementation actually gets imported — before
    the server ever starts serving. That's deliberate: Config.load() only
    resolves protocol classes/settings, it doesn't bind a socket or touch
    the app's before_serving hooks, so a failure there can only mean "this
    backend isn't usable". Catching exceptions from server.run() instead
    would also catch anything raised late during Ctrl-C shutdown (e.g.
    that same close-race, from inside uvicorn's own teardown) and
    misread it as "backend unavailable" — restarting the whole server,
    and every device's startup with it, in the middle of shutting down.
    That's exactly what the previous version of this function did.
    """

    # Only the long-lived noctua-app process should survive an
@@ -84,11 +87,22 @@ def run():
    # noctua-guider never call this, so they keep failing loudly.
    check.set_recoverable(True)

    for ws in ("websockets-sansio", "websockets"):
        config = uvicorn.Config(app, host="0.0.0.0", port=5533, ws=ws)
        try:
        _serve(ws="websockets-sansio")
            config.load()
        except Exception as e:
        log.warning(f"uvicorn ws='websockets-sansio' unavailable ({e}), falling back to 'websockets'")
        _serve(ws="websockets")
            log.warning(f"uvicorn ws={ws!r} unavailable ({e}), trying next")
            continue

        try:
            uvicorn.Server(config).run()
        except KeyboardInterrupt as e:
            print(f"Shutting down: {e}")
        return

    raise RuntimeError(
        "No usable uvicorn websocket implementation (tried websockets-sansio, websockets)")


if __name__ == "__main__":
+2 −2
Original line number Diff line number Diff line
@@ -78,8 +78,8 @@
      {{ w.widget_standard({
         "label": "Offsets",
         "inputs": [
         {"label": "Zd", "value": -300},
            {"label": "Az", "value": -300}
         {"label": "Zd", "value": 0},
            {"label": "Az", "value": 0}
            ],
             "unit": "″",
             "buttons": [{"label": "Set", "endpoint": "/telescope/coordinates/offset", "method": "PUT"}],
+28 −28
Original line number Diff line number Diff line
@@ -191,16 +191,6 @@
                ],
            }) }}

            {{ w.widget_standard({
                "label": "Cooling",
                "info": "scicam1-cooler",
                "map": "bool_yesno",
                "buttons": [
                    {"label": "On",  "endpoint": "/scicam1/cooler", "val": true,  "method": "PUT"},
                    {"label": "Off", "endpoint": "/scicam1/cooler", "val": false, "method": "PUT"}
                ],
            }) }}

            {{ w.widget_standard({
                "label": "Temp_1",
                "inputs": [{"value": -10}],
@@ -212,6 +202,16 @@
                    {"label": "T",   "status": "scicam1-settings-temperature", "transform": "round_1"}
                ]
            }) }}

            {{ w.widget_standard({
                "label": "Cooling",
                "info": "scicam1-cooler",
                "map": "bool_yesno",
                "buttons": [
                    {"label": "On",  "endpoint": "/scicam1/cooler", "val": true,  "method": "PUT"},
                    {"label": "Off", "endpoint": "/scicam1/cooler", "val": false, "method": "PUT"}
                ],
            }) }}
        </section>

        <section>
@@ -227,16 +227,6 @@
                ],
            }) }}

            {{ w.widget_standard({
                "label": "Cooling",
                "info": "scicam2-cooler",
                "map": "bool_yesno",
                "buttons": [
                    {"label": "On",  "endpoint": "/scicam2/cooler", "val": true,  "method": "PUT"},
                    {"label": "Off", "endpoint": "/scicam2/cooler", "val": false, "method": "PUT"}
                ],
            }) }}

            {{ w.widget_standard({
                "label": "Temp_2",
                "inputs": [{"value": -10}],
@@ -248,21 +238,21 @@
                    {"label": "T",   "status": "scicam2-settings-temperature", "transform":"round_1"}
                ]
            }) }}
        </section>


        <section>
            <h4 class="mt-2 mb-2">Camera - Échelle</h4>

            {{ w.widget_standard({
                "label": "Cooling",
                "info": "scicam3-cooler",
                "info": "scicam2-cooler",
                "map": "bool_yesno",
                "buttons": [
                    {"label": "On",  "endpoint": "/scicam3/cooler", "val": true,  "method": "PUT"},
                    {"label": "Off", "endpoint": "/scicam3/cooler", "val": false, "method": "PUT"}
                    {"label": "On",  "endpoint": "/scicam2/cooler", "val": true,  "method": "PUT"},
                    {"label": "Off", "endpoint": "/scicam2/cooler", "val": false, "method": "PUT"}
                ],
            }) }}
        </section>


        <section>
            <h4 class="mt-2 mb-2">Camera - Échelle</h4>

            {{ w.widget_standard({
                "label": "Temp_3",
@@ -275,6 +265,16 @@
                    {"label": "T",   "status": "scicam3-settings-temperature", "transform": "round_1"}
                ]
            }) }}

            {{ w.widget_standard({
                "label": "Cooling",
                "info": "scicam3-cooler",
                "map": "bool_yesno",
                "buttons": [
                    {"label": "On",  "endpoint": "/scicam3/cooler", "val": true,  "method": "PUT"},
                    {"label": "Off", "endpoint": "/scicam3/cooler", "val": false, "method": "PUT"}
                ],
            }) }}
        </section>
        
    </article>