Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 6438ef9

Browse files
committed
internal: Bring back JodChild into flychecking for cancellation
1 parent 7db7387 commit 6438ef9

File tree

4 files changed

+114
-61
lines changed

4 files changed

+114
-61
lines changed

crates/flycheck/src/lib.rs

Lines changed: 53 additions & 24 deletions
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,14 +157,24 @@ impl FlycheckActor {
153157
while let Some(event) = self.next_event(&inbox) {
154158
match event {
155159
Event::Restart(Restart) => {
160+
// Drop and cancel the previously spawned process
161+
self.cargo_handle.take();
156162
while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}
157163

158164
self.cancel_check_process();
159165

160166
let command = self.check_command();
161-
tracing::info!("restart flycheck {:?}", command);
162-
self.cargo_handle = Some(CargoHandle::spawn(command));
163-
self.progress(Progress::DidStart);
167+
let command_f = format!("restart flycheck {command:?}");
168+
match CargoHandle::spawn(command) {
169+
Ok(cargo_handle) => {
170+
tracing::info!("{}", command_f);
171+
self.cargo_handle = Some(cargo_handle);
172+
self.progress(Progress::DidStart);
173+
}
174+
Err(e) => {
175+
tracing::error!("{command_f} failed: {e:?}",);
176+
}
177+
}
164178
}
165179
Event::CheckEvent(None) => {
166180
// Watcher finished, replace it with a never channel to
@@ -249,37 +263,58 @@ impl FlycheckActor {
249263
}
250264
}
251265

266+
/// A handle to a cargo process used for fly-checking.
252267
struct CargoHandle {
253-
thread: jod_thread::JoinHandle<io::Result<()>>,
268+
/// The handle to the actual cargo process. As we cannot cancel directly from with
269+
/// a read syscall dropping and therefor terminating the process is our best option.
270+
child: JodChild,
271+
thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
254272
receiver: Receiver<CargoMessage>,
255273
}
256274

257275
impl CargoHandle {
258-
fn spawn(command: Command) -> CargoHandle {
276+
fn spawn(mut command: Command) -> std::io::Result<CargoHandle> {
277+
command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
278+
let mut child = JodChild::spawn(command)?;
279+
280+
let stdout = child.stdout.take().unwrap();
281+
let stderr = child.stderr.take().unwrap();
282+
259283
let (sender, receiver) = unbounded();
260-
let actor = CargoActor::new(sender);
284+
let actor = CargoActor::new(sender, stdout, stderr);
261285
let thread = jod_thread::Builder::new()
262286
.name("CargoHandle".to_owned())
263-
.spawn(move || actor.run(command))
287+
.spawn(move || actor.run())
264288
.expect("failed to spawn thread");
265-
CargoHandle { thread, receiver }
289+
Ok(CargoHandle { child, thread, receiver })
266290
}
267291

268292
fn join(self) -> io::Result<()> {
269-
self.thread.join()
293+
let exit_status = self.child.wait()?;
294+
let (read_at_least_one_message, error) = self.thread.join()?;
295+
if read_at_least_one_message || exit_status.success() {
296+
Ok(())
297+
} else {
298+
Err(io::Error::new(io::ErrorKind::Other, format!(
299+
"Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
300+
exit_status, error
301+
)))
302+
}
270303
}
271304
}
272305

273306
struct CargoActor {
274307
sender: Sender<CargoMessage>,
308+
stdout: ChildStdout,
309+
stderr: ChildStderr,
275310
}
276311

277312
impl CargoActor {
278-
fn new(sender: Sender<CargoMessage>) -> CargoActor {
279-
CargoActor { sender }
313+
fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
314+
CargoActor { sender, stdout, stderr }
280315
}
281316

282-
fn run(self, command: Command) -> io::Result<()> {
317+
fn run(self) -> io::Result<(bool, String)> {
283318
// We manually read a line at a time, instead of using serde's
284319
// stream deserializers, because the deserializer cannot recover
285320
// from an error, resulting in it getting stuck, because we try to
@@ -292,7 +327,8 @@ impl CargoActor {
292327
let mut error = String::new();
293328
let mut read_at_least_one_message = false;
294329
let output = streaming_output(
295-
command,
330+
self.stdout,
331+
self.stderr,
296332
&mut |line| {
297333
read_at_least_one_message = true;
298334

@@ -325,14 +361,7 @@ impl CargoActor {
325361
},
326362
);
327363
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-
}
364+
Ok(_) => Ok((read_at_least_one_message, error)),
336365
Err(e) => Err(io::Error::new(e.kind(), format!("{:?}: {}", e, error))),
337366
}
338367
}

crates/project-model/src/build_scripts.rs

Lines changed: 1 addition & 1 deletion
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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Missing batteries for standard libraries.
22
use std::iter;
3+
use std::process::Command;
34
use std::{cmp::Ordering, ops, time::Instant};
45

56
mod macros;
@@ -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,16 @@ impl Drop for JodChild {
157159
}
158160

159161
impl JodChild {
162+
pub fn spawn(mut command: Command) -> std::io::Result<Self> {
163+
command.spawn().map(Self)
164+
}
165+
166+
pub fn wait(self) -> std::io::Result<std::process::ExitStatus> {
167+
let mut inner = self.into_inner();
168+
let _ = inner.kill();
169+
inner.wait()
170+
}
171+
160172
pub fn into_inner(self) -> std::process::Child {
161173
if cfg!(target_arch = "wasm32") {
162174
panic!("no processes on wasm");

crates/stdx/src/process.rs

Lines changed: 48 additions & 36 deletions
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)