1use std::collections::{HashMap, HashSet, VecDeque};
2use std::io::{self, Write};
3use std::sync::{Arc, Mutex};
4
5use anyhow::Result;
6#[cfg(not(miri))]
7use anyhow::bail;
8#[cfg(not(miri))]
9use oxdock_pipe::OsPipeWriter;
10use oxdock_pipe::{
11 KeeperGuard, Materialized, PipeHandle, PipeInfo, PipeInner, SharedInput, SharedOutput,
12 inspect as inspect_handle, materialize, peek as peek_handle, script_backend,
13};
14use oxdock_process::{CommandStderr, CommandStdin, CommandStdout};
15
16#[derive(Clone, Default)]
23pub(super) struct PipeRegistry;
24
25#[cfg(not(miri))]
31fn spent_handle(idx: usize) -> anyhow::Error {
32 anyhow::anyhow!(
33 "step {}: OS pipe handle has already been consumed by another binding; declare a fresh LET $x: PIPE for a new session",
34 idx + 1,
35 )
36}
37
38impl PipeRegistry {
39 pub(super) fn ensure_handle(handle: &PipeHandle, promote: bool) -> Result<()> {
44 materialize(handle, promote)?;
45 Ok(())
46 }
47
48 pub fn peek_pipe_content(handle: &PipeHandle) -> Result<Vec<u8>> {
57 peek_handle(handle)
58 }
59
60 pub(super) fn inspect_pipe(handle: &PipeHandle) -> PipeInfo {
62 inspect_handle(handle)
63 }
64
65 pub(super) fn pin_keeper(handle: &PipeHandle) -> Result<Option<KeeperGuard>> {
71 Ok(script_backend(handle).map(KeeperGuard::new))
72 }
73
74 pub(super) fn resolve_stdin(
81 idx: usize,
82 handle: &PipeHandle,
83 direct: bool,
84 promote: bool,
85 ) -> Result<(CommandStdin, Option<Arc<PipeInner>>)> {
86 let _ = idx;
89 let _ = direct;
90 match materialize(handle, promote)? {
91 Materialized::Script(backend) => {
92 Ok((CommandStdin::Stream(backend.reader_handle()), Some(backend)))
93 }
94 #[cfg(not(miri))]
95 Materialized::Os(entry) => {
96 if direct {
97 return Ok((CommandStdin::OsPipe(entry.reader.clone()), None));
98 }
99 let owned = entry.reader.take().map_err(|_| spent_handle(idx))?;
100 Ok((
101 CommandStdin::Stream(Arc::new(std::sync::Mutex::new(owned))),
102 None,
103 ))
104 }
105 }
106 }
107
108 pub(super) fn resolve_stdout(
113 idx: usize,
114 handle: &PipeHandle,
115 direct: bool,
116 promote: bool,
117 ) -> Result<(StreamHandle, Option<Arc<PipeInner>>)> {
118 let _ = idx;
121 let _ = direct;
122 match materialize(handle, promote)? {
123 Materialized::Script(backend) => {
124 Ok((StreamHandle::Stream(backend.writer_handle()), Some(backend)))
125 }
126 #[cfg(not(miri))]
127 Materialized::Os(entry) => {
128 if direct {
129 return Ok((StreamHandle::Os(entry.writer.clone()), None));
130 }
131 let owned = entry.writer.take().map_err(|_| spent_handle(idx))?;
132 Ok((
133 StreamHandle::Stream(Arc::new(std::sync::Mutex::new(owned))),
134 None,
135 ))
136 }
137 }
138 }
139
140 pub(super) fn resolve_stderr(
147 idx: usize,
148 handle: &PipeHandle,
149 direct: bool,
150 promote: bool,
151 ) -> Result<StreamHandle> {
152 let _ = idx;
155 let _ = direct;
156 match materialize(handle, promote)? {
157 Materialized::Script(backend) => Ok(StreamHandle::Stream(backend.writer_handle())),
158 #[cfg(not(miri))]
159 Materialized::Os(entry) => {
160 if direct {
161 return Ok(StreamHandle::Os(entry.writer.clone()));
162 }
163 let owned = entry.writer.take().map_err(|_| spent_handle(idx))?;
164 Ok(StreamHandle::Stream(Arc::new(std::sync::Mutex::new(owned))))
165 }
166 }
167 }
168}
169
170#[derive(Clone, Default)]
171pub struct ExecIo {
172 stdin: Option<SharedInput>,
173 stdout: Option<SharedOutput>,
174 stderr: Option<SharedOutput>,
175 inherit_env_overrides: HashMap<String, String>,
176 inherit_env_removed: HashSet<String>,
177}
178
179pub const CHUNK_SIZE: usize = 8192;
181
182const MIN_RING_CAPACITY: usize = 1024;
184
185pub(crate) struct SlidingWindow {
188 pub(crate) needle: Vec<u8>,
189 ring: VecDeque<u8>,
190 pub matched: bool,
191}
192
193impl SlidingWindow {
194 pub fn new(needle: Vec<u8>) -> Self {
195 Self {
196 ring: VecDeque::with_capacity(needle.len().max(MIN_RING_CAPACITY)),
197 needle,
198 matched: false,
199 }
200 }
201
202 pub fn push_chunk(&mut self, chunk: &[u8]) {
203 if self.matched {
204 return;
205 }
206 let limit = self.needle.len().max(MIN_RING_CAPACITY);
208 for &byte in chunk {
209 self.ring.push_back(byte);
210 if self.ring.len() > limit {
211 self.ring.pop_front();
212 }
213 self.check_match();
214 }
215 }
216
217 pub fn update_needle(&mut self, new_needle: Vec<u8>) {
220 if self.matched {
221 return;
222 }
223 self.needle = new_needle;
224 self.check_match();
225 }
226
227 fn check_match(&mut self) {
228 if self.matched || self.ring.len() < self.needle.len() {
229 return;
230 }
231 let start = self.ring.len() - self.needle.len();
232 if self
233 .ring
234 .iter()
235 .skip(start)
236 .zip(self.needle.iter())
237 .all(|(a, b)| a == b)
238 {
239 self.matched = true;
240 }
241 }
242
243 pub fn ring_buffer(&self) -> Vec<u8> {
245 self.ring.iter().copied().collect()
246 }
247}
248
249#[derive(Clone)]
250pub(super) enum StreamHandle {
251 Stream(SharedOutput),
252 #[cfg(not(miri))]
256 Os(OsPipeWriter),
257}
258
259impl StreamHandle {
260 pub(super) fn to_stdout(&self) -> CommandStdout {
261 match self {
262 StreamHandle::Stream(writer) => CommandStdout::Stream(writer.clone()),
263 #[cfg(not(miri))]
264 StreamHandle::Os(writer) => CommandStdout::OsPipe(writer.clone()),
265 }
266 }
267
268 pub(super) fn to_stderr(&self) -> CommandStderr {
269 match self {
270 StreamHandle::Stream(writer) => CommandStderr::Stream(writer.clone()),
271 #[cfg(not(miri))]
272 StreamHandle::Os(writer) => CommandStderr::OsPipe(writer.clone()),
273 }
274 }
275}
276
277pub(super) fn write_stdout<F>(handle: Option<StreamHandle>, op: F) -> Result<()>
278where
279 F: FnOnce(&mut dyn Write) -> Result<()>,
280{
281 match handle {
282 Some(StreamHandle::Stream(writer)) => {
283 if let Ok(mut guard) = writer.lock() {
284 op(&mut *guard)?;
285 }
286 Ok(())
287 }
288 #[cfg(not(miri))]
292 Some(StreamHandle::Os(_)) => {
293 bail!("cannot write DSL output to a live OS pipe")
294 }
295 None => {
298 let mut stdout = io::stdout();
299 op(&mut stdout)
300 }
301 }
302}
303
304pub struct PipeStream {
319 reader: Option<SharedInput>,
320 writer: Option<SharedOutput>,
321}
322
323impl PipeStream {
324 pub fn reader(reader: SharedInput) -> Self {
326 Self {
327 reader: Some(reader),
328 writer: None,
329 }
330 }
331
332 pub fn writer(writer: SharedOutput) -> Self {
334 Self {
335 reader: None,
336 writer: Some(writer),
337 }
338 }
339
340 pub fn pair(reader: SharedInput, writer: SharedOutput) -> Self {
342 Self {
343 reader: Some(reader),
344 writer: Some(writer),
345 }
346 }
347}
348
349impl std::io::Read for PipeStream {
350 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
351 let Some(reader) = &self.reader else {
352 return Err(std::io::Error::new(
353 std::io::ErrorKind::NotConnected,
354 "pipe stream has no reader half",
355 ));
356 };
357 let mut guard = reader
358 .lock()
359 .map_err(|_| std::io::Error::other("pipe reader lock poisoned"))?;
360 guard.read(buf)
361 }
362}
363
364impl std::io::Write for PipeStream {
365 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
366 let Some(writer) = &self.writer else {
367 return Err(std::io::Error::new(
368 std::io::ErrorKind::NotConnected,
369 "pipe stream has no writer half",
370 ));
371 };
372 let mut guard = writer
373 .lock()
374 .map_err(|_| std::io::Error::other("pipe writer lock poisoned"))?;
375 guard.write(buf)
376 }
377
378 fn flush(&mut self) -> std::io::Result<()> {
379 let Some(writer) = &self.writer else {
380 return Err(std::io::Error::new(
381 std::io::ErrorKind::NotConnected,
382 "pipe stream has no writer half",
383 ));
384 };
385 let mut guard = writer
386 .lock()
387 .map_err(|_| std::io::Error::other("pipe writer lock poisoned"))?;
388 guard.flush()
389 }
390}
391
392pub(crate) const EXACT_STDOUT_CAP: usize = 8 * 1024 * 1024;
396
397pub(crate) struct ExactCapture {
402 pub(crate) bytes: Vec<u8>,
403 pub(crate) overflowed: bool,
404}
405
406impl ExactCapture {
407 pub fn new() -> Self {
408 Self {
409 bytes: Vec::new(),
410 overflowed: false,
411 }
412 }
413
414 pub fn push_chunk(&mut self, chunk: &[u8]) {
415 if self.overflowed {
416 return;
417 }
418 if self.bytes.len() + chunk.len() > EXACT_STDOUT_CAP {
419 self.overflowed = true;
420 return;
421 }
422 self.bytes.extend_from_slice(chunk);
423 }
424}
425
426#[derive(Clone, Copy)]
428enum TeeStream {
429 Stdout,
430 Stderr,
431}
432
433struct TeeWriter {
438 inner: Option<SharedOutput>,
439 stream: TeeStream,
440 windows: Arc<Mutex<HashMap<(usize, usize), SlidingWindow>>>,
441 exact: Arc<Mutex<HashMap<usize, ExactCapture>>>,
442}
443
444impl Write for TeeWriter {
445 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
446 match &self.inner {
448 Some(inner) => {
449 let mut guard = inner
450 .lock()
451 .map_err(|_| io::Error::other("stdout sink poisoned"))?;
452 guard.write_all(buf)?;
453 }
454 None => match self.stream {
455 TeeStream::Stdout => io::stdout().write_all(buf)?,
456 TeeStream::Stderr => io::stderr().write_all(buf)?,
457 },
458 }
459 if let Ok(mut windows) = self.windows.lock() {
461 for window in windows.values_mut() {
462 window.push_chunk(buf);
463 }
464 }
465 if let Ok(mut exact) = self.exact.lock() {
466 for capture in exact.values_mut() {
467 capture.push_chunk(buf);
468 }
469 }
470 Ok(buf.len())
471 }
472
473 fn flush(&mut self) -> io::Result<()> {
474 match &self.inner {
475 Some(inner) => {
476 let mut guard = inner
477 .lock()
478 .map_err(|_| io::Error::other("stdout sink poisoned"))?;
479 guard.flush()?;
480 }
481 None => match self.stream {
482 TeeStream::Stdout => io::stdout().flush()?,
483 TeeStream::Stderr => io::stderr().flush()?,
484 },
485 }
486 Ok(())
487 }
488}
489
490pub(crate) fn teed_stdout(
492 sink: Option<SharedOutput>,
493 windows: Arc<Mutex<HashMap<(usize, usize), SlidingWindow>>>,
494 exact: Arc<Mutex<HashMap<usize, ExactCapture>>>,
495) -> SharedOutput {
496 Arc::new(Mutex::new(TeeWriter {
497 inner: sink,
498 stream: TeeStream::Stdout,
499 windows,
500 exact,
501 }))
502}
503
504pub(crate) fn teed_stderr(
508 sink: Option<SharedOutput>,
509 windows: Arc<Mutex<HashMap<(usize, usize), SlidingWindow>>>,
510) -> SharedOutput {
511 Arc::new(Mutex::new(TeeWriter {
512 inner: sink,
513 stream: TeeStream::Stderr,
514 windows,
515 exact: Arc::new(Mutex::new(HashMap::new())),
516 }))
517}
518
519impl ExecIo {
520 pub fn new() -> Self {
521 Self::default()
522 }
523
524 pub fn set_stdin(&mut self, stdin: Option<SharedInput>) {
525 self.stdin = stdin;
526 }
527
528 pub fn set_stdout(&mut self, stdout: Option<SharedOutput>) {
529 self.stdout = stdout.clone();
530 if self.stderr.is_none() {
531 self.stderr = stdout;
532 }
533 }
534
535 pub fn set_stderr(&mut self, stderr: Option<SharedOutput>) {
536 self.stderr = stderr;
537 }
538
539 pub fn insert_inherit_env<S: Into<String>, V: Into<String>>(&mut self, key: S, value: V) {
540 let key = key.into();
541 self.inherit_env_removed.remove(&key);
542 self.inherit_env_overrides.insert(key, value.into());
543 }
544
545 pub fn remove_inherit_env<S: Into<String>>(&mut self, key: S) {
546 let key = key.into();
547 self.inherit_env_overrides.remove(&key);
548 self.inherit_env_removed.insert(key);
549 }
550
551 pub fn inherit_env_value(&self, key: &str) -> Option<&String> {
552 self.inherit_env_overrides.get(key)
553 }
554
555 pub fn inherit_env_is_removed(&self, key: &str) -> bool {
556 self.inherit_env_removed.contains(key)
557 }
558
559 pub fn inherit_env_overrides(&self) -> &std::collections::HashMap<String, String> {
560 &self.inherit_env_overrides
561 }
562
563 pub(super) fn ensure_handle(&self, handle: &PipeHandle, promote: bool) -> Result<()> {
568 PipeRegistry::ensure_handle(handle, promote)
569 }
570
571 pub(super) fn inspect_pipe(&self, handle: &PipeHandle) -> PipeInfo {
573 PipeRegistry::inspect_pipe(handle)
574 }
575
576 pub fn peek_pipe_content(&self, handle: &PipeHandle) -> Result<Vec<u8>> {
584 PipeRegistry::peek_pipe_content(handle)
585 }
586
587 pub(super) fn pin_keeper(&self, handle: &PipeHandle) -> Result<Option<KeeperGuard>> {
592 PipeRegistry::pin_keeper(handle)
593 }
594
595 pub(super) fn resolve_stdin(
598 &self,
599 idx: usize,
600 handle: &PipeHandle,
601 direct: bool,
602 promote: bool,
603 ) -> Result<(CommandStdin, Option<Arc<PipeInner>>)> {
604 PipeRegistry::resolve_stdin(idx, handle, direct, promote)
605 }
606
607 pub(super) fn resolve_stdout(
610 &self,
611 idx: usize,
612 handle: &PipeHandle,
613 direct: bool,
614 promote: bool,
615 ) -> Result<(StreamHandle, Option<Arc<PipeInner>>)> {
616 PipeRegistry::resolve_stdout(idx, handle, direct, promote)
617 }
618
619 pub(super) fn resolve_stderr(
621 &self,
622 idx: usize,
623 handle: &PipeHandle,
624 direct: bool,
625 promote: bool,
626 ) -> Result<StreamHandle> {
627 PipeRegistry::resolve_stderr(idx, handle, direct, promote)
628 }
629
630 pub fn stdin(&self) -> Option<SharedInput> {
631 self.stdin.clone()
632 }
633
634 pub fn stdout(&self) -> Option<SharedOutput> {
635 self.stdout.clone()
636 }
637
638 pub fn stderr(&self) -> Option<SharedOutput> {
639 self.stderr.clone().or_else(|| self.stdout.clone())
640 }
641}
642
643pub(super) fn assemble_default_io(
644 stdin: Option<SharedInput>,
645 stdout: Option<SharedOutput>,
646) -> ExecIo {
647 let mut io = ExecIo::new();
648 io.set_stdin(stdin);
649 io.set_stdout(stdout.clone());
650 io.set_stderr(stdout);
651 io
652}