Skip to main content

cargo/util/
diagnostic_server.rs

1//! A small TCP server to handle collection of diagnostics information in a
2//! cross-platform way for the `cargo fix` command.
3
4use std::collections::HashSet;
5use std::io::{BufReader, Read, Write};
6use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::thread::{self, JoinHandle};
11
12use annotate_snippets::Group;
13use annotate_snippets::Level;
14use annotate_snippets::Origin;
15use anyhow::{Context as _, Error};
16use cargo_util::ProcessBuilder;
17use serde::{Deserialize, Serialize};
18use tracing::warn;
19
20use crate::core::Edition;
21use crate::util::GlobalContext;
22use crate::util::errors::CargoResult;
23use crate::util::network::LOCALHOST;
24
25const DIAGNOSTICS_SERVER_VAR: &str = "__CARGO_FIX_DIAGNOSTICS_SERVER";
26
27#[derive(Deserialize, Serialize, Hash, Eq, PartialEq, Clone)]
28pub enum Message {
29    Migrating {
30        file: String,
31        from_edition: Edition,
32        to_edition: Edition,
33    },
34    Fixing {
35        file: String,
36    },
37    Fixed {
38        file: String,
39        fixes: u32,
40    },
41    FixFailed {
42        files: Vec<String>,
43        krate: Option<String>,
44        errors: Vec<String>,
45        abnormal_exit: Option<String>,
46    },
47    ReplaceFailed {
48        file: String,
49        message: String,
50    },
51    EditionAlreadyEnabled {
52        message: String,
53        edition: Edition,
54    },
55}
56
57impl Message {
58    pub fn post(&self, gctx: &GlobalContext) -> Result<(), Error> {
59        let addr = gctx
60            .get_env(DIAGNOSTICS_SERVER_VAR)
61            .context("diagnostics collector misconfigured")?;
62        let mut client =
63            TcpStream::connect(&addr).context("failed to connect to parent diagnostics target")?;
64
65        let s = serde_json::to_string(self).context("failed to serialize message")?;
66        client
67            .write_all(s.as_bytes())
68            .context("failed to write message to diagnostics target")?;
69        client
70            .shutdown(Shutdown::Write)
71            .context("failed to shutdown")?;
72
73        client
74            .read_to_end(&mut Vec::new())
75            .context("failed to receive a disconnect")?;
76
77        Ok(())
78    }
79}
80
81/// A printer that will print diagnostics messages to the shell.
82pub struct DiagnosticPrinter<'a> {
83    /// The context to get the shell to print to.
84    gctx: &'a GlobalContext,
85    /// An optional wrapper to be used in addition to `rustc.wrapper` for workspace crates.
86    /// This is used to get the correct bug report URL. For instance,
87    /// if `clippy-driver` is set as the value for the wrapper,
88    /// then the correct bug report URL for `clippy` can be obtained.
89    workspace_wrapper: &'a Option<PathBuf>,
90    // A set of messages that have already been printed.
91    dedupe: HashSet<Message>,
92}
93
94impl<'a> DiagnosticPrinter<'a> {
95    pub fn new(
96        gctx: &'a GlobalContext,
97        workspace_wrapper: &'a Option<PathBuf>,
98    ) -> DiagnosticPrinter<'a> {
99        DiagnosticPrinter {
100            gctx,
101            workspace_wrapper,
102            dedupe: HashSet::new(),
103        }
104    }
105
106    pub fn print(&mut self, msg: &Message) -> CargoResult<()> {
107        match msg {
108            Message::Migrating {
109                file,
110                from_edition,
111                to_edition,
112            } => {
113                if !self.dedupe.insert(msg.clone()) {
114                    return Ok(());
115                }
116                self.gctx.shell().status(
117                    "Migrating",
118                    &format!("{file} from {from_edition} edition to {to_edition}"),
119                )
120            }
121            Message::Fixing { file } => self
122                .gctx
123                .shell()
124                .verbose(|shell| shell.status("Fixing", file)),
125            Message::Fixed { file, fixes } => {
126                let msg = if *fixes == 1 { "fix" } else { "fixes" };
127                let msg = format!("{file} ({fixes} {msg})");
128                self.gctx.shell().status("Fixed", msg)
129            }
130            Message::ReplaceFailed { file, message } => {
131                let issue_link = get_bug_report_url(self.workspace_wrapper);
132
133                let report = &[
134                    Level::ERROR
135                        .secondary_title("error applying suggestions")
136                        .element(Origin::path(file))
137                        .element(Level::ERROR.with_name("cause").message(message)),
138                    gen_please_report_this_bug_group(issue_link),
139                    gen_suggest_broken_code_group(),
140                ];
141                self.gctx.shell().print_report(report, false)?;
142                Ok(())
143            }
144            Message::FixFailed {
145                files,
146                krate,
147                errors,
148                abnormal_exit,
149            } => {
150                let to_crate = if let Some(ref krate) = *krate {
151                    format!(" to crate `{krate}`",)
152                } else {
153                    "".to_owned()
154                };
155                let issue_link = get_bug_report_url(self.workspace_wrapper);
156
157                let cause_message = if !errors.is_empty() {
158                    Some(errors.join("\n").trim().to_owned())
159                } else {
160                    None
161                };
162
163                let report = &[
164                    Level::ERROR
165                        .secondary_title(format!("errors present after applying fixes{to_crate}"))
166                        .elements(files.iter().map(|f| Origin::path(f)))
167                        .elements(
168                            cause_message
169                                .into_iter()
170                                .map(|err| Level::ERROR.with_name("cause").message(err)),
171                        )
172                        .elements(abnormal_exit.iter().map(|exit| {
173                            Level::ERROR
174                                .with_name("cause")
175                                .message(format!("rustc exited abnormally: {exit}"))
176                        })),
177                    gen_please_report_this_bug_group(issue_link),
178                    gen_suggest_broken_code_group(),
179                    Group::with_title(
180                        Level::NOTE.secondary_title("original diagnostics will follow:"),
181                    ),
182                ];
183
184                self.gctx.shell().print_report(report, false)?;
185                Ok(())
186            }
187            Message::EditionAlreadyEnabled { message, edition } => {
188                if !self.dedupe.insert(msg.clone()) {
189                    return Ok(());
190                }
191                // Don't give a really verbose warning if it has already been issued.
192                if self.dedupe.insert(Message::EditionAlreadyEnabled {
193                    message: "".to_string(), // Dummy, so that this only long-warns once.
194                    edition: *edition,
195                }) {
196                    self.gctx.shell().warn(&format!("\
197{message}
198
199If you are trying to migrate from the previous edition ({prev_edition}), the
200process requires following these steps:
201
2021. Start with `edition = \"{prev_edition}\"` in `Cargo.toml`
2032. Run `cargo fix --edition`
2043. Modify `Cargo.toml` to set `edition = \"{this_edition}\"`
2054. Run `cargo build` or `cargo test` to verify the fixes worked
206
207More details may be found at
208https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html
209",
210                        this_edition=edition, prev_edition=edition.previous().unwrap()
211                    ))
212                } else {
213                    self.gctx.shell().warn(message)
214                }
215            }
216        }
217    }
218}
219
220fn gen_please_report_this_bug_group(url: &str) -> Group<'static> {
221    Group::with_title(Level::HELP.secondary_title(format!(
222        "to report this as a bug, open an issue at {url}, quoting the full output of this command"
223    )))
224}
225
226fn gen_suggest_broken_code_group() -> Group<'static> {
227    Group::with_title(
228        Level::HELP
229            .secondary_title("to possibly apply more fixes, pass in the `--broken-code` flag"),
230    )
231}
232
233fn get_bug_report_url(rustc_workspace_wrapper: &Option<PathBuf>) -> &str {
234    let clippy = std::ffi::OsStr::new("clippy-driver");
235    let issue_link = match rustc_workspace_wrapper.as_ref().and_then(|x| x.file_stem()) {
236        Some(wrapper) if wrapper == clippy => "https://github.com/rust-lang/rust-clippy/issues",
237        _ => "https://github.com/rust-lang/rust/issues",
238    };
239
240    issue_link
241}
242
243#[derive(Debug)]
244pub struct RustfixDiagnosticServer {
245    listener: TcpListener,
246    addr: SocketAddr,
247}
248
249pub struct StartedServer {
250    addr: SocketAddr,
251    done: Arc<AtomicBool>,
252    thread: Option<JoinHandle<()>>,
253}
254
255impl RustfixDiagnosticServer {
256    pub fn new() -> Result<Self, Error> {
257        let listener = TcpListener::bind(&LOCALHOST[..])
258            .context("failed to bind TCP listener to manage locking")?;
259        let addr = listener.local_addr()?;
260
261        Ok(RustfixDiagnosticServer { listener, addr })
262    }
263
264    pub fn configure(&self, process: &mut ProcessBuilder) {
265        process.env(DIAGNOSTICS_SERVER_VAR, self.addr.to_string());
266    }
267
268    pub fn start<F>(self, on_message: F) -> Result<StartedServer, Error>
269    where
270        F: Fn(Message) + Send + 'static,
271    {
272        let addr = self.addr;
273        let done = Arc::new(AtomicBool::new(false));
274        let done2 = done.clone();
275        let thread = thread::spawn(move || {
276            self.run(&on_message, &done2);
277        });
278
279        Ok(StartedServer {
280            addr,
281            thread: Some(thread),
282            done,
283        })
284    }
285
286    fn run(self, on_message: &dyn Fn(Message), done: &AtomicBool) {
287        while let Ok((client, _)) = self.listener.accept() {
288            if done.load(Ordering::SeqCst) {
289                break;
290            }
291            let mut client = BufReader::new(client);
292            let mut s = String::new();
293            if let Err(e) = client.read_to_string(&mut s) {
294                warn!("diagnostic server failed to read: {e}");
295            } else {
296                match serde_json::from_str(&s) {
297                    Ok(message) => on_message(message),
298                    Err(e) => warn!("invalid diagnostics message: {e}"),
299                }
300            }
301            // The client should be kept alive until after `on_message` is
302            // called to ensure that the client doesn't exit too soon (and
303            // Message::Finish getting posted before Message::FixDiagnostic).
304            drop(client);
305        }
306    }
307}
308
309impl Drop for StartedServer {
310    fn drop(&mut self) {
311        self.done.store(true, Ordering::SeqCst);
312        // Ignore errors here as this is largely best-effort
313        if TcpStream::connect(&self.addr).is_err() {
314            return;
315        }
316        drop(self.thread.take().unwrap().join());
317    }
318}