#
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}