diff options
author | Aleksey Kladov <[email protected]> | 2021-04-20 14:06:20 +0100 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2021-04-20 16:02:54 +0100 |
commit | 1772eb0f1a5c714c91f8ae45cc67cbae6b7ff348 (patch) | |
tree | c7498a99c42a9fd30b437cc12e66c5093f8b4d6b /crates | |
parent | 1834938d6f37c0d308e407e50514180d0f08d96f (diff) |
fix: no longer get stuck on windows
reading both stdout & stderr is a common gotcha, you need to drain them
concurrently to avoid deadlocks. Not sure why I didn't do the right
thing from the start. Seems like I assumed the stderr is short? That's
not the case when cargo spams `compiling xyz` messages
Diffstat (limited to 'crates')
-rw-r--r-- | crates/project_model/src/build_data.rs | 134 | ||||
-rw-r--r-- | crates/stdx/Cargo.toml | 5 | ||||
-rw-r--r-- | crates/stdx/src/lib.rs | 15 | ||||
-rw-r--r-- | crates/stdx/src/process.rs | 238 |
4 files changed, 327 insertions, 65 deletions
diff --git a/crates/project_model/src/build_data.rs b/crates/project_model/src/build_data.rs index ab5cc8c49..faca336de 100644 --- a/crates/project_model/src/build_data.rs +++ b/crates/project_model/src/build_data.rs | |||
@@ -1,7 +1,6 @@ | |||
1 | //! Handles build script specific information | 1 | //! Handles build script specific information |
2 | 2 | ||
3 | use std::{ | 3 | use std::{ |
4 | io::BufReader, | ||
5 | path::PathBuf, | 4 | path::PathBuf, |
6 | process::{Command, Stdio}, | 5 | process::{Command, Stdio}, |
7 | sync::Arc, | 6 | sync::Arc, |
@@ -13,7 +12,8 @@ use cargo_metadata::{BuildScript, Message}; | |||
13 | use itertools::Itertools; | 12 | use itertools::Itertools; |
14 | use paths::{AbsPath, AbsPathBuf}; | 13 | use paths::{AbsPath, AbsPathBuf}; |
15 | use rustc_hash::FxHashMap; | 14 | use rustc_hash::FxHashMap; |
16 | use stdx::{format_to, JodChild}; | 15 | use serde::Deserialize; |
16 | use stdx::format_to; | ||
17 | 17 | ||
18 | use crate::{cfg_flag::CfgFlag, CargoConfig}; | 18 | use crate::{cfg_flag::CfgFlag, CargoConfig}; |
19 | 19 | ||
@@ -171,67 +171,86 @@ impl WorkspaceBuildData { | |||
171 | 171 | ||
172 | cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); | 172 | cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); |
173 | 173 | ||
174 | let mut child = cmd.spawn().map(JodChild)?; | ||
175 | let child_stdout = child.stdout.take().unwrap(); | ||
176 | let stdout = BufReader::new(child_stdout); | ||
177 | |||
178 | let mut res = WorkspaceBuildData::default(); | 174 | let mut res = WorkspaceBuildData::default(); |
179 | for message in cargo_metadata::Message::parse_stream(stdout).flatten() { | ||
180 | match message { | ||
181 | Message::BuildScriptExecuted(BuildScript { | ||
182 | package_id, | ||
183 | out_dir, | ||
184 | cfgs, | ||
185 | env, | ||
186 | .. | ||
187 | }) => { | ||
188 | let cfgs = { | ||
189 | let mut acc = Vec::new(); | ||
190 | for cfg in cfgs { | ||
191 | match cfg.parse::<CfgFlag>() { | ||
192 | Ok(it) => acc.push(it), | ||
193 | Err(err) => { | ||
194 | anyhow::bail!("invalid cfg from cargo-metadata: {}", err) | ||
195 | } | ||
196 | }; | ||
197 | } | ||
198 | acc | ||
199 | }; | ||
200 | let package_build_data = | ||
201 | res.per_package.entry(package_id.repr.clone()).or_default(); | ||
202 | // cargo_metadata crate returns default (empty) path for | ||
203 | // older cargos, which is not absolute, so work around that. | ||
204 | if !out_dir.as_str().is_empty() { | ||
205 | let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir.into_os_string())); | ||
206 | package_build_data.out_dir = Some(out_dir); | ||
207 | package_build_data.cfgs = cfgs; | ||
208 | } | ||
209 | 175 | ||
210 | package_build_data.envs = env; | 176 | let mut callback_err = None; |
177 | let output = stdx::process::streaming_output( | ||
178 | cmd, | ||
179 | &mut |line| { | ||
180 | if callback_err.is_some() { | ||
181 | return; | ||
211 | } | 182 | } |
212 | Message::CompilerArtifact(message) => { | 183 | |
213 | progress(format!("metadata {}", message.target.name)); | 184 | // Copy-pasted from existing cargo_metadata. It seems like we |
214 | 185 | // should be using sered_stacker here? | |
215 | if message.target.kind.contains(&"proc-macro".to_string()) { | 186 | let mut deserializer = serde_json::Deserializer::from_str(&line); |
216 | let package_id = message.package_id; | 187 | deserializer.disable_recursion_limit(); |
217 | // Skip rmeta file | 188 | let message = Message::deserialize(&mut deserializer) |
218 | if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name)) | 189 | .unwrap_or(Message::TextLine(line.to_string())); |
219 | { | 190 | |
220 | let filename = AbsPathBuf::assert(PathBuf::from(&filename)); | 191 | match message { |
221 | let package_build_data = | 192 | Message::BuildScriptExecuted(BuildScript { |
222 | res.per_package.entry(package_id.repr.clone()).or_default(); | 193 | package_id, |
223 | package_build_data.proc_macro_dylib_path = Some(filename); | 194 | out_dir, |
195 | cfgs, | ||
196 | env, | ||
197 | .. | ||
198 | }) => { | ||
199 | let cfgs = { | ||
200 | let mut acc = Vec::new(); | ||
201 | for cfg in cfgs { | ||
202 | match cfg.parse::<CfgFlag>() { | ||
203 | Ok(it) => acc.push(it), | ||
204 | Err(err) => { | ||
205 | callback_err = Some(anyhow::format_err!( | ||
206 | "invalid cfg from cargo-metadata: {}", | ||
207 | err | ||
208 | )); | ||
209 | return; | ||
210 | } | ||
211 | }; | ||
212 | } | ||
213 | acc | ||
214 | }; | ||
215 | let package_build_data = | ||
216 | res.per_package.entry(package_id.repr.clone()).or_default(); | ||
217 | // cargo_metadata crate returns default (empty) path for | ||
218 | // older cargos, which is not absolute, so work around that. | ||
219 | if !out_dir.as_str().is_empty() { | ||
220 | let out_dir = | ||
221 | AbsPathBuf::assert(PathBuf::from(out_dir.into_os_string())); | ||
222 | package_build_data.out_dir = Some(out_dir); | ||
223 | package_build_data.cfgs = cfgs; | ||
224 | } | 224 | } |
225 | |||
226 | package_build_data.envs = env; | ||
225 | } | 227 | } |
228 | Message::CompilerArtifact(message) => { | ||
229 | progress(format!("metadata {}", message.target.name)); | ||
230 | |||
231 | if message.target.kind.contains(&"proc-macro".to_string()) { | ||
232 | let package_id = message.package_id; | ||
233 | // Skip rmeta file | ||
234 | if let Some(filename) = | ||
235 | message.filenames.iter().find(|name| is_dylib(name)) | ||
236 | { | ||
237 | let filename = AbsPathBuf::assert(PathBuf::from(&filename)); | ||
238 | let package_build_data = | ||
239 | res.per_package.entry(package_id.repr.clone()).or_default(); | ||
240 | package_build_data.proc_macro_dylib_path = Some(filename); | ||
241 | } | ||
242 | } | ||
243 | } | ||
244 | Message::CompilerMessage(message) => { | ||
245 | progress(message.target.name.clone()); | ||
246 | } | ||
247 | Message::BuildFinished(_) => {} | ||
248 | Message::TextLine(_) => {} | ||
249 | _ => {} | ||
226 | } | 250 | } |
227 | Message::CompilerMessage(message) => { | 251 | }, |
228 | progress(message.target.name.clone()); | 252 | &mut |_| (), |
229 | } | 253 | )?; |
230 | Message::BuildFinished(_) => {} | ||
231 | Message::TextLine(_) => {} | ||
232 | _ => {} | ||
233 | } | ||
234 | } | ||
235 | 254 | ||
236 | for package in packages { | 255 | for package in packages { |
237 | let package_build_data = res.per_package.entry(package.id.repr.clone()).or_default(); | 256 | let package_build_data = res.per_package.entry(package.id.repr.clone()).or_default(); |
@@ -244,7 +263,6 @@ impl WorkspaceBuildData { | |||
244 | } | 263 | } |
245 | } | 264 | } |
246 | 265 | ||
247 | let output = child.into_inner().wait_with_output()?; | ||
248 | if !output.status.success() { | 266 | if !output.status.success() { |
249 | let mut stderr = String::from_utf8(output.stderr).unwrap_or_default(); | 267 | let mut stderr = String::from_utf8(output.stderr).unwrap_or_default(); |
250 | if stderr.is_empty() { | 268 | if stderr.is_empty() { |
diff --git a/crates/stdx/Cargo.toml b/crates/stdx/Cargo.toml index d28b5e658..f78c5da7c 100644 --- a/crates/stdx/Cargo.toml +++ b/crates/stdx/Cargo.toml | |||
@@ -10,10 +10,15 @@ edition = "2018" | |||
10 | doctest = false | 10 | doctest = false |
11 | 11 | ||
12 | [dependencies] | 12 | [dependencies] |
13 | libc = "0.2.93" | ||
13 | backtrace = { version = "0.3.44", optional = true } | 14 | backtrace = { version = "0.3.44", optional = true } |
14 | always-assert = { version = "0.1.2", features = ["log"] } | 15 | always-assert = { version = "0.1.2", features = ["log"] } |
15 | # Think twice before adding anything here | 16 | # Think twice before adding anything here |
16 | 17 | ||
18 | [target.'cfg(windows)'.dependencies] | ||
19 | miow = "0.3.6" | ||
20 | winapi = "0.3.9" | ||
21 | |||
17 | [features] | 22 | [features] |
18 | # Uncomment to enable for the whole crate graph | 23 | # Uncomment to enable for the whole crate graph |
19 | # default = [ "backtrace" ] | 24 | # default = [ "backtrace" ] |
diff --git a/crates/stdx/src/lib.rs b/crates/stdx/src/lib.rs index b0a18d58d..e3eb10915 100644 --- a/crates/stdx/src/lib.rs +++ b/crates/stdx/src/lib.rs | |||
@@ -1,7 +1,8 @@ | |||
1 | //! Missing batteries for standard libraries. | 1 | //! Missing batteries for standard libraries. |
2 | use std::{cmp::Ordering, ops, process, time::Instant}; | 2 | use std::{cmp::Ordering, ops, time::Instant}; |
3 | 3 | ||
4 | mod macros; | 4 | mod macros; |
5 | pub mod process; | ||
5 | pub mod panic_context; | 6 | pub mod panic_context; |
6 | 7 | ||
7 | pub use always_assert::{always, never}; | 8 | pub use always_assert::{always, never}; |
@@ -179,17 +180,17 @@ where | |||
179 | } | 180 | } |
180 | 181 | ||
181 | #[repr(transparent)] | 182 | #[repr(transparent)] |
182 | pub struct JodChild(pub process::Child); | 183 | pub struct JodChild(pub std::process::Child); |
183 | 184 | ||
184 | impl ops::Deref for JodChild { | 185 | impl ops::Deref for JodChild { |
185 | type Target = process::Child; | 186 | type Target = std::process::Child; |
186 | fn deref(&self) -> &process::Child { | 187 | fn deref(&self) -> &std::process::Child { |
187 | &self.0 | 188 | &self.0 |
188 | } | 189 | } |
189 | } | 190 | } |
190 | 191 | ||
191 | impl ops::DerefMut for JodChild { | 192 | impl ops::DerefMut for JodChild { |
192 | fn deref_mut(&mut self) -> &mut process::Child { | 193 | fn deref_mut(&mut self) -> &mut std::process::Child { |
193 | &mut self.0 | 194 | &mut self.0 |
194 | } | 195 | } |
195 | } | 196 | } |
@@ -202,9 +203,9 @@ impl Drop for JodChild { | |||
202 | } | 203 | } |
203 | 204 | ||
204 | impl JodChild { | 205 | impl JodChild { |
205 | pub fn into_inner(self) -> process::Child { | 206 | pub fn into_inner(self) -> std::process::Child { |
206 | // SAFETY: repr transparent | 207 | // SAFETY: repr transparent |
207 | unsafe { std::mem::transmute::<JodChild, process::Child>(self) } | 208 | unsafe { std::mem::transmute::<JodChild, std::process::Child>(self) } |
208 | } | 209 | } |
209 | } | 210 | } |
210 | 211 | ||
diff --git a/crates/stdx/src/process.rs b/crates/stdx/src/process.rs new file mode 100644 index 000000000..b0fa12f76 --- /dev/null +++ b/crates/stdx/src/process.rs | |||
@@ -0,0 +1,238 @@ | |||
1 | //! Read both stdout and stderr of child without deadlocks. | ||
2 | //! | ||
3 | //! https://github.com/rust-lang/cargo/blob/905af549966f23a9288e9993a85d1249a5436556/crates/cargo-util/src/read2.rs | ||
4 | //! https://github.com/rust-lang/cargo/blob/58a961314437258065e23cb6316dfc121d96fb71/crates/cargo-util/src/process_builder.rs#L231 | ||
5 | |||
6 | use std::{ | ||
7 | io, | ||
8 | process::{Command, Output, Stdio}, | ||
9 | }; | ||
10 | |||
11 | pub fn streaming_output( | ||
12 | mut cmd: Command, | ||
13 | on_stdout_line: &mut dyn FnMut(&str), | ||
14 | on_stderr_line: &mut dyn FnMut(&str), | ||
15 | ) -> io::Result<Output> { | ||
16 | let mut stdout = Vec::new(); | ||
17 | let mut stderr = Vec::new(); | ||
18 | |||
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 | } | ||
33 | }; | ||
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 | } | ||
49 | } | ||
50 | } | ||
51 | })?; | ||
52 | child.wait()? | ||
53 | }; | ||
54 | |||
55 | Ok(Output { status, stdout, stderr }) | ||
56 | } | ||
57 | |||
58 | #[cfg(unix)] | ||
59 | mod imp { | ||
60 | use std::{ | ||
61 | io::{self, prelude::*}, | ||
62 | mem, | ||
63 | os::unix::prelude::*, | ||
64 | process::{ChildStderr, ChildStdout}, | ||
65 | }; | ||
66 | |||
67 | pub(crate) fn read2( | ||
68 | mut out_pipe: ChildStdout, | ||
69 | mut err_pipe: ChildStderr, | ||
70 | data: &mut dyn FnMut(bool, &mut Vec<u8>, bool), | ||
71 | ) -> io::Result<()> { | ||
72 | unsafe { | ||
73 | libc::fcntl(out_pipe.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK); | ||
74 | libc::fcntl(err_pipe.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK); | ||
75 | } | ||
76 | |||
77 | let mut out_done = false; | ||
78 | let mut err_done = false; | ||
79 | let mut out = Vec::new(); | ||
80 | let mut err = Vec::new(); | ||
81 | |||
82 | let mut fds: [libc::pollfd; 2] = unsafe { mem::zeroed() }; | ||
83 | fds[0].fd = out_pipe.as_raw_fd(); | ||
84 | fds[0].events = libc::POLLIN; | ||
85 | fds[1].fd = err_pipe.as_raw_fd(); | ||
86 | fds[1].events = libc::POLLIN; | ||
87 | let mut nfds = 2; | ||
88 | let mut errfd = 1; | ||
89 | |||
90 | while nfds > 0 { | ||
91 | // wait for either pipe to become readable using `select` | ||
92 | let r = unsafe { libc::poll(fds.as_mut_ptr(), nfds, -1) }; | ||
93 | if r == -1 { | ||
94 | let err = io::Error::last_os_error(); | ||
95 | if err.kind() == io::ErrorKind::Interrupted { | ||
96 | continue; | ||
97 | } | ||
98 | return Err(err); | ||
99 | } | ||
100 | |||
101 | // Read as much as we can from each pipe, ignoring EWOULDBLOCK or | ||
102 | // EAGAIN. If we hit EOF, then this will happen because the underlying | ||
103 | // reader will return Ok(0), in which case we'll see `Ok` ourselves. In | ||
104 | // this case we flip the other fd back into blocking mode and read | ||
105 | // whatever's leftover on that file descriptor. | ||
106 | let handle = |res: io::Result<_>| match res { | ||
107 | Ok(_) => Ok(true), | ||
108 | Err(e) => { | ||
109 | if e.kind() == io::ErrorKind::WouldBlock { | ||
110 | Ok(false) | ||
111 | } else { | ||
112 | Err(e) | ||
113 | } | ||
114 | } | ||
115 | }; | ||
116 | if !err_done && fds[errfd].revents != 0 && handle(err_pipe.read_to_end(&mut err))? { | ||
117 | err_done = true; | ||
118 | nfds -= 1; | ||
119 | } | ||
120 | data(false, &mut err, err_done); | ||
121 | if !out_done && fds[0].revents != 0 && handle(out_pipe.read_to_end(&mut out))? { | ||
122 | out_done = true; | ||
123 | fds[0].fd = err_pipe.as_raw_fd(); | ||
124 | errfd = 0; | ||
125 | nfds -= 1; | ||
126 | } | ||
127 | data(true, &mut out, out_done); | ||
128 | } | ||
129 | Ok(()) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | #[cfg(windows)] | ||
134 | mod imp { | ||
135 | use std::{ | ||
136 | io, | ||
137 | os::windows::prelude::*, | ||
138 | process::{ChildStderr, ChildStdout}, | ||
139 | slice, | ||
140 | }; | ||
141 | |||
142 | use miow::{ | ||
143 | iocp::{CompletionPort, CompletionStatus}, | ||
144 | pipe::NamedPipe, | ||
145 | Overlapped, | ||
146 | }; | ||
147 | use winapi::shared::winerror::ERROR_BROKEN_PIPE; | ||
148 | |||
149 | struct Pipe<'a> { | ||
150 | dst: &'a mut Vec<u8>, | ||
151 | overlapped: Overlapped, | ||
152 | pipe: NamedPipe, | ||
153 | done: bool, | ||
154 | } | ||
155 | |||
156 | pub(crate) fn read2( | ||
157 | out_pipe: ChildStdout, | ||
158 | err_pipe: ChildStderr, | ||
159 | data: &mut dyn FnMut(bool, &mut Vec<u8>, bool), | ||
160 | ) -> io::Result<()> { | ||
161 | let mut out = Vec::new(); | ||
162 | let mut err = Vec::new(); | ||
163 | |||
164 | let port = CompletionPort::new(1)?; | ||
165 | port.add_handle(0, &out_pipe)?; | ||
166 | port.add_handle(1, &err_pipe)?; | ||
167 | |||
168 | unsafe { | ||
169 | let mut out_pipe = Pipe::new(out_pipe, &mut out); | ||
170 | let mut err_pipe = Pipe::new(err_pipe, &mut err); | ||
171 | |||
172 | out_pipe.read()?; | ||
173 | err_pipe.read()?; | ||
174 | |||
175 | let mut status = [CompletionStatus::zero(), CompletionStatus::zero()]; | ||
176 | |||
177 | while !out_pipe.done || !err_pipe.done { | ||
178 | for status in port.get_many(&mut status, None)? { | ||
179 | if status.token() == 0 { | ||
180 | out_pipe.complete(status); | ||
181 | data(true, out_pipe.dst, out_pipe.done); | ||
182 | out_pipe.read()?; | ||
183 | } else { | ||
184 | err_pipe.complete(status); | ||
185 | data(false, err_pipe.dst, err_pipe.done); | ||
186 | err_pipe.read()?; | ||
187 | } | ||
188 | } | ||
189 | } | ||
190 | |||
191 | Ok(()) | ||
192 | } | ||
193 | } | ||
194 | |||
195 | impl<'a> Pipe<'a> { | ||
196 | unsafe fn new<P: IntoRawHandle>(p: P, dst: &'a mut Vec<u8>) -> Pipe<'a> { | ||
197 | Pipe { | ||
198 | dst, | ||
199 | pipe: NamedPipe::from_raw_handle(p.into_raw_handle()), | ||
200 | overlapped: Overlapped::zero(), | ||
201 | done: false, | ||
202 | } | ||
203 | } | ||
204 | |||
205 | unsafe fn read(&mut self) -> io::Result<()> { | ||
206 | let dst = slice_to_end(self.dst); | ||
207 | match self.pipe.read_overlapped(dst, self.overlapped.raw()) { | ||
208 | Ok(_) => Ok(()), | ||
209 | Err(e) => { | ||
210 | if e.raw_os_error() == Some(ERROR_BROKEN_PIPE as i32) { | ||
211 | self.done = true; | ||
212 | Ok(()) | ||
213 | } else { | ||
214 | Err(e) | ||
215 | } | ||
216 | } | ||
217 | } | ||
218 | } | ||
219 | |||
220 | unsafe fn complete(&mut self, status: &CompletionStatus) { | ||
221 | let prev = self.dst.len(); | ||
222 | self.dst.set_len(prev + status.bytes_transferred() as usize); | ||
223 | if status.bytes_transferred() == 0 { | ||
224 | self.done = true; | ||
225 | } | ||
226 | } | ||
227 | } | ||
228 | |||
229 | unsafe fn slice_to_end(v: &mut Vec<u8>) -> &mut [u8] { | ||
230 | if v.capacity() == 0 { | ||
231 | v.reserve(16); | ||
232 | } | ||
233 | if v.capacity() == v.len() { | ||
234 | v.reserve(1); | ||
235 | } | ||
236 | slice::from_raw_parts_mut(v.as_mut_ptr().add(v.len()), v.capacity() - v.len()) | ||
237 | } | ||
238 | } | ||