use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context, Result};
use tokio::io::unix::AsyncFd;
use tokio::io::Interest;
use tokio::process::Child;
use tokio::sync::{watch, Mutex};
use tokio::task::JoinHandle;
use workload_spec::{EnvValue, MeshIdent, WorkloadSpec};
use crate::native::argv;
use crate::socket_custody::{netns_path, SocketCustodian};
use crate::{MeshAssignment, WorkloadState, WorkloadStatus};
const LISTEN_FD_CHILD: RawFd = 3;
const FORK_FAIL_BACKOFF: Duration = Duration::from_secs(1);
const TERM_GRACE: Duration = Duration::from_secs(5);
struct JitHandle {
mesh_ip: Ipv4Addr,
pid: Arc<AtomicU32>,
status: watch::Receiver<WorkloadStatus>,
shutdown: watch::Sender<bool>,
task: JoinHandle<()>,
}
pub struct JitRuntime {
custodian: Arc<SocketCustodian>,
state_dir: PathBuf,
workloads: Mutex<HashMap<String, JitHandle>>,
}
impl JitRuntime {
pub fn new(state_dir: impl Into<PathBuf>) -> Self {
Self {
custodian: Arc::new(SocketCustodian::new()),
state_dir: state_dir.into(),
workloads: Mutex::new(HashMap::new()),
}
}
pub async fn deploy_on_demand(
&self,
spec: &WorkloadSpec,
mesh: &MeshAssignment,
listen_addr: &str,
) -> Result<()> {
let _ = argv(spec)?;
let ident = spec.expose.mesh.identity.clone();
self.teardown_workload(&ident).await?;
let netns = mesh.netns_name.as_deref().map(netns_path);
self.custodian
.bind_and_hold(&ident.0, listen_addr, netns.as_deref())
.with_context(|| {
format!(
"binding on-demand listen socket {listen_addr} for {}",
ident.0
)
})?;
let listen_fd = self
.custodian
.held_raw_fds(&ident.0)
.and_then(|fds| fds.into_iter().next())
.ok_or_else(|| anyhow!("custodian holds no fd for {} after bind_and_hold", ident.0))?;
let pid = Arc::new(AtomicU32::new(0));
let (status_tx, status_rx) = watch::channel(WorkloadStatus::Pending);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let task = tokio::spawn(supervise_on_demand(
self.state_dir.clone(),
spec.clone(),
mesh.mesh_ip,
listen_fd,
Arc::clone(&pid),
status_tx,
shutdown_rx,
));
self.workloads.lock().await.insert(
ident.0.clone(),
JitHandle {
mesh_ip: mesh.mesh_ip,
pid,
status: status_rx,
shutdown: shutdown_tx,
task,
},
);
Ok(())
}
pub async fn list_workloads(&self) -> Vec<WorkloadState> {
let map = self.workloads.lock().await;
map.iter()
.map(|(ident, h)| WorkloadState {
ident: MeshIdent(ident.clone()),
container_id: format!("jit-{}", h.pid.load(Ordering::SeqCst)),
status: h.status.borrow().clone(),
mesh_ip: Some(h.mesh_ip),
})
.collect()
}
pub async fn get_workload(&self, ident: &MeshIdent) -> Option<WorkloadState> {
let map = self.workloads.lock().await;
map.get(&ident.0).map(|h| WorkloadState {
ident: ident.clone(),
container_id: format!("jit-{}", h.pid.load(Ordering::SeqCst)),
status: h.status.borrow().clone(),
mesh_ip: Some(h.mesh_ip),
})
}
pub async fn holds(&self, ident: &MeshIdent) -> bool {
self.workloads.lock().await.contains_key(&ident.0)
}
pub async fn teardown_workload(&self, ident: &MeshIdent) -> Result<()> {
let handle = self.workloads.lock().await.remove(&ident.0);
if let Some(handle) = handle {
let _ = handle.shutdown.send(true);
let _ = handle.task.await;
}
self.custodian.release(&ident.0);
Ok(())
}
}
struct BorrowedListenFd(RawFd);
impl AsRawFd for BorrowedListenFd {
fn as_raw_fd(&self) -> RawFd {
self.0
}
}
async fn wait_for_connection(listen_fd: RawFd) -> Result<()> {
let afd = AsyncFd::with_interest(BorrowedListenFd(listen_fd), Interest::READABLE)
.context("registering on-demand listener with the reactor")?;
let _guard = afd
.readable()
.await
.context("awaiting listener readability")?;
Ok(())
}
async fn supervise_on_demand(
state_dir: PathBuf,
spec: WorkloadSpec,
mesh_ip: Ipv4Addr,
listen_fd: RawFd,
pid: Arc<AtomicU32>,
status_tx: watch::Sender<WorkloadStatus>,
mut shutdown: watch::Receiver<bool>,
) {
loop {
pid.store(0, Ordering::SeqCst);
let _ = status_tx.send(WorkloadStatus::Pending);
tokio::select! {
biased;
_ = shutdown.changed() => {
if *shutdown.borrow() { return; }
}
r = wait_for_connection(listen_fd) => {
if let Err(e) = r {
let _ = status_tx.send(WorkloadStatus::Failed {
reason: format!("on-demand readable-watch failed: {e:#}"),
});
if backoff_or_shutdown(&mut shutdown, FORK_FAIL_BACKOFF).await {
return;
}
continue;
}
}
}
if *shutdown.borrow() {
return;
}
let mut child = match spawn_jit_child(&state_dir, &spec, mesh_ip, listen_fd) {
Ok(c) => c,
Err(e) => {
let _ = status_tx.send(WorkloadStatus::Failed {
reason: format!("on-demand fork failed: {e:#}"),
});
if backoff_or_shutdown(&mut shutdown, FORK_FAIL_BACKOFF).await {
return;
}
continue;
}
};
pid.store(child.pid, Ordering::SeqCst);
let _ = status_tx.send(WorkloadStatus::Running);
tokio::select! {
biased;
_ = shutdown.changed() => {
if *shutdown.borrow() {
stop_child(&mut child.child, child.pid).await;
return;
}
}
_ = child.child.wait() => {
}
}
}
}
async fn backoff_or_shutdown(shutdown: &mut watch::Receiver<bool>, backoff: Duration) -> bool {
tokio::select! {
biased;
_ = shutdown.changed() => *shutdown.borrow(),
_ = tokio::time::sleep(backoff) => false,
}
}
struct JitChild {
child: Child,
pid: u32,
}
fn spawn_jit_child(
state_dir: &Path,
spec: &WorkloadSpec,
mesh_ip: Ipv4Addr,
listen_fd: RawFd,
) -> Result<JitChild> {
let ident = &spec.expose.mesh.identity;
let argv = argv(spec)?;
let dir = state_dir.join(&ident.0);
std::fs::create_dir_all(&dir)
.with_context(|| format!("creating state dir {}", dir.display()))?;
let open = |name: &str| -> std::io::Result<std::fs::File> {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(dir.join(name))
};
let stdout_file =
open("stdout.log").with_context(|| format!("opening {}/stdout.log", dir.display()))?;
let stderr_file =
open("stderr.log").with_context(|| format!("opening {}/stderr.log", dir.display()))?;
let mut cmd = tokio::process::Command::new(&argv[0]);
cmd.args(&argv[1..])
.stdin(Stdio::null())
.stdout(Stdio::from(stdout_file))
.stderr(Stdio::from(stderr_file))
.env("YAH_MESH_IP", mesh_ip.to_string())
.env("LISTEN_FDS", "1")
.kill_on_drop(false);
if let Some(workdir) = &spec.workdir {
cmd.current_dir(workdir);
}
for e in &spec.env {
if let EnvValue::Literal { value } = &e.value {
cmd.env(&e.name, value);
}
}
unsafe {
cmd.as_std_mut().pre_exec(move || {
if listen_fd != LISTEN_FD_CHILD && libc::dup2(listen_fd, LISTEN_FD_CHILD) < 0 {
return Err(std::io::Error::last_os_error());
}
let flags = libc::fcntl(LISTEN_FD_CHILD, libc::F_GETFD);
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fcntl(LISTEN_FD_CHILD, libc::F_SETFD, flags & !libc::FD_CLOEXEC) < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = cmd
.spawn()
.with_context(|| format!("forking {} for on-demand workload {}", argv[0], spec.name))?;
let pid = child
.id()
.ok_or_else(|| anyhow!("on-demand child for {} exited before pid read", spec.name))?;
Ok(JitChild { child, pid })
}
async fn stop_child(child: &mut Child, pid: u32) {
if matches!(child.try_wait(), Ok(Some(_))) {
return;
}
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
}
if tokio::time::timeout(TERM_GRACE, child.wait())
.await
.is_err()
{
let _ = child.start_kill();
let _ = child.wait().await;
}
}