use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use diesel_async::AsyncPgConnection;
use diesel_async::pooled_connection::deadpool::Pool;
use postgresql_embedded::{PostgreSQL, Settings, VersionReq};
use crate::config::DatabaseConfig;
use crate::db::{DatabasePoolProvider, PoolError, create_pool};
const MANAGED_DB_NAME: &str = "autumn";
pub const MANAGED_PG_DATA_DIR_ENV: &str = "AUTUMN_MANAGED_PG_DATA_DIR";
pub const MANAGED_PG_ATTACH_URL_ENV: &str = "AUTUMN_MANAGED_PG_ATTACH_URL";
pub const PUBLISHED_URL_FILE: &str = ".autumn-pg-url";
const STARTUP_TIMEOUT: Duration = Duration::from_secs(60);
static EMERGENCY_PROVIDER: std::sync::Mutex<Option<ManagedPostgresPoolProvider>> =
std::sync::Mutex::new(None);
pub(crate) fn emergency_stop() {
let Some(provider) = EMERGENCY_PROVIDER.lock().ok().and_then(|g| g.clone()) else {
return;
};
match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt.block_on(provider.stop()),
Err(e) => {
tracing::warn!(error = %e, "could not stop managed Postgres during emergency shutdown");
}
}
}
pub(crate) async fn emergency_stop_async() {
let provider = EMERGENCY_PROVIDER.lock().ok().and_then(|g| g.clone());
if let Some(provider) = provider {
provider.stop().await;
}
}
#[cfg(unix)]
fn enforce_password_file_mode_or_exit(path: &Path) {
use std::os::unix::fs::PermissionsExt;
if let Err(e) = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) {
tracing::error!(error = %e, path = %path.display(),
"managed Postgres: cannot enforce owner-only permissions on the superuser password file");
eprintln!(
"autumn: refusing to start managed Postgres — cannot set 0600 on {} ({e}). \
The cluster superuser password would be readable by other local users.",
path.display()
);
std::process::exit(1);
}
}
#[cfg(not(unix))]
fn enforce_password_file_mode_or_exit(_path: &Path) {}
#[cfg(unix)]
fn harden_credential_dir_or_exit(dir: &Path) {
use std::os::unix::fs::PermissionsExt;
match std::fs::symlink_metadata(dir) {
Ok(meta) if meta.file_type().is_symlink() => {
reject_credential_dir(dir, "credential directory is a symlink");
}
Ok(_) => {
if std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).is_err() {
reject_credential_dir(
dir,
"cannot enforce owner-only (0700) permissions on the credential directory",
);
}
}
Err(_) => reject_credential_dir(dir, "cannot stat the credential directory"),
}
}
#[cfg(unix)]
fn reject_credential_dir(dir: &Path, reason: &str) -> ! {
tracing::error!(path = %dir.display(), "managed Postgres: {reason}");
eprintln!(
"autumn: refusing to start managed Postgres — {reason} for {}. The cluster \
superuser password file lives here and could be replaced by another \
local user.",
dir.display()
);
std::process::exit(1);
}
#[cfg(not(unix))]
fn harden_credential_dir_or_exit(_dir: &Path) {}
fn clear_emergency_registration() {
if let Ok(mut g) = EMERGENCY_PROVIDER.lock() {
*g = None;
}
}
fn generate_password() -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut bytes = [0u8; 24];
if getrandom::getrandom(&mut bytes).is_err() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0u128, |d| d.as_nanos());
let seed = nanos.to_le_bytes();
for (i, b) in bytes.iter_mut().enumerate() {
*b = seed[i % seed.len()];
}
}
bytes
.iter()
.map(|b| ALPHABET[usize::from(*b) % ALPHABET.len()] as char)
.collect()
}
#[derive(Clone)]
pub struct ManagedPostgresPoolProvider {
data_dir: PathBuf,
version: Option<VersionReq>,
instance: Arc<Mutex<Option<PostgreSQL>>>,
resolved_url: Arc<Mutex<Option<String>>>,
attached: Arc<AtomicBool>,
}
impl std::fmt::Debug for ManagedPostgresPoolProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ManagedPostgresPoolProvider")
.field("data_dir", &self.data_dir)
.field("version", &self.version)
.finish_non_exhaustive()
}
}
impl Default for ManagedPostgresPoolProvider {
fn default() -> Self {
Self::new()
}
}
impl ManagedPostgresPoolProvider {
#[must_use]
pub fn new() -> Self {
Self::with_data_dir(default_data_dir())
}
#[must_use]
pub fn with_data_dir(data_dir: PathBuf) -> Self {
Self {
data_dir,
version: None,
instance: Arc::new(Mutex::new(None)),
resolved_url: Arc::new(Mutex::new(None)),
attached: Arc::new(AtomicBool::new(false)),
}
}
fn installation_dir(&self) -> PathBuf {
self.data_dir
.parent()
.unwrap_or(&self.data_dir)
.join("postgresql")
}
fn published_url_path(&self) -> PathBuf {
self.data_dir
.parent()
.unwrap_or(&self.data_dir)
.join(PUBLISHED_URL_FILE)
}
fn publish_url(&self, url: &str) {
let path = self.published_url_path();
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
match options.open(&path) {
Ok(mut f) => {
use std::io::Write;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
.is_err()
{
tracing::warn!(path = %path.display(),
"managed Postgres: cannot enforce owner-only mode on the cluster URL file; not publishing");
let _ = std::fs::remove_file(&path);
return;
}
}
if let Err(e) = f.write_all(url.as_bytes()) {
tracing::warn!(error = %e, path = %path.display(),
"managed Postgres: failed to publish cluster URL for task/build attach");
}
}
Err(e) => tracing::warn!(error = %e, path = %path.display(),
"managed Postgres: failed to open cluster URL file for publishing"),
}
}
async fn ensure_running(&self) -> Result<String, postgresql_embedded::Error> {
if let Ok(guard) = self.instance.lock()
&& let Some(pg) = guard.as_ref()
{
return Ok(pg.settings().url(MANAGED_DB_NAME));
}
if let Some(url) = std::env::var_os(MANAGED_PG_ATTACH_URL_ENV)
.and_then(|v| v.into_string().ok())
.filter(|s| !s.trim().is_empty())
{
self.attached.store(true, Ordering::SeqCst);
if let Ok(mut g) = self.resolved_url.lock() {
*g = Some(url.clone());
}
return Ok(url);
}
let mut settings = Settings::new();
settings.data_dir.clone_from(&self.data_dir);
settings.installation_dir = self.installation_dir();
if let Some(version) = &self.version {
settings.version = version.clone();
}
settings.temporary = false;
settings.timeout = Some(STARTUP_TIMEOUT);
let (password, password_file) = self.stable_credentials();
settings.password = password;
settings.password_file = password_file;
let mut pg = PostgreSQL::new(settings);
pg.setup().await?;
pg.start().await?;
if let Err(e) = Self::ensure_app_database(&pg).await {
let _ = pg.stop().await;
return Err(e);
}
let url = pg.settings().url(MANAGED_DB_NAME);
if let Ok(mut guard) = self.instance.lock() {
*guard = Some(pg);
}
if let Ok(mut g) = self.resolved_url.lock() {
*g = Some(url.clone());
}
self.publish_url(&url);
if let Ok(mut g) = EMERGENCY_PROVIDER.lock() {
*g = Some(self.clone());
}
Ok(url)
}
async fn ensure_app_database(pg: &PostgreSQL) -> Result<(), postgresql_embedded::Error> {
if !pg.database_exists(MANAGED_DB_NAME).await? {
pg.create_database(MANAGED_DB_NAME).await?;
}
Ok(())
}
fn stable_credentials(&self) -> (String, PathBuf) {
let parent = self
.data_dir
.parent()
.unwrap_or(&self.data_dir)
.to_path_buf();
let _ = std::fs::create_dir_all(&parent);
harden_credential_dir_or_exit(&parent);
let password_file = parent.join(".autumn-pg-superuser");
if let Ok(existing) = std::fs::read_to_string(&password_file) {
let trimmed = existing.trim();
if !trimmed.is_empty() {
enforce_password_file_mode_or_exit(&password_file);
return (trimmed.to_owned(), password_file);
}
}
let password = generate_password();
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
if let Ok(mut f) = options.open(&password_file) {
use std::io::Write;
enforce_password_file_mode_or_exit(&password_file);
let _ = f.write_all(password.as_bytes());
}
(password, password_file)
}
pub async fn stop(&self) {
if self.attached.load(Ordering::SeqCst) {
return;
}
let taken = self.instance.lock().ok().and_then(|mut guard| guard.take());
let Some(pg) = taken else {
clear_emergency_registration();
return;
};
if let Err(e) = pg.stop().await {
tracing::warn!(error = %e, "failed to stop managed Postgres cleanly; will retry on next stop");
if let Ok(mut guard) = self.instance.lock() {
*guard = Some(pg);
}
return;
}
clear_emergency_registration();
let _ = std::fs::remove_file(self.published_url_path());
}
}
impl DatabasePoolProvider for ManagedPostgresPoolProvider {
async fn create_pool(
&self,
config: &DatabaseConfig,
) -> Result<Option<Pool<AsyncPgConnection>>, PoolError> {
if config.has_shards() {
reject_managed_shards();
}
let url = match self.ensure_running().await {
Ok(url) => url,
Err(e) => {
tracing::error!(error = %e, data_dir = %self.data_dir.display(),
"managed Postgres failed to provision/start");
eprintln!(
"autumn: managed Postgres failed to start ({e}). \
Data dir: {}",
self.data_dir.display()
);
std::process::exit(1);
}
};
let mut managed = config.clone();
managed.primary_url = Some(url);
managed.url = None;
create_pool(&managed)
}
async fn create_topology(
&self,
config: &DatabaseConfig,
) -> Result<Option<crate::db::DatabaseTopology>, PoolError> {
let Some(primary) = self.create_pool(config).await? else {
return Ok(None);
};
let migration_url =
self.resolved_url
.lock()
.ok()
.and_then(|g| g.clone())
.or_else(|| {
self.instance.lock().ok().and_then(|guard| {
guard.as_ref().map(|pg| pg.settings().url(MANAGED_DB_NAME))
})
});
Ok(Some(
crate::db::DatabaseTopology::from_pools(primary, None)
.with_migration_url(migration_url),
))
}
async fn create_shard_topology(
&self,
_shard: &crate::config::ShardConfig,
_defaults: &DatabaseConfig,
) -> Result<crate::db::DatabaseTopology, PoolError> {
self.stop().await;
reject_managed_shards();
}
}
fn reject_managed_shards() -> ! {
tracing::error!(
"managed Postgres does not support [[database.shards]]; remove the \
shard configuration or use an external database"
);
eprintln!(
"autumn: managed Postgres (--bundled-pg) does not support \
[[database.shards]]. Remove shard config or use an external database."
);
std::process::exit(1);
}
fn default_data_dir() -> PathBuf {
if let Some(dir) = std::env::var_os(MANAGED_PG_DATA_DIR_ENV) {
return PathBuf::from(dir);
}
let project = project_data_namespace();
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home)
.join(".local/share/autumn")
.join(project)
.join("pg");
}
if let Some(local_appdata) = std::env::var_os("LOCALAPPDATA") {
return PathBuf::from(local_appdata)
.join("autumn")
.join(project)
.join("pg");
}
std::env::temp_dir().join("autumn").join(project).join("pg")
}
fn project_data_namespace() -> String {
use crate::config::Env;
use sha2::{Digest, Sha256};
let base = crate::config::OsEnv
.var("AUTUMN_MANIFEST_DIR")
.ok()
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.and_then(|d| std::fs::canonicalize(&d).ok().or(Some(d)))
.unwrap_or_else(|| PathBuf::from("."));
let digest = Sha256::digest(base.to_string_lossy().as_bytes());
hex::encode(&digest[..4])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_data_dir_honors_env_override() {
temp_env::with_var(MANAGED_PG_DATA_DIR_ENV, Some("/tmp/custom/pg"), || {
assert_eq!(default_data_dir(), PathBuf::from("/tmp/custom/pg"));
});
}
#[test]
fn with_data_dir_is_used() {
let p = ManagedPostgresPoolProvider::with_data_dir(PathBuf::from("/var/lib/x"));
assert_eq!(p.data_dir, PathBuf::from("/var/lib/x"));
}
#[test]
fn attach_mode_uses_env_url_without_starting() {
temp_env::with_var(
MANAGED_PG_ATTACH_URL_ENV,
Some("postgresql://u:p@localhost:5499/autumn"),
|| {
let provider =
ManagedPostgresPoolProvider::with_data_dir(PathBuf::from("/nonexistent/pg"));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let url = rt.block_on(provider.ensure_running()).unwrap();
assert_eq!(url, "postgresql://u:p@localhost:5499/autumn");
assert!(provider.attached.load(Ordering::SeqCst));
rt.block_on(provider.stop());
},
);
}
#[test]
fn published_url_path_is_beside_data_dir() {
let p = ManagedPostgresPoolProvider::with_data_dir(PathBuf::from("/var/lib/app/pg"));
assert_eq!(
p.published_url_path(),
PathBuf::from("/var/lib/app").join(PUBLISHED_URL_FILE)
);
}
}