blob: 72eafa63c09ae8dfa4b1f4091e0a5de7adb6a398 [file] [log] [blame]
//! A small TCP server to handle collection of diagnostics information in a
//! cross-platform way for the `cargo fix` command.
use std::collections::HashSet;
use std::io::{BufReader, Read, Write};
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use anyhow::{Context as _, Error};
use cargo_util::ProcessBuilder;
use serde::{Deserialize, Serialize};
use tracing::warn;
use crate::core::Edition;
use crate::util::errors::CargoResult;
use crate::util::GlobalContext;
const DIAGNOSTICS_SERVER_VAR: &str = "__CARGO_FIX_DIAGNOSTICS_SERVER";
#[derive(Deserialize, Serialize, Hash, Eq, PartialEq, Clone)]
pub enum Message {
Migrating {
file: String,
from_edition: Edition,
to_edition: Edition,
},
Fixing {
file: String,
},
Fixed {
file: String,
fixes: u32,
},
FixFailed {
files: Vec<String>,
krate: Option<String>,
errors: Vec<String>,
abnormal_exit: Option<String>,
},
ReplaceFailed {
file: String,
message: String,
},
EditionAlreadyEnabled {
message: String,
edition: Edition,
},
}
impl Message {
pub fn post(&self, gctx: &GlobalContext) -> Result<(), Error> {
let addr = gctx
.get_env(DIAGNOSTICS_SERVER_VAR)
.context("diagnostics collector misconfigured")?;
let mut client =
TcpStream::connect(&addr).context("failed to connect to parent diagnostics target")?;
let s = serde_json::to_string(self).context("failed to serialize message")?;
client
.write_all(s.as_bytes())
.context("failed to write message to diagnostics target")?;
client
.shutdown(Shutdown::Write)
.context("failed to shutdown")?;
client
.read_to_end(&mut Vec::new())
.context("failed to receive a disconnect")?;
Ok(())
}
}
/// A printer that will print diagnostics messages to the shell.
pub struct DiagnosticPrinter<'a> {
/// The context to get the shell to print to.
gctx: &'a GlobalContext,
/// An optional wrapper to be used in addition to `rustc.wrapper` for workspace crates.
/// This is used to get the correct bug report URL. For instance,
/// if `clippy-driver` is set as the value for the wrapper,
/// then the correct bug report URL for `clippy` can be obtained.
workspace_wrapper: &'a Option<PathBuf>,
// A set of messages that have already been printed.
dedupe: HashSet<Message>,
}
impl<'a> DiagnosticPrinter<'a> {
pub fn new(
gctx: &'a GlobalContext,
workspace_wrapper: &'a Option<PathBuf>,
) -> DiagnosticPrinter<'a> {
DiagnosticPrinter {
gctx,
workspace_wrapper,
dedupe: HashSet::new(),
}
}
pub fn print(&mut self, msg: &Message) -> CargoResult<()> {
match msg {
Message::Migrating {
file,
from_edition,
to_edition,
} => {
if !self.dedupe.insert(msg.clone()) {
return Ok(());
}
self.gctx.shell().status(
"Migrating",
&format!("{} from {} edition to {}", file, from_edition, to_edition),
)
}
Message::Fixing { file } => self
.gctx
.shell()
.verbose(|shell| shell.status("Fixing", file)),
Message::Fixed { file, fixes } => {
let msg = if *fixes == 1 { "fix" } else { "fixes" };
let msg = format!("{} ({} {})", file, fixes, msg);
self.gctx.shell().status("Fixed", msg)
}
Message::ReplaceFailed { file, message } => {
let msg = format!("error applying suggestions to `{}`\n", file);
self.gctx.shell().warn(&msg)?;
write!(
self.gctx.shell().err(),
"The full error message was:\n\n> {}\n\n",
message,
)?;
let issue_link = get_bug_report_url(self.workspace_wrapper);
write!(
self.gctx.shell().err(),
"{}",
gen_please_report_this_bug_text(issue_link)
)?;
Ok(())
}
Message::FixFailed {
files,
krate,
errors,
abnormal_exit,
} => {
if let Some(ref krate) = *krate {
self.gctx.shell().warn(&format!(
"failed to automatically apply fixes suggested by rustc \
to crate `{}`",
krate,
))?;
} else {
self.gctx
.shell()
.warn("failed to automatically apply fixes suggested by rustc")?;
}
if !files.is_empty() {
writeln!(
self.gctx.shell().err(),
"\nafter fixes were automatically applied the compiler \
reported errors within these files:\n"
)?;
for file in files {
writeln!(self.gctx.shell().err(), " * {}", file)?;
}
writeln!(self.gctx.shell().err())?;
}
let issue_link = get_bug_report_url(self.workspace_wrapper);
write!(
self.gctx.shell().err(),
"{}",
gen_please_report_this_bug_text(issue_link)
)?;
if !errors.is_empty() {
writeln!(
self.gctx.shell().err(),
"The following errors were reported:"
)?;
for error in errors {
write!(self.gctx.shell().err(), "{}", error)?;
if !error.ends_with('\n') {
writeln!(self.gctx.shell().err())?;
}
}
}
if let Some(exit) = abnormal_exit {
writeln!(self.gctx.shell().err(), "rustc exited abnormally: {}", exit)?;
}
writeln!(
self.gctx.shell().err(),
"Original diagnostics will follow.\n"
)?;
Ok(())
}
Message::EditionAlreadyEnabled { message, edition } => {
if !self.dedupe.insert(msg.clone()) {
return Ok(());
}
// Don't give a really verbose warning if it has already been issued.
if self.dedupe.insert(Message::EditionAlreadyEnabled {
message: "".to_string(), // Dummy, so that this only long-warns once.
edition: *edition,
}) {
self.gctx.shell().warn(&format!("\
{}
If you are trying to migrate from the previous edition ({prev_edition}), the
process requires following these steps:
1. Start with `edition = \"{prev_edition}\"` in `Cargo.toml`
2. Run `cargo fix --edition`
3. Modify `Cargo.toml` to set `edition = \"{this_edition}\"`
4. Run `cargo build` or `cargo test` to verify the fixes worked
More details may be found at
https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html
",
message, this_edition=edition, prev_edition=edition.previous().unwrap()
))
} else {
self.gctx.shell().warn(message)
}
}
}
}
}
fn gen_please_report_this_bug_text(url: &str) -> String {
format!(
"This likely indicates a bug in either rustc or cargo itself,\n\
and we would appreciate a bug report! You're likely to see\n\
a number of compiler warnings after this message which cargo\n\
attempted to fix but failed. If you could open an issue at\n\
{}\n\
quoting the full output of this command we'd be very appreciative!\n\
Note that you may be able to make some more progress in the near-term\n\
fixing code with the `--broken-code` flag\n\n\
",
url
)
}
fn get_bug_report_url(rustc_workspace_wrapper: &Option<PathBuf>) -> &str {
let clippy = std::ffi::OsStr::new("clippy-driver");
let issue_link = match rustc_workspace_wrapper.as_ref().and_then(|x| x.file_stem()) {
Some(wrapper) if wrapper == clippy => "https://github.com/rust-lang/rust-clippy/issues",
_ => "https://github.com/rust-lang/rust/issues",
};
issue_link
}
#[derive(Debug)]
pub struct RustfixDiagnosticServer {
listener: TcpListener,
addr: SocketAddr,
}
pub struct StartedServer {
addr: SocketAddr,
done: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
impl RustfixDiagnosticServer {
pub fn new() -> Result<Self, Error> {
let listener = TcpListener::bind("127.0.0.1:0")
.with_context(|| "failed to bind TCP listener to manage locking")?;
let addr = listener.local_addr()?;
Ok(RustfixDiagnosticServer { listener, addr })
}
pub fn configure(&self, process: &mut ProcessBuilder) {
process.env(DIAGNOSTICS_SERVER_VAR, self.addr.to_string());
}
pub fn start<F>(self, on_message: F) -> Result<StartedServer, Error>
where
F: Fn(Message) + Send + 'static,
{
let addr = self.addr;
let done = Arc::new(AtomicBool::new(false));
let done2 = done.clone();
let thread = thread::spawn(move || {
self.run(&on_message, &done2);
});
Ok(StartedServer {
addr,
thread: Some(thread),
done,
})
}
fn run(self, on_message: &dyn Fn(Message), done: &AtomicBool) {
while let Ok((client, _)) = self.listener.accept() {
if done.load(Ordering::SeqCst) {
break;
}
let mut client = BufReader::new(client);
let mut s = String::new();
if let Err(e) = client.read_to_string(&mut s) {
warn!("diagnostic server failed to read: {}", e);
} else {
match serde_json::from_str(&s) {
Ok(message) => on_message(message),
Err(e) => warn!("invalid diagnostics message: {}", e),
}
}
// The client should be kept alive until after `on_message` is
// called to ensure that the client doesn't exit too soon (and
// Message::Finish getting posted before Message::FixDiagnostic).
drop(client);
}
}
}
impl Drop for StartedServer {
fn drop(&mut self) {
self.done.store(true, Ordering::SeqCst);
// Ignore errors here as this is largely best-effort
if TcpStream::connect(&self.addr).is_err() {
return;
}
drop(self.thread.take().unwrap().join());
}
}