1use std::io::{self, Read, Write};
11use std::sync::{Arc, Condvar, Mutex};
12use std::time::Duration;
13
14use crate::slot::{SharedInput, SharedOutput};
15use crate::spill::SpillBuffer;
16
17pub struct ScriptPipe {
19 inner: Arc<PipeInner>,
20 reader: SharedInput,
21}
22
23impl ScriptPipe {
24 pub fn new() -> Self {
26 let inner = Arc::new(PipeInner::new());
27 let reader: SharedInput = Arc::new(Mutex::new(PipeReader::new(inner.clone())));
28 Self { inner, reader }
29 }
30 pub fn with_thresholds(spill_threshold: usize, max_backlog: u64) -> Self {
33 let inner = Arc::new(PipeInner::with_thresholds(spill_threshold, max_backlog));
34 let reader: SharedInput = Arc::new(Mutex::new(PipeReader::new(inner.clone())));
35 Self { inner, reader }
36 }
37
38 pub fn reader(&self) -> SharedInput {
40 self.reader.clone()
41 }
42
43 pub fn endpoint(&self) -> ScriptPipeEndpoint {
46 ScriptPipeEndpoint::new(self.inner.clone())
47 }
48
49 pub fn pipe_inner(&self) -> Arc<PipeInner> {
51 self.inner.clone()
52 }
53
54 #[allow(clippy::disallowed_types)]
56 pub fn temp_path(&self) -> Option<std::path::PathBuf> {
57 self.inner.temp_path()
58 }
59}
60
61impl Default for ScriptPipe {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[derive(Clone)]
70pub struct ScriptPipeEndpoint {
71 inner: Arc<PipeInner>,
72}
73
74impl ScriptPipeEndpoint {
75 fn new(inner: Arc<PipeInner>) -> Self {
76 Self { inner }
77 }
78
79 pub fn for_backend(inner: &Arc<PipeInner>) -> Self {
82 Self::new(Arc::clone(inner))
83 }
84
85 pub fn stream_handle(&self) -> SharedOutput {
87 Arc::new(Mutex::new(PipeWriter::new(self.inner.clone())))
88 }
89}
90
91pub struct PipeInner {
94 state: Mutex<PipeState>,
95 ready: Condvar,
96}
97
98struct PipeState {
99 buffer: SpillBuffer,
100 writers: usize,
101 keepers: usize,
102 closed: bool,
103}
104
105impl PipeState {
106 fn new(spill_threshold: usize, max_backlog: u64) -> Self {
107 Self {
108 buffer: SpillBuffer::with_thresholds(spill_threshold, max_backlog),
109 writers: 0,
110 keepers: 0,
111 closed: false,
112 }
113 }
114}
115
116impl PipeInner {
117 fn new() -> Self {
118 Self {
119 state: Mutex::new(PipeState::new(
120 crate::spill::DEFAULT_SPILL_THRESHOLD,
121 crate::spill::DEFAULT_MAX_BACKLOG,
122 )),
123 ready: Condvar::new(),
124 }
125 }
126
127 pub fn reader_handle(self: &Arc<Self>) -> crate::slot::SharedInput {
131 Arc::new(Mutex::new(PipeReader::new(Arc::clone(self))))
132 }
133
134 pub fn writer_handle(self: &Arc<Self>) -> crate::slot::SharedOutput {
137 Arc::new(Mutex::new(PipeWriter::new(Arc::clone(self))))
138 }
139
140 fn with_thresholds(spill_threshold: usize, max_backlog: u64) -> Self {
141 Self {
142 state: Mutex::new(PipeState::new(spill_threshold, max_backlog)),
143 ready: Condvar::new(),
144 }
145 }
146
147 #[allow(clippy::disallowed_types)]
148 fn temp_path(&self) -> Option<std::path::PathBuf> {
149 self.lock_state().buffer.temp_path()
150 }
151
152 fn attach_writer(&self) {
153 let mut state = self.lock_state();
154 state.writers += 1;
155 state.closed = false;
156 }
157
158 pub fn writer_count(&self) -> usize {
161 self.lock_state().writers
162 }
163
164 pub fn buffered_bytes(&self) -> u64 {
166 self.lock_state().buffer.buffered_bytes()
167 }
168
169 pub fn peek_bytes(&self) -> io::Result<Vec<u8>> {
172 self.lock_state().buffer.peek_bytes()
173 }
174
175 fn detach_writer(&self) {
176 let mut state = self.lock_state();
177 state.writers = state.writers.saturating_sub(1);
178 if state.writers == 0 && state.keepers == 0 {
179 state.closed = true;
180 }
181 drop(state);
182 self.ready.notify_all();
183 }
184
185 pub fn force_close(&self) {
192 let mut state = self.lock_state();
193 state.closed = true;
194 drop(state);
195 self.ready.notify_all();
196 }
197
198 pub fn pin_keeper(&self) {
205 let mut state = self.lock_state();
206 state.keepers += 1;
207 }
208
209 pub fn unpin_keeper(&self) {
213 let mut state = self.lock_state();
214 state.keepers = state.keepers.saturating_sub(1);
215 if state.writers == 0 && state.keepers == 0 {
216 state.closed = true;
217 }
218 drop(state);
219 self.ready.notify_all();
220 }
221
222 fn push_bytes(&self, data: &[u8]) -> io::Result<()> {
223 let state = self.lock_state();
224 let res = state.buffer.push_bytes(data);
225 drop(state);
226 self.ready.notify_all();
227 res
228 }
229
230 fn read_into(&self, buf: &mut [u8]) -> io::Result<usize> {
231 if buf.is_empty() {
232 return Ok(0);
233 }
234 let mut state = self.lock_state();
235 loop {
236 let n = state.buffer.read_into(buf)?;
237 if n > 0 {
238 return Ok(n);
239 }
240 if state.closed {
241 return Ok(0);
242 }
243 state = self
244 .ready
245 .wait(state)
246 .map_err(|_| io::Error::other("pipe wait poisoned"))?;
247 }
248 }
249
250 pub fn read_into_timeout(
256 &self,
257 buf: &mut [u8],
258 backstop: Duration,
259 ) -> io::Result<Option<usize>> {
260 if buf.is_empty() {
261 return Ok(Some(0));
262 }
263 let mut state = self.lock_state();
264 loop {
265 let n = state.buffer.read_into(buf)?;
266 if n > 0 {
267 return Ok(Some(n));
268 }
269 if state.closed {
270 return Ok(Some(0));
271 }
272 let (guard, waited) = self
273 .ready
274 .wait_timeout(state, backstop)
275 .map_err(|_| io::Error::other("pipe wait poisoned"))?;
276 state = guard;
277 if waited.timed_out() {
278 return Ok(None);
279 }
280 }
281 }
282
283 fn lock_state(&self) -> std::sync::MutexGuard<'_, PipeState> {
284 self.state.lock().expect("script pipe state poisoned")
285 }
286}
287
288struct PipeReader {
289 inner: Arc<PipeInner>,
290}
291
292impl PipeReader {
293 fn new(inner: Arc<PipeInner>) -> Self {
294 Self { inner }
295 }
296}
297
298impl Read for PipeReader {
299 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
300 self.inner.read_into(buf)
301 }
302}
303
304struct PipeWriter {
305 inner: Arc<PipeInner>,
306}
307
308impl PipeWriter {
309 fn new(inner: Arc<PipeInner>) -> Self {
310 inner.attach_writer();
311 Self { inner }
312 }
313}
314
315impl Write for PipeWriter {
316 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
317 self.inner.push_bytes(buf)?;
318 Ok(buf.len())
319 }
320
321 fn flush(&mut self) -> io::Result<()> {
322 Ok(())
323 }
324}
325
326impl Drop for PipeWriter {
327 fn drop(&mut self) {
328 self.inner.detach_writer();
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum PipeKindDesc {
335 Unbound,
337 Script,
339 #[cfg_attr(miri, allow(dead_code))]
344 Os,
345}
346
347impl PipeKindDesc {
348 pub fn as_str(self) -> &'static str {
350 match self {
351 PipeKindDesc::Unbound => "unbound",
352 PipeKindDesc::Script => "script",
353 PipeKindDesc::Os => "os",
354 }
355 }
356
357 pub fn is_os(self) -> bool {
359 matches!(self, PipeKindDesc::Os)
360 }
361}
362
363#[derive(Debug, Clone)]
366pub struct PipeInfo {
367 pub kind: PipeKindDesc,
369 pub buffered: u64,
372 pub readers: usize,
375 pub writers: usize,
378}
379
380pub fn script_backend(handle: &crate::slot::PipeHandle) -> Option<Arc<PipeInner>> {
385 use crate::slot::Slot;
386 let guard = handle.cell().lock().expect("pipe handle lock poisoned");
387 match &*guard {
388 Slot::Script { backend } => Some(Arc::clone(backend)),
389 Slot::Unbound => None,
390 #[cfg(not(miri))]
391 Slot::Os { .. } => None,
392 }
393}
394
395pub fn peek(handle: &crate::slot::PipeHandle) -> anyhow::Result<Vec<u8>> {
400 use crate::slot::Slot;
401 let backend = {
402 let guard = handle.cell().lock().expect("pipe handle lock poisoned");
403 match &*guard {
404 Slot::Script { backend } => Some(Arc::clone(backend)),
405 Slot::Unbound => {
406 return Err(anyhow::anyhow!(
407 "cannot peek unbound pipe: bind it to a command first"
408 ));
409 }
410 #[cfg(not(miri))]
411 Slot::Os { .. } => {
412 return Err(anyhow::anyhow!(
413 "cannot peek OS-materialized pipe: drain it through a bound command instead"
414 ));
415 }
416 }
417 };
418 let Some(inner) = backend else {
419 unreachable!("unbound/os arms return above");
420 };
421 inner
422 .peek_bytes()
423 .map_err(|e| anyhow::anyhow!("failed to peek pipe: {e}"))
424}
425
426pub fn inspect(handle: &crate::slot::PipeHandle) -> PipeInfo {
430 use crate::slot::Slot;
431 let backend = {
432 let guard = handle.cell().lock().expect("pipe handle lock poisoned");
433 match &*guard {
434 Slot::Unbound => None,
435 Slot::Script { backend } => Some(backend.clone()),
436 #[cfg(not(miri))]
437 Slot::Os { .. } => {
438 return PipeInfo {
439 kind: PipeKindDesc::Os,
440 buffered: 0,
441 readers: 1,
442 writers: 1,
443 };
444 }
445 }
446 };
447 match backend {
448 Some(inner) => PipeInfo {
449 kind: PipeKindDesc::Script,
450 buffered: inner.buffered_bytes(),
451 readers: 1,
452 writers: inner.writer_count(),
453 },
454 None => PipeInfo {
455 kind: PipeKindDesc::Unbound,
456 buffered: 0,
457 readers: 0,
458 writers: 0,
459 },
460 }
461}
462
463pub struct KeeperGuard {
467 inner: Option<Arc<PipeInner>>,
468}
469
470impl KeeperGuard {
471 pub fn new(inner: Arc<PipeInner>) -> Self {
473 inner.pin_keeper();
474 Self { inner: Some(inner) }
475 }
476}
477
478impl Drop for KeeperGuard {
479 fn drop(&mut self) {
480 if let Some(inner) = self.inner.take() {
481 inner.unpin_keeper();
482 }
483 }
484}