exlab_wizard.tray.server_runner#

Programmatic uvicorn launcher with atomic server.json writes. Backend Spec §4.3.2.

The tray process owns the FastAPI server in-process. ServerRunner encapsulates the lifecycle:

  1. Pick a free localhost port from the OS at start time (Backend §15.3.1).

  2. Launch uvicorn.Server.run on a dedicated worker thread so the pystray main-thread event loop is unaffected.

  3. Atomically write <state_dir>/server.json with {port, pid, started_at} so exlab_wizard.window (a separate process) can discover the live server (Backend §4.2 – “Window<->server discovery”).

  4. On stop, signal uvicorn to exit and delete the state file.

The atomic write follows the §4.4.5 idiom (write tmp, fsync, replace) so a crash during the write never leaves a half-written state file behind that the window subprocess could try to parse.

Functions

pick_free_port()

Return a free localhost port from the OS.

Classes

ServerRunner(*, app, state_dir)

Starts uvicorn on a free localhost port and tracks its state file.

class exlab_wizard.tray.server_runner.ServerRunner(*, app, state_dir)[source]#

Bases: object

Starts uvicorn on a free localhost port and tracks its state file.

Backend Spec §4.3.2 + §15.3.1.

Parameters:
property is_running: bool#

Return True while the worker thread is alive.

property port: int#

Return the port the server is bound to.

Raises RuntimeError when called before start().

start()[source]#

Pick a port, launch uvicorn in a worker thread, write server.json.

Returns the chosen port. Idempotent in the sense that calling start() a second time before stop() raises RuntimeError – the runner manages exactly one server.

Return type:

int

property state_file: Path#

Return the absolute path of the server.json state file.

stop()[source]#

Signal uvicorn to exit, join the worker thread, delete server.json.

Idempotent: a second call is a no-op.

Return type:

None

exlab_wizard.tray.server_runner.pick_free_port()[source]#

Return a free localhost port from the OS.

Binds a SOCK_STREAM socket to ("127.0.0.1", 0) and reads the OS-assigned port back, then closes the socket. Subject to the usual tiny race between close and re-bind by uvicorn – acceptable in practice; the alternative is leaking the socket which uvicorn cannot accept ownership of.

Return type:

int