Files
kyleandClaude Opus 5.5 5f48ba65b0 M5b plan: task 13, instance lock in brokerd, memoryd and gatewayd
Each locks <home>/run/<role>.lock in main (gatewayd after its secret,
so nothing is made before the secret is read). brokerd's old
second-instance test in serve.rs expected the audit lock's message,
so serve.rs has a new version; task 01 now copies serve.rs-task01.

Checks: given tests fail at ref 12 as the task says (2 each in
brokerd, memoryd, gatewayd; serve.rs passes); the task's quoted edits
applied to ref 12 reproduce ref 13's tree exactly; replay gate ok with
1118 tests (1113 - 1 + 6), /tmp unchanged; 14 mutations, all caught;
the six instance test files stressed 25 runs under 16 busy loops and
20 under 32, no failure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 17:21:14 -07:00

224 lines
6.9 KiB
Rust

//! One `memoryd` per home (M5b spec, section 9a): a second `memoryd serve` against the same home is
//! refused with the lock's line, and the first keeps serving; once the first is killed, a new one
//! removes the stale sockets and serves. Every wait has a deadline, and every process is killed
//! and every path removed by a guard (tip T44). Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use bxcore::{Empty, Envelope, Message, PROTOCOL_VERSION, read_frame, write_frame};
use tmp::TempDir;
const POINTER: &str = "see docs/runbook.md#already-running";
const SOCKETS: [&str; 3] = [
"run/loop-memory/memory.sock",
"run/broker-memory/memory.sock",
"run/owner-memory/admin.sock",
];
struct Home {
dir: TempDir,
config: PathBuf,
}
impl Home {
fn new(tag: &str) -> Home {
let dir = TempDir::new(tag);
let soul = dir.write("etc/soul.md", "You are Boxmaker.\n");
let text = format!(
"[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n",
dir.path().display(),
soul.display()
);
let config = dir.write("memoryd.toml", &text);
Home { dir, config }
}
fn path(&self, relative: &str) -> PathBuf {
self.dir.path().join(relative)
}
fn command(&self) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_memoryd"));
command
.args(["serve", "--config"])
.arg(&self.config)
.stdin(Stdio::null())
.stdout(Stdio::null());
command
}
/// `memoryd serve` with its stderr in `<home>/<name>.err`, for the messages of a failing test.
fn start(&self, name: &str) -> Daemon {
let err = std::fs::File::create(self.path(&format!("{name}.err"))).unwrap();
Daemon(self.command().stderr(Stdio::from(err)).spawn().unwrap())
}
fn stderr_of(&self, name: &str) -> String {
std::fs::read_to_string(self.path(&format!("{name}.err"))).unwrap_or_default()
}
/// Runs `memoryd serve` to its end, at most 10 s (then it is killed), and gives its exit code
/// (`None` if it had to be killed) and its stderr.
fn run_to_end(&self) -> (Option<i32>, String) {
let mut child = Daemon(self.command().stderr(Stdio::piped()).spawn().unwrap());
let deadline = Instant::now() + Duration::from_secs(10);
let mut code = None;
while Instant::now() < deadline {
if let Ok(Some(status)) = child.0.try_wait() {
code = status.code();
break;
}
std::thread::sleep(Duration::from_millis(20));
}
let _ = child.0.kill();
let _ = child.0.wait();
let mut stderr = String::new();
if let Some(mut pipe) = child.0.stderr.take() {
let _ = std::io::Read::read_to_string(&mut pipe, &mut stderr);
}
(code, stderr)
}
fn inodes(&self) -> Vec<u64> {
SOCKETS.iter().map(|s| inode(&self.path(s))).collect()
}
}
/// A running `memoryd serve`, killed and waited for when dropped. Declared after the `Home`, so it
/// drops first.
struct Daemon(Child);
impl Drop for Daemon {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
impl Daemon {
fn running(&mut self) -> bool {
matches!(self.0.try_wait(), Ok(None))
}
}
/// True when `memoryd` answers on `socket`, `loopd`'s: `memory_block` gets the block. Any other
/// answer, or none within 5 s, is false.
fn answers(socket: &Path) -> bool {
let Ok(mut stream) = UnixStream::connect(socket) else {
return false;
};
let limit = Some(Duration::from_secs(5));
if stream.set_read_timeout(limit).is_err() || stream.set_write_timeout(limit).is_err() {
return false;
}
let ask = Envelope {
v: PROTOCOL_VERSION,
id: 1,
r#final: true,
msg: Message::MemoryBlock(Empty {}),
};
if write_frame(&mut stream, &ask).is_err() {
return false;
}
matches!(
read_frame(&mut stream),
Ok(Envelope {
msg: Message::MemoryBlockReply(_),
..
})
)
}
/// Waits up to 10 s for `memoryd` to answer on `loopd`'s socket and to listen on the other two.
fn wait_until_it_answers(home: &Home) -> bool {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if answers(&home.path(SOCKETS[0]))
&& SOCKETS[1..]
.iter()
.all(|s| UnixStream::connect(home.path(s)).is_ok())
{
return true;
}
std::thread::sleep(Duration::from_millis(20));
}
false
}
fn inode(path: &Path) -> u64 {
use std::os::unix::fs::MetadataExt;
std::fs::symlink_metadata(path)
.map(|m| m.ino())
.unwrap_or(0)
}
#[test]
fn a_second_memoryd_on_the_same_home_is_refused_and_the_first_keeps_serving() {
let home = Home::new("inst-twice");
let mut first = home.start("first");
assert!(
wait_until_it_answers(&home),
"the first memoryd never answered; stderr: {}",
home.stderr_of("first")
);
let before = home.inodes();
let (code, stderr) = home.run_to_end();
let line = format!(
"memoryd: another memoryd is running (it holds {}); {POINTER}",
home.path("run/memoryd.lock").display()
);
assert_eq!(code, Some(1), "the second memoryd: {stderr}");
assert_eq!(
stderr,
format!("{line}\n"),
"the lock's line, and nothing else"
);
assert!(first.running(), "the first memoryd is still running");
assert_eq!(home.inodes(), before, "the first's sockets are still there");
assert!(
answers(&home.path(SOCKETS[0])),
"the first memoryd still answers; its stderr: {}",
home.stderr_of("first")
);
}
#[test]
fn after_the_first_is_killed_a_new_memoryd_removes_the_stale_sockets_and_serves() {
let home = Home::new("inst-restart");
let mut first = home.start("first");
assert!(
wait_until_it_answers(&home),
"the first memoryd never answered; stderr: {}",
home.stderr_of("first")
);
let _ = first.0.kill();
let _ = first.0.wait();
assert!(
SOCKETS.iter().all(|s| home.path(s).exists()),
"a killed memoryd leaves its sockets"
);
assert!(
!answers(&home.path(SOCKETS[0])),
"nobody serves the stale socket"
);
let _next = home.start("next");
assert!(
wait_until_it_answers(&home),
"the new memoryd never answered; stderr: {}",
home.stderr_of("next")
);
let lock = home.path("run/memoryd.lock");
let mode = std::fs::metadata(&lock).map(|m| m.permissions().mode() & 0o777);
assert_eq!(mode.ok(), Some(0o600), "the lock file, mode 0600");
}