Skip to main content

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}