Skip to content

Commit 5bb123d

Browse files
committed
Auto merge of rust-lang#12520 - Veykril:flycheck-cancel, r=Veykril
internal: Bring back JodChild into flychecking for cancellation cc https://github.com/rust-lang/rust-analyzer/pull/10517/files#r895241975
2 parents 401a71d + 5979931 commit 5bb123d

File tree

4 files changed

+131
-68
lines changed

4 files changed

+131
-68
lines changed

crates/flycheck/src/lib.rs

+75-30
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
//! another compatible command (f.x. clippy) in a background thread and provide
33
//! LSP diagnostics based on the output of the command.
44
5-
use std::{fmt, io, process::Command, time::Duration};
5+
use std::{
6+
fmt, io,
7+
process::{ChildStderr, ChildStdout, Command, Stdio},
8+
time::Duration,
9+
};
610

711
use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
812
use paths::AbsPathBuf;
913
use serde::Deserialize;
10-
use stdx::process::streaming_output;
14+
use stdx::{process::streaming_output, JodChild};
1115

1216
pub use cargo_metadata::diagnostic::{
1317
Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
@@ -117,7 +121,7 @@ struct FlycheckActor {
117121
sender: Box<dyn Fn(Message) + Send>,
118122
config: FlycheckConfig,
119123
workspace_root: AbsPathBuf,
120-
/// WatchThread exists to wrap around the communication needed to be able to
124+
/// CargoHandle exists to wrap around the communication needed to be able to
121125
/// run `cargo check` without blocking. Currently the Rust standard library
122126
/// doesn't provide a way to read sub-process output without blocking, so we
123127
/// have to wrap sub-processes output handling in a thread and pass messages
@@ -153,18 +157,36 @@ impl FlycheckActor {
153157
while let Some(event) = self.next_event(&inbox) {
154158
match event {
155159
Event::Restart(Restart) => {
160+
if let Some(cargo_handle) = self.cargo_handle.take() {
161+
// Cancel the previously spawned process
162+
cargo_handle.cancel();
163+
}
156164
while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}
157-
158-
self.cancel_check_process();
165+
self.progress(Progress::DidCancel);
159166

160167
let command = self.check_command();
161-
tracing::info!("restart flycheck {:?}", command);
162-
self.cargo_handle = Some(CargoHandle::spawn(command));
163-
self.progress(Progress::DidStart);
168+
tracing::debug!(?command, "will restart flycheck");
169+
match CargoHandle::spawn(command) {
170+
Ok(cargo_handle) => {
171+
tracing::debug!(
172+
command = ?self.check_command(),
173+
"did restart flycheck"
174+
);
175+
self.cargo_handle = Some(cargo_handle);
176+
self.progress(Progress::DidStart);
177+
}
178+
Err(error) => {
179+
tracing::error!(
180+
command = ?self.check_command(),
181+
%error, "failed to restart flycheck"
182+
);
183+
}
184+
}
164185
}
165186
Event::CheckEvent(None) => {
166-
// Watcher finished, replace it with a never channel to
167-
// avoid busy-waiting.
187+
tracing::debug!("flycheck finished");
188+
189+
// Watcher finished
168190
let cargo_handle = self.cargo_handle.take().unwrap();
169191
let res = cargo_handle.join();
170192
if res.is_err() {
@@ -192,8 +214,10 @@ impl FlycheckActor {
192214
// If we rerun the thread, we need to discard the previous check results first
193215
self.cancel_check_process();
194216
}
217+
195218
fn cancel_check_process(&mut self) {
196-
if self.cargo_handle.take().is_some() {
219+
if let Some(cargo_handle) = self.cargo_handle.take() {
220+
cargo_handle.cancel();
197221
self.progress(Progress::DidCancel);
198222
}
199223
}
@@ -249,37 +273,64 @@ impl FlycheckActor {
249273
}
250274
}
251275

276+
/// A handle to a cargo process used for fly-checking.
252277
struct CargoHandle {
253-
thread: jod_thread::JoinHandle<io::Result<()>>,
278+
/// The handle to the actual cargo process. As we cannot cancel directly from with
279+
/// a read syscall dropping and therefor terminating the process is our best option.
280+
child: JodChild,
281+
thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
254282
receiver: Receiver<CargoMessage>,
255283
}
256284

257285
impl CargoHandle {
258-
fn spawn(command: Command) -> CargoHandle {
286+
fn spawn(mut command: Command) -> std::io::Result<CargoHandle> {
287+
command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
288+
let mut child = JodChild::spawn(command)?;
289+
290+
let stdout = child.stdout.take().unwrap();
291+
let stderr = child.stderr.take().unwrap();
292+
259293
let (sender, receiver) = unbounded();
260-
let actor = CargoActor::new(sender);
294+
let actor = CargoActor::new(sender, stdout, stderr);
261295
let thread = jod_thread::Builder::new()
262296
.name("CargoHandle".to_owned())
263-
.spawn(move || actor.run(command))
297+
.spawn(move || actor.run())
264298
.expect("failed to spawn thread");
265-
CargoHandle { thread, receiver }
299+
Ok(CargoHandle { child, thread, receiver })
266300
}
267301

268-
fn join(self) -> io::Result<()> {
269-
self.thread.join()
302+
fn cancel(mut self) {
303+
let _ = self.child.kill();
304+
let _ = self.child.wait();
305+
}
306+
307+
fn join(mut self) -> io::Result<()> {
308+
let _ = self.child.kill();
309+
let exit_status = self.child.wait()?;
310+
let (read_at_least_one_message, error) = self.thread.join()?;
311+
if read_at_least_one_message || exit_status.success() {
312+
Ok(())
313+
} else {
314+
Err(io::Error::new(io::ErrorKind::Other, format!(
315+
"Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
316+
exit_status, error
317+
)))
318+
}
270319
}
271320
}
272321

273322
struct CargoActor {
274323
sender: Sender<CargoMessage>,
324+
stdout: ChildStdout,
325+
stderr: ChildStderr,
275326
}
276327

277328
impl CargoActor {
278-
fn new(sender: Sender<CargoMessage>) -> CargoActor {
279-
CargoActor { sender }
329+
fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
330+
CargoActor { sender, stdout, stderr }
280331
}
281332

282-
fn run(self, command: Command) -> io::Result<()> {
333+
fn run(self) -> io::Result<(bool, String)> {
283334
// We manually read a line at a time, instead of using serde's
284335
// stream deserializers, because the deserializer cannot recover
285336
// from an error, resulting in it getting stuck, because we try to
@@ -292,7 +343,8 @@ impl CargoActor {
292343
let mut error = String::new();
293344
let mut read_at_least_one_message = false;
294345
let output = streaming_output(
295-
command,
346+
self.stdout,
347+
self.stderr,
296348
&mut |line| {
297349
read_at_least_one_message = true;
298350

@@ -325,14 +377,7 @@ impl CargoActor {
325377
},
326378
);
327379
match output {
328-
Ok(_) if read_at_least_one_message => Ok(()),
329-
Ok(output) if output.status.success() => Ok(()),
330-
Ok(output) => {
331-
Err(io::Error::new(io::ErrorKind::Other, format!(
332-
"Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
333-
output.status, error
334-
)))
335-
}
380+
Ok(_) => Ok((read_at_least_one_message, error)),
336381
Err(e) => Err(io::Error::new(e.kind(), format!("{:?}: {}", e, error))),
337382
}
338383
}

crates/project-model/src/build_scripts.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl WorkspaceBuildScripts {
110110
};
111111

112112
tracing::info!("Running build scripts: {:?}", cmd);
113-
let output = stdx::process::streaming_output(
113+
let output = stdx::process::spawn_with_streaming_output(
114114
cmd,
115115
&mut |line| {
116116
// Copy-pasted from existing cargo_metadata. It seems like we

crates/stdx/src/lib.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Missing batteries for standard libraries.
2-
use std::iter;
2+
use std::process::Command;
33
use std::{cmp::Ordering, ops, time::Instant};
4+
use std::{io as sio, iter};
45

56
mod macros;
67
pub mod process;
@@ -132,6 +133,7 @@ pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
132133
D(Some(f))
133134
}
134135

136+
/// A [`std::process::Child`] wrapper that will kill the child on drop.
135137
#[cfg_attr(not(target_arch = "wasm32"), repr(transparent))]
136138
#[derive(Debug)]
137139
pub struct JodChild(pub std::process::Child);
@@ -157,6 +159,10 @@ impl Drop for JodChild {
157159
}
158160

159161
impl JodChild {
162+
pub fn spawn(mut command: Command) -> sio::Result<Self> {
163+
command.spawn().map(Self)
164+
}
165+
160166
pub fn into_inner(self) -> std::process::Child {
161167
if cfg!(target_arch = "wasm32") {
162168
panic!("no processes on wasm");

crates/stdx/src/process.rs

+48-36
Original file line numberDiff line numberDiff line change
@@ -5,54 +5,66 @@
55
66
use std::{
77
io,
8-
process::{Command, Output, Stdio},
8+
process::{ChildStderr, ChildStdout, Command, Output, Stdio},
99
};
1010

11+
use crate::JodChild;
12+
1113
pub fn streaming_output(
12-
mut cmd: Command,
14+
out: ChildStdout,
15+
err: ChildStderr,
1316
on_stdout_line: &mut dyn FnMut(&str),
1417
on_stderr_line: &mut dyn FnMut(&str),
15-
) -> io::Result<Output> {
18+
) -> io::Result<(Vec<u8>, Vec<u8>)> {
1619
let mut stdout = Vec::new();
1720
let mut stderr = Vec::new();
1821

19-
let cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
20-
21-
let status = {
22-
let mut child = cmd.spawn()?;
23-
let out = child.stdout.take().unwrap();
24-
let err = child.stderr.take().unwrap();
25-
imp::read2(out, err, &mut |is_out, data, eof| {
26-
let idx = if eof {
27-
data.len()
28-
} else {
29-
match data.iter().rposition(|b| *b == b'\n') {
30-
Some(i) => i + 1,
31-
None => return,
32-
}
22+
imp::read2(out, err, &mut |is_out, data, eof| {
23+
let idx = if eof {
24+
data.len()
25+
} else {
26+
match data.iter().rposition(|b| *b == b'\n') {
27+
Some(i) => i + 1,
28+
None => return,
29+
}
30+
};
31+
{
32+
// scope for new_lines
33+
let new_lines = {
34+
let dst = if is_out { &mut stdout } else { &mut stderr };
35+
let start = dst.len();
36+
let data = data.drain(..idx);
37+
dst.extend(data);
38+
&dst[start..]
3339
};
34-
{
35-
// scope for new_lines
36-
let new_lines = {
37-
let dst = if is_out { &mut stdout } else { &mut stderr };
38-
let start = dst.len();
39-
let data = data.drain(..idx);
40-
dst.extend(data);
41-
&dst[start..]
42-
};
43-
for line in String::from_utf8_lossy(new_lines).lines() {
44-
if is_out {
45-
on_stdout_line(line);
46-
} else {
47-
on_stderr_line(line);
48-
}
40+
for line in String::from_utf8_lossy(new_lines).lines() {
41+
if is_out {
42+
on_stdout_line(line);
43+
} else {
44+
on_stderr_line(line);
4945
}
5046
}
51-
})?;
52-
let _ = child.kill();
53-
child.wait()?
54-
};
47+
}
48+
})?;
49+
50+
Ok((stdout, stderr))
51+
}
52+
53+
pub fn spawn_with_streaming_output(
54+
mut cmd: Command,
55+
on_stdout_line: &mut dyn FnMut(&str),
56+
on_stderr_line: &mut dyn FnMut(&str),
57+
) -> io::Result<Output> {
58+
let cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
5559

60+
let mut child = JodChild(cmd.spawn()?);
61+
let (stdout, stderr) = streaming_output(
62+
child.stdout.take().unwrap(),
63+
child.stderr.take().unwrap(),
64+
on_stdout_line,
65+
on_stderr_line,
66+
)?;
67+
let status = child.wait()?;
5668
Ok(Output { status, stdout, stderr })
5769
}
5870

0 commit comments

Comments
 (0)