diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 69175a54..8b5b056b 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -438,7 +438,47 @@ pub async fn rebuild_no_meta( } run(&["update", &container, "--flake", &flake_ref]).await?; if was_running { - run(&["start", &container]).await + // Normal path: start into the new generation. The activation + // script runs inside the container to transition old → new. + // This can fail when packages are removed between generations — + // the old-generation activation references units that no longer + // exist in the new closure, causing systemd to exit non-zero. + // + // Fallback: stop + kill + start (cold-start). The activation + // script can fail when packages are removed between generations — + // `start` exits non-zero but the container may be half-started. + // `stop` requests a graceful SIGTERM drain; `kill` then SIGKILLs + // any lingering processes so the next `start` enters a clean state + // without a generation transition, letting the activation succeed. + if let Err(start_err) = run(&["start", &container]).await { + tracing::warn!( + container = %container, + error = %start_err, + "start after rebuild failed (possible activation error); \ + retrying via stop + kill + start" + ); + run(&["stop", &container]).await.unwrap_or_else(|e| { + tracing::warn!( + container = %container, + error = %e, + "stop before cold-start retry failed (ignored)" + ); + }); + run(&["kill", &container]).await.unwrap_or_else(|e| { + tracing::warn!( + container = %container, + error = %e, + "kill before cold-start retry failed (ignored)" + ); + }); + run(&["start", &container]).await + .map_err(|e| anyhow::anyhow!( + "cold-start fallback also failed: {e:#} \ + (original start error: {start_err:#})" + )) + } else { + Ok(()) + } } else { Ok(()) }