Skip to main content

flag_rs/
completion_timeout.rs

1//! Timeout handling for completion functions
2//!
3//! This module provides utilities to wrap completion functions with timeouts
4//! to prevent slow operations from hanging the shell completion experience.
5
6use crate::completion::CompletionResult;
7use crate::context::Context;
8use crate::error::{Error, Result};
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::Duration;
12
13/// Default timeout for completion operations (2 seconds)
14pub const DEFAULT_COMPLETION_TIMEOUT: Duration = Duration::from_secs(2);
15
16/// Wraps a completion function with a timeout
17///
18/// This function ensures that completion operations don't hang indefinitely
19/// by imposing a timeout. If the operation doesn't complete within the timeout,
20/// it returns a partial result with a help message indicating the timeout.
21///
22/// # Arguments
23///
24/// * `f` - The completion function to wrap
25/// * `timeout` - Maximum duration to wait for completion
26/// * `ctx` - The context for the completion
27/// * `prefix` - The prefix being completed
28///
29/// # Returns
30///
31/// Returns the completion result if it completes within the timeout,
32/// or a partial result with timeout information if it exceeds the timeout.
33pub fn with_timeout<F>(
34    f: F,
35    timeout: Duration,
36    ctx: &Context,
37    prefix: &str,
38) -> Result<CompletionResult>
39where
40    F: FnOnce(&Context, &str) -> Result<CompletionResult> + Send + 'static,
41{
42    let result: Arc<Mutex<Option<Result<CompletionResult>>>> = Arc::new(Mutex::new(None));
43    let result_clone = Arc::clone(&result);
44
45    let ctx_clone = Context::new(ctx.args().to_vec());
46    let prefix_clone = prefix.to_string();
47
48    let handle = thread::spawn(move || {
49        let completion_result = f(&ctx_clone, &prefix_clone);
50        if let Ok(mut result_lock) = result_clone.lock() {
51            *result_lock = Some(completion_result);
52        }
53    });
54
55    if matches!(handle.join_timeout(timeout), Ok(())) {
56        // Thread completed within timeout
57        result.lock().map_or_else(
58            |_| {
59                Err(Error::Completion(
60                    "Failed to access completion result".to_string(),
61                ))
62            },
63            |mut result_lock| {
64                result_lock.take().unwrap_or_else(|| {
65                    Err(Error::Completion(
66                        "Completion function did not return a result".to_string(),
67                    ))
68                })
69            },
70        )
71    } else {
72        // Timeout occurred
73        let mut partial_result = CompletionResult::new();
74        partial_result = partial_result.add_help_text(
75            "⚠️  Completion timed out - results may be incomplete. Try a more specific prefix.",
76        );
77
78        // If we have any partial results from before the timeout, include them
79        if let Ok(result_lock) = result.lock()
80            && let Some(Ok(ref partial)) = *result_lock
81        {
82            partial_result = partial_result.merge(partial.clone());
83        }
84
85        Ok(partial_result)
86    }
87}
88
89/// Creates a timeout-wrapped completion function
90///
91/// This is a convenience function that creates a new completion function
92/// with built-in timeout handling.
93///
94/// # Arguments
95///
96/// * `timeout` - Maximum duration to wait for completion
97/// * `f` - The original completion function
98///
99/// # Returns
100///
101/// A new completion function that enforces the timeout
102pub fn make_timeout_completion<F>(
103    timeout: Duration,
104    f: F,
105) -> impl Fn(&Context, &str) -> Result<CompletionResult>
106where
107    F: Fn(&Context, &str) -> Result<CompletionResult> + Clone + Send + 'static,
108{
109    move |ctx: &Context, prefix: &str| {
110        let f_clone = f.clone();
111        with_timeout(move |c, p| f_clone(c, p), timeout, ctx, prefix)
112    }
113}
114
115// Extension trait to add timeout support to threads
116trait JoinHandleExt<T>: Sized {
117    fn join_timeout(self, timeout: Duration) -> std::result::Result<T, Self>;
118}
119
120impl<T> JoinHandleExt<T> for thread::JoinHandle<T> {
121    fn join_timeout(self, timeout: Duration) -> std::result::Result<T, Self> {
122        let start = std::time::Instant::now();
123
124        loop {
125            if self.is_finished() {
126                return self.join().map_or_else(|_| panic!("Thread panicked"), Ok);
127            }
128
129            if start.elapsed() >= timeout {
130                return Err(self);
131            }
132
133            thread::sleep(Duration::from_millis(10));
134        }
135    }
136}
137
138impl CompletionResult {
139    /// Merges two completion results, combining their values and help messages
140    #[must_use]
141    pub fn merge(mut self, other: Self) -> Self {
142        self.values.extend(other.values);
143        self.descriptions.extend(other.descriptions);
144        self.active_help.extend(other.active_help);
145        self
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::thread;
153    use std::time::Duration;
154
155    #[test]
156    fn test_completion_with_timeout_success() {
157        let ctx = Context::new(vec![]);
158
159        let result = with_timeout(
160            |_ctx, prefix| {
161                // Fast completion
162                Ok(CompletionResult::new()
163                    .add("item1")
164                    .add("item2")
165                    .add(format!("prefix_{prefix}")))
166            },
167            Duration::from_secs(1),
168            &ctx,
169            "test",
170        );
171
172        assert!(result.is_ok());
173        let completion = result.unwrap();
174        assert_eq!(completion.values.len(), 3);
175        assert!(completion.values.contains(&"prefix_test".to_string()));
176    }
177
178    #[test]
179    fn test_completion_with_timeout_exceeded() {
180        let ctx = Context::new(vec![]);
181
182        let result = with_timeout(
183            |_ctx, _prefix| {
184                // Slow completion that will timeout
185                thread::sleep(Duration::from_secs(2));
186                Ok(CompletionResult::new().add("never_returned"))
187            },
188            Duration::from_millis(100),
189            &ctx,
190            "test",
191        );
192
193        assert!(result.is_ok());
194        let completion = result.unwrap();
195        // Should have timeout warning in active help
196        assert!(!completion.active_help.is_empty());
197        assert!(
198            completion.active_help[0]
199                .message
200                .contains("Completion timed out")
201        );
202    }
203
204    #[test]
205    fn test_make_timeout_completion() {
206        let wrapped = make_timeout_completion(Duration::from_secs(1), |_ctx, prefix| {
207            Ok(CompletionResult::new().add(format!("result_{prefix}")))
208        });
209
210        let ctx = Context::new(vec![]);
211        let result = wrapped(&ctx, "test").unwrap();
212        assert_eq!(result.values.len(), 1);
213        assert_eq!(result.values[0], "result_test");
214    }
215}