1use rustyline::error::ReadlineError;
2use rustyline::DefaultEditor;
3use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
4use tokio::sync::mpsc::{Receiver, Sender};
5
6use crate::floor::host::Str;
7use crate::floor::{list, list_elements, pair, Val};
8use crate::world::{reply, request, Event};
9
10fn sym(s: &str) -> Val {
11 Val::Sym(s.into())
12}
13
14fn throw(why: Val) -> Val {
15 pair(sym("throw"), why)
16}
17
18fn text(v: &Val) -> String {
21 match Str::of(v) {
22 Some(s) => s.to_string(),
23 None => v.to_string(),
24 }
25}
26
27async fn answer(back: &Sender<Event>, msg: &Val, v: Val) -> bool {
30 match request(msg) {
31 Some((from, id, _)) => back.send(Event::Post(from, reply(id, v))).await.is_ok(),
32 None => true,
33 }
34}
35
36fn body(msg: &Val) -> Val {
38 match request(msg) {
39 Some((_, _, body)) => body,
40 None => msg.clone(),
41 }
42}
43
44pub async fn stdout(mut inbox: Receiver<Val>, back: Sender<Event>) {
46 let mut out = tokio::io::stdout();
47 while let Some(msg) = inbox.recv().await {
48 let v = match list_elements(&body(&msg)).as_deref() {
49 Some([verb, arg]) if *verb == sym("write") => put(&mut out, &text(arg)).await,
50 Some([verb, arg]) if *verb == sym("line") => {
51 put(&mut out, &format!("{}\n", text(arg))).await
52 }
53 _ => throw(pair(sym("bad-request"), body(&msg))),
54 };
55 if !answer(&back, &msg, v).await {
56 return;
57 }
58 }
59}
60
61async fn put(out: &mut tokio::io::Stdout, s: &str) -> Val {
62 match out.write_all(s.as_bytes()).await {
63 Ok(()) => match out.flush().await {
64 Ok(()) => sym("ok"),
65 Err(e) => throw(pair(sym("io"), Str::val(e.to_string()))),
66 },
67 Err(e) => throw(pair(sym("io"), Str::val(e.to_string()))),
68 }
69}
70
71pub async fn stdin(mut inbox: Receiver<Val>, back: Sender<Event>) {
75 let mut lines = BufReader::new(tokio::io::stdin()).lines();
76 while let Some(msg) = inbox.recv().await {
77 let Some((from, id, ask)) = request(&msg) else {
78 continue;
79 };
80 let v = match list_elements(&ask).as_deref() {
81 Some([verb] | [verb, _]) if *verb == sym("read") => match lines.next_line().await {
82 Ok(Some(s)) => Str::val(s),
83 Ok(None) => sym("eof"),
84 Err(e) => throw(pair(sym("io"), Str::val(e.to_string()))),
85 },
86 _ => throw(pair(sym("bad-request"), ask)),
87 };
88 if back.send(Event::Post(from, reply(id, v))).await.is_err() {
89 return;
90 }
91 }
92}
93
94pub async fn terminal(mut inbox: Receiver<Val>, back: Sender<Event>) {
103 let _ = tokio::task::spawn_blocking(move || {
104 let Ok(mut rl) = DefaultEditor::new() else {
105 return;
106 };
107 while let Some(msg) = inbox.blocking_recv() {
108 let v = match list_elements(&body(&msg)).as_deref() {
109 Some([verb, arg]) if *verb == sym("write") => emit(&text(arg)),
110 Some([verb, arg]) if *verb == sym("line") => emit(&format!("{}\n", text(arg))),
111 Some([verb] | [verb, _]) if *verb == sym("read") => {
112 let prompt = match list_elements(&body(&msg)).as_deref() {
113 Some([_, p]) => text(p),
114 _ => String::new(),
115 };
116 match rl.readline(&prompt) {
117 Ok(line) => {
118 rl.add_history_entry(line.as_str()).ok();
119 Str::val(line)
120 }
121 Err(ReadlineError::Interrupted) => Str::val(""),
123 Err(ReadlineError::Eof) => sym("eof"),
124 Err(e) => throw(pair(sym("io"), Str::val(e.to_string()))),
125 }
126 }
127 _ => throw(pair(sym("bad-request"), body(&msg))),
128 };
129 if let Some((from, id, _)) = request(&msg) {
130 if back.blocking_send(Event::Post(from, reply(id, v))).is_err() {
131 return;
132 }
133 }
134 }
135 })
136 .await;
137}
138
139pub async fn files(root: String, mut inbox: Receiver<Val>, back: Sender<Event>) {
148 while let Some(msg) = inbox.recv().await {
149 let Some((from, id, ask)) = request(&msg) else {
150 continue;
151 };
152 let v = match list_elements(&ask).as_deref() {
153 Some([verb, path]) if *verb == sym("read") => {
154 let name = text(path);
155 match tokio::fs::read_to_string(&name).await {
156 Ok(s) => Str::val(s),
157 Err(e) => throw(pair(sym("io"), Str::val(format!("{name}: {e}")))),
160 }
161 }
162 Some([verb]) if *verb == sym("root") => Str::val(root.clone()),
163 _ => throw(pair(sym("bad-request"), ask)),
164 };
165 if back.send(Event::Post(from, reply(id, v))).await.is_err() {
166 return;
167 }
168 }
169}
170
171pub async fn disk(mut inbox: Receiver<Val>, back: Sender<Event>) {
178 while let Some(msg) = inbox.recv().await {
179 let Some((from, id, ask)) = request(&msg) else {
180 continue;
181 };
182 let v = match list_elements(&ask).as_deref() {
183 Some([verb, path, body]) if *verb == sym("write") => {
184 let name = text(path);
185 match tokio::fs::write(&name, text(body)).await {
186 Ok(()) => sym("ok"),
187 Err(e) => throw(pair(sym("io"), Str::val(format!("{name}: {e}")))),
188 }
189 }
190 _ => throw(pair(sym("bad-request"), ask)),
191 };
192 if back.send(Event::Post(from, reply(id, v))).await.is_err() {
193 return;
194 }
195 }
196}
197
198fn store_path(url: &str) -> Result<&str, String> {
208 let rest = url
209 .strip_prefix("https://")
210 .ok_or_else(|| format!("{url}: want an https url"))?;
211 if rest.split('/').count() < 2 {
212 return Err(format!("{url}: want a host and a path"));
213 }
214 for part in rest.split('/') {
215 if part.is_empty() || part.starts_with('-') || part.starts_with('.') {
216 return Err(format!("{url}: bad url"));
217 }
218 if !part
219 .chars()
220 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
221 {
222 return Err(format!("{url}: bad url"));
223 }
224 }
225 Ok(rest)
226}
227
228fn rev(s: &str) -> Result<&str, String> {
231 if s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit()) {
232 Ok(s)
233 } else {
234 Err(format!("{s}: not an object name"))
235 }
236}
237
238async fn git(args: &[&str]) -> Result<String, String> {
242 let out = tokio::process::Command::new("git")
243 .args(args)
244 .output()
245 .await
246 .map_err(|e| format!("git: {e}"))?;
247 if out.status.success() {
248 String::from_utf8(out.stdout).map_err(|e| format!("git: {e}"))
249 } else {
250 Err(format!(
251 "git {}: {}",
252 args.join(" "),
253 String::from_utf8_lossy(&out.stderr).trim()
254 ))
255 }
256}
257
258pub async fn modules(root: String, mut inbox: Receiver<Val>, back: Sender<Event>) {
273 while let Some(msg) = inbox.recv().await {
274 let Some((from, id, ask)) = request(&msg) else {
275 continue;
276 };
277 let v = match list_elements(&ask).as_deref() {
278 Some([verb, url, want]) if *verb == sym("fetch") => {
279 match fetch(&root, &text(url), &text(want)).await {
280 Ok((at, where_)) => list(&[Str::val(at), Str::val(where_)]),
281 Err(why) => throw(pair(sym("fetch"), Str::val(why))),
282 }
283 }
284 _ => throw(pair(sym("bad-request"), ask)),
285 };
286 if back.send(Event::Post(from, reply(id, v))).await.is_err() {
287 return;
288 }
289 }
290}
291
292async fn fetch(root: &str, url: &str, want: &str) -> Result<(String, String), String> {
296 let path = store_path(url)?;
297 let at = match rev(want) {
298 Ok(known) => known.to_string(),
299 Err(_) => {
300 let line = git(&["ls-remote", url, want]).await?;
301 let head = line
302 .split_whitespace()
303 .next()
304 .ok_or(format!("{url}: no ref {want}"))?;
305 rev(head)?.to_string()
306 }
307 };
308
309 let dest = format!("{root}/{path}/{at}");
310 if tokio::fs::metadata(&dest).await.is_ok() {
311 return Ok((at, path.to_string()));
312 }
313
314 tokio::fs::create_dir_all(&dest)
315 .await
316 .map_err(|e| format!("{dest}: {e}"))?;
317 git(&["init", "-q", &dest]).await?;
318 git(&["-C", &dest, "fetch", "-q", "--depth", "1", url, &at]).await?;
319 git(&["-C", &dest, "checkout", "-q", "FETCH_HEAD"]).await?;
320 tokio::fs::remove_dir_all(format!("{dest}/.git"))
321 .await
322 .map_err(|e| format!("{dest}/.git: {e}"))?;
323 Ok((at, path.to_string()))
324}
325
326pub async fn clock(mut inbox: Receiver<Val>, back: Sender<Event>) {
342 while let Some(msg) = inbox.recv().await {
343 let ask = body(&msg);
344 if let Some([verb, Val::Num(ms)]) = list_elements(&ask).as_deref() {
347 if *verb == sym("sleep") {
348 if let Some((from, id, _)) = request(&msg) {
349 let (ms, port) = (*ms.max(&0) as u64, back.clone());
350 tokio::spawn(async move {
351 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
352 let _ = port.send(Event::Post(from, reply(id, sym("ok")))).await;
353 });
354 }
355 continue;
356 }
357 }
358 let v = match list_elements(&ask).as_deref() {
359 Some([verb]) if *verb == sym("now") => Val::Num(now_ms()),
360 Some([verb, Val::Num(ms), to, note]) if *verb == sym("after") => {
361 let (ms, to, note) = (*ms.max(&0) as u64, to.clone(), note.clone());
362 let port = back.clone();
363 tokio::spawn(async move {
364 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
365 let _ = port.send(Event::Post(to, note)).await;
366 });
367 sym("ok")
368 }
369 _ => throw(pair(sym("bad-request"), ask)),
370 };
371 if !answer(&back, &msg, v).await {
372 return;
373 }
374 }
375}
376
377fn now_ms() -> i64 {
378 std::time::SystemTime::now()
379 .duration_since(std::time::UNIX_EPOCH)
380 .map(|d| d.as_millis() as i64)
381 .unwrap_or(0)
382}
383
384fn emit(s: &str) -> Val {
385 use std::io::Write;
386 let mut out = std::io::stdout();
387 match out.write_all(s.as_bytes()).and_then(|()| out.flush()) {
388 Ok(()) => sym("ok"),
389 Err(e) => throw(pair(sym("io"), Str::val(e.to_string()))),
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::floor::list;
397
398 #[test]
399 fn a_url_becomes_its_place_in_the_store() {
400 assert_eq!(
401 store_path("https://github.com/octocat/Hello-World"),
402 Ok("github.com/octocat/Hello-World")
403 );
404 assert_eq!(
405 store_path("https://git.avery.garden/thorn/narju"),
406 Ok("git.avery.garden/thorn/narju")
407 );
408 }
409
410 #[test]
411 fn a_url_that_would_leave_the_store_is_refused() {
412 for bad in [
413 "https://forge/../etc/passwd",
414 "https://forge/-x/y",
415 "https://forge/a/.ssh",
416 "http://forge/a/b",
417 "git@github.com:a/b",
418 "file:///etc",
419 "https://forge",
420 "https://forge//b",
421 ] {
422 assert!(store_path(bad).is_err(), "{bad}");
423 }
424 }
425 fn clocked() -> (Sender<Val>, Receiver<Event>) {
426 let (to, inbox) = tokio::sync::mpsc::channel(8);
427 let (back, out) = tokio::sync::mpsc::channel(8);
428 tokio::spawn(clock(inbox, back));
429 (to, out)
430 }
431
432 fn ask(id: i64, body: Val) -> Val {
433 list(&[sym("req"), sym("caller"), Val::Num(id), body])
434 }
435
436 async fn posted(out: &mut Receiver<Event>) -> (Val, Val) {
437 match out.recv().await {
438 Some(Event::Post(to, msg)) => (to, msg),
439 _ => panic!("nothing was posted"),
440 }
441 }
442
443 #[tokio::test]
444 async fn the_clock_says_what_time_it_is() {
445 let (to, mut out) = clocked();
446 to.send(ask(1, list(&[sym("now")]))).await.expect("sent");
447 let (_, answer) = posted(&mut out).await;
448 match list_elements(&answer).as_deref() {
449 Some([tag, id, Val::Num(ms)]) if *tag == sym("reply") => {
450 assert_eq!(*id, Val::Num(1));
451 assert!(*ms > 1_700_000_000_000, "{ms} is not a time");
452 }
453 _ => panic!("not a reply: {answer}"),
454 }
455 }
456
457 #[tokio::test]
458 async fn a_timer_posts_the_message_it_was_given_where_it_was_told() {
459 let (to, mut out) = clocked();
460 let note = list(&[
461 sym("reply"),
462 Val::Num(7),
463 pair(sym("throw"), sym("timeout")),
464 ]);
465 to.send(list(&[
466 sym("after"),
467 Val::Num(1),
468 sym("elsewhere"),
469 note.clone(),
470 ]))
471 .await
472 .expect("sent");
473 assert_eq!(posted(&mut out).await, (sym("elsewhere"), note));
474 }
475
476 #[tokio::test]
477 async fn a_sleep_does_not_stop_the_clock_answering_anyone_else() {
478 let (to, mut out) = clocked();
479 to.send(ask(1, list(&[sym("sleep"), Val::Num(80)])))
480 .await
481 .expect("sent");
482 to.send(ask(2, list(&[sym("sleep"), Val::Num(1)])))
483 .await
484 .expect("sent");
485 assert_eq!(
486 posted(&mut out).await,
487 (sym("caller"), reply(Val::Num(2), sym("ok")))
488 );
489 assert_eq!(
490 posted(&mut out).await,
491 (sym("caller"), reply(Val::Num(1), sym("ok")))
492 );
493 }
494
495 #[tokio::test]
496 async fn a_verb_the_clock_does_not_know_is_refused() {
497 let (to, mut out) = clocked();
498 to.send(ask(1, list(&[sym("nonsense")])))
499 .await
500 .expect("sent");
501 assert_eq!(
502 posted(&mut out).await,
503 (
504 sym("caller"),
505 reply(
506 Val::Num(1),
507 throw(pair(sym("bad-request"), list(&[sym("nonsense")])))
508 )
509 )
510 );
511 }
512
513 #[tokio::test]
514 async fn a_call_that_runs_out_of_time_raises_and_leaves_the_loop_usable() {
515 let mut t = crate::tower::Tower::load().expect("loads");
516 let mut entry = |src: &str| {
517 let forms = crate::floor::read::read(src).expect("reads");
518 t.task(&crate::surface::desugar_body(&forms).expect("desugars"))
519 .expect("no fault")
520 };
521
522 let driver = entry(
523 "(task-loop self
524 (lambda h (st msg)
525 (let ((deaf (spawn (lambda (me)
526 (task-loop me
527 (lambda h2 (s2 m2)
528 (if (eq? s2 'asked) (cons 'stop 'ok) 'asked))
529 0)))))
530 (let ((late (attempt (lambda () (call-within 20 deaf '(hello))))))
531 (let ((now (call-within 10000 '(host clock) '(now))))
532 (begin (send deaf 'bye)
533 (cons 'stop (list late now)))))))
534 0)",
535 );
536
537 let mut w = crate::world::World::new(50_000_000);
538 let d = w.heap().spawn(driver);
539 w.heap().post(&crate::sched::addr(d), sym("go"));
540
541 let (tx, rx) = tokio::sync::mpsc::channel(8);
542 w.serve("clock", tx);
543 tokio::spawn(clock(rx, w.port()));
544
545 let ended = w.run().await.expect("no bug");
546 let Some((id, Val::Pair(tag, answered))) = ended.iter().find(|(id, _)| *id == d) else {
547 panic!("the driver did not end: {ended:?}")
548 };
549 assert_eq!((*id, &**tag), (d, &sym("ok")));
550 match list_elements(answered).as_deref() {
551 Some([late, Val::Num(_)]) => {
552 assert_eq!(*late, pair(sym("throw"), sym("timeout")));
553 }
554 _ => panic!("not what the driver answered: {answered}"),
555 }
556 }
557
558 fn written() -> (Sender<Val>, Receiver<Event>) {
559 let (to, inbox) = tokio::sync::mpsc::channel(8);
560 let (back, out) = tokio::sync::mpsc::channel(8);
561 tokio::spawn(disk(inbox, back));
562 (to, out)
563 }
564
565 #[tokio::test]
566 async fn a_write_puts_the_text_where_it_was_told() {
567 let path = std::env::temp_dir().join("narju-disk-writes.txt");
568 let (to, mut out) = written();
569 to.send(ask(
570 1,
571 list(&[
572 sym("write"),
573 Str::val(path.to_string_lossy().into_owned()),
574 Str::val("hi\n"),
575 ]),
576 ))
577 .await
578 .expect("sent");
579 let (_, answer) = posted(&mut out).await;
580 assert_eq!(answer, list(&[sym("reply"), Val::Num(1), sym("ok")]));
581 assert_eq!(std::fs::read_to_string(&path).expect("written"), "hi\n");
582 std::fs::remove_file(&path).expect("removed");
583 }
584
585 #[tokio::test]
586 async fn a_write_makes_no_directory_to_write_into() {
587 let dir = std::env::temp_dir().join("narju-disk-absent");
588 let (to, mut out) = written();
589 to.send(ask(
590 2,
591 list(&[
592 sym("write"),
593 Str::val(dir.join("f.txt").to_string_lossy().into_owned()),
594 Str::val(""),
595 ]),
596 ))
597 .await
598 .expect("sent");
599 let (_, answer) = posted(&mut out).await;
600 match list_elements(&answer).as_deref() {
601 Some([_, _, Val::Pair(tag, why)]) => {
602 assert_eq!(**tag, sym("throw"));
603 assert!(
604 matches!(&**why, Val::Pair(t, _) if **t == sym("io")),
605 "{why}"
606 );
607 }
608 _ => panic!("not a refusal: {answer}"),
609 }
610 assert!(!dir.exists(), "the directory was made");
611 }
612}