From 74dbaeb2ad353426309589b91f158764963fe29c Mon Sep 17 00:00:00 2001 From: Nihal Kumar Date: Mon, 16 Mar 2026 21:59:24 +0530 Subject: [PATCH] debugger: Fall back to cached adapter binaries when offline (#50928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python (debugpy) and Go (delve-shim-dap) debug adapters unconditionally require network access on every debug session start — even when the adapter binary is already cached locally. This makes the debugger completely unusable offline. Python: fetch_debugpy_whl → maybe_fetch_new_wheel hits pypi; failure propagates as a fatal error. Go: install_shim → fetch_latest_adapter_version hits the GitHub API; failure propagates as a fatal error. CodeLLDB and JavaScript adapters already handle this correctly. Fix for Python and Go: Wrap the network fetch in each adapter with a fallback: if the request fails and a previously downloaded adapter exists on disk, log a warning and use the cached version. If no cache exists, the original error propagates unchanged. Logs after the fix: ``` 2026-03-06T16:31:51+05:30 WARN [dap_adapters::python] Failed to fetch latest debugpy, using cached version: getting latest release 2026-03-06T16:31:51+05:30 INFO [dap::transport] Debug adapter has connected to TCP server 127.0.0.1:45533 ``` Limitations: The debugger must be run at least once with internet connectivity to populate the cache on disk. This PR does not change that requirement. Closes #45781 Before you mark this PR as ready for review, make sure that you have: - [ ] Added a solid test coverage and/or screenshots from doing manual testing - No automated tests are included. The existing MockDelegate stubs http_client() and fs() as unimplemented!(), so testing the fallback path would require new mock infrastructure. The fix was verified manually by running the debug build offline with and without cached adapters. The CodeLLDB adapter's equivalent fallback (lines 357-374) also has no dedicated test. - [x] Done a self-review taking into account security and performance aspects Release Notes: - Fixed debugger failing to start when offline if a debug adapter was previously downloaded. --- crates/dap_adapters/src/go.rs | 71 ++++++++++++++++++++++--------- crates/dap_adapters/src/python.rs | 31 +++++++++----- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index af81f5cca5390d7e72e1805331e25da0a036d9d8..93d0e8a958568cd7899208daca05a9c1dd2f846b 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -8,6 +8,7 @@ use dap::{ }, }; use fs::Fs; +use futures::StreamExt; use gpui::{AsyncApp, SharedString}; use language::LanguageName; use log::warn; @@ -71,27 +72,59 @@ impl GoDebugAdapter { return Ok(path); } - let asset = Self::fetch_latest_adapter_version(delegate).await?; - let ty = if consts::OS == "windows" { - DownloadedFileType::Zip - } else { - DownloadedFileType::GzipTar - }; - download_adapter_from_github( - "delve-shim-dap".into(), - asset.clone(), - ty, - delegate.as_ref(), - ) - .await?; + let adapter_dir = paths::debug_adapters_dir().join("delve-shim-dap"); + + match Self::fetch_latest_adapter_version(delegate).await { + Ok(asset) => { + let ty = if consts::OS == "windows" { + DownloadedFileType::Zip + } else { + DownloadedFileType::GzipTar + }; + download_adapter_from_github( + "delve-shim-dap".into(), + asset.clone(), + ty, + delegate.as_ref(), + ) + .await?; + + let path = adapter_dir + .join(format!("delve-shim-dap_{}", asset.tag_name)) + .join(format!("delve-shim-dap{}", consts::EXE_SUFFIX)); + self.shim_path.set(path.clone()).ok(); - let path = paths::debug_adapters_dir() - .join("delve-shim-dap") - .join(format!("delve-shim-dap_{}", asset.tag_name)) - .join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX)); - self.shim_path.set(path.clone()).ok(); + Ok(path) + } + Err(error) => { + let binary_name = format!("delve-shim-dap{}", consts::EXE_SUFFIX); + let mut cached = None; + if let Ok(mut entries) = delegate.fs().read_dir(&adapter_dir).await { + while let Some(entry) = entries.next().await { + if let Ok(version_dir) = entry { + let candidate = version_dir.join(&binary_name); + if delegate + .fs() + .metadata(&candidate) + .await + .is_ok_and(|m| m.is_some()) + { + cached = Some(candidate); + break; + } + } + } + } - Ok(path) + if let Some(path) = cached { + warn!("Failed to fetch latest delve-shim-dap, using cached version: {error:#}"); + self.shim_path.set(path.clone()).ok(); + Ok(path) + } else { + Err(error) + } + } + } } } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 96bdde12672cae471edf1d6e603a06413d1f4b21..111eab5a1d1bf4dea5f99ce83c01ce8fdb9e47e3 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -224,16 +224,27 @@ impl PythonDebugAdapter { ) -> Result, String> { self.debugpy_whl_base_path .get_or_init(|| async move { - self.maybe_fetch_new_wheel(toolchain, delegate) - .await - .map_err(|e| format!("{e}"))?; - Ok(Arc::from( - debug_adapters_dir() - .join(Self::ADAPTER_NAME) - .join("debugpy") - .join("adapter") - .as_ref(), - )) + let adapter_path = debug_adapters_dir() + .join(Self::ADAPTER_NAME) + .join("debugpy") + .join("adapter"); + + if let Err(error) = self.maybe_fetch_new_wheel(toolchain, delegate).await { + if delegate + .fs() + .metadata(&adapter_path) + .await + .is_ok_and(|m| m.is_some()) + { + log::warn!( + "Failed to fetch latest debugpy, using cached version: {error:#}" + ); + } else { + return Err(format!("{error}")); + } + } + + Ok(Arc::from(adapter_path.as_ref())) }) .await .clone()