debugger: Fix issues with running Zed-installed debugpy + hangs when downloading (#32034)

Piotr Osiewicz created

Closes #32018

Release Notes:

- Fixed issues with launching Python debug adapter downloaded by Zed.
You might need to clear the old install of Debugpy from
`$HOME/.local/share/zed/debug_adapters/Debugpy` (Linux) or
`$HOME/Library/Application Support/Zed/debug_adapters/Debugpy` (Mac).

Change summary

crates/dap/src/adapters.rs        | 18 -------
crates/dap_adapters/src/python.rs | 75 +++++++++++++++++++-------------
2 files changed, 44 insertions(+), 49 deletions(-)

Detailed changes

crates/dap/src/adapters.rs 🔗

@@ -333,24 +333,6 @@ pub async fn download_adapter_from_github(
     Ok(version_path)
 }
 
-pub async fn fetch_latest_adapter_version_from_github(
-    github_repo: GithubRepo,
-    delegate: &dyn DapDelegate,
-) -> Result<AdapterVersion> {
-    let release = latest_github_release(
-        &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
-        false,
-        false,
-        delegate.http_client(),
-    )
-    .await?;
-
-    Ok(AdapterVersion {
-        tag_name: release.tag_name,
-        url: release.zipball_url,
-    })
-}
-
 #[async_trait(?Send)]
 pub trait DebugAdapter: 'static + Send + Sync {
     fn name(&self) -> DebugAdapterName;

crates/dap_adapters/src/python.rs 🔗

@@ -1,7 +1,8 @@
 use crate::*;
 use anyhow::Context as _;
+use dap::adapters::latest_github_release;
 use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
-use gpui::{AsyncApp, SharedString};
+use gpui::{AppContext, AsyncApp, SharedString};
 use json_dotpath::DotPaths;
 use language::{LanguageName, Toolchain};
 use serde_json::Value;
@@ -21,12 +22,13 @@ pub(crate) struct PythonDebugAdapter {
 
 impl PythonDebugAdapter {
     const ADAPTER_NAME: &'static str = "Debugpy";
+    const DEBUG_ADAPTER_NAME: DebugAdapterName =
+        DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));
     const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
     const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
     const LANGUAGE_NAME: &'static str = "Python";
 
     async fn generate_debugpy_arguments(
-        &self,
         host: &Ipv4Addr,
         port: u16,
         user_installed_path: Option<&Path>,
@@ -54,7 +56,7 @@ impl PythonDebugAdapter {
                 format!("--port={}", port),
             ])
         } else {
-            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
+            let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
             let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
 
             let debugpy_dir =
@@ -107,22 +109,21 @@ impl PythonDebugAdapter {
             repo_owner: "microsoft".into(),
         };
 
-        adapters::fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
+        fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
     }
 
     async fn install_binary(
-        &self,
+        adapter_name: DebugAdapterName,
         version: AdapterVersion,
-        delegate: &Arc<dyn DapDelegate>,
+        delegate: Arc<dyn DapDelegate>,
     ) -> Result<()> {
         let version_path = adapters::download_adapter_from_github(
-            self.name(),
+            adapter_name,
             version,
-            adapters::DownloadedFileType::Zip,
+            adapters::DownloadedFileType::GzipTar,
             delegate.as_ref(),
         )
         .await?;
-
         // only needed when you install the latest version for the first time
         if let Some(debugpy_dir) =
             util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
@@ -171,14 +172,13 @@ impl PythonDebugAdapter {
         let python_command = python_path.context("failed to find binary path for Python")?;
         log::debug!("Using Python executable: {}", python_command);
 
-        let arguments = self
-            .generate_debugpy_arguments(
-                &host,
-                port,
-                user_installed_path.as_deref(),
-                installed_in_venv,
-            )
-            .await?;
+        let arguments = Self::generate_debugpy_arguments(
+            &host,
+            port,
+            user_installed_path.as_deref(),
+            installed_in_venv,
+        )
+        .await?;
 
         log::debug!(
             "Starting debugpy adapter with command: {} {}",
@@ -204,7 +204,7 @@ impl PythonDebugAdapter {
 #[async_trait(?Send)]
 impl DebugAdapter for PythonDebugAdapter {
     fn name(&self) -> DebugAdapterName {
-        DebugAdapterName(Self::ADAPTER_NAME.into())
+        Self::DEBUG_ADAPTER_NAME
     }
 
     fn adapter_language_name(&self) -> Option<LanguageName> {
@@ -635,7 +635,9 @@ impl DebugAdapter for PythonDebugAdapter {
         if self.checked.set(()).is_ok() {
             delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
             if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
-                self.install_binary(version, delegate).await?;
+                cx.background_spawn(Self::install_binary(self.name(), version, delegate.clone()))
+                    .await
+                    .context("Failed to install debugpy")?;
             }
         }
 
@@ -644,6 +646,24 @@ impl DebugAdapter for PythonDebugAdapter {
     }
 }
 
+async fn fetch_latest_adapter_version_from_github(
+    github_repo: GithubRepo,
+    delegate: &dyn DapDelegate,
+) -> Result<AdapterVersion> {
+    let release = latest_github_release(
+        &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
+        false,
+        false,
+        delegate.http_client(),
+    )
+    .await?;
+
+    Ok(AdapterVersion {
+        tag_name: release.tag_name,
+        url: release.tarball_url,
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -651,20 +671,18 @@ mod tests {
 
     #[gpui::test]
     async fn test_debugpy_install_path_cases() {
-        let adapter = PythonDebugAdapter::default();
         let host = Ipv4Addr::new(127, 0, 0, 1);
         let port = 5678;
 
         // Case 1: User-defined debugpy path (highest precedence)
         let user_path = PathBuf::from("/custom/path/to/debugpy");
-        let user_args = adapter
-            .generate_debugpy_arguments(&host, port, Some(&user_path), false)
-            .await
-            .unwrap();
+        let user_args =
+            PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), false)
+                .await
+                .unwrap();
 
         // Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
-        let venv_args = adapter
-            .generate_debugpy_arguments(&host, port, None, true)
+        let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, true)
             .await
             .unwrap();
 
@@ -679,9 +697,4 @@ mod tests {
 
         // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API.
     }
-
-    #[test]
-    fn test_adapter_path_constant() {
-        assert_eq!(PythonDebugAdapter::ADAPTER_PATH, "src/debugpy/adapter");
-    }
 }