proc_macro_api/
process.rs

1//! Handle process life-time and message passing for proc-macro client
2
3use std::{
4    io::{self, BufRead, BufReader, Read, Write},
5    panic::AssertUnwindSafe,
6    process::{Child, ChildStdin, ChildStdout, Command, Stdio},
7    sync::{Arc, Mutex, OnceLock},
8};
9
10use paths::AbsPath;
11use semver::Version;
12use stdx::JodChild;
13
14use crate::{
15    ProcMacroKind, ServerError,
16    legacy_protocol::{self, SpanMode},
17    version,
18};
19
20/// Represents a process handling proc-macro communication.
21#[derive(Debug)]
22pub(crate) struct ProcMacroServerProcess {
23    /// The state of the proc-macro server process, the protocol is currently strictly sequential
24    /// hence the lock on the state.
25    state: Mutex<ProcessSrvState>,
26    version: u32,
27    protocol: Protocol,
28    /// Populated when the server exits.
29    exited: OnceLock<AssertUnwindSafe<ServerError>>,
30}
31
32#[derive(Debug, Clone)]
33pub(crate) enum Protocol {
34    LegacyJson { mode: SpanMode },
35    LegacyPostcard { mode: SpanMode },
36}
37
38/// Maintains the state of the proc-macro server process.
39#[derive(Debug)]
40struct ProcessSrvState {
41    process: Process,
42    stdin: ChildStdin,
43    stdout: BufReader<ChildStdout>,
44}
45
46impl ProcMacroServerProcess {
47    /// Starts the proc-macro server and performs a version check
48    pub(crate) fn run<'a>(
49        process_path: &AbsPath,
50        env: impl IntoIterator<
51            Item = (impl AsRef<std::ffi::OsStr>, &'a Option<impl 'a + AsRef<std::ffi::OsStr>>),
52        > + Clone,
53        version: Option<&Version>,
54    ) -> io::Result<ProcMacroServerProcess> {
55        const VERSION: Version = Version::new(1, 93, 0);
56        // we do `>` for nightly as this started working in the middle of the 1.93 nightly release, so we dont want to break on half of the nightlies
57        let has_working_format_flag = version.map_or(false, |v| {
58            if v.pre.as_str() == "nightly" { *v > VERSION } else { *v >= VERSION }
59        });
60
61        let formats: &[_] = if std::env::var_os("RUST_ANALYZER_USE_POSTCARD").is_some()
62            && has_working_format_flag
63        {
64            &[
65                (Some("postcard-legacy"), Protocol::LegacyPostcard { mode: SpanMode::Id }),
66                (Some("json-legacy"), Protocol::LegacyJson { mode: SpanMode::Id }),
67            ]
68        } else {
69            &[(None, Protocol::LegacyJson { mode: SpanMode::Id })]
70        };
71
72        let mut err = None;
73        for &(format, ref protocol) in formats {
74            let create_srv = || {
75                let mut process = Process::run(process_path, env.clone(), format)?;
76                let (stdin, stdout) = process.stdio().expect("couldn't access child stdio");
77
78                io::Result::Ok(ProcMacroServerProcess {
79                    state: Mutex::new(ProcessSrvState { process, stdin, stdout }),
80                    version: 0,
81                    protocol: protocol.clone(),
82                    exited: OnceLock::new(),
83                })
84            };
85            let mut srv = create_srv()?;
86            tracing::info!("sending proc-macro server version check");
87            match srv.version_check() {
88                Ok(v) if v > version::CURRENT_API_VERSION => {
89                    #[allow(clippy::disallowed_methods)]
90                    let process_version = Command::new(process_path)
91                        .arg("--version")
92                        .output()
93                        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
94                        .unwrap_or_else(|_| "unknown version".to_owned());
95                    err = Some(io::Error::other(format!(
96                        "Your installed proc-macro server is too new for your rust-analyzer. API version: {}, server version: {process_version}. \
97                        This will prevent proc-macro expansion from working. Please consider updating your rust-analyzer to ensure compatibility with your current toolchain.",
98                        version::CURRENT_API_VERSION
99                    )));
100                }
101                Ok(v) => {
102                    tracing::info!("Proc-macro server version: {v}");
103                    srv.version = v;
104                    if srv.version >= version::RUST_ANALYZER_SPAN_SUPPORT
105                        && let Ok(new_mode) = srv.enable_rust_analyzer_spans()
106                    {
107                        match &mut srv.protocol {
108                            Protocol::LegacyJson { mode } | Protocol::LegacyPostcard { mode } => {
109                                *mode = new_mode
110                            }
111                        }
112                    }
113                    tracing::info!("Proc-macro server protocol: {:?}", srv.protocol);
114                    return Ok(srv);
115                }
116                Err(e) => {
117                    tracing::info!(%e, "proc-macro version check failed");
118                    err = Some(io::Error::other(format!(
119                        "proc-macro server version check failed: {e}"
120                    )))
121                }
122            }
123        }
124        Err(err.unwrap())
125    }
126
127    /// Returns the server error if the process has exited.
128    pub(crate) fn exited(&self) -> Option<&ServerError> {
129        self.exited.get().map(|it| &it.0)
130    }
131
132    pub(crate) fn use_postcard(&self) -> bool {
133        matches!(self.protocol, Protocol::LegacyPostcard { .. })
134    }
135
136    /// Retrieves the API version of the proc-macro server.
137    pub(crate) fn version(&self) -> u32 {
138        self.version
139    }
140
141    /// Enable support for rust-analyzer span mode if the server supports it.
142    pub(crate) fn rust_analyzer_spans(&self) -> bool {
143        match self.protocol {
144            Protocol::LegacyJson { mode } => mode == SpanMode::RustAnalyzer,
145            Protocol::LegacyPostcard { mode } => mode == SpanMode::RustAnalyzer,
146        }
147    }
148
149    /// Checks the API version of the running proc-macro server.
150    fn version_check(&self) -> Result<u32, ServerError> {
151        match self.protocol {
152            Protocol::LegacyJson { .. } => legacy_protocol::version_check(self),
153            Protocol::LegacyPostcard { .. } => legacy_protocol::version_check(self),
154        }
155    }
156
157    /// Enable support for rust-analyzer span mode if the server supports it.
158    fn enable_rust_analyzer_spans(&self) -> Result<SpanMode, ServerError> {
159        match self.protocol {
160            Protocol::LegacyJson { .. } => legacy_protocol::enable_rust_analyzer_spans(self),
161            Protocol::LegacyPostcard { .. } => legacy_protocol::enable_rust_analyzer_spans(self),
162        }
163    }
164
165    /// Finds proc-macros in a given dynamic library.
166    pub(crate) fn find_proc_macros(
167        &self,
168        dylib_path: &AbsPath,
169    ) -> Result<Result<Vec<(String, ProcMacroKind)>, String>, ServerError> {
170        match self.protocol {
171            Protocol::LegacyJson { .. } => legacy_protocol::find_proc_macros(self, dylib_path),
172            Protocol::LegacyPostcard { .. } => legacy_protocol::find_proc_macros(self, dylib_path),
173        }
174    }
175
176    pub(crate) fn send_task<Request, Response, Buf>(
177        &self,
178        serialize_req: impl FnOnce(
179            &mut dyn Write,
180            &mut dyn BufRead,
181            Request,
182            &mut Buf,
183        ) -> Result<Option<Response>, ServerError>,
184        req: Request,
185    ) -> Result<Response, ServerError>
186    where
187        Buf: Default,
188    {
189        let state = &mut *self.state.lock().unwrap();
190        let mut buf = Buf::default();
191        serialize_req(&mut state.stdin, &mut state.stdout, req, &mut buf)
192            .and_then(|res| {
193                res.ok_or_else(|| {
194                    let message = "proc-macro server did not respond with data".to_owned();
195                    ServerError {
196                        io: Some(Arc::new(io::Error::new(
197                            io::ErrorKind::BrokenPipe,
198                            message.clone(),
199                        ))),
200                        message,
201                    }
202                })
203            })
204            .map_err(|e| {
205                if e.io.as_ref().map(|it| it.kind()) == Some(io::ErrorKind::BrokenPipe) {
206                    match state.process.child.try_wait() {
207                        Ok(None) | Err(_) => e,
208                        Ok(Some(status)) => {
209                            let mut msg = String::new();
210                            if !status.success()
211                                && let Some(stderr) = state.process.child.stderr.as_mut()
212                            {
213                                _ = stderr.read_to_string(&mut msg);
214                            }
215                            let server_error = ServerError {
216                                message: format!(
217                                    "proc-macro server exited with {status}{}{msg}",
218                                    if msg.is_empty() { "" } else { ": " }
219                                ),
220                                io: None,
221                            };
222                            // `AssertUnwindSafe` is fine here, we already correct initialized
223                            // server_error at this point.
224                            self.exited.get_or_init(|| AssertUnwindSafe(server_error)).0.clone()
225                        }
226                    }
227                } else {
228                    e
229                }
230            })
231    }
232}
233
234/// Manages the execution of the proc-macro server process.
235#[derive(Debug)]
236struct Process {
237    child: JodChild,
238}
239
240impl Process {
241    /// Runs a new proc-macro server process with the specified environment variables.
242    fn run<'a>(
243        path: &AbsPath,
244        env: impl IntoIterator<
245            Item = (impl AsRef<std::ffi::OsStr>, &'a Option<impl 'a + AsRef<std::ffi::OsStr>>),
246        >,
247        format: Option<&str>,
248    ) -> io::Result<Process> {
249        let child = JodChild(mk_child(path, env, format)?);
250        Ok(Process { child })
251    }
252
253    /// Retrieves stdin and stdout handles for the process.
254    fn stdio(&mut self) -> Option<(ChildStdin, BufReader<ChildStdout>)> {
255        let stdin = self.child.stdin.take()?;
256        let stdout = self.child.stdout.take()?;
257        let read = BufReader::new(stdout);
258
259        Some((stdin, read))
260    }
261}
262
263/// Creates and configures a new child process for the proc-macro server.
264fn mk_child<'a>(
265    path: &AbsPath,
266    extra_env: impl IntoIterator<
267        Item = (impl AsRef<std::ffi::OsStr>, &'a Option<impl 'a + AsRef<std::ffi::OsStr>>),
268    >,
269    format: Option<&str>,
270) -> io::Result<Child> {
271    #[allow(clippy::disallowed_methods)]
272    let mut cmd = Command::new(path);
273    for env in extra_env {
274        match env {
275            (key, Some(val)) => cmd.env(key, val),
276            (key, None) => cmd.env_remove(key),
277        };
278    }
279    if let Some(format) = format {
280        cmd.arg("--format");
281        cmd.arg(format);
282    }
283    cmd.env("RUST_ANALYZER_INTERNALS_DO_NOT_USE", "this is unstable")
284        .stdin(Stdio::piped())
285        .stdout(Stdio::piped())
286        .stderr(Stdio::inherit());
287    if cfg!(windows) {
288        let mut path_var = std::ffi::OsString::new();
289        path_var.push(path.parent().unwrap().parent().unwrap());
290        path_var.push("\\bin;");
291        path_var.push(std::env::var_os("PATH").unwrap_or_default());
292        cmd.env("PATH", path_var);
293    }
294    cmd.spawn()
295}