godspeed/program.rs
1// SPDX-License-Identifier: Apache-2.0
2//! A program that does one job and ends: a `utility`.
3//!
4//! A service runs until it is stopped. A utility is started by the shell for one command line, is handed
5//! its arguments, writes its output, and ends with a status - and the shell shows that output, pipes it
6//! onward, and reports the status. This module is the whole of what that needs:
7//!
8//! ```ignore
9//! use godspeed::{self as gs, ServiceContext};
10//!
11//! #[allow(unsafe_code)]
12//! #[no_mangle]
13//! pub extern "C" fn service_main(ctx: ServiceContext) -> ! {
14//! let mut buf = [0u8; gs::program::ARGS_MAX];
15//! let args = gs::program::args(&ctx, &mut buf);
16//! let mut out = gs::program::Output::new(&ctx);
17//! let _ = core::fmt::Write::write_fmt(&mut out, format_args!("hello, {}\n", if args.is_empty() { "world" } else { args }));
18//! out.finish(0)
19//! }
20//! ```
21//!
22//! **How it reaches the shell.** The shell starts an installed utility through the supervisor wired to send
23//! to the shell (its first peer), sends the command line's arguments as one message and then the end byte,
24//! and reads everything the program sends until the end byte - the same protocol every pipe stage speaks
25//! (`docs/pipes.md`), which is why a utility's output can be piped like any built-in's. [`Output::finish`]
26//! sends the end byte and then ends the program with its status (the `Exit` syscall, CLAUDE.md 14.5
27//! amendment), so the shell can say whether it worked.
28
29use core::fmt;
30
31use crate::ipc::{self, Message};
32use crate::ServiceContext;
33
34/// The end of a stream: the byte every pipe stage sends last.
35pub const EOT: u8 = 0x04;
36/// The longest argument line a program is handed: one message.
37pub const ARGS_MAX: usize = 4096;
38/// How long a program waits for its arguments before it takes the line as empty. The shell sends them as soon
39/// as the program has its endpoint, so this is only reached when nothing started it with any.
40pub const ARGS_SECS: i64 = 10;
41
42/// The mode this program was started in: a number its spawn request carried, 0 for every ordinary start.
43/// A program that does more than one thing - `observe` once or live, `probe` one test or another - reads
44/// which here. What a mode means is the program's own.
45#[inline(always)]
46pub fn mode(ctx: &ServiceContext) -> u32 {
47 ctx.probe_mode()
48}
49
50/// The arguments this program was started with, as one line - or empty when it was given none.
51///
52/// The first message on this program's endpoint, read once; the end byte that follows it is taken too.
53pub fn args<'b>(ctx: &ServiceContext, buf: &'b mut [u8; ARGS_MAX]) -> &'b str {
54 let Some(m) = ipc::recv_within(ctx, ARGS_SECS) else { return "" };
55 let p = m.payload_bytes();
56 if p == [EOT] { return ""; }
57 let n = p.len().min(buf.len());
58 buf[..n].copy_from_slice(&p[..n]);
59 // Its end byte, so it is not read later as input.
60 if let Some(e) = ipc::recv_within(ctx, 1) {
61 if e.payload_bytes() != [EOT] {
62 // Not the end byte: there was more than one message of arguments, which this does not take.
63 ctx.log("program: more arguments than one message - only the first was taken");
64 }
65 }
66 let line = core::str::from_utf8(&buf[..n]).unwrap_or("");
67 line.trim()
68}
69
70/// What a program writes, sent to the shell a message at a time.
71///
72/// Implements [`fmt::Write`], so `write!` works. Bounded: one message is buffered and sent when full, so a
73/// program can write any amount without holding it all.
74pub struct Output<'c> {
75 ctx: &'c ServiceContext,
76 buf: [u8; ARGS_MAX],
77 n: usize,
78 /// Whether the shell stopped taking it: a send failed, so later writes are dropped and [`finish`] reports
79 /// a failure (Output::finish) rather than claiming the output arrived.
80 lost: bool,
81}
82
83impl<'c> Output<'c> {
84 /// Output to whoever started this program: its first peer, the shell.
85 pub fn new(ctx: &'c ServiceContext) -> Output<'c> {
86 Output { ctx, buf: [0; ARGS_MAX], n: 0, lost: false }
87 }
88
89 /// Send what is buffered now.
90 pub fn flush(&mut self) {
91 if self.n == 0 || self.lost { self.n = 0; return; }
92 let sent = ipc::peer_at(self.ctx, 0)
93 .map(|down| ipc::send_to(self.ctx, down, &Message::from_bytes(&self.buf[..self.n])).is_ok())
94 .unwrap_or(false);
95 if !sent { self.lost = true; }
96 self.n = 0;
97 }
98
99 /// Send the rest and the end byte, then end the program with `status` - 0 for success. A status of 0
100 /// becomes 1 when the output could not be delivered, so the shell is never told it worked when it did not.
101 pub fn finish(mut self, status: u32) -> ! {
102 self.flush();
103 let ended = ipc::peer_at(self.ctx, 0)
104 .map(|down| ipc::send_to(self.ctx, down, &Message::from_bytes(&[EOT])).is_ok())
105 .unwrap_or(false);
106 let status = if status == 0 && (self.lost || !ended) { 1 } else { status };
107 exit(self.ctx, status)
108 }
109}
110
111impl fmt::Write for Output<'_> {
112 fn write_str(&mut self, s: &str) -> fmt::Result {
113 let mut b = s.as_bytes();
114 while !b.is_empty() {
115 if self.n == self.buf.len() { self.flush(); }
116 let take = b.len().min(self.buf.len() - self.n);
117 self.buf[self.n..self.n + take].copy_from_slice(&b[..take]);
118 self.n += take;
119 b = &b[take..];
120 }
121 Ok(())
122 }
123}
124
125/// End this program now, with `status`. A program that writes output ends through [`Output::finish`] instead,
126/// so the shell receives the end of its output first.
127pub fn exit(ctx: &ServiceContext, status: u32) -> ! {
128 ctx.exit(status)
129}