Skip to main content

tmp_postgrust/
synchronous.rs

1use std::convert::TryInto;
2use std::fs::create_dir_all;
3use std::io::BufReader;
4use std::io::Lines;
5use std::os::unix::process::CommandExt;
6use std::path::{Path, PathBuf};
7use std::process::Child;
8use std::process::ChildStderr;
9use std::process::ChildStdout;
10use std::process::Command;
11use std::process::Stdio;
12use std::sync::Arc;
13
14use nix::sys::signal;
15use nix::sys::signal::Signal;
16use nix::unistd::User;
17use nix::unistd::{Pid, Uid};
18use tempfile::TempDir;
19use tracing::{debug, instrument};
20
21use crate::errors::{ProcessCapture, TmpPostgrustError, TmpPostgrustResult};
22use crate::search::all_dir_entries;
23use crate::search::build_copy_dst_path;
24use crate::POSTGRES_UID_GID;
25
26#[instrument(skip(command, fail))]
27fn exec_process(
28    command: &mut Command,
29    fail: impl FnOnce(ProcessCapture) -> TmpPostgrustError,
30) -> TmpPostgrustResult<()> {
31    debug!("running command: {:?}", command);
32
33    let output = command
34        .output()
35        .map_err(|err| TmpPostgrustError::ExecSubprocessFailed {
36            source: err,
37            command: format!("{command:?}"),
38        })?;
39
40    if output.status.success() {
41        for line in String::from_utf8(output.stdout).unwrap().lines() {
42            debug!("{}", line);
43        }
44        Ok(())
45    } else {
46        Err(fail(ProcessCapture {
47            stdout: String::from_utf8(output.stdout).unwrap(),
48            stderr: String::from_utf8(output.stderr).unwrap(),
49        }))
50    }
51}
52
53#[instrument]
54pub(crate) fn start_postgres_subprocess(
55    postgres_bin: &Path,
56    data_directory: &Path,
57    port: u32,
58) -> TmpPostgrustResult<Child> {
59    let mut command = Command::new(postgres_bin);
60    command
61        .env("PGDATA", data_directory.to_str().unwrap())
62        .arg("-p")
63        .arg(port.to_string())
64        .stdout(Stdio::piped())
65        .stderr(Stdio::piped());
66    cmd_as_non_root(&mut command);
67    command
68        .spawn()
69        .map_err(TmpPostgrustError::SpawnSubprocessFailed)
70}
71
72#[instrument]
73pub(crate) fn exec_init_db(initdb_bin: &Path, data_directory: &Path) -> TmpPostgrustResult<()> {
74    debug!("Initializing database in: {:?}", data_directory);
75
76    let mut command = Command::new(initdb_bin);
77    command
78        .env("PGDATA", data_directory.to_str().unwrap())
79        .arg("--username=postgres");
80    cmd_as_non_root(&mut command);
81    exec_process(&mut command, TmpPostgrustError::InitDBFailed)
82}
83
84#[instrument]
85pub(crate) fn exec_copy_dir(src_dir: &Path, dst_dir: &Path) -> TmpPostgrustResult<()> {
86    let (dirs, others) = all_dir_entries(src_dir)?;
87
88    for entry in dirs {
89        create_dir_all(build_copy_dst_path(&entry, src_dir, dst_dir)?)
90            .map_err(TmpPostgrustError::CopyCachedInitDBFailedFileNotFound)?;
91    }
92
93    for entry in others {
94        reflink_copy::reflink_or_copy(&entry, build_copy_dst_path(&entry, src_dir, dst_dir)?)
95            .map_err(TmpPostgrustError::CopyCachedInitDBFailedFileNotFound)?;
96    }
97
98    Ok(())
99}
100
101#[instrument]
102pub(crate) fn chown_to_non_root(dir: &Path) -> TmpPostgrustResult<()> {
103    let current_uid = Uid::effective();
104    if !current_uid.is_root() {
105        return Ok(());
106    }
107
108    let (uid, gid) = POSTGRES_UID_GID.get_or_init(|| {
109        User::from_name("postgres")
110            .ok()
111            .flatten()
112            .map(|u| (u.uid, u.gid))
113            .expect("no user `postgres` found is system")
114    });
115    let mut cmd = Command::new("chown");
116    cmd.arg("-R").arg(format!("{uid}:{gid}")).arg(dir);
117    exec_process(&mut cmd, TmpPostgrustError::UpdatingPermissionsFailed)?;
118    Ok(())
119}
120
121#[instrument]
122pub(crate) fn exec_create_db(
123    createdb_bin: &Path,
124    socket: &Path,
125    port: u32,
126    owner: &str,
127    dbname: &str,
128    template_db: Option<&str>,
129) -> TmpPostgrustResult<()> {
130    let mut command = Command::new(createdb_bin);
131    command
132        .arg("-h")
133        .arg(socket)
134        .arg("-p")
135        .arg(port.to_string())
136        .arg("-U")
137        .arg("postgres")
138        .arg("-O")
139        .arg(owner)
140        .arg("--echo");
141    if let Some(template_db) = template_db {
142        command.arg("-T").arg(template_db);
143    }
144    command.arg(dbname);
145    cmd_as_non_root(&mut command);
146    exec_process(&mut command, TmpPostgrustError::CreateDBFailed)
147}
148
149#[instrument]
150pub(crate) fn exec_create_user(
151    createuser_bin: &Path,
152    socket: &Path,
153    port: u32,
154    username: &str,
155) -> TmpPostgrustResult<()> {
156    let mut command = Command::new(createuser_bin);
157    command
158        .arg("-h")
159        .arg(socket)
160        .arg("-p")
161        .arg(port.to_string())
162        .arg("-U")
163        .arg("postgres")
164        .arg("--superuser")
165        .arg("--echo")
166        .arg(username);
167    cmd_as_non_root(&mut command);
168    exec_process(&mut command, TmpPostgrustError::CreateDBFailed)
169}
170
171/// `ProcessGuard` represents a postgresql process that is running in the background.
172/// once the guard is dropped the process will be killed.
173pub struct ProcessGuard {
174    /// Allows users to read stdout by line for debugging.
175    pub stdout_reader: Option<Lines<BufReader<ChildStdout>>>,
176    /// Allows users to read stderr by line for debugging.
177    pub stderr_reader: Option<Lines<BufReader<ChildStderr>>>,
178    /// Parameters for connecting to the temporary postgresql instance.
179    ///
180    /// A user shouldn't need to use these and should call `connection_string` instead.
181    ///
182    /// Port number that Postgresql is serving on
183    pub port: u32,
184    /// Database name to connect to.
185    pub db_name: String,
186    /// Username to connect as.
187    pub user_name: String,
188
189    // Signal that the postgres process should be killed.
190    pub(crate) postgres_process: Child,
191    // Prevent the data directory from being dropped while
192    // the process is running.
193    pub(crate) _data_directory: Arc<TempDir>,
194    // Prevent the cache directory from being dropped while
195    // the process is running.
196    pub(crate) _cache_directory: Arc<TempDir>,
197    /// Socket directory for connection to the running process.
198    pub(crate) socket_dir: Arc<TempDir>,
199    pub(crate) createdb_bin: PathBuf,
200}
201
202impl ProcessGuard {
203    /// Get a Postgresql format connection String for the process guard.
204    ///
205    /// # Panics
206    ///
207    /// Panics if a string file path cannot be obtained from the socket directory.
208    #[must_use]
209    pub fn connection_string(&self) -> String {
210        self.connection_string_for_db(&self.db_name)
211    }
212
213    /// Get a Postgresql format connection string for a database on this running instance.
214    ///
215    /// # Panics
216    ///
217    /// Panics if a string file path cannot be obtained from the socket directory.
218    #[must_use]
219    pub fn connection_string_for_db(&self, db_name: &str) -> String {
220        format!(
221            "postgresql:///?host={}&port={}&dbname={}&user={}",
222            self.socket_dir
223                .path()
224                .to_str()
225                .expect("Failed to convert socket directory to a path"),
226            self.port,
227            db_name,
228            self.user_name,
229        )
230    }
231
232    /// Clone the current database in this running instance.
233    ///
234    /// The returned connection string points at the newly-created database.
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if `createdb` fails to clone the current database.
239    pub fn clone_database(&self, db_name: &str) -> TmpPostgrustResult<String> {
240        exec_create_db(
241            &self.createdb_bin,
242            self.socket_dir.path(),
243            self.port,
244            &self.user_name,
245            db_name,
246            Some(&self.db_name),
247        )?;
248        Ok(self.connection_string_for_db(db_name))
249    }
250}
251
252/// Signal that the process needs to end.
253impl Drop for ProcessGuard {
254    fn drop(&mut self) {
255        signal::kill(
256            Pid::from_raw(self.postgres_process.id().try_into().unwrap()),
257            Signal::SIGINT,
258        )
259        .unwrap();
260        self.postgres_process.wait().unwrap();
261    }
262}
263
264fn cmd_as_non_root(command: &mut Command) {
265    let current_uid = Uid::effective();
266    if current_uid.is_root() {
267        let (uid, gid) = POSTGRES_UID_GID.get_or_init(|| {
268            User::from_name("postgres")
269                .ok()
270                .flatten()
271                .map(|u| (u.uid, u.gid))
272                .expect("no user `postgres` found is system")
273        });
274        command.uid(uid.as_raw()).gid(gid.as_raw());
275        // PostgreSQL cannot be run as root, so change to default user
276        command.uid(uid.as_raw()).gid(gid.as_raw());
277    }
278}