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