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;
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::search::find_postgresql_command;
25use crate::POSTGRES_UID_GID;
26
27#[instrument(skip(command, fail))]
28fn exec_process(
29    command: &mut Command,
30    fail: impl FnOnce(ProcessCapture) -> TmpPostgrustError,
31) -> TmpPostgrustResult<()> {
32    debug!("running command: {:?}", command);
33
34    let output = command
35        .output()
36        .map_err(|err| TmpPostgrustError::ExecSubprocessFailed {
37            source: err,
38            command: format!("{command:?}"),
39        })?;
40
41    if output.status.success() {
42        for line in String::from_utf8(output.stdout).unwrap().lines() {
43            debug!("{}", line);
44        }
45        Ok(())
46    } else {
47        Err(fail(ProcessCapture {
48            stdout: String::from_utf8(output.stdout).unwrap(),
49            stderr: String::from_utf8(output.stderr).unwrap(),
50        }))
51    }
52}
53
54#[instrument]
55pub(crate) fn start_postgres_subprocess(
56    data_directory: &Path,
57    port: u32,
58) -> TmpPostgrustResult<Child> {
59    let postgres_path =
60        find_postgresql_command("bin", "postgres").expect("failed to find postgres");
61
62    let mut command = Command::new(postgres_path);
63    command
64        .env("PGDATA", data_directory.to_str().unwrap())
65        .arg("-p")
66        .arg(port.to_string())
67        .stdout(Stdio::piped())
68        .stderr(Stdio::piped());
69    cmd_as_non_root(&mut command);
70    command
71        .spawn()
72        .map_err(TmpPostgrustError::SpawnSubprocessFailed)
73}
74
75#[instrument]
76pub(crate) fn exec_init_db(data_directory: &Path) -> TmpPostgrustResult<()> {
77    let initdb_path = find_postgresql_command("bin", "initdb").expect("failed to find initdb");
78
79    debug!("Initializing database in: {:?}", data_directory);
80
81    let mut command = Command::new(initdb_path);
82    command
83        .env("PGDATA", data_directory.to_str().unwrap())
84        .arg("--username=postgres");
85    cmd_as_non_root(&mut command);
86    exec_process(&mut command, TmpPostgrustError::InitDBFailed)
87}
88
89#[instrument]
90pub(crate) fn exec_copy_dir(src_dir: &Path, dst_dir: &Path) -> TmpPostgrustResult<()> {
91    let (dirs, others) = all_dir_entries(src_dir)?;
92
93    for entry in dirs {
94        create_dir_all(build_copy_dst_path(&entry, src_dir, dst_dir)?)
95            .map_err(TmpPostgrustError::CopyCachedInitDBFailedFileNotFound)?;
96    }
97
98    for entry in others {
99        reflink_copy::reflink_or_copy(&entry, build_copy_dst_path(&entry, src_dir, dst_dir)?)
100            .map_err(TmpPostgrustError::CopyCachedInitDBFailedFileNotFound)?;
101    }
102
103    Ok(())
104}
105
106#[instrument]
107pub(crate) fn chown_to_non_root(dir: &Path) -> TmpPostgrustResult<()> {
108    let current_uid = Uid::effective();
109    if !current_uid.is_root() {
110        return Ok(());
111    }
112
113    let (uid, gid) = POSTGRES_UID_GID.get_or_init(|| {
114        User::from_name("postgres")
115            .ok()
116            .flatten()
117            .map(|u| (u.uid, u.gid))
118            .expect("no user `postgres` found is system")
119    });
120    let mut cmd = Command::new("chown");
121    cmd.arg("-R").arg(format!("{uid}:{gid}")).arg(dir);
122    exec_process(&mut cmd, TmpPostgrustError::UpdatingPermissionsFailed)?;
123    Ok(())
124}
125
126#[instrument]
127pub(crate) fn exec_create_db(
128    socket: &Path,
129    port: u32,
130    owner: &str,
131    dbname: &str,
132) -> TmpPostgrustResult<()> {
133    let mut command = Command::new("createdb");
134    command
135        .arg("-h")
136        .arg(socket)
137        .arg("-p")
138        .arg(port.to_string())
139        .arg("-U")
140        .arg("postgres")
141        .arg("-O")
142        .arg(owner)
143        .arg("--echo")
144        .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(socket: &Path, port: u32, username: &str) -> TmpPostgrustResult<()> {
151    let mut command = Command::new("createuser");
152    command
153        .arg("-h")
154        .arg(socket)
155        .arg("-p")
156        .arg(port.to_string())
157        .arg("-U")
158        .arg("postgres")
159        .arg("--superuser")
160        .arg("--echo")
161        .arg(username);
162    cmd_as_non_root(&mut command);
163    exec_process(&mut command, TmpPostgrustError::CreateDBFailed)
164}
165
166/// `ProcessGuard` represents a postgresql process that is running in the background.
167/// once the guard is dropped the process will be killed.
168pub struct ProcessGuard {
169    /// Allows users to read stdout by line for debugging.
170    pub stdout_reader: Option<Lines<BufReader<ChildStdout>>>,
171    /// Allows users to read stderr by line for debugging.
172    pub stderr_reader: Option<Lines<BufReader<ChildStderr>>>,
173    /// Parameters for connecting to the temporary postgresql instance.
174    ///
175    /// A user shouldn't need to use these and should call `connection_string` instead.
176    ///
177    /// Port number that Postgresql is serving on
178    pub port: u32,
179    /// Database name to connect to.
180    pub db_name: String,
181    /// Username to connect as.
182    pub user_name: String,
183
184    // Signal that the postgres process should be killed.
185    pub(crate) postgres_process: Child,
186    // Prevent the data directory from being dropped while
187    // the process is running.
188    pub(crate) _data_directory: Arc<TempDir>,
189    // Prevent the cache directory from being dropped while
190    // the process is running.
191    pub(crate) _cache_directory: Arc<TempDir>,
192    /// Socket directory for connection to the running process.
193    pub(crate) socket_dir: Arc<TempDir>,
194}
195
196impl ProcessGuard {
197    /// Get a Postgresql format connection String for the process guard.
198    ///
199    /// # Panics
200    ///
201    /// Panics if a string file path cannot be obtained from the socket directory.
202    #[must_use]
203    pub fn connection_string(&self) -> String {
204        format!(
205            "postgresql:///?host={}&port={}&dbname={}&user={}",
206            self.socket_dir
207                .path()
208                .to_str()
209                .expect("Failed to convert socket directory to a path"),
210            self.port,
211            self.db_name,
212            self.user_name,
213        )
214    }
215}
216
217/// Signal that the process needs to end.
218impl Drop for ProcessGuard {
219    fn drop(&mut self) {
220        signal::kill(
221            Pid::from_raw(self.postgres_process.id().try_into().unwrap()),
222            Signal::SIGINT,
223        )
224        .unwrap();
225        self.postgres_process.wait().unwrap();
226    }
227}
228
229fn cmd_as_non_root(command: &mut Command) {
230    let current_uid = Uid::effective();
231    if current_uid.is_root() {
232        let (uid, gid) = POSTGRES_UID_GID.get_or_init(|| {
233            User::from_name("postgres")
234                .ok()
235                .flatten()
236                .map(|u| (u.uid, u.gid))
237                .expect("no user `postgres` found is system")
238        });
239        command.uid(uid.as_raw()).gid(gid.as_raw());
240        // PostgreSQL cannot be run as root, so change to default user
241        command.uid(uid.as_raw()).gid(gid.as_raw());
242    }
243}