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