use std::path::Path;
use std::sync::Arc;
use micromux::{CancellationToken, Handles};
use micromux_control::{
CanonicalConfigPath, ControlServer, ProjectClaim, SessionIdentity, bind_project, claim_project,
endpoint_for, runtime_dir,
};
pub enum SpawnStatus {
Started,
OwnedByOther,
Unavailable,
}
fn session_name(configured: Option<String>, working_dir: &Path) -> String {
configured.unwrap_or_else(|| {
working_dir
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "micromux".to_string())
})
}
pub fn release_at_shutdown(claim: Option<ProjectClaim>, shutdown: CancellationToken) {
if let Some(claim) = claim {
tokio::spawn(async move {
shutdown.cancelled().await;
drop(claim);
});
}
}
pub fn spawn(
handles: &Handles,
claim: Option<ProjectClaim>,
config_path: &CanonicalConfigPath,
working_dir: &Path,
name: Option<String>,
shutdown: CancellationToken,
) -> SpawnStatus {
if !micromux_control::transport_supported() {
tracing::warn!("control plane disabled: unsupported platform");
release_at_shutdown(claim, shutdown);
return SpawnStatus::Unavailable;
}
let Some(runtime_dir) = runtime_dir() else {
tracing::warn!("control plane disabled: no runtime directory could be resolved");
release_at_shutdown(claim, shutdown);
return SpawnStatus::Unavailable;
};
let claim = match claim.map_or_else(|| claim_project(config_path), Ok) {
Ok(claim) => claim,
Err(micromux_control::ClaimError::Held { .. }) => {
tracing::warn!("control plane disabled: another micromux already owns this project");
return SpawnStatus::OwnedByOther;
}
Err(micromux_control::ClaimError::Control(err)) => {
tracing::warn!(?err, "control plane disabled");
return SpawnStatus::Unavailable;
}
};
let endpoint = endpoint_for(&runtime_dir, config_path);
let bound = bind_project(&endpoint, &claim);
release_at_shutdown(Some(claim), shutdown.clone());
let guard = match bound {
Ok(Some(guard)) => guard,
Ok(None) => {
tracing::warn!(
"control plane disabled: another micromux already owns this project's endpoint"
);
return SpawnStatus::OwnedByOther;
}
Err(err) => {
tracing::warn!(?err, "control plane disabled");
return SpawnStatus::Unavailable;
}
};
let identity = SessionIdentity::new(session_name(name, working_dir), working_dir, config_path);
let server = Arc::new(
ControlServer::new(
handles.reader.clone(),
handles.service_control(),
identity,
handles.dynamic_services.clone(),
)
.with_session_status(handles.session.clone()),
);
tracing::info!(endpoint = ?endpoint, "control plane listening");
tokio::spawn(async move {
if let Err(err) = server.serve(guard, shutdown).await {
tracing::warn!(?err, "control server exited with an error");
}
});
SpawnStatus::Started
}
pub async fn resolve_config_path(
explicit: Option<&Path>,
working_dir: &Path,
) -> Result<CanonicalConfigPath, crate::Error> {
let config_path = match explicit {
Some(path) => Some(path.to_path_buf()),
None => micromux::find_config_file(working_dir).await?,
};
let config_path =
config_path.ok_or_else(|| crate::Error::Message("missing config file".to_string()))?;
Ok(CanonicalConfigPath::new(config_path)?)
}
#[cfg(all(test, unix))]
mod tests {
use color_eyre::eyre;
#[tokio::test]
async fn a_claim_is_released_when_the_session_starts_stopping() -> eyre::Result<()> {
let directory = tempfile::tempdir()?;
let lock = directory.path().join("project.session.lock");
let shutdown = micromux::CancellationToken::new();
super::release_at_shutdown(
Some(micromux_control::claim_project_at(&lock)?),
shutdown.clone(),
);
tokio::task::yield_now().await;
eyre::ensure!(
matches!(
micromux_control::claim_project_at(&lock),
Err(micromux_control::ClaimError::Held { .. })
),
"the claim was released while the session ran"
);
shutdown.cancel();
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while micromux_control::claim_project_at(&lock).is_err() {
eyre::ensure!(
tokio::time::Instant::now() < deadline,
"the claim outlived the shutdown"
);
tokio::task::yield_now().await;
}
Ok(())
}
}