The genro-asgi command
Status: Draft; implementation checked against the development source on 2026-09-08.
What it does
Installing the package puts a genro-asgi command on your path. It boots a
server without you writing an entry point, and it keeps a small registry of
named servers so you can start, list and stop them from any shell.
genro-asgi serve <source> [--host H] [--port P] [--reload] [--name N] [--debug [PARAMETERS]]
genro-asgi apps
genro-asgi stop <name>
genro-asgi remove <name>
Everything lives in genro_asgi/__main__.py; the server core knows nothing
about it. python3 -m genro_asgi ... is the same command, useful when the
console script is not on the path.
When to use it
Use it for development and for a container CMD: a config.py plus
genro-asgi serve is a complete deployment unit, no main.py to maintain. Keep
writing your own Python entry point when the process must do something around
the server — build objects the config cannot express, run migrations first, or
embed the server in a larger program. .serve() remains the programmatic way in
and the command adds nothing you cannot do by hand.
Setup
Nothing to arm. The command ships with the package, and uvicorn — which it boots under — is already a dependency.
Serving from a config.py
The primary form. A source that is an existing .py path is handed to
AsgiServer(config=...). Two files in one directory — the application and the
recipe that serves it:
# hello.py
from genro_asgi import RoutedApplication
from genro_routes import route
class Hello(RoutedApplication):
mount = ""
@route()
def greet(self, name: str = "world") -> dict[str, str]:
return {"hello": name}
# config.py
from genro_asgi.config import AsgiConfigBuilder
from hello import Hello
class ServerConfiguration(AsgiConfigBuilder):
def main(self, root):
cfg = root.configuration()
cfg.server(host="127.0.0.1", port=8123)
cfg.applications().application(code="hello", app_class=Hello)
What a recipe can contain — the sections, the resolvers that keep secrets out of it, and how the server reads it back — is the subject of Configuration.
The command puts the config file’s own directory on sys.path before loading
it, so from hello import Hello resolves regardless of how the command is
invoked or from where.
$ genro-asgi serve ./config.py
genro-asgi serving http://127.0.0.1:8123
INFO: Started server process [72897]
INFO: Application startup complete.
The contract on that file is the config handler’s own, not the command’s:
“must define exactly one ConfigBuilder subclass” — in a genro-asgi recipe
that subclass is AsgiConfigBuilder. The command ships no loader of its own,
so a recipe error surfaces as the same boot error you get from
AsgiServer(config=...) in a script.
Serving one application, no config
For a quick run there is the application= form, which resolves a class and
hands it to AsgiServer(applications=[...]) — instantiated with no arguments:
$ genro-asgi serve application=./hello.py:Hello --port 8124
genro-asgi serving http://127.0.0.1:8124
Two spellings are accepted after application=:
package.module:ClassName— a plain import, for an installed or importable module;path/to/file.py:ClassName— a single file, loaded directly, no packaging needed.
A target without the : separator is an error naming both forms.
--host and --port are forwarded as AsgiServer kwargs. The server’s own
rule does the precedence — an explicit kwarg wins over the configured value,
wholesale per kwarg — and the command computes nothing.
The registry of named servers
--name registers the server under that name and records its pid, so other
shells can see and stop it:
$ genro-asgi serve ./config.py --name demo
$ genro-asgi apps
demo running (pid 72897) -:- ./config.py
$ genro-asgi stop demo
demo: stopped (pid 72897)
$ genro-asgi apps
demo stopped -:- ./config.py
$ genro-asgi remove demo
demo: removed
A registered name then becomes a source of its own — genro-asgi serve demo
relaunches with the stored options — and an unknown name is an error listing the
names that do exist.
Naming an instance also arms the session snapshot: the sessions of
--name demo are pickled to ~/.genroasgi/sessions/demo.pickle at shutdown
and reloaded at the next boot (expired ones filtered out by their TTL). A
nameless serve stays volatile. This is a development convenience — production
deployments will bring their own persistence. See the
sessions guide for details.
The store is ~/.genroasgi: apps/<name>.json holds the pointer (the
source string and the options you gave), run/<name>.pid the pid of the running
process. It never copies your application, so relaunching by name always runs the
current code. stop sends SIGTERM; remove refuses to drop a registration
while it is running and tells you to stop it first.
A pidfile is never trusted. Missing, unreadable, or naming a process that no
longer exists — all three read the same way: not running. A crashed server
therefore shows as stopped rather than as a phantom, and stop cleans the
stale file up.
Reloading on source changes
$ genro-asgi serve ./config.py --reload --name demo
uvicorn’s reload supervisor accepts only an import string, never a built
server instance: it starts a fresh process on every restart, and nothing of the
parent survives into it. So the command passes it
genro_asgi.__main__:factory and sends the description of the server across the
process boundary in one environment variable, GENRO_ASGI_LAUNCHER — a JSON
object carrying one source key (config or application, always an absolute
path) plus host/port only when you gave them explicitly, so an absent key
still lets the config’s own value apply. factory() reads it and rebuilds the
very same server each time.
You never set GENRO_ASGI_LAUNCHER yourself; called outside the launcher,
factory() says so and stops. The watched directory is the one holding the
source file (a dotted target has no file to anchor on, so the working directory
is watched instead).
The pidfile written under --name records the supervisor, which is the
process stop must signal — the supervisor honours SIGTERM and takes its
child down with it, so stop behaves identically with and without --reload.
How to verify it
With the hello.py above — a RoutedApplication with a greet route — in the
current directory:
$ genro-asgi serve application=./hello.py:Hello --port 8124 --name quick
genro-asgi serving http://127.0.0.1:8124
$ curl -s 'http://127.0.0.1:8124/greet?name=cli'
{"hello":"cli"}
$ genro-asgi apps
quick running (pid 75171) -:8124 application=./hello.py:Hello
$ genro-asgi stop quick
quick: stopped (pid 75171)
Gotchas
appsshows the options you gave, not the address in use. The registry stores the command line, so a host or port that came from theconfig.pyprints as-. The line the server prints on boot (genro-asgi serving http://...) is the address it actually bound.A relative source is resolved against the shell you serve from. It is stored in the registry as you typed it, so
genro-asgi serve demofrom a different directory will not find a relative./config.py. Register with an absolute path if you plan to relaunch from elsewhere.No
--workers. The CLI starts one server process (plus a reload supervisor when requested). A multiworker SPA starts its own configured pool.--debugdeclares a usage mode (optionally a comma-separated parameter list); the core does not branch on it.Exit codes:
0success,2argparse usage errors,1runtime errors — reported as one line on stderr.--reloadis a development tool. It costs a supervisor process and a file watcher; do not ship it in a container image.