Merge remote-tracking branch 'gitea-ai/main' into gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf

This commit is contained in:
archipelago
2026-08-08 10:10:33 -04:00
3 changed files with 167 additions and 1 deletions
+40 -1
View File
@@ -518,8 +518,46 @@ impl RpcHandler {
// ── The rotation itself ──────────────────────────────────────────────────────
/// Hold LND's lifecycle lock for the whole rotation, then do the work.
///
/// Between "stop LND" and "start LND" this owns a stopped container with its
/// credential material deleted — the single worst moment for another actor to
/// step in. Two would, unasked: the health monitor restarts any container it
/// finds stopped, and the reconciler starts one whose unit is enabled. Either
/// brings LND back up mid-deletion, and LND re-mints `macaroons.db` on unlock —
/// so the deletion loop would race a live process writing that file, or
/// "succeed" against material that had already been regenerated, leaving the
/// operator told they had rotated while the old root key was still in service.
///
/// `app_ops::op_lock` is the mechanism both of those actors already consult
/// (`lifecycle_op_in_flight`, via `lifecycle_op_covers_container` in the health
/// monitor), and it also serialises against the package.start/stop/restart
/// workers, so an operator hitting "Restart" on Lightning mid-rotation queues
/// instead of interleaving.
///
/// Chosen over the `user-stopped` marker that `recreate_wallet_destructively`
/// uses for its own window: that marker is a file on disk, so a rotation that
/// died between marking and clearing would leave Lightning suppressed
/// *permanently*, fixable only by finding and editing JSON on the node. This
/// guard releases when it drops, on every path including a panic.
async fn run_rotation(
orchestrator: Option<Arc<dyn crate::container::ContainerOrchestrator>>,
) -> Result<()> {
let lock = crate::app_ops::op_lock(LND_CONTAINER);
// Fail fast rather than queue. This is a button someone just pressed: a
// silent wait behind a start/stop/restart that may itself take minutes reads
// as "nothing happened", and the honest answer is short.
let _guard = lock.try_lock().map_err(|_| {
anyhow::anyhow!(
"another Lightning start/stop/restart is in progress on this node — \
wait for it to finish and try again"
)
})?;
rotate_with_lnd_pinned(orchestrator).await
}
async fn rotate_with_lnd_pinned(
orchestrator: Option<Arc<dyn crate::container::ContainerOrchestrator>>,
) -> Result<()> {
// 1. Preflight — establish what must survive, while LND can still be asked.
with_progress(|p| p.set("preflight", StepState::Running, None));
@@ -584,7 +622,8 @@ async fn run_rotation(
);
});
// 3. Stop.
// 3. Stop. Nothing may restart LND from here until step 5 — see the lock
// `run_rotation` holds around this whole function.
with_progress(|p| p.set("stop", StepState::Running, None));
stop_lnd().await.context("stopping LND")?;
with_progress(|p| p.set("stop", StepState::Done, None));