#![cfg(any(target_os = "linux", target_os = "macos"))] use std::path::{Path, PathBuf}; use std::time::Duration; use avril_subprocess::{Executable, Launch, null, pipe, spawn}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; fn shell() -> PathBuf { std::env::split_paths(&std::env::var_os("PATH").unwrap()) .map(|directory| directory.join("sh")) .find(|path| path.is_file()) .unwrap() .canonicalize() .unwrap() } fn launch<'a>(program: &'a Path, args: &'a [String], cwd: &'a Path) -> Launch<'a> { #[cfg(target_os = "linux")] let (executable, _) = Executable::open_at( &std::fs::File::open(program.parent().unwrap()).unwrap(), program.file_name().unwrap(), ) .unwrap(); #[cfg(target_os = "macos")] let (executable, _) = Executable::open(program).unwrap(); Launch { executable, program, args, env: Vec::new(), cwd, group: true, join: None, terminal: false, endpoints: (0..3).map(|fd| (fd, null(fd == 0).unwrap())).collect(), } } #[tokio::test] async fn launches_with_explicit_environment_directory_and_pipes() { let program = shell(); let directory = tempfile::tempdir().unwrap(); let cwd = directory.path().canonicalize().unwrap(); let args = [ "-c".into(), "read -r value; printf '%s:%s:%s' \"$VALUE\" \"$value\" \"$PWD\"; exit 7".into(), ]; let mut command = launch(&program, &args, &cwd); command .env .push(("VALUE".into(), "from environment".into())); let (mut input, stdin) = pipe(true).unwrap(); let (mut output, stdout) = pipe(false).unwrap(); command.endpoints = vec![(0, stdin), (1, stdout), (2, null(false).unwrap())]; let mut child = spawn(command).unwrap(); let text = tokio::time::timeout(Duration::from_secs(5), async { input.write_all(b"from input\n").await.unwrap(); drop(input); let mut text = String::new(); output.read_to_string(&mut text).await.unwrap(); text }) .await .unwrap(); assert_eq!( text, format!("from environment:from input:{}", cwd.display()) ); let deadline = std::time::Instant::now() + Duration::from_secs(5); let observed = loop { if let Some(status) = child.observe_exit().unwrap() { break status; } assert!(std::time::Instant::now() < deadline, "child did not exit"); tokio::time::sleep(Duration::from_millis(5)).await; }; assert_eq!(observed.code(), Some(7)); assert_eq!(child.observe_exit().unwrap(), Some(observed)); assert_eq!(child.wait().unwrap(), observed); assert!(!child.signal(true, "SIGKILL").unwrap()); } #[test] fn rejects_invalid_descriptor_assignments_before_spawn() { let program = shell(); let args = ["-c".into(), "exit 0".into()]; for descriptor in [0, u32::MAX] { let mut command = launch(&program, &args, Path::new("/")); command.endpoints.push((descriptor, null(false).unwrap())); assert_eq!( spawn(command).err().unwrap().kind(), std::io::ErrorKind::InvalidInput ); } } #[test] fn exec_failure_is_a_spawn_error() { let directory = tempfile::tempdir().unwrap(); let program = directory.path().join("not-executable"); std::fs::write(&program, "not a program").unwrap(); let command = launch(&program, &[], directory.path()); assert_eq!( spawn(command).err().unwrap().kind(), std::io::ErrorKind::PermissionDenied ); } #[cfg(target_os = "linux")] #[test] fn opened_executable_survives_path_replacement() { use std::os::unix::fs::PermissionsExt; let directory = tempfile::tempdir().unwrap(); let program = directory.path().join("program"); std::fs::write(&program, format!("#!{}\nexit 7\n", shell().display())).unwrap(); std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap(); let command = launch(&program, &[], directory.path()); std::fs::rename(&program, directory.path().join("original")).unwrap(); std::fs::write(&program, format!("#!{}\nexit 42\n", shell().display())).unwrap(); std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap(); assert_eq!(spawn(command).unwrap().wait().unwrap().code(), Some(7)); }