From 46109ee567b370c4fe3d38b8d0b843265314124a Mon Sep 17 00:00:00 2001 From: rain Date: Tue, 10 Nov 2026 07:07:41 +0800 Subject: [PATCH] fix(sandbox): retry a Retained unmount through the workspace record's token (#538) workspace.unmount re-sent a retryable Retained detach by calling state.token(path), which re-runs unmount_guard::check and re-reports the Retained custody before the retry reaches the daemon. Read the WorkspaceToken from the workspace record (files::token_of, as sandbox.close does) instead, so a later unmount actually re-attempts the recorded EBUSY detach once the holder is gone. --- control/crates/layerfs-sandbox/src/files.rs | 2 +- .../layerfs-sandbox/src/unmount_guard.rs | 13 +++++++++++++ .../layerfs-sandbox/src/workspace/serve.rs | 18 ++++++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/control/crates/layerfs-sandbox/src/files.rs b/control/crates/layerfs-sandbox/src/files.rs index f952590740..f155c5f276 100644 --- a/control/crates/layerfs-sandbox/src/files.rs +++ b/control/crates/layerfs-sandbox/src/files.rs @@ -317,7 +317,7 @@ pub fn workspaces(directory: &Path) -> Result> { Ok(found) } /// A Workspace record's token: its id (64 hex digits) and namespace. -fn token_of(value: &WorkspaceRecord) -> Result { +pub fn token_of(value: &WorkspaceRecord) -> Result { let id = helpers::required(value.id(), "id")?; let bytes: Vec = (0..id.len()) .step_by(2) diff --git a/control/crates/layerfs-sandbox/src/unmount_guard.rs b/control/crates/layerfs-sandbox/src/unmount_guard.rs index dd3e30843f..e69f38d3c7 100644 --- a/control/crates/layerfs-sandbox/src/unmount_guard.rs +++ b/control/crates/layerfs-sandbox/src/unmount_guard.rs @@ -219,6 +219,19 @@ fn find_in(directory: &std::path::Path, path: &str) -> Result, } Ok(None) } +/// The token of the Workspace recorded at `path`, read from its record: the +/// live mount table no longer answers a Retained Workspace. +pub fn token(state: &SandboxState, path: &str) -> Result { + for (_, record) in discovery::workspaces(&state.directory)? { + if record.path() == Some(path) { + return discovery::token_of(&record); + } + } + Err(refused( + "NotMounted", + format!("{}:{path} is not mounted", state.id), + )) +} /// Closing a running Sandbox may not race a registered terminal operation. pub fn check_all(state: &SandboxState) -> Result, SdkRefusal> { let mut recoverable = Vec::new(); diff --git a/control/crates/layerfs-sandbox/src/workspace/serve.rs b/control/crates/layerfs-sandbox/src/workspace/serve.rs index 567945145e..8a547e2684 100644 --- a/control/crates/layerfs-sandbox/src/workspace/serve.rs +++ b/control/crates/layerfs-sandbox/src/workspace/serve.rs @@ -259,12 +259,22 @@ pub fn unmount(host: &Host, path: Option<&str>, discard: bool, force: bool) -> O let guard = layerfs_store::project::discovery::guard(&state.directory)?; // Another process may have registered while this call waited for the // short admission section. Check again before sending anything. - if let Some(value) = crate::unmount_guard::find(&state, path)? { - if !crate::unmount_guard::retry(&value) { + let retrying = match crate::unmount_guard::find(&state, path)? { + Some(value) if !crate::unmount_guard::retry(&value) => { return crate::unmount_guard::repeat(&value); } - } - let token = state.token(path)?; + Some(_) => true, + None => false, + }; + // A retryable Retained Workspace's live mount table no longer answers: + // read its token from the workspace record (as `sandbox.close` does) + // instead of re-running `unmount_guard::check`, which would re-report + // the Retained custody before the retry ever reaches the daemon (#538). + let token = if retrying { + crate::unmount_guard::token(&state, path)? + } else { + state.token(path)? + }; // its running commands (D-10): `Busy`, or with `force` cancelled first let wsid = layerfs_bridge::sdk_values::hex(&token.workspace.to_bytes()); let unreached = &mut crate::exec::cancel::unreached;