1use std::cell::RefCell;
24use std::rc::Rc;
25use std::sync::Arc;
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
28use std::time::{Duration, Instant};
29
30use rhai::{Array, Dynamic, Engine, EvalAltResult};
31
32const WAIT: f64 = 30.0;
34const KEEP: usize = 16 * 1024;
36
37pub enum ToScript {
39 Data(Vec<u8>),
41 Screen(String),
43}
44
45#[derive(Debug, PartialEq, Eq)]
47pub enum FromScript {
48 Send(String),
50 Notify(String),
51 WantScreen,
53 HangUp,
54 Done(Result<(), String>),
56}
57
58#[derive(Clone, Debug, Default)]
60pub struct CallInfo {
61 pub bbs: String,
62 pub user: String,
63 pub password: String,
64}
65
66pub struct Script {
68 pub name: String,
69 tx: Sender<ToScript>,
70 rx: Receiver<FromScript>,
71 stop: Arc<AtomicBool>,
72 done: bool,
73}
74
75impl Script {
76 pub fn start(name: &str, source: String, info: CallInfo, wake: impl Fn() + Send + 'static) -> Script {
79 let (tx, script_rx) = mpsc::channel();
80 let (script_tx, rx) = mpsc::channel();
81 let stop = Arc::new(AtomicBool::new(false));
82 let host = Host {
83 rx: script_rx,
84 tx: script_tx,
85 wake: Box::new(wake),
86 stop: stop.clone(),
87 text: String::new(),
88 esc: Esc::None,
89 info,
90 };
91 std::thread::Builder::new().name("script".into()).spawn(move || run(source, host)).expect("starting a thread");
92 Script { name: name.to_string(), tx, rx, stop, done: false }
93 }
94
95 pub fn feed(&self, data: &[u8]) {
97 if !data.is_empty() {
98 let _ = self.tx.send(ToScript::Data(data.to_vec()));
99 }
100 }
101
102 pub fn screen(&self, text: String) {
103 let _ = self.tx.send(ToScript::Screen(text));
104 }
105
106 pub fn stop(&self) {
108 self.stop.store(true, Ordering::Relaxed);
109 }
110
111 pub fn poll(&mut self) -> Vec<FromScript> {
113 let out: Vec<FromScript> = self.rx.try_iter().collect();
114 if out.iter().any(|m| matches!(m, FromScript::Done(_))) {
115 self.done = true;
116 }
117 out
118 }
119
120 pub fn is_done(&self) -> bool {
121 self.done
122 }
123}
124
125impl Drop for Script {
126 fn drop(&mut self) {
127 self.stop();
128 }
129}
130
131#[derive(Clone, Copy)]
133enum Esc {
134 None,
135 Start,
137 Csi,
139 Str,
141 StrEnd,
142}
143
144struct Host {
145 rx: Receiver<ToScript>,
146 tx: Sender<FromScript>,
147 wake: Box<dyn Fn() + Send>,
148 stop: Arc<AtomicBool>,
149 text: String,
151 esc: Esc,
152 info: CallInfo,
153}
154
155type Res<T> = Result<T, Box<EvalAltResult>>;
156
157impl Host {
158 fn ask(&self, m: FromScript) {
159 let _ = self.tx.send(m);
160 (self.wake)();
161 }
162
163 fn stopped(&self) -> Res<()> {
164 if self.stop.load(Ordering::Relaxed) { Err("stopped".into()) } else { Ok(()) }
165 }
166
167 fn take(&mut self, data: &[u8]) {
170 for &b in data {
171 self.esc = match (self.esc, b) {
172 (Esc::None, 0x1B) => Esc::Start,
173 (Esc::None, b'\r' | b'\n' | b'\t') => {
174 self.text.push(b as char);
175 Esc::None
176 }
177 (Esc::None, 0..=0x1F | 0x7F) => Esc::None,
178 (Esc::None, _) => {
179 self.text.push(crate::cp437::to_unicode(b));
180 Esc::None
181 }
182 (Esc::Start, b'[') => Esc::Csi,
183 (Esc::Start, b']' | b'_' | b'P' | b'^') => Esc::Str,
184 (Esc::Start, _) => Esc::None,
185 (Esc::Csi, 0x40..=0x7E) => Esc::None,
186 (Esc::Csi, _) => Esc::Csi,
187 (Esc::Str, 0x07) => Esc::None,
188 (Esc::Str, 0x1B) => Esc::StrEnd,
189 (Esc::Str, _) => Esc::Str,
190 (Esc::StrEnd, b'\\') => Esc::None,
191 (Esc::StrEnd, _) => Esc::Str,
192 };
193 }
194 if self.text.len() > KEEP {
195 let mut cut = self.text.len() - KEEP / 2;
196 while !self.text.is_char_boundary(cut) {
197 cut += 1;
198 }
199 self.text.drain(..cut);
200 }
201 }
202
203 fn next(&mut self, until: Instant) -> Res<Option<String>> {
206 loop {
207 self.stopped()?;
208 let left = until.saturating_duration_since(Instant::now());
209 if left.is_zero() {
210 return Ok(None);
211 }
212 match self.rx.recv_timeout(left.min(Duration::from_millis(100))) {
213 Ok(ToScript::Data(d)) => {
214 self.take(&d);
215 return Ok(Some(String::new()));
216 }
217 Ok(ToScript::Screen(s)) => return Ok(Some(s)),
218 Err(RecvTimeoutError::Timeout) => {}
219 Err(RecvTimeoutError::Disconnected) => return Err("the call ended".into()),
220 }
221 }
222 }
223
224 fn wait_any(&mut self, texts: &[String], secs: f64) -> Res<i64> {
226 let until = Instant::now() + Duration::from_secs_f64(secs.clamp(0.0, 86_400.0));
227 loop {
228 let found = texts
229 .iter()
230 .enumerate()
231 .filter(|(_, t)| !t.is_empty())
232 .filter_map(|(i, t)| self.text.find(t.as_str()).map(|at| (at, i, at + t.len())))
233 .min();
234 if let Some((_, i, end)) = found {
235 self.text.drain(..end);
236 return Ok(i as i64);
237 }
238 if self.next(until)?.is_none() {
239 return Ok(-1);
240 }
241 }
242 }
243
244 fn screen(&mut self) -> Res<String> {
245 self.ask(FromScript::WantScreen);
246 let until = Instant::now() + Duration::from_secs(5);
247 loop {
248 match self.next(until)? {
249 None => return Ok(String::new()),
250 Some(s) if !s.is_empty() => return Ok(s),
251 Some(_) => {}
252 }
253 }
254 }
255
256 fn sleep(&mut self, secs: f64) -> Res<()> {
257 let until = Instant::now() + Duration::from_secs_f64(secs.clamp(0.0, 86_400.0));
258 while self.next(until)?.is_some() {}
259 Ok(())
260 }
261}
262
263fn run(source: String, host: Host) {
264 let tx = host.tx.clone();
265 let host = Rc::new(RefCell::new(host));
266 let result = engine(&host).run(&source).map_err(|e| match *e {
267 EvalAltResult::ErrorTerminated(..) => "stopped".to_string(),
268 EvalAltResult::ErrorRuntime(ref v, _) if v.to_string() == "stopped" => "stopped".to_string(),
269 _ => e.to_string(),
270 });
271 let result = match result {
272 Err(e) if e == "stopped" => Ok(()),
273 other => other,
274 };
275 let h = host.borrow();
276 let _ = tx.send(FromScript::Done(result));
277 (h.wake)();
278}
279
280fn engine(host: &Rc<RefCell<Host>>) -> Engine {
281 let mut e = Engine::new();
282 let h = host.clone();
284 e.on_progress(move |_| h.borrow().stop.load(Ordering::Relaxed).then_some(Dynamic::UNIT));
285 let h = host.clone();
286 e.on_print(move |s| h.borrow().ask(FromScript::Notify(s.to_string())));
287 let h = host.clone();
288 e.register_fn("send", move |s: &str| -> Res<()> {
289 let h = h.borrow();
290 h.stopped()?;
291 h.ask(FromScript::Send(s.to_string()));
292 Ok(())
293 });
294 let h = host.clone();
295 e.register_fn("notify", move |s: &str| h.borrow().ask(FromScript::Notify(s.to_string())));
296 let h = host.clone();
297 e.register_fn("hangup", move || h.borrow().ask(FromScript::HangUp));
298 let h = host.clone();
299 e.register_fn("wait", move |s: &str, secs: f64| -> Res<bool> {
300 Ok(h.borrow_mut().wait_any(&[s.to_string()], secs)? == 0)
301 });
302 let h = host.clone();
303 e.register_fn("wait", move |s: &str, secs: i64| -> Res<bool> {
304 Ok(h.borrow_mut().wait_any(&[s.to_string()], secs as f64)? == 0)
305 });
306 let h = host.clone();
307 e.register_fn("wait", move |s: &str| -> Res<bool> { Ok(h.borrow_mut().wait_any(&[s.to_string()], WAIT)? == 0) });
308 let texts = |a: Array| a.into_iter().map(|d| d.to_string()).collect::<Vec<_>>();
309 let h = host.clone();
310 e.register_fn("wait_any", move |a: Array, secs: f64| -> Res<i64> { h.borrow_mut().wait_any(&texts(a), secs) });
311 let h = host.clone();
312 e.register_fn("wait_any", move |a: Array, secs: i64| -> Res<i64> {
313 h.borrow_mut().wait_any(&texts(a), secs as f64)
314 });
315 let h = host.clone();
316 e.register_fn("wait_any", move |a: Array| -> Res<i64> { h.borrow_mut().wait_any(&texts(a), WAIT) });
317 let h = host.clone();
318 e.register_fn("sleep", move |secs: f64| -> Res<()> { h.borrow_mut().sleep(secs) });
319 let h = host.clone();
320 e.register_fn("sleep", move |secs: i64| -> Res<()> { h.borrow_mut().sleep(secs as f64) });
321 let h = host.clone();
322 e.register_fn("screen", move || -> Res<String> { h.borrow_mut().screen() });
323 let h = host.clone();
324 e.register_fn("user", move || h.borrow().info.user.clone());
325 let h = host.clone();
326 e.register_fn("password", move || h.borrow().info.password.clone());
327 let h = host.clone();
328 e.register_fn("bbs", move || h.borrow().info.bbs.clone());
329 e
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 fn talk(source: &str, greeting: &[u8], replies: &[(&str, &[u8])]) -> (Vec<FromScript>, Result<(), String>) {
339 let info = CallInfo { bbs: "Test BBS".into(), user: "sysop".into(), password: "secret".into() };
340 let mut s = Script::start("test", source.to_string(), info, || {});
341 s.feed(greeting);
342 let mut seen = Vec::new();
343 let until = Instant::now() + Duration::from_secs(10);
344 while Instant::now() < until {
345 for m in s.poll() {
346 match m {
347 FromScript::Done(r) => return (seen, r),
348 FromScript::Send(ref t) => {
349 if let Some((_, r)) = replies.iter().find(|(k, _)| k == t) {
350 s.feed(r);
351 }
352 seen.push(m);
353 }
354 FromScript::WantScreen => s.screen("Row one\nRow two".into()),
355 m => seen.push(m),
356 }
357 }
358 std::thread::sleep(Duration::from_millis(5));
359 }
360 panic!("the script did not finish");
361 }
362
363 #[test]
364 fn logs_in_through_ansi() {
365 let script = r#"
366 if wait("Login:", 5) { send(user() + "\r"); }
367 wait("Password:");
368 send(password() + "\r");
369 let i = wait_any(["Bad password", "Main Menu"], 5);
370 if i == 1 { notify("in " + bbs()); }
371 if !wait("never", 0.2) { print("timed out"); }
372 if screen().contains("Row two") { send("ok"); }
373 "#;
374 let (seen, done) = talk(
375 script,
376 b"\x1b[2J\x1b[1;33mWelcome\r\nLogin: ",
377 &[("sysop\r", b"\x1b[0mPass\x1b[1mword: "), ("secret\r", b"\x1b]0;title\x07\r\n\x1b[32mMain Menu\r\n")],
378 );
379 assert_eq!(done, Ok(()));
380 assert_eq!(
381 seen,
382 [
383 FromScript::Send("sysop\r".into()),
384 FromScript::Send("secret\r".into()),
385 FromScript::Notify("in Test BBS".into()),
386 FromScript::Notify("timed out".into()),
387 FromScript::Send("ok".into()),
388 ]
389 );
390 }
391
392 #[test]
393 fn the_example_login_script() {
394 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../extras/scripts/login.rhai");
395 let Ok(source) = std::fs::read_to_string(path) else { return };
396 let (seen, done) = talk(
397 &source,
398 b"Welcome! Press any key to continue\r\n",
399 &[
400 ("\r", b"Login: "),
401 ("sysop\r", b"Password: "),
402 ("secret\r", b"Read new mail? [Y/n] "),
403 ("n", b"\x1b[1mMain Menu\x1b[0m\r\nCommand: "),
404 ],
405 );
406 assert_eq!(done, Ok(()));
407 assert_eq!(seen.last(), Some(&FromScript::Notify("Logged in to Test BBS".into())));
408 assert_eq!(seen.len(), 5, "{seen:?}");
409 }
410
411 #[test]
412 fn errors_and_stopping() {
413 let (_, done) = talk("let x = ;", b"", &[]);
414 assert!(done.is_err());
415 let (_, done) = talk("wait(\"x\", 0.1); hangup(); throw \"no login\";", b"", &[]);
416 assert!(done.unwrap_err().contains("no login"));
417 let mut s = Script::start("loop", "loop { }".into(), CallInfo::default(), || {});
419 std::thread::sleep(Duration::from_millis(50));
420 s.stop();
421 let until = Instant::now() + Duration::from_secs(5);
422 while !s.is_done() && Instant::now() < until {
423 s.poll();
424 std::thread::sleep(Duration::from_millis(5));
425 }
426 assert!(s.is_done());
427 }
428}