codex_shell_escalation/unix/
stopwatch.rs1use std::future::Future;
2use std::sync::Arc;
3use std::time::Duration;
4use std::time::Instant;
5
6use tokio::sync::Mutex;
7use tokio::sync::Notify;
8use tokio_util::sync::CancellationToken;
9
10#[derive(Clone, Debug)]
11pub struct Stopwatch {
12 limit: Option<Duration>,
13 inner: Arc<Mutex<StopwatchState>>,
14 notify: Arc<Notify>,
15}
16
17#[derive(Debug)]
18struct StopwatchState {
19 elapsed: Duration,
20 running_since: Option<Instant>,
21 active_pauses: u32,
22}
23
24impl Stopwatch {
25 pub fn new(limit: Duration) -> Self {
26 Self {
27 inner: Arc::new(Mutex::new(StopwatchState {
28 elapsed: Duration::ZERO,
29 running_since: Some(Instant::now()),
30 active_pauses: 0,
31 })),
32 notify: Arc::new(Notify::new()),
33 limit: Some(limit),
34 }
35 }
36
37 pub fn unlimited() -> Self {
38 Self {
39 inner: Arc::new(Mutex::new(StopwatchState {
40 elapsed: Duration::ZERO,
41 running_since: Some(Instant::now()),
42 active_pauses: 0,
43 })),
44 notify: Arc::new(Notify::new()),
45 limit: None,
46 }
47 }
48
49 pub fn cancellation_token(&self) -> CancellationToken {
50 let token = CancellationToken::new();
51 let Some(limit) = self.limit else {
52 return token;
53 };
54 let cancel = token.clone();
55 let inner = Arc::clone(&self.inner);
56 let notify = Arc::clone(&self.notify);
57 tokio::spawn(async move {
58 loop {
59 let (remaining, running) = {
60 let guard = inner.lock().await;
61 let elapsed = guard.elapsed
62 + guard
63 .running_since
64 .map(|since| since.elapsed())
65 .unwrap_or_default();
66 if elapsed >= limit {
67 break;
68 }
69 (limit - elapsed, guard.running_since.is_some())
70 };
71
72 if !running {
73 notify.notified().await;
74 continue;
75 }
76
77 let sleep = tokio::time::sleep(remaining);
78 tokio::pin!(sleep);
79 tokio::select! {
80 _ = &mut sleep => {
81 break;
82 }
83 _ = notify.notified() => {
84 continue;
85 }
86 }
87 }
88 cancel.cancel();
89 });
90 token
91 }
92
93 pub async fn pause_for<F, T>(&self, fut: F) -> T
98 where
99 F: Future<Output = T>,
100 {
101 self.pause().await;
102 let result = fut.await;
103 self.resume().await;
104 result
105 }
106
107 async fn pause(&self) {
108 let mut guard = self.inner.lock().await;
109 guard.active_pauses += 1;
110 if guard.active_pauses == 1
111 && let Some(since) = guard.running_since.take()
112 {
113 guard.elapsed += since.elapsed();
114 self.notify.notify_waiters();
115 }
116 }
117
118 async fn resume(&self) {
119 let mut guard = self.inner.lock().await;
120 if guard.active_pauses == 0 {
121 return;
122 }
123 guard.active_pauses -= 1;
124 if guard.active_pauses == 0 && guard.running_since.is_none() {
125 guard.running_since = Some(Instant::now());
126 self.notify.notify_waiters();
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::Stopwatch;
134 use tokio::time::Duration;
135 use tokio::time::Instant;
136 use tokio::time::sleep;
137 use tokio::time::timeout;
138
139 #[tokio::test]
140 async fn cancellation_receiver_fires_after_limit() {
141 let stopwatch = Stopwatch::new(Duration::from_millis(50));
142 let token = stopwatch.cancellation_token();
143 let start = Instant::now();
144 token.cancelled().await;
145 assert!(start.elapsed() >= Duration::from_millis(50));
146 }
147
148 #[tokio::test]
149 async fn pause_prevents_timeout_until_resumed() {
150 let stopwatch = Stopwatch::new(Duration::from_millis(50));
151 let token = stopwatch.cancellation_token();
152
153 let pause_handle = tokio::spawn({
154 let stopwatch = stopwatch.clone();
155 async move {
156 stopwatch
157 .pause_for(async {
158 sleep(Duration::from_millis(100)).await;
159 })
160 .await;
161 }
162 });
163
164 assert!(
165 timeout(Duration::from_millis(30), token.cancelled())
166 .await
167 .is_err()
168 );
169
170 pause_handle.await.expect("pause task should finish");
171
172 token.cancelled().await;
173 }
174
175 #[tokio::test]
176 async fn overlapping_pauses_only_resume_once() {
177 let stopwatch = Stopwatch::new(Duration::from_millis(50));
178 let token = stopwatch.cancellation_token();
179
180 let pause1 = {
182 let stopwatch = stopwatch.clone();
183 tokio::spawn(async move {
184 stopwatch
185 .pause_for(async {
186 sleep(Duration::from_millis(80)).await;
187 })
188 .await;
189 })
190 };
191
192 let pause2 = {
194 let stopwatch = stopwatch.clone();
195 tokio::spawn(async move {
196 stopwatch
197 .pause_for(async {
198 sleep(Duration::from_millis(30)).await;
199 })
200 .await;
201 })
202 };
203
204 assert!(
206 timeout(Duration::from_millis(40), token.cancelled())
207 .await
208 .is_err()
209 );
210
211 pause2.await.expect("short pause should complete");
212
213 assert!(
215 timeout(Duration::from_millis(30), token.cancelled())
216 .await
217 .is_err()
218 );
219
220 pause1.await.expect("long pause should complete");
221
222 token.cancelled().await;
224 }
225
226 #[tokio::test]
227 async fn unlimited_stopwatch_never_cancels() {
228 let stopwatch = Stopwatch::unlimited();
229 let token = stopwatch.cancellation_token();
230
231 assert!(
232 timeout(Duration::from_millis(30), token.cancelled())
233 .await
234 .is_err()
235 );
236 }
237}