Skip to main content

port_file/
lib.rs

1#![cfg_attr(doc_cfg, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4
5//! Facilities for reading and writing port files.
6//!
7//! # Motivation
8//!
9//! In many situations, services don't have a fixed TCP or UDP port assigned to
10//! them. In that case, these services typically bind to port 0. All operating
11//! systems use port 0 as a signal to find an available _ephemeral port_ for the
12//! service to bind to.
13//!
14//! But, if a supervisor process (such as a test) wants to find out the actual
15//! port the service was bound to, there must be a protocol to report that
16//! information from the service to the supervisor. A _port file_ is one such
17//! very simple protocol, using a text file to do this reporting.
18//!
19//! A port file is most useful when the service lives in another _process_ from
20//! the supervisor. Sometimes, the service is spun up in-process—in that case,
21//! it's likely easier for the supervisor to query the service through some kind
22//! of in-memory function call. (But a port file can still be used for external
23//! synchronization.)
24//!
25//! # Terminology
26//!
27//! As of this writing, _port file_ is not standard terminology, but it is the
28//! term that the author believes communicates the intent best.
29//!
30//! Some other terms that have been used for this concept:
31//!
32//! * _Listening URL file_, which usually indicates an entire URL; this crate
33//!   isn't necessarily tied to a _port_ ([`SocketAddr`]) specifically, and can be
34//!   used just as well for such a file
35//!
36//! * _Connection file_, as used by
37//!   [Jupyter](https://github.com/jupyter/jupyter_client/blob/main/docs/kernels.rst#connection-files)
38//!
39//! * _Rendezvous file_, which is a term that comes from distributed systems. At
40//!   Oxide, we have an analogous concept called _rendezvous tables_ (see
41//!   [RFD 541](https://rfd.shared.oxide.computer/rfd/0541) if you have access).
42//!
43//! # Usage
44//!
45//! We're going to assume a simple scenario where a parent process asks a child
46//! process to bind a single service to TCP port 0.
47//!
48//! In the child process, expose the bind address and the port file location
49//! as command-line options:
50//!
51//! ```no_run
52//! use camino::Utf8PathBuf;
53//! use clap::Parser;
54//! use std::net::{SocketAddr, TcpListener};
55//!
56//! /// A service that publishes its bound address to a port file.
57//! #[derive(Parser)]
58//! struct Args {
59//!     /// The address to bind to (use port 0 for an ephemeral port).
60//!     #[arg(long)]
61//!     bind_address: SocketAddr,
62//!
63//!     /// If set, the file to publish the bound address to.
64//!     #[arg(long)]
65//!     port_file: Option<Utf8PathBuf>,
66//! }
67//!
68//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
69//! let args = Args::parse();
70//! let listener = TcpListener::bind(args.bind_address)?;
71//!
72//! // Publish the address that was actually bound.
73//! if let Some(path) = &args.port_file {
74//!     port_file::write(path, listener.local_addr()?)?;
75//! }
76//!
77//! // Now, serve connections on `listener`.
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! In the parent process:
83//!
84//! ```no_run
85//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
86//! use port_file::{PollInterval, Timeout};
87//! use std::{net::SocketAddr, process::Command, time::Duration};
88//!
89//! // Pick a location for the port file. A fresh temporary directory keeps
90//! // concurrent runs isolated from each other.
91//! let dir = camino_tempfile::tempdir()?;
92//! let path = dir.path().join("service.port");
93//!
94//! // Spawn the child, telling it where to bind and where to write the
95//! // port file.
96//! let mut child = Command::new("my-service")
97//!     .args(["--bind-address", "[::1]:0"])
98//!     .args(["--port-file", path.as_str()])
99//!     .spawn()?;
100//!
101//! // Wait for the child to publish its address. Passing `child.try_wait()`
102//! // means a child that dies before binding fails fast rather than hanging
103//! // until the timeout.
104//! let addr: SocketAddr = match port_file::wait_for_blocking(
105//!     &path,
106//!     || child.try_wait(),
107//!     PollInterval(Duration::from_millis(25)),
108//!     Timeout(Duration::from_secs(30)),
109//! ) {
110//!     Ok(addr) => addr,
111//!     Err(error) => {
112//!         // The child is not stopped automatically on failure, so remember
113//!         // to kill and reap it so it doesn't outlive the supervisor.
114//!         //
115//!         // In this example, we ignore cleanup errors, since the error
116//!         // produced by wait_for_blocking is the one worth reporting.
117//!         let _ = child.kill();
118//!         let _ = child.wait();
119//!         return Err(error.into());
120//!     }
121//! };
122//!
123//! // The service is now reachable at `addr`.
124//! println!("service is listening at {addr}");
125//!
126//! // Now you can make requests to addr.
127//! # Ok(())
128//! # }
129//! ```
130//!
131//! ## Notes
132//!
133//! The port file must not already exist. A write to an existing file will fail
134//! with a [`WriteStage::Persist`] error. It is strongly recommended that a
135//! fresh temporary directory is used to store the port file in.
136//!
137//! If you're using Tokio, use [`wait_for`] instead, which is the async
138//! equivalent of [`wait_for_blocking`].
139//!
140//! ## Extensions
141//!
142//! The port file logic is not tied to [`SocketAddr`]–rather, it is generic over
143//! any type that implements both `FromStr` and `Display`. To communicate
144//! information more complicated than a single IP-port pair, use your own type
145//! that implements these traits with roundtrip serialization. (This can even be
146//! something like a JSON blob.)
147//!
148//! # Alternatives
149//!
150//! ## Roll it yourself
151//!
152//! This crate is quite minimal and straightforward. You're welcome to write
153//! your own version of this. There are a few details this crate gets right that
154//! you may want to copy. In particular:
155//!
156//! * The write side uses [atomic writes](atomicwrites) and refuses to overwrite
157//!   existing files.
158//! * The read side has careful handling for permanent errors to avoid waiting
159//!   out the entire timeout.
160//!
161//! ## In-process services
162//!
163//! If your service can run within the same process as the supervisor, you may
164//! simply be able to make an in-memory function call to the service to get the
165//! bound port.
166//!
167//! ## Query the kernel
168//!
169//! You can use tools like [`ss`] or [`lsof`] on Linux, or equivalents on other
170//! operating systems, to see which sockets a child process has active. But this
171//! is a heavyweight approach for a purely user-level concern. This approach has
172//! also been known to [cause process
173//! crashes](https://www.illumos.org/issues/18222) in some situations.
174//!
175//! ## Unix domain sockets
176//!
177//! Instead of a TCP or UDP port, you can use a [Unix domain socket] (UDS), also
178//! known as a local socket. If possible, this is the preferred way to bind to
179//! ephemeral ports. But this has a few limitations:
180//!
181//! * While UDS (despite the name) are available on Windows, as of Rust 1.96
182//!   they're [not part](https://github.com/rust-lang/rust/issues/150487) of the
183//!   stable Rust standard library yet. Many ecosystem tools might only support
184//!   UDS on Unix-like platforms.
185//! * Many tools do not support UDS. For example, it is uncommon for HTTP servers
186//!   and clients to support UDS as their transport (though there are crates like
187//!   [hyperlocal] for this, and reqwest 0.13 has a [`unix_socket` method]).
188//! * If UDS are only used in tests, this would introduce a divergence between
189//!   test and production code. Bugs can often be specific to a particular
190//!   transport layer.
191//!
192//! [`SocketAddr`]: std::net::SocketAddr
193//! [Unix domain socket]: std::os::unix::net::UnixListener
194//! [hyperlocal]: https://crates.io/crates/hyperlocal
195//! [`unix_socket` method]: https://docs.rs/reqwest/0.13/reqwest/struct.ClientBuilder.html#method.unix_socket
196//! [`ss`]: https://man7.org/linux/man-pages/man8/ss.8.html
197//! [`lsof`]: https://man7.org/linux/man-pages/man8/lsof.8.html
198
199mod read;
200mod write;
201
202#[cfg(feature = "tokio")]
203pub use read::wait_for;
204pub use read::{
205    PollError, PollInterval, Readiness, Timeout, WaitForError, poll_once,
206    wait_for_blocking,
207};
208pub use write::{WriteError, WriteStage, write};