From a2619f6e5f7fcadcb7fc634ff564ffb544586b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Mon, 14 Sep 2026 14:50:10 +0200 Subject: [PATCH 1/2] fix: verify saved pid is a live postgres; tolerate missing pid; C locale on Windows initdb - Only treat an instance as running when the saved pid matches postmaster.pid and is a live postgres process, so a pid reused after reboot no longer blocks start or gets signalled by stop/drop (#37). - Make InstanceInfo.pid optional so instance.json without pid (or null/0) loads (#36). - On Windows, run initdb ourselves with --locale=C for new clusters so localized non-ASCII locale names don't fail (#35). --- CHANGELOG.md | 8 ++ src/main.rs | 208 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 174 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index accc580..6630ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to pg0 are documented in this file. +## [Unreleased] + +### Fixed + +- `start`, `stop`, and `drop` no longer trust a saved pid that the OS reused after a reboot; a pid only counts as running if it matches `postmaster.pid` and is a live postgres process. This fixes a permanent "Instance already running" refusal and prevents `stop` from signalling an unrelated process (#37). +- `instance.json` without a `pid` (or with `null` / `0`) no longer breaks `start` (#36). +- New Windows clusters are initialized with `--locale=C`, so `initdb` no longer fails on localized locale names such as `Turkish_Türkiye.1252` (#35). Existing data directories are unchanged. + ## [0.15.1] - 2026-07-31 ### Fixed diff --git a/src/main.rs b/src/main.rs index 9050cb0..41515b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -183,7 +183,8 @@ enum OutputFormat { #[derive(Serialize, Deserialize)] struct InstanceInfo { - pid: u32, + // Optional: state written by other tools may lack it or hold null (#36). + pid: Option, port: u16, data_dir: PathBuf, installation_dir: PathBuf, @@ -279,21 +280,34 @@ fn list_instances() -> Result, CliError> { Ok(names) } -fn is_process_running(pid: u32) -> bool { +/// Whether `pid` is a live PostgreSQL server process. Liveness alone is not +/// enough: after a reboot the OS may hand a saved pid to an unrelated process, +/// which we must neither wait on nor signal (#37). +fn is_postgres_process(pid: u32) -> bool { + if pid == 0 { + // kill(0, ...) targets our own process group and always "succeeds". + return false; + } + let is_postgres_path = |path: &str| { + Path::new(path.trim()) + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("postgres")) + }; #[cfg(unix)] { - use std::process::Command; - Command::new("kill") - .args(["-0", &pid.to_string()]) + process::Command::new("ps") + .args(["-p", &pid.to_string(), "-o", "comm="]) .output() - .map(|o| o.status.success()) + .map(|o| o.status.success() && is_postgres_path(&String::from_utf8_lossy(&o.stdout))) .unwrap_or(false) } #[cfg(windows)] { use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE}; use windows_sys::Win32::System::Threading::{ - GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, + GetExitCodeProcess, OpenProcess, QueryFullProcessImageNameW, + PROCESS_QUERY_LIMITED_INFORMATION, }; // `tasklist` is an external command and can block before `start` emits @@ -305,14 +319,25 @@ fn is_process_running(pid: u32) -> bool { } let mut exit_code = 0; + let mut buf = [0u16; 1024]; + let mut len = buf.len() as u32; let running = GetExitCodeProcess(handle, &mut exit_code) != 0 - && exit_code == STILL_ACTIVE as u32; + && exit_code == STILL_ACTIVE as u32 + && QueryFullProcessImageNameW(handle, 0, buf.as_mut_ptr(), &mut len) != 0 + && is_postgres_path(&String::from_utf16_lossy(&buf[..len as usize])); let _ = CloseHandle(handle); running } } } +/// The pid of this instance's live postmaster, if any: the saved pid must +/// match the data dir's postmaster.pid and be a live postgres process. +fn running_pid(info: &InstanceInfo) -> Option { + let pid = info.pid?; + (read_postmaster_pid(&info.data_dir).ok()? == pid && is_postgres_process(pid)).then_some(pid) +} + /// Read the PID from PostgreSQL's postmaster.pid file fn read_postmaster_pid(data_dir: &PathBuf) -> Result { let pid_file = data_dir.join("postmaster.pid"); @@ -775,14 +800,19 @@ fn start( ) -> Result<(), CliError> { // Check if already running if let Some(info) = load_instance(&name)? { - if is_process_running(info.pid) { - return Err(CliError::AlreadyRunning(info.pid)); + // Trust the data dir's postmaster.pid (verified as a live postgres) + // over the saved pid, which can be stale or reused after a reboot. + if let Some(pid) = read_postmaster_pid(&info.data_dir) + .ok() + .filter(|&pid| is_postgres_process(pid)) + { + return Err(CliError::AlreadyRunning(pid)); } // Stale instance: clean up instance metadata but preserve data directory. // Remove stale postmaster.pid so PostgreSQL can start with existing data. let pid_file = info.data_dir.join("postmaster.pid"); if pid_file.exists() { - println!("Removing stale postmaster.pid (process {} no longer running)...", info.pid); + println!("Removing stale postmaster.pid (server no longer running)..."); fs::remove_file(&pid_file)?; } remove_instance(&name)?; @@ -856,6 +886,15 @@ fn start( // Extract bundled PostgreSQL let version_install_dir = extract_bundled_postgresql(&installation_dir, &version)?; + // Windows initdb takes its locale from the OS (LC_ALL is ignored) and + // rejects localized names like "Turkish_Türkiye.1252" (#35). + // postgresql_embedded can't pass --locale, so initialize the cluster + // ourselves; setup() then skips its own initdb. + #[cfg(windows)] + if !data_dir.join("postgresql.conf").exists() { + init_data_dir(&version_install_dir, &data_dir, &password)?; + } + let settings = Settings { version: version_req, port, @@ -935,7 +974,7 @@ fn start( let pid = read_postmaster_pid(&data_dir)?; let info = InstanceInfo { - pid, + pid: Some(pid), port, data_dir: data_dir.clone(), installation_dir, @@ -974,6 +1013,28 @@ fn start( Ok(()) } +/// Run initdb exactly as postgresql_embedded does, plus `--locale=C`. +#[cfg(windows)] +fn init_data_dir(version_dir: &Path, data_dir: &Path, password: &str) -> Result<(), CliError> { + let pwfile = std::env::temp_dir().join(format!("pg0-initdb-{}.pw", process::id())); + fs::write(&pwfile, password)?; + let output = process::Command::new(version_dir.join("bin").join("initdb.exe")) + .arg("-D") + .arg(data_dir) + .args(["-U", "postgres", "--auth=password", "--encoding=UTF8", "--locale=C"]) + .arg(format!("--pwfile={}", pwfile.display())) + .output(); + let _ = fs::remove_file(&pwfile); + let output = output?; + if !output.status.success() { + return Err(CliError::Other(format!( + "initdb failed: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + Ok(()) +} + fn send_kill_signal(pid: u32) { #[cfg(unix)] { @@ -1002,7 +1063,7 @@ fn wait_for_shutdown(pid: u32, data_dir: &PathBuf, timeout: std::time::Duration) let deadline = std::time::Instant::now() + timeout; let pid_file = data_dir.join("postmaster.pid"); loop { - if !is_process_running(pid) && !pid_file.exists() { + if !is_postgres_process(pid) && !pid_file.exists() { return true; } if std::time::Instant::now() >= deadline { @@ -1049,12 +1110,12 @@ fn find_pg_ctl_binary(installation_dir: &PathBuf) -> Result { fn stop(name: String, timeout_secs: u64) -> Result<(), CliError> { let info = load_instance(&name)?.ok_or(CliError::NoInstance)?; - if !is_process_running(info.pid) { + let Some(pid) = running_pid(&info) else { println!("PostgreSQL instance '{}' is not running.", name); return Ok(()); - } + }; - println!("Stopping PostgreSQL instance '{}' (pid: {})...", name, info.pid); + println!("Stopping PostgreSQL instance '{}' (pid: {})...", name, pid); let pg_ctl = find_pg_ctl_binary(&info.installation_dir)?; let pg_ctl_status = std::process::Command::new(&pg_ctl) @@ -1073,7 +1134,7 @@ fn stop(name: String, timeout_secs: u64) -> Result<(), CliError> { // subsequent start runs. if pg_ctl_status.success() && wait_for_shutdown( - info.pid, + pid, &info.data_dir, std::time::Duration::from_secs(5), ) @@ -1086,7 +1147,9 @@ fn stop(name: String, timeout_secs: u64) -> Result<(), CliError> { "PostgreSQL did not shut down within {}s, sending SIGKILL...", timeout_secs ); - send_kill_signal(info.pid); + if is_postgres_process(pid) { + send_kill_signal(pid); + } Err(CliError::Other(format!( "PostgreSQL instance '{}' did not shut down within {}s; sent SIGKILL", name, timeout_secs @@ -1122,8 +1185,8 @@ fn drop_instance(name: String, force: bool) -> Result<(), CliError> { // Stop if running — wait for the postmaster to fully exit before // deleting the data directory so we don't yank files out from under // an in-progress shutdown. - if is_process_running(info.pid) { - println!("Stopping PostgreSQL instance '{}' (pid: {})...", name, info.pid); + if let Some(pid) = running_pid(&info) { + println!("Stopping PostgreSQL instance '{}' (pid: {})...", name, pid); let stopped = match find_pg_ctl_binary(&info.installation_dir) { Ok(pg_ctl) => std::process::Command::new(&pg_ctl) .arg("stop") @@ -1140,9 +1203,11 @@ fn drop_instance(name: String, force: bool) -> Result<(), CliError> { Err(_) => false, }; if !stopped - || !wait_for_shutdown(info.pid, &info.data_dir, std::time::Duration::from_secs(5)) + || !wait_for_shutdown(pid, &info.data_dir, std::time::Duration::from_secs(5)) { - send_kill_signal(info.pid); + if is_postgres_process(pid) { + send_kill_signal(pid); + } } } @@ -1177,7 +1242,7 @@ fn info(name: String, output_format: OutputFormat) -> Result<(), CliError> { InfoOutput { name: name.clone(), running: true, - pid: Some(info.pid), + pid: info.pid, port: Some(info.port), version: Some(info.version), username: Some(info.username), @@ -1284,7 +1349,7 @@ fn find_psql_binary(installation_dir: &PathBuf) -> Result { /// memory has been removed). Do not report such an instance as running: /// callers use this status to decide whether it is safe to reuse a database. fn is_database_healthy(info: &InstanceInfo) -> bool { - if !is_process_running(info.pid) { + if running_pid(info).is_none() { return false; } @@ -1316,7 +1381,7 @@ fn is_database_healthy(info: &InstanceInfo) -> bool { fn psql(name: String, args: Vec) -> Result<(), CliError> { let info = load_instance(&name)?.ok_or(CliError::NoInstance)?; - if !is_process_running(info.pid) { + if running_pid(&info).is_none() { remove_instance(&name)?; return Err(CliError::NoInstance); } @@ -1449,7 +1514,7 @@ fn find_installed_version(installation_dir: &PathBuf) -> Result Result<(), CliError> { let info = load_instance(&instance_name)?.ok_or(CliError::NoInstance)?; - if !is_process_running(info.pid) { + if running_pid(&info).is_none() { remove_instance(&instance_name)?; return Err(CliError::NoInstance); } @@ -1526,7 +1591,7 @@ fn list(output_format: OutputFormat) -> Result<(), CliError> { InfoOutput { name: name.clone(), running: true, - pid: Some(info.pid), + pid: info.pid, port: Some(info.port), version: Some(info.version), username: Some(info.username), @@ -1623,15 +1688,81 @@ mod tests { use std::os::unix::fs::PermissionsExt; use std::time::{SystemTime, UNIX_EPOCH}; - #[test] - fn health_check_requires_a_successful_query() { - let test_dir = std::env::temp_dir().join(format!( - "pg0-health-check-{}", + fn unique_dir(prefix: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "{}-{}", + prefix, SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos() - )); + )) + } + + /// Spawn `sleep` renamed to `postgres` and record it in postmaster.pid. + fn spawn_fake_postmaster(test_dir: &Path) -> process::Child { + let fake = test_dir.join("postgres"); + fs::copy("/bin/sleep", &fake).unwrap(); + let child = process::Command::new(&fake).arg("30").spawn().unwrap(); + let data_dir = test_dir.join("data"); + fs::create_dir_all(&data_dir).unwrap(); + fs::write(data_dir.join("postmaster.pid"), format!("{}\n", child.id())).unwrap(); + child + } + + fn instance(test_dir: &Path, pid: Option) -> InstanceInfo { + InstanceInfo { + pid, + port: 5432, + data_dir: test_dir.join("data"), + installation_dir: test_dir.to_path_buf(), + username: "postgres".to_string(), + password: "postgres".to_string(), + database: "postgres".to_string(), + version: "18.1.0".to_string(), + } + } + + #[test] + fn running_pid_rejects_stale_or_reused_pids() { + let test_dir = unique_dir("pg0-running-pid"); + fs::create_dir_all(&test_dir).unwrap(); + let mut child = spawn_fake_postmaster(&test_dir); + let pid = child.id(); + + assert_eq!(running_pid(&instance(&test_dir, Some(pid))), Some(pid)); + // #36: missing / zero pid. + assert_eq!(running_pid(&instance(&test_dir, None)), None); + assert_eq!(running_pid(&instance(&test_dir, Some(0))), None); + assert!(!is_postgres_process(0)); + // #37: saved pid is alive but not postgres (reused after reboot). + let me = process::id(); + let data_dir = test_dir.join("data"); + fs::write(data_dir.join("postmaster.pid"), format!("{}\n", me)).unwrap(); + assert_eq!(running_pid(&instance(&test_dir, Some(me))), None); + // Saved pid doesn't match postmaster.pid. + fs::write(data_dir.join("postmaster.pid"), format!("{}\n", pid)).unwrap(); + assert_eq!(running_pid(&instance(&test_dir, Some(me))), None); + // No postmaster.pid at all. + fs::remove_file(data_dir.join("postmaster.pid")).unwrap(); + assert_eq!(running_pid(&instance(&test_dir, Some(pid))), None); + + child.kill().unwrap(); + fs::remove_dir_all(test_dir).unwrap(); + } + + #[test] + fn instance_json_without_pid_loads() { + let info: InstanceInfo = serde_json::from_str( + r#"{"port":5432,"data_dir":"/d","installation_dir":"/i","username":"u","password":"p","database":"db","version":"18"}"#, + ) + .unwrap(); + assert_eq!(info.pid, None); + } + + #[test] + fn health_check_requires_a_successful_query() { + let test_dir = unique_dir("pg0-health-check"); let bin_dir = test_dir.join("18.1.0").join("bin"); fs::create_dir_all(&bin_dir).unwrap(); let psql_path = bin_dir.join("psql"); @@ -1642,18 +1773,11 @@ mod tests { .unwrap(); fs::set_permissions(&psql_path, fs::Permissions::from_mode(0o755)).unwrap(); - let info = InstanceInfo { - pid: process::id(), - port: 5432, - data_dir: test_dir.join("data"), - installation_dir: test_dir.clone(), - username: "postgres".to_string(), - password: "postgres".to_string(), - database: "postgres".to_string(), - version: "18.1.0".to_string(), - }; + let mut child = spawn_fake_postmaster(&test_dir); + let info = instance(&test_dir, Some(child.id())); assert!(is_database_healthy(&info)); + child.kill().unwrap(); fs::remove_dir_all(test_dir).unwrap(); } } From fc239c24b0868dc5142035765bbb7448c345a211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Mon, 14 Sep 2026 15:17:36 +0200 Subject: [PATCH 2/2] test: unit tests for every pid/locale change; run cargo test in CI - Extract check_not_running (start's stale-pidfile handling) and kill_if_postgres (guarded SIGKILL fallback) so they can be tested. - Tests: is_postgres_process, running_pid, check_not_running (live, reused, garbage, missing pidfile), kill_if_postgres, wait_for_shutdown with a reused pid, health check without a live postmaster, instance.json with missing/null pid and round-trip, and init_data_dir running the real bundled initdb with --locale=C. - Run cargo test in the macOS CI job; the suite was never run in CI. --- .github/workflows/ci.yml | 3 + src/main.rs | 210 ++++++++++++++++++++++++++++++++++----- 2 files changed, 187 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a78fbc6..2a271f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,9 @@ jobs: - name: Build run: cargo build --release + - name: Unit tests + run: cargo test --release + - name: Upload CLI artifact uses: actions/upload-artifact@v4 with: diff --git a/src/main.rs b/src/main.rs index 41515b8..617f36d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -800,21 +800,8 @@ fn start( ) -> Result<(), CliError> { // Check if already running if let Some(info) = load_instance(&name)? { - // Trust the data dir's postmaster.pid (verified as a live postgres) - // over the saved pid, which can be stale or reused after a reboot. - if let Some(pid) = read_postmaster_pid(&info.data_dir) - .ok() - .filter(|&pid| is_postgres_process(pid)) - { - return Err(CliError::AlreadyRunning(pid)); - } + check_not_running(&info.data_dir)?; // Stale instance: clean up instance metadata but preserve data directory. - // Remove stale postmaster.pid so PostgreSQL can start with existing data. - let pid_file = info.data_dir.join("postmaster.pid"); - if pid_file.exists() { - println!("Removing stale postmaster.pid (server no longer running)..."); - fs::remove_file(&pid_file)?; - } remove_instance(&name)?; } @@ -1013,12 +1000,42 @@ fn start( Ok(()) } +/// Refuse if a live postgres owns this data dir's postmaster.pid; otherwise +/// remove a stale postmaster.pid so PostgreSQL can start with existing data. +/// The pidfile is trusted over instance.json's pid, which can be stale or +/// reused after a reboot (#37). +fn check_not_running(data_dir: &Path) -> Result<(), CliError> { + let data_dir = data_dir.to_path_buf(); + if let Some(pid) = read_postmaster_pid(&data_dir) + .ok() + .filter(|&pid| is_postgres_process(pid)) + { + return Err(CliError::AlreadyRunning(pid)); + } + let pid_file = data_dir.join("postmaster.pid"); + if pid_file.exists() { + println!("Removing stale postmaster.pid (server no longer running)..."); + fs::remove_file(&pid_file)?; + } + Ok(()) +} + +/// SIGKILL fallback that re-checks the pid first, so a pid the OS has +/// already handed to another process is never killed (#37). +fn kill_if_postgres(pid: u32) { + if is_postgres_process(pid) { + send_kill_signal(pid); + } +} + /// Run initdb exactly as postgresql_embedded does, plus `--locale=C`. -#[cfg(windows)] +/// Only called on Windows; kept cross-platform so it is tested everywhere. +#[cfg_attr(not(windows), allow(dead_code))] fn init_data_dir(version_dir: &Path, data_dir: &Path, password: &str) -> Result<(), CliError> { let pwfile = std::env::temp_dir().join(format!("pg0-initdb-{}.pw", process::id())); fs::write(&pwfile, password)?; - let output = process::Command::new(version_dir.join("bin").join("initdb.exe")) + let initdb = if cfg!(windows) { "initdb.exe" } else { "initdb" }; + let output = process::Command::new(version_dir.join("bin").join(initdb)) .arg("-D") .arg(data_dir) .args(["-U", "postgres", "--auth=password", "--encoding=UTF8", "--locale=C"]) @@ -1147,9 +1164,7 @@ fn stop(name: String, timeout_secs: u64) -> Result<(), CliError> { "PostgreSQL did not shut down within {}s, sending SIGKILL...", timeout_secs ); - if is_postgres_process(pid) { - send_kill_signal(pid); - } + kill_if_postgres(pid); Err(CliError::Other(format!( "PostgreSQL instance '{}' did not shut down within {}s; sent SIGKILL", name, timeout_secs @@ -1205,9 +1220,7 @@ fn drop_instance(name: String, force: bool) -> Result<(), CliError> { if !stopped || !wait_for_shutdown(pid, &info.data_dir, std::time::Duration::from_secs(5)) { - if is_postgres_process(pid) { - send_kill_signal(pid); - } + kill_if_postgres(pid); } } @@ -1751,15 +1764,160 @@ mod tests { fs::remove_dir_all(test_dir).unwrap(); } + const STATE: &str = r#""port":5432,"data_dir":"/d","installation_dir":"/i","username":"u","password":"p","database":"db","version":"18""#; + #[test] fn instance_json_without_pid_loads() { - let info: InstanceInfo = serde_json::from_str( - r#"{"port":5432,"data_dir":"/d","installation_dir":"/i","username":"u","password":"p","database":"db","version":"18"}"#, - ) - .unwrap(); + let info: InstanceInfo = serde_json::from_str(&format!("{{{}}}", STATE)).unwrap(); assert_eq!(info.pid, None); } + #[test] + fn instance_json_with_null_pid_loads() { + let info: InstanceInfo = + serde_json::from_str(&format!(r#"{{"pid":null,{}}}"#, STATE)).unwrap(); + assert_eq!(info.pid, None); + } + + #[test] + fn instance_json_round_trips_pid() { + let json = serde_json::to_string(&instance(Path::new("/t"), Some(42))).unwrap(); + let info: InstanceInfo = serde_json::from_str(&json).unwrap(); + assert_eq!(info.pid, Some(42)); + } + + #[test] + fn is_postgres_process_checks_liveness_and_name() { + let test_dir = unique_dir("pg0-is-postgres"); + fs::create_dir_all(&test_dir).unwrap(); + let mut child = spawn_fake_postmaster(&test_dir); + let pid = child.id(); + + assert!(is_postgres_process(pid)); + // Alive but not postgres, e.g. a pid reused after reboot. + assert!(!is_postgres_process(process::id())); + // kill(0) would target our own process group. + assert!(!is_postgres_process(0)); + + child.kill().unwrap(); + child.wait().unwrap(); + assert!(!is_postgres_process(pid)); + fs::remove_dir_all(test_dir).unwrap(); + } + + #[test] + fn check_not_running_refuses_live_postmaster() { + let test_dir = unique_dir("pg0-check-live"); + fs::create_dir_all(&test_dir).unwrap(); + let mut child = spawn_fake_postmaster(&test_dir); + let data_dir = test_dir.join("data"); + + let result = check_not_running(&data_dir); + assert!(matches!(result, Err(CliError::AlreadyRunning(p)) if p == child.id())); + // Never remove a live server's pidfile. + assert!(data_dir.join("postmaster.pid").exists()); + + child.kill().unwrap(); + fs::remove_dir_all(test_dir).unwrap(); + } + + #[test] + fn check_not_running_clears_stale_pidfiles() { + let data_dir = unique_dir("pg0-check-stale"); + fs::create_dir_all(&data_dir).unwrap(); + let pid_file = data_dir.join("postmaster.pid"); + + // #37: pidfile names a pid the OS reused for a non-postgres process. + fs::write(&pid_file, format!("{}\n", process::id())).unwrap(); + check_not_running(&data_dir).unwrap(); + assert!(!pid_file.exists()); + + // Unparseable pidfile. + fs::write(&pid_file, "garbage\n").unwrap(); + check_not_running(&data_dir).unwrap(); + assert!(!pid_file.exists()); + + // No pidfile at all. + check_not_running(&data_dir).unwrap(); + fs::remove_dir_all(data_dir).unwrap(); + } + + #[test] + fn kill_if_postgres_spares_other_processes() { + let test_dir = unique_dir("pg0-kill"); + fs::create_dir_all(&test_dir).unwrap(); + + let mut other = process::Command::new("sleep").arg("30").spawn().unwrap(); + kill_if_postgres(other.id()); + std::thread::sleep(std::time::Duration::from_millis(200)); + assert!(other.try_wait().unwrap().is_none(), "non-postgres process was killed"); + other.kill().unwrap(); + + let mut postgres = spawn_fake_postmaster(&test_dir); + kill_if_postgres(postgres.id()); + assert!(!postgres.wait().unwrap().success()); + + fs::remove_dir_all(test_dir).unwrap(); + } + + #[test] + fn wait_for_shutdown_ignores_reused_pid() { + let test_dir = unique_dir("pg0-wait"); + fs::create_dir_all(&test_dir).unwrap(); + let timeout = std::time::Duration::from_millis(300); + + // Postgres alive with its pidfile: not shut down. + let mut postgres = spawn_fake_postmaster(&test_dir); + let data_dir = test_dir.join("data"); + assert!(!wait_for_shutdown(postgres.id(), &data_dir, timeout)); + postgres.kill().unwrap(); + + // Pid now held by a non-postgres process and no pidfile: shut down. + fs::remove_file(data_dir.join("postmaster.pid")).unwrap(); + assert!(wait_for_shutdown(process::id(), &data_dir, timeout)); + + fs::remove_dir_all(test_dir).unwrap(); + } + + #[test] + fn init_data_dir_uses_c_locale() { + let test_dir = unique_dir("pg0-initdb"); + let version_dir = + extract_bundled_postgresql(&test_dir.join("installation"), env!("PG_VERSION")).unwrap(); + let data_dir = test_dir.join("data"); + fs::create_dir_all(&data_dir).unwrap(); + + init_data_dir(&version_dir, &data_dir, "secret").unwrap(); + + let conf = fs::read_to_string(data_dir.join("postgresql.conf")).unwrap(); + assert!( + conf.lines().any(|l| l.starts_with("lc_messages = C")), + "cluster not initialized with --locale=C" + ); + let hba = fs::read_to_string(data_dir.join("pg_hba.conf")).unwrap(); + assert!(hba.lines().any(|l| !l.starts_with('#') && l.trim_end().ends_with("password"))); + // The temporary password file must not be left behind. + assert!(!std::env::temp_dir() + .join(format!("pg0-initdb-{}.pw", process::id())) + .exists()); + // setup() skips its own initdb when this file exists. + assert!(data_dir.join("postgresql.conf").exists()); + + fs::remove_dir_all(test_dir).unwrap(); + } + + #[test] + fn health_check_fails_without_live_postmaster() { + let test_dir = unique_dir("pg0-health-stale"); + fs::create_dir_all(test_dir.join("data")).unwrap(); + // Reused pid: alive, matches the pidfile, but not postgres. + let me = process::id(); + fs::write(test_dir.join("data").join("postmaster.pid"), format!("{}\n", me)).unwrap(); + assert!(!is_database_healthy(&instance(&test_dir, Some(me)))); + assert!(!is_database_healthy(&instance(&test_dir, None))); + fs::remove_dir_all(test_dir).unwrap(); + } + #[test] fn health_check_requires_a_successful_query() { let test_dir = unique_dir("pg0-health-check");