#[cfg(unix)]
pub use super::transfer_fd::Fds;
use async_trait::async_trait;
#[cfg(unix)]
use log::warn;
use log::{debug, error, info};
use parking_lot::Mutex;
#[cfg(unix)]
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::broadcast;
#[cfg(feature = "sentry")]
use sentry::ClientOptions;
#[cfg(unix)]
use crate::server::daemon::notify_parent_ready_for_fds;
#[cfg(unix)]
use crate::server::ListenFds;
#[cfg(unix)]
use std::time::Duration;
use crate::{
prelude::Opt,
server::{configuration::ServerConf, ExecutionPhase, ShutdownWatch},
services::{background::BackgroundService, ServiceReadyNotifier},
};
#[cfg(unix)]
const DEFAULT_DAEMON_NOTIFY_TIMEOUT: Duration = Duration::from_secs(60);
pub struct BootstrapService {
inner: Arc<Mutex<Bootstrap>>,
}
impl BootstrapService {
pub fn new(inner: &Arc<Mutex<Bootstrap>>) -> Self {
BootstrapService {
inner: Arc::clone(inner),
}
}
}
pub struct Bootstrap {
completed: bool,
test: bool,
upgrade: bool,
upgrade_sock: String,
execution_phase_watch: broadcast::Sender<ExecutionPhase>,
#[cfg(unix)]
listen_fds: ListenFds,
#[cfg(unix)]
expected_listen_addrs: Option<HashSet<String>>,
#[cfg(unix)]
notify_parent_pid: Option<u32>,
#[cfg(unix)]
daemon_notify_timeout: std::time::Duration,
#[cfg(feature = "sentry")]
#[cfg_attr(docsrs, doc(cfg(feature = "sentry")))]
pub sentry: Option<ClientOptions>,
#[cfg(all(not(debug_assertions), feature = "sentry"))]
sentry_guard: Option<sentry::ClientInitGuard>,
}
impl Bootstrap {
pub fn new(
options: &Option<Opt>,
conf: &ServerConf,
execution_phase_watch: &broadcast::Sender<ExecutionPhase>,
) -> Self {
let (test, upgrade) = options
.as_ref()
.map(|opt| (opt.test, opt.upgrade))
.unwrap_or_default();
let upgrade_sock = conf.upgrade_sock.clone();
Bootstrap {
test,
upgrade,
upgrade_sock,
#[cfg(unix)]
listen_fds: Arc::new(Mutex::new(Fds::new())),
#[cfg(unix)]
expected_listen_addrs: None,
#[cfg(unix)]
notify_parent_pid: None,
#[cfg(unix)]
daemon_notify_timeout: conf
.daemon_notify_timeout_seconds
.map(|n| Duration::from_secs(n.get()))
.unwrap_or(DEFAULT_DAEMON_NOTIFY_TIMEOUT),
execution_phase_watch: execution_phase_watch.clone(),
completed: false,
#[cfg(feature = "sentry")]
sentry: None,
#[cfg(all(not(debug_assertions), feature = "sentry"))]
sentry_guard: None,
}
}
#[cfg(feature = "sentry")]
pub fn set_sentry_config(&mut self, sentry_config: Option<ClientOptions>) {
self.sentry = sentry_config;
}
#[cfg(unix)]
pub fn set_notify_parent_pid(&mut self, pid: u32) {
self.notify_parent_pid = Some(pid);
}
#[cfg(unix)]
pub fn set_expected_listen_addrs(&mut self, addrs: HashSet<String>) {
Self::close_unclaimed_fds(&mut self.listen_fds.lock(), &addrs);
self.expected_listen_addrs = Some(addrs);
}
#[cfg(feature = "sentry")]
pub(super) fn start_sentry(&mut self) {
#[cfg(not(debug_assertions))]
{
self.sentry_guard = self.sentry.as_ref().map(|opts| sentry::init(opts.clone()));
}
}
pub fn bootstrap(&mut self) {
if self.completed {
return;
}
info!("Bootstrap starting");
self.execution_phase_watch
.send(ExecutionPhase::Bootstrap)
.ok();
#[cfg(all(not(debug_assertions), feature = "sentry"))]
let _guard = if self.sentry_guard.is_none() {
self.sentry.as_ref().map(|opts| sentry::init(opts.clone()))
} else {
None
};
if self.test {
info!("Server Test passed, exiting");
std::process::exit(0);
}
#[cfg(unix)]
if let Some(pid) = self.notify_parent_pid {
notify_parent_ready_for_fds(pid, self.daemon_notify_timeout);
}
#[cfg(unix)]
match self.load_fds(self.upgrade) {
Ok(_) => {
info!("Bootstrap done");
}
Err(e) => {
#[cfg(all(not(debug_assertions), feature = "sentry"))]
sentry::capture_error(&e);
error!("Bootstrap failed on error: {:?}, exiting.", e);
std::process::exit(1);
}
}
self.completed = true;
self.execution_phase_watch
.send(ExecutionPhase::BootstrapComplete)
.ok();
}
#[cfg(unix)]
fn load_fds(&mut self, upgrade: bool) -> Result<(), nix::Error> {
if upgrade {
debug!("Trying to receive socks");
let mut fds = Fds::new();
fds.get_from_sock(self.upgrade_sock.as_str())?;
if let Some(expected) = &self.expected_listen_addrs {
Self::close_unclaimed_fds(&mut fds, expected);
}
*self.listen_fds.lock() = fds;
}
Ok(())
}
#[cfg(unix)]
fn close_unclaimed_fds(fds: &mut Fds, expected: &HashSet<String>) {
let closed = fds.close_unclaimed(expected);
if !closed.is_empty() {
warn!(
"Closed {} inherited listening socket(s) not claimed by any service: {:?}",
closed.len(),
closed
);
}
}
#[cfg(unix)]
pub fn get_fds(&self) -> ListenFds {
self.listen_fds.clone()
}
}
#[async_trait]
impl BackgroundService for BootstrapService {
async fn start_with_ready_notifier(
&self,
_shutdown: ShutdownWatch,
notifier: ServiceReadyNotifier,
) {
self.inner.lock().bootstrap();
notifier.notify_ready();
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::io::{ErrorKind, Read};
use std::os::unix::io::{FromRawFd, IntoRawFd, OwnedFd};
use std::os::unix::net::UnixStream;
#[test]
fn expected_addresses_prune_already_loaded_fds() {
let (execution_phase_watch, _) = broadcast::channel(1);
let mut bootstrap = Bootstrap::new(&None, &ServerConf::default(), &execution_phase_watch);
let (mut keep_peer, keep_local) = UnixStream::pair().unwrap();
let (mut drop_peer, drop_local) = UnixStream::pair().unwrap();
keep_peer.set_nonblocking(true).unwrap();
drop_peer.set_nonblocking(true).unwrap();
let keep_fd = keep_local.into_raw_fd();
{
let mut fds = bootstrap.listen_fds.lock();
fds.add("127.0.0.1:80".to_string(), keep_fd);
fds.add("127.0.0.1:9090".to_string(), drop_local.into_raw_fd());
}
bootstrap.set_expected_listen_addrs(["127.0.0.1:80".to_string()].into_iter().collect());
let fds = bootstrap.listen_fds.lock();
assert_eq!(*fds.get("127.0.0.1:80").unwrap(), keep_fd);
assert!(fds.get("127.0.0.1:9090").is_none());
drop(fds);
assert!(
matches!(drop_peer.read(&mut [0u8; 1]), Ok(0)),
"the pruned descriptor was not closed"
);
assert!(
matches!(keep_peer.read(&mut [0u8; 1]), Err(e) if e.kind() == ErrorKind::WouldBlock),
"the expected descriptor was closed"
);
drop(unsafe { OwnedFd::from_raw_fd(keep_fd) });
}
}