python.rs

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