rust_expect/session/
handle.rs

1//! Session handle for interacting with spawned processes.
2//!
3//! This module provides the main `Session` type that users interact with
4//! to control spawned processes, send input, and expect output.
5
6use std::sync::Arc;
7use std::time::Duration;
8
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::sync::Mutex;
11
12#[cfg(unix)]
13use crate::backend::{AsyncPty, PtyConfig, PtySpawner};
14#[cfg(windows)]
15use crate::backend::{PtyConfig, PtySpawner, WindowsAsyncPty};
16use crate::config::SessionConfig;
17use crate::dialog::{Dialog, DialogExecutor, DialogResult};
18use crate::error::{ExpectError, Result};
19use crate::expect::{ExpectState, MatchResult, Matcher, Pattern, PatternManager, PatternSet};
20use crate::interact::InteractBuilder;
21use crate::types::{ControlChar, Dimensions, Match, ProcessExitStatus, SessionId, SessionState};
22
23/// A session handle for interacting with a spawned process.
24///
25/// The session provides methods to send input, expect patterns in output,
26/// and manage the lifecycle of the process.
27pub struct Session<T: AsyncReadExt + AsyncWriteExt + Unpin + Send> {
28    /// The underlying transport (PTY, SSH channel, etc.).
29    transport: Arc<Mutex<T>>,
30    /// Session configuration.
31    config: SessionConfig,
32    /// Pattern matcher.
33    matcher: Matcher,
34    /// Pattern manager for before/after patterns.
35    pattern_manager: PatternManager,
36    /// Current session state.
37    state: SessionState,
38    /// Unique session identifier.
39    id: SessionId,
40    /// EOF flag.
41    eof: bool,
42}
43
44impl<T: AsyncReadExt + AsyncWriteExt + Unpin + Send> Session<T> {
45    /// Create a new session with the given transport.
46    pub fn new(transport: T, config: SessionConfig) -> Self {
47        let buffer_size = config.buffer.max_size;
48        let mut matcher = Matcher::new(buffer_size);
49        matcher.set_default_timeout(config.timeout.default);
50        Self {
51            transport: Arc::new(Mutex::new(transport)),
52            config,
53            matcher,
54            pattern_manager: PatternManager::new(),
55            state: SessionState::Starting,
56            id: SessionId::new(),
57            eof: false,
58        }
59    }
60
61    /// Get the session ID.
62    #[must_use]
63    pub const fn id(&self) -> &SessionId {
64        &self.id
65    }
66
67    /// Get the current session state.
68    #[must_use]
69    pub const fn state(&self) -> SessionState {
70        self.state
71    }
72
73    /// Get the session configuration.
74    #[must_use]
75    pub const fn config(&self) -> &SessionConfig {
76        &self.config
77    }
78
79    /// Check if EOF has been detected.
80    #[must_use]
81    pub const fn is_eof(&self) -> bool {
82        self.eof
83    }
84
85    /// Get the current buffer contents.
86    #[must_use]
87    pub fn buffer(&mut self) -> String {
88        self.matcher.buffer_str()
89    }
90
91    /// Clear the buffer.
92    pub fn clear_buffer(&mut self) {
93        self.matcher.clear();
94    }
95
96    /// Get the pattern manager for before/after patterns.
97    #[must_use]
98    pub const fn pattern_manager(&self) -> &PatternManager {
99        &self.pattern_manager
100    }
101
102    /// Get mutable access to the pattern manager.
103    pub const fn pattern_manager_mut(&mut self) -> &mut PatternManager {
104        &mut self.pattern_manager
105    }
106
107    /// Set the session state.
108    pub const fn set_state(&mut self, state: SessionState) {
109        self.state = state;
110    }
111
112    /// Send bytes to the process.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the write fails.
117    #[allow(clippy::significant_drop_tightening)]
118    pub async fn send(&mut self, data: &[u8]) -> Result<()> {
119        if matches!(self.state, SessionState::Closed | SessionState::Exited(_)) {
120            return Err(ExpectError::SessionClosed);
121        }
122
123        let mut transport = self.transport.lock().await;
124        transport
125            .write_all(data)
126            .await
127            .map_err(|e| ExpectError::io_context("writing to process", e))?;
128        transport
129            .flush()
130            .await
131            .map_err(|e| ExpectError::io_context("flushing process output", e))?;
132        Ok(())
133    }
134
135    /// Send a string to the process.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the write fails.
140    pub async fn send_str(&mut self, s: &str) -> Result<()> {
141        self.send(s.as_bytes()).await
142    }
143
144    /// Send a line to the process (appends newline based on config).
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the write fails.
149    pub async fn send_line(&mut self, line: &str) -> Result<()> {
150        let line_ending = self.config.line_ending.as_str();
151        let data = format!("{line}{line_ending}");
152        self.send(data.as_bytes()).await
153    }
154
155    /// Send a control character to the process.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the write fails.
160    pub async fn send_control(&mut self, ctrl: ControlChar) -> Result<()> {
161        self.send(&[ctrl.as_byte()]).await
162    }
163
164    /// Expect a pattern in the output.
165    ///
166    /// Blocks until the pattern is matched, EOF is detected, or timeout occurs.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error on timeout, EOF (if not expected), or I/O error.
171    pub async fn expect(&mut self, pattern: impl Into<Pattern>) -> Result<Match> {
172        let patterns = PatternSet::from_patterns(vec![pattern.into()]);
173        self.expect_any(&patterns).await
174    }
175
176    /// Expect any of the given patterns.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error on timeout, EOF (if not expected), or I/O error.
181    pub async fn expect_any(&mut self, patterns: &PatternSet) -> Result<Match> {
182        let timeout = self.matcher.get_timeout(patterns);
183        let state = ExpectState::new(patterns.clone(), timeout);
184
185        loop {
186            // Check before patterns first
187            if let Some((_, action)) = self
188                .pattern_manager
189                .check_before(&self.matcher.buffer_str())
190            {
191                match action {
192                    crate::expect::HandlerAction::Continue => {}
193                    crate::expect::HandlerAction::Return(s) => {
194                        return Ok(Match::new(0, s, String::new(), self.matcher.buffer_str()));
195                    }
196                    crate::expect::HandlerAction::Abort(msg) => {
197                        return Err(ExpectError::PatternNotFound {
198                            pattern: msg,
199                            buffer: self.matcher.buffer_str(),
200                        });
201                    }
202                    crate::expect::HandlerAction::Respond(s) => {
203                        self.send_str(&s).await?;
204                    }
205                }
206            }
207
208            // Check for pattern match
209            if let Some(result) = self.matcher.try_match_any(patterns) {
210                return Ok(self.matcher.consume_match(&result));
211            }
212
213            // Check for timeout
214            if state.is_timed_out() {
215                return Err(ExpectError::Timeout {
216                    duration: timeout,
217                    pattern: patterns
218                        .iter()
219                        .next()
220                        .map(|p| p.pattern.as_str().to_string())
221                        .unwrap_or_default(),
222                    buffer: self.matcher.buffer_str(),
223                });
224            }
225
226            // Check for EOF
227            if self.eof {
228                if state.expects_eof() {
229                    return Ok(Match::new(
230                        0,
231                        String::new(),
232                        self.matcher.buffer_str(),
233                        String::new(),
234                    ));
235                }
236                return Err(ExpectError::Eof {
237                    buffer: self.matcher.buffer_str(),
238                });
239            }
240
241            // Read more data
242            self.read_with_timeout(state.remaining_time()).await?;
243        }
244    }
245
246    /// Expect with a specific timeout.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error on timeout, EOF, or I/O error.
251    pub async fn expect_timeout(
252        &mut self,
253        pattern: impl Into<Pattern>,
254        timeout: Duration,
255    ) -> Result<Match> {
256        let pattern = pattern.into();
257        let mut patterns = PatternSet::new();
258        patterns.add(pattern).add(Pattern::timeout(timeout));
259        self.expect_any(&patterns).await
260    }
261
262    /// Read data from the transport with timeout.
263    async fn read_with_timeout(&mut self, timeout: Duration) -> Result<usize> {
264        let mut buf = [0u8; 4096];
265        let mut transport = self.transport.lock().await;
266
267        match tokio::time::timeout(timeout, transport.read(&mut buf)).await {
268            Ok(Ok(0)) => {
269                self.eof = true;
270                Ok(0)
271            }
272            Ok(Ok(n)) => {
273                self.matcher.append(&buf[..n]);
274                Ok(n)
275            }
276            Ok(Err(e)) => {
277                // On Linux, reading from PTY master returns EIO when the slave is closed
278                // (i.e., the child process has terminated). Treat this as EOF.
279                // See: https://bugs.python.org/issue5380
280                if is_pty_eof_error(&e) {
281                    self.eof = true;
282                    Ok(0)
283                } else {
284                    Err(ExpectError::io_context("reading from process", e))
285                }
286            }
287            Err(_) => {
288                // Timeout, but not an error - caller will handle
289                Ok(0)
290            }
291        }
292    }
293
294    /// Wait for the process to exit.
295    ///
296    /// This method blocks until EOF is detected on the session, which typically
297    /// happens when the child process terminates.
298    ///
299    /// # Warning
300    ///
301    /// This method has no timeout and may block indefinitely if the process
302    /// does not exit. Consider using [`wait_timeout`](Self::wait_timeout) or
303    /// [`expect_eof_timeout`](Self::expect_eof_timeout) for bounded waits.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if waiting fails due to I/O error.
308    pub async fn wait(&mut self) -> Result<ProcessExitStatus> {
309        // Read until EOF
310        while !self.eof {
311            if self.read_with_timeout(Duration::from_millis(100)).await? == 0 && !self.eof {
312                tokio::time::sleep(Duration::from_millis(10)).await;
313            }
314        }
315
316        // Return unknown status - actual status depends on backend
317        self.state = SessionState::Exited(ProcessExitStatus::Unknown);
318        Ok(ProcessExitStatus::Unknown)
319    }
320
321    /// Wait for the process to exit with a timeout.
322    ///
323    /// Like [`wait`](Self::wait), but with a maximum duration to wait.
324    ///
325    /// # Errors
326    ///
327    /// Returns an error if:
328    /// - The timeout expires before the process exits
329    /// - An I/O error occurs while waiting
330    pub async fn wait_timeout(&mut self, timeout: Duration) -> Result<ProcessExitStatus> {
331        let deadline = tokio::time::Instant::now() + timeout;
332
333        while !self.eof {
334            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
335            if remaining.is_zero() {
336                return Err(ExpectError::timeout(
337                    timeout,
338                    "<EOF>",
339                    self.matcher.buffer_str(),
340                ));
341            }
342
343            // Use smaller of remaining time or 100ms for polling
344            let poll_timeout = remaining.min(Duration::from_millis(100));
345            if self.read_with_timeout(poll_timeout).await? == 0 && !self.eof {
346                tokio::time::sleep(Duration::from_millis(10)).await;
347            }
348        }
349
350        self.state = SessionState::Exited(ProcessExitStatus::Unknown);
351        Ok(ProcessExitStatus::Unknown)
352    }
353
354    /// Check if a pattern matches immediately without blocking.
355    #[must_use]
356    pub fn check(&mut self, pattern: &Pattern) -> Option<MatchResult> {
357        self.matcher.try_match(pattern)
358    }
359
360    /// Get the underlying transport.
361    ///
362    /// Use with caution as direct access bypasses session management.
363    #[must_use]
364    pub const fn transport(&self) -> &Arc<Mutex<T>> {
365        &self.transport
366    }
367
368    /// Start an interactive session with pattern hooks.
369    ///
370    /// This returns a builder that allows you to configure pattern-based
371    /// callbacks that fire when patterns match in the output or input.
372    ///
373    /// # Example
374    ///
375    /// ```ignore
376    /// use rust_expect::{Session, InteractAction};
377    ///
378    /// #[tokio::main]
379    /// async fn main() -> Result<(), rust_expect::ExpectError> {
380    ///     let mut session = Session::spawn("/bin/bash", &[]).await?;
381    ///
382    ///     session.interact()
383    ///         .on_output("password:", |ctx| {
384    ///             ctx.send("my_password\n")
385    ///         })
386    ///         .on_output("logout", |_| {
387    ///             InteractAction::Stop
388    ///         })
389    ///         .start()
390    ///         .await?;
391    ///
392    ///     Ok(())
393    /// }
394    /// ```
395    #[must_use]
396    pub fn interact(&self) -> InteractBuilder<'_, T>
397    where
398        T: 'static,
399    {
400        InteractBuilder::new(&self.transport)
401    }
402
403    /// Run a dialog on this session.
404    ///
405    /// A dialog is a predefined sequence of expect/send operations.
406    /// This method executes the dialog and returns the result.
407    ///
408    /// # Example
409    ///
410    /// ```ignore
411    /// use rust_expect::{Session, Dialog, DialogStep};
412    ///
413    /// #[tokio::main]
414    /// async fn main() -> Result<(), rust_expect::ExpectError> {
415    ///     let mut session = Session::spawn("/bin/bash", &[]).await?;
416    ///
417    ///     let dialog = Dialog::named("shell_test")
418    ///         .step(DialogStep::new("prompt")
419    ///             .with_expect("$")
420    ///             .with_send("echo hello\n"))
421    ///         .step(DialogStep::new("verify")
422    ///             .with_expect("hello"));
423    ///
424    ///     let result = session.run_dialog(&dialog).await?;
425    ///     assert!(result.success);
426    ///     Ok(())
427    /// }
428    /// ```
429    ///
430    /// # Errors
431    ///
432    /// Returns an error if I/O fails. Step-level timeouts are reported
433    /// in the `DialogResult` rather than as errors.
434    pub async fn run_dialog(&mut self, dialog: &Dialog) -> Result<DialogResult> {
435        let executor = DialogExecutor::default();
436        executor.execute(self, dialog).await
437    }
438
439    /// Run a dialog with a custom executor.
440    ///
441    /// This allows customizing the executor settings (max steps, default timeout).
442    ///
443    /// # Errors
444    ///
445    /// Returns an error if I/O fails.
446    pub async fn run_dialog_with(
447        &mut self,
448        dialog: &Dialog,
449        executor: &DialogExecutor,
450    ) -> Result<DialogResult> {
451        executor.execute(self, dialog).await
452    }
453
454    /// Expect end-of-file (process termination).
455    ///
456    /// This is a convenience method for waiting until the process terminates
457    /// and closes its output stream.
458    ///
459    /// # Example
460    ///
461    /// ```ignore
462    /// use rust_expect::Session;
463    ///
464    /// #[tokio::main]
465    /// async fn main() -> Result<(), rust_expect::ExpectError> {
466    ///     let mut session = Session::spawn("echo", &["hello"]).await?;
467    ///     session.expect("hello").await?;
468    ///     session.expect_eof().await?;
469    ///     Ok(())
470    /// }
471    /// ```
472    ///
473    /// # Errors
474    ///
475    /// Returns an error if the session times out before EOF or an I/O error occurs.
476    pub async fn expect_eof(&mut self) -> Result<Match> {
477        self.expect(Pattern::eof()).await
478    }
479
480    /// Expect end-of-file with a specific timeout.
481    ///
482    /// # Errors
483    ///
484    /// Returns an error if the session times out before EOF or an I/O error occurs.
485    pub async fn expect_eof_timeout(&mut self, timeout: Duration) -> Result<Match> {
486        let mut patterns = PatternSet::new();
487        patterns.add(Pattern::eof()).add(Pattern::timeout(timeout));
488        self.expect_any(&patterns).await
489    }
490
491    /// Run a batch of commands, waiting for the prompt after each.
492    ///
493    /// This is a convenience method for executing multiple shell commands
494    /// in sequence. For each command, it sends the command line and waits
495    /// for the prompt pattern to appear.
496    ///
497    /// # Example
498    ///
499    /// ```ignore
500    /// use rust_expect::{Session, Pattern};
501    ///
502    /// #[tokio::main]
503    /// async fn main() -> Result<(), rust_expect::ExpectError> {
504    ///     let mut session = Session::spawn("/bin/bash", &[]).await?;
505    ///     session.expect(Pattern::shell_prompt()).await?;
506    ///
507    ///     // Run a batch of commands
508    ///     let results = session.run_script(
509    ///         &["pwd", "whoami", "date"],
510    ///         Pattern::shell_prompt(),
511    ///     ).await?;
512    ///
513    ///     for result in &results {
514    ///         println!("Output: {}", result.before.trim());
515    ///     }
516    ///
517    ///     Ok(())
518    /// }
519    /// ```
520    ///
521    /// # Errors
522    ///
523    /// Returns an error if any command times out or I/O fails.
524    /// On error, partial results are lost; consider using [`Self::run_script_with_results`]
525    /// if you need to capture partial results on failure.
526    pub async fn run_script<I, S>(&mut self, commands: I, prompt: Pattern) -> Result<Vec<Match>>
527    where
528        I: IntoIterator<Item = S>,
529        S: AsRef<str>,
530    {
531        let mut results = Vec::new();
532
533        for cmd in commands {
534            self.send_line(cmd.as_ref()).await?;
535            let result = self.expect(prompt.clone()).await?;
536            results.push(result);
537        }
538
539        Ok(results)
540    }
541
542    /// Run a batch of commands with a specific timeout per command.
543    ///
544    /// Like [`run_script`](Self::run_script), but applies the given timeout
545    /// to each command individually.
546    ///
547    /// # Errors
548    ///
549    /// Returns an error if any command times out or I/O fails.
550    pub async fn run_script_timeout<I, S>(
551        &mut self,
552        commands: I,
553        prompt: Pattern,
554        timeout: Duration,
555    ) -> Result<Vec<Match>>
556    where
557        I: IntoIterator<Item = S>,
558        S: AsRef<str>,
559    {
560        let mut results = Vec::new();
561
562        for cmd in commands {
563            self.send_line(cmd.as_ref()).await?;
564            let result = self.expect_timeout(prompt.clone(), timeout).await?;
565            results.push(result);
566        }
567
568        Ok(results)
569    }
570
571    /// Run a batch of commands, collecting results even on failure.
572    ///
573    /// Unlike [`run_script`](Self::run_script), this method continues
574    /// collecting results and returns them along with any error that occurred.
575    ///
576    /// # Returns
577    ///
578    /// A tuple of `(results, error)` where:
579    /// - `results` contains the matches for successfully completed commands
580    /// - `error` is `Some(err)` if an error occurred, `None` if all commands succeeded
581    ///
582    /// # Example
583    ///
584    /// ```ignore
585    /// use rust_expect::{Session, Pattern};
586    ///
587    /// #[tokio::main]
588    /// async fn main() -> Result<(), rust_expect::ExpectError> {
589    ///     let mut session = Session::spawn("/bin/bash", &[]).await?;
590    ///     session.expect(Pattern::shell_prompt()).await?;
591    ///
592    ///     let (results, error) = session.run_script_with_results(
593    ///         &["pwd", "bad_command", "date"],
594    ///         Pattern::shell_prompt(),
595    ///     ).await;
596    ///
597    ///     println!("Completed {} commands", results.len());
598    ///     if let Some(e) = error {
599    ///         eprintln!("Script failed at command {}: {}", results.len(), e);
600    ///     }
601    ///
602    ///     Ok(())
603    /// }
604    /// ```
605    pub async fn run_script_with_results<I, S>(
606        &mut self,
607        commands: I,
608        prompt: Pattern,
609    ) -> (Vec<Match>, Option<ExpectError>)
610    where
611        I: IntoIterator<Item = S>,
612        S: AsRef<str>,
613    {
614        let mut results = Vec::new();
615
616        for cmd in commands {
617            match self.send_line(cmd.as_ref()).await {
618                Ok(()) => {}
619                Err(e) => return (results, Some(e)),
620            }
621
622            match self.expect(prompt.clone()).await {
623                Ok(result) => results.push(result),
624                Err(e) => return (results, Some(e)),
625            }
626        }
627
628        (results, None)
629    }
630}
631
632impl<T: AsyncReadExt + AsyncWriteExt + Unpin + Send> std::fmt::Debug for Session<T> {
633    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634        f.debug_struct("Session")
635            .field("id", &self.id)
636            .field("state", &self.state)
637            .field("eof", &self.eof)
638            .finish_non_exhaustive()
639    }
640}
641
642// Unix-specific spawn implementation
643#[cfg(unix)]
644impl Session<AsyncPty> {
645    /// Spawn a new process with the given command.
646    ///
647    /// This creates a new PTY, forks a child process, and returns a Session
648    /// connected to the child's terminal.
649    ///
650    /// # Example
651    ///
652    /// ```ignore
653    /// use rust_expect::Session;
654    ///
655    /// #[tokio::main]
656    /// async fn main() -> Result<(), rust_expect::ExpectError> {
657    ///     let mut session = Session::spawn("/bin/bash", &[]).await?;
658    ///     session.expect("$").await?;
659    ///     session.send_line("echo hello").await?;
660    ///     session.expect("hello").await?;
661    ///     Ok(())
662    /// }
663    /// ```
664    ///
665    /// # Errors
666    ///
667    /// Returns an error if:
668    /// - The command contains null bytes
669    /// - PTY allocation fails
670    /// - Fork fails
671    /// - The command cannot be executed
672    pub async fn spawn(command: &str, args: &[&str]) -> Result<Self> {
673        Self::spawn_with_config(command, args, SessionConfig::default()).await
674    }
675
676    /// Spawn a new process with custom configuration.
677    ///
678    /// # Errors
679    ///
680    /// Returns an error if spawning fails.
681    pub async fn spawn_with_config(
682        command: &str,
683        args: &[&str],
684        config: SessionConfig,
685    ) -> Result<Self> {
686        let pty_config = PtyConfig::from(&config);
687        let spawner = PtySpawner::with_config(pty_config);
688
689        // Convert &[&str] to Vec<String> for the spawner
690        let args_owned: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
691
692        // Spawn the process
693        let handle = spawner.spawn(command, &args_owned).await?;
694
695        // Wrap in AsyncPty for async I/O
696        let async_pty = AsyncPty::from_handle(handle)
697            .map_err(|e| ExpectError::io_context("creating async PTY wrapper", e))?;
698
699        // Create the session
700        let mut session = Self::new(async_pty, config);
701        session.state = SessionState::Running;
702
703        Ok(session)
704    }
705
706    /// Get the child process ID.
707    #[must_use]
708    pub fn pid(&self) -> u32 {
709        // We need to access the inner transport's pid
710        // For now, use the blocking lock since we know it's not contended
711        // during a sync call like this
712        if let Ok(transport) = self.transport.try_lock() {
713            transport.pid()
714        } else {
715            0
716        }
717    }
718
719    /// Resize the terminal.
720    ///
721    /// # Errors
722    ///
723    /// Returns an error if the resize ioctl fails.
724    pub async fn resize_pty(&mut self, cols: u16, rows: u16) -> Result<()> {
725        let mut transport = self.transport.lock().await;
726        transport.resize(cols, rows)
727    }
728
729    /// Send a signal to the child process.
730    ///
731    /// # Errors
732    ///
733    /// Returns an error if sending the signal fails.
734    pub fn signal(&self, signal: i32) -> Result<()> {
735        if let Ok(transport) = self.transport.try_lock() {
736            transport.signal(signal)
737        } else {
738            Err(ExpectError::io_context(
739                "sending signal to process",
740                std::io::Error::new(std::io::ErrorKind::WouldBlock, "transport is locked"),
741            ))
742        }
743    }
744
745    /// Kill the child process.
746    ///
747    /// # Errors
748    ///
749    /// Returns an error if killing the process fails.
750    pub fn kill(&self) -> Result<()> {
751        if let Ok(transport) = self.transport.try_lock() {
752            transport.kill()
753        } else {
754            Err(ExpectError::io_context(
755                "killing process",
756                std::io::Error::new(std::io::ErrorKind::WouldBlock, "transport is locked"),
757            ))
758        }
759    }
760}
761
762// Windows-specific spawn implementation
763#[cfg(windows)]
764impl Session<WindowsAsyncPty> {
765    /// Spawn a new process with the given command.
766    ///
767    /// This creates a new PTY using Windows ConPTY, spawns a child process,
768    /// and returns a Session connected to the child's terminal.
769    ///
770    /// # Example
771    ///
772    /// ```ignore
773    /// use rust_expect::Session;
774    ///
775    /// #[tokio::main]
776    /// async fn main() -> Result<(), rust_expect::ExpectError> {
777    ///     let mut session = Session::spawn("cmd.exe", &[]).await?;
778    ///     session.expect(">").await?;
779    ///     session.send_line("echo hello").await?;
780    ///     session.expect("hello").await?;
781    ///     Ok(())
782    /// }
783    /// ```
784    ///
785    /// # Errors
786    ///
787    /// Returns an error if:
788    /// - ConPTY is not available (Windows version too old)
789    /// - PTY allocation fails
790    /// - The command cannot be executed
791    pub async fn spawn(command: &str, args: &[&str]) -> Result<Self> {
792        Self::spawn_with_config(command, args, SessionConfig::default()).await
793    }
794
795    /// Spawn a new process with custom configuration.
796    ///
797    /// # Errors
798    ///
799    /// Returns an error if spawning fails.
800    pub async fn spawn_with_config(
801        command: &str,
802        args: &[&str],
803        config: SessionConfig,
804    ) -> Result<Self> {
805        let pty_config = PtyConfig::from(&config);
806        let spawner = PtySpawner::with_config(pty_config);
807
808        // Convert &[&str] to Vec<String> for the spawner
809        let args_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
810
811        // Spawn the process
812        let handle = spawner.spawn(command, &args_owned).await?;
813
814        // Wrap in WindowsAsyncPty for async I/O
815        let async_pty = WindowsAsyncPty::from_handle(handle);
816
817        // Create the session
818        let mut session = Session::new(async_pty, config);
819        session.state = SessionState::Running;
820
821        Ok(session)
822    }
823
824    /// Get the child process ID.
825    #[must_use]
826    pub fn pid(&self) -> u32 {
827        if let Ok(transport) = self.transport.try_lock() {
828            transport.pid()
829        } else {
830            0
831        }
832    }
833
834    /// Resize the terminal.
835    ///
836    /// # Errors
837    ///
838    /// Returns an error if the resize operation fails.
839    pub async fn resize_pty(&mut self, cols: u16, rows: u16) -> Result<()> {
840        let mut transport = self.transport.lock().await;
841        transport.resize(cols, rows)
842    }
843
844    /// Check if the child process is still running.
845    #[must_use]
846    pub fn is_running(&self) -> bool {
847        if let Ok(transport) = self.transport.try_lock() {
848            transport.is_running()
849        } else {
850            true // Assume running if we can't check
851        }
852    }
853
854    /// Kill the child process.
855    ///
856    /// # Errors
857    ///
858    /// Returns an error if killing the process fails.
859    pub fn kill(&self) -> Result<()> {
860        if let Ok(mut transport) = self.transport.try_lock() {
861            transport.kill()
862        } else {
863            Err(ExpectError::io_context(
864                "killing process",
865                std::io::Error::new(std::io::ErrorKind::WouldBlock, "transport is locked"),
866            ))
867        }
868    }
869}
870
871/// Extension trait for session operations.
872pub trait SessionExt {
873    /// Send and expect in one call.
874    fn send_expect(
875        &mut self,
876        send: &str,
877        expect: impl Into<Pattern>,
878    ) -> impl std::future::Future<Output = Result<Match>> + Send;
879
880    /// Resize the terminal.
881    fn resize(
882        &mut self,
883        dimensions: Dimensions,
884    ) -> impl std::future::Future<Output = Result<()>> + Send;
885}
886
887/// Check if an I/O error indicates PTY EOF.
888///
889/// On Linux, reading from the PTY master returns EIO when the slave side
890/// has been closed (i.e., the child process has terminated). This is different
891/// from the standard EOF behavior where `read()` returns 0 bytes.
892///
893/// This function returns true for errors that should be treated as EOF:
894/// - EIO (errno 5) on Unix systems
895/// - `BrokenPipe` on any platform
896fn is_pty_eof_error(e: &std::io::Error) -> bool {
897    use std::io::ErrorKind;
898
899    // BrokenPipe indicates the other end has closed
900    if e.kind() == ErrorKind::BrokenPipe {
901        return true;
902    }
903
904    // On Unix, check for EIO which indicates slave PTY closed
905    #[cfg(unix)]
906    {
907        if let Some(errno) = e.raw_os_error() {
908            // EIO is 5 on Linux/macOS/BSD
909            if errno == libc::EIO {
910                return true;
911            }
912        }
913    }
914
915    false
916}