From 453c1207402f1ea5a458d222b982bad03a8f0a28 Mon Sep 17 00:00:00 2001 From: leookun Date: Sun, 30 Aug 2026 20:57:02 +0800 Subject: [PATCH] fix: plugin in windows --- server/src/plugin/data.rs | 41 +++++++++++++++++++++++++++---- server/src/plugin/definition.rs | 1 + server/src/plugin/installation.rs | 1 + server/src/plugin/mod.rs | 9 +++++++ server/src/plugin/worker.rs | 1 + 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/server/src/plugin/data.rs b/server/src/plugin/data.rs index d01deb1..08c3601 100644 --- a/server/src/plugin/data.rs +++ b/server/src/plugin/data.rs @@ -70,11 +70,7 @@ impl PluginDataStore { .await?; file.sync_all().await?; drop(file); - #[cfg(windows)] - if path.exists() { - tokio::fs::remove_file(&path).await?; - } - tokio::fs::rename(&temporary, &path).await?; + replace_file(&temporary, &path).await?; set_file_permissions(&path)?; Ok(()) } @@ -106,6 +102,41 @@ impl PluginDataStore { } } +/// 原子替换目标文件。Windows 上 rename 不覆盖已存在文件,且目标可能被 +/// 杀毒软件或索引器短暂锁定(拒绝访问/共享冲突),需删除后重试。 +async fn replace_file(temporary: &Path, path: &Path) -> Result<()> { + #[cfg(windows)] + { + const ACCESS_DENIED: i32 = 5; + const SHARING_VIOLATION: i32 = 32; + let mut attempts = 0; + loop { + if path.exists() { + let _ = tokio::fs::remove_file(path).await; + } + match tokio::fs::rename(temporary, path).await { + Ok(()) => return Ok(()), + Err(error) + if attempts < 10 + && matches!( + error.raw_os_error(), + Some(ACCESS_DENIED | SHARING_VIOLATION) + ) => + { + attempts += 1; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + Err(error) => return Err(error.into()), + } + } + } + #[cfg(not(windows))] + { + tokio::fs::rename(temporary, path).await?; + Ok(()) + } +} + fn validate_component(value: &str, label: &str) -> Result<()> { if value.is_empty() || value.len() > 128 diff --git a/server/src/plugin/definition.rs b/server/src/plugin/definition.rs index 969a3bb..c5a0182 100644 --- a/server/src/plugin/definition.rs +++ b/server/src/plugin/definition.rs @@ -107,6 +107,7 @@ impl PluginDefinitionLoader { ) -> Result { let entry_url = file_url(entry)?; let mut command = tokio::process::Command::new(executable); + super::detach_console(&mut command); command .arg("run") .arg("--quiet") diff --git a/server/src/plugin/installation.rs b/server/src/plugin/installation.rs index fde08c3..9b30ef2 100644 --- a/server/src/plugin/installation.rs +++ b/server/src/plugin/installation.rs @@ -207,6 +207,7 @@ fn extract_runtime_archive(archive: &Path, output: &Path, executable_name: &str) async fn validate_runtime(executable: &Path, cancellation: &CancellationToken) -> Result<()> { let mut command = tokio::process::Command::new(executable); + super::detach_console(&mut command); command .arg("--version") .stdin(Stdio::null()) diff --git a/server/src/plugin/mod.rs b/server/src/plugin/mod.rs index 839f2bb..ce7faa3 100644 --- a/server/src/plugin/mod.rs +++ b/server/src/plugin/mod.rs @@ -21,3 +21,12 @@ pub use descriptor::{ pub use registry::{ImportResponse, OAuthBeginResponse, OAuthPollResponse, PluginRegistry}; pub use runtime::{PluginRuntime, PluginRuntimePhase, PluginRuntimeState, PluginRuntimeStatus}; pub(crate) use wire::llm_request as plugin_llm_request; + +/// Windows 下阻止 Deno 子进程弹出控制台窗口(CREATE_NO_WINDOW)。 +#[cfg(windows)] +fn detach_console(command: &mut tokio::process::Command) { + command.creation_flags(0x0800_0000); +} + +#[cfg(not(windows))] +fn detach_console(_command: &mut tokio::process::Command) {} diff --git a/server/src/plugin/worker.rs b/server/src/plugin/worker.rs index 783960f..2361244 100644 --- a/server/src/plugin/worker.rs +++ b/server/src/plugin/worker.rs @@ -223,6 +223,7 @@ impl PluginWorker { async fn spawn(&self) -> Result { let entry_url = file_url(&self.inner.entry)?; let mut command = tokio::process::Command::new(&self.inner.executable); + super::detach_console(&mut command); command .arg("run") .arg("--quiet")