1#![warn(missing_docs)]
18#![allow(clippy::module_name_repetitions)]
19
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::{Duration, SystemTime, UNIX_EPOCH};
23
24use zerodds_dcps::runtime::UserReaderConfig;
25use zerodds_qos::{DeadlineQosPolicy, DurabilityKind, LivelinessQosPolicy, OwnershipKind};
26use zerodds_rtps::wire_types::GuidPrefix;
27
28#[must_use]
34pub fn stable_prefix(marker: u8) -> GuidPrefix {
35 let mut bytes = [0u8; 12];
36 let pid = std::process::id();
37 bytes[0..4].copy_from_slice(&pid.to_le_bytes());
38 let nanos = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap_or_default()
41 .subsec_nanos();
42 bytes[4..8].copy_from_slice(&nanos.to_le_bytes());
43 bytes[8] = marker;
44 GuidPrefix::from_bytes(bytes)
45}
46
47#[must_use]
50pub fn participant_guid(prefix: GuidPrefix) -> [u8; 16] {
51 let mut g = [0u8; 16];
52 g[..12].copy_from_slice(&prefix.0);
53 g[12..15].copy_from_slice(&[0, 0, 0]);
54 g[15] = 0xC1;
55 g
56}
57
58#[must_use]
60pub fn unix_ns_now() -> i64 {
61 let dur = SystemTime::now()
62 .duration_since(UNIX_EPOCH)
63 .unwrap_or_default();
64 let total = dur
65 .as_secs()
66 .saturating_mul(1_000_000_000)
67 .saturating_add(u64::from(dur.subsec_nanos()));
68 i64::try_from(total).unwrap_or(i64::MAX)
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct DurationParseError {
74 pub input: String,
76}
77
78impl std::fmt::Display for DurationParseError {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(f, "invalid duration spec: {}", self.input)
81 }
82}
83
84impl std::error::Error for DurationParseError {}
85
86pub fn parse_duration(s: &str) -> Result<Duration, DurationParseError> {
91 let bad = || DurationParseError {
92 input: s.to_string(),
93 };
94 let (num, unit) = s
95 .find(|c: char| c.is_alphabetic())
96 .map_or((s, "s"), |idx| (&s[..idx], &s[idx..]));
97 let n: u64 = num.parse().map_err(|_| bad())?;
98 let secs = match unit {
99 "s" | "" => n,
100 "m" => n.checked_mul(60).ok_or_else(bad)?,
101 "h" => n.checked_mul(3600).ok_or_else(bad)?,
102 _ => return Err(bad()),
103 };
104 Ok(Duration::from_secs(secs))
105}
106
107pub fn install_signal_handler(stop: Arc<AtomicBool>) {
111 install_inner(stop);
112}
113
114#[cfg(unix)]
115fn install_inner(stop: Arc<AtomicBool>) {
116 use std::sync::Mutex;
117 static HOOK: Mutex<Option<Arc<AtomicBool>>> = Mutex::new(None);
118 if let Ok(mut g) = HOOK.lock() {
119 *g = Some(stop);
120 }
121 extern "C" fn handler(_: i32) {
122 if let Ok(g) = HOOK.lock() {
123 if let Some(s) = g.as_ref() {
124 s.store(true, Ordering::Relaxed);
125 }
126 }
127 }
128 unsafe {
131 libc::signal(libc::SIGINT, handler as usize);
132 libc::signal(libc::SIGTERM, handler as usize);
133 }
134}
135
136#[cfg(not(unix))]
137fn install_inner(_stop: Arc<AtomicBool>) {}
138
139#[must_use]
141pub fn raw_reader_config(topic: &str) -> UserReaderConfig {
142 raw_reader_config_typed(topic, RAW_BYTES_TYPE_NAME)
143}
144
145pub const RAW_BYTES_TYPE_NAME: &str = "zerodds::RawBytes";
148
149#[must_use]
162pub fn raw_reader_config_typed(topic: &str, type_name: &str) -> UserReaderConfig {
163 UserReaderConfig {
164 topic_name: topic.to_string(),
165 type_name: type_name.to_string(),
166 reliable: true,
167 durability: DurabilityKind::Volatile,
168 deadline: DeadlineQosPolicy::default(),
169 liveliness: LivelinessQosPolicy::default(),
170 ownership: OwnershipKind::Shared,
171 partition: Vec::new(),
172 user_data: Vec::new(),
173 topic_data: Vec::new(),
174 group_data: Vec::new(),
175 type_identifier: zerodds_types::TypeIdentifier::None,
176 type_consistency: zerodds_types::qos::TypeConsistencyEnforcement::default(),
177 data_representation_offer: None,
178 }
179}
180
181#[derive(Debug, Default)]
190pub struct TypeFollower {
191 topics: std::collections::HashSet<String>,
193 seen: std::collections::HashSet<(String, String)>,
195}
196
197impl TypeFollower {
198 #[must_use]
200 pub fn new<I, S>(topics: I) -> Self
201 where
202 I: IntoIterator<Item = S>,
203 S: Into<String>,
204 {
205 Self {
206 topics: topics.into_iter().map(Into::into).collect(),
207 seen: std::collections::HashSet::new(),
208 }
209 }
210
211 #[must_use]
213 pub fn wants_topic(&self, topic: &str) -> bool {
214 self.topics.is_empty() || self.topics.contains(topic)
215 }
216
217 pub fn newly_seen<I, A, B>(&mut self, discovered: I) -> Vec<(String, String)>
222 where
223 I: IntoIterator<Item = (A, B)>,
224 A: Into<String>,
225 B: Into<String>,
226 {
227 let mut fresh = Vec::new();
228 for (t, ty) in discovered {
229 let pair = (t.into(), ty.into());
230 if !self.wants_topic(&pair.0) {
231 continue;
232 }
233 if self.seen.insert(pair.clone()) {
234 fresh.push(pair);
235 }
236 }
237 fresh
238 }
239
240 pub fn poll(&mut self, runtime: &zerodds_dcps::runtime::DcpsRuntime) -> Vec<(String, String)> {
243 self.newly_seen(runtime.discovered_publication_topics())
244 }
245}
246
247#[cfg(test)]
248#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn raw_reader_config_defaults_to_rawbytes_type() {
254 let c = raw_reader_config("Track");
255 assert_eq!(c.topic_name, "Track");
256 assert_eq!(c.type_name, RAW_BYTES_TYPE_NAME);
257 }
258
259 #[test]
260 fn raw_reader_config_typed_carries_the_supplied_type_name() {
261 let c = raw_reader_config_typed("Track", "cuas::Track");
262 assert_eq!(c.topic_name, "Track");
263 assert_eq!(c.type_name, "cuas::Track");
264 assert!(c.reliable);
266 assert!(matches!(
267 c.type_identifier,
268 zerodds_types::TypeIdentifier::None
269 ));
270 }
271
272 #[test]
273 fn type_follower_reports_each_pair_once() {
274 let mut tf = TypeFollower::new(Vec::<String>::new()); let fresh = tf.newly_seen([("Track", "cuas::Track"), ("Track", "cuas::Track")]);
276 assert_eq!(
277 fresh,
278 vec![("Track".to_string(), "cuas::Track".to_string())]
279 );
280 assert!(tf.newly_seen([("Track", "cuas::Track")]).is_empty());
282 }
283
284 #[test]
285 fn type_follower_picks_up_late_joiner_types() {
286 let mut tf = TypeFollower::new(Vec::<String>::new());
287 assert_eq!(tf.newly_seen([("Track", "cuas::Track")]).len(), 1);
288 let fresh = tf.newly_seen([("Track", "cuas::Track"), ("Track", "cuas::TrackV2")]);
290 assert_eq!(
291 fresh,
292 vec![("Track".to_string(), "cuas::TrackV2".to_string())]
293 );
294 }
295
296 #[test]
297 fn type_follower_filters_by_topic_allowlist() {
298 let mut tf = TypeFollower::new(["Track"]);
299 assert!(tf.wants_topic("Track"));
300 assert!(!tf.wants_topic("Other"));
301 let fresh = tf.newly_seen([("Track", "cuas::Track"), ("Other", "x::Y")]);
302 assert_eq!(
303 fresh,
304 vec![("Track".to_string(), "cuas::Track".to_string())]
305 );
306 }
307
308 #[test]
309 fn parse_duration_seconds() {
310 assert_eq!(parse_duration("5").unwrap(), Duration::from_secs(5));
311 assert_eq!(parse_duration("5s").unwrap(), Duration::from_secs(5));
312 }
313
314 #[test]
315 fn parse_duration_minutes() {
316 assert_eq!(parse_duration("3m").unwrap(), Duration::from_secs(180));
317 }
318
319 #[test]
320 fn parse_duration_hours() {
321 assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
322 }
323
324 #[test]
325 fn parse_duration_rejects_garbage() {
326 assert!(parse_duration("3x").is_err());
327 assert!(parse_duration("abc").is_err());
328 }
329
330 #[test]
331 fn stable_prefix_carries_marker() {
332 let p = stable_prefix(0xAB);
333 assert_eq!(p.0[8], 0xAB);
334 }
335
336 #[test]
337 fn participant_guid_has_participant_eid() {
338 let prefix = stable_prefix(0x42);
339 let g = participant_guid(prefix);
340 assert_eq!(&g[..12], &prefix.0[..]);
341 assert_eq!(&g[12..], &[0, 0, 0, 0xC1]);
342 }
343
344 #[test]
345 fn unix_ns_now_is_positive() {
346 assert!(unix_ns_now() > 0);
347 }
348}