Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72e42a8c9 | ||
|
|
01a821ab72 |
+3
-1
@@ -132,7 +132,9 @@ the request: the request's `type` in past tense (`spawned`, `upgraded`,
|
||||
| `subscribe`, `unsubscribe` | `actor` | receive (or not) what `actor` publishes |
|
||||
| `tap`, `untap` | | receive (or not) a copy of every message the broker routes |
|
||||
| `watch`, `unwatch` | `path` | receive (or not) `fs_changed` for every change of `path` or anything under it; answer `{"type": "watching" \| "unwatched", "path"}` with the path as an `/actor/...` path |
|
||||
| `exit` | | answer `exiting`, stop every actor, and end the `sausage` process (a wrapper like `nix run .#loop` starts it again, updated) |
|
||||
| `exit` | | answer `exiting`, stop every actor, and end the `sausage` process with exit code 75 (a wrapper like `nix run .#loop` starts it again, updated) |
|
||||
|
||||
**Headless.** `sausage run --headless [dir]` (or `sausage --headless`, or `SAUSAGE_HEADLESS=1`) turns the console input off: the host does not read stdin, so end of input (`/dev/null`) does not stop the world. `nix run .#loop -- --headless <world>` passes it through the loop. It runs until SIGTERM or SIGINT (stops every actor, exit code 0) or a broker `exit` request (exit code 75, as above). Without the flag, EOF on stdin or `/quit` ends the host.
|
||||
|
||||
Paths are `/actor/...`, or the sender's `/public/...` or `/data/...`.
|
||||
|
||||
|
||||
@@ -48,6 +48,18 @@ From a clone (`git clone https://gitea.surma.technology/surma/sausage`), the
|
||||
same commands are `nix run`, `nix run -- init`, `nix run -- run` and
|
||||
`nix run .#loop`.
|
||||
|
||||
### Headless
|
||||
|
||||
`--headless` (or `SAUSAGE_HEADLESS=1`) turns the console input off: the host
|
||||
never reads stdin, so end of input does not stop the world. It runs until
|
||||
SIGTERM/SIGINT (clean stop, exit code 0) or a broker `exit` request (exit
|
||||
code 75, so the loop starts the newest version). Create the world first
|
||||
(`init`), then pass the flag through the loop:
|
||||
|
||||
```sh
|
||||
nix run git+https://gitea.surma.technology/surma/sausage#loop -- --headless my-world
|
||||
```
|
||||
|
||||
In the console:
|
||||
|
||||
```text
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
//! - `@<actor> <json>` sends a JSON body. `@<actor> <text>` sends
|
||||
//! `{"type": "text", "text": ...}`.
|
||||
//! - A line without `@` goes to `agent` as text.
|
||||
//! - `/help`, `/quit`. EOF on stdin also quits.
|
||||
//! - `/help`, `/quit`. EOF on stdin also quits (not with `--headless`, which
|
||||
//! never reads stdin).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
+41
-5
@@ -5,6 +5,7 @@ use anyhow::{Context, Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use include_dir::{Dir, include_dir};
|
||||
use serde_json::json;
|
||||
use tokio::signal::unix::Signal;
|
||||
|
||||
use sausage::world::{ActorMeta, World};
|
||||
use sausage::{Host, cargo_actor, console};
|
||||
@@ -17,6 +18,11 @@ static AGENT_SOURCE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../agent");
|
||||
#[derive(Parser)]
|
||||
#[command(version, about)]
|
||||
struct Cli {
|
||||
/// Do not read stdin, so end of input does not stop the world. It runs
|
||||
/// until SIGTERM (or SIGINT), or until an actor sends
|
||||
/// `@broker {"type":"exit"}`. Also set by SAUSAGE_HEADLESS=1.
|
||||
#[arg(long, global = true)]
|
||||
headless: bool,
|
||||
#[command(subcommand)]
|
||||
command: Option<Cmd>,
|
||||
}
|
||||
@@ -91,13 +97,41 @@ async fn init(dir: PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(dir: PathBuf) -> Result<()> {
|
||||
/// Install the SIGTERM and SIGINT handlers now (not when first polled).
|
||||
fn termination_signals() -> Result<(Signal, Signal)> {
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
let term = signal(SignalKind::terminate()).context("install SIGTERM handler")?;
|
||||
let int = signal(SignalKind::interrupt()).context("install SIGINT handler")?;
|
||||
Ok((term, int))
|
||||
}
|
||||
|
||||
/// Resolves on SIGTERM or SIGINT.
|
||||
async fn termination((mut term, mut int): (Signal, Signal)) {
|
||||
tokio::select! {
|
||||
_ = term.recv() => {}
|
||||
_ = int.recv() => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(dir: PathBuf, headless: bool) -> Result<()> {
|
||||
// Register the signal handlers before the host starts, so a signal that
|
||||
// arrives early is never the default (kill) action.
|
||||
let signals = if headless { Some(termination_signals()?) } else { None };
|
||||
let host = Host::start(&dir).await?;
|
||||
tokio::spawn(console::run_mailbox(host.mailbox(console::ID)));
|
||||
let broker = host.broker.clone();
|
||||
let exit = tokio::select! {
|
||||
let exit = match signals {
|
||||
Some(signals) => {
|
||||
println!("Sausage headless: running until SIGTERM or an exit request.");
|
||||
tokio::select! {
|
||||
_ = termination(signals) => false,
|
||||
_ = broker.exit_requested() => true,
|
||||
}
|
||||
}
|
||||
None => tokio::select! {
|
||||
_ = console::run_input(host.broker.clone()) => false,
|
||||
_ = broker.exit_requested() => true,
|
||||
},
|
||||
};
|
||||
println!("Stopping actors …");
|
||||
host.stop().await;
|
||||
@@ -113,16 +147,18 @@ async fn run(dir: PathBuf) -> Result<()> {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
match Cli::parse().command {
|
||||
let cli = Cli::parse();
|
||||
let headless = cli.headless || std::env::var("SAUSAGE_HEADLESS").is_ok_and(|v| !v.is_empty() && v != "0");
|
||||
match cli.command {
|
||||
Some(Cmd::Init { dir }) => init(dir).await,
|
||||
Some(Cmd::Run { dir }) => run(dir).await,
|
||||
Some(Cmd::Run { dir }) => run(dir, headless).await,
|
||||
None => {
|
||||
let dir = PathBuf::from("world");
|
||||
if !dir.join("actor").is_dir() {
|
||||
println!("No world in ./world yet, creating it (same as `sausage init`).");
|
||||
init(dir.clone()).await?;
|
||||
}
|
||||
run(dir).await
|
||||
run(dir, headless).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,3 +39,47 @@ fn bare_command_without_a_world_starts_init() {
|
||||
// It asks before it creates anything.
|
||||
assert!(!dir.join("world").exists());
|
||||
}
|
||||
|
||||
/// Start a headless host on a fresh world with stdin at EOF; returns once it
|
||||
/// printed its ready line (printed after the signal handlers are installed).
|
||||
fn start_headless(label: &str) -> (std::process::Child, std::io::Lines<std::io::BufReader<std::process::ChildStdout>>) {
|
||||
use std::io::{BufRead, BufReader};
|
||||
let dir = empty_dir(label);
|
||||
let world = dir.join("world");
|
||||
sausage::World::create(&world).unwrap();
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_sausage"))
|
||||
.args(["run", "--headless"])
|
||||
.arg(&world)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut lines = BufReader::new(child.stdout.take().unwrap()).lines();
|
||||
let line = lines.next().expect("host exited before it started").unwrap();
|
||||
assert!(line.contains("headless"), "unexpected first line: {line}");
|
||||
(child, lines)
|
||||
}
|
||||
|
||||
/// With stdin at EOF (as soon as the host starts), a headless host stays up.
|
||||
#[test]
|
||||
fn headless_ignores_stdin_eof() {
|
||||
let (mut child, _lines) = start_headless("headless-eof");
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
let alive = child.try_wait().unwrap().is_none();
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
assert!(alive, "headless host exited on stdin EOF");
|
||||
}
|
||||
|
||||
/// SIGTERM right after the ready line stops the actors and exits with 0.
|
||||
#[test]
|
||||
fn headless_stops_cleanly_on_sigterm_right_after_ready() {
|
||||
let (mut child, lines) = start_headless("headless-term");
|
||||
let pid = child.id().to_string();
|
||||
assert!(Command::new("kill").args(["-TERM", &pid]).status().unwrap().success());
|
||||
let rest: Vec<String> = lines.map(|l| l.unwrap()).collect();
|
||||
let status = child.wait().unwrap();
|
||||
assert!(status.success(), "exit status {status:?}, output {rest:?}");
|
||||
assert!(rest.iter().any(|l| l.contains("Stopping actors")), "output {rest:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user