char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
4.3 KiB124 linesraw
1#![cfg(any(target_os = "linux", target_os = "macos"))]
2
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6use avril_subprocess::{Executable, Launch, null, pipe, spawn};
7use tokio::io::{AsyncReadExt, AsyncWriteExt};
8
9fn shell() -> PathBuf {
10    std::env::split_paths(&std::env::var_os("PATH").unwrap())
11        .map(|directory| directory.join("sh"))
12        .find(|path| path.is_file())
13        .unwrap()
14        .canonicalize()
15        .unwrap()
16}
17
18fn launch<'a>(program: &'a Path, args: &'a [String], cwd: &'a Path) -> Launch<'a> {
19    #[cfg(target_os = "linux")]
20    let (executable, _) = Executable::open_at(
21        &std::fs::File::open(program.parent().unwrap()).unwrap(),
22        program.file_name().unwrap(),
23    )
24    .unwrap();
25    #[cfg(target_os = "macos")]
26    let (executable, _) = Executable::open(program).unwrap();
27    Launch {
28        executable,
29        program,
30        args,
31        env: Vec::new(),
32        cwd,
33        group: true,
34        join: None,
35        terminal: false,
36        endpoints: (0..3).map(|fd| (fd, null(fd == 0).unwrap())).collect(),
37    }
38}
39
40#[tokio::test]
41async fn launches_with_explicit_environment_directory_and_pipes() {
42    let program = shell();
43    let directory = tempfile::tempdir().unwrap();
44    let cwd = directory.path().canonicalize().unwrap();
45    let args = [
46        "-c".into(),
47        "read -r value; printf '%s:%s:%s' \"$VALUE\" \"$value\" \"$PWD\"; exit 7".into(),
48    ];
49    let mut command = launch(&program, &args, &cwd);
50    command
51        .env
52        .push(("VALUE".into(), "from environment".into()));
53    let (mut input, stdin) = pipe(true).unwrap();
54    let (mut output, stdout) = pipe(false).unwrap();
55    command.endpoints = vec![(0, stdin), (1, stdout), (2, null(false).unwrap())];
56    let mut child = spawn(command).unwrap();
57    let text = tokio::time::timeout(Duration::from_secs(5), async {
58        input.write_all(b"from input\n").await.unwrap();
59        drop(input);
60        let mut text = String::new();
61        output.read_to_string(&mut text).await.unwrap();
62        text
63    })
64    .await
65    .unwrap();
66    assert_eq!(
67        text,
68        format!("from environment:from input:{}", cwd.display())
69    );
70    let deadline = std::time::Instant::now() + Duration::from_secs(5);
71    let observed = loop {
72        if let Some(status) = child.observe_exit().unwrap() {
73            break status;
74        }
75        assert!(std::time::Instant::now() < deadline, "child did not exit");
76        tokio::time::sleep(Duration::from_millis(5)).await;
77    };
78    assert_eq!(observed.code(), Some(7));
79    assert_eq!(child.observe_exit().unwrap(), Some(observed));
80    assert_eq!(child.wait().unwrap(), observed);
81    assert!(!child.signal(true, "SIGKILL").unwrap());
82}
83
84#[test]
85fn rejects_invalid_descriptor_assignments_before_spawn() {
86    let program = shell();
87    let args = ["-c".into(), "exit 0".into()];
88    for descriptor in [0, u32::MAX] {
89        let mut command = launch(&program, &args, Path::new("/"));
90        command.endpoints.push((descriptor, null(false).unwrap()));
91        assert_eq!(
92            spawn(command).err().unwrap().kind(),
93            std::io::ErrorKind::InvalidInput
94        );
95    }
96}
97
98#[test]
99fn exec_failure_is_a_spawn_error() {
100    let directory = tempfile::tempdir().unwrap();
101    let program = directory.path().join("not-executable");
102    std::fs::write(&program, "not a program").unwrap();
103    let command = launch(&program, &[], directory.path());
104    assert_eq!(
105        spawn(command).err().unwrap().kind(),
106        std::io::ErrorKind::PermissionDenied
107    );
108}
109
110#[cfg(target_os = "linux")]
111#[test]
112fn opened_executable_survives_path_replacement() {
113    use std::os::unix::fs::PermissionsExt;
114
115    let directory = tempfile::tempdir().unwrap();
116    let program = directory.path().join("program");
117    std::fs::write(&program, format!("#!{}\nexit 7\n", shell().display())).unwrap();
118    std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();
119    let command = launch(&program, &[], directory.path());
120    std::fs::rename(&program, directory.path().join("original")).unwrap();
121    std::fs::write(&program, format!("#!{}\nexit 42\n", shell().display())).unwrap();
122    std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();
123    assert_eq!(spawn(command).unwrap().wait().unwrap().code(), Some(7));
124}