1use std::{collections::HashMap, sync::mpsc};
28
29use bytes::Bytes;
30use tokio::runtime::Runtime;
31use zeromq::{PullSocket, PushSocket, Socket, SocketRecv, SocketSend, ZmqMessage};
32
33#[derive(Debug, thiserror::Error)]
35pub enum NetError {
36 #[error("zmq: {0}")]
38 Zmq(#[from] zeromq::ZmqError),
39 #[error("starting the PD socket runtime: {0}")]
41 Runtime(#[from] std::io::Error),
42 #[error("the bound endpoint {0} has no port")]
44 Port(String),
45}
46
47pub struct Sockets {
49 runtime: Runtime,
50 outgoing: tokio::sync::mpsc::UnboundedSender<(String, Vec<Bytes>)>,
51 port: u16,
52}
53
54impl Sockets {
55 pub fn start(host: &str, incoming: mpsc::Sender<Vec<Bytes>>) -> Result<Self, NetError> {
58 let runtime = tokio::runtime::Builder::new_multi_thread()
59 .worker_threads(1)
60 .thread_name("ts-pd-net")
61 .enable_all()
62 .build()?;
63 let address = if host.contains(':') {
64 format!("tcp://[{host}]:0")
65 } else {
66 format!("tcp://{host}:0")
67 };
68 let (pull, port) = runtime.block_on(async {
69 let mut pull = PullSocket::new();
70 let bound = pull.bind(&address).await?.to_string();
71 let port = bound
72 .rsplit(':')
73 .next()
74 .and_then(|port| port.parse().ok())
75 .ok_or(NetError::Port(bound.clone()))?;
76 Ok::<_, NetError>((pull, port))
77 })?;
78 runtime.spawn(async move {
79 let mut pull = pull;
80 loop {
81 match pull.recv().await {
82 Ok(message) => {
83 if incoming.send(message.into_vec()).is_err() {
84 return;
85 }
86 }
87 Err(error) => {
88 tracing::warn!(%error, "PD control socket closed");
89 return;
90 }
91 }
92 }
93 });
94 let (outgoing, mut queue) = tokio::sync::mpsc::unbounded_channel::<(String, Vec<Bytes>)>();
95 runtime.spawn(async move {
96 let mut sockets: HashMap<String, PushSocket> = HashMap::new();
97 while let Some((endpoint, frames)) = queue.recv().await {
98 if !sockets.contains_key(&endpoint) {
99 let mut push = PushSocket::new();
100 if let Err(error) = push.connect(&endpoint).await {
101 tracing::warn!(endpoint, %error, "cannot reach a PD peer");
102 continue;
103 }
104 sockets.insert(endpoint.clone(), push);
105 }
106 let Ok(message) = ZmqMessage::try_from(frames) else {
107 continue;
108 };
109 let push = sockets.get_mut(&endpoint).expect("connected above");
110 if let Err(error) = push.send(message).await {
111 tracing::warn!(endpoint, %error, "a PD message did not go out");
112 sockets.remove(&endpoint);
113 }
114 }
115 });
116 Ok(Self {
117 runtime,
118 outgoing,
119 port,
120 })
121 }
122
123 pub fn port(&self) -> u16 {
125 self.port
126 }
127
128 pub fn send(&self, host: &str, port: u16, frames: Vec<Bytes>) {
130 let endpoint = if host.contains(':') && !host.starts_with('[') {
131 format!("tcp://[{host}]:{port}")
132 } else {
133 format!("tcp://{host}:{port}")
134 };
135 if self.outgoing.send((endpoint, frames)).is_err() {
136 tracing::warn!("the PD socket runtime stopped");
137 }
138 }
139}
140
141impl Drop for Sockets {
142 fn drop(&mut self) {
143 self.runtime
146 .block_on(async { tokio::time::sleep(std::time::Duration::from_millis(50)).await });
147 }
148}
149
150pub fn ascii(value: impl ToString) -> Bytes {
152 Bytes::from(value.to_string())
153}
154
155pub fn parse_uint(frame: &[u8], maximum: u64) -> Option<u64> {
157 let digits = maximum.to_string().len();
158 if frame.is_empty()
159 || frame.len() > digits
160 || !frame.iter().all(u8::is_ascii_digit)
161 || (frame.len() > 1 && frame[0] == b'0')
162 {
163 return None;
164 }
165 let value: u64 = std::str::from_utf8(frame).ok()?.parse().ok()?;
166 (value <= maximum).then_some(value)
167}
168
169#[cfg(test)]
170mod tests {
171 use std::time::Duration;
172
173 use super::*;
174
175 #[test]
176 fn a_pushed_message_reaches_the_pull_socket() {
177 let (incoming, received) = mpsc::channel();
178 let receiver = Sockets::start("127.0.0.1", incoming).unwrap();
179 let (unused, _never) = mpsc::channel();
180 let sender = Sockets::start("127.0.0.1", unused).unwrap();
181 sender.send(
182 "127.0.0.1",
183 receiver.port(),
184 vec![ascii(7), Bytes::from_static(b"x")],
185 );
186 let frames = received.recv_timeout(Duration::from_secs(10)).unwrap();
187 assert_eq!(frames, [Bytes::from_static(b"7"), Bytes::from_static(b"x")]);
188 }
189
190 #[test]
191 fn unsigned_frames_are_canonical() {
192 assert_eq!(parse_uint(b"42", 100), Some(42));
193 assert_eq!(parse_uint(b"042", 100), None);
194 assert_eq!(parse_uint(b"101", 100), None);
195 assert_eq!(parse_uint(b"", 100), None);
196 assert_eq!(parse_uint(b"4a", 100), None);
197 }
198}