perf(async): remove blocking std::process::Command from async paths
Every production process spawn reachable from a tokio worker now uses tokio::process: the install path's podman-port probe, the dependencies disk check, factory-reset restart, config host-IP detection, the orchestrator's host-facts helpers (resolve_dynamic_env and its call sites made async to carry it through), and AutoRuntime's podman/docker probes. The FIPS transport probe is the special case: is_available() is a sync trait method called from async route(), so instead of blocking ~50ms on systemctl per stale-cache hit it now serves the cached value and refreshes on a background thread (stale-while-revalidate) — bounded staleness, zero stalled workers. §C of the 1.8.0 hardening plan; container/transport/config/package suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
01cbec27ed
commit
4c75bb3d38
@ -472,7 +472,7 @@ pub(super) async fn check_bitcoin_pruning_compatibility(package_id: &str) -> Res
|
|||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if detect_disk_gb() < ARCHIVAL_BITCOIN_DISK_GB {
|
if detect_disk_gb().await < ARCHIVAL_BITCOIN_DISK_GB {
|
||||||
anyhow::bail!(archival_bitcoin_required_message(package_id));
|
anyhow::bail!(archival_bitcoin_required_message(package_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,10 +497,11 @@ fn check_blockchain_info_for_pruning(package_id: &str, json: &serde_json::Value)
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_disk_gb() -> u64 {
|
async fn detect_disk_gb() -> u64 {
|
||||||
let output = std::process::Command::new("df")
|
let output = tokio::process::Command::new("df")
|
||||||
.args(["-BG", "/var/lib/archipelago"])
|
.args(["-BG", "/var/lib/archipelago"])
|
||||||
.output();
|
.output()
|
||||||
|
.await;
|
||||||
let Ok(output) = output else {
|
let Ok(output) = output else {
|
||||||
return u64::MAX;
|
return u64::MAX;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2196,13 +2196,14 @@ async fn ensure_host_port_listener(
|
|||||||
container_name: &str,
|
container_name: &str,
|
||||||
runtime_ports: &[String],
|
runtime_ports: &[String],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let Some(port) = runtime_ports
|
let mut port = runtime_ports
|
||||||
.first()
|
.first()
|
||||||
.and_then(|p| p.split(':').next())
|
.and_then(|p| p.split(':').next())
|
||||||
.and_then(|p| p.parse::<u16>().ok())
|
.and_then(|p| p.parse::<u16>().ok());
|
||||||
.or_else(|| published_host_port(container_name))
|
if port.is_none() {
|
||||||
.or_else(|| required_host_port(package_id))
|
port = published_host_port(container_name).await;
|
||||||
else {
|
}
|
||||||
|
let Some(port) = port.or_else(|| required_host_port(package_id)) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2248,10 +2249,11 @@ async fn ensure_host_port_listener(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn published_host_port(container_name: &str) -> Option<u16> {
|
async fn published_host_port(container_name: &str) -> Option<u16> {
|
||||||
let output = std::process::Command::new("podman")
|
let output = tokio::process::Command::new("podman")
|
||||||
.args(["port", container_name])
|
.args(["port", container_name])
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.ok()?;
|
.ok()?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
@ -575,7 +575,7 @@ impl RpcHandler {
|
|||||||
// Restart the service via systemd
|
// Restart the service via systemd
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
let _ = std::process::Command::new("sudo")
|
let _ = tokio::process::Command::new("sudo")
|
||||||
.args(["systemctl", "restart", "archipelago"])
|
.args(["systemctl", "restart", "archipelago"])
|
||||||
.spawn();
|
.spawn();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -81,10 +81,11 @@ pub struct Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Detect primary host IP (first non-loopback IPv4)
|
/// Detect primary host IP (first non-loopback IPv4)
|
||||||
fn detect_host_ip() -> Result<String> {
|
async fn detect_host_ip() -> Result<String> {
|
||||||
let output = std::process::Command::new("hostname")
|
let output = tokio::process::Command::new("hostname")
|
||||||
.args(["-I"])
|
.args(["-I"])
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.context("Failed to run hostname -I")?;
|
.context("Failed to run hostname -I")?;
|
||||||
let s = String::from_utf8_lossy(&output.stdout);
|
let s = String::from_utf8_lossy(&output.stdout);
|
||||||
let ip = s
|
let ip = s
|
||||||
@ -210,7 +211,9 @@ impl Config {
|
|||||||
if let Ok(ip) = std::env::var("ARCHIPELAGO_HOST_IP") {
|
if let Ok(ip) = std::env::var("ARCHIPELAGO_HOST_IP") {
|
||||||
config.host_ip = ip;
|
config.host_ip = ip;
|
||||||
} else {
|
} else {
|
||||||
config.host_ip = Self::detect_host_ip().unwrap_or_else(|_| "127.0.0.1".to_string());
|
config.host_ip = Self::detect_host_ip()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Ensure data directory exists
|
||||||
|
|||||||
@ -1440,7 +1440,7 @@ impl ProdContainerOrchestrator {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
let mut report = ReconcileReport::default();
|
let mut report = ReconcileReport::default();
|
||||||
let disk_gb = self.disk_gb();
|
let disk_gb = self.disk_gb().await;
|
||||||
// Register every candidate before the (sequential, possibly slow)
|
// Register every candidate before the (sequential, possibly slow)
|
||||||
// pass so the scanner overlays queued-but-down apps as Restarting
|
// pass so the scanner overlays queued-but-down apps as Restarting
|
||||||
// instead of Stopped. Each app is deregistered as its turn finishes,
|
// instead of Stopped. Each app is deregistered as its turn finishes,
|
||||||
@ -1620,7 +1620,7 @@ impl ProdContainerOrchestrator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut resolved_manifest = lm.manifest.clone();
|
let mut resolved_manifest = lm.manifest.clone();
|
||||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
self.resolve_dynamic_env(&mut resolved_manifest).await?;
|
||||||
let name = compute_container_name(&lm.manifest);
|
let name = compute_container_name(&lm.manifest);
|
||||||
|
|
||||||
// An explicitly user-stopped app MUST stay stopped. The reconcile filter
|
// An explicitly user-stopped app MUST stay stopped. The reconcile filter
|
||||||
@ -1970,7 +1970,7 @@ impl ProdContainerOrchestrator {
|
|||||||
async fn install_fresh(&self, lm: &LoadedManifest) -> Result<()> {
|
async fn install_fresh(&self, lm: &LoadedManifest) -> Result<()> {
|
||||||
self.ensure_app_secrets(&lm.manifest.app.id).await?;
|
self.ensure_app_secrets(&lm.manifest.app.id).await?;
|
||||||
let mut resolved_manifest = lm.manifest.clone();
|
let mut resolved_manifest = lm.manifest.clone();
|
||||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
self.resolve_dynamic_env(&mut resolved_manifest).await?;
|
||||||
resolve_catalog_image(&mut resolved_manifest);
|
resolve_catalog_image(&mut resolved_manifest);
|
||||||
|
|
||||||
let resolved = resolved_manifest.app.container.resolve().ok_or_else(|| {
|
let resolved = resolved_manifest.app.container.resolve().ok_or_else(|| {
|
||||||
@ -2263,7 +2263,7 @@ impl ProdContainerOrchestrator {
|
|||||||
// Re-render the manifest with dynamic env baked in, then go
|
// Re-render the manifest with dynamic env baked in, then go
|
||||||
// through the same install path a fresh install would.
|
// through the same install path a fresh install would.
|
||||||
let mut resolved = lm.manifest.clone();
|
let mut resolved = lm.manifest.clone();
|
||||||
self.resolve_dynamic_env(&mut resolved)?;
|
self.resolve_dynamic_env(&mut resolved).await?;
|
||||||
self.install_via_quadlet(&resolved, name)
|
self.install_via_quadlet(&resolved, name)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("Phase 3.3: re-install {name} via Quadlet"))?;
|
.with_context(|| format!("Phase 3.3: re-install {name} via Quadlet"))?;
|
||||||
@ -2329,7 +2329,7 @@ impl ProdContainerOrchestrator {
|
|||||||
let restart_required = quadlet::contains_stale_health_gate(&old_body);
|
let restart_required = quadlet::contains_stale_health_gate(&old_body);
|
||||||
|
|
||||||
let mut resolved = lm.manifest.clone();
|
let mut resolved = lm.manifest.clone();
|
||||||
self.resolve_dynamic_env(&mut resolved)?;
|
self.resolve_dynamic_env(&mut resolved).await?;
|
||||||
// Same catalog/pinned-version image resolution the installer applies, so
|
// Same catalog/pinned-version image resolution the installer applies, so
|
||||||
// the drift re-render doesn't revert a pinned version back to the
|
// the drift re-render doesn't revert a pinned version back to the
|
||||||
// manifest's shipped `:latest` tag on the next reconcile tick.
|
// manifest's shipped `:latest` tag on the next reconcile tick.
|
||||||
@ -2404,7 +2404,7 @@ impl ProdContainerOrchestrator {
|
|||||||
|
|
||||||
for dep in dependencies {
|
for dep in dependencies {
|
||||||
let mut resolved = dep.manifest.clone();
|
let mut resolved = dep.manifest.clone();
|
||||||
self.resolve_dynamic_env(&mut resolved)?;
|
self.resolve_dynamic_env(&mut resolved).await?;
|
||||||
let name = compute_container_name(&dep.manifest);
|
let name = compute_container_name(&dep.manifest);
|
||||||
if self.runtime.get_container_status(&name).await.is_err() {
|
if self.runtime.get_container_status(&name).await.is_err() {
|
||||||
continue;
|
continue;
|
||||||
@ -2588,7 +2588,7 @@ impl ProdContainerOrchestrator {
|
|||||||
}
|
}
|
||||||
.read("bitcoin-rpc-password")
|
.read("bitcoin-rpc-password")
|
||||||
.context("lnd pre-start: read bitcoin RPC password")?;
|
.context("lnd pre-start: read bitcoin RPC password")?;
|
||||||
let bitcoin_host = self.bitcoin_host();
|
let bitcoin_host = self.bitcoin_host().await;
|
||||||
let outcome = lnd::ensure_config(&self.lnd_paths, &rpc_pass, &bitcoin_host)
|
let outcome = lnd::ensure_config(&self.lnd_paths, &rpc_pass, &bitcoin_host)
|
||||||
.await
|
.await
|
||||||
.context("lnd pre-start: ensure lnd.conf")?;
|
.context("lnd pre-start: ensure lnd.conf")?;
|
||||||
@ -2706,10 +2706,12 @@ impl ProdContainerOrchestrator {
|
|||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_host_facts(&self) -> HostFacts {
|
async fn detect_host_facts(&self) -> HostFacts {
|
||||||
let host_ip = Self::detect_host_ip().unwrap_or_else(|| "127.0.0.1".to_string());
|
let host_ip = Self::detect_host_ip()
|
||||||
let host_mdns = Self::detect_host_mdns();
|
.await
|
||||||
let disk_gb = self.disk_gb();
|
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||||
|
let host_mdns = Self::detect_host_mdns().await;
|
||||||
|
let disk_gb = self.disk_gb().await;
|
||||||
HostFacts {
|
HostFacts {
|
||||||
host_ip,
|
host_ip,
|
||||||
host_mdns,
|
host_mdns,
|
||||||
@ -2723,9 +2725,8 @@ impl ProdContainerOrchestrator {
|
|||||||
|
|
||||||
/// Container name of the running Bitcoin node (`bitcoin-knots` or
|
/// Container name of the running Bitcoin node (`bitcoin-knots` or
|
||||||
/// `bitcoin-core`) for the `{{BITCOIN_HOST}}` derived-env placeholder.
|
/// `bitcoin-core`) for the `{{BITCOIN_HOST}}` derived-env placeholder.
|
||||||
/// Synchronous `podman ps` to match the surrounding host-fact detection;
|
/// Defaults to `bitcoin-knots` when none is running (B12).
|
||||||
/// defaults to `bitcoin-knots` when none is running (B12).
|
async fn bitcoin_host(&self) -> String {
|
||||||
fn bitcoin_host(&self) -> String {
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(host) = &self.test_bitcoin_host {
|
if let Some(host) = &self.test_bitcoin_host {
|
||||||
return host.clone();
|
return host.clone();
|
||||||
@ -2733,9 +2734,10 @@ impl ProdContainerOrchestrator {
|
|||||||
// Mirrors api::rpc::package::dependencies (the legacy install path);
|
// Mirrors api::rpc::package::dependencies (the legacy install path);
|
||||||
// both Bitcoin node variants are reachable on archy-net by name.
|
// both Bitcoin node variants are reachable on archy-net by name.
|
||||||
const BITCOIN_NAMES: &[&str] = &["bitcoin-knots", "bitcoin-core", "bitcoin"];
|
const BITCOIN_NAMES: &[&str] = &["bitcoin-knots", "bitcoin-core", "bitcoin"];
|
||||||
let names = Command::new("podman")
|
let names = tokio::process::Command::new("podman")
|
||||||
.args(["ps", "--format", "{{.Names}}"])
|
.args(["ps", "--format", "{{.Names}}"])
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|o| o.status.success())
|
.filter(|o| o.status.success())
|
||||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||||
@ -2753,8 +2755,12 @@ impl ProdContainerOrchestrator {
|
|||||||
self.test_bitcoin_host = Some(host.to_string());
|
self.test_bitcoin_host = Some(host.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_host_ip() -> Option<String> {
|
async fn detect_host_ip() -> Option<String> {
|
||||||
let output = Command::new("hostname").arg("-I").output().ok()?;
|
let output = tokio::process::Command::new("hostname")
|
||||||
|
.arg("-I")
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -2762,9 +2768,10 @@ impl ProdContainerOrchestrator {
|
|||||||
stdout.split_whitespace().next().map(|s| s.to_string())
|
stdout.split_whitespace().next().map(|s| s.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_host_mdns() -> String {
|
async fn detect_host_mdns() -> String {
|
||||||
let hostname = Command::new("hostname")
|
let hostname = tokio::process::Command::new("hostname")
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|o| {
|
.and_then(|o| {
|
||||||
if o.status.success() {
|
if o.status.success() {
|
||||||
@ -2782,13 +2789,18 @@ impl ProdContainerOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_disk_gb() -> u64 {
|
async fn detect_disk_gb() -> u64 {
|
||||||
let target = if Path::new("/var/lib/archipelago").exists() {
|
let target = if Path::new("/var/lib/archipelago").exists() {
|
||||||
"/var/lib/archipelago"
|
"/var/lib/archipelago"
|
||||||
} else {
|
} else {
|
||||||
"/"
|
"/"
|
||||||
};
|
};
|
||||||
let output = match Command::new("df").arg("-k").arg(target).output() {
|
let output = match tokio::process::Command::new("df")
|
||||||
|
.arg("-k")
|
||||||
|
.arg(target)
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(o) if o.status.success() => o,
|
Ok(o) if o.status.success() => o,
|
||||||
_ => return 0,
|
_ => return 0,
|
||||||
};
|
};
|
||||||
@ -2808,12 +2820,12 @@ impl ProdContainerOrchestrator {
|
|||||||
kb / 1_000_000
|
kb / 1_000_000
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disk_gb(&self) -> u64 {
|
async fn disk_gb(&self) -> u64 {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(disk_gb) = self.test_disk_gb {
|
if let Some(disk_gb) = self.test_disk_gb {
|
||||||
return disk_gb;
|
return disk_gb;
|
||||||
}
|
}
|
||||||
Self::detect_disk_gb()
|
Self::detect_disk_gb().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure app-specific secrets exist *before* env resolution. The Bitcoin
|
/// Ensure app-specific secrets exist *before* env resolution. The Bitcoin
|
||||||
@ -2838,14 +2850,14 @@ impl ProdContainerOrchestrator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> {
|
async fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> {
|
||||||
// Materialise any manifest-declared generated secrets before they're
|
// Materialise any manifest-declared generated secrets before they're
|
||||||
// read below. This is the single chokepoint every install/reconcile
|
// read below. This is the single chokepoint every install/reconcile
|
||||||
// path funnels through, so an app's secrets exist by the time its
|
// path funnels through, so an app's secrets exist by the time its
|
||||||
// `secret_env` resolves — no per-app code, no host provisioning.
|
// `secret_env` resolves — no per-app code, no host provisioning.
|
||||||
crate::container::secrets::ensure_generated_secrets(&self.secrets_dir, manifest)?;
|
crate::container::secrets::ensure_generated_secrets(&self.secrets_dir, manifest)?;
|
||||||
|
|
||||||
let mut facts = self.detect_host_facts();
|
let mut facts = self.detect_host_facts().await;
|
||||||
// Only pay the podman cost to detect Knots-vs-Core when this manifest
|
// Only pay the podman cost to detect Knots-vs-Core when this manifest
|
||||||
// actually templates the Bitcoin node into its env (mempool — B12).
|
// actually templates the Bitcoin node into its env (mempool — B12).
|
||||||
if manifest
|
if manifest
|
||||||
@ -2855,7 +2867,7 @@ impl ProdContainerOrchestrator {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|e| e.template.contains("{{BITCOIN_HOST}}"))
|
.any(|e| e.template.contains("{{BITCOIN_HOST}}"))
|
||||||
{
|
{
|
||||||
facts.bitcoin_host = self.bitcoin_host();
|
facts.bitcoin_host = self.bitcoin_host().await;
|
||||||
}
|
}
|
||||||
let mut env = manifest.app.environment.clone();
|
let mut env = manifest.app.environment.clone();
|
||||||
env.extend(manifest.app.container.resolve_derived_env(&facts));
|
env.extend(manifest.app.container.resolve_derived_env(&facts));
|
||||||
@ -3208,7 +3220,7 @@ impl ProdContainerOrchestrator {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut out = content.to_string();
|
let mut out = content.to_string();
|
||||||
if out.contains("{{HOST_IP}}") || out.contains("{{HOST_MDNS}}") {
|
if out.contains("{{HOST_IP}}") || out.contains("{{HOST_MDNS}}") {
|
||||||
let facts = self.detect_host_facts();
|
let facts = self.detect_host_facts().await;
|
||||||
out = out
|
out = out
|
||||||
.replace("{{HOST_IP}}", &facts.host_ip)
|
.replace("{{HOST_IP}}", &facts.host_ip)
|
||||||
.replace("{{HOST_MDNS}}", &facts.host_mdns);
|
.replace("{{HOST_MDNS}}", &facts.host_mdns);
|
||||||
@ -3297,7 +3309,7 @@ impl ProdContainerOrchestrator {
|
|||||||
/// however the box is reached locally. (Generalised from the old per-app
|
/// however the box is reached locally. (Generalised from the old per-app
|
||||||
/// netbird TLS helper, deleted in #20 ph4: rsa:2048, 10-year, no per-app Rust.)
|
/// netbird TLS helper, deleted in #20 ph4: rsa:2048, 10-year, no per-app Rust.)
|
||||||
async fn ensure_manifest_certs(&self, manifest: &AppManifest) -> Result<()> {
|
async fn ensure_manifest_certs(&self, manifest: &AppManifest) -> Result<()> {
|
||||||
let facts = self.detect_host_facts();
|
let facts = self.detect_host_facts().await;
|
||||||
let render = |s: &str| {
|
let render = |s: &str| {
|
||||||
s.replace("{{HOST_IP}}", &facts.host_ip)
|
s.replace("{{HOST_IP}}", &facts.host_ip)
|
||||||
.replace("{{HOST_MDNS}}", &facts.host_mdns)
|
.replace("{{HOST_MDNS}}", &facts.host_mdns)
|
||||||
@ -3594,7 +3606,7 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
|||||||
self.ensure_app_secrets(app_id).await?;
|
self.ensure_app_secrets(app_id).await?;
|
||||||
let name = compute_container_name(&lm.manifest);
|
let name = compute_container_name(&lm.manifest);
|
||||||
let mut resolved_manifest = lm.manifest.clone();
|
let mut resolved_manifest = lm.manifest.clone();
|
||||||
self.resolve_dynamic_env(&mut resolved_manifest)?;
|
self.resolve_dynamic_env(&mut resolved_manifest).await?;
|
||||||
|
|
||||||
let service = format!("{name}.service");
|
let service = format!("{name}.service");
|
||||||
if self.quadlet_unit_exists(&name).await? {
|
if self.quadlet_unit_exists(&name).await? {
|
||||||
@ -4328,7 +4340,7 @@ app:
|
|||||||
orch.set_bitcoin_host_for_test(node);
|
orch.set_bitcoin_host_for_test(node);
|
||||||
|
|
||||||
let mut manifest = AppManifest::parse(yaml).unwrap();
|
let mut manifest = AppManifest::parse(yaml).unwrap();
|
||||||
orch.resolve_dynamic_env(&mut manifest).unwrap();
|
orch.resolve_dynamic_env(&mut manifest).await.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
manifest
|
manifest
|
||||||
|
|||||||
@ -23,26 +23,36 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|||||||
/// TTL keeps the result responsive to daemon flaps without pounding DBus.
|
/// TTL keeps the result responsive to daemon flaps without pounding DBus.
|
||||||
const AVAILABILITY_CACHE_TTL: Duration = Duration::from_secs(10);
|
const AVAILABILITY_CACHE_TTL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// Availability cache shared with the background probe thread, so the
|
||||||
|
/// sync `is_available()` hot path never blocks on `systemctl`.
|
||||||
|
struct AvailabilityCache {
|
||||||
|
available: AtomicBool,
|
||||||
|
probed_at_ms: AtomicU64,
|
||||||
|
probe_in_flight: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FipsTransport {
|
pub struct FipsTransport {
|
||||||
identity_dir: PathBuf,
|
identity_dir: PathBuf,
|
||||||
available_cached: AtomicBool,
|
availability: std::sync::Arc<AvailabilityCache>,
|
||||||
available_cached_at_ms: AtomicU64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FipsTransport {
|
impl FipsTransport {
|
||||||
pub fn new(identity_dir: &Path) -> Self {
|
pub fn new(identity_dir: &Path) -> Self {
|
||||||
Self {
|
Self {
|
||||||
identity_dir: identity_dir.to_path_buf(),
|
identity_dir: identity_dir.to_path_buf(),
|
||||||
available_cached: AtomicBool::new(false),
|
availability: std::sync::Arc::new(AvailabilityCache {
|
||||||
available_cached_at_ms: AtomicU64::new(0),
|
available: AtomicBool::new(false),
|
||||||
|
probed_at_ms: AtomicU64::new(0),
|
||||||
|
probe_in_flight: AtomicBool::new(false),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn probe_daemon_active() -> bool {
|
fn probe_daemon_active() -> bool {
|
||||||
// Cheap blocking probe: spawn `systemctl is-active` synchronously.
|
// Blocking probe — only ever run on a dedicated background thread
|
||||||
// Short-circuit if either the archipelago-managed unit or the
|
// (see is_available), never on a tokio worker. Short-circuit if
|
||||||
// upstream fips.service is active — legacy/dev nodes run only the
|
// either the archipelago-managed unit or the upstream fips.service
|
||||||
// upstream unit.
|
// is active — legacy/dev nodes run only the upstream unit.
|
||||||
for unit in [
|
for unit in [
|
||||||
crate::fips::SERVICE_UNIT,
|
crate::fips::SERVICE_UNIT,
|
||||||
crate::fips::UPSTREAM_SERVICE_UNIT,
|
crate::fips::UPSTREAM_SERVICE_UNIT,
|
||||||
@ -70,14 +80,30 @@ impl NodeTransport for FipsTransport {
|
|||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_millis() as u64)
|
.map(|d| d.as_millis() as u64)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let cached_at = self.available_cached_at_ms.load(Ordering::Relaxed);
|
let cached_at = self.availability.probed_at_ms.load(Ordering::Relaxed);
|
||||||
|
let cached = self.availability.available.load(Ordering::Relaxed);
|
||||||
if now_ms.saturating_sub(cached_at) < AVAILABILITY_CACHE_TTL.as_millis() as u64 {
|
if now_ms.saturating_sub(cached_at) < AVAILABILITY_CACHE_TTL.as_millis() as u64 {
|
||||||
return self.available_cached.load(Ordering::Relaxed);
|
return cached;
|
||||||
}
|
}
|
||||||
|
// Cache is stale. This sync trait method is called from async
|
||||||
|
// route(), so running the ~50ms systemctl probe inline stalls a
|
||||||
|
// tokio worker. Serve the stale value and refresh on a background
|
||||||
|
// thread instead — the transport supervisor's warm loop keeps this
|
||||||
|
// fresh in steady state, so staleness is bounded to one probe round.
|
||||||
|
let cache = std::sync::Arc::clone(&self.availability);
|
||||||
|
if !cache.probe_in_flight.swap(true, Ordering::Relaxed) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
let val = Self::probe_daemon_active();
|
let val = Self::probe_daemon_active();
|
||||||
self.available_cached.store(val, Ordering::Relaxed);
|
let probed_ms = SystemTime::now()
|
||||||
self.available_cached_at_ms.store(now_ms, Ordering::Relaxed);
|
.duration_since(UNIX_EPOCH)
|
||||||
val
|
.map(|d| d.as_millis() as u64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
cache.available.store(val, Ordering::Relaxed);
|
||||||
|
cache.probed_at_ms.store(probed_ms, Ordering::Relaxed);
|
||||||
|
cache.probe_in_flight.store(false, Ordering::Relaxed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cached
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send<'a>(
|
fn send<'a>(
|
||||||
|
|||||||
@ -853,11 +853,11 @@ pub struct AutoRuntime {
|
|||||||
impl AutoRuntime {
|
impl AutoRuntime {
|
||||||
pub async fn new(user: String) -> Result<Self> {
|
pub async fn new(user: String) -> Result<Self> {
|
||||||
// Try Podman first
|
// Try Podman first
|
||||||
if Self::check_podman_available() {
|
if Self::check_podman_available().await {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
runtime: Box::new(PodmanRuntime::new(user)),
|
runtime: Box::new(PodmanRuntime::new(user)),
|
||||||
})
|
})
|
||||||
} else if Self::check_docker_available() {
|
} else if Self::check_docker_available().await {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
runtime: Box::new(DockerRuntime::new(user)),
|
runtime: Box::new(DockerRuntime::new(user)),
|
||||||
})
|
})
|
||||||
@ -866,12 +866,20 @@ impl AutoRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_podman_available() -> bool {
|
async fn check_podman_available() -> bool {
|
||||||
Command::new("podman").arg("--version").output().is_ok()
|
TokioCommand::new("podman")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_docker_available() -> bool {
|
async fn check_docker_available() -> bool {
|
||||||
Command::new("docker").arg("--version").output().is_ok()
|
TokioCommand::new("docker")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -114,11 +114,16 @@ modules; production request/boot paths are essentially panic-free. The real risk
|
|||||||
sweep (`scheduler.rs`), block-header cache (`mesh/mod.rs`), 7× peer-transport badge
|
sweep (`scheduler.rs`), block-header cache (`mesh/mod.rs`), 7× peer-transport badge
|
||||||
(`sync.rs` + `content.rs`). Federation tombstone/untombstone upgraded to hard errors
|
(`sync.rs` + `content.rs`). Federation tombstone/untombstone upgraded to hard errors
|
||||||
(see §I). Install-log line write left fire-and-forget with an explanatory comment.
|
(see §I). Install-log line write left fire-and-forget with an explanatory comment.
|
||||||
- [ ] 🟠 **Remove blocking `std::process::Command` from async handlers.**
|
- [x] 🟠 **Remove blocking `std::process::Command` from async handlers.** DONE 2026-07-03:
|
||||||
`install.rs:2222` `published_host_port` (sync podman on the install path),
|
converted to `tokio::process` — `published_host_port` (install), `detect_disk_gb`
|
||||||
`dependencies.rs:316` (`df`), `system/handlers.rs:578` (`sudo`), `transport/fips.rs:50`
|
(dependencies), factory-reset restart (system/handlers), `config.rs detect_host_ip`,
|
||||||
(`systemctl`) stall tokio workers under load. Convert to `tokio::process` or
|
the orchestrator host-facts helpers (`detect_host_ip/mdns/disk_gb`, `bitcoin_host`,
|
||||||
`spawn_blocking`. Only 8 files use `std::process::Command` — bounded.
|
`resolve_dynamic_env` now async through all 6 call sites), and `AutoRuntime::new`
|
||||||
|
probes. `transport/fips.rs is_available()` (sync trait method on the async route path)
|
||||||
|
now serves the cached value and refreshes via a background thread (stale-while-
|
||||||
|
revalidate) instead of blocking on systemctl. `image_verifier.rs` cosign sites have no
|
||||||
|
callers yet — handled with the §A cosign item. Tests: container 155 / transport 29 /
|
||||||
|
config 29 / package 46 all green.
|
||||||
- [ ] 🟡 **Restrict Bitcoin RPC exposure.** `bootstrap.rs:409` writes
|
- [ ] 🟡 **Restrict Bitcoin RPC exposure.** `bootstrap.rs:409` writes
|
||||||
`rpcallowip=0.0.0.0/0`. Scope to the container subnet / `127.0.0.1`.
|
`rpcallowip=0.0.0.0/0`. Scope to the container subnet / `127.0.0.1`.
|
||||||
- [ ] 🟡 **Move generated secrets from env to file mounts.** `manifest.rs:1208-1226`
|
- [ ] 🟡 **Move generated secrets from env to file mounts.** `manifest.rs:1208-1226`
|
||||||
@ -159,9 +164,11 @@ The real issues are the app-bridge origin model and a bloated bundle.
|
|||||||
(precached by the service worker → blocks PWA install), plus ~18 MB of ~1 MB full-screen
|
(precached by the service worker → blocks PWA install), plus ~18 MB of ~1 MB full-screen
|
||||||
JPEGs. Convert backgrounds to WebP/AVIF at responsive sizes, lazy/stream the intro video,
|
JPEGs. Convert backgrounds to WebP/AVIF at responsive sizes, lazy/stream the intro video,
|
||||||
and exclude video/audio from the Workbox precache. Biggest, easiest perf win.
|
and exclude video/audio from the Workbox precache. Biggest, easiest perf win.
|
||||||
- [ ] 🟢 **DOMPurify the `Server.vue` QR SVG** (`:283/:295` render `v-html` unsanitized while
|
- [x] 🟢 **DOMPurify the `Server.vue` QR SVG / guard `Mesh.vue` pollInterval / surface
|
||||||
`TwoFactorSection.vue` sanitizes the analogous SVG); guard the unguarded `pollInterval`
|
`curatedApps.ts` fetch failures.** DONE 2026-07-03: WireGuard peer QR now sanitized with
|
||||||
(`Mesh.vue:391`); surface silent data-fetch failures (`curatedApps.ts:58/71`).
|
the same `USE_PROFILES: {svg}` call as TwoFactorSection; Mesh poll interval guarded +
|
||||||
|
nulled on unmount; catalog fetch failures log per-URL console.warn incl. the
|
||||||
|
all-sources-failed fallback. Bundle-verified.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user