char/avril

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

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

Charlotte Somadd progress to module imports from registries7b01146

main
8.5 KiB276 linesraw
1#![cfg(unix)]
2
3#[path = "common/registry.rs"]
4mod fixture;
5
6use std::fs::File;
7use std::io::{Read, Write};
8use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
9use std::os::unix::process::CommandExt;
10use std::process::{Child, Command, Stdio};
11use std::sync::Mutex;
12use std::sync::atomic::Ordering;
13use std::thread;
14use std::time::{Duration, Instant};
15
16use fixture::Registry;
17
18const DEADLINE: Duration = Duration::from_secs(10);
19
20struct Pty {
21    child: Child,
22    master: File,
23    bytes: Vec<u8>,
24    cursor_scan: usize,
25    eof: bool,
26}
27
28impl Pty {
29    fn spawn(args: &[String], directory: &std::path::Path) -> Self {
30        let winsize = libc::winsize {
31            ws_row: 24,
32            ws_col: 80,
33            ws_xpixel: 0,
34            ws_ypixel: 0,
35        };
36        let (mut master, mut slave) = (-1, -1);
37        assert_eq!(
38            unsafe {
39                libc::openpty(
40                    &mut master,
41                    &mut slave,
42                    std::ptr::null_mut(),
43                    std::ptr::null(),
44                    &winsize,
45                )
46            },
47            0
48        );
49        let master = unsafe { OwnedFd::from_raw_fd(master) };
50        let slave = unsafe { OwnedFd::from_raw_fd(slave) };
51        for fd in [&master, &slave] {
52            let flags = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFD) };
53            assert!(flags >= 0);
54            assert_eq!(
55                unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, flags | libc::FD_CLOEXEC) },
56                0
57            );
58        }
59
60        let mut command = Command::new(env!("CARGO_BIN_EXE_avril"));
61        command
62            .args(args)
63            .current_dir(directory)
64            .env("TERM", "xterm")
65            .env("NO_COLOR", "1")
66            .stdin(Stdio::from(slave.try_clone().unwrap()))
67            .stdout(Stdio::from(slave.try_clone().unwrap()))
68            .stderr(Stdio::from(slave.try_clone().unwrap()));
69        unsafe {
70            command.pre_exec(|| {
71                if libc::setsid() < 0 || libc::ioctl(libc::STDIN_FILENO, libc::TIOCSCTTY, 0) < 0 {
72                    return Err(std::io::Error::last_os_error());
73                }
74                libc::signal(libc::SIGINT, libc::SIG_DFL);
75                libc::signal(libc::SIGTERM, libc::SIG_DFL);
76                Ok(())
77            });
78        }
79        let child = command.spawn().unwrap();
80        drop(slave);
81        Self {
82            child,
83            master: File::from(master),
84            bytes: Vec::new(),
85            cursor_scan: 0,
86            eof: false,
87        }
88    }
89
90    fn write(&mut self, bytes: &[u8]) {
91        self.master.write_all(bytes).unwrap();
92    }
93
94    fn read_until(&mut self, needle: &[u8]) -> usize {
95        self.read_until_from(0, needle)
96    }
97
98    fn read_until_from(&mut self, start: usize, needle: &[u8]) -> usize {
99        let deadline = Instant::now() + DEADLINE;
100        loop {
101            if let Some(position) = self.bytes[start..]
102                .windows(needle.len())
103                .position(|w| w == needle)
104            {
105                return start + position + needle.len();
106            }
107            assert!(
108                Instant::now() < deadline,
109                "timed out waiting for {:?}; got {:?}",
110                String::from_utf8_lossy(needle),
111                String::from_utf8_lossy(&self.bytes)
112            );
113            assert!(
114                self.read_more(deadline),
115                "PTY closed before {:?}: {}",
116                String::from_utf8_lossy(needle),
117                String::from_utf8_lossy(&self.bytes)
118            );
119        }
120    }
121
122    fn read_more(&mut self, deadline: Instant) -> bool {
123        if self.eof {
124            return false;
125        }
126        let mut poll = libc::pollfd {
127            fd: self.master.as_raw_fd(),
128            events: libc::POLLIN,
129            revents: 0,
130        };
131        let timeout = deadline
132            .saturating_duration_since(Instant::now())
133            .as_millis()
134            .min(i32::MAX as u128) as i32;
135        let ready = unsafe { libc::poll(&mut poll, 1, timeout) };
136        if ready < 0 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted {
137            return true;
138        }
139        assert!(
140            ready >= 0,
141            "PTY poll failed: {}",
142            std::io::Error::last_os_error()
143        );
144        if ready == 0 {
145            return false;
146        }
147        let mut buffer = [0u8; 4096];
148        match self.master.read(&mut buffer) {
149            Ok(0) => self.eof = true,
150            Ok(count) => {
151                self.bytes.extend_from_slice(&buffer[..count]);
152                self.answer_cursor_queries();
153                return true;
154            }
155            Err(error) if error.raw_os_error() == Some(libc::EIO) => self.eof = true,
156            Err(error) if error.kind() == std::io::ErrorKind::Interrupted => return true,
157            Err(error) => panic!("PTY read failed: {error}"),
158        }
159        false
160    }
161
162    fn answer_cursor_queries(&mut self) {
163        while let Some(position) = self.bytes[self.cursor_scan..]
164            .windows(4)
165            .position(|w| w == b"\x1b[6n")
166        {
167            self.cursor_scan += position + 4;
168            self.write(b"\x1b[1;1R");
169        }
170    }
171
172    fn wait_exit(&mut self) {
173        let deadline = Instant::now() + DEADLINE;
174        loop {
175            if let Some(status) = self.child.try_wait().unwrap() {
176                while self.read_more(Instant::now()) {}
177                assert!(
178                    status.success(),
179                    "child exited {status}: {}",
180                    String::from_utf8_lossy(&self.bytes)
181                );
182                return;
183            }
184            assert!(Instant::now() < deadline, "child did not exit");
185            self.read_more(deadline);
186            thread::yield_now();
187        }
188    }
189}
190
191impl Drop for Pty {
192    fn drop(&mut self) {
193        if self.child.try_wait().ok().flatten().is_none() {
194            let _ = self.child.kill();
195            let _ = self.child.wait();
196        }
197    }
198}
199
200fn wait_for(observed: &Mutex<Vec<String>>, path: &str) {
201    let deadline = Instant::now() + DEADLINE;
202    while !observed.lock().unwrap().iter().any(|item| item == path) {
203        assert!(Instant::now() < deadline, "server did not record {path}");
204        thread::yield_now();
205    }
206}
207
208#[test]
209fn interrupted_repl_suppresses_distinct_late_http_progress() {
210    let server = Registry::new();
211    server.concurrent.store(true, Ordering::Relaxed);
212    server.files.lock().unwrap().insert(
213        "/mod.ts".into(),
214        b"import './helper.ts'; export const value = 42;".to_vec(),
215    );
216    server.files.lock().unwrap().insert(
217        "/helper.ts".into(),
218        b"console.log('LATE_HELPER_COMPLETE'); export {};".to_vec(),
219    );
220    server
221        .hold_paths
222        .lock()
223        .unwrap()
224        .extend(["/mod.ts".into(), "/helper.ts".into()]);
225    let directory = tempfile::tempdir().unwrap();
226    let mut child = Pty::spawn(
227        &[
228            "repl".into(),
229            format!("--cache-dir={}", directory.path().join("cache").display()),
230        ],
231        directory.path(),
232    );
233    child.read_until(b"> ");
234    // Keep module names out of echoed input so it cannot masquerade as progress.
235    child.write(
236        format!(
237            "await (await import('{}' + 'mod' + '.ts')).value\n",
238            server.url
239        )
240        .as_bytes(),
241    );
242    wait_for(&server.requests, "/mod.ts");
243    let progress = child.read_until(b"mod.ts");
244    assert_eq!(
245        unsafe { libc::kill(child.child.id() as i32, libc::SIGINT) },
246        0
247    );
248    let prompt = child.read_until_from(progress, b"> ");
249    server.hold_paths.lock().unwrap().remove("/mod.ts");
250    wait_for(&server.requests, "/helper.ts");
251    server.hold_paths.lock().unwrap().remove("/helper.ts");
252    wait_for(&server.responses, "/mod.ts");
253    wait_for(&server.responses, "/helper.ts");
254
255    let barrier = child.bytes.len();
256    child.write(
257        format!(
258            "await import('{}' + 'helper' + '.ts'); ['PROGRESS', 'BARRIER'].join('_')\n",
259            server.url
260        )
261        .as_bytes(),
262    );
263    child.read_until_from(prompt, b"LATE_HELPER_COMPLETE");
264    let completion = child.read_until_from(barrier, b"PROGRESS_BARRIER");
265    child.read_until_from(completion, b"> ");
266    let idle_output = String::from_utf8_lossy(&child.bytes[prompt..]);
267    for target in ["mod.ts", "helper.ts"] {
268        assert!(
269            !idle_output.contains(target),
270            "late progress escaped scope: {idle_output}"
271        );
272    }
273    child.write(b".exit\n");
274    child.wait_exit();
275    assert!(!directory.path().join("avril.lock").exists());
276}