zond_engine/core/models/timer.rs
1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7//! High-performance timing and lifecycle management for network scanning engines.
8//!
9//! Provides the `ScanTimer` data structure, a robust, general-purpose component
10//! designed to govern the lifecycle of synchronous and asynchronous scanning loops
11//! (e.g., port discovery, host discovery).
12
13use std::time::{Duration, Instant};
14
15/// Manages the loop lifecycle and operational boundaries for network scanning operations.
16///
17/// `ScanTimer` tracks hard deadlines, enforces minimum runtimes, and aborts loops
18/// early if a period of network "silence" (time since the last relevant packet)
19/// exceeds a configured maximum.
20#[derive(Debug, Clone, Copy)]
21pub struct ScanTimer {
22 // Configuration
23 hard_deadline: Instant,
24 min_runtime: Instant,
25 max_silence: Duration,
26
27 // State
28 last_activity: Instant,
29}
30
31impl ScanTimer {
32 /// Constructs a new `ScanTimer` with the specified operational limits.
33 ///
34 /// # Arguments
35 /// * `max_total_duration` - The absolute maximum time the scan is allowed to run.
36 /// * `min_runtime_duration` - The absolute minimum time the scan must run before it can abort due to silence.
37 /// * `max_silence` - The maximum duration of network silence allowed after the minimum runtime is reached.
38 pub fn new(
39 max_total_duration: Duration,
40 min_runtime_duration: Duration,
41 max_silence: Duration,
42 ) -> Self {
43 let now = Instant::now();
44 Self {
45 hard_deadline: now + max_total_duration,
46 min_runtime: now + min_runtime_duration,
47 max_silence,
48 last_activity: now,
49 }
50 }
51
52 /// Resets the internal "silence" tracker.
53 ///
54 /// This should be called whenever a relevant packet or activity is observed on the network.
55 pub fn mark_activity(&mut self) {
56 self.last_activity = Instant::now();
57 }
58
59 /// Calculates how long to wait for the next timeout event.
60 ///
61 /// Returns a fallback duration (e.g., 100ms) if the maximum silence period has already been exceeded.
62 pub fn time_until_next_tick(&self) -> Duration {
63 let now = Instant::now();
64 let time_since_last = now.duration_since(self.last_activity);
65
66 self.max_silence
67 .checked_sub(time_since_last)
68 .unwrap_or_else(|| Duration::from_millis(100))
69 }
70
71 /// Checks if the entire operation should abort due to hard limits or excessive silence.
72 ///
73 /// Returns `true` if:
74 /// 1. The current time has exceeded the `hard_deadline`.
75 /// 2. The `min_runtime` has elapsed AND the time since the last recorded activity exceeds `max_silence`.
76 pub fn has_expired(&self) -> bool {
77 let now = Instant::now();
78
79 if now > self.hard_deadline {
80 return true;
81 }
82
83 let time_since_last = now.duration_since(self.last_activity);
84 if now > self.min_runtime && time_since_last >= self.max_silence {
85 return true;
86 }
87
88 false
89 }
90
91 /// Helper to decide if a socket timeout is fatal or if the scan should continue.
92 ///
93 /// Returns `true` if the minimum runtime has been met, indicating that a timeout
94 /// could be a valid reason to break the loop.
95 pub fn should_break_on_timeout(&self) -> bool {
96 Instant::now() >= self.min_runtime
97 }
98}
99
100// ╔════════════════════════════════════════════╗
101// ║ ████████╗███████╗███████╗████████╗███████╗ ║
102// ║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝ ║
103// ║ ██║ █████╗ ███████╗ ██║ ███████╗ ║
104// ║ ██║ ██╔══╝ ╚════██║ ██║ ╚════██║ ║
105// ║ ██║ ███████╗███████║ ██║ ███████║ ║
106// ║ ╚═╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝ ║
107// ╚════════════════════════════════════════════╝
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::thread::sleep;
113
114 #[test]
115 fn test_initialization() {
116 let timer = ScanTimer::new(
117 Duration::from_secs(10),
118 Duration::from_secs(5),
119 Duration::from_secs(1),
120 );
121 assert!(!timer.has_expired());
122 assert!(!timer.should_break_on_timeout());
123 }
124
125 #[test]
126 fn test_mark_activity() {
127 let mut timer = ScanTimer::new(
128 Duration::from_secs(10),
129 Duration::from_secs(5),
130 Duration::from_millis(500),
131 );
132
133 let wait_time1 = timer.time_until_next_tick();
134 sleep(Duration::from_millis(50));
135 let wait_time2 = timer.time_until_next_tick();
136
137 assert!(wait_time2 < wait_time1);
138
139 timer.mark_activity();
140 let wait_time3 = timer.time_until_next_tick();
141
142 // Wait time should reset to near the original max_silence
143 assert!(wait_time3 > wait_time2);
144 }
145
146 #[test]
147 fn test_time_until_next_tick_fallback() {
148 let timer = ScanTimer::new(
149 Duration::from_secs(10),
150 Duration::from_secs(5),
151 Duration::from_millis(10),
152 );
153
154 sleep(Duration::from_millis(15)); // Exceed max_silence
155
156 // Should return the 100ms fallback since the time since last activity is greater than max_silence
157 assert_eq!(timer.time_until_next_tick(), Duration::from_millis(100));
158 }
159
160 #[test]
161 fn test_hard_deadline_expiration() {
162 let timer = ScanTimer::new(
163 Duration::from_millis(10), // short hard deadline
164 Duration::from_millis(100), // long min runtime (will not be reached)
165 Duration::from_secs(1),
166 );
167
168 assert!(!timer.has_expired());
169 sleep(Duration::from_millis(15));
170 assert!(timer.has_expired());
171 }
172
173 #[test]
174 fn test_silence_expiration() {
175 let timer = ScanTimer::new(
176 Duration::from_secs(10),
177 Duration::from_millis(10), // short min runtime
178 Duration::from_millis(10), // short max silence
179 );
180
181 assert!(!timer.has_expired());
182 sleep(Duration::from_millis(25)); // Exceed both min_runtime and max_silence
183 assert!(timer.has_expired());
184 }
185
186 #[test]
187 fn test_should_break_on_timeout() {
188 let timer = ScanTimer::new(
189 Duration::from_secs(10),
190 Duration::from_millis(10),
191 Duration::from_secs(1),
192 );
193
194 assert!(!timer.should_break_on_timeout());
195 sleep(Duration::from_millis(15));
196 assert!(timer.should_break_on_timeout());
197 }
198}