rust_analyzer/
command.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Utilities for running a cargo command like `cargo check` or `cargo test` in a separate thread
//! and parse its stdout/stderr.

use std::{
    ffi::OsString,
    fmt, io,
    marker::PhantomData,
    path::PathBuf,
    process::{ChildStderr, ChildStdout, Command, Stdio},
};

use crossbeam_channel::Sender;
use process_wrap::std::{StdChildWrapper, StdCommandWrap};
use stdx::process::streaming_output;

/// Cargo output is structured as a one JSON per line. This trait abstracts parsing one line of
/// cargo output into a Rust data type.
pub(crate) trait ParseFromLine: Sized + Send + 'static {
    fn from_line(line: &str, error: &mut String) -> Option<Self>;
    fn from_eof() -> Option<Self>;
}

struct CargoActor<T> {
    sender: Sender<T>,
    stdout: ChildStdout,
    stderr: ChildStderr,
}

impl<T: ParseFromLine> CargoActor<T> {
    fn new(sender: Sender<T>, stdout: ChildStdout, stderr: ChildStderr) -> Self {
        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.
            if let Some(t) = T::from_line(line, error) {
                self.sender.send(t).unwrap();
                true
            } else {
                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;
                }
            },
            &mut || {
                if let Some(t) = T::from_eof() {
                    self.sender.send(t).unwrap();
                }
            },
        );

        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}"))),
        }
    }
}

struct JodGroupChild(Box<dyn StdChildWrapper>);

impl Drop for JodGroupChild {
    fn drop(&mut self) {
        _ = self.0.kill();
        _ = self.0.wait();
    }
}

/// A handle to a cargo process used for fly-checking.
pub(crate) struct CommandHandle<T> {
    /// 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)>>,
    program: OsString,
    arguments: Vec<OsString>,
    current_dir: Option<PathBuf>,
    _phantom: PhantomData<T>,
}

impl<T> fmt::Debug for CommandHandle<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("CommandHandle")
            .field("program", &self.program)
            .field("arguments", &self.arguments)
            .field("current_dir", &self.current_dir)
            .finish()
    }
}

impl<T: ParseFromLine> CommandHandle<T> {
    pub(crate) fn spawn(mut command: Command, sender: Sender<T>) -> std::io::Result<Self> {
        command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());

        let program = command.get_program().into();
        let arguments = command.get_args().map(|arg| arg.into()).collect::<Vec<OsString>>();
        let current_dir = command.get_current_dir().map(|arg| arg.to_path_buf());

        let mut child = StdCommandWrap::from(command);
        #[cfg(unix)]
        child.wrap(process_wrap::std::ProcessSession);
        #[cfg(windows)]
        child.wrap(process_wrap::std::JobObject);
        let mut child = child.spawn().map(JodGroupChild)?;

        let stdout = child.0.stdout().take().unwrap();
        let stderr = child.0.stderr().take().unwrap();

        let actor = CargoActor::<T>::new(sender, stdout, stderr);
        let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
            .name("CommandHandle".to_owned())
            .spawn(move || actor.run())
            .expect("failed to spawn thread");
        Ok(CommandHandle { program, arguments, current_dir, child, thread, _phantom: PhantomData })
    }

    pub(crate) fn cancel(mut self) {
        let _ = self.child.0.kill();
        let _ = self.child.0.wait();
    }

    pub(crate) fn join(mut self) -> io::Result<()> {
        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}"
        )))
        }
    }
}