diff --git a/granian/_granian.pyi b/granian/_granian.pyi index d156bc76..3a807b13 100644 --- a/granian/_granian.pyi +++ b/granian/_granian.pyi @@ -144,6 +144,10 @@ if sys.platform != 'win32': def __new__(cls, bind: str, backlog: int, permissions: int | None) -> UnixListenerSpec: ... def build(self) -> SocketHolder: ... def is_uds(self) -> bool: ... + class FdListenerSpec: + def __new__(cls, fd: int) -> FdListenerSpec: ... + def build(self) -> SocketHolder: ... + def is_uds(self) -> bool: ... class CallbackScheduler: _loop: Any diff --git a/granian/cli.py b/granian/cli.py index 06b672f3..c517f09c 100644 --- a/granian/cli.py +++ b/granian/cli.py @@ -127,6 +127,7 @@ def option(*param_decls: str, cls: type[click.Option] | None = None, **attrs: An default=Interfaces.RSGI, help='Application interface type', ) +@option('--fd', type=int, default=None, help='Listen on socket from this file-descriptor.') @option('--http', type=EnumType(HTTPModes), default=HTTPModes.auto, help='HTTP version') @option('--ws/--no-ws', 'websockets', default=True, help='Enable websockets handling') @option('--workers', type=click.IntRange(1), default=1, help='Number of worker processes') @@ -438,6 +439,7 @@ def cli( port: int, uds: pathlib.Path | None, uds_permissions: int | None, + fd: int | None, interface: Interfaces, http: HTTPModes, websockets: bool, @@ -524,6 +526,7 @@ def cli( port=port, uds=uds, uds_permissions=uds_permissions, + fd=fd, interface=interface, workers=workers, blocking_threads=blocking_threads, diff --git a/granian/net.py b/granian/net.py index 2fa31f1f..86021f6a 100644 --- a/granian/net.py +++ b/granian/net.py @@ -8,8 +8,10 @@ copyreg.pickle(SocketSpec, lambda v: (SocketSpec, v.__getstate__())) if sys.platform != 'win32': - from ._granian import UnixListenerSpec as UnixSocketSpec + from ._granian import UnixListenerSpec as UnixSocketSpec, FdListenerSpec as FdSocketSpec copyreg.pickle(UnixSocketSpec, lambda v: (UnixSocketSpec, v.__getstate__())) + copyreg.pickle(FdSocketSpec, lambda v: (FdSocketSpec, v.__getstate__())) else: UnixSocketSpec = None + FdSocketSpec = None diff --git a/granian/server/common.py b/granian/server/common.py index 1369a22e..e34cbdfb 100644 --- a/granian/server/common.py +++ b/granian/server/common.py @@ -21,7 +21,7 @@ from ..errors import ConfigurationError, PidFileError from ..http import HTTP1Settings, HTTP2Settings from ..log import DEFAULT_ACCESSLOG_FMT, LogLevels, configure_logging, logger -from ..net import SocketSpec, UnixSocketSpec +from ..net import SocketSpec, UnixSocketSpec, FdSocketSpec WT = TypeVar('WT') @@ -87,6 +87,7 @@ def __init__( port: int = 8000, uds: Path | None = None, uds_permissions: int | None = None, + fd: int | None = None, interface: Interfaces = Interfaces.RSGI, workers: int = 1, blocking_threads: int | None = None, @@ -149,6 +150,7 @@ def __init__( self.bind_port = port self.bind_uds = uds.resolve() if uds else None self.uds_permissions = uds_permissions + self.bind_fd = fd self.interface = interface self.workers = max(1, workers) self.runtime_threads = max(1, runtime_threads) @@ -291,7 +293,12 @@ def build_ssl_context( @property def _bind_addr_fmt(self): - return f'unix:{self.bind_uds}' if self.bind_uds else f'{self.bind_addr}:{self.bind_port}' + if self.bind_fd is not None: + return f'fd:{self.bind_fd}' + elif self.bind_uds : + return f'unix:{self.bind_uds}' + else: + f'{self.bind_addr}:{self.bind_port}' @staticmethod def _call_hooks(hooks): @@ -311,7 +318,9 @@ def on_shutdown(self, hook: Callable[[], Any]) -> Callable[[], Any]: return hook def _init_shared_socket(self): - if self.bind_uds: + if self.bind_fd: + self._ssp = FdSocketSpec(self.bind_fd) + elif self.bind_uds: self._ssp = UnixSocketSpec(str(self.bind_uds), self.backlog, self.uds_permissions) else: self._ssp = SocketSpec(self.bind_addr, self.bind_port, self.backlog) diff --git a/src/net.rs b/src/net.rs index c76cca37..591e605e 100644 --- a/src/net.rs +++ b/src/net.rs @@ -67,6 +67,15 @@ pub struct UnixListenerSpec { permissions: Option, } + +#[cfg(unix)] +#[pyclass(frozen, module = "granian._granian")] +#[derive(Clone)] +pub struct FdListenerSpec { + inp: (i32,), + fd: i32, +} + impl ListenerSpec { pub(crate) fn as_socket(&self) -> Result { let socket = Socket::new(self.domain, Type::STREAM, Some(Protocol::TCP))?; @@ -161,6 +170,35 @@ impl UnixListenerSpec { } } + +#[cfg(unix)] +impl FdListenerSpec { + pub(crate) fn as_socket(&self) -> Result { + let socket = unsafe { Socket::from_raw_fd(self.fd) }; + Ok(socket) + } +} + +#[cfg(unix)] +#[pymethods] +impl FdListenerSpec { + #[new] + fn new(fd: i32) -> PyResult { + Ok(Self { + inp: (fd,), + fd, + }) + } + + fn build(&self) -> Result { + SocketHolder::from_fd_spec(self) + } + + pub fn __getstate__(&self, py: Python) -> Py { + self.inp.clone().into_py_any(py).unwrap() + } +} + #[cfg(not(any(windows, target_os = "linux", target_os = "freebsd")))] #[pyclass(frozen, module = "granian._granian")] pub struct SocketHolder { @@ -186,6 +224,15 @@ impl SocketHolder { }) } + fn from_fd_spec(spec: &FdListenerSpec) -> Result { + let socket = spec.as_socket()?; + let domain = socket.domain().unwrap(); + Ok(Self { + socket: Some(socket), + uds: domain == Domain::UNIX, + }) + } + #[allow(clippy::unnecessary_wraps)] pub fn as_tcp_listener(&self) -> Result { let listener = unsafe { TcpListener::from_raw_fd(self.socket.as_ref().unwrap().as_raw_fd()) }; @@ -260,6 +307,16 @@ impl SocketHolder { }) } + fn from_fd_spec(spec: &FdListenerSpec) -> Result { + let socket = spec.as_socket()?; + let domain = socket.domain().unwrap(); + Ok(Self { + socket: Some(socket), + uds: domain == Domain::UNIX, + backlog: 20, + }) + } + pub fn as_tcp_listener(&self) -> Result { let socket = self.socket.as_ref().unwrap(); socket.listen(self.backlog)?; @@ -355,6 +412,7 @@ pub(crate) fn init_pymodule(module: &Bound) -> PyResult<()> { module.add_class::()?; #[cfg(unix)] module.add_class::()?; + module.add_class::()?; Ok(()) }