php_lsp/analysis/external/
mod.rs1#[cfg(not(target_family = "wasm"))]
8pub mod phpcs;
9#[cfg(not(target_family = "wasm"))]
10pub mod phpstan;
11
12#[cfg(not(target_family = "wasm"))]
13use std::path::Path;
14
15#[cfg(not(target_family = "wasm"))]
16use tower_lsp_server::ls_types::Diagnostic;
17
18#[cfg(not(target_family = "wasm"))]
19use crate::lang::config::ExternalToolsConfig;
20
21#[cfg(not(target_family = "wasm"))]
22const MAX_CONCURRENT_RUNS: usize = 2;
23
24#[cfg(not(target_family = "wasm"))]
25static RUN_SLOTS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(MAX_CONCURRENT_RUNS);
26
27#[cfg(not(target_family = "wasm"))]
31pub async fn acquire_run_slot() -> tokio::sync::SemaphorePermit<'static> {
32 RUN_SLOTS
33 .acquire()
34 .await
35 .expect("RUN_SLOTS is never closed")
36}
37
38#[cfg(not(target_family = "wasm"))]
41#[derive(Default)]
42pub struct RunRegistry {
43 runs: dashmap::DashMap<tower_lsp_server::ls_types::Uri, tokio::task::AbortHandle>,
44}
45
46#[cfg(not(target_family = "wasm"))]
47impl RunRegistry {
48 pub fn start(
50 &self,
51 uri: tower_lsp_server::ls_types::Uri,
52 run: impl std::future::Future<Output = ()> + Send + 'static,
53 ) {
54 let handle = tokio::spawn(run).abort_handle();
55 if let Some(previous) = self.runs.insert(uri, handle) {
56 previous.abort();
57 }
58 }
59
60 pub fn cancel(&self, uri: &tower_lsp_server::ls_types::Uri) {
61 if let Some((_, run)) = self.runs.remove(uri) {
62 run.abort();
63 }
64 }
65}
66
67#[cfg(unix)]
69struct ProcessGroupGuard(Option<u32>);
70
71#[cfg(unix)]
72impl Drop for ProcessGroupGuard {
73 fn drop(&mut self) {
74 if let Some(pgid) = self.0.and_then(|pid| i32::try_from(pid).ok()) {
75 unsafe {
77 libc::killpg(pgid, libc::SIGKILL);
78 }
79 }
80 }
81}
82
83#[cfg(not(target_family = "wasm"))]
86async fn output_with_timeout(
87 mut command: tokio::process::Command,
88 timeout: std::time::Duration,
89 tool: &str,
90) -> Option<std::process::Output> {
91 command.kill_on_drop(true);
92 #[cfg(unix)]
93 command.process_group(0);
94 let child = match command.spawn() {
95 Ok(child) => child,
96 Err(e) => {
97 tracing::debug!("{tool} did not run: {e}");
98 return None;
99 }
100 };
101 #[cfg(unix)]
102 let _group = ProcessGroupGuard(child.id());
103 match tokio::time::timeout(timeout, child.wait_with_output()).await {
104 Ok(Ok(output)) => Some(output),
105 Ok(Err(e)) => {
106 tracing::debug!("{tool} did not run: {e}");
107 None
108 }
109 Err(_) => {
110 tracing::warn!("{tool} killed after {timeout:?}");
111 None
112 }
113 }
114}
115
116#[cfg(not(target_family = "wasm"))]
121pub async fn run_external_diagnostics(
122 cfg: &ExternalToolsConfig,
123 path: &Path,
124 workspace_root: Option<&Path>,
125) -> Vec<Diagnostic> {
126 let timeout = std::time::Duration::from_millis(cfg.timeout_ms);
127 let mut out = Vec::new();
128 if cfg.phpstan.enabled {
129 out.extend(phpstan::run(&cfg.phpstan, path, workspace_root, timeout).await);
130 }
131 if cfg.phpcs.enabled {
132 out.extend(phpcs::run(&cfg.phpcs, path, workspace_root, timeout).await);
133 }
134 out
135}