python.rs

  1use crate::*;
  2use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
  3use gpui::AsyncApp;
  4use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
  5use util::ResultExt;
  6
  7#[derive(Default)]
  8pub(crate) struct PythonDebugAdapter {
  9    checked: OnceLock<()>,
 10}
 11
 12impl PythonDebugAdapter {
 13    const ADAPTER_NAME: &'static str = "Debugpy";
 14    const ADAPTER_PACKAGE_NAME: &'static str = "debugpy";
 15    const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
 16    const LANGUAGE_NAME: &'static str = "Python";
 17
 18    fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
 19        let mut args = json!({
 20            "request": match config.request {
 21                DebugRequest::Launch(_) => "launch",
 22                DebugRequest::Attach(_) => "attach",
 23            },
 24            "subProcess": true,
 25            "redirectOutput": true,
 26        });
 27        let map = args.as_object_mut().unwrap();
 28        match &config.request {
 29            DebugRequest::Attach(attach) => {
 30                map.insert("processId".into(), attach.process_id.into());
 31            }
 32            DebugRequest::Launch(launch) => {
 33                map.insert("program".into(), launch.program.clone().into());
 34                map.insert("args".into(), launch.args.clone().into());
 35
 36                if let Some(stop_on_entry) = config.stop_on_entry {
 37                    map.insert("stopOnEntry".into(), stop_on_entry.into());
 38                }
 39                if let Some(cwd) = launch.cwd.as_ref() {
 40                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
 41                }
 42            }
 43        }
 44        StartDebuggingRequestArguments {
 45            configuration: args,
 46            request: config.request.to_dap(),
 47        }
 48    }
 49    async fn fetch_latest_adapter_version(
 50        &self,
 51        delegate: &dyn DapDelegate,
 52    ) -> Result<AdapterVersion> {
 53        let github_repo = GithubRepo {
 54            repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
 55            repo_owner: "microsoft".into(),
 56        };
 57
 58        adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
 59    }
 60
 61    async fn install_binary(
 62        &self,
 63        version: AdapterVersion,
 64        delegate: &dyn DapDelegate,
 65    ) -> Result<()> {
 66        let version_path = adapters::download_adapter_from_github(
 67            self.name(),
 68            version,
 69            adapters::DownloadedFileType::Zip,
 70            delegate,
 71        )
 72        .await?;
 73
 74        // only needed when you install the latest version for the first time
 75        if let Some(debugpy_dir) =
 76            util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
 77                file_name.starts_with("microsoft-debugpy-")
 78            })
 79            .await
 80        {
 81            // TODO Debugger: Rename folder instead of moving all files to another folder
 82            // We're doing unnecessary IO work right now
 83            util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
 84                .await?;
 85        }
 86
 87        Ok(())
 88    }
 89
 90    async fn get_installed_binary(
 91        &self,
 92        delegate: &dyn DapDelegate,
 93        config: &DebugTaskDefinition,
 94        user_installed_path: Option<PathBuf>,
 95        cx: &mut AsyncApp,
 96    ) -> Result<DebugAdapterBinary> {
 97        const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
 98        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
 99        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
100
101        let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
102            user_installed_path
103        } else {
104            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
105            let file_name_prefix = format!("{}_", Self::ADAPTER_NAME);
106
107            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
108                file_name.starts_with(&file_name_prefix)
109            })
110            .await
111            .ok_or_else(|| anyhow!("Debugpy directory not found"))?
112        };
113
114        let toolchain = delegate
115            .toolchain_store()
116            .active_toolchain(
117                delegate.worktree_id(),
118                Arc::from("".as_ref()),
119                language::LanguageName::new(Self::LANGUAGE_NAME),
120                cx,
121            )
122            .await;
123
124        let python_path = if let Some(toolchain) = toolchain {
125            Some(toolchain.path.to_string())
126        } else {
127            BINARY_NAMES
128                .iter()
129                .filter_map(|cmd| {
130                    delegate
131                        .which(OsStr::new(cmd))
132                        .map(|path| path.to_string_lossy().to_string())
133                })
134                .find(|_| true)
135        };
136
137        Ok(DebugAdapterBinary {
138            command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
139            arguments: vec![
140                debugpy_dir
141                    .join(Self::ADAPTER_PATH)
142                    .to_string_lossy()
143                    .to_string(),
144                format!("--port={}", port),
145                format!("--host={}", host),
146            ],
147            connection: Some(adapters::TcpArguments {
148                host,
149                port,
150                timeout,
151            }),
152            cwd: None,
153            envs: HashMap::default(),
154            request_args: self.request_args(config),
155        })
156    }
157}
158
159#[async_trait(?Send)]
160impl DebugAdapter for PythonDebugAdapter {
161    fn name(&self) -> DebugAdapterName {
162        DebugAdapterName(Self::ADAPTER_NAME.into())
163    }
164
165    async fn get_binary(
166        &self,
167        delegate: &dyn DapDelegate,
168        config: &DebugTaskDefinition,
169        user_installed_path: Option<PathBuf>,
170        cx: &mut AsyncApp,
171    ) -> Result<DebugAdapterBinary> {
172        if self.checked.set(()).is_ok() {
173            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
174            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
175                self.install_binary(version, delegate).await?;
176            }
177        }
178
179        self.get_installed_binary(delegate, &config, user_installed_path, cx)
180            .await
181    }
182}