#!/usr/bin/env python3
"""``platform-server`` — ASGI server launcher for the Ferrosoft platform.
Builds an argument list for either **granian** (default) or **uvicorn** and
replaces the current process via :func:`os.execvp`, so the chosen server
becomes PID 1-compatible in container deployments.
Network modes
-------------
unix (default)
Listen on a Unix-domain socket (``--socket``, default
``/run/platform.sock``). Suitable for reverse-proxy setups (nginx,
Caddy) where the proxy and app share a filesystem namespace.
tcp
Listen on a TCP address/port (``--address`` / ``--port``). Suitable for
direct exposure or Docker-based deployments where socket sharing is
impractical.
CLI synopsis::
platform-server [--impl {granian,uvicorn}]
[-n {unix,tcp}]
[-S SOCKET] [-a ADDRESS] [-p PORT]
[-w WORKERS] [-R]
"""
import argparse
import os
import sys
ASGI_APP = "ferrosoft.asgi:application"
def _granian_args(args) -> (str, list[str]):
"""Build the granian argv list from parsed CLI arguments.
Args:
args: Parsed :class:`argparse.Namespace` from :func:`main`.
Returns:
A tuple of ``(executable_name, argv)`` suitable for :func:`os.execvp`.
"""
server_args = ["granian", "--interface", "asgi"]
if args.network == "unix":
server_args += ["--uds", args.socket]
elif args.network == "tcp":
server_args += ["--host", args.address, "--port", args.port]
if args.reload:
server_args += ["--reload"]
server_args += [ASGI_APP]
return "granian", server_args
def _uvicorn_args(args) -> (str, list[str]):
"""Build the uvicorn argv list from parsed CLI arguments.
Args:
args: Parsed :class:`argparse.Namespace` from :func:`main`.
Returns:
A tuple of ``(executable_name, argv)`` suitable for :func:`os.execvp`.
"""
server_args = ["uvicorn"]
if args.network == "unix":
server_args += ["--uds", args.socket]
elif args.network == "tcp":
server_args += ["--host", args.address, "--port", args.port]
if args.reload:
server_args += ["--reload"]
server_args += [ASGI_APP]
return "uvicorn", server_args
[docs]
def main():
"""Parse CLI arguments and exec the chosen ASGI server process.
This function never returns on success — it calls :func:`os.execvp` to
replace the current process with the server binary. On argument error or
unsupported ``--impl`` value it exits with a non-zero status code.
"""
parser = argparse.ArgumentParser(
prog="platform-server",
description="Run Ferrosoft platform application server",
)
parser.add_argument(
"--impl",
choices=["granian", "uvicorn"],
default="granian",
)
parser.add_argument(
"-n",
"--network",
choices=["unix", "tcp"],
default="unix",
)
parser.add_argument(
"-S",
"--socket",
help="Socket file name (only for Unix network)",
default="/run/platform.sock",
)
parser.add_argument(
"-p",
"--port",
help="Port number (only for TCP)",
default="8000",
)
parser.add_argument(
"-a",
"--address",
help="IP address (only for TCP)",
default="0.0.0.0",
)
parser.add_argument(
"-w",
"--workers",
help="Number of workers",
type=int,
default=4,
)
parser.add_argument(
"-R",
"--reload",
action="store_true",
help="Enable auto-reload",
)
args = parser.parse_args()
server_name = "platform-server"
server_args = ["--help"]
match args.impl:
case "granian":
server_name, server_args = _granian_args(args)
case "uvicorn":
server_name, server_args = _uvicorn_args(args)
case _:
print(
"Server implementation not supported: %s" % args.impl,
file=sys.stderr,
)
sys.exit(1)
print(
"""
░█▀▀░█▀▀░█▀▄░█▀▄░█▀█░█▀▀░█▀█░█▀▀░▀█▀
░█▀▀░█▀▀░█▀▄░█▀▄░█░█░▀▀█░█░█░█▀▀░░█░
░▀░░░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀░░░░▀░
""",
file=sys.stderr,
)
print("Running %s" % " ".join(server_args), file=sys.stderr)
os.execvp(server_name, server_args)
if __name__ == "__main__":
main()