use std::{
ffi::OsString,
fs::{File, read_link},
io::{Error as IoError, Write},
os::{fd::BorrowedFd, unix::io::RawFd},
path::PathBuf,
};
#[cfg(target_os = "linux")]
use libc::{cpu_set_t, pid_t};
use nix::{
errno::Errno,
fcntl::{FcntlArg, FdFlag, fcntl},
};
use sozu_command_lib::config::Config;
use sozu_lib::metrics::{self, MetricError};
use crate::{cli, command};
#[derive(thiserror::Error, Debug)]
pub enum UtilError {
#[error("could not get flags (F_GETFD) on file descriptor {0}: {1}")]
GetFlags(RawFd, Errno),
#[error("could not convert flags for file descriptor {0}")]
ConvertFlags(RawFd),
#[error("could not set flags for file descriptor {0}: {1}")]
SetFlags(RawFd, Errno),
#[error("could not create pid file {0}: {1}")]
CreatePidFile(String, IoError),
#[error("could not write pid file {0}: {1}")]
WritePidFile(String, IoError),
#[error("could not sync pid file {0}: {1}")]
SyncPidFile(String, IoError),
#[error("Failed to convert PathBuf {0} to String: {1:?}")]
OsString(PathBuf, OsString),
#[error("could not read file {0}: {1}")]
Read(String, IoError),
#[error("failed to retrieve current executable path: {0}")]
CurrentExe(IoError),
#[error("could not setup metrics: {0}")]
SetupMetrics(MetricError),
#[error(
"Configuration file hasn't been specified. Either use -c with the start command,
or use the SOZU_CONFIG environment variable when building sozu."
)]
GetConfigFilePath,
}
pub fn enable_close_on_exec(raw_fd: RawFd) -> Result<i32, UtilError> {
let fd = unsafe { BorrowedFd::borrow_raw(raw_fd) };
let old_flags =
fcntl(fd, FcntlArg::F_GETFD).map_err(|err_no| UtilError::GetFlags(raw_fd, err_no))?;
let mut new_flags = FdFlag::from_bits(old_flags).ok_or(UtilError::ConvertFlags(raw_fd))?;
new_flags.insert(FdFlag::FD_CLOEXEC);
fcntl(fd, FcntlArg::F_SETFD(new_flags)).map_err(|err_no| UtilError::SetFlags(raw_fd, err_no))
}
pub fn disable_close_on_exec(raw_fd: RawFd) -> Result<i32, UtilError> {
let fd = unsafe { BorrowedFd::borrow_raw(raw_fd) };
let old_flags =
fcntl(fd, FcntlArg::F_GETFD).map_err(|err_no| UtilError::GetFlags(raw_fd, err_no))?;
let mut new_flags = FdFlag::from_bits(old_flags).ok_or(UtilError::ConvertFlags(raw_fd))?;
new_flags.remove(FdFlag::FD_CLOEXEC);
fcntl(fd, FcntlArg::F_SETFD(new_flags)).map_err(|err_no| UtilError::SetFlags(raw_fd, err_no))
}
pub fn setup_metrics(config: &Config) -> Result<(), UtilError> {
if let Some(metrics) = config.metrics.as_ref() {
return metrics::setup(
&metrics.address,
"MAIN",
metrics.tagged_metrics,
metrics.prefix.clone(),
metrics.detail,
)
.map_err(UtilError::SetupMetrics);
}
Ok(())
}
pub fn write_pid_file(config: &Config) -> Result<(), UtilError> {
let pid_file_path: Option<&str> = config
.pid_file_path
.as_ref()
.map(|pid_file_path| pid_file_path.as_ref());
if let Some(path) = pid_file_path {
let mut file = File::create(path)
.map_err(|io_err| UtilError::CreatePidFile(path.to_owned(), io_err))?;
let pid = unsafe { libc::getpid() };
file.write_all(format!("{pid}").as_bytes())
.map_err(|write_err| UtilError::WritePidFile(path.to_owned(), write_err))?;
file.sync_all()
.map_err(|sync_err| UtilError::SyncPidFile(path.to_owned(), sync_err))?;
}
Ok(())
}
pub fn get_config_file_path(args: &cli::Args) -> Result<&str, UtilError> {
match args.config.as_ref() {
Some(config_file) => Ok(config_file.as_str()),
None => option_env!("SOZU_CONFIG").ok_or(UtilError::GetConfigFilePath),
}
}
#[cfg(target_os = "freebsd")]
pub unsafe fn get_executable_path() -> Result<String, UtilError> {
use libc::{CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, PATH_MAX};
use libc::{c_void, sysctl};
let mut capacity = PATH_MAX as usize;
let mut path = vec![0; capacity];
let mib: Vec<i32> = vec![CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1];
let len = mib.len() * size_of::<i32>();
let element_size = size_of::<i32>();
let res = sysctl(
mib.as_ptr(),
(len / element_size) as u32,
path.as_mut_ptr() as *mut c_void,
&mut capacity,
std::ptr::null() as *const c_void,
0,
);
if res != 0 {
panic!("Could not retrieve the path of the executable");
}
Ok(String::from_raw_parts(
path.as_mut_ptr(),
capacity - 1,
path.len(),
))
}
#[cfg(target_os = "linux")]
pub unsafe fn get_executable_path() -> Result<String, UtilError> {
let path = read_link("/proc/self/exe")
.map_err(|io_err| UtilError::Read("/proc/self/exe".to_string(), io_err))?;
let mut path_str = path
.clone()
.into_os_string()
.into_string()
.map_err(|string_err| UtilError::OsString(path, string_err))?;
if path_str.ends_with(" (deleted)") {
let len = path_str.len();
path_str.truncate(len - 10)
}
Ok(path_str)
}
#[cfg(target_os = "linux")]
pub fn get_executable_exec_path() -> Result<String, UtilError> {
use std::os::fd::IntoRawFd;
let owned_fd = nix::fcntl::open(
"/proc/self/exe",
nix::fcntl::OFlag::O_PATH | nix::fcntl::OFlag::O_CLOEXEC,
nix::sys::stat::Mode::empty(),
)
.map_err(|errno| {
UtilError::Read(
"/proc/self/exe".to_string(),
IoError::from_raw_os_error(errno as i32),
)
})?;
let raw_fd = owned_fd.into_raw_fd();
Ok(format!("/proc/self/fd/{raw_fd}"))
}
#[cfg(not(target_os = "linux"))]
pub fn get_executable_exec_path() -> Result<String, UtilError> {
unsafe { get_executable_path() }
}
#[cfg(target_os = "macos")]
unsafe extern "C" {
pub fn _NSGetExecutablePath(buf: *mut libc::c_char, size: *mut u32) -> i32;
}
#[cfg(target_os = "macos")]
pub unsafe fn get_executable_path() -> Result<String, UtilError> {
let path = std::env::current_exe().map_err(|io_err| UtilError::CurrentExe(io_err))?;
Ok(path.to_string_lossy().to_string())
}
#[cfg(target_os = "linux")]
pub fn set_workers_affinity(workers: &Vec<command::sessions::WorkerSession>) {
let mut cpu_count = 0;
let max_cpu = num_cpus::get();
if (workers.len() + 1) > max_cpu {
warn!(
"There are more workers than available CPU cores, \
multiple workers will be bound to the same CPU core. \
This may impact performances"
);
}
let main_pid = unsafe { libc::getpid() };
set_process_affinity(main_pid, cpu_count);
cpu_count += 1;
for worker in workers {
if cpu_count >= max_cpu {
cpu_count = 0;
}
set_process_affinity(worker.pid, cpu_count);
cpu_count += 1;
}
}
#[cfg(not(target_os = "linux"))]
pub fn set_workers_affinity(_: &Vec<command::sessions::WorkerSession>) {}
#[cfg(target_os = "linux")]
use std::mem;
#[cfg(target_os = "linux")]
pub fn set_process_affinity(pid: pid_t, cpu: usize) {
unsafe {
let mut cpu_set: cpu_set_t = mem::zeroed();
let size_cpu_set = mem::size_of::<cpu_set_t>();
libc::CPU_SET(cpu, &mut cpu_set);
libc::sched_setaffinity(pid, size_cpu_set, &cpu_set);
debug!("Worker {} bound to CPU core {}", pid, cpu);
};
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn get_executable_exec_path_returns_proc_self_fd_on_linux() {
let path = get_executable_exec_path().expect("open /proc/self/exe O_PATH");
assert!(
path.starts_with("/proc/self/fd/"),
"expected /proc/self/fd/<n>, got {path}"
);
let meta =
std::fs::metadata(&path).unwrap_or_else(|e| panic!("metadata({path}) failed: {e}"));
assert!(
meta.is_file(),
"/proc/self/fd/<n> did not resolve to a regular file"
);
}
#[cfg(target_os = "linux")]
#[test]
fn get_executable_exec_path_distinct_calls_yield_distinct_fds() {
let p1 = get_executable_exec_path().expect("first call");
let p2 = get_executable_exec_path().expect("second call");
assert_ne!(
p1, p2,
"expected distinct fd paths from two opens, got {p1} and {p2}"
);
}
}