239 lines
8.0 KiB
Rust
239 lines
8.0 KiB
Rust
//! `memoryd serve` with a `[honcho]` section (M5c spec, section 10): an invalid one stops it at
|
|
//! start with the reason and the `honcho-config-invalid` pointer, before it takes its lock or
|
|
//! touches its home; a valid one starts it as before. A key is never printed. 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::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#honcho-config-invalid";
|
|
const START_FAILED: &str = "see docs/runbook.md#memoryd-start-failed";
|
|
|
|
/// The values of the two keys the configs name. Built from pieces, so that no file holds one
|
|
/// whole; neither may appear in anything `memoryd` prints.
|
|
fn env_key() -> String {
|
|
["hk", "env", "7f3a9c"].join("-")
|
|
}
|
|
fn file_key() -> String {
|
|
["hk", "file", "b81d2e"].join("-")
|
|
}
|
|
|
|
const SECTION: &str = "[honcho]
|
|
url = \"http://127.0.0.1:9\"
|
|
agent = \"boxmaker0\"
|
|
owner = \"kyle\"
|
|
trusted = \"boxmaker0-trusted\"
|
|
untrusted = \"boxmaker0-untrusted\"
|
|
fleet = \"fleet\"
|
|
[[honcho.read]]
|
|
agent = \"boxmaker1\"
|
|
workspace = \"boxmaker1-trusted\"
|
|
";
|
|
|
|
struct Home {
|
|
dir: TempDir,
|
|
config: PathBuf,
|
|
}
|
|
|
|
impl Home {
|
|
/// A home whose `memoryd.toml` is the usual one and `extra`, with a key file holding
|
|
/// `file_key()` (mode 0600) at `<home>/keys/honcho.key`.
|
|
fn new(tag: &str, extra: &str) -> Home {
|
|
let dir = TempDir::new(tag);
|
|
let soul = dir.write("etc/soul.md", "You are Boxmaker.\n");
|
|
let key = dir.write("keys/honcho.key", &format!("{}\n", file_key()));
|
|
std::fs::set_permissions(&key, std::fs::Permissions::from_mode(0o600)).unwrap();
|
|
let text = format!(
|
|
"[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n{extra}",
|
|
dir.path().display(),
|
|
soul.display()
|
|
);
|
|
let text = text.replace("KEY_FILE", &key.display().to_string());
|
|
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)
|
|
.env("HONCHO_TEST_KEY", env_key())
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null());
|
|
command
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
/// `memoryd serve` with its stderr in `<home>/memoryd.err`.
|
|
fn start(&self) -> Daemon {
|
|
let err = std::fs::File::create(self.path("memoryd.err")).unwrap();
|
|
Daemon(self.command().stderr(Stdio::from(err)).spawn().unwrap())
|
|
}
|
|
|
|
fn stderr(&self) -> String {
|
|
std::fs::read_to_string(self.path("memoryd.err")).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
/// A running `memoryd serve`, killed and waited for when dropped. Each test declares it after its
|
|
/// `Home`, so it drops first.
|
|
struct Daemon(Child);
|
|
|
|
impl Drop for Daemon {
|
|
fn drop(&mut self) {
|
|
let _ = self.0.kill();
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
/// True once `memoryd` answers `memory_block` on `loopd`'s socket, within 10 s.
|
|
fn answers(home: &Home) -> bool {
|
|
let socket = home.path("run/loop-memory/memory.sock");
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
while Instant::now() < deadline {
|
|
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
|
let limit = Some(Duration::from_secs(5));
|
|
let ask = Envelope {
|
|
v: PROTOCOL_VERSION,
|
|
id: 1,
|
|
r#final: true,
|
|
msg: Message::MemoryBlock(Empty {}),
|
|
};
|
|
if stream.set_read_timeout(limit).is_ok()
|
|
&& write_frame(&mut stream, &ask).is_ok()
|
|
&& matches!(
|
|
read_frame(&mut stream),
|
|
Ok(Envelope {
|
|
msg: Message::MemoryBlockReply(_),
|
|
..
|
|
})
|
|
)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
false
|
|
}
|
|
|
|
fn assert_no_key(text: &str) {
|
|
assert!(
|
|
!text.contains(&env_key()),
|
|
"the env key was printed: {text}"
|
|
);
|
|
assert!(
|
|
!text.contains(&file_key()),
|
|
"the file key was printed: {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn an_invalid_section_stops_memoryd_with_its_reason_and_the_pointer() {
|
|
let extra = format!(
|
|
"{}[secrets.honcho_trusted]\nenv = \"HONCHO_TEST_KEY\"\n[secrets.honcho_fleet]\nfile = \"KEY_FILE\"\n",
|
|
SECTION.replace("owner = \"kyle\"", "owner = \"boxmaker0\"")
|
|
);
|
|
let home = Home::new("honcho-bad", &extra);
|
|
let (code, stderr) = home.run_to_end();
|
|
assert_eq!(code, Some(1), "{stderr}");
|
|
assert_eq!(
|
|
stderr,
|
|
format!(
|
|
"memoryd: {}: [honcho] agent and owner are both \"boxmaker0\": the agent's peer must not be the owner's\n{POINTER}\n",
|
|
home.config.display()
|
|
)
|
|
);
|
|
assert_no_key(&stderr);
|
|
assert!(
|
|
!home.path("run").exists(),
|
|
"memoryd stopped before its lock and sockets"
|
|
);
|
|
assert!(!home.path("memory").exists(), "and before its memory");
|
|
}
|
|
|
|
#[test]
|
|
fn a_key_for_an_agent_not_read_stops_memoryd() {
|
|
let extra = format!("{SECTION}[secrets.honcho_read_boxmaker2]\nenv = \"HONCHO_TEST_KEY\"\n");
|
|
let home = Home::new("honcho-key", &extra);
|
|
let (code, stderr) = home.run_to_end();
|
|
assert_eq!(code, Some(1), "{stderr}");
|
|
assert_eq!(
|
|
stderr,
|
|
format!(
|
|
"memoryd: {}: [secrets.honcho_read_boxmaker2] is the key of agent \"boxmaker2\", which is not in [[honcho.read]]\n{POINTER}\n",
|
|
home.config.display()
|
|
)
|
|
);
|
|
assert_no_key(&stderr);
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_key_in_the_section_is_still_a_start_failure() {
|
|
let extra = SECTION.replace("fleet = \"fleet\"", "fleet = \"fleet\"\nfleat = \"x\"");
|
|
let home = Home::new("honcho-unknown", &extra);
|
|
let (code, stderr) = home.run_to_end();
|
|
assert_eq!(code, Some(1), "{stderr}");
|
|
assert!(
|
|
stderr.starts_with(&format!("memoryd: {}: ", home.config.display())),
|
|
"{stderr}"
|
|
);
|
|
assert!(stderr.contains("fleat"), "{stderr}");
|
|
assert!(stderr.ends_with(&format!("\n{START_FAILED}\n")), "{stderr}");
|
|
assert!(!stderr.contains(POINTER), "{stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_valid_section_with_keys_starts_memoryd_as_before() {
|
|
let extra = format!(
|
|
"{SECTION}[secrets.honcho_trusted]\nenv = \"HONCHO_TEST_KEY\"\n[secrets.honcho_fleet]\nfile = \"KEY_FILE\"\n[secrets.honcho_read_boxmaker1]\nenv = \"HONCHO_TEST_KEY\"\n"
|
|
);
|
|
let home = Home::new("honcho-good", &extra);
|
|
let _daemon = home.start();
|
|
assert!(
|
|
answers(&home),
|
|
"memoryd never answered; stderr: {}",
|
|
home.stderr()
|
|
);
|
|
let stderr = home.stderr();
|
|
assert!(stderr.contains("memoryd: serving\n"), "{stderr}");
|
|
assert_no_key(&stderr);
|
|
assert!(!stderr.contains(POINTER), "{stderr}");
|
|
}
|