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}