1use crate::error::{ErrorCode, MitosisError, Result};
2use crate::pg::binaries::PgInstall;
3use crate::proc_util::{has_exited, is_alive, process_cwd, process_start_time};
4use nix::errno::Errno;
5use nix::sys::signal::{kill, killpg, Signal};
6use nix::unistd::Pid;
7use std::io::{Read, Seek, SeekFrom, Write};
8use std::os::unix::fs::MetadataExt;
9use std::os::unix::process::CommandExt;
10use std::path::{Path, PathBuf};
11use std::process::{Command, Stdio};
12use std::time::{Duration, Instant};
13
14#[derive(Clone, Debug)]
15pub struct StartOptions {
16 pub port: u16,
17 pub socket_dir: PathBuf,
18 pub listen_tcp: bool,
19 pub durable: bool,
20 pub log_file: PathBuf,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct RunningPg {
25 pub pid: i32,
26 pub start_time: u64,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct PostmasterInfo {
31 pub pid: i32,
32 pub data_dir: PathBuf,
33 pub port: u16,
34 pub start_time: u64,
36}
37
38fn internal(msg: impl Into<String>) -> MitosisError {
39 MitosisError::new(ErrorCode::Internal, msg)
40}
41
42const AUTH_LOCAL: &str = "trust";
44const AUTH_HOST: &str = "scram-sha-256";
45
46pub fn initdb(install: &PgInstall, data_dir: &Path) -> Result<()> {
47 let out = Command::new(install.bin("initdb"))
48 .arg("-D")
49 .arg(data_dir)
50 .args(["-U", "postgres", "-E", "UTF8"])
51 .arg(format!("--auth-local={AUTH_LOCAL}"))
52 .arg(format!("--auth-host={AUTH_HOST}"))
53 .output()?;
54 if !out.status.success() {
55 return Err(internal(format!(
56 "initdb failed: {}",
57 String::from_utf8_lossy(&out.stderr).trim()
58 )));
59 }
60 Ok(())
61}
62
63fn hba_text() -> String {
66 let mut text = String::new();
67 for db in ["all", "replication"] {
68 text.push_str(&format!(
69 "{:<8}{db:<16}{:<40}{AUTH_LOCAL}\n",
70 "local", "all"
71 ));
72 for addr in ["127.0.0.1/32", "::1/128"] {
73 text.push_str(&format!(
74 "{:<8}{db:<16}{:<16}{addr:<24}{AUTH_HOST}\n",
75 "host", "all"
76 ));
77 }
78 }
79 text
80}
81
82fn comment_out(text: &str, hit: impl Fn(&str) -> bool) -> (String, Vec<String>) {
85 let mut out = String::with_capacity(text.len());
86 let mut hits = Vec::new();
87 for line in text.split_inclusive('\n') {
88 if hit(line.trim_start()) {
89 hits.push(line.trim().to_string());
90 out.push_str("#mitosis: ");
91 }
92 out.push_str(line);
93 }
94 (out, hits)
95}
96
97fn setting_name(line: &str) -> Option<String> {
100 line.split(|c: char| c == '=' || c.is_whitespace())
101 .next()
102 .filter(|n| !n.is_empty() && !n.starts_with('#'))
103 .map(str::to_ascii_lowercase)
104}
105
106fn include_leaves_dir(line: &str, data_dir: &Path) -> bool {
109 let Some((_, rest)) = line.split_once(|c: char| c == '=' || c.is_whitespace()) else {
110 return true;
111 };
112 let rest = rest.trim_start_matches(|c: char| c == '=' || c.is_whitespace());
113 let path = rest
114 .strip_prefix('\'')
115 .and_then(|r| r.split('\'').next())
116 .unwrap_or_else(|| rest.split('#').next().unwrap_or("").trim());
117 let path = Path::new(path);
118 if path.is_absolute() {
119 !path.starts_with(data_dir)
120 } else {
121 path.components()
122 .any(|c| matches!(c, std::path::Component::ParentDir))
123 }
124}
125
126const NEUTRALISED: [(&str, Option<&str>); 11] = [
131 ("archive_mode", Some("off")),
132 ("archive_command", Some("''")),
133 ("ssl", Some("off")),
134 ("restore_command", Some("''")),
135 ("primary_conninfo", Some("''")),
136 ("primary_slot_name", Some("''")),
137 ("synchronous_standby_names", Some("''")),
139 ("data_directory", None),
140 ("hba_file", None),
141 ("ident_file", None),
142 ("external_pid_file", None),
143];
144
145pub fn make_standalone(data_dir: &Path) -> Result<Vec<String>> {
152 for name in [
153 "standby.signal",
154 "recovery.signal",
155 "postmaster.pid",
156 "postmaster.opts",
157 ] {
158 match std::fs::remove_file(data_dir.join(name)) {
159 Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
160 return Err(internal(format!("remove {name}: {e}")))
161 }
162 _ => {}
163 }
164 }
165 let write = |name: &str, text: &str| {
166 std::fs::write(data_dir.join(name), text)
167 .map_err(|e| internal(format!("write {name}: {e}")))
168 };
169 let mut warnings = Vec::new();
170 write("pg_hba.conf", &hba_text())?;
171 write("postgresql.auto.conf", "")?;
172 if !data_dir.join("pg_ident.conf").exists() {
173 warnings.push("pg_ident.conf was not in the copied folder; wrote an empty one".into());
174 write("pg_ident.conf", "# Mitosis: no user name maps\n")?;
175 }
176
177 let conf = data_dir.join("postgresql.conf");
178 let text = match std::fs::read_to_string(&conf) {
179 Ok(t) => t,
180 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
181 warnings.push(
182 "postgresql.conf was not in the copied folder (the source keeps it elsewhere); \
183 using Postgres defaults"
184 .into(),
185 );
186 String::new()
187 }
188 Err(e) => return Err(internal(format!("read {}: {e}", conf.display()))),
189 };
190 let (mut text, hits) = comment_out(&text, |line| match setting_name(line).as_deref() {
191 Some("include" | "include_dir" | "include_if_exists") => include_leaves_dir(line, data_dir),
192 Some(name) => NEUTRALISED.iter().any(|(n, _)| *n == name),
193 None => false,
194 });
195 warnings.extend(
197 hits.iter()
198 .filter_map(|l| setting_name(l))
199 .map(|name| format!("commented out the source's {name} setting in postgresql.conf")),
200 );
201 if !text.is_empty() && !text.ends_with('\n') {
203 text.push('\n');
204 }
205 text.push_str("# Mitosis: a copy must not inherit these\n");
206 for (name, value) in NEUTRALISED {
207 if let Some(v) = value {
208 text.push_str(&format!("{name} = {v}\n"));
209 }
210 }
211 write("postgresql.conf", &text)?;
212 Ok(warnings)
213}
214
215pub fn disable_preload_libraries(data_dir: &Path) -> Result<Vec<String>> {
218 let conf = data_dir.join("postgresql.conf");
219 let text = match std::fs::read_to_string(&conf) {
220 Ok(t) => t,
221 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
222 Err(e) => return Err(internal(format!("read {}: {e}", conf.display()))),
223 };
224 let (text, hits) = comment_out(&text, |line| {
225 setting_name(line).as_deref() == Some("shared_preload_libraries")
226 });
227 if !hits.is_empty() {
228 std::fs::write(&conf, text)
229 .map_err(|e| internal(format!("write {}: {e}", conf.display())))?;
230 }
231 Ok(hits)
232}
233
234pub fn is_missing_library(message: &str) -> bool {
236 message.contains("could not access file")
237 || message.contains("could not load library")
238 || message.contains("shared_preload_libraries")
239}
240
241pub fn ensure_postgres_superuser(install: &PgInstall, data_dir: &Path) -> Result<()> {
246 let sql = "DO $$BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'postgres') \
248 THEN CREATE ROLE postgres SUPERUSER LOGIN; ELSE ALTER ROLE postgres SUPERUSER LOGIN; \
249 END IF; END$$;\n";
250 let mut child = Command::new(install.bin("postgres"))
251 .env("LC_ALL", "C")
252 .arg("--single")
253 .arg("-D")
254 .arg(data_dir)
255 .args(["-c", "shared_buffers=32MB", "-c", "max_connections=20"])
257 .arg("template1")
258 .stdin(Stdio::piped())
259 .stdout(Stdio::piped())
260 .stderr(Stdio::piped())
261 .spawn()
262 .map_err(|e| internal(format!("could not run postgres --single: {e}")))?;
263 let mut stdin = child.stdin.take().expect("stdin is piped");
264 let writer = std::thread::spawn(move || stdin.write_all(sql.as_bytes()));
265 let out = child.wait_with_output()?;
266 let _ = writer.join();
267 let stderr = String::from_utf8_lossy(&out.stderr);
269 if !out.status.success() || stderr.contains("ERROR:") || stderr.contains("FATAL:") {
270 return Err(MitosisError::new(
271 ErrorCode::PostgresStartFailed,
272 format!("preparing the copied cluster failed:\n{}", stderr.trim()),
273 ));
274 }
275 Ok(())
276}
277
278const MAX_SOCKET_PATH: usize = 103;
280
281pub fn check_socket_path(socket_dir: &Path, port: u16) -> Result<()> {
283 let path = socket_dir.join(format!(".s.PGSQL.{port}"));
284 let len = path.as_os_str().len();
285 if len > MAX_SOCKET_PATH {
286 return Err(MitosisError::new(
287 ErrorCode::PostgresStartFailed,
288 format!(
289 "socket path too long ({len} bytes, max {MAX_SOCKET_PATH}): {}",
290 path.display()
291 ),
292 )
293 .with_hint("set MITOSIS_HOME to a shorter path"));
294 }
295 Ok(())
296}
297
298pub fn start(install: &PgInstall, data_dir: &Path, opts: &StartOptions) -> Result<RunningPg> {
300 check_socket_path(&opts.socket_dir, opts.port)?;
301 if let Some(parent) = opts.log_file.parent() {
302 std::fs::create_dir_all(parent)?;
303 }
304 std::fs::create_dir_all(&opts.socket_dir)?;
305 let log = std::fs::OpenOptions::new()
306 .create(true)
307 .append(true)
308 .open(&opts.log_file)?;
309 let tries = std::fs::read_to_string(&opts.log_file)
311 .map(|t| t.matches(TRY_MARKER).count())
312 .unwrap_or(0);
313 writeln!(&log, "{TRY_MARKER} {}", tries + 1)?;
314 let log_err = log.try_clone()?;
315
316 let listen = if opts.listen_tcp {
317 "listen_addresses=127.0.0.1"
318 } else {
319 "listen_addresses="
320 };
321 let mut cmd = Command::new(install.bin("postgres"));
322 cmd.env("LC_ALL", "C");
325 cmd.arg("-D")
326 .arg(data_dir)
327 .args(["-p", &opts.port.to_string()])
328 .arg("-k")
329 .arg(&opts.socket_dir)
330 .args([
331 "-c",
332 listen,
333 "-c",
334 "shared_buffers=32MB",
335 "-c",
336 "max_connections=20",
337 ]);
338 if !opts.durable {
339 cmd.args([
340 "-c",
341 "fsync=off",
342 "-c",
343 "synchronous_commit=off",
344 "-c",
345 "full_page_writes=off",
346 ]);
347 }
348 let child = cmd
349 .process_group(0)
350 .stdin(Stdio::null())
351 .stdout(log)
352 .stderr(log_err)
353 .spawn()
354 .map_err(|e| {
355 MitosisError::new(
356 ErrorCode::PostgresStartFailed,
357 format!("could not spawn postgres: {e}"),
358 )
359 })?;
360 let pid = child.id() as i32;
361 drop(child); Ok(RunningPg {
364 pid,
365 start_time: process_start_time(pid).unwrap_or(0),
366 })
367}
368
369const TRY_MARKER: &str = "mitosis: start try";
370
371fn current_try_tail(path: &Path, n: usize) -> String {
373 let tail = log_tail(path, usize::MAX);
374 let after = match tail.rfind(TRY_MARKER) {
375 Some(i) => tail[i..].split_once('\n').map_or("", |(_, rest)| rest),
376 None => &tail,
377 };
378 let lines: Vec<&str> = after.lines().collect();
379 lines[lines.len().saturating_sub(n)..].join("\n")
380}
381
382pub(crate) fn log_tail(path: &Path, n: usize) -> String {
384 let read = || -> std::io::Result<String> {
385 let mut f = std::fs::File::open(path)?;
386 let len = f.metadata()?.len();
387 f.seek(SeekFrom::Start(len.saturating_sub(64 * 1024)))?;
388 let mut buf = Vec::new();
389 f.read_to_end(&mut buf)?;
390 Ok(String::from_utf8_lossy(&buf).into_owned())
391 };
392 let text = read().unwrap_or_default();
393 let lines: Vec<&str> = text.lines().collect();
394 lines[lines.len().saturating_sub(n)..].join("\n")
395}
396
397fn start_failure(log_tail: &str, port: u16) -> MitosisError {
398 if log_tail.contains("could not bind") || log_tail.contains("Address already in use") {
399 MitosisError::new(
400 ErrorCode::PortInUse,
401 format!("port {port} is already in use"),
402 )
403 } else {
404 MitosisError::new(
405 ErrorCode::PostgresStartFailed,
406 format!("postgres exited during startup:\n{log_tail}"),
407 )
408 }
409}
410
411pub async fn wait_ready(
413 install: &PgInstall,
414 pg: &RunningPg,
415 opts: &StartOptions,
416 timeout: Duration,
417) -> Result<()> {
418 let deadline = Instant::now() + timeout;
419 loop {
420 if has_exited(pg.pid) {
421 return Err(start_failure(
422 ¤t_try_tail(&opts.log_file, 20),
423 opts.port,
424 ));
425 }
426 let ready = tokio::process::Command::new(install.bin("pg_isready"))
427 .arg("-h")
428 .arg(&opts.socket_dir)
429 .args(["-p", &opts.port.to_string()])
430 .stdout(Stdio::null())
431 .stderr(Stdio::null())
432 .status()
433 .await?;
434 if ready.success() {
435 return Ok(());
436 }
437 if Instant::now() >= deadline {
438 return Err(MitosisError::new(
439 ErrorCode::PostgresStartFailed,
440 format!(
441 "postgres not ready after {}s:\n{}",
442 timeout.as_secs(),
443 current_try_tail(&opts.log_file, 20)
444 ),
445 ));
446 }
447 tokio::time::sleep(Duration::from_millis(50)).await;
448 }
449}
450
451pub fn psql(
454 install: &PgInstall,
455 socket_dir: &Path,
456 port: u16,
457 db: &str,
458 sql: &str,
459) -> Result<String> {
460 let mut child = Command::new(install.bin("psql"))
461 .arg("-h")
462 .arg(socket_dir)
463 .args(["-p", &port.to_string(), "-U", "postgres", "-d", db])
464 .args(["-v", "ON_ERROR_STOP=1", "-X", "-A", "-t", "-q", "-f", "-"])
465 .env("PGCONNECT_TIMEOUT", "5")
466 .stdin(Stdio::piped())
467 .stdout(Stdio::piped())
468 .stderr(Stdio::piped())
469 .spawn()?;
470 let mut stdin = child.stdin.take().expect("psql stdin is piped");
471 let input = sql.to_owned();
472 let writer = std::thread::spawn(move || stdin.write_all(input.as_bytes())); let out = child.wait_with_output()?;
475 let written = writer
476 .join()
477 .unwrap_or_else(|_| Err(std::io::Error::other("stdin writer panicked")));
478 if !out.status.success() {
479 return Err(internal(format!(
480 "psql failed: {}",
481 String::from_utf8_lossy(&out.stderr).trim()
482 )));
483 }
484 written.map_err(|e| internal(format!("could not send SQL to psql: {e}")))?;
485 Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
486}
487
488fn wait_exited(pid: i32, wait: Duration) -> bool {
490 let deadline = Instant::now() + wait;
491 while !has_exited(pid) {
492 if Instant::now() >= deadline {
493 return false;
494 }
495 std::thread::sleep(Duration::from_millis(20));
496 }
497 true
498}
499
500fn is_same_process(pg: RunningPg) -> bool {
503 pg.start_time != 0 && !has_exited(pg.pid) && process_start_time(pg.pid) == Some(pg.start_time)
504}
505
506fn serves_dir(pid: i32, data_dir: &Path) -> Option<bool> {
510 match process_cwd(pid) {
511 Ok((dev, ino, path)) => {
512 let same_inode =
513 std::fs::metadata(data_dir).is_ok_and(|m| (m.dev(), m.ino()) == (dev, ino));
514 let same_path = data_dir.canonicalize().is_ok_and(|d| d == path);
515 Some(same_inode || same_path)
516 }
517 Err(e) if matches!(e.raw_os_error(), Some(libc::ESRCH) | Some(libc::EPERM)) => Some(false),
518 Err(_) => None,
519 }
520}
521
522pub fn running_server(data_dir: &Path, expected: Option<RunningPg>) -> Result<Option<RunningPg>> {
526 if let Some(pg) = expected.filter(|pg| is_same_process(*pg)) {
527 return Ok(Some(pg));
528 }
529 let Some(info) = read_postmaster_pid(data_dir)? else {
530 return Ok(None);
531 };
532 if has_exited(info.pid) {
533 return Ok(None);
534 }
535 let before = process_start_time(info.pid);
537 match serves_dir(info.pid, data_dir) {
538 Some(true) => {}
539 Some(false) => return Ok(None),
540 None => {
541 return Err(internal(format!(
542 "cannot tell whether pid {} (from postmaster.pid) serves {}",
543 info.pid,
544 data_dir.display()
545 )))
546 }
547 }
548 match (before, process_start_time(info.pid)) {
549 (Some(a), Some(b)) if a == b => Ok(Some(RunningPg {
550 pid: info.pid,
551 start_time: a,
552 })),
553 _ => Ok(None), }
555}
556
557fn remove_dead_pid_file(data_dir: &Path) -> Result<()> {
559 let Some(info) = read_postmaster_pid(data_dir).ok().flatten() else {
560 return Ok(());
561 };
562 if is_alive(info.pid) {
563 return Ok(()); }
565 match std::fs::remove_file(data_dir.join("postmaster.pid")) {
566 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(e.into()),
567 _ => Ok(()),
568 }
569}
570
571pub fn stop(
575 install: &PgInstall,
576 data_dir: &Path,
577 expected: Option<RunningPg>,
578 timeout: Duration,
579) -> Result<()> {
580 let Some(pg) = running_server(data_dir, expected)? else {
581 return remove_dead_pid_file(data_dir);
582 };
583 let file_pid = read_postmaster_pid(data_dir).ok().flatten().map(|i| i.pid);
585 if file_pid == Some(pg.pid) {
586 let secs = timeout.as_secs().max(1);
587 let out = Command::new(install.bin("pg_ctl"))
588 .arg("stop")
589 .arg("-D")
590 .arg(data_dir)
591 .args(["-m", "fast", "-t", &secs.to_string()])
592 .output()?;
593 if out.status.success() {
594 wait_exited(pg.pid, Duration::from_secs(5)); return Ok(());
596 }
597 }
598 if !is_same_process(pg) {
600 return remove_dead_pid_file(data_dir);
601 }
602 let pid = Pid::from_raw(pg.pid);
603 let mut res = killpg(pid, Signal::SIGKILL);
605 if res == Err(Errno::ESRCH) {
606 res = kill(pid, Signal::SIGKILL);
607 }
608 match res {
609 Ok(()) | Err(Errno::ESRCH) => {}
610 Err(e) => {
611 return Err(internal(format!(
612 "could not kill postgres pid {}: {e}",
613 pg.pid
614 )))
615 }
616 }
617 if !wait_exited(pg.pid, Duration::from_secs(5)) {
618 return Err(internal(format!(
619 "postgres pid {} for {} is still alive after SIGKILL",
620 pg.pid,
621 data_dir.display()
622 )));
623 }
624 remove_dead_pid_file(data_dir)
625}
626
627#[derive(Clone, Debug, PartialEq, Eq)]
629pub enum ControlState {
630 ShutDown,
632 InProduction,
633 Other(String),
634}
635
636pub fn parse_control_state(text: &str) -> Option<ControlState> {
638 let state = text
639 .lines()
640 .find_map(|l| l.strip_prefix("Database cluster state:"))?
641 .trim();
642 Some(match state {
643 "shut down" | "shut down in recovery" => ControlState::ShutDown,
644 "in production" => ControlState::InProduction,
645 other => ControlState::Other(other.to_string()),
646 })
647}
648
649pub fn control_state(install: &PgInstall, data_dir: &Path) -> Result<ControlState> {
651 let out = Command::new(install.bin("pg_controldata"))
652 .arg("-D")
653 .arg(data_dir)
654 .env("LC_ALL", "C") .output()?;
656 let text = String::from_utf8_lossy(&out.stdout);
657 match parse_control_state(&text) {
658 Some(s) if out.status.success() => Ok(s),
659 _ => Err(internal(format!(
660 "pg_controldata failed for {}: {}",
661 data_dir.display(),
662 String::from_utf8_lossy(&out.stderr).trim()
663 ))),
664 }
665}
666
667pub fn read_postmaster_pid(data_dir: &Path) -> Result<Option<PostmasterInfo>> {
670 let text = match std::fs::read_to_string(data_dir.join("postmaster.pid")) {
671 Ok(t) => t,
672 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
673 Err(e) => return Err(e.into()),
674 };
675 let lines: Vec<&str> = text.lines().collect();
676 let bad = || {
677 internal(format!(
678 "malformed postmaster.pid in {}",
679 data_dir.display()
680 ))
681 };
682 let pid: i32 = lines
683 .first()
684 .and_then(|l| l.trim().parse().ok())
685 .filter(|p| *p > 1)
686 .ok_or_else(bad)?;
687 let dir = lines
688 .get(1)
689 .map(|l| l.trim())
690 .filter(|l| !l.is_empty())
691 .ok_or_else(bad)?;
692 let start_time = lines
693 .get(2)
694 .and_then(|l| l.trim().parse().ok())
695 .ok_or_else(bad)?;
696 let port = lines
697 .get(3)
698 .and_then(|l| l.trim().parse().ok())
699 .ok_or_else(bad)?;
700 Ok(Some(PostmasterInfo {
701 pid,
702 data_dir: PathBuf::from(dir),
703 port,
704 start_time,
705 }))
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn parses_postmaster_pid() {
714 let dir = tempfile::tempdir().unwrap();
715 std::fs::write(
716 dir.path().join("postmaster.pid"),
717 "4242\n/tmp/data\n1700000000\n54321\n/tmp/sock\nlocalhost\n 1234 5678\nready \n",
718 )
719 .unwrap();
720 let info = read_postmaster_pid(dir.path()).unwrap().unwrap();
721 assert_eq!(
722 info,
723 PostmasterInfo {
724 pid: 4242,
725 data_dir: PathBuf::from("/tmp/data"),
726 port: 54321,
727 start_time: 1_700_000_000
728 }
729 );
730 }
731
732 #[test]
733 fn stop_never_runs_pg_ctl_for_a_foreign_or_dead_pid() {
734 let install = PgInstall {
736 bin_dir: PathBuf::from("/nonexistent"),
737 major: 16,
738 version: "16.0".into(),
739 };
740 let dir = tempfile::tempdir().unwrap();
741 let pid_file = dir.path().join("postmaster.pid");
742
743 let me = std::process::id();
746 std::fs::write(&pid_file, format!("{me}\n/tmp/data\n1000\n54321\n")).unwrap();
747 assert_eq!(running_server(dir.path(), None).unwrap(), None);
748 stop(&install, dir.path(), None, Duration::from_secs(1)).unwrap();
749 assert!(pid_file.exists() && is_alive(me as i32));
750
751 let wrong = RunningPg {
753 pid: me as i32,
754 start_time: process_start_time(me as i32).unwrap() - 100,
755 };
756 stop(&install, dir.path(), Some(wrong), Duration::from_secs(1)).unwrap();
757 assert!(is_alive(me as i32));
758
759 let mut child = Command::new("true").spawn().unwrap();
760 let dead = child.id();
761 child.wait().unwrap();
762 std::fs::write(&pid_file, format!("{dead}\n/tmp/data\n1000\n54321\n")).unwrap();
763 stop(&install, dir.path(), None, Duration::from_secs(1)).unwrap();
764 assert!(
765 !pid_file.exists(),
766 "a pid file whose pid is dead is removed"
767 );
768 }
769
770 #[test]
771 fn a_process_running_in_the_data_dir_is_its_server() {
772 let dir = tempfile::tempdir().unwrap();
773 let mut child = Command::new("sleep")
774 .arg("30")
775 .current_dir(dir.path())
776 .spawn()
777 .unwrap();
778 let pid = child.id() as i32;
779 std::fs::write(
780 dir.path().join("postmaster.pid"),
781 format!("{pid}\n/tmp/data\n1\n54321\n"),
782 )
783 .unwrap();
784 let found = running_server(dir.path(), None).unwrap();
785 let start = process_start_time(pid);
786 let other = tempfile::tempdir().unwrap();
787 std::fs::copy(
788 dir.path().join("postmaster.pid"),
789 other.path().join("postmaster.pid"),
790 )
791 .unwrap();
792 let elsewhere = running_server(other.path(), None).unwrap();
793 child.kill().unwrap();
794 child.wait().unwrap();
795 assert_eq!(
796 found,
797 Some(RunningPg {
798 pid,
799 start_time: start.unwrap()
800 }),
801 "the recorded start time is the kernel's, not line 3 of the pid file"
802 );
803 assert_eq!(
804 elsewhere, None,
805 "same pid file, other folder: not its server"
806 );
807 assert_eq!(running_server(dir.path(), None).unwrap(), None, "exited");
808 }
809
810 #[test]
811 fn missing_or_malformed_postmaster_pid() {
812 let dir = tempfile::tempdir().unwrap();
813 assert!(read_postmaster_pid(dir.path()).unwrap().is_none());
814 std::fs::write(dir.path().join("postmaster.pid"), "oops\n").unwrap();
815 assert!(read_postmaster_pid(dir.path()).is_err());
816 for pid in ["0", "1", "-1"] {
817 std::fs::write(
818 dir.path().join("postmaster.pid"),
819 format!("{pid}\n/tmp/data\n1700000000\n54321\n"),
820 )
821 .unwrap();
822 assert!(
823 read_postmaster_pid(dir.path()).is_err(),
824 "pid {pid} must be rejected"
825 );
826 }
827 std::fs::write(
828 dir.path().join("postmaster.pid"),
829 "4242\n/tmp/data\nnotatime\n54321\n",
830 )
831 .unwrap();
832 assert!(read_postmaster_pid(dir.path()).is_err());
833 }
834
835 #[test]
836 fn log_tail_keeps_last_lines() {
837 let dir = tempfile::tempdir().unwrap();
838 let p = dir.path().join("pg.log");
839 let text: String = (1..=30).map(|i| format!("line {i}\n")).collect();
840 std::fs::write(&p, text).unwrap();
841 let tail = log_tail(&p, 20);
842 assert_eq!(tail.lines().count(), 20);
843 assert!(tail.starts_with("line 11") && tail.ends_with("line 30"));
844 assert_eq!(log_tail(&dir.path().join("none.log"), 20), "");
845 }
846
847 #[test]
848 fn socket_path_length_is_checked() {
849 let ok = PathBuf::from(format!("/s/{}", "a".repeat(82)));
851 assert!(check_socket_path(&ok, 54321).is_ok());
852 let edge = PathBuf::from(format!("/s/{}", "a".repeat(85))); assert!(check_socket_path(&edge, 54321).is_ok());
854 let long = PathBuf::from(format!("/s/{}", "a".repeat(86))); let e = check_socket_path(&long, 54321).unwrap_err();
856 assert_eq!(e.code, ErrorCode::PostgresStartFailed);
857 assert!(
858 e.message
859 .starts_with("socket path too long (104 bytes, max 103): /s/"),
860 "{}",
861 e.message
862 );
863 assert_eq!(
864 e.hint.as_deref(),
865 Some("set MITOSIS_HOME to a shorter path")
866 );
867 }
868
869 #[test]
870 fn start_failure_classifies_port_conflicts() {
871 let e = start_failure(
872 "FATAL: could not bind IPv4 address \"127.0.0.1\": Address already in use",
873 54321,
874 );
875 assert_eq!(e.code, ErrorCode::PortInUse);
876 let e = start_failure("FATAL: data directory has wrong ownership", 54321);
877 assert_eq!(e.code, ErrorCode::PostgresStartFailed);
878 assert!(e.message.contains("wrong ownership"));
879 }
880
881 #[test]
882 fn current_try_tail_ignores_earlier_tries() {
883 let dir = tempfile::tempdir().unwrap();
884 let p = dir.path().join("pg.log");
885 std::fs::write(
886 &p,
887 "mitosis: start try 1\nFATAL: Address already in use\nmitosis: start try 2\nFATAL: other\n",
888 )
889 .unwrap();
890 assert_eq!(current_try_tail(&p, 20), "FATAL: other");
891 std::fs::write(&p, "no marker\n").unwrap();
892 assert_eq!(current_try_tail(&p, 20), "no marker");
893 }
894
895 #[test]
896 fn parse_control_state_cases() {
897 let sample = |state: &str| {
898 format!(
899 "pg_control version number: 1300\n\
900 Catalog version number: 202307071\n\
901 Database system identifier: 7300000000000000000\n\
902 Database cluster state: {state}\n\
903 pg_control last modified: Mon Oct 19 10:00:00 2026\n"
904 )
905 };
906 assert_eq!(
907 parse_control_state(&sample("shut down")),
908 Some(ControlState::ShutDown)
909 );
910 assert_eq!(
911 parse_control_state(&sample("shut down in recovery")),
912 Some(ControlState::ShutDown)
913 );
914 assert_eq!(
915 parse_control_state(&sample("in production")),
916 Some(ControlState::InProduction)
917 );
918 assert_eq!(
919 parse_control_state(&sample("in crash recovery")),
920 Some(ControlState::Other("in crash recovery".into()))
921 );
922 assert_eq!(
923 parse_control_state("pg_control version number: 1300\n"),
924 None
925 );
926 }
927
928 #[test]
929 fn make_standalone_comments_outside_includes() {
930 let tmp = tempfile::tempdir().unwrap();
931 let d = tmp.path();
932 std::fs::write(
933 d.join("postgresql.conf"),
934 format!(
935 "shared_buffers = 1GB\n#include 'commented.conf'\ninclude '/etc/pg/extra.conf'\n\
936 include_dir = '../conf.d' # up\ninclude_if_exists='/etc/pg/maybe.conf'\n\
937 include 'local.conf'\ninclude_dir '{}/conf.d'\nshared_preload_libraries = 'x'\n\
938 INCLUDE '/etc/pg/upper.conf'\n",
939 d.display()
940 ),
941 )
942 .unwrap();
943 std::fs::write(d.join("pg_ident.conf"), "").unwrap();
944
945 let warnings = make_standalone(d).unwrap();
946
947 assert_eq!(warnings.len(), 4, "{warnings:?}");
948 assert!(std::fs::read_to_string(d.join("postgresql.conf"))
949 .unwrap()
950 .contains("#mitosis: INCLUDE '/etc/pg/upper.conf'"));
951 let conf = std::fs::read_to_string(d.join("postgresql.conf")).unwrap();
952 assert!(conf.contains("#mitosis: include '/etc/pg/extra.conf'"));
953 assert!(conf.contains("#mitosis: include_dir = '../conf.d'"));
954 assert!(conf.contains("#mitosis: include_if_exists='/etc/pg/maybe.conf'"));
955 assert!(conf.contains("\ninclude 'local.conf'\n"));
957 assert!(conf.contains(&format!("\ninclude_dir '{}/conf.d'\n", d.display())));
958 assert!(conf.lines().any(|l| l == "shared_buffers = 1GB"));
959 assert!(conf.contains("\nshared_preload_libraries = 'x'\n"));
960
961 let off = disable_preload_libraries(d).unwrap();
962 assert_eq!(off, vec!["shared_preload_libraries = 'x'".to_string()]);
963 let conf = std::fs::read_to_string(d.join("postgresql.conf")).unwrap();
964 assert!(conf.contains("#mitosis: shared_preload_libraries = 'x'"));
965 assert!(disable_preload_libraries(d).unwrap().is_empty());
966 }
967
968 #[test]
969 fn missing_library_messages() {
970 assert!(is_missing_library(
971 "postgres exited during startup:\nFATAL: could not access file \"timescaledb\": No such file"
972 ));
973 assert!(is_missing_library("could not load library \"$libdir/x\""));
974 assert!(!is_missing_library("port 5432 is already in use"));
975 }
976}