//! One `gatewayd` per home (M5b spec, section 9a): a second `gatewayd serve` against the same home //! is refused with the lock's line, and the first keeps serving `gateway.sock`; once the first is //! killed, a new one removes the stale socket and serves. Each runs against the fake Mattermost. //! Every wait has a deadline, and every process is killed and every path removed by a guard (tip //! T44). Do not edit. #[path = "support/brokerd_client.rs"] mod brokerd_client; #[path = "support/fake_mm.rs"] mod fake_mm; #[path = "support/tmp.rs"] mod tmp; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; use brokerd_client::{ask, pending}; use fake_mm::{FakeMm, KYLE}; use tmp::TempDir; const POINTER: &str = "see docs/runbook.md#already-running"; const TOKEN: &str = "tok-instance-test"; const WAIT: Duration = Duration::from_secs(10); struct Home { dir: TempDir, config: PathBuf, } impl Home { fn new(tag: &str, mm: &FakeMm) -> Home { let dir = TempDir::new(tag); let text = format!( "[mattermost]\nurl = \"{}\"\n[secrets.mattermost_token]\nenv = \"GW_TEST_TOKEN\"\n\ [allow]\nusers = [\"{KYLE}\"]\n[paths]\nhome = \"{}\"\n", mm.url(), dir.path().display() ); let config = dir.write("gatewayd.toml", &text); Home { dir, config } } fn path(&self, relative: &str) -> PathBuf { self.dir.path().join(relative) } fn socket(&self) -> PathBuf { self.path("run/gateway/gateway.sock") } fn command(&self) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_gatewayd")); command .args(["serve", "--config"]) .arg(&self.config) .env_remove("CREDENTIALS_DIRECTORY") .env("GW_TEST_TOKEN", TOKEN) .stdin(Stdio::null()) .stdout(Stdio::null()); command } /// `gatewayd serve` with its stderr in `/.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 `gatewayd 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, String) { let mut child = Daemon(self.command().stderr(Stdio::piped()).spawn().unwrap()); let deadline = Instant::now() + WAIT; 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) } } /// A running `gatewayd serve`, killed and waited for when dropped. Declared after the `Home` and /// the `FakeMm`, 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 `gatewayd` answers on `gateway.sock` as it does for `brokerd`: approval `approval`, /// of a `bxctl chat` session, is posted to the first approver and the posts come back. The /// connection is then closed, which ends the approval. fn answers(socket: &Path, approval: u64) -> bool { match ask(socket, pending(approval, "chat-1789700000-1", "{}")) { Ok(asking) => { let answered = !asking.posts.is_empty(); asking.close(); answered } Err(_) => false, } } /// Waits up to 10 s for `gatewayd` to answer on `socket`. fn wait_until_it_answers(socket: &Path, approval: u64) -> bool { let deadline = Instant::now() + WAIT; while Instant::now() < deadline { if answers(socket, approval) { return true; } std::thread::sleep(Duration::from_millis(50)); } 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_gatewayd_on_the_same_home_is_refused_and_the_first_keeps_serving() { let mm = FakeMm::start(); let home = Home::new("inst-twice", &mm); let mut first = home.start("first"); let _ws = mm.next_ws(WAIT); assert!( wait_until_it_answers(&home.socket(), 1), "the first gatewayd never answered; stderr: {}", home.stderr_of("first") ); let before = inode(&home.socket()); let (code, stderr) = home.run_to_end(); let line = format!( "gatewayd: another gatewayd is running (it holds {}); {POINTER}", home.path("run/gatewayd.lock").display() ); assert_eq!(code, Some(1), "the second gatewayd: {stderr}"); assert_eq!( stderr, format!("{line}\n"), "the lock's line, and nothing else" ); assert!(first.running(), "the first gatewayd is still running"); assert_eq!( inode(&home.socket()), before, "the first's socket is still there" ); assert!( answers(&home.socket(), 2), "the first gatewayd still answers; its stderr: {}", home.stderr_of("first") ); } #[test] fn after_the_first_is_killed_a_new_gatewayd_removes_the_stale_socket_and_serves() { let mm = FakeMm::start(); let home = Home::new("inst-restart", &mm); let mut first = home.start("first"); let _ws = mm.next_ws(WAIT); assert!( wait_until_it_answers(&home.socket(), 1), "the first gatewayd never answered; stderr: {}", home.stderr_of("first") ); let _ = first.0.kill(); let _ = first.0.wait(); assert!( home.socket().exists(), "a killed gatewayd leaves its socket" ); assert!( !answers(&home.socket(), 2), "nobody serves the stale socket" ); let _next = home.start("next"); let _ws = mm.next_ws(WAIT); assert!( wait_until_it_answers(&home.socket(), 3), "the new gatewayd never answered; stderr: {}", home.stderr_of("next") ); let lock = home.path("run/gatewayd.lock"); let mode = std::fs::metadata(&lock).map(|m| m.permissions().mode() & 0o777); assert_eq!(mode.ok(), Some(0o600), "the lock file, mode 0600"); }