tmp_postgrust/
synchronous.rs1use 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
171pub struct ProcessGuard {
174 pub stdout_reader: Option<Lines<BufReader<ChildStdout>>>,
176 pub stderr_reader: Option<Lines<BufReader<ChildStderr>>>,
178 pub port: u32,
184 pub db_name: String,
186 pub user_name: String,
188
189 pub(crate) postgres_process: Child,
191 pub(crate) _data_directory: Arc<TempDir>,
194 pub(crate) _cache_directory: Arc<TempDir>,
197 pub(crate) socket_dir: Arc<TempDir>,
199 pub(crate) createdb_bin: PathBuf,
200}
201
202impl ProcessGuard {
203 #[must_use]
209 pub fn connection_string(&self) -> String {
210 self.connection_string_for_db(&self.db_name)
211 }
212
213 #[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 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
252impl 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 command.uid(uid.as_raw()).gid(gid.as_raw());
277 }
278}