rust_expect/
sync.rs

1//! Synchronous wrapper for async expect operations.
2//!
3//! This module provides a blocking API for users who prefer or require
4//! synchronous operations instead of async/await.
5
6use std::time::Duration;
7
8use tokio::runtime::{Builder, Runtime};
9
10#[cfg(unix)]
11use crate::backend::AsyncPty;
12#[cfg(windows)]
13use crate::backend::WindowsAsyncPty;
14use crate::config::SessionConfig;
15use crate::error::Result;
16use crate::expect::Pattern;
17use crate::session::Session;
18use crate::types::{ControlChar, Match};
19
20/// A synchronous session wrapper.
21///
22/// This wraps an async session and provides blocking methods for
23/// use in synchronous contexts.
24#[cfg(unix)]
25pub struct SyncSession {
26    /// The tokio runtime.
27    runtime: Runtime,
28    /// The inner async session.
29    inner: Session<AsyncPty>,
30}
31
32/// A synchronous session wrapper (Windows).
33///
34/// This wraps an async session and provides blocking methods for
35/// use in synchronous contexts using Windows ConPTY.
36#[cfg(windows)]
37pub struct SyncSession {
38    /// The tokio runtime.
39    runtime: Runtime,
40    /// The inner async session.
41    inner: Session<WindowsAsyncPty>,
42}
43
44#[cfg(unix)]
45impl SyncSession {
46    /// Spawn a command and create a session.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if spawning fails.
51    pub fn spawn(command: &str, args: &[&str]) -> Result<Self> {
52        let runtime = Builder::new_current_thread()
53            .enable_all()
54            .build()
55            .map_err(|e| crate::error::ExpectError::io_context("creating tokio runtime", e))?;
56
57        let inner = runtime.block_on(Session::spawn(command, args))?;
58
59        Ok(Self { runtime, inner })
60    }
61
62    /// Spawn with custom configuration.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if spawning fails.
67    pub fn spawn_with_config(command: &str, args: &[&str], config: SessionConfig) -> Result<Self> {
68        let runtime = Builder::new_current_thread()
69            .enable_all()
70            .build()
71            .map_err(|e| crate::error::ExpectError::io_context("creating tokio runtime", e))?;
72
73        let inner = runtime.block_on(Session::spawn_with_config(command, args, config))?;
74
75        Ok(Self { runtime, inner })
76    }
77
78    /// Get the session configuration.
79    #[must_use]
80    pub const fn config(&self) -> &SessionConfig {
81        self.inner.config()
82    }
83
84    /// Check if the session is active.
85    #[must_use]
86    pub const fn is_active(&self) -> bool {
87        !self.inner.is_eof()
88    }
89
90    /// Get the child process ID.
91    #[must_use]
92    pub fn pid(&self) -> u32 {
93        self.inner.pid()
94    }
95
96    /// Send bytes to the session.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the write fails.
101    pub fn send(&mut self, data: &[u8]) -> Result<()> {
102        self.runtime.block_on(self.inner.send(data))
103    }
104
105    /// Send a string to the session.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the write fails.
110    pub fn send_str(&mut self, s: &str) -> Result<()> {
111        self.runtime.block_on(self.inner.send_str(s))
112    }
113
114    /// Send a line to the session.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the write fails.
119    pub fn send_line(&mut self, line: &str) -> Result<()> {
120        self.runtime.block_on(self.inner.send_line(line))
121    }
122
123    /// Send a control character.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the write fails.
128    pub fn send_control(&mut self, ctrl: ControlChar) -> Result<()> {
129        self.runtime.block_on(self.inner.send_control(ctrl))
130    }
131
132    /// Expect a pattern in the output.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error on timeout or EOF.
137    pub fn expect(&mut self, pattern: impl Into<Pattern>) -> Result<Match> {
138        self.runtime.block_on(self.inner.expect(pattern))
139    }
140
141    /// Expect a pattern with a specific timeout.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error on timeout or EOF.
146    pub fn expect_timeout(
147        &mut self,
148        pattern: impl Into<Pattern>,
149        timeout: Duration,
150    ) -> Result<Match> {
151        self.runtime
152            .block_on(self.inner.expect_timeout(pattern, timeout))
153    }
154
155    /// Get the current buffer contents.
156    #[must_use]
157    pub fn buffer(&mut self) -> String {
158        self.inner.buffer()
159    }
160
161    /// Clear the buffer.
162    pub fn clear_buffer(&mut self) {
163        self.inner.clear_buffer();
164    }
165
166    /// Resize the terminal.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the resize fails.
171    pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
172        self.runtime.block_on(self.inner.resize_pty(cols, rows))
173    }
174
175    /// Send a signal to the child process.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if sending the signal fails.
180    pub fn signal(&self, signal: i32) -> Result<()> {
181        self.inner.signal(signal)
182    }
183
184    /// Kill the child process.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if killing fails.
189    pub fn kill(&self) -> Result<()> {
190        self.inner.kill()
191    }
192
193    /// Run an async operation synchronously.
194    pub fn block_on<F, T>(&self, future: F) -> T
195    where
196        F: std::future::Future<Output = T>,
197    {
198        self.runtime.block_on(future)
199    }
200}
201
202#[cfg(windows)]
203impl SyncSession {
204    /// Spawn a command and create a session.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if spawning fails.
209    pub fn spawn(command: &str, args: &[&str]) -> Result<Self> {
210        let runtime = Builder::new_current_thread()
211            .enable_all()
212            .build()
213            .map_err(|e| crate::error::ExpectError::io_context("creating tokio runtime", e))?;
214
215        let inner = runtime.block_on(Session::spawn(command, args))?;
216
217        Ok(Self { runtime, inner })
218    }
219
220    /// Spawn with custom configuration.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if spawning fails.
225    pub fn spawn_with_config(command: &str, args: &[&str], config: SessionConfig) -> Result<Self> {
226        let runtime = Builder::new_current_thread()
227            .enable_all()
228            .build()
229            .map_err(|e| crate::error::ExpectError::io_context("creating tokio runtime", e))?;
230
231        let inner = runtime.block_on(Session::spawn_with_config(command, args, config))?;
232
233        Ok(Self { runtime, inner })
234    }
235
236    /// Get the session configuration.
237    #[must_use]
238    pub const fn config(&self) -> &SessionConfig {
239        self.inner.config()
240    }
241
242    /// Check if the session is active.
243    #[must_use]
244    pub fn is_active(&self) -> bool {
245        !self.inner.is_eof()
246    }
247
248    /// Get the child process ID.
249    #[must_use]
250    pub fn pid(&self) -> u32 {
251        self.inner.pid()
252    }
253
254    /// Send bytes to the session.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the write fails.
259    pub fn send(&mut self, data: &[u8]) -> Result<()> {
260        self.runtime.block_on(self.inner.send(data))
261    }
262
263    /// Send a string to the session.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the write fails.
268    pub fn send_str(&mut self, s: &str) -> Result<()> {
269        self.runtime.block_on(self.inner.send_str(s))
270    }
271
272    /// Send a line to the session.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the write fails.
277    pub fn send_line(&mut self, line: &str) -> Result<()> {
278        self.runtime.block_on(self.inner.send_line(line))
279    }
280
281    /// Send a control character.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if the write fails.
286    pub fn send_control(&mut self, ctrl: ControlChar) -> Result<()> {
287        self.runtime.block_on(self.inner.send_control(ctrl))
288    }
289
290    /// Expect a pattern in the output.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error on timeout or EOF.
295    pub fn expect(&mut self, pattern: impl Into<Pattern>) -> Result<Match> {
296        self.runtime.block_on(self.inner.expect(pattern))
297    }
298
299    /// Expect a pattern with a specific timeout.
300    ///
301    /// # Errors
302    ///
303    /// Returns an error on timeout or EOF.
304    pub fn expect_timeout(
305        &mut self,
306        pattern: impl Into<Pattern>,
307        timeout: Duration,
308    ) -> Result<Match> {
309        self.runtime
310            .block_on(self.inner.expect_timeout(pattern, timeout))
311    }
312
313    /// Get the current buffer contents.
314    #[must_use]
315    pub fn buffer(&mut self) -> String {
316        self.inner.buffer()
317    }
318
319    /// Clear the buffer.
320    pub fn clear_buffer(&mut self) {
321        self.inner.clear_buffer();
322    }
323
324    /// Resize the terminal.
325    ///
326    /// # Errors
327    ///
328    /// Returns an error if the resize fails.
329    pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
330        self.runtime.block_on(self.inner.resize_pty(cols, rows))
331    }
332
333    /// Check if the child process is still running.
334    #[must_use]
335    pub fn is_running(&self) -> bool {
336        self.inner.is_running()
337    }
338
339    /// Kill the child process.
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if killing fails.
344    pub fn kill(&self) -> Result<()> {
345        self.inner.kill()
346    }
347
348    /// Run an async operation synchronously.
349    pub fn block_on<F, T>(&self, future: F) -> T
350    where
351        F: std::future::Future<Output = T>,
352    {
353        self.runtime.block_on(future)
354    }
355}
356
357impl std::fmt::Debug for SyncSession {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        f.debug_struct("SyncSession").finish_non_exhaustive()
360    }
361}
362
363/// A blocking expect operation.
364pub struct BlockingExpect<'a> {
365    session: &'a mut SyncSession,
366    timeout: Duration,
367}
368
369impl<'a> BlockingExpect<'a> {
370    /// Create a new blocking expect operation.
371    pub const fn new(session: &'a mut SyncSession) -> Self {
372        let timeout = session.config().timeout.default;
373        Self { session, timeout }
374    }
375
376    /// Set the timeout.
377    #[must_use]
378    pub const fn timeout(mut self, timeout: Duration) -> Self {
379        self.timeout = timeout;
380        self
381    }
382
383    /// Execute the expect operation.
384    ///
385    /// # Errors
386    ///
387    /// Returns an error on timeout or EOF.
388    pub fn pattern(self, pattern: impl Into<Pattern>) -> Result<Match> {
389        self.session.expect_timeout(pattern, self.timeout)
390    }
391}
392
393/// Run async code synchronously.
394///
395/// This is a convenience function for running a single async operation
396/// without managing a runtime.
397///
398/// # Errors
399///
400/// Returns an error if the runtime cannot be created.
401pub fn block_on<F, T>(future: F) -> Result<T>
402where
403    F: std::future::Future<Output = T>,
404{
405    let runtime = Builder::new_current_thread()
406        .enable_all()
407        .build()
408        .map_err(|e| {
409            crate::error::ExpectError::io_context("creating tokio runtime for block_on", e)
410        })?;
411
412    Ok(runtime.block_on(future))
413}
414
415/// Spawn a session synchronously.
416///
417/// # Errors
418///
419/// Returns an error if spawning fails.
420pub fn spawn(command: &str, args: &[&str]) -> Result<SyncSession> {
421    SyncSession::spawn(command, args)
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn block_on_simple() {
430        let result = block_on(async { 42 });
431        assert!(result.is_ok());
432        assert_eq!(result.unwrap(), 42);
433    }
434
435    #[cfg(unix)]
436    #[test]
437    fn sync_session_spawn_echo() {
438        let mut session =
439            SyncSession::spawn("/bin/echo", &["hello"]).expect("Failed to spawn echo");
440
441        // Verify PID is valid
442        assert!(session.pid() > 0);
443
444        // Expect the output
445        let m = session.expect("hello").expect("Failed to expect hello");
446        assert!(m.matched.contains("hello"));
447    }
448}