diff --git a/apps/oxlint/src/js_plugins/external_linter.rs b/apps/oxlint/src/js_plugins/external_linter.rs index 4d241302d47d4..b49a919b7c7de 100644 --- a/apps/oxlint/src/js_plugins/external_linter.rs +++ b/apps/oxlint/src/js_plugins/external_linter.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, atomic::Ordering, mpsc::channel}; +use std::{ + sync::{Arc, atomic::Ordering, mpsc::channel}, + time::Duration, +}; use napi::{ Status, @@ -326,8 +329,40 @@ fn wrap_create_workspace(cb: JsCreateWorkspaceCb) -> ExternalLinterCreateWorkspa } /// Wrap `destroyWorkspace` JS callback as a normal Rust function. +/// +/// The JS-side `destroyWorkspace` function is synchronous, but it's wrapped in a `ThreadsafeFunction`, +/// so cannot be called synchronously. Use an `mpsc::channel` to wait for the result from JS side. +/// +/// Uses a timeout to prevent indefinite blocking during shutdown, which can cause issues +/// in multi-root workspace scenarios where multiple workspaces are being destroyed concurrently. fn wrap_destroy_workspace(cb: JsDestroyWorkspaceCb) -> ExternalLinterDestroyWorkspaceCb { Arc::new(Box::new(move |workspace_uri| { - let _ = cb.call(workspace_uri, ThreadsafeFunctionCallMode::NonBlocking); + let (tx, rx) = channel(); + + // Send data to JS + let status = cb.call_with_return_value( + workspace_uri, + ThreadsafeFunctionCallMode::NonBlocking, + move |result, _env| { + // Ignore send errors - the receiver may have timed out + let _ = tx.send(result); + Ok(()) + }, + ); + + if status == Status::Ok { + // Use a timeout to prevent blocking indefinitely during shutdown. + // If JS side doesn't respond within the timeout, we proceed with shutdown anyway. + match rx.recv_timeout(Duration::from_secs(5)) { + // Destroying workspace succeeded + Ok(Ok(())) + // Timeout or sender dropped - proceed with shutdown + | Err(_) => Ok(()), + // `destroyWorkspace` threw an error + Ok(Err(err)) => Err(format!("`destroyWorkspace` threw an error: {err}")), + } + } else { + Err(format!("Failed to schedule `destroyWorkspace` callback: {status:?}")) + } })) } diff --git a/apps/oxlint/src/lsp/server_linter.rs b/apps/oxlint/src/lsp/server_linter.rs index 7da9b9d752438..786fbbcd7f0ff 100644 --- a/apps/oxlint/src/lsp/server_linter.rs +++ b/apps/oxlint/src/lsp/server_linter.rs @@ -301,7 +301,11 @@ impl ToolBuilder for ServerLinterBuilder { fn shutdown(&self, root_uri: &Uri) { // Destroy JS workspace if let Some(external_linter) = &self.external_linter { - (external_linter.destroy_workspace)(root_uri.as_str().to_string()); + let res = (external_linter.destroy_workspace)(root_uri.as_str().to_string()); + + if let Err(err) = res { + error!("Failed to destroy JS workspace:\n{err}\n"); + } } } } diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs index ba9f62b42be0c..e87d31ab27258 100644 --- a/crates/oxc_linter/src/external_linter.rs +++ b/crates/oxc_linter/src/external_linter.rs @@ -12,7 +12,8 @@ use crate::{ pub type ExternalLinterCreateWorkspaceCb = Arc Result<(), String> + Send + Sync>>; -pub type ExternalLinterDestroyWorkspaceCb = Arc>; +pub type ExternalLinterDestroyWorkspaceCb = + Arc Result<(), String> + Send + Sync>>; pub type ExternalLinterLoadPluginCb = Arc< Box<