python.rs

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