feat(extensions): 取消原生暂存并暴露持久提交状态
This commit is contained in:
@@ -149,6 +149,7 @@ pub async fn extension_install_preview(
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StageRequest {
|
||||
request_id: String,
|
||||
operation_id: String,
|
||||
source: String,
|
||||
release: notesagent_host::extension_package::Release,
|
||||
@@ -160,22 +161,70 @@ pub async fn extension_stage(
|
||||
request: StageRequest,
|
||||
) -> Result<Value, String> {
|
||||
main_window(&window)?;
|
||||
let mut lease = host.extension_requests.claim(&request.request_id)?;
|
||||
let checkpoint = lease.checkpoint();
|
||||
let extensions = host.extensions.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let mut store = extensions.lock().map_err(|_| "HOST_BUSY")?;
|
||||
let mut store = loop {
|
||||
checkpoint()?;
|
||||
match extensions.try_lock() {
|
||||
Ok(guard) => break guard,
|
||||
Err(std::sync::TryLockError::Poisoned(_)) => return Err("HOST_BUSY".into()),
|
||||
Err(std::sync::TryLockError::WouldBlock) => {
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
}
|
||||
};
|
||||
let store = store.as_mut().ok_or("EXTENSIONS_NOT_READY")?;
|
||||
let receipt = tauri::async_runtime::block_on(store.stage_online(
|
||||
&request.operation_id,
|
||||
&request.source,
|
||||
&request.release,
|
||||
))
|
||||
.map_err(|e| e.code)?;
|
||||
let receipt = tauri::async_runtime::block_on(lease.run(async {
|
||||
checkpoint()?;
|
||||
store
|
||||
.stage_online_checked(
|
||||
&request.operation_id,
|
||||
&request.source,
|
||||
&request.release,
|
||||
|| {
|
||||
checkpoint()
|
||||
.map_err(|code| notesagent_host::workspace::HostError::new(&code))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.code)
|
||||
}))?;
|
||||
serde_json::to_value(receipt).map_err(|_| "EXTENSION_STAGE_FAILED".into())
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "EXTENSION_STAGE_FAILED".to_string())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn extension_stage_prepare(
|
||||
window: WebviewWindow,
|
||||
host: State<'_, Host>,
|
||||
) -> Result<String, String> {
|
||||
main_window(&window)?;
|
||||
host.extension_requests.prepare(60_000)
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn extension_stage_cancel(
|
||||
window: WebviewWindow,
|
||||
host: State<'_, Host>,
|
||||
request_id: String,
|
||||
) -> Result<(), String> {
|
||||
main_window(&window)?;
|
||||
host.extension_requests.cancel(&request_id)
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn extension_stage_status(
|
||||
window: WebviewWindow,
|
||||
host: State<'_, Host>,
|
||||
operation_id: String,
|
||||
) -> Result<Value, String> {
|
||||
main_window(&window)?;
|
||||
let receipt = store(&host, |s| s.stage_receipt(&operation_id))?;
|
||||
serde_json::to_value(receipt).map_err(|_| "EXTENSION_STATUS_FAILED".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -606,6 +606,17 @@ impl ExtensionStore {
|
||||
source_url: &str,
|
||||
release: &Release,
|
||||
) -> Result<Receipt> {
|
||||
self.stage_online_checked(operation, source_url, release, || Ok(()))
|
||||
.await
|
||||
}
|
||||
pub async fn stage_online_checked(
|
||||
&mut self,
|
||||
operation: &str,
|
||||
source_url: &str,
|
||||
release: &Release,
|
||||
checkpoint: impl Fn() -> Result<()>,
|
||||
) -> Result<Receipt> {
|
||||
checkpoint()?;
|
||||
Uuid::parse_str(operation).map_err(|_| HostError::new("OPERATION_ID_INVALID"))?;
|
||||
let origin = source(source_url)?;
|
||||
let trusted = self
|
||||
@@ -637,19 +648,22 @@ impl ExtensionStore {
|
||||
let archive = client
|
||||
.download(&checked, release, &trusted.public_key)
|
||||
.await?;
|
||||
self.stage(Stage {
|
||||
operation_id: operation,
|
||||
source: &origin,
|
||||
release,
|
||||
archive: &archive,
|
||||
withdrawn: false,
|
||||
signer: Signer {
|
||||
public_key: &trusted.public_key,
|
||||
key_id: &trusted.key_id,
|
||||
namespace: &trusted.namespace,
|
||||
revoked: false,
|
||||
self.stage_inner(
|
||||
Stage {
|
||||
operation_id: operation,
|
||||
source: &origin,
|
||||
release,
|
||||
archive: &archive,
|
||||
withdrawn: false,
|
||||
signer: Signer {
|
||||
public_key: &trusted.public_key,
|
||||
key_id: &trusted.key_id,
|
||||
namespace: &trusted.namespace,
|
||||
revoked: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|_| checkpoint(),
|
||||
)
|
||||
}
|
||||
/// Online installation gate, using confirmed Host trust settings only.
|
||||
pub async fn switch_online(
|
||||
@@ -1015,6 +1029,21 @@ impl ExtensionStore {
|
||||
crate::payloads::verify(&path, digest, size)
|
||||
.map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))
|
||||
}
|
||||
pub fn stage_receipt(&self, operation: &str) -> Result<Option<Receipt>> {
|
||||
Uuid::parse_str(operation).map_err(|_| HostError::new("OPERATION_ID_INVALID"))?;
|
||||
let json: Option<String> = self
|
||||
.db
|
||||
.query_row(
|
||||
"SELECT receipt FROM stage_operations WHERE id=?1",
|
||||
[operation],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.optional()?;
|
||||
json.map(|v| {
|
||||
serde_json::from_str(&v).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
pub fn archive(&self, package_key: &str) -> Result<Vec<u8>> {
|
||||
let (digest, size): (String, u64) = self.db.query_row(
|
||||
"SELECT archive_hash,size FROM versions WHERE package_key=?1",
|
||||
@@ -1082,6 +1111,34 @@ mod tests {
|
||||
archive,
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn cancelled_stage_reports_durable_commit_race_truthfully() {
|
||||
let (release, archive, key) = fixture();
|
||||
for boundary in ["object_stored", "receipt_recorded", "committed"] {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let mut store = ExtensionStore::open(temp.path()).unwrap();
|
||||
let operation = Uuid::new_v4().to_string();
|
||||
assert_eq!(
|
||||
store
|
||||
.stage_inner(request(&operation, &release, &archive, &key), |at| {
|
||||
if at == boundary {
|
||||
Err(HostError::new("REQUEST_CANCELLED"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"REQUEST_CANCELLED"
|
||||
);
|
||||
drop(store);
|
||||
let store = ExtensionStore::open(temp.path()).unwrap();
|
||||
assert_eq!(
|
||||
store.stage_receipt(&operation).unwrap().is_some(),
|
||||
boundary == "committed"
|
||||
);
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn installation_preview_binds_target_state_and_rejects_stale_confirmation_before_network()
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ struct Host {
|
||||
requests: Requests,
|
||||
extensions: Arc<Mutex<Option<notesagent_host::extension_store::ExtensionStore>>>,
|
||||
extension_reviews: extension_commands::Reviews,
|
||||
extension_requests: Requests,
|
||||
sync: Arc<sync_commands::Runtime>,
|
||||
workspace: Arc<Mutex<Option<Workspace>>>,
|
||||
recent: Mutex<Option<RecentVaultStore>>,
|
||||
@@ -851,6 +852,9 @@ fn main() {
|
||||
extension_trust_confirm,
|
||||
extension_install_preview,
|
||||
extension_stage,
|
||||
extension_stage_prepare,
|
||||
extension_stage_cancel,
|
||||
extension_stage_status,
|
||||
record_get,
|
||||
record_write,
|
||||
sync_login,
|
||||
|
||||
Reference in New Issue
Block a user