Make legacy core.exit share core.restart's supervisor gate and pending request. Seed the stable agent prompt and boot comments with the approval and shutdown contract, and check real init output under a deadline. Cover both aliases' gate, response grace, coalescing and exit status. Card: SAU-135
130 lines
4.6 KiB
Rust
130 lines
4.6 KiB
Rust
//! Supervisor-gated restart requests; the executable still owns normal shutdown.
|
|
use crate::{Host, RpcResult, error};
|
|
use serde_json::json;
|
|
use std::{ffi::OsStr, sync::atomic::Ordering, time::Duration};
|
|
|
|
pub(crate) fn enabled(value: Option<&OsStr>) -> bool {
|
|
value == Some(OsStr::new("1"))
|
|
}
|
|
|
|
impl Host {
|
|
pub(crate) fn request_restart(&self) -> RpcResult {
|
|
if !self.0.loop_supervised {
|
|
return Err(error(
|
|
-32600,
|
|
"restart needs the loop supervisor (SAUSAGE_LOOP not set); start with SAUSAGE_LOOP=1",
|
|
));
|
|
}
|
|
if !self.0.restart_pending.swap(true, Ordering::SeqCst) {
|
|
let host = self.clone();
|
|
tokio::spawn(async move {
|
|
// Let the RPC response reach the caller before closing any stdin,
|
|
// including that of a detached gateway carrying this response.
|
|
tokio::time::sleep(Duration::from_millis(250)).await;
|
|
host.0.exit.send_replace(true);
|
|
});
|
|
}
|
|
Ok(json!({}))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{runtime::Runtime, world::World};
|
|
use std::sync::Arc;
|
|
use tokio::time::timeout;
|
|
|
|
#[test]
|
|
fn only_explicit_loop_marker_enables_restart() {
|
|
assert!(!enabled(None));
|
|
for value in ["", "0", "true", "yes", "2"] {
|
|
assert!(!enabled(Some(OsStr::new(value))));
|
|
}
|
|
assert!(enabled(Some(OsStr::new("1"))));
|
|
}
|
|
|
|
async fn host(supervised: bool) -> (tempfile::TempDir, Host) {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
World::create(&dir.path().join("world")).unwrap();
|
|
let runtime = Runtime::new(&dir.path().join("cache")).unwrap();
|
|
let mut host = Host::test_world(&dir.path().join("world"), runtime)
|
|
.await
|
|
.unwrap();
|
|
// No global environment mutation: parallel tests have independent gates.
|
|
Arc::get_mut(&mut host.0).unwrap().loop_supervised = supervised;
|
|
(dir, host)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn restart_without_supervisor_never_triggers_exit() {
|
|
timeout(Duration::from_secs(10), async {
|
|
let (_dir, host) = host(false).await;
|
|
for method in ["core.restart", "core.exit"] {
|
|
let error = host.call(method, json!({})).await.unwrap_err();
|
|
assert_eq!(error["code"], -32600);
|
|
assert!(
|
|
error["message"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("SAUSAGE_LOOP not set")
|
|
);
|
|
let indirect = host
|
|
.call("core.call", json!({"method":method}))
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(indirect, error);
|
|
}
|
|
assert!(!host.0.restart_pending.load(Ordering::SeqCst));
|
|
assert!(
|
|
timeout(Duration::from_millis(300), host.exit_requested())
|
|
.await
|
|
.is_err()
|
|
);
|
|
host.stop().await;
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn restart_and_exit_share_response_grace_and_coalescing() {
|
|
timeout(Duration::from_secs(10), async {
|
|
for first in ["core.restart", "core.exit"] {
|
|
let (_dir, host) = host(true).await;
|
|
// Same dispatch as a plugin's core.call, without killing this runner.
|
|
assert_eq!(
|
|
host.call("core.call", json!({"method":first}))
|
|
.await
|
|
.unwrap(),
|
|
json!({})
|
|
);
|
|
assert!(!host.restart_requested());
|
|
assert!(host.0.restart_pending.load(Ordering::SeqCst));
|
|
assert_eq!(
|
|
host.call("core.restart", json!({})).await.unwrap(),
|
|
json!({})
|
|
);
|
|
timeout(Duration::from_secs(1), host.exit_requested())
|
|
.await
|
|
.unwrap();
|
|
// A duplicate after the trigger must not schedule a second task.
|
|
host.0.exit.send_replace(false);
|
|
assert_eq!(
|
|
host.call("core.restart", json!({})).await.unwrap(),
|
|
json!({})
|
|
);
|
|
assert_eq!(host.call("core.exit", json!({})).await.unwrap(), json!({}));
|
|
assert!(
|
|
timeout(Duration::from_millis(300), host.exit_requested())
|
|
.await
|
|
.is_err()
|
|
);
|
|
host.stop().await;
|
|
}
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|