1use std::collections::VecDeque;
14use std::ffi::OsString;
15use std::fmt;
16use std::io::{self, Read, Write};
17use std::path::PathBuf;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::{Arc, Mutex, MutexGuard, PoisonError, mpsc};
20use std::thread;
21use std::time::{Duration, Instant};
22
23use portable_pty::{ChildKiller, CommandBuilder, MasterPty, PtySize, native_pty_system};
24
25pub type Notify = Arc<dyn Fn() + Send + Sync>;
29
30#[derive(Clone, Debug, PartialEq, Eq, Hash)]
32pub struct ExitStatus {
33 code: u32,
34 signal: Option<String>,
35}
36
37impl ExitStatus {
38 pub fn with_code(code: u32) -> ExitStatus {
40 ExitStatus { code, signal: None }
41 }
42
43 pub fn with_signal(signal: impl Into<String>) -> ExitStatus {
45 ExitStatus {
46 code: 1,
47 signal: Some(signal.into()),
48 }
49 }
50
51 pub fn code(&self) -> u32 {
53 self.code
54 }
55
56 pub fn signal(&self) -> Option<&str> {
58 self.signal.as_deref()
59 }
60
61 pub fn success(&self) -> bool {
63 self.code == 0 && self.signal.is_none()
64 }
65}
66
67impl fmt::Display for ExitStatus {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match &self.signal {
70 Some(signal) => write!(f, "ended by signal {signal}"),
71 None => write!(f, "exited with code {}", self.code),
72 }
73 }
74}
75
76impl From<portable_pty::ExitStatus> for ExitStatus {
77 fn from(status: portable_pty::ExitStatus) -> ExitStatus {
78 match status.signal() {
79 Some(signal) => ExitStatus::with_signal(signal),
80 None => ExitStatus::with_code(status.exit_code()),
81 }
82 }
83}
84
85pub trait PtyHost {
88 fn start(&mut self, columns: u16, rows: u16) -> io::Result<()>;
91
92 fn write(&mut self, bytes: &[u8]) -> io::Result<()>;
97
98 fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()>;
100
101 fn read(&mut self) -> Vec<u8>;
104
105 fn set_notify(&mut self, notify: Notify);
109
110 fn exit_status(&mut self) -> Option<ExitStatus>;
113
114 fn kill(&mut self) -> io::Result<()>;
116}
117
118#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct Command {
123 program: OsString,
124 args: Vec<OsString>,
125 env: Vec<(OsString, OsString)>,
126 cwd: Option<PathBuf>,
127}
128
129impl Command {
130 pub fn new(program: impl Into<OsString>) -> Command {
131 Command {
132 program: program.into(),
133 args: Vec::new(),
134 env: Vec::new(),
135 cwd: None,
136 }
137 }
138
139 #[must_use]
140 pub fn arg(mut self, arg: impl Into<OsString>) -> Command {
141 self.args.push(arg.into());
142 self
143 }
144
145 #[must_use]
146 pub fn args<I: IntoIterator<Item = S>, S: Into<OsString>>(mut self, args: I) -> Command {
147 self.args.extend(args.into_iter().map(Into::into));
148 self
149 }
150
151 #[must_use]
154 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Command {
155 self.env.push((key.into(), value.into()));
156 self
157 }
158
159 #[must_use]
161 pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Command {
162 self.cwd = Some(dir.into());
163 self
164 }
165
166 pub fn program(&self) -> &OsString {
168 &self.program
169 }
170
171 fn builder(&self) -> CommandBuilder {
172 let mut builder = CommandBuilder::new(&self.program);
173 builder.args(&self.args);
174 builder.env("TERM", "xterm-256color");
175 for (key, value) in &self.env {
176 builder.env(key, value);
177 }
178 match &self.cwd {
179 Some(dir) => builder.cwd(dir),
180 None => {
182 if let Ok(dir) = std::env::current_dir() {
183 builder.cwd(dir);
184 }
185 }
186 }
187 builder
188 }
189}
190
191impl From<&str> for Command {
192 fn from(program: &str) -> Command {
193 Command::new(program)
194 }
195}
196
197impl From<String> for Command {
198 fn from(program: String) -> Command {
199 Command::new(program)
200 }
201}
202
203impl<S: Into<OsString>, const N: usize> From<[S; N]> for Command {
204 fn from(parts: [S; N]) -> Command {
205 Command::from_parts(parts)
206 }
207}
208
209impl<S: Into<OsString>> From<Vec<S>> for Command {
210 fn from(parts: Vec<S>) -> Command {
211 Command::from_parts(parts)
212 }
213}
214
215impl Command {
216 fn from_parts<S: Into<OsString>>(parts: impl IntoIterator<Item = S>) -> Command {
217 let mut parts = parts.into_iter().map(Into::into);
218 let program = parts.next().unwrap_or_default();
219 Command::new(program).args(parts)
220 }
221}
222
223const MAX_BUFFERED: usize = 16 * 1024 * 1024;
227const MAX_INPUT: usize = 1024 * 1024;
230const KILL_GRACE: Duration = Duration::from_secs(1);
232const WAIT_TURN: Duration = Duration::from_millis(20);
234
235#[derive(Default)]
236struct Output {
237 buffer: VecDeque<u8>,
238 eof: bool,
240 paused: bool,
242 closed: bool,
244 hung_up: Option<Instant>,
246 status: Option<ExitStatus>,
247 notify: Option<Notify>,
248}
249
250fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
251 mutex.lock().unwrap_or_else(PoisonError::into_inner)
252}
253
254fn notify(shared: &Mutex<Output>) {
256 let notify = lock(shared).notify.clone();
257 if let Some(notify) = notify {
258 notify();
259 }
260}
261
262pub struct LocalPty {
265 command: Command,
266 shared: Arc<Mutex<Output>>,
267 master: Option<Box<dyn MasterPty + Send>>,
268 writer: Option<mpsc::Sender<Vec<u8>>>,
271 queued: Arc<AtomicUsize>,
273 killer: Option<Box<dyn ChildKiller + Send + Sync>>,
274 backpressure: Option<usize>,
276}
277
278impl LocalPty {
279 pub fn new(command: impl Into<Command>) -> LocalPty {
280 LocalPty {
281 command: command.into(),
282 shared: Arc::default(),
283 master: None,
284 writer: None,
285 queued: Arc::default(),
286 killer: None,
287 backpressure: None,
288 }
289 }
290
291 #[must_use]
301 pub fn backpressure(mut self, bytes: usize) -> LocalPty {
302 self.backpressure = Some(bytes.max(1));
303 self
304 }
305
306 fn size(columns: u16, rows: u16) -> PtySize {
307 PtySize {
308 rows: rows.max(1),
309 cols: columns.max(1),
310 pixel_width: 0,
311 pixel_height: 0,
312 }
313 }
314}
315
316impl PtyHost for LocalPty {
317 fn start(&mut self, columns: u16, rows: u16) -> io::Result<()> {
318 if self.master.is_some() {
319 return Ok(());
320 }
321 let pty = native_pty_system()
322 .openpty(LocalPty::size(columns, rows))
323 .map_err(io::Error::other)?;
324 let mut child = pty
325 .slave
326 .spawn_command(self.command.builder())
327 .map_err(io::Error::other)?;
328 drop(pty.slave);
330 let mut reader = pty.master.try_clone_reader().map_err(io::Error::other)?;
331 let mut writer = pty.master.take_writer().map_err(io::Error::other)?;
332 let (input, keys) = mpsc::channel::<Vec<u8>>();
333 let queued = Arc::clone(&self.queued);
334 thread::spawn(move || {
335 for bytes in keys {
336 if writer
337 .write_all(&bytes)
338 .and_then(|()| writer.flush())
339 .is_err()
340 {
341 queued.store(0, Ordering::SeqCst);
343 break;
344 }
345 queued.fetch_sub(bytes.len(), Ordering::SeqCst);
346 }
347 });
348 self.writer = Some(input);
349 self.killer = Some(child.clone_killer());
350 self.master = Some(pty.master);
351
352 let shared = Arc::clone(&self.shared);
353 let limit = self.backpressure;
354 thread::spawn(move || {
355 let mut chunk = [0u8; 65536];
356 loop {
357 if let Some(limit) = limit
358 && !wait_for_room(&shared, limit)
359 {
360 break;
361 }
362 let read = match reader.read(&mut chunk) {
363 Ok(0) | Err(_) => break,
364 Ok(read) => read,
365 };
366 keep(&mut lock(&shared), &chunk[..read], limit.is_none());
367 notify(&shared);
368 }
369 lock(&shared).eof = true;
370 notify(&shared);
371 });
372
373 let shared = Arc::clone(&self.shared);
374 let leader = child.process_id();
375 thread::spawn(move || {
376 let mut killed = false;
380 let status = loop {
381 match child.try_wait() {
382 Ok(Some(status)) => break ExitStatus::from(status),
383 Ok(None) => {}
384 Err(error) => break ExitStatus::with_signal(error.to_string()),
385 }
386 let hung_up = lock(&shared).hung_up;
387 if !killed && hung_up.is_some_and(|at| at.elapsed() >= KILL_GRACE) {
388 if let Some(leader) = leader {
389 kill_group(leader);
390 }
391 killed = true;
392 }
393 thread::sleep(WAIT_TURN);
394 };
395 let mut waited = 0;
400 while waited < 50 {
401 let (eof, paused) = {
402 let output = lock(&shared);
403 (output.eof, output.paused)
404 };
405 if eof {
406 break;
407 }
408 if !paused {
409 waited += 1;
410 }
411 thread::sleep(Duration::from_millis(10));
412 }
413 lock(&shared).status = Some(status);
414 notify(&shared);
415 });
416 Ok(())
417 }
418
419 fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
424 if self.writer.is_some() && self.queued.load(Ordering::SeqCst) >= MAX_INPUT {
425 return Err(io::Error::new(
426 io::ErrorKind::WouldBlock,
427 "the program is not reading its input",
428 ));
429 }
430 match &self.writer {
431 Some(writer) => {
432 self.queued.fetch_add(bytes.len(), Ordering::SeqCst);
433 writer.send(bytes.to_vec()).map_err(|_| {
434 self.queued.store(0, Ordering::SeqCst);
435 io::Error::new(io::ErrorKind::BrokenPipe, "the program has gone")
436 })
437 }
438 None => Err(io::Error::new(
439 io::ErrorKind::NotConnected,
440 "the program has not started",
441 )),
442 }
443 }
444
445 fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
446 match &self.master {
447 Some(master) => master
448 .resize(LocalPty::size(columns, rows))
449 .map_err(io::Error::other),
450 None => Ok(()),
451 }
452 }
453
454 fn read(&mut self) -> Vec<u8> {
455 lock(&self.shared).buffer.drain(..).collect()
456 }
457
458 fn set_notify(&mut self, notify: Notify) {
459 let waiting = {
460 let mut output = lock(&self.shared);
461 output.notify = Some(notify.clone());
462 !output.buffer.is_empty() || output.status.is_some()
463 };
464 if waiting {
465 notify();
466 }
467 }
468
469 fn exit_status(&mut self) -> Option<ExitStatus> {
470 let output = lock(&self.shared);
471 if output.buffer.is_empty() {
472 output.status.clone()
473 } else {
474 None
475 }
476 }
477
478 fn kill(&mut self) -> io::Result<()> {
483 {
484 let mut output = lock(&self.shared);
485 if output.status.is_some() {
486 return Ok(());
487 }
488 if self.killer.is_some() {
489 output.hung_up.get_or_insert_with(Instant::now);
490 }
491 }
492 match &mut self.killer {
493 Some(killer) => killer.kill(),
494 None => Ok(()),
495 }
496 }
497}
498
499impl Drop for LocalPty {
500 fn drop(&mut self) {
501 lock(&self.shared).closed = true;
502 let _ = self.kill();
503 }
504}
505
506#[cfg(unix)]
509#[allow(unsafe_code)]
510fn kill_group(leader: u32) {
511 if let Ok(group) = libc::pid_t::try_from(leader) {
512 unsafe {
515 libc::killpg(group, libc::SIGKILL);
516 }
517 }
518}
519
520#[cfg(not(unix))]
522fn kill_group(_leader: u32) {}
523
524fn keep(output: &mut Output, bytes: &[u8], drop_oldest: bool) {
529 output.buffer.extend(bytes);
530 if drop_oldest {
531 let over = output.buffer.len().saturating_sub(MAX_BUFFERED);
532 output.buffer.drain(..over);
533 }
534}
535
536fn wait_for_room(shared: &Mutex<Output>, limit: usize) -> bool {
539 loop {
540 {
541 let mut output = lock(shared);
542 if output.closed {
543 return false;
544 }
545 output.paused = output.buffer.len() >= limit;
546 if !output.paused {
547 return true;
548 }
549 }
550 thread::sleep(Duration::from_millis(5));
551 }
552}
553
554#[derive(Default)]
557struct Replay {
558 script: Vec<u8>,
560 script_exit: Option<ExitStatus>,
562 pending: Vec<u8>,
564 status: Option<ExitStatus>,
565 started: Option<(u16, u16)>,
566 written: Vec<u8>,
567 sizes: Vec<(u16, u16)>,
568 killed: bool,
569 notify: Option<Notify>,
570}
571
572#[derive(Default)]
585pub struct ReplayHost {
586 state: Arc<Mutex<Replay>>,
587}
588
589impl ReplayHost {
590 pub fn new() -> ReplayHost {
591 ReplayHost::default()
592 }
593
594 #[must_use]
596 pub fn output(self, bytes: impl AsRef<[u8]>) -> ReplayHost {
597 lock(&self.state).script.extend_from_slice(bytes.as_ref());
598 self
599 }
600
601 #[must_use]
603 pub fn exit(self, status: ExitStatus) -> ReplayHost {
604 lock(&self.state).script_exit = Some(status);
605 self
606 }
607
608 pub fn handle(&self) -> ReplayHandle {
611 ReplayHandle {
612 state: Arc::clone(&self.state),
613 }
614 }
615}
616
617#[derive(Clone)]
619pub struct ReplayHandle {
620 state: Arc<Mutex<Replay>>,
621}
622
623impl ReplayHandle {
624 fn notify(&self) {
625 let notify = lock(&self.state).notify.clone();
626 if let Some(notify) = notify {
627 notify();
628 }
629 }
630
631 pub fn feed(&self, bytes: impl AsRef<[u8]>) {
634 {
635 let mut state = lock(&self.state);
636 if state.started.is_some() {
637 state.pending.extend_from_slice(bytes.as_ref());
638 } else {
639 state.script.extend_from_slice(bytes.as_ref());
640 }
641 }
642 self.notify();
643 }
644
645 pub fn exit(&self, status: ExitStatus) {
647 {
648 let mut state = lock(&self.state);
649 if state.started.is_some() {
650 state.status = Some(status);
651 } else {
652 state.script_exit = Some(status);
653 }
654 }
655 self.notify();
656 }
657
658 pub fn written(&self) -> Vec<u8> {
660 lock(&self.state).written.clone()
661 }
662
663 pub fn take_written(&self) -> Vec<u8> {
665 std::mem::take(&mut lock(&self.state).written)
666 }
667
668 pub fn started(&self) -> Option<(u16, u16)> {
670 lock(&self.state).started
671 }
672
673 pub fn sizes(&self) -> Vec<(u16, u16)> {
675 lock(&self.state).sizes.clone()
676 }
677
678 pub fn killed(&self) -> bool {
680 lock(&self.state).killed
681 }
682}
683
684impl PtyHost for ReplayHost {
685 fn start(&mut self, columns: u16, rows: u16) -> io::Result<()> {
686 {
687 let mut state = lock(&self.state);
688 if state.started.is_some() {
689 return Ok(());
690 }
691 state.started = Some((columns, rows));
692 let script = std::mem::take(&mut state.script);
693 state.pending.extend_from_slice(&script);
694 state.status = state.script_exit.take();
695 }
696 self.handle().notify();
697 Ok(())
698 }
699
700 fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
701 lock(&self.state).written.extend_from_slice(bytes);
702 Ok(())
703 }
704
705 fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
706 lock(&self.state).sizes.push((columns, rows));
707 Ok(())
708 }
709
710 fn read(&mut self) -> Vec<u8> {
711 std::mem::take(&mut lock(&self.state).pending)
712 }
713
714 fn set_notify(&mut self, notify: Notify) {
715 let waiting = {
716 let mut state = lock(&self.state);
717 state.notify = Some(notify.clone());
718 !state.pending.is_empty() || state.status.is_some()
719 };
720 if waiting {
721 notify();
722 }
723 }
724
725 fn exit_status(&mut self) -> Option<ExitStatus> {
726 let state = lock(&self.state);
727 if state.pending.is_empty() {
728 state.status.clone()
729 } else {
730 None
731 }
732 }
733
734 fn kill(&mut self) -> io::Result<()> {
735 let mut state = lock(&self.state);
736 state.killed = true;
737 if state.status.is_none() && state.started.is_some() {
738 state.status = Some(ExitStatus::with_signal("Killed"));
739 }
740 Ok(())
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747
748 #[test]
749 fn backpressure_never_drops_output_past_the_legacy_limit() {
750 let chunk = vec![b'x'; 1 << 20];
751 let mut held = Output::default();
753 for _ in 0..(MAX_BUFFERED / chunk.len() + 4) {
754 keep(&mut held, &chunk, false);
755 }
756 assert_eq!(held.buffer.len(), MAX_BUFFERED + 4 * chunk.len());
757 let mut dropped = Output::default();
759 for _ in 0..(MAX_BUFFERED / chunk.len() + 4) {
760 keep(&mut dropped, &chunk, true);
761 }
762 assert_eq!(dropped.buffer.len(), MAX_BUFFERED);
763 }
764}