Skip to main content

ognibuild/session/
mod.rs

1use std::collections::HashMap;
2use std::io::Write;
3use std::process::ExitStatus;
4
5/// Plain session implementation.
6pub mod plain;
7/// Schroot session implementation (Linux only).
8#[cfg(target_os = "linux")]
9pub mod schroot;
10/// Unshare session implementation (Linux only).
11#[cfg(target_os = "linux")]
12pub mod unshare;
13
14#[derive(Debug)]
15/// Errors related to image operations (downloading, caching, etc.)
16pub enum ImageError {
17    /// Cached image specified was not found and downloading is not allowed
18    CachedImageNotFound {
19        /// Path where the image was expected to be cached
20        path: std::path::PathBuf,
21    },
22    /// There is no cached image
23    NoCachedImage,
24    /// Download is not available (missing feature or other reason)
25    DownloadNotAvailable {
26        /// Reason why download is not available
27        reason: String,
28    },
29    /// Architecture not supported for cloud images
30    UnsupportedArchitecture {
31        /// The unsupported architecture
32        arch: String,
33    },
34    /// Failed to download cloud image
35    DownloadFailed {
36        /// URL that failed to download
37        url: String,
38        /// Error message describing the failure
39        error: String,
40    },
41}
42
43#[derive(Debug)]
44/// Errors that can occur in a session.
45pub enum Error {
46    /// Error caused by a command that exited with a non-zero status code.
47    CalledProcessError(ExitStatus),
48    /// Error from an IO operation.
49    IoError(std::io::Error),
50    /// Error from setting up the session, with a message and detailed description.
51    SetupFailure(String, String),
52    /// Error from image operations (download, cache, etc.)
53    ImageError(ImageError),
54}
55
56impl From<std::io::Error> for Error {
57    fn from(e: std::io::Error) -> Self {
58        Error::IoError(e)
59    }
60}
61
62impl std::fmt::Display for ImageError {
63    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64        match self {
65            ImageError::NoCachedImage => {
66                write!(f, "No cached image available")
67            }
68            ImageError::CachedImageNotFound { path } => {
69                write!(
70                    f,
71                    "Cached image not found at {} and downloading is not allowed",
72                    path.display()
73                )
74            }
75            ImageError::DownloadNotAvailable { reason } => {
76                write!(f, "Download not available: {}", reason)
77            }
78            ImageError::UnsupportedArchitecture { arch } => {
79                write!(f, "Architecture {} not supported for cloud images", arch)
80            }
81            ImageError::DownloadFailed { url, error } => {
82                write!(f, "Failed to download from {}: {}", url, error)
83            }
84        }
85    }
86}
87
88impl std::fmt::Display for Error {
89    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
90        match self {
91            Error::CalledProcessError(code) => write!(f, "CalledProcessError({})", code),
92            Error::IoError(e) => write!(f, "IoError({})", e),
93            Error::SetupFailure(msg, _long_description) => write!(f, "SetupFailure({})", msg),
94            Error::ImageError(e) => write!(f, "ImageError: {}", e),
95        }
96    }
97}
98
99impl std::error::Error for Error {}
100
101/// Which session backend to run commands in.
102///
103/// Parsed from a command-line string (see [`SessionKind::from_str`]) and turned
104/// into a live [`Session`] with [`SessionKind::build`]. This is the shared way
105/// for the command-line tools to select a session backend.
106#[derive(Clone, Debug, PartialEq, Eq, Default)]
107pub enum SessionKind {
108    /// Run directly on the host.
109    #[default]
110    Plain,
111    /// Run inside the named schroot chroot.
112    Schroot(String),
113    /// Run inside an unshare session bootstrapped from a cached Debian image of
114    /// the given suite.
115    Unshare(String),
116}
117
118impl std::str::FromStr for SessionKind {
119    type Err = String;
120
121    fn from_str(s: &str) -> Result<Self, Self::Err> {
122        match s.split_once(':') {
123            None if s == "plain" => Ok(SessionKind::Plain),
124            None if s == "unshare" => Ok(SessionKind::Unshare("sid".to_string())),
125            None => Err(format!(
126                "unknown session kind {s:?}; expected \"plain\", \"schroot:<name>\", or \"unshare:<suite>\""
127            )),
128            Some(("schroot", name)) if !name.is_empty() => {
129                Ok(SessionKind::Schroot(name.to_string()))
130            }
131            Some(("schroot", _)) => Err("schroot session requires a chroot name".to_string()),
132            Some(("unshare", suite)) if !suite.is_empty() => {
133                Ok(SessionKind::Unshare(suite.to_string()))
134            }
135            Some(("unshare", _)) => Err("unshare session requires a suite".to_string()),
136            Some((kind, _)) => Err(format!(
137                "unknown session kind {kind:?}; expected \"plain\", \"schroot:<name>\", or \"unshare:<suite>\""
138            )),
139        }
140    }
141}
142
143/// Reconcile the `--session` and `--schroot` command-line options into a single
144/// [`SessionKind`].
145///
146/// `--schroot <name>` is shorthand for `--session schroot:<name>`. Supplying both
147/// is an error.
148pub fn resolve_session_kind(
149    session: Option<SessionKind>,
150    schroot: Option<String>,
151) -> Result<SessionKind, String> {
152    match (session, schroot) {
153        (Some(_), Some(_)) => Err("--schroot and --session are mutually exclusive".to_string()),
154        (Some(kind), None) => Ok(kind),
155        (None, Some(chroot)) => Ok(SessionKind::Schroot(chroot)),
156        (None, None) => Ok(SessionKind::default()),
157    }
158}
159
160impl SessionKind {
161    /// Build a live session from this kind.
162    ///
163    /// `session_prefix` is used to label schroot sessions (e.g. the name of the
164    /// invoking tool); it is ignored by the other backends.
165    pub fn build(&self, session_prefix: Option<&str>) -> Result<Box<dyn Session>, Error> {
166        match self {
167            SessionKind::Plain => Ok(Box::new(plain::PlainSession::new())),
168            #[cfg(target_os = "linux")]
169            SessionKind::Schroot(chroot) => Ok(Box::new(schroot::SchrootSession::new(
170                chroot,
171                session_prefix,
172            )?)),
173            #[cfg(target_os = "linux")]
174            SessionKind::Unshare(suite) => Ok(Box::new(
175                unshare::UnshareSession::cached_debian_session(suite)?,
176            )),
177            #[cfg(not(target_os = "linux"))]
178            SessionKind::Schroot(_) | SessionKind::Unshare(_) => Err(Error::SetupFailure(
179                "unsupported session backend".to_string(),
180                "schroot and unshare sessions are only available on Linux".to_string(),
181            )),
182        }
183    }
184}
185
186/// Session interface for running commands in different environments.
187///
188/// This trait defines the interface for running commands in different environments,
189/// such as the local system, a chroot, or a container.
190pub trait Session {
191    /// Change the current working directory in the session.
192    fn chdir(&mut self, path: &std::path::Path) -> Result<(), crate::session::Error>;
193
194    /// Get the current working directory in the session.
195    ///
196    /// # Returns
197    /// The current working directory
198    fn pwd(&self) -> &std::path::Path;
199
200    /// Return the external path for a path inside the session.
201    ///
202    /// The path need not exist; callers may use this to find out where to
203    /// create a file.
204    fn external_path(&self, path: &std::path::Path) -> std::path::PathBuf;
205
206    /// Return the location of the session.
207    fn location(&self) -> std::path::PathBuf;
208
209    /// Run a command and return its output.
210    ///
211    /// This method runs a command in the session and returns its output
212    /// if the command exits successfully.
213    ///
214    /// # Arguments
215    /// * `argv` - The command and its arguments
216    /// * `cwd` - Optional current working directory
217    /// * `user` - Optional user to run the command as
218    /// * `env` - Optional environment variables
219    ///
220    /// # Returns
221    /// * `Ok(Vec<u8>)` - The command output if successful
222    /// * `Err(Error)` - If the command fails
223    fn check_output(
224        &self,
225        argv: Vec<&str>,
226        cwd: Option<&std::path::Path>,
227        user: Option<&str>,
228        env: Option<HashMap<String, String>>,
229    ) -> Result<Vec<u8>, Error>;
230
231    /// Ensure that the current users' home directory exists.
232    fn create_home(&self) -> Result<(), Error>;
233
234    /// Run a command and check that it exits successfully.
235    ///
236    /// This method runs a command in the session and returns success
237    /// if the command exits with a zero status code.
238    ///
239    /// # Arguments
240    /// * `argv` - The command and its arguments
241    /// * `cwd` - Optional current working directory
242    /// * `user` - Optional user to run the command as
243    /// * `env` - Optional environment variables
244    ///
245    /// # Returns
246    /// * `Ok(())` - If the command exited successfully
247    /// * `Err(Error)` - If the command fails
248    fn check_call(
249        &self,
250        argv: Vec<&str>,
251        cwd: Option<&std::path::Path>,
252        user: Option<&str>,
253        env: Option<std::collections::HashMap<String, String>>,
254    ) -> Result<(), crate::session::Error>;
255
256    /// Check if a file or directory exists.
257    fn exists(&self, path: &std::path::Path) -> bool;
258
259    /// Create a directory.
260    fn mkdir(&self, path: &std::path::Path) -> Result<(), crate::session::Error>;
261
262    /// Recursively remove a directory.
263    fn rmtree(&self, path: &std::path::Path) -> Result<(), crate::session::Error>;
264
265    /// Setup a project from an existing directory.
266    ///
267    /// # Arguments
268    /// * `path` - The path to the directory to setup the session from.
269    /// * `subdir` - The subdirectory to use as the session root.
270    fn project_from_directory(
271        &self,
272        path: &std::path::Path,
273        subdir: Option<&str>,
274    ) -> Result<Project, Error>;
275
276    /// Create a new command builder for the session.
277    ///
278    /// # Arguments
279    /// * `argv` - The command and its arguments
280    ///
281    /// # Returns
282    /// A new CommandBuilder instance
283    fn command<'a>(&'a self, argv: Vec<&'a str>) -> CommandBuilder<'a>;
284
285    /// Start a process in the session.
286    ///
287    /// # Arguments
288    /// * `argv` - The command and its arguments
289    /// * `cwd` - Optional current working directory
290    /// * `user` - Optional user to run the command as
291    /// * `stdout` - Optional stdout configuration
292    /// * `stderr` - Optional stderr configuration
293    /// * `stdin` - Optional stdin configuration
294    /// * `env` - Optional environment variables
295    ///
296    /// # Returns
297    /// * `Ok(Child)` - A handle to the running process
298    /// * `Err(Error)` - If starting the process fails
299    fn popen(
300        &self,
301        argv: Vec<&str>,
302        cwd: Option<&std::path::Path>,
303        user: Option<&str>,
304        stdout: Option<std::process::Stdio>,
305        stderr: Option<std::process::Stdio>,
306        stdin: Option<std::process::Stdio>,
307        env: Option<&std::collections::HashMap<String, String>>,
308    ) -> Result<std::process::Child, Error>;
309
310    /// Check if the session is temporary.
311    fn is_temporary(&self) -> bool;
312
313    #[cfg(feature = "breezy")]
314    /// Setup a project from a VCS tree.
315    ///
316    /// # Arguments
317    /// * `tree` - The VCS tree to setup the session from.
318    /// * `include_controldir` - Whether to include the control directory.
319    /// * `subdir` - The subdirectory to use as the session root.
320    ///
321    /// # Returns
322    /// A tuple containing the path to the tree in the session and
323    /// the external path.
324    fn project_from_vcs(
325        &self,
326        tree: &dyn crate::vcs::DupableTree,
327        include_controldir: Option<bool>,
328        subdir: Option<&str>,
329    ) -> Result<Project, Error>;
330
331    /// Read the contents of a directory.
332    ///
333    /// # Arguments
334    /// * `path` - Path to the directory to read
335    ///
336    /// # Returns
337    /// * `Ok(Vec<DirEntry>)` - The directory entries if successful
338    /// * `Err(Error)` - If reading the directory fails
339    fn read_dir(&self, path: &std::path::Path) -> Result<Vec<std::fs::DirEntry>, Error>;
340
341    /// Control whether commands run in this session are isolated from the
342    /// network.
343    ///
344    /// Only sessions that can isolate the network (currently the unshare
345    /// session) act on this; for others it is a no-op. Sessions that isolate by
346    /// default need this to be turned off when a command must reach the network
347    /// (e.g. installing a build system's declared dependencies from PyPI or npm).
348    fn set_isolate_network(&self, _isolate: bool) {}
349
350    /// Whether commands run in this session are currently isolated from the
351    /// network. Sessions that cannot isolate the network always report `false`.
352    fn is_network_isolated(&self) -> bool {
353        false
354    }
355}
356
357/// Run `f` with the session's network access enabled, restoring the previous
358/// isolation state afterwards.
359///
360/// Use this around steps that need to reach the network (e.g. apt downloading
361/// packages) while the surrounding run is otherwise isolated.
362pub fn with_network<T>(session: &dyn Session, f: impl FnOnce() -> T) -> T {
363    let was_isolated = session.is_network_isolated();
364    session.set_isolate_network(false);
365    let result = f();
366    session.set_isolate_network(was_isolated);
367    result
368}
369
370/// Represents a project in a session, either as a temporary copy or a direct reference.
371pub enum Project {
372    /// A project that does not need to be cleaned up.
373    Noop(std::path::PathBuf),
374
375    /// A temporary project that needs to be cleaned up.
376    /// A temporary copy of a project, which exists only for the duration of the session.
377    Temporary {
378        /// The path to the project from the external environment.
379        external_path: std::path::PathBuf,
380        /// The path to the project inside the session.
381        internal_path: std::path::PathBuf,
382        /// The path to the temporary directory.
383        td: std::path::PathBuf,
384    },
385}
386
387impl Drop for Project {
388    fn drop(&mut self) {
389        match self {
390            Project::Noop(_) => {}
391            Project::Temporary {
392                external_path: _,
393                internal_path: _,
394                td,
395            } => {
396                log::info!("Removing temporary project {}", td.display());
397                std::fs::remove_dir_all(td).unwrap();
398            }
399        }
400    }
401}
402
403impl Project {
404    /// Get the path to the project inside the session.
405    ///
406    /// # Returns
407    /// The path to the project inside the session
408    pub fn internal_path(&self) -> &std::path::Path {
409        match self {
410            Project::Noop(path) => path,
411            Project::Temporary { internal_path, .. } => internal_path,
412        }
413    }
414
415    /// Get the path to the project from the external environment.
416    ///
417    /// # Returns
418    /// The path to the project from the external environment
419    pub fn external_path(&self) -> &std::path::Path {
420        match self {
421            Project::Noop(path) => path,
422            Project::Temporary { external_path, .. } => external_path,
423        }
424    }
425}
426
427impl From<tempfile::TempDir> for Project {
428    fn from(tempdir: tempfile::TempDir) -> Self {
429        Project::Temporary {
430            external_path: tempdir.path().to_path_buf(),
431            internal_path: tempdir.path().to_path_buf(),
432            td: tempdir.keep(),
433        }
434    }
435}
436
437/// Builder for creating and running commands in a session.
438///
439/// This struct provides a fluent interface for configuring and executing
440/// commands within a session, handling options like working directory,
441/// environment variables, input/output redirection, and more.
442pub struct CommandBuilder<'a> {
443    /// The session to run the command in
444    session: &'a dyn Session,
445    /// The command and its arguments
446    argv: Vec<&'a str>,
447    /// Optional current working directory
448    cwd: Option<&'a std::path::Path>,
449    /// Optional user to run the command as
450    user: Option<&'a str>,
451    /// Optional environment variables
452    env: Option<std::collections::HashMap<String, String>>,
453    /// Optional stdin configuration
454    stdin: Option<std::process::Stdio>,
455    /// Optional stdout configuration
456    stdout: Option<std::process::Stdio>,
457    /// Optional stderr configuration
458    stderr: Option<std::process::Stdio>,
459    /// Whether to suppress output
460    quiet: bool,
461}
462
463impl<'a> CommandBuilder<'a> {
464    /// Create a new CommandBuilder.
465    ///
466    /// # Arguments
467    /// * `session` - The session to run the command in
468    /// * `argv` - The command and its arguments
469    ///
470    /// # Returns
471    /// A new CommandBuilder instance
472    pub fn new(session: &'a dyn Session, argv: Vec<&'a str>) -> Self {
473        CommandBuilder {
474            session,
475            argv,
476            cwd: None,
477            user: None,
478            env: None,
479            stdin: None,
480            stdout: None,
481            stderr: None,
482            quiet: false,
483        }
484    }
485
486    /// Set whether the command should run quietly.
487    ///
488    /// # Arguments
489    /// * `quiet` - Whether to suppress output
490    ///
491    /// # Returns
492    /// Self for method chaining
493    pub fn quiet(mut self, quiet: bool) -> Self {
494        self.quiet = quiet;
495        self
496    }
497
498    /// Set the current working directory for the command.
499    pub fn cwd(mut self, cwd: &'a std::path::Path) -> Self {
500        self.cwd = Some(cwd);
501        self
502    }
503
504    /// Set the user to run the command as.
505    pub fn user(mut self, user: &'a str) -> Self {
506        self.user = Some(user);
507        self
508    }
509
510    /// Set the environment for the command.
511    pub fn env(mut self, env: std::collections::HashMap<String, String>) -> Self {
512        assert!(self.env.is_none());
513        self.env = Some(env);
514        self
515    }
516
517    /// Add an environment variable to the command.
518    pub fn setenv(mut self, key: String, value: String) -> Self {
519        self.env = match self.env {
520            Some(mut env) => {
521                env.insert(key, value);
522                Some(env)
523            }
524            None => Some(std::collections::HashMap::from([(key, value)])),
525        };
526        self
527    }
528
529    /// Set the stdin for the command.
530    ///
531    /// # Arguments
532    /// * `stdin` - The stdin configuration
533    ///
534    /// # Returns
535    /// Self for method chaining
536    pub fn stdin(mut self, stdin: std::process::Stdio) -> Self {
537        self.stdin = Some(stdin);
538        self
539    }
540
541    /// Set the stdout for the command.
542    ///
543    /// # Arguments
544    /// * `stdout` - The stdout configuration
545    ///
546    /// # Returns
547    /// Self for method chaining
548    pub fn stdout(mut self, stdout: std::process::Stdio) -> Self {
549        self.stdout = Some(stdout);
550        self
551    }
552
553    /// Set the stderr for the command.
554    ///
555    /// # Arguments
556    /// * `stderr` - The stderr configuration
557    ///
558    /// # Returns
559    /// Self for method chaining
560    pub fn stderr(mut self, stderr: std::process::Stdio) -> Self {
561        self.stderr = Some(stderr);
562        self
563    }
564
565    /// Run the command and capture its output, while also displaying it.
566    ///
567    /// This method executes the command and collects its output, while also
568    /// displaying it in real time.
569    ///
570    /// # Returns
571    /// * `Ok((ExitStatus, Vec<String>))` - The exit status and output lines if successful
572    /// * `Err(Error)` - If the command fails
573    pub fn run_with_tee(self) -> Result<(ExitStatus, Vec<String>), Error> {
574        assert!(self.stdout.is_none());
575        assert!(self.stderr.is_none());
576        run_with_tee(
577            self.session,
578            self.argv,
579            self.cwd,
580            self.user,
581            self.env.as_ref(),
582            self.stdin,
583            self.quiet,
584        )
585    }
586
587    /// Run the command and analyze the output for problems.
588    ///
589    /// This method executes the command and analyzes its output for common
590    /// build problems, returning a more detailed error when issues are detected.
591    ///
592    /// # Returns
593    /// * `Ok(Vec<String>)` - The output lines if successful
594    /// * `Err(AnalyzedError)` - A detailed error if the command fails
595    pub fn run_detecting_problems(self) -> Result<Vec<String>, crate::analyze::AnalyzedError> {
596        assert!(self.stdout.is_none());
597        assert!(self.stderr.is_none());
598        crate::analyze::run_detecting_problems(
599            self.session,
600            self.argv,
601            None,
602            self.quiet,
603            self.cwd,
604            self.user,
605            self.env.as_ref(),
606            self.stdin,
607        )
608    }
609
610    /// Run the command and attempt to fix any problems that occur.
611    ///
612    /// This method executes the command and applies fixes if it fails,
613    /// potentially retrying multiple times with different fixers.
614    ///
615    /// # Arguments
616    /// * `fixers` - List of fixers to try if the command fails
617    ///
618    /// # Returns
619    /// * `Ok(Vec<String>)` - The command output if successful
620    /// * `Err(IterateBuildError)` - If the command fails and can't be fixed
621    pub fn run_fixing_problems<
622        I: std::error::Error,
623        E: From<I> + std::error::Error + From<std::io::Error>,
624    >(
625        self,
626        fixers: &[&dyn crate::fix_build::BuildFixer<I>],
627    ) -> Result<Vec<String>, crate::fix_build::IterateBuildError<E>> {
628        assert!(self.stdin.is_none());
629        assert!(self.stdout.is_none());
630        assert!(self.stderr.is_none());
631        crate::fix_build::run_fixing_problems(
632            fixers,
633            None,
634            self.session,
635            self.argv.as_slice(),
636            self.quiet,
637            self.cwd,
638            self.user,
639            self.env.as_ref(),
640        )
641    }
642
643    /// Start the command and return a handle to the running process.
644    ///
645    /// # Returns
646    /// * `Ok(Child)` - A handle to the running process
647    /// * `Err(Error)` - If starting the process fails
648    pub fn child(self) -> Result<std::process::Child, Error> {
649        self.session.popen(
650            self.argv,
651            self.cwd,
652            self.user,
653            self.stdout,
654            self.stderr,
655            self.stdin,
656            self.env.as_ref(),
657        )
658    }
659
660    /// Run the command and return its exit status.
661    ///
662    /// # Returns
663    /// * `Ok(ExitStatus)` - The exit status if successful
664    /// * `Err(Error)` - If the command fails
665    pub fn run(self) -> Result<std::process::ExitStatus, Error> {
666        let mut p = self.child()?;
667        let status = p.wait()?;
668        Ok(status)
669    }
670
671    /// Run the command and return its output.
672    ///
673    /// # Returns
674    /// * `Ok(Output)` - The command output if successful
675    /// * `Err(Error)` - If the command fails
676    pub fn output(self) -> Result<std::process::Output, Error> {
677        let p = self.child()?;
678        let output = p.wait_with_output()?;
679        Ok(output)
680    }
681
682    /// Run the command and check that it exits successfully.
683    ///
684    /// # Returns
685    /// * `Ok(())` - If the command exited successfully
686    /// * `Err(Error)` - If the command fails
687    pub fn check_call(self) -> Result<(), Error> {
688        self.session
689            .check_call(self.argv, self.cwd, self.user, self.env)
690    }
691
692    /// Run the command and return its output.
693    ///
694    /// # Returns
695    /// * `Ok(Vec<u8>)` - The command output if successful
696    /// * `Err(Error)` - If the command fails
697    pub fn check_output(self) -> Result<Vec<u8>, Error> {
698        self.session
699            .check_output(self.argv, self.cwd, self.user, self.env)
700    }
701}
702
703/// Find the path to an executable in the session's PATH.
704///
705/// # Arguments
706/// * `session` - The session to search in
707/// * `name` - The name of the executable to find
708///
709/// # Returns
710/// The full path to the executable if found, or None if not found
711pub fn which(session: &dyn Session, name: &str) -> Option<String> {
712    let ret = match session.check_output(
713        vec!["which", name],
714        Some(std::path::Path::new("/")),
715        None,
716        None,
717    ) {
718        Ok(ret) => ret,
719        Err(Error::CalledProcessError(status)) if status.code() == Some(1) => return None,
720        Err(e) => panic!("Unexpected error: {:?}", e),
721    };
722    if ret.is_empty() {
723        None
724    } else {
725        Some(String::from_utf8(ret).unwrap().trim().to_owned())
726    }
727}
728
729/// Get the current user in the session.
730///
731/// # Arguments
732/// * `session` - The session to get the user from
733///
734/// # Returns
735/// The username of the current user
736pub fn get_user(session: &dyn Session) -> String {
737    // Use `id -un` rather than `$USER`: the latter is frequently unset in
738    // containers and minimal environments, where it would wrongly read as a
739    // non-root user and make ognibuild pick the user installation scope (and
740    // so drop the apt installer) even when running as root.
741    String::from_utf8(
742        session
743            .check_output(
744                vec!["id", "-un"],
745                Some(std::path::Path::new("/")),
746                None,
747                None,
748            )
749            .unwrap(),
750    )
751    .unwrap()
752    .trim()
753    .to_owned()
754}
755
756/// A function to capture and forward stdout and stderr of a child process.
757fn capture_output(
758    mut child: std::process::Child,
759    forward: bool,
760) -> Result<(std::process::ExitStatus, Vec<String>), std::io::Error> {
761    use std::io::{BufRead, BufReader};
762    use std::sync::mpsc::{channel, Receiver, Sender};
763    use std::thread;
764    let mut output_log = Vec::<String>::new();
765
766    // Channels to handle communication from threads
767    let (tx, rx): (Sender<Option<String>>, Receiver<Option<String>>) = channel();
768
769    // Function to handle the stdout of the child process
770    let stdout_tx = tx.clone();
771    let stdout = child.stdout.take().expect("Failed to capture stdout");
772    let stdout_handle = thread::spawn(move || -> Result<(), std::io::Error> {
773        let reader = BufReader::new(stdout);
774        for line in reader.lines() {
775            let line = line?;
776            if forward {
777                std::io::stdout().write_all(line.as_bytes())?;
778                std::io::stdout().write_all(b"\n")?;
779            }
780            stdout_tx
781                .send(Some(line))
782                .expect("Failed to send stdout through channel");
783        }
784
785        stdout_tx
786            .send(None)
787            .expect("Failed to send None through channel");
788        Ok(())
789    });
790
791    // Function to handle the stderr of the child process
792    let stderr_tx = tx.clone();
793    let stderr = child.stderr.take().expect("Failed to capture stderr");
794    let stderr_handle = thread::spawn(move || -> Result<(), std::io::Error> {
795        let reader = BufReader::new(stderr);
796        for line in reader.lines() {
797            let line = line?;
798            if forward {
799                std::io::stderr().write_all(line.as_bytes())?;
800                std::io::stderr().write_all(b"\n")?;
801            }
802            stderr_tx
803                .send(Some(line))
804                .expect("Failed to send stderr through channel");
805        }
806        stderr_tx
807            .send(None)
808            .expect("Failed to send None through channel");
809        Ok(())
810    });
811
812    // Wait for the child process to exit
813    let status = child.wait().expect("Child process wasn't running");
814    stderr_handle
815        .join()
816        .expect("Failed to join stderr thread")?;
817    stdout_handle
818        .join()
819        .expect("Failed to join stdout thread")?;
820
821    let mut terminated = 0;
822
823    // Collect all output from both stdout and stderr
824    while let Ok(line) = rx.recv() {
825        if let Some(line) = line {
826            output_log.push(line);
827        } else {
828            terminated += 1;
829            if terminated == 2 {
830                break;
831            }
832        }
833    }
834
835    Ok((status, output_log))
836}
837
838/// Run a command and capture its output, while also displaying it.
839///
840/// This function executes a command in the given session and collects
841/// its output, while also displaying it in real time.
842///
843/// # Arguments
844/// * `session` - The session to run the command in
845/// * `args` - The command and its arguments
846/// * `cwd` - Optional current working directory
847/// * `user` - Optional user to run the command as
848/// * `env` - Optional environment variables
849/// * `stdin` - Optional stdin configuration
850/// * `quiet` - Whether to suppress output
851///
852/// # Returns
853/// * `Ok((ExitStatus, Vec<String>))` - The exit status and output lines if successful
854/// * `Err(Error)` - If the command fails
855pub fn run_with_tee(
856    session: &dyn Session,
857    args: Vec<&str>,
858    cwd: Option<&std::path::Path>,
859    user: Option<&str>,
860    env: Option<&std::collections::HashMap<String, String>>,
861    stdin: Option<std::process::Stdio>,
862    quiet: bool,
863) -> Result<(ExitStatus, Vec<String>), Error> {
864    if let (Some(cwd), Some(user)) = (cwd, user) {
865        log::debug!("Running command: {:?} in {:?} as user {}", args, cwd, user);
866    } else if let Some(cwd) = cwd {
867        log::debug!("Running command: {:?} in {:?}", args, cwd);
868    } else if let Some(user) = user {
869        log::debug!("Running command: {:?} as user {}", args, user);
870    } else {
871        log::debug!("Running command: {:?}", args);
872    }
873    let p = session.popen(
874        args,
875        cwd,
876        user,
877        Some(std::process::Stdio::piped()),
878        Some(std::process::Stdio::piped()),
879        Some(stdin.unwrap_or(std::process::Stdio::null())),
880        env,
881    )?;
882    // While the process is running, read its output and write it to stdout
883    // *and* to the contents variable.
884    Ok(capture_output(p, !quiet)?)
885}
886
887/// Create the user's home directory in the session.
888///
889/// This function creates the user's home directory in the session,
890/// which is needed for some commands that write to the home directory.
891///
892/// # Arguments
893/// * `session` - The session to create the home directory in
894///
895/// # Returns
896/// * `Ok(())` if the home directory was created successfully
897/// * `Err(Error)` if creating the home directory fails
898pub fn create_home(session: &impl Session) -> Result<(), Error> {
899    let cwd = std::path::Path::new("/");
900
901    // Resolve the home directory and ownership from the session's own user
902    // database rather than the inherited $HOME/$LOGNAME, which leak in from the
903    // host and may not match the user inside the session (e.g. an unshare
904    // session whose uid maps to a different name in /etc/passwd). Fall back to
905    // $HOME only if the passwd entry has no home field.
906    let uid = run_single_line(session, cwd, "id -u", "determine current uid in session")?;
907    let gid = run_single_line(session, cwd, "id -g", "determine current gid in session")?;
908    let home = run_single_line(
909        session,
910        cwd,
911        "getent passwd \"$(id -u)\" | cut -d: -f6 || true",
912        "determine home directory in session",
913    )?;
914    let home = if home.is_empty() {
915        run_single_line(session, cwd, "echo \"$HOME\"", "determine $HOME in session")?
916    } else {
917        home
918    };
919
920    log::info!("Creating home directory {} in session.", home);
921    session.check_call(vec!["mkdir", "-p", &home], Some(cwd), Some("root"), None)?;
922    // Chown by numeric uid:gid, which always exists, rather than a username that
923    // may not be present in the session's passwd database.
924    let owner = format!("{}:{}", uid, gid);
925    session.check_call(vec!["chown", &owner, &home], Some(cwd), Some("root"), None)?;
926    Ok(())
927}
928
929/// Run a shell snippet in the session and return its trimmed single-line output.
930fn run_single_line(
931    session: &impl Session,
932    cwd: &std::path::Path,
933    script: &str,
934    what: &str,
935) -> Result<String, Error> {
936    let out = session.check_output(vec!["sh", "-c", script], Some(cwd), None, None)?;
937    String::from_utf8(out)
938        .map_err(|e| {
939            Error::SetupFailure(
940                format!("Failed to {}", what),
941                format!("non-UTF-8 output: {}", e),
942            )
943        })
944        .map(|s| s.trim().to_string())
945}
946
947#[cfg(test)]
948mod tests {
949    #[test]
950    fn test_get_user() {
951        let session = super::plain::PlainSession::new();
952        let user = super::get_user(&session);
953        let expected = String::from_utf8(
954            std::process::Command::new("id")
955                .arg("-un")
956                .output()
957                .unwrap()
958                .stdout,
959        )
960        .unwrap()
961        .trim()
962        .to_owned();
963        assert_eq!(user, expected);
964    }
965
966    #[test]
967    fn test_which() {
968        let session = super::plain::PlainSession::new();
969        let which = super::which(&session, "ls");
970        assert!(which.unwrap().ends_with("/ls"));
971    }
972
973    #[test]
974    fn test_session_kind_from_str() {
975        use super::SessionKind;
976        use std::str::FromStr;
977        assert_eq!(SessionKind::from_str("plain"), Ok(SessionKind::Plain));
978        assert_eq!(
979            SessionKind::from_str("schroot:sid"),
980            Ok(SessionKind::Schroot("sid".to_string()))
981        );
982        assert_eq!(
983            SessionKind::from_str("unshare:bookworm"),
984            Ok(SessionKind::Unshare("bookworm".to_string()))
985        );
986        assert_eq!(
987            SessionKind::from_str("unshare"),
988            Ok(SessionKind::Unshare("sid".to_string()))
989        );
990    }
991
992    #[test]
993    fn test_session_kind_from_str_errors() {
994        use super::SessionKind;
995        use std::str::FromStr;
996        assert!(SessionKind::from_str("bogus").is_err());
997        assert!(SessionKind::from_str("schroot:").is_err());
998        assert!(SessionKind::from_str("unshare:").is_err());
999        assert!(SessionKind::from_str("docker:foo").is_err());
1000    }
1001
1002    #[test]
1003    fn test_resolve_session_kind() {
1004        use super::{resolve_session_kind, SessionKind};
1005        assert_eq!(resolve_session_kind(None, None), Ok(SessionKind::Plain));
1006        assert_eq!(
1007            resolve_session_kind(Some(SessionKind::Unshare("sid".to_string())), None),
1008            Ok(SessionKind::Unshare("sid".to_string()))
1009        );
1010        assert_eq!(
1011            resolve_session_kind(None, Some("foo".to_string())),
1012            Ok(SessionKind::Schroot("foo".to_string()))
1013        );
1014        assert!(resolve_session_kind(Some(SessionKind::Plain), Some("foo".to_string())).is_err());
1015    }
1016
1017    #[test]
1018    fn test_capture_and_forward_output() {
1019        let p = std::process::Command::new("echo")
1020            .arg("Hello, world!")
1021            .stdout(std::process::Stdio::piped())
1022            .stderr(std::process::Stdio::piped())
1023            .spawn()
1024            .unwrap();
1025
1026        let (status, output) = super::capture_output(p, false).unwrap();
1027        assert!(status.success());
1028        assert_eq!(output, vec!["Hello, world!"]);
1029    }
1030}
1031
1032/// Test utilities for sessions
1033#[cfg(test)]
1034pub mod test_utils {
1035    /// Get a test session for use in tests.
1036    ///
1037    /// This returns an isolated session that's suitable for testing.
1038    /// On Linux, it tries to create an UnshareSession for better isolation.
1039    /// If that fails (e.g., no unshare permissions), it falls back to PlainSession.
1040    /// The session is isolated from the host system when possible.
1041    ///
1042    /// Returns None only if no session can be created at all.
1043    #[cfg(target_os = "linux")]
1044    pub fn get_test_session() -> Option<Box<dyn super::Session>> {
1045        // In CI environments like GitHub Actions, skip UnshareSession due to permission restrictions
1046        if std::env::var("GITHUB_ACTIONS").is_ok() {
1047            return Some(Box::new(super::plain::PlainSession::new()));
1048        }
1049
1050        // Try to create an UnshareSession for isolation
1051        if let Ok(session) = super::unshare::UnshareSession::bootstrap() {
1052            return Some(Box::new(session));
1053        }
1054
1055        // Fall back to PlainSession if unshare isn't available
1056        Some(Box::new(super::plain::PlainSession::new()))
1057    }
1058
1059    /// Get a test session for use in tests (non-Linux fallback).
1060    ///
1061    /// On non-Linux systems, returns a PlainSession for testing.
1062    #[cfg(not(target_os = "linux"))]
1063    pub fn get_test_session() -> Option<Box<dyn super::Session>> {
1064        Some(Box::new(super::plain::PlainSession::new()))
1065    }
1066}