6bca9f2aac
Specify thread types using Quality of Service API
<details>
<summary>Some background (in case you haven’t heard of QoS before)</summary>
Heterogenous multi-core CPUs are increasingly found in laptops and desktops (e.g. Alder Lake, Snapdragon 8cx Gen 3, M1). To maximize efficiency on this kind of hardware, it is important to provide the operating system with more information so threads can be scheduled on different core types appropriately.
The approach that XNU (the kernel of macOS, iOS, etc) and Windows have taken is to provide a high-level semantic API – quality of service, or QoS – which informs the OS of the program’s intent. For instance, you might specify that a thread is running a render loop for a game. This makes the OS provide this thread with as large a share of the system’s resources as possible. Specifying a thread is running an unimportant background task, on the other hand, is cause for it to be scheduled exclusively on high-efficiency cores instead of high-performance cores.
QoS APIs allows for easy configuration of many different parameters at once; for instance, setting QoS on XNU affects scheduling, timer latency, I/O priorities, and of course what core type the thread in question should run on. I don’t know any details on how QoS works on Windows, but I would guess it’s similar.
Hypothetically, taking advantage of these APIs would improve power consumption, thermals, battery life if applicable, etc.
</details>
# Relevance to rust-analyzer
From what I can tell the philosophy behind both the XNU and Windows QoS APIs is that _user interfaces should never stutter under any circumstances._ You can see this in the array of QoS classes which are available: the highest QoS class in both APIs is one intended explicitly for UI render loops.
Imagine rust-analyzer is performing CPU-intensive background work – maybe you just invoked Find Usages on `usize` or opened a large project – in this scenario the editor’s render loop should absolutely get higher priority than rust-analyzer, no matter what. You could view it in terms of “realtime-ness”: flight control software is hard realtime, audio software is soft realtime, GUIs are softer realtime, and rust-analyzer is not realtime at all. Of course, maximizing responsiveness is important, but respecting the rest of the system is more important.
# Implementation
I’ve tried my best to unify thread creation in `stdx`, where the new API I’ve introduced _requires_ specifying a QoS class. Different points along the performance/efficiency curve can make a great difference; the M1’s e-cores use around three times less power than the p-cores, so putting in this effort is worthwhile IMO.
It’s worth mentioning that Linux does not [yet](https://youtu.be/RfgPWpTwTQo) have a QoS API. Maybe translating QoS into regular thread priorities would be acceptable? From what I can tell the only scheduling-related code in rust-analyzer is Windows-specific, so ignoring QoS entirely on Linux shouldn’t cause any new issues. Also, I haven’t implemented support for the Windows QoS APIs because I don’t have a Windows machine to test on, and because I’m completely unfamiliar with Windows APIs :)
I noticed that rust-analyzer handles some requests on the main thread (using `.on_sync()`) and others on a threadpool (using `.on()`). I think it would make sense to run the main thread at the User Initiated QoS and the threadpool at Utility, but only if all requests that are caused by typing use `.on_sync()` and all that don’t use `.on()`. I don’t understand how the `.on_sync()`/`.on()` split that’s currently present was chosen, so I’ve let this code be for the moment. Let me know if changing this to what I proposed makes any sense.
To avoid having to change everything back in case I’ve misunderstood something, I’ve left all threads at the Utility QoS for now. Of course, this isn’t what I hope the code will look like in the end, but I figured I have to start somewhere :P
# References
<ul>
<li><a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html">Apple documentation related to QoS</a></li>
<li><a href="67e155c940/include/pthread/qos.h
">pthread API for setting QoS on XNU</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows/win32/procthread/quality-of-service">Windows’s QoS classes</a></li>
<li>
<details>
<summary>Full documentation of XNU QoS classes. This documentation is only available as a huge not-very-readable comment in a header file, so I’ve reformatted it and put it here for reference.</summary>
<ul>
<li><p><strong><code>QOS_CLASS_USER_INTERACTIVE</code>: A QOS class which indicates work performed by this thread is interactive with the user.</strong></p><p>Such work is requested to run at high priority relative to other work on the system. Specifying this QOS class is a request to run with nearly all available system CPU and I/O bandwidth even under contention. This is not an energy-efficient QOS class to use for large tasks. The use of this QOS class should be limited to critical interaction with the user such as handling events on the main event loop, view drawing, animation, etc.</p></li>
<li><p><strong><code>QOS_CLASS_USER_INITIATED</code>: A QOS class which indicates work performed by this thread was initiated by the user and that the user is likely waiting for the results.</strong></p><p>Such work is requested to run at a priority below critical user-interactive work, but relatively higher than other work on the system. This is not an energy-efficient QOS class to use for large tasks. Its use should be limited to operations of short enough duration that the user is unlikely to switch tasks while waiting for the results. Typical user-initiated work will have progress indicated by the display of placeholder content or modal user interface.</p></li>
<li><p><strong><code>QOS_CLASS_DEFAULT</code>: A default QOS class used by the system in cases where more specific QOS class information is not available.</strong></p><p>Such work is requested to run at a priority below critical user-interactive and user-initiated work, but relatively higher than utility and background tasks. Threads created by <code>pthread_create()</code> without an attribute specifying a QOS class will default to <code>QOS_CLASS_DEFAULT</code>. This QOS class value is not intended to be used as a work classification, it should only be set when propagating or restoring QOS class values provided by the system.</p></li>
<li><p><strong><code>QOS_CLASS_UTILITY</code>: A QOS class which indicates work performed by this thread may or may not be initiated by the user and that the user is unlikely to be immediately waiting for the results.</strong></p><p>Such work is requested to run at a priority below critical user-interactive and user-initiated work, but relatively higher than low-level system maintenance tasks. The use of this QOS class indicates the work should be run in an energy and thermally-efficient manner. The progress of utility work may or may not be indicated to the user, but the effect of such work is user-visible.</p></li>
<li><p><strong><code>QOS_CLASS_BACKGROUND</code>: A QOS class which indicates work performed by this thread was not initiated by the user and that the user may be unaware of the results.</strong></p><p>Such work is requested to run at a priority below other work. The use of this QOS class indicates the work should be run in the most energy and thermally-efficient manner.</p></li>
<li><p><strong><code>QOS_CLASS_UNSPECIFIED</code>: A QOS class value which indicates the absence or removal of QOS class information.</strong></p><p>As an API return value, may indicate that threads or pthread attributes were configured with legacy API incompatible or in conflict with the QOS class system.</p></li>
</ul>
</details>
</li>
</ul>
527 lines
18 KiB
Rust
527 lines
18 KiB
Rust
//! Flycheck provides the functionality needed to run `cargo check` or
|
|
//! another compatible command (f.x. clippy) in a background thread and provide
|
|
//! LSP diagnostics based on the output of the command.
|
|
|
|
#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
|
|
|
|
use std::{
|
|
fmt, io,
|
|
process::{ChildStderr, ChildStdout, Command, Stdio},
|
|
time::Duration,
|
|
};
|
|
|
|
use command_group::{CommandGroup, GroupChild};
|
|
use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
|
|
use paths::AbsPathBuf;
|
|
use rustc_hash::FxHashMap;
|
|
use serde::Deserialize;
|
|
use stdx::process::streaming_output;
|
|
|
|
pub use cargo_metadata::diagnostic::{
|
|
Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
|
|
DiagnosticSpanMacroExpansion,
|
|
};
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum InvocationStrategy {
|
|
Once,
|
|
#[default]
|
|
PerWorkspace,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub enum InvocationLocation {
|
|
Root(AbsPathBuf),
|
|
#[default]
|
|
Workspace,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum FlycheckConfig {
|
|
CargoCommand {
|
|
command: String,
|
|
target_triples: Vec<String>,
|
|
all_targets: bool,
|
|
no_default_features: bool,
|
|
all_features: bool,
|
|
features: Vec<String>,
|
|
extra_args: Vec<String>,
|
|
extra_env: FxHashMap<String, String>,
|
|
ansi_color_output: bool,
|
|
},
|
|
CustomCommand {
|
|
command: String,
|
|
args: Vec<String>,
|
|
extra_env: FxHashMap<String, String>,
|
|
invocation_strategy: InvocationStrategy,
|
|
invocation_location: InvocationLocation,
|
|
},
|
|
}
|
|
|
|
impl fmt::Display for FlycheckConfig {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
|
|
FlycheckConfig::CustomCommand { command, args, .. } => {
|
|
write!(f, "{command} {}", args.join(" "))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Flycheck wraps the shared state and communication machinery used for
|
|
/// running `cargo check` (or other compatible command) and providing
|
|
/// diagnostics based on the output.
|
|
/// The spawned thread is shut down when this struct is dropped.
|
|
#[derive(Debug)]
|
|
pub struct FlycheckHandle {
|
|
// XXX: drop order is significant
|
|
sender: Sender<StateChange>,
|
|
_thread: stdx::thread::JoinHandle,
|
|
id: usize,
|
|
}
|
|
|
|
impl FlycheckHandle {
|
|
pub fn spawn(
|
|
id: usize,
|
|
sender: Box<dyn Fn(Message) + Send>,
|
|
config: FlycheckConfig,
|
|
workspace_root: AbsPathBuf,
|
|
) -> FlycheckHandle {
|
|
let actor = FlycheckActor::new(id, sender, config, workspace_root);
|
|
let (sender, receiver) = unbounded::<StateChange>();
|
|
let thread = stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
|
.name("Flycheck".to_owned())
|
|
.spawn(move || actor.run(receiver))
|
|
.expect("failed to spawn thread");
|
|
FlycheckHandle { id, sender, _thread: thread }
|
|
}
|
|
|
|
/// Schedule a re-start of the cargo check worker.
|
|
pub fn restart(&self) {
|
|
self.sender.send(StateChange::Restart).unwrap();
|
|
}
|
|
|
|
/// Stop this cargo check worker.
|
|
pub fn cancel(&self) {
|
|
self.sender.send(StateChange::Cancel).unwrap();
|
|
}
|
|
|
|
pub fn id(&self) -> usize {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
pub enum Message {
|
|
/// Request adding a diagnostic with fixes included to a file
|
|
AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
|
|
|
|
/// Request check progress notification to client
|
|
Progress {
|
|
/// Flycheck instance ID
|
|
id: usize,
|
|
progress: Progress,
|
|
},
|
|
}
|
|
|
|
impl fmt::Debug for Message {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Message::AddDiagnostic { id, workspace_root, diagnostic } => f
|
|
.debug_struct("AddDiagnostic")
|
|
.field("id", id)
|
|
.field("workspace_root", workspace_root)
|
|
.field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
|
|
.finish(),
|
|
Message::Progress { id, progress } => {
|
|
f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Progress {
|
|
DidStart,
|
|
DidCheckCrate(String),
|
|
DidFinish(io::Result<()>),
|
|
DidCancel,
|
|
DidFailToRestart(String),
|
|
}
|
|
|
|
enum StateChange {
|
|
Restart,
|
|
Cancel,
|
|
}
|
|
|
|
/// A [`FlycheckActor`] is a single check instance of a workspace.
|
|
struct FlycheckActor {
|
|
/// The workspace id of this flycheck instance.
|
|
id: usize,
|
|
sender: Box<dyn Fn(Message) + Send>,
|
|
config: FlycheckConfig,
|
|
/// Either the workspace root of the workspace we are flychecking,
|
|
/// or the project root of the project.
|
|
root: AbsPathBuf,
|
|
/// CargoHandle exists to wrap around the communication needed to be able to
|
|
/// run `cargo check` without blocking. Currently the Rust standard library
|
|
/// doesn't provide a way to read sub-process output without blocking, so we
|
|
/// have to wrap sub-processes output handling in a thread and pass messages
|
|
/// back over a channel.
|
|
cargo_handle: Option<CargoHandle>,
|
|
}
|
|
|
|
enum Event {
|
|
RequestStateChange(StateChange),
|
|
CheckEvent(Option<CargoMessage>),
|
|
}
|
|
|
|
impl FlycheckActor {
|
|
fn new(
|
|
id: usize,
|
|
sender: Box<dyn Fn(Message) + Send>,
|
|
config: FlycheckConfig,
|
|
workspace_root: AbsPathBuf,
|
|
) -> FlycheckActor {
|
|
tracing::info!(%id, ?workspace_root, "Spawning flycheck");
|
|
FlycheckActor { id, sender, config, root: workspace_root, cargo_handle: None }
|
|
}
|
|
|
|
fn report_progress(&self, progress: Progress) {
|
|
self.send(Message::Progress { id: self.id, progress });
|
|
}
|
|
|
|
fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
|
|
let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
|
|
if let Ok(msg) = inbox.try_recv() {
|
|
// give restarts a preference so check outputs don't block a restart or stop
|
|
return Some(Event::RequestStateChange(msg));
|
|
}
|
|
select! {
|
|
recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
|
|
recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
|
|
}
|
|
}
|
|
|
|
fn run(mut self, inbox: Receiver<StateChange>) {
|
|
'event: while let Some(event) = self.next_event(&inbox) {
|
|
match event {
|
|
Event::RequestStateChange(StateChange::Cancel) => {
|
|
tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
|
|
self.cancel_check_process();
|
|
}
|
|
Event::RequestStateChange(StateChange::Restart) => {
|
|
// Cancel the previously spawned process
|
|
self.cancel_check_process();
|
|
while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
|
|
// restart chained with a stop, so just cancel
|
|
if let StateChange::Cancel = restart {
|
|
continue 'event;
|
|
}
|
|
}
|
|
|
|
let command = self.check_command();
|
|
tracing::debug!(?command, "will restart flycheck");
|
|
match CargoHandle::spawn(command) {
|
|
Ok(cargo_handle) => {
|
|
tracing::debug!(
|
|
command = ?self.check_command(),
|
|
"did restart flycheck"
|
|
);
|
|
self.cargo_handle = Some(cargo_handle);
|
|
self.report_progress(Progress::DidStart);
|
|
}
|
|
Err(error) => {
|
|
self.report_progress(Progress::DidFailToRestart(format!(
|
|
"Failed to run the following command: {:?} error={}",
|
|
self.check_command(),
|
|
error
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
Event::CheckEvent(None) => {
|
|
tracing::debug!(flycheck_id = self.id, "flycheck finished");
|
|
|
|
// Watcher finished
|
|
let cargo_handle = self.cargo_handle.take().unwrap();
|
|
let res = cargo_handle.join();
|
|
if res.is_err() {
|
|
tracing::error!(
|
|
"Flycheck failed to run the following command: {:?}",
|
|
self.check_command()
|
|
);
|
|
}
|
|
self.report_progress(Progress::DidFinish(res));
|
|
}
|
|
Event::CheckEvent(Some(message)) => match message {
|
|
CargoMessage::CompilerArtifact(msg) => {
|
|
tracing::trace!(
|
|
flycheck_id = self.id,
|
|
artifact = msg.target.name,
|
|
"artifact received"
|
|
);
|
|
self.report_progress(Progress::DidCheckCrate(msg.target.name));
|
|
}
|
|
|
|
CargoMessage::Diagnostic(msg) => {
|
|
tracing::trace!(
|
|
flycheck_id = self.id,
|
|
message = msg.message,
|
|
"diagnostic received"
|
|
);
|
|
self.send(Message::AddDiagnostic {
|
|
id: self.id,
|
|
workspace_root: self.root.clone(),
|
|
diagnostic: msg,
|
|
});
|
|
}
|
|
},
|
|
}
|
|
}
|
|
// If we rerun the thread, we need to discard the previous check results first
|
|
self.cancel_check_process();
|
|
}
|
|
|
|
fn cancel_check_process(&mut self) {
|
|
if let Some(cargo_handle) = self.cargo_handle.take() {
|
|
tracing::debug!(
|
|
command = ?self.check_command(),
|
|
"did cancel flycheck"
|
|
);
|
|
cargo_handle.cancel();
|
|
self.report_progress(Progress::DidCancel);
|
|
}
|
|
}
|
|
|
|
fn check_command(&self) -> Command {
|
|
let (mut cmd, args) = match &self.config {
|
|
FlycheckConfig::CargoCommand {
|
|
command,
|
|
target_triples,
|
|
no_default_features,
|
|
all_targets,
|
|
all_features,
|
|
extra_args,
|
|
features,
|
|
extra_env,
|
|
ansi_color_output,
|
|
} => {
|
|
let mut cmd = Command::new(toolchain::cargo());
|
|
cmd.arg(command);
|
|
cmd.current_dir(&self.root);
|
|
cmd.arg("--workspace");
|
|
|
|
cmd.arg(if *ansi_color_output {
|
|
"--message-format=json-diagnostic-rendered-ansi"
|
|
} else {
|
|
"--message-format=json"
|
|
});
|
|
|
|
cmd.arg("--manifest-path");
|
|
cmd.arg(self.root.join("Cargo.toml").as_os_str());
|
|
|
|
for target in target_triples {
|
|
cmd.args(["--target", target.as_str()]);
|
|
}
|
|
if *all_targets {
|
|
cmd.arg("--all-targets");
|
|
}
|
|
if *all_features {
|
|
cmd.arg("--all-features");
|
|
} else {
|
|
if *no_default_features {
|
|
cmd.arg("--no-default-features");
|
|
}
|
|
if !features.is_empty() {
|
|
cmd.arg("--features");
|
|
cmd.arg(features.join(" "));
|
|
}
|
|
}
|
|
cmd.envs(extra_env);
|
|
(cmd, extra_args)
|
|
}
|
|
FlycheckConfig::CustomCommand {
|
|
command,
|
|
args,
|
|
extra_env,
|
|
invocation_strategy,
|
|
invocation_location,
|
|
} => {
|
|
let mut cmd = Command::new(command);
|
|
cmd.envs(extra_env);
|
|
|
|
match invocation_location {
|
|
InvocationLocation::Workspace => {
|
|
match invocation_strategy {
|
|
InvocationStrategy::Once => {
|
|
cmd.current_dir(&self.root);
|
|
}
|
|
InvocationStrategy::PerWorkspace => {
|
|
// FIXME: cmd.current_dir(&affected_workspace);
|
|
cmd.current_dir(&self.root);
|
|
}
|
|
}
|
|
}
|
|
InvocationLocation::Root(root) => {
|
|
cmd.current_dir(root);
|
|
}
|
|
}
|
|
|
|
(cmd, args)
|
|
}
|
|
};
|
|
|
|
cmd.args(args);
|
|
cmd
|
|
}
|
|
|
|
fn send(&self, check_task: Message) {
|
|
(self.sender)(check_task);
|
|
}
|
|
}
|
|
|
|
struct JodGroupChild(GroupChild);
|
|
|
|
impl Drop for JodGroupChild {
|
|
fn drop(&mut self) {
|
|
_ = self.0.kill();
|
|
_ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
/// A handle to a cargo process used for fly-checking.
|
|
struct CargoHandle {
|
|
/// The handle to the actual cargo process. As we cannot cancel directly from with
|
|
/// a read syscall dropping and therefore terminating the process is our best option.
|
|
child: JodGroupChild,
|
|
thread: stdx::thread::JoinHandle<io::Result<(bool, String)>>,
|
|
receiver: Receiver<CargoMessage>,
|
|
}
|
|
|
|
impl CargoHandle {
|
|
fn spawn(mut command: Command) -> std::io::Result<CargoHandle> {
|
|
command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
|
|
let mut child = command.group_spawn().map(JodGroupChild)?;
|
|
|
|
let stdout = child.0.inner().stdout.take().unwrap();
|
|
let stderr = child.0.inner().stderr.take().unwrap();
|
|
|
|
let (sender, receiver) = unbounded();
|
|
let actor = CargoActor::new(sender, stdout, stderr);
|
|
let thread = stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
|
.name("CargoHandle".to_owned())
|
|
.spawn(move || actor.run())
|
|
.expect("failed to spawn thread");
|
|
Ok(CargoHandle { child, thread, receiver })
|
|
}
|
|
|
|
fn cancel(mut self) {
|
|
let _ = self.child.0.kill();
|
|
let _ = self.child.0.wait();
|
|
}
|
|
|
|
fn join(mut self) -> io::Result<()> {
|
|
let _ = self.child.0.kill();
|
|
let exit_status = self.child.0.wait()?;
|
|
let (read_at_least_one_message, error) = self.thread.join()?;
|
|
if read_at_least_one_message || exit_status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err(io::Error::new(io::ErrorKind::Other, format!(
|
|
"Cargo watcher failed, the command produced no valid metadata (exit code: {exit_status:?}):\n{error}"
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CargoActor {
|
|
sender: Sender<CargoMessage>,
|
|
stdout: ChildStdout,
|
|
stderr: ChildStderr,
|
|
}
|
|
|
|
impl CargoActor {
|
|
fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
|
|
CargoActor { sender, stdout, stderr }
|
|
}
|
|
|
|
fn run(self) -> io::Result<(bool, String)> {
|
|
// We manually read a line at a time, instead of using serde's
|
|
// stream deserializers, because the deserializer cannot recover
|
|
// from an error, resulting in it getting stuck, because we try to
|
|
// be resilient against failures.
|
|
//
|
|
// Because cargo only outputs one JSON object per line, we can
|
|
// simply skip a line if it doesn't parse, which just ignores any
|
|
// erroneous output.
|
|
|
|
let mut stdout_errors = String::new();
|
|
let mut stderr_errors = String::new();
|
|
let mut read_at_least_one_stdout_message = false;
|
|
let mut read_at_least_one_stderr_message = false;
|
|
let process_line = |line: &str, error: &mut String| {
|
|
// Try to deserialize a message from Cargo or Rustc.
|
|
let mut deserializer = serde_json::Deserializer::from_str(line);
|
|
deserializer.disable_recursion_limit();
|
|
if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
|
|
match message {
|
|
// Skip certain kinds of messages to only spend time on what's useful
|
|
JsonMessage::Cargo(message) => match message {
|
|
cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
|
|
self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
|
|
}
|
|
cargo_metadata::Message::CompilerMessage(msg) => {
|
|
self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
|
|
}
|
|
_ => (),
|
|
},
|
|
JsonMessage::Rustc(message) => {
|
|
self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
error.push_str(line);
|
|
error.push('\n');
|
|
false
|
|
};
|
|
let output = streaming_output(
|
|
self.stdout,
|
|
self.stderr,
|
|
&mut |line| {
|
|
if process_line(line, &mut stdout_errors) {
|
|
read_at_least_one_stdout_message = true;
|
|
}
|
|
},
|
|
&mut |line| {
|
|
if process_line(line, &mut stderr_errors) {
|
|
read_at_least_one_stderr_message = true;
|
|
}
|
|
},
|
|
);
|
|
|
|
let read_at_least_one_message =
|
|
read_at_least_one_stdout_message || read_at_least_one_stderr_message;
|
|
let mut error = stdout_errors;
|
|
error.push_str(&stderr_errors);
|
|
match output {
|
|
Ok(_) => Ok((read_at_least_one_message, error)),
|
|
Err(e) => Err(io::Error::new(e.kind(), format!("{e:?}: {error}"))),
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CargoMessage {
|
|
CompilerArtifact(cargo_metadata::Artifact),
|
|
Diagnostic(Diagnostic),
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(untagged)]
|
|
enum JsonMessage {
|
|
Cargo(cargo_metadata::Message),
|
|
Rustc(Diagnostic),
|
|
}
|