1use std::collections::{BTreeMap, VecDeque};
4use std::sync::{Arc, Condvar, Mutex};
5use std::time::Duration;
6
7use termesh_core::{
8 PtyEvent, PtyRequest, TerminalGeneration, TerminalId, TerminalSize, TerminalSpec,
9};
10use termesh_terminal::{PtyError, PtyEventSink, PtyResult, PtyService};
11
12#[derive(Default)]
13struct State {
14 live: BTreeMap<TerminalId, (TerminalGeneration, PtyEventSink)>,
15 history: Vec<PtyRequest>,
16 pending: VecDeque<PtyRequest>,
17}
18
19type Shared = Arc<(Mutex<State>, Condvar)>;
20
21#[derive(Clone, Default)]
22pub struct ScriptedPty {
23 shared: Shared,
24}
25
26#[derive(Clone)]
27pub struct ScriptedPtyControl {
28 shared: Shared,
29}
30
31impl ScriptedPty {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn control(&self) -> ScriptedPtyControl {
37 ScriptedPtyControl { shared: self.shared.clone() }
38 }
39
40 fn record(&self, request: PtyRequest) {
41 let (lock, ready) = &*self.shared;
42 let mut state = lock.lock().expect("scripted PTY state poisoned");
43 state.history.push(request.clone());
44 state.pending.push_back(request);
45 ready.notify_all();
46 }
47}
48
49impl ScriptedPtyControl {
50 pub fn requests(&self) -> Vec<PtyRequest> {
51 self.shared.0.lock().expect("scripted PTY state poisoned").history.clone()
52 }
53
54 pub fn recv_request(&self, timeout: Duration) -> Option<PtyRequest> {
55 let (lock, ready) = &*self.shared;
56 let state = lock.lock().expect("scripted PTY state poisoned");
57 let (mut state, _) = ready
58 .wait_timeout_while(state, timeout, |state| state.pending.is_empty())
59 .expect("scripted PTY state poisoned");
60 state.pending.pop_front()
61 }
62
63 pub fn emit(&self, event: PtyEvent) -> bool {
64 let terminal = event_terminal(&event);
65 let sink = self
66 .shared
67 .0
68 .lock()
69 .expect("scripted PTY state poisoned")
70 .live
71 .get(&terminal)
72 .map(|(_, sink)| sink.clone());
73 if let Some(sink) = sink {
74 sink(event);
75 true
76 } else {
77 false
78 }
79 }
80}
81
82impl PtyService for ScriptedPty {
83 fn spawn(
84 &mut self,
85 terminal: TerminalId,
86 generation: TerminalGeneration,
87 spec: TerminalSpec,
88 size: TerminalSize,
89 sink: PtyEventSink,
90 ) -> PtyResult<()> {
91 {
92 let mut state = self.shared.0.lock().expect("scripted PTY state poisoned");
93 if state.live.contains_key(&terminal) {
94 return Err(PtyError::AlreadyExists(terminal));
95 }
96 state.live.insert(terminal, (generation, sink.clone()));
97 }
98 self.record(PtyRequest::Spawn { terminal, generation, spec, size });
99 sink(PtyEvent::Spawned { terminal, generation, process_id: None });
100 Ok(())
101 }
102
103 fn write(&mut self, terminal: TerminalId, bytes: &[u8]) -> PtyResult<()> {
104 self.require_live(terminal)?;
105 let generation = self.generation(terminal)?;
106 self.record(PtyRequest::Write { terminal, generation, bytes: bytes.to_vec() });
107 Ok(())
108 }
109
110 fn resize(&mut self, terminal: TerminalId, size: TerminalSize) -> PtyResult<()> {
111 self.require_live(terminal)?;
112 let generation = self.generation(terminal)?;
113 self.record(PtyRequest::Resize { terminal, generation, size });
114 Ok(())
115 }
116
117 fn kill(&mut self, terminal: TerminalId) -> PtyResult<()> {
118 let generation = self.generation(terminal)?;
119 self.record(PtyRequest::Kill { terminal, generation });
120 Ok(())
121 }
122
123 fn release(&mut self, terminal: TerminalId) -> PtyResult<()> {
124 let generation = self.generation(terminal)?;
125 self.shared.0.lock().expect("scripted PTY state poisoned").live.remove(&terminal);
130 self.record(PtyRequest::Release { terminal, generation });
131 Ok(())
132 }
133}
134
135impl ScriptedPty {
136 fn generation(&self, terminal: TerminalId) -> PtyResult<TerminalGeneration> {
137 self.shared
138 .0
139 .lock()
140 .expect("scripted PTY state poisoned")
141 .live
142 .get(&terminal)
143 .map(|(generation, _)| *generation)
144 .ok_or(PtyError::UnknownTerminal(terminal))
145 }
146
147 fn require_live(&self, terminal: TerminalId) -> PtyResult<()> {
148 if self.shared.0.lock().expect("scripted PTY state poisoned").live.contains_key(&terminal) {
149 Ok(())
150 } else {
151 Err(PtyError::UnknownTerminal(terminal))
152 }
153 }
154}
155
156fn event_terminal(event: &PtyEvent) -> TerminalId {
157 match event {
158 PtyEvent::Spawned { terminal, .. }
159 | PtyEvent::Output { terminal, .. }
160 | PtyEvent::Exited { terminal, .. }
161 | PtyEvent::Failed { terminal, .. } => *terminal,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use std::sync::mpsc;
169 use std::time::Duration;
170 use termesh_terminal::PtyWorker;
171
172 fn spec() -> TerminalSpec {
173 TerminalSpec {
174 program: "helper".into(),
175 args: vec!["--one".into()],
176 cwd: "/proj".into(),
177 env: Vec::new(),
178 }
179 }
180
181 #[test]
182 fn scripted_pty_records_requests_and_emits_only_for_live_terminals() {
183 let pty = ScriptedPty::new();
184 let control = pty.control();
185 let (tx, rx) = mpsc::channel();
186 let worker = PtyWorker::spawn(pty, move |event| {
187 tx.send(event).unwrap();
188 });
189 let terminal = TerminalId::new(3);
190 let generation = TerminalGeneration::new(1);
191 assert!(!control.emit(PtyEvent::Output {
192 terminal,
193 generation,
194 bytes: b"too early".to_vec(),
195 }));
196
197 let spawn = PtyRequest::Spawn {
198 terminal,
199 generation,
200 spec: spec(),
201 size: TerminalSize { rows: 24, cols: 80 },
202 };
203 assert!(worker.request(spawn.clone()));
204 assert_eq!(control.recv_request(Duration::from_secs(1)), Some(spawn));
205 assert!(matches!(
206 rx.recv_timeout(Duration::from_secs(1)).unwrap(),
207 PtyEvent::Spawned { terminal: id, .. } if id == terminal
208 ));
209
210 assert!(control.emit(PtyEvent::Output { terminal, generation, bytes: b"ok\r\n".to_vec() }));
211 assert!(matches!(
212 rx.recv_timeout(Duration::from_secs(1)).unwrap(),
213 PtyEvent::Output { terminal: id, bytes, .. }
214 if id == terminal && bytes == b"ok\r\n"
215 ));
216
217 assert!(worker.request(PtyRequest::Release { terminal, generation }));
218 assert_eq!(
219 control.recv_request(Duration::from_secs(1)),
220 Some(PtyRequest::Release { terminal, generation })
221 );
222 assert!(!control.emit(PtyEvent::Output {
223 terminal,
224 generation,
225 bytes: b"too late".to_vec(),
226 }));
227 }
228}