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