From 2fdf195095f6c0636c268f0924e1cfbd0c9a9032 Mon Sep 17 00:00:00 2001 From: Duosion Date: Sun, 17 Aug 2025 23:11:31 -0500 Subject: [PATCH 01/15] Add Windows support. --- Cargo.lock | 25 ++++++++++++- Cargo.toml | 1 + src/daemon.rs | 91 +++++++++++++++++++++++++++++++----------------- src/lib.rs | 21 +++++++++-- src/run_db.rs | 5 +++ tests/startup.rs | 11 ++++-- 6 files changed, 116 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad22823..a17c9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1466,6 +1466,7 @@ dependencies = [ "tempfile", "tokio", "url", + "winapi", ] [[package]] @@ -2629,6 +2630,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 2091a80..41fae41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ libc = "^0.2" url = "^2.5" tokio = { version = "^1", features = ["full"] } clap = { version = "^4.4", features = ["derive"], optional = true } +winapi = { version = "0.3.9", features = ["wincon", "processenv"] } [dev-dependencies] # testing and examples diff --git a/src/daemon.rs b/src/daemon.rs index aa915a4..37be20e 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -2,9 +2,8 @@ use crate::{PgTempDB, PgTempDBBuilder}; use std::net::SocketAddr; -use tokio::io::AsyncWriteExt; +use tokio::io::{self, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; -use tokio::signal::unix::{signal, SignalKind}; #[cfg(feature = "cli")] /// Contains the clap args struct @@ -144,7 +143,7 @@ impl PgTempDaemon { /// Start the daemon, listening for either TCP connections on the configured port. The server /// shuts down when sent a SIGINT (e.g. via ctrl-C). - pub async fn start(mut self) { + pub async fn start(self) { let uri = self.conn_uri(); if self.single_mode { println!("starting pgtemp server in single mode at {}", uri); @@ -155,39 +154,33 @@ impl PgTempDaemon { let listener = TcpListener::bind(("127.0.0.1", self.port)) .await .expect("failed to bind to daemon port"); + self.listen(listener).await + } + + /// Main daemon listening loop for unix + #[cfg(unix)] + async fn listen(mut self, listener: TcpListener) { + use tokio::signal::unix::{signal, SignalKind}; + let mut sig = signal(SignalKind::interrupt()).expect("failed to hook to interrupt signal"); loop { tokio::select! { - res = listener.accept() => { - if let Ok((client_conn, client_addr)) = res { - client_conn.set_nodelay(true).expect("failed to set nodelay on client connection"); - let db: Option; - let db_port: u16; - if self.single_mode { - db = None; - db_port = self.dbs[0].db_port(); - } - else { - let take_db = self.dbs.pop().unwrap(); - db_port = take_db.db_port(); - db = Some(take_db); - } - let db_conn = TcpStream::connect(("127.0.0.1", db_port)) - .await - .expect("failed to connect to postgres server"); - db_conn - .set_nodelay(true) - .expect("failed to set nodelay on db connection"); - tokio::spawn(async move { proxy_connection(db, db_conn, client_conn, client_addr).await }); - // preallocate a new db after one is used - if self.dbs.is_empty() && !self.single_mode { - self.allocate_db().await; - } - } - else { - println!("idk when this errs"); - } + res = listener.accept() => self.on_listener_accept(res).await, + _sig_event = sig.recv() => { + println!("got interrupt, exiting"); + break; } + } + } + } + + /// Main daemon listening loop for windows + #[cfg(windows)] + async fn listen(mut self, listener: TcpListener) { + let mut sig = tokio::signal::windows::ctrl_c().expect("failed to hook windows interrupt signal"); + loop { + tokio::select! { + res = listener.accept() => self.on_listener_accept(res).await, _sig_event = sig.recv() => { println!("got interrupt, exiting"); break; @@ -195,6 +188,40 @@ impl PgTempDaemon { } } } + + /// Called when a connection is accepted from a TcpListener. + async fn on_listener_accept(&mut self, result: io::Result<(TcpStream, SocketAddr)>) { + if let Ok((client_conn, client_addr)) = result { + client_conn + .set_nodelay(true) + .expect("failed to set nodelay on client connection"); + let db: Option; + let db_port: u16; + if self.single_mode { + db = None; + db_port = self.dbs[0].db_port(); + } else { + let take_db = self.dbs.pop().unwrap(); + db_port = take_db.db_port(); + db = Some(take_db); + } + let db_conn = TcpStream::connect(("127.0.0.1", db_port)) + .await + .expect("failed to connect to postgres server"); + db_conn + .set_nodelay(true) + .expect("failed to set nodelay on db connection"); + tokio::spawn( + async move { proxy_connection(db, db_conn, client_conn, client_addr).await }, + ); + // preallocate a new db after one is used + if self.dbs.is_empty() && !self.single_mode { + self.allocate_db().await; + } + } else { + println!("idk when this errs"); + } + } } /// When we're in single mode, we pass None to the db here so it doesn't get deallocated when the diff --git a/src/lib.rs b/src/lib.rs index 0db4c38..5bfe815 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,10 +157,16 @@ impl PgTempDB { self.dump_database(path); } - let postgres_process = self + #[cfg(unix)] + let postgres_process: Child; + #[cfg(windows)] + let mut postgres_process: Child; + + postgres_process = self .postgres_process .take() .expect("shutdown with no postgres process"); + let temp_dir = self.temp_dir.take().unwrap(); // fast (not graceful) shutdown via SIGINT @@ -173,7 +179,16 @@ impl PgTempDB { // the postgres server says "we're still connected to a client, can't shut down yet" and we // have a deadlock. #[allow(clippy::cast_possible_wrap)] - let _ret = unsafe { libc::kill(postgres_process.id() as i32, libc::SIGINT) }; + #[cfg(unix)] + { + unsafe { libc::kill(postgres_process.id() as i32, libc::SIGINT); } + } + #[cfg(windows)] + { + postgres_process.kill() + .expect("failed to kill postgress process"); + } + let _output = postgres_process .wait_with_output() .expect("postgres server failed to exit cleanly"); @@ -463,4 +478,4 @@ fn get_unused_port() -> u16 { sock.local_addr() .expect("failed to get local addr from socket") .port() -} +} \ No newline at end of file diff --git a/src/run_db.rs b/src/run_db.rs index f269162..f606af2 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -9,9 +9,14 @@ use crate::PgTempDBBuilder; const CREATEDB_MAX_TRIES: u32 = 10; const CREATEDB_RETRY_DELAY: Duration = Duration::from_millis(100); +#[cfg(unix)] fn current_user_is_root() -> bool { unsafe { libc::getuid() == 0 } } +#[cfg(target_os = "windows")] +fn current_user_is_root() -> bool { + false +} /// Execute the `initdb` binary with the parameters configured in PgTempDBBuilder. pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { diff --git a/tests/startup.rs b/tests/startup.rs index 3f6f1ca..00e6d49 100644 --- a/tests/startup.rs +++ b/tests/startup.rs @@ -1,7 +1,12 @@ //! Basic startup/shutdown tests -use pgtemp::{PgTempDB, PgTempDBBuilder}; +use pgtemp::{PgTempDB}; + +#[cfg(unix)] +use pgtemp::PgTempDBBuilder; +#[cfg(unix)] use std::{io::Write, os::unix::fs::OpenOptionsExt}; +#[cfg(unix)] use tempfile::TempDir; #[test] @@ -67,6 +72,7 @@ fn test_tempdb_bringup_shutdown_persist() { #[test] #[should_panic(expected = "this is not initdb")] +#[cfg(unix)] /// Start a database by specifying the bin_path fn test_tempdb_bin_path() { use std::io::Write; @@ -88,6 +94,7 @@ fn test_tempdb_bin_path() { } #[test] +#[cfg(unix)] fn test_slow_postgres_startup() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let dir_path = temp_dir.path().to_owned(); @@ -112,4 +119,4 @@ fn test_slow_postgres_startup() { .with_bin_path(dir_path) .with_dbname("unique_db_name") .start(); -} +} \ No newline at end of file From 2c1c62fcdfa2cfd1698fd59499ae6b0b84170781 Mon Sep 17 00:00:00 2001 From: Duosion Date: Sun, 17 Aug 2025 23:32:51 -0500 Subject: [PATCH 02/15] Remove winapi dependency. --- Cargo.lock | 23 ----------------------- Cargo.toml | 1 - 2 files changed, 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a17c9e6..f217ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1466,7 +1466,6 @@ dependencies = [ "tempfile", "tokio", "url", - "winapi", ] [[package]] @@ -2630,28 +2629,6 @@ dependencies = [ "wasite", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 41fae41..2091a80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ libc = "^0.2" url = "^2.5" tokio = { version = "^1", features = ["full"] } clap = { version = "^4.4", features = ["derive"], optional = true } -winapi = { version = "0.3.9", features = ["wincon", "processenv"] } [dev-dependencies] # testing and examples From 94714aa6d9524ea621e085ecee9577b37665c6ae Mon Sep 17 00:00:00 2001 From: Duosion Date: Tue, 19 Aug 2025 21:58:42 -0500 Subject: [PATCH 03/15] Begin implementation of build-test windows test workflow. --- .github/workflows/build-test.yaml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 1e96e81..7b9497a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -18,12 +18,26 @@ jobs: run: make lint test: name: Unit Tests - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - name: Install postgres - run: sudo apt-get install postgresql postgresql-client + run: | + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + sudo apt-get install postgresql postgresql-client + else + winget install PostgreSQL.PostgreSQL.16 + winget install GnuWin32.Make + fi - name: Update path - run: find /usr/lib/postgresql/ -type d -name "bin" >> $GITHUB_PATH + run: | + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + find /usr/lib/postgresql/ -type d -name "bin" >> $GITHUB_PATH + else + echo todo + fi - name: Checkout uses: actions/checkout@v4 - name: Run tests From b37d2d27d7423eecd39a08bcfefd590931523b66 Mon Sep 17 00:00:00 2001 From: Duosion Date: Tue, 19 Aug 2025 22:01:17 -0500 Subject: [PATCH 04/15] Change shell type to bash in unit test workflow. --- .github/workflows/build-test.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 7b9497a..4b1b3f2 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -24,6 +24,7 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Install postgres + shell: bash run: | if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then sudo apt-get install postgresql postgresql-client @@ -31,13 +32,16 @@ jobs: winget install PostgreSQL.PostgreSQL.16 winget install GnuWin32.Make fi + - name: Update path + shell: bash run: | if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then find /usr/lib/postgresql/ -type d -name "bin" >> $GITHUB_PATH else echo todo fi + - name: Checkout uses: actions/checkout@v4 - name: Run tests From 3db33c1a76b3d7d4a98043163db3c8d7bb907486 Mon Sep 17 00:00:00 2001 From: Duosion Date: Tue, 19 Aug 2025 22:04:21 -0500 Subject: [PATCH 05/15] Use windows 2025 runner. --- .github/workflows/build-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 4b1b3f2..f40df44 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-2025] steps: - name: Install postgres shell: bash @@ -41,7 +41,7 @@ jobs: else echo todo fi - + - name: Checkout uses: actions/checkout@v4 - name: Run tests From c9db82191908a24050b3f492f194a32489b5e36e Mon Sep 17 00:00:00 2001 From: Duosion Date: Tue, 19 Aug 2025 22:08:10 -0500 Subject: [PATCH 06/15] Accept agreements. --- .github/workflows/build-test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index f40df44..439495a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -29,8 +29,8 @@ jobs: if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then sudo apt-get install postgresql postgresql-client else - winget install PostgreSQL.PostgreSQL.16 - winget install GnuWin32.Make + winget install PostgreSQL.PostgreSQL.16 --accept-package-agreements --accept-source-agreements + winget install GnuWin32.Make --accept-package-agreements --accept-source-agreements fi - name: Update path From 7aea9cf641c7bb4c6df56f3ba673ef5c1b5e6db2 Mon Sep 17 00:00:00 2001 From: Duosion Date: Wed, 20 Aug 2025 20:20:50 -0500 Subject: [PATCH 07/15] Update windows test job, cargo fmt. --- .github/workflows/build-test.yaml | 40 ++++++++++++++----------------- src/daemon.rs | 3 ++- src/lib.rs | 11 +++++---- tests/startup.rs | 4 ++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 439495a..9e5d84d 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -16,32 +16,28 @@ jobs: run: cargo fmt --all -- --check - name: Run cargo clippy run: make lint - test: - name: Unit Tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-2025] + + test-windows: + name: Windows Unit Tests + runs-on: windows-latest steps: - name: Install postgres - shell: bash - run: | - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - sudo apt-get install postgresql postgresql-client - else - winget install PostgreSQL.PostgreSQL.16 --accept-package-agreements --accept-source-agreements - winget install GnuWin32.Make --accept-package-agreements --accept-source-agreements - fi + uses: tj-actions/install-postgresql@v3 + with: + postgresql-version: 17 + - name: Checkout + uses: actions/checkout@v4 + - name: Run tests + run: make test-ci + test-unix: + name: Unix Unit Tests + runs-on: ubuntu-latest + steps: + - name: Install postgres + run: sudo apt-get install postgresql postgresql-client - name: Update path - shell: bash - run: | - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - find /usr/lib/postgresql/ -type d -name "bin" >> $GITHUB_PATH - else - echo todo - fi - + run: find /usr/lib/postgresql/ -type d -name "bin" >> $GITHUB_PATH - name: Checkout uses: actions/checkout@v4 - name: Run tests diff --git a/src/daemon.rs b/src/daemon.rs index 37be20e..b1f9903 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -177,7 +177,8 @@ impl PgTempDaemon { /// Main daemon listening loop for windows #[cfg(windows)] async fn listen(mut self, listener: TcpListener) { - let mut sig = tokio::signal::windows::ctrl_c().expect("failed to hook windows interrupt signal"); + let mut sig = + tokio::signal::windows::ctrl_c().expect("failed to hook windows interrupt signal"); loop { tokio::select! { res = listener.accept() => self.on_listener_accept(res).await, diff --git a/src/lib.rs b/src/lib.rs index 5bfe815..558dbf2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,7 +166,7 @@ impl PgTempDB { .postgres_process .take() .expect("shutdown with no postgres process"); - + let temp_dir = self.temp_dir.take().unwrap(); // fast (not graceful) shutdown via SIGINT @@ -181,11 +181,14 @@ impl PgTempDB { #[allow(clippy::cast_possible_wrap)] #[cfg(unix)] { - unsafe { libc::kill(postgres_process.id() as i32, libc::SIGINT); } + unsafe { + libc::kill(postgres_process.id() as i32, libc::SIGINT); + } } #[cfg(windows)] { - postgres_process.kill() + postgres_process + .kill() .expect("failed to kill postgress process"); } @@ -478,4 +481,4 @@ fn get_unused_port() -> u16 { sock.local_addr() .expect("failed to get local addr from socket") .port() -} \ No newline at end of file +} diff --git a/tests/startup.rs b/tests/startup.rs index 00e6d49..a3a1438 100644 --- a/tests/startup.rs +++ b/tests/startup.rs @@ -1,6 +1,6 @@ //! Basic startup/shutdown tests -use pgtemp::{PgTempDB}; +use pgtemp::PgTempDB; #[cfg(unix)] use pgtemp::PgTempDBBuilder; @@ -119,4 +119,4 @@ fn test_slow_postgres_startup() { .with_bin_path(dir_path) .with_dbname("unique_db_name") .start(); -} \ No newline at end of file +} From 607ff2aa40768079bc790557950ad693cb306617 Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 00:13:27 -0500 Subject: [PATCH 08/15] Use pg_ctl instead of direct postgres commands. --- src/lib.rs | 19 +++---- src/run_db.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 150 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 558dbf2..585c318 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,16 +157,12 @@ impl PgTempDB { self.dump_database(path); } - #[cfg(unix)] - let postgres_process: Child; - #[cfg(windows)] - let mut postgres_process: Child; - - postgres_process = self + let postgres_process = self .postgres_process .take() .expect("shutdown with no postgres process"); + let data_dir = self.data_dir(); let temp_dir = self.temp_dir.take().unwrap(); // fast (not graceful) shutdown via SIGINT @@ -187,9 +183,14 @@ impl PgTempDB { } #[cfg(windows)] { - postgres_process - .kill() - .expect("failed to kill postgress process"); + use std::process::Command; + + Command::new("pg_ctl") + .arg("stop") + .arg("-D") + .arg(data_dir) + .output() + .expect("Failed to stop server with pg_ctl. Is it installed and on your path?"); } let _output = postgres_process diff --git a/src/run_db.rs b/src/run_db.rs index f606af2..bacdfe6 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -1,6 +1,5 @@ use std::{ - process::{Child, Command}, - time::Duration, + env::args, process::{Child, Command}, time::Duration }; use tempfile::TempDir; @@ -18,7 +17,6 @@ fn current_user_is_root() -> bool { false } -/// Execute the `initdb` binary with the parameters configured in PgTempDBBuilder. pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { let temp_dir = { if let Some(base_dir) = builder.temp_dir_prefix.clone() { @@ -28,6 +26,53 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { } }; + let data_dir = temp_dir.path().join("pg_data_dir"); + let data_dir_str = data_dir.to_str().unwrap(); + + let user = builder.get_user(); + let password = builder.get_password(); + + // write out password file for initdb + let pwfile = temp_dir.path().join("user_password.txt"); + let pwfile_str = pwfile.to_str().unwrap(); + std::fs::write(&pwfile, password).expect("failed to write password file"); + + let pg_ctl_path = builder + .bin_path + .as_ref() + .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); + + let initdb_output = Command::new(pg_ctl_path) + .arg("init") + .args(["-D", data_dir_str]) + .arg("-o") + .arg(format!("-N --username {} --pwfile {}", user, pwfile_str)) + .output() + .expect("Failed to start pg_ctl. Is it installed and on your path?"); + + if !initdb_output.status.success() { + let stdout = initdb_output.stdout; + let stderr = initdb_output.stderr; + panic!( + "initdb failed! stdout: {}\n\nstderr: {}", + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr) + ); + } + + temp_dir +} + +/// Execute the `initdb` binary with the parameters configured in PgTempDBBuilder. +pub fn unix_init_db(builder: &mut PgTempDBBuilder) -> TempDir { + let temp_dir = { + if let Some(base_dir) = builder.temp_dir_prefix.clone() { + TempDir::with_prefix_in("pgtemp-", base_dir).expect("failed to create tempdir") + } else { + TempDir::with_prefix("pgtemp-").expect("failed to create tempdir") + } + }; + // if current user is root, data dir etc need to be owned by postgres user if current_user_is_root() { // TODO: don't shell out to chown, get the userid of postgres and just call std::os @@ -100,6 +145,98 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { let data_dir_str = data_dir.to_str().unwrap(); let port = builder.get_port_or_set_random(); + // postgres will not run as root, so try to run as postgres if we are root + let pg_ctl_path = builder + .bin_path + .as_ref() + .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); + + // build postgres command args + let mut pg_cmd_args = vec![ + format!("-c unix_socket_directories={}", data_dir_str), + format!("-c port={}", port), + "-c fsync=off".into(), + "-c synchronous_commit=off".into(), + "-c full_page_writes=off".into(), + "-c autovacuum=off".into(), + ]; + for (key, val) in &builder.server_configs { + pg_cmd_args.push(format!("-c {}={}", key, val)); + } + + let postgres_server_process = Command::new(pg_ctl_path) + .arg("start") + .args(["-D", data_dir_str]) + .args([ + "-o", + &pg_cmd_args.join(" ") + ]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("Failed to start postgres. Is it installed and on your path?"); + + std::thread::sleep(CREATEDB_RETRY_DELAY); + + let user = builder.get_user(); + //let password = builder.get_password(); + let port = builder.get_port_or_set_random(); + let dbname = builder.get_dbname(); + + if dbname != "postgres" { + // TODO: don't use createdb, connect directly to the db and run CREATE DATABASE. removes + // dependency on OS package which is often separate from postgres server package (at + // expense of adding Cargo dependency) + // + // alternatively just use psql + let createdb_path = builder + .bin_path + .as_ref() + .map_or("createdb".into(), |p| p.join("createdb")); + let mut createdb_last_error_output = None; + + for _ in 0..CREATEDB_MAX_TRIES { + let mut dbcmd = Command::new(createdb_path.clone()); + dbcmd + .args(["--host", "localhost"]) + .args(["--port", &port.to_string()]) + .args(["--username", &user]) + // TODO: use template in pgtemp daemon single-cluster mode? + //.args(["--template", "..."] + // TODO: since we trust local users by default we don't actually + // need the password but we should provide it anyway since we + // provide it everywhere else + .arg("--no-password") + .arg(&dbname); + + let output = dbcmd.output().expect("Failed to start createdb. Is it installed and on your path? It's typically part of the postgres-libs or postgres-client package."); + if output.status.success() { + createdb_last_error_output = None; + break; + } + createdb_last_error_output = Some(output); + std::thread::sleep(CREATEDB_RETRY_DELAY); + } + + if let Some(output) = createdb_last_error_output { + let stdout = output.stdout; + let stderr = output.stderr; + panic!( + "createdb failed! stdout: {}\n\nstderr: {}", + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr) + ); + } + } + + postgres_server_process +} + +pub fn unix_run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { + let data_dir = temp_dir.path().join("pg_data_dir"); + let data_dir_str = data_dir.to_str().unwrap(); + let port = builder.get_port_or_set_random(); + // postgres will not run as root, so try to run as postgres if we are root let postgres_path = builder .bin_path From 6f0e3fbeda008fd3b55334fc5aec188961975398 Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 00:41:26 -0500 Subject: [PATCH 09/15] Reimplement unix permission handling in `run_db.rs`. Wait for the DB to be ready in `run_db` --- src/run_db.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/src/run_db.rs b/src/run_db.rs index bacdfe6..bd6e83a 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -1,5 +1,7 @@ +#[cfg(windows)] +use std::ffi::OsStr; use std::{ - env::args, process::{Child, Command}, time::Duration + path::PathBuf, process::{Child, Command, Output}, time::Duration }; use tempfile::TempDir; @@ -12,11 +14,31 @@ const CREATEDB_RETRY_DELAY: Duration = Duration::from_millis(100); fn current_user_is_root() -> bool { unsafe { libc::getuid() == 0 } } -#[cfg(target_os = "windows")] -fn current_user_is_root() -> bool { - false + +#[cfg(unix)] +fn get_command(program: S) -> Command +where + S: AsRef, +{ + // postgres will not run as root, so try to run initdb as postgres user if we are root so that + // when running the server as the postgres user it can access the files + if current_user_is_root() { + let mut cmd = Command::new("sudo"); + cmd.args(["-u", "postgres"]).arg(program); + cmd + } else { + Command::new(program) + } +} +#[cfg(windows)] +fn get_command(program: S) -> Command +where + S: AsRef, +{ + Command::new(program) } +/// Execute the `initdb` binary with the parameters configured in PgTempDBBuilder. pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { let temp_dir = { if let Some(base_dir) = builder.temp_dir_prefix.clone() { @@ -26,6 +48,25 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { } }; + // if current user is root, data dir etc need to be owned by postgres user + #[cfg(unix)] + if current_user_is_root() { + // TODO: don't shell out to chown, get the userid of postgres and just call std::os + let chown_output = Command::new("chown") + .args(["-R", "postgres", temp_dir.path().to_str().unwrap()]) + .output() + .expect("failed to chown data dir to postgres user"); + if !chown_output.status.success() { + let stdout = chown_output.stdout; + let stderr = chown_output.stderr; + panic!( + "chowning data dir failed! stdout: {}\n\nstderr: {}", + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr) + ); + } + } + let data_dir = temp_dir.path().join("pg_data_dir"); let data_dir_str = data_dir.to_str().unwrap(); @@ -46,7 +87,7 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { .arg("init") .args(["-D", data_dir_str]) .arg("-o") - .arg(format!("-N --username {} --pwfile {}", user, pwfile_str)) + .arg(format!("-N --username {} --pwfile {}", user, pwfile_str)) // "-N" = no fsync, starts slightly faster .output() .expect("Failed to start pg_ctl. Is it installed and on your path?"); @@ -63,6 +104,7 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { temp_dir } +#[cfg(unix)] /// Execute the `initdb` binary with the parameters configured in PgTempDBBuilder. pub fn unix_init_db(builder: &mut PgTempDBBuilder) -> TempDir { let temp_dir = { @@ -140,6 +182,33 @@ pub fn unix_init_db(builder: &mut PgTempDBBuilder) -> TempDir { temp_dir } +/// waits for a db to be ready +/// if the server never started, returns the last command output +fn wait_for_db_ready(bin_path: &Option, port: u16, max_retries: u32, retry_delay: Duration) -> Option { + let mut isready_last_error_output = None; + let isready_path = bin_path + .as_ref() + .map_or("pg_isready".into(), |p| p.join("pg_isready")); + + let mut server_status_cmd = Command::new(&isready_path); + server_status_cmd.args(["-p", &port.to_string()]); + + for _ in 0..max_retries { + let server_status = server_status_cmd + .output() + .unwrap(); + + if server_status.status.success() { + isready_last_error_output = None; + break; + } + isready_last_error_output = Some(server_status); + std::thread::sleep(retry_delay); + } + + isready_last_error_output +} + pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { let data_dir = temp_dir.path().join("pg_data_dir"); let data_dir_str = data_dir.to_str().unwrap(); @@ -164,19 +233,17 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { pg_cmd_args.push(format!("-c {}={}", key, val)); } - let postgres_server_process = Command::new(pg_ctl_path) + let postgres_server_process = get_command(pg_ctl_path) .arg("start") .args(["-D", data_dir_str]) - .args([ - "-o", - &pg_cmd_args.join(" ") - ]) + .args(["-o", &pg_cmd_args.join(" ")]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .expect("Failed to start postgres. Is it installed and on your path?"); - std::thread::sleep(CREATEDB_RETRY_DELAY); + // wait for db to be started + wait_for_db_ready(&builder.bin_path, port, CREATEDB_MAX_TRIES, CREATEDB_RETRY_DELAY); let user = builder.get_user(); //let password = builder.get_password(); @@ -232,6 +299,7 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { postgres_server_process } +#[cfg(unix)] pub fn unix_run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { let data_dir = temp_dir.path().join("pg_data_dir"); let data_dir_str = data_dir.to_str().unwrap(); From 93ee4671f814cbad31dfdb690a0898a24a180144 Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 00:44:34 -0500 Subject: [PATCH 10/15] Import OsStr on unix. --- src/run_db.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/run_db.rs b/src/run_db.rs index bd6e83a..a60ab29 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -1,4 +1,3 @@ -#[cfg(windows)] use std::ffi::OsStr; use std::{ path::PathBuf, process::{Child, Command, Output}, time::Duration From f3e1911e3c060a68e214ce8a57f3b87a398de7de Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 02:33:24 -0500 Subject: [PATCH 11/15] Install python in windows unit test job. --- .github/workflows/build-test.yaml | 4 + src/run_db.rs | 261 ++++++++---------------------- 2 files changed, 75 insertions(+), 190 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 9e5d84d..14c5c8b 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -21,6 +21,10 @@ jobs: name: Windows Unit Tests runs-on: windows-latest steps: + - name: Install python + uses: actions/setup-python@v5 + with: + python-version: '3.13' - name: Install postgres uses: tj-actions/install-postgresql@v3 with: diff --git a/src/run_db.rs b/src/run_db.rs index a60ab29..e4ca2e6 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -1,6 +1,8 @@ use std::ffi::OsStr; use std::{ - path::PathBuf, process::{Child, Command, Output}, time::Duration + path::PathBuf, + process::{Child, Command, Output}, + time::Duration, }; use tempfile::TempDir; @@ -47,73 +49,7 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { } }; - // if current user is root, data dir etc need to be owned by postgres user #[cfg(unix)] - if current_user_is_root() { - // TODO: don't shell out to chown, get the userid of postgres and just call std::os - let chown_output = Command::new("chown") - .args(["-R", "postgres", temp_dir.path().to_str().unwrap()]) - .output() - .expect("failed to chown data dir to postgres user"); - if !chown_output.status.success() { - let stdout = chown_output.stdout; - let stderr = chown_output.stderr; - panic!( - "chowning data dir failed! stdout: {}\n\nstderr: {}", - String::from_utf8_lossy(&stdout), - String::from_utf8_lossy(&stderr) - ); - } - } - - let data_dir = temp_dir.path().join("pg_data_dir"); - let data_dir_str = data_dir.to_str().unwrap(); - - let user = builder.get_user(); - let password = builder.get_password(); - - // write out password file for initdb - let pwfile = temp_dir.path().join("user_password.txt"); - let pwfile_str = pwfile.to_str().unwrap(); - std::fs::write(&pwfile, password).expect("failed to write password file"); - - let pg_ctl_path = builder - .bin_path - .as_ref() - .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); - - let initdb_output = Command::new(pg_ctl_path) - .arg("init") - .args(["-D", data_dir_str]) - .arg("-o") - .arg(format!("-N --username {} --pwfile {}", user, pwfile_str)) // "-N" = no fsync, starts slightly faster - .output() - .expect("Failed to start pg_ctl. Is it installed and on your path?"); - - if !initdb_output.status.success() { - let stdout = initdb_output.stdout; - let stderr = initdb_output.stderr; - panic!( - "initdb failed! stdout: {}\n\nstderr: {}", - String::from_utf8_lossy(&stdout), - String::from_utf8_lossy(&stderr) - ); - } - - temp_dir -} - -#[cfg(unix)] -/// Execute the `initdb` binary with the parameters configured in PgTempDBBuilder. -pub fn unix_init_db(builder: &mut PgTempDBBuilder) -> TempDir { - let temp_dir = { - if let Some(base_dir) = builder.temp_dir_prefix.clone() { - TempDir::with_prefix_in("pgtemp-", base_dir).expect("failed to create tempdir") - } else { - TempDir::with_prefix("pgtemp-").expect("failed to create tempdir") - } - }; - // if current user is root, data dir etc need to be owned by postgres user if current_user_is_root() { // TODO: don't shell out to chown, get the userid of postgres and just call std::os @@ -150,13 +86,7 @@ pub fn unix_init_db(builder: &mut PgTempDBBuilder) -> TempDir { // postgres will not run as root, so try to run initdb as postgres user if we are root so that // when running the server as the postgres user it can access the files - let mut cmd: Command; - if current_user_is_root() { - cmd = Command::new("sudo"); - cmd.args(["-u", "postgres"]).arg(initdb_path); - } else { - cmd = Command::new(initdb_path); - } + let mut cmd: Command = get_command(initdb_path); cmd.args(["-D", data_dir_str]) .arg("-N") // no fsync, starts slightly faster @@ -183,19 +113,22 @@ pub fn unix_init_db(builder: &mut PgTempDBBuilder) -> TempDir { /// waits for a db to be ready /// if the server never started, returns the last command output -fn wait_for_db_ready(bin_path: &Option, port: u16, max_retries: u32, retry_delay: Duration) -> Option { +fn wait_for_db_ready( + bin_path: &Option, + port: u16, + max_retries: u32, + retry_delay: Duration, +) -> Option { let mut isready_last_error_output = None; let isready_path = bin_path .as_ref() .map_or("pg_isready".into(), |p| p.join("pg_isready")); - + let mut server_status_cmd = Command::new(&isready_path); server_status_cmd.args(["-p", &port.to_string()]); for _ in 0..max_retries { - let server_status = server_status_cmd - .output() - .unwrap(); + let server_status = server_status_cmd.output().unwrap(); if server_status.status.success() { isready_last_error_output = None; @@ -214,123 +147,56 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { let port = builder.get_port_or_set_random(); // postgres will not run as root, so try to run as postgres if we are root - let pg_ctl_path = builder - .bin_path - .as_ref() - .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); - - // build postgres command args - let mut pg_cmd_args = vec![ - format!("-c unix_socket_directories={}", data_dir_str), - format!("-c port={}", port), - "-c fsync=off".into(), - "-c synchronous_commit=off".into(), - "-c full_page_writes=off".into(), - "-c autovacuum=off".into(), - ]; - for (key, val) in &builder.server_configs { - pg_cmd_args.push(format!("-c {}={}", key, val)); - } - - let postgres_server_process = get_command(pg_ctl_path) - .arg("start") - .args(["-D", data_dir_str]) - .args(["-o", &pg_cmd_args.join(" ")]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - .expect("Failed to start postgres. Is it installed and on your path?"); - - // wait for db to be started - wait_for_db_ready(&builder.bin_path, port, CREATEDB_MAX_TRIES, CREATEDB_RETRY_DELAY); - - let user = builder.get_user(); - //let password = builder.get_password(); - let port = builder.get_port_or_set_random(); - let dbname = builder.get_dbname(); + let mut pgcmd: Command; - if dbname != "postgres" { - // TODO: don't use createdb, connect directly to the db and run CREATE DATABASE. removes - // dependency on OS package which is often separate from postgres server package (at - // expense of adding Cargo dependency) - // - // alternatively just use psql - let createdb_path = builder + #[cfg(unix)] + { + let postgres_path = builder .bin_path .as_ref() - .map_or("createdb".into(), |p| p.join("createdb")); - let mut createdb_last_error_output = None; - - for _ in 0..CREATEDB_MAX_TRIES { - let mut dbcmd = Command::new(createdb_path.clone()); - dbcmd - .args(["--host", "localhost"]) - .args(["--port", &port.to_string()]) - .args(["--username", &user]) - // TODO: use template in pgtemp daemon single-cluster mode? - //.args(["--template", "..."] - // TODO: since we trust local users by default we don't actually - // need the password but we should provide it anyway since we - // provide it everywhere else - .arg("--no-password") - .arg(&dbname); - - let output = dbcmd.output().expect("Failed to start createdb. Is it installed and on your path? It's typically part of the postgres-libs or postgres-client package."); - if output.status.success() { - createdb_last_error_output = None; - break; - } - createdb_last_error_output = Some(output); - std::thread::sleep(CREATEDB_RETRY_DELAY); - } - - if let Some(output) = createdb_last_error_output { - let stdout = output.stdout; - let stderr = output.stderr; - panic!( - "createdb failed! stdout: {}\n\nstderr: {}", - String::from_utf8_lossy(&stdout), - String::from_utf8_lossy(&stderr) - ); + .map_or("postgres".into(), |p| p.join("postgres")); + pgcmd = get_command(postgres_path); + + pgcmd + .args(["-c", &format!("unix_socket_directories={}", data_dir_str)]) + .args(["-c", &format!("port={port}")]) + // https://www.postgresql.org/docs/current/non-durability.html + // https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server + .args(["-c", "fsync=off"]) + .args(["-c", "synchronous_commit=off"]) + .args(["-c", "full_page_writes=off"]) + .args(["-c", "autovacuum=off"]) + .args(["-D", data_dir.to_str().unwrap()]); + for (key, val) in &builder.server_configs { + pgcmd.args(["-c", &format!("{}={}", key, val)]); } } + #[cfg(windows)] + { + let pg_ctl_path = builder + .bin_path + .as_ref() + .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); + + // build postgres command args + let mut pg_cmd_args = vec![ + format!("-c unix_socket_directories={}", data_dir_str), + format!("-c port={}", port), + "-c fsync=off".into(), + "-c synchronous_commit=off".into(), + "-c full_page_writes=off".into(), + "-c autovacuum=off".into(), + ]; + for (key, val) in &builder.server_configs { + pg_cmd_args.push(format!("-c {}={}", key, val)); + } - postgres_server_process -} - -#[cfg(unix)] -pub fn unix_run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { - let data_dir = temp_dir.path().join("pg_data_dir"); - let data_dir_str = data_dir.to_str().unwrap(); - let port = builder.get_port_or_set_random(); - - // postgres will not run as root, so try to run as postgres if we are root - let postgres_path = builder - .bin_path - .as_ref() - .map_or("postgres".into(), |p| p.join("postgres")); - let mut pgcmd: Command; - if current_user_is_root() { - pgcmd = Command::new("sudo"); - pgcmd.args(["-u", "postgres"]).arg(postgres_path); - } else { - pgcmd = Command::new(postgres_path); - }; - - pgcmd - .args(["-c", &format!("unix_socket_directories={}", data_dir_str)]) - .args(["-c", &format!("port={port}")]) - // https://www.postgresql.org/docs/current/non-durability.html - // https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server - .args(["-c", "fsync=off"]) - .args(["-c", "synchronous_commit=off"]) - .args(["-c", "full_page_writes=off"]) - .args(["-c", "autovacuum=off"]) - .args(["-D", data_dir.to_str().unwrap()]); - for (key, val) in &builder.server_configs { - pgcmd.args(["-c", &format!("{}={}", key, val)]); + pgcmd = get_command(pg_ctl_path); + pgcmd.arg("start") + .args(["-D", data_dir_str]) + .args(["-o", &pg_cmd_args.join(" ")]); } - + // don't output postgres output to stdout/stderr pgcmd .stdout(std::process::Stdio::piped()) @@ -340,7 +206,22 @@ pub fn unix_run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { .spawn() .expect("Failed to start postgres. Is it installed and on your path?"); - std::thread::sleep(CREATEDB_RETRY_DELAY); + // wait for db to be started + let db_ready_error = wait_for_db_ready( + &builder.bin_path, + port, + CREATEDB_MAX_TRIES, + CREATEDB_RETRY_DELAY, + ); + if let Some(output) = db_ready_error { + let stdout = output.stdout; + let stderr = output.stderr; + panic!( + "db did not start! stdout: {}\n\nstderr: {}", + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr) + ); + } let user = builder.get_user(); //let password = builder.get_password(); From e881ebe0603ca5153d60e3178f142106baf9aa16 Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 02:39:04 -0500 Subject: [PATCH 12/15] Do not wait for db ready on unix. --- src/lib.rs | 11 ++++++++++- src/run_db.rs | 36 +++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 585c318..ab5de10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,8 @@ pub struct PgTempDB { // See shutdown implementation for why these are options temp_dir: Option, postgres_process: Option, + /// Prefix PostgreSQL binary names (`initdb`, `createdb`, and `postgres`) with this path, instead of searching $PATH + bin_path: Option, } impl PgTempDB { @@ -49,6 +51,7 @@ impl PgTempDB { let persist = builder.persist_data_dir; let dump_path = builder.dump_path.clone(); let load_path = builder.load_path.clone(); + let bin_path = builder.bin_path.clone(); let temp_dir = run_db::init_db(&mut builder); let postgres_process = Some(run_db::run_db(&temp_dir, builder)); @@ -63,6 +66,7 @@ impl PgTempDB { dump_path, temp_dir, postgres_process, + bin_path }; if let Some(path) = load_path { @@ -185,7 +189,12 @@ impl PgTempDB { { use std::process::Command; - Command::new("pg_ctl") + let pg_ctl_path = self + .bin_path + .as_ref() + .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); + + Command::new(pg_ctl_path) .arg("stop") .arg("-D") .arg(data_dir) diff --git a/src/run_db.rs b/src/run_db.rs index e4ca2e6..1dc2ebf 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -192,11 +192,12 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { } pgcmd = get_command(pg_ctl_path); - pgcmd.arg("start") + pgcmd + .arg("start") .args(["-D", data_dir_str]) .args(["-o", &pg_cmd_args.join(" ")]); } - + // don't output postgres output to stdout/stderr pgcmd .stdout(std::process::Stdio::piped()) @@ -207,21 +208,26 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { .expect("Failed to start postgres. Is it installed and on your path?"); // wait for db to be started - let db_ready_error = wait_for_db_ready( - &builder.bin_path, - port, - CREATEDB_MAX_TRIES, - CREATEDB_RETRY_DELAY, - ); - if let Some(output) = db_ready_error { - let stdout = output.stdout; - let stderr = output.stderr; - panic!( - "db did not start! stdout: {}\n\nstderr: {}", - String::from_utf8_lossy(&stdout), - String::from_utf8_lossy(&stderr) + #[cfg(windows)] + { + let db_ready_error = wait_for_db_ready( + &builder.bin_path, + port, + CREATEDB_MAX_TRIES, + CREATEDB_RETRY_DELAY, ); + if let Some(output) = db_ready_error { + let stdout = output.stdout; + let stderr = output.stderr; + panic!( + "db did not start! stdout: {}\n\nstderr: {}", + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr) + ); + } } + #[cfg(unix)] + std::thread::sleep(CREATEDB_RETRY_DELAY); let user = builder.get_user(); //let password = builder.get_password(); From 7c17d3e435ebb7245e38f19e50bb1e2cb7f84234 Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 02:44:04 -0500 Subject: [PATCH 13/15] Remove Unix dead code. --- src/lib.rs | 20 ++++---------------- src/run_db.rs | 1 + 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ab5de10..1c8f165 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,8 +37,6 @@ pub struct PgTempDB { // See shutdown implementation for why these are options temp_dir: Option, postgres_process: Option, - /// Prefix PostgreSQL binary names (`initdb`, `createdb`, and `postgres`) with this path, instead of searching $PATH - bin_path: Option, } impl PgTempDB { @@ -51,7 +49,6 @@ impl PgTempDB { let persist = builder.persist_data_dir; let dump_path = builder.dump_path.clone(); let load_path = builder.load_path.clone(); - let bin_path = builder.bin_path.clone(); let temp_dir = run_db::init_db(&mut builder); let postgres_process = Some(run_db::run_db(&temp_dir, builder)); @@ -66,7 +63,6 @@ impl PgTempDB { dump_path, temp_dir, postgres_process, - bin_path }; if let Some(path) = load_path { @@ -166,9 +162,6 @@ impl PgTempDB { .take() .expect("shutdown with no postgres process"); - let data_dir = self.data_dir(); - let temp_dir = self.temp_dir.take().unwrap(); - // fast (not graceful) shutdown via SIGINT // TODO: graceful shutdown via SIGTERM // was having issues with using graceful shutdown by default and some tests/examples using @@ -187,17 +180,10 @@ impl PgTempDB { } #[cfg(windows)] { - use std::process::Command; - - let pg_ctl_path = self - .bin_path - .as_ref() - .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); - - Command::new(pg_ctl_path) + std::process::Command::new("pg_ctl") .arg("stop") .arg("-D") - .arg(data_dir) + .arg(self.data_dir()) .output() .expect("Failed to stop server with pg_ctl. Is it installed and on your path?"); } @@ -206,6 +192,8 @@ impl PgTempDB { .wait_with_output() .expect("postgres server failed to exit cleanly"); + let temp_dir = self.temp_dir.take().unwrap(); + if self.persist { // this prevents the dir from being deleted on drop let _path = temp_dir.into_path(); diff --git a/src/run_db.rs b/src/run_db.rs index 1dc2ebf..4068548 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -111,6 +111,7 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { temp_dir } +#[cfg(windows)] /// waits for a db to be ready /// if the server never started, returns the last command output fn wait_for_db_ready( From 9ba00d86f02a600e4c0c6e12b80e43b2387ed6a6 Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 03:13:13 -0500 Subject: [PATCH 14/15] Cargo fmt, clippy, and adjust makefile for windows. --- .github/workflows/build-test.yaml | 8 +- Makefile | 6 +- src/run_db.rs | 120 ++++++++++++++++-------------- 3 files changed, 72 insertions(+), 62 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 14c5c8b..ec11166 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -21,10 +21,6 @@ jobs: name: Windows Unit Tests runs-on: windows-latest steps: - - name: Install python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - name: Install postgres uses: tj-actions/install-postgresql@v3 with: @@ -32,7 +28,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Run tests - run: make test-ci + run: make test-ci-windows test-unix: name: Unix Unit Tests @@ -45,7 +41,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Run tests - run: make test-ci + run: make test-ci-unix doc: name: Documentation Check diff --git a/Makefile b/Makefile index f60a8a9..8682b2c 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,15 @@ test: cargo test --tests --examples --all-features # Run all tests with and without all features -test-ci: +test-ci-unix: cargo test --tests --examples --no-default-features cargo test --tests --examples --all-features make -C examples/python-sqlalchemy test-ci +test-ci-windows: + cargo test --tests --examples --no-default-features + cargo test --tests --examples --all-features + # Run clippy lint: cargo clippy --no-deps --all-targets --all-features -- -W clippy::pedantic \ diff --git a/src/run_db.rs b/src/run_db.rs index 4068548..c26fc54 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::ffi::OsStr; use std::{ path::PathBuf, @@ -115,15 +116,13 @@ pub fn init_db(builder: &mut PgTempDBBuilder) -> TempDir { /// waits for a db to be ready /// if the server never started, returns the last command output fn wait_for_db_ready( - bin_path: &Option, + bin_path: Option<&PathBuf>, port: u16, max_retries: u32, retry_delay: Duration, ) -> Option { let mut isready_last_error_output = None; - let isready_path = bin_path - .as_ref() - .map_or("pg_isready".into(), |p| p.join("pg_isready")); + let isready_path = bin_path.map_or("pg_isready".into(), |p| p.join("pg_isready")); let mut server_status_cmd = Command::new(&isready_path); server_status_cmd.args(["-p", &port.to_string()]); @@ -142,62 +141,73 @@ fn wait_for_db_ready( isready_last_error_output } +#[cfg(unix)] +fn get_run_db_cmd( + bin_path: Option<&PathBuf>, + data_dir_str: &str, + port: u16, + server_configs: &HashMap, +) -> Command { + let postgres_path = bin_path.map_or("postgres".into(), |p| p.join("postgres")); + + let mut pgcmd = get_command(postgres_path); + pgcmd + .args(["-c", &format!("unix_socket_directories={}", data_dir_str)]) + .args(["-c", &format!("port={port}")]) + // https://www.postgresql.org/docs/current/non-durability.html + // https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server + .args(["-c", "fsync=off"]) + .args(["-c", "synchronous_commit=off"]) + .args(["-c", "full_page_writes=off"]) + .args(["-c", "autovacuum=off"]) + .args(["-D", data_dir_str]); + for (key, val) in server_configs { + pgcmd.args(["-c", &format!("{}={}", key, val)]); + } + pgcmd +} +#[cfg(windows)] +fn get_run_db_cmd( + bin_path: Option<&PathBuf>, + data_dir_str: &str, + port: u16, + server_configs: &HashMap, +) -> Command { + let pg_ctl_path = bin_path.map_or("pg_ctl".into(), |p| p.join("pg_ctl")); + + // build postgres command args + let mut pg_cmd_args = vec![ + format!("-c unix_socket_directories={}", data_dir_str), + format!("-c port={}", port), + "-c fsync=off".into(), + "-c synchronous_commit=off".into(), + "-c full_page_writes=off".into(), + "-c autovacuum=off".into(), + ]; + for (key, val) in server_configs { + pg_cmd_args.push(format!("-c {}={}", key, val)); + } + + let mut pgcmd = get_command(pg_ctl_path); + pgcmd + .arg("start") + .args(["-D", data_dir_str]) + .args(["-o", &pg_cmd_args.join(" ")]); + pgcmd +} + pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { let data_dir = temp_dir.path().join("pg_data_dir"); let data_dir_str = data_dir.to_str().unwrap(); let port = builder.get_port_or_set_random(); // postgres will not run as root, so try to run as postgres if we are root - let mut pgcmd: Command; - - #[cfg(unix)] - { - let postgres_path = builder - .bin_path - .as_ref() - .map_or("postgres".into(), |p| p.join("postgres")); - pgcmd = get_command(postgres_path); - - pgcmd - .args(["-c", &format!("unix_socket_directories={}", data_dir_str)]) - .args(["-c", &format!("port={port}")]) - // https://www.postgresql.org/docs/current/non-durability.html - // https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server - .args(["-c", "fsync=off"]) - .args(["-c", "synchronous_commit=off"]) - .args(["-c", "full_page_writes=off"]) - .args(["-c", "autovacuum=off"]) - .args(["-D", data_dir.to_str().unwrap()]); - for (key, val) in &builder.server_configs { - pgcmd.args(["-c", &format!("{}={}", key, val)]); - } - } - #[cfg(windows)] - { - let pg_ctl_path = builder - .bin_path - .as_ref() - .map_or("pg_ctl".into(), |p| p.join("pg_ctl")); - - // build postgres command args - let mut pg_cmd_args = vec![ - format!("-c unix_socket_directories={}", data_dir_str), - format!("-c port={}", port), - "-c fsync=off".into(), - "-c synchronous_commit=off".into(), - "-c full_page_writes=off".into(), - "-c autovacuum=off".into(), - ]; - for (key, val) in &builder.server_configs { - pg_cmd_args.push(format!("-c {}={}", key, val)); - } - - pgcmd = get_command(pg_ctl_path); - pgcmd - .arg("start") - .args(["-D", data_dir_str]) - .args(["-o", &pg_cmd_args.join(" ")]); - } + let mut pgcmd: Command = get_run_db_cmd( + builder.bin_path.as_ref(), + data_dir_str, + port, + &builder.server_configs, + ); // don't output postgres output to stdout/stderr pgcmd @@ -212,7 +222,7 @@ pub fn run_db(temp_dir: &TempDir, mut builder: PgTempDBBuilder) -> Child { #[cfg(windows)] { let db_ready_error = wait_for_db_ready( - &builder.bin_path, + builder.bin_path.as_ref(), port, CREATEDB_MAX_TRIES, CREATEDB_RETRY_DELAY, From 5fc5e767983d0a31cea35acdbb35db4b63b6542c Mon Sep 17 00:00:00 2001 From: Duosion Date: Thu, 21 Aug 2025 03:19:11 -0500 Subject: [PATCH 15/15] Make Output import fully qualified to avoid unix unused import. --- src/run_db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/run_db.rs b/src/run_db.rs index c26fc54..2a98eb1 100644 --- a/src/run_db.rs +++ b/src/run_db.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::{ path::PathBuf, - process::{Child, Command, Output}, + process::{Child, Command}, time::Duration, }; use tempfile::TempDir; @@ -120,7 +120,7 @@ fn wait_for_db_ready( port: u16, max_retries: u32, retry_delay: Duration, -) -> Option { +) -> Option { let mut isready_last_error_output = None; let isready_path = bin_path.map_or("pg_isready".into(), |p| p.join("pg_isready"));