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::env;
6use std::io::{BufReader, Read, Write};
7use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use std::thread::{self, JoinHandle};
11
12use anyhow::{Context, Error};
13use log::warn;
14use serde::{Deserialize, Serialize};
15
16use crate::util::errors::CargoResult;
17use crate::util::{Config, ProcessBuilder};
18
19const DIAGNOSICS_SERVER_VAR: &str = "__CARGO_FIX_DIAGNOSTICS_SERVER";
20const PLEASE_REPORT_THIS_BUG: &str =
21    "This likely indicates a bug in either rustc or cargo itself,\n\
22     and we would appreciate a bug report! You're likely to see \n\
23     a number of compiler warnings after this message which cargo\n\
24     attempted to fix but failed. If you could open an issue at\n\
25     https://github.com/rust-lang/rust/issues\n\
26     quoting the full output of this command we'd be very appreciative!\n\
27     Note that you may be able to make some more progress in the near-term\n\
28     fixing code with the `--broken-code` flag\n\n\
29     ";
30
31#[derive(Deserialize, Serialize)]
32pub enum Message {
33    Fixing {
34        file: String,
35        fixes: u32,
36    },
37    FixFailed {
38        files: Vec<String>,
39        krate: Option<String>,
40        errors: Vec<String>,
41    },
42    ReplaceFailed {
43        file: String,
44        message: String,
45    },
46    EditionAlreadyEnabled {
47        file: String,
48        edition: String,
49    },
50    IdiomEditionMismatch {
51        file: String,
52        idioms: String,
53        edition: Option<String>,
54    },
55}
56
57impl Message {
58    pub fn post(&self) -> Result<(), Error> {
59        let addr =
60            env::var(DIAGNOSICS_SERVER_VAR).context("diagnostics collector misconfigured")?;
61        let mut client =
62            TcpStream::connect(&addr).context("failed to connect to parent diagnostics target")?;
63
64        let s = serde_json::to_string(self).context("failed to serialize message")?;
65        client
66            .write_all(s.as_bytes())
67            .context("failed to write message to diagnostics target")?;
68        client
69            .shutdown(Shutdown::Write)
70            .context("failed to shutdown")?;
71
72        let mut tmp = Vec::new();
73        client
74            .read_to_end(&mut tmp)
75            .context("failed to receive a disconnect")?;
76
77        Ok(())
78    }
79}
80
81pub struct DiagnosticPrinter<'a> {
82    config: &'a Config,
83    edition_already_enabled: HashSet<String>,
84    idiom_mismatch: HashSet<String>,
85}
86
87impl<'a> DiagnosticPrinter<'a> {
88    pub fn new(config: &'a Config) -> DiagnosticPrinter<'a> {
89        DiagnosticPrinter {
90            config,
91            edition_already_enabled: HashSet::new(),
92            idiom_mismatch: HashSet::new(),
93        }
94    }
95
96    pub fn print(&mut self, msg: &Message) -> CargoResult<()> {
97        match msg {
98            Message::Fixing { file, fixes } => {
99                let msg = if *fixes == 1 { "fix" } else { "fixes" };
100                let msg = format!("{} ({} {})", file, fixes, msg);
101                self.config.shell().status("Fixing", msg)
102            }
103            Message::ReplaceFailed { file, message } => {
104                let msg = format!("error applying suggestions to `{}`\n", file);
105                self.config.shell().warn(&msg)?;
106                write!(
107                    self.config.shell().err(),
108                    "The full error message was:\n\n> {}\n\n",
109                    message,
110                )?;
111                write!(self.config.shell().err(), "{}", PLEASE_REPORT_THIS_BUG)?;
112                Ok(())
113            }
114            Message::FixFailed {
115                files,
116                krate,
117                errors,
118            } => {
119                if let Some(ref krate) = *krate {
120                    self.config.shell().warn(&format!(
121                        "failed to automatically apply fixes suggested by rustc \
122                         to crate `{}`",
123                        krate,
124                    ))?;
125                } else {
126                    self.config
127                        .shell()
128                        .warn("failed to automatically apply fixes suggested by rustc")?;
129                }
130                if !files.is_empty() {
131                    writeln!(
132                        self.config.shell().err(),
133                        "\nafter fixes were automatically applied the compiler \
134                         reported errors within these files:\n"
135                    )?;
136                    for file in files {
137                        writeln!(self.config.shell().err(), "  * {}", file)?;
138                    }
139                    writeln!(self.config.shell().err())?;
140                }
141                write!(self.config.shell().err(), "{}", PLEASE_REPORT_THIS_BUG)?;
142                if !errors.is_empty() {
143                    writeln!(
144                        self.config.shell().err(),
145                        "The following errors were reported:"
146                    )?;
147                    for error in errors {
148                        write!(self.config.shell().err(), "{}", error)?;
149                        if !error.ends_with('\n') {
150                            writeln!(self.config.shell().err())?;
151                        }
152                    }
153                }
154                writeln!(
155                    self.config.shell().err(),
156                    "Original diagnostics will follow.\n"
157                )?;
158                Ok(())
159            }
160            Message::EditionAlreadyEnabled { file, edition } => {
161                // Like above, only warn once per file
162                if !self.edition_already_enabled.insert(file.clone()) {
163                    return Ok(());
164                }
165
166                let msg = format!(
167                    "\
168cannot prepare for the {} edition when it is enabled, so cargo cannot
169automatically fix errors in `{}`
170
171To prepare for the {0} edition you should first remove `edition = '{0}'` from
172your `Cargo.toml` and then rerun this command. Once all warnings have been fixed
173then you can re-enable the `edition` key in `Cargo.toml`. For some more
174information about transitioning to the {0} edition see:
175
176  https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html
177",
178                    edition,
179                    file,
180                );
181                self.config.shell().error(&msg)?;
182                Ok(())
183            }
184            Message::IdiomEditionMismatch {
185                file,
186                idioms,
187                edition,
188            } => {
189                // Same as above
190                if !self.idiom_mismatch.insert(file.clone()) {
191                    return Ok(());
192                }
193                self.config.shell().error(&format!(
194                    "\
195cannot migrate to the idioms of the {} edition for `{}`
196because it is compiled {}, which doesn't match {0}
197
198consider migrating to the {0} edition by adding `edition = '{0}'` to
199`Cargo.toml` and then rerunning this command; a more detailed transition
200guide can be found at
201
202  https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html
203",
204                    idioms,
205                    file,
206                    match edition {
207                        Some(s) => format!("with the {} edition", s),
208                        None => "without an edition".to_string(),
209                    },
210                ))?;
211                Ok(())
212            }
213        }
214    }
215}
216
217#[derive(Debug)]
218pub struct RustfixDiagnosticServer {
219    listener: TcpListener,
220    addr: SocketAddr,
221}
222
223pub struct StartedServer {
224    addr: SocketAddr,
225    done: Arc<AtomicBool>,
226    thread: Option<JoinHandle<()>>,
227}
228
229impl RustfixDiagnosticServer {
230    pub fn new() -> Result<Self, Error> {
231        let listener = TcpListener::bind("127.0.0.1:0")
232            .with_context(|| "failed to bind TCP listener to manage locking")?;
233        let addr = listener.local_addr()?;
234
235        Ok(RustfixDiagnosticServer { listener, addr })
236    }
237
238    pub fn configure(&self, process: &mut ProcessBuilder) {
239        process.env(DIAGNOSICS_SERVER_VAR, self.addr.to_string());
240    }
241
242    pub fn start<F>(self, on_message: F) -> Result<StartedServer, Error>
243    where
244        F: Fn(Message) + Send + 'static,
245    {
246        let addr = self.addr;
247        let done = Arc::new(AtomicBool::new(false));
248        let done2 = done.clone();
249        let thread = thread::spawn(move || {
250            self.run(&on_message, &done2);
251        });
252
253        Ok(StartedServer {
254            addr,
255            thread: Some(thread),
256            done,
257        })
258    }
259
260    fn run(self, on_message: &dyn Fn(Message), done: &AtomicBool) {
261        while let Ok((client, _)) = self.listener.accept() {
262            if done.load(Ordering::SeqCst) {
263                break;
264            }
265            let mut client = BufReader::new(client);
266            let mut s = String::new();
267            if let Err(e) = client.read_to_string(&mut s) {
268                warn!("diagnostic server failed to read: {}", e);
269            } else {
270                match serde_json::from_str(&s) {
271                    Ok(message) => on_message(message),
272                    Err(e) => warn!("invalid diagnostics message: {}", e),
273                }
274            }
275            // The client should be kept alive until after `on_message` is
276            // called to ensure that the client doesn't exit too soon (and
277            // Message::Finish getting posted before Message::FixDiagnostic).
278            drop(client);
279        }
280    }
281}
282
283impl Drop for StartedServer {
284    fn drop(&mut self) {
285        self.done.store(true, Ordering::SeqCst);
286        // Ignore errors here as this is largely best-effort
287        if TcpStream::connect(&self.addr).is_err() {
288            return;
289        }
290        drop(self.thread.take().unwrap().join());
291    }
292}