1use std::fmt;
33use std::io;
34use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
35use std::sync::{Arc, LazyLock, Mutex};
36
37use thiserror::Error;
38use tokio::io::{AsyncWrite, AsyncWriteExt};
39use tokio::sync::{mpsc, oneshot};
40use tokio::time::{Duration, sleep};
41
42pub const RAW_STDOUT_RETRY_DELAY: Duration = Duration::from_millis(10);
44
45pub const RAW_STDOUT_QUEUE_CAPACITY: usize = 256;
50
51#[derive(Debug, Error, Clone)]
53pub enum OutputGuardError {
54 #[error("raw stdout writer task is shut down")]
56 WriterShutdown,
57
58 #[error("raw stdout coordinator requires a Tokio runtime: {0}")]
60 RuntimeUnavailable(String),
61
62 #[error("raw stdout I/O failed: {0}")]
64 Io(String),
65
66 #[error("raw stdout writer failed earlier: {0}")]
70 Latched(String),
71}
72
73impl OutputGuardError {
74 fn io(err: &io::Error) -> Self {
75 Self::Io(err.to_string())
76 }
77
78 fn latched(message: impl Into<String>) -> Self {
79 Self::Latched(message.into())
80 }
81}
82
83pub type Result<T, E = OutputGuardError> = std::result::Result<T, E>;
85
86enum Command {
87 Write {
88 bytes: Vec<u8>,
89 seq: u64,
91 },
92 Wait {
93 target_seq: u64,
95 reply: oneshot::Sender<Result<()>>,
96 },
97 Flush {
98 target_seq: u64,
99 reply: oneshot::Sender<Result<()>>,
100 },
101 Shutdown {
102 reply: oneshot::Sender<Result<()>>,
103 },
104 InstallSink {
106 sink: Box<dyn AsyncWrite + Unpin + Send>,
107 reply: oneshot::Sender<Result<()>>,
108 },
109}
110
111struct Shared {
112 taken_over: AtomicBool,
113 enqueued_seq: AtomicU64,
115 completed_seq: AtomicU64,
117 fatal: Mutex<Option<String>>,
119 tx: mpsc::Sender<Command>,
120}
121
122impl Shared {
123 fn take_fatal(&self) -> Option<OutputGuardError> {
124 self.fatal
125 .lock()
126 .unwrap_or_else(std::sync::PoisonError::into_inner)
127 .clone()
128 .map(OutputGuardError::latched)
129 }
130
131 fn set_fatal(&self, err: &OutputGuardError) {
132 let message = err.to_string();
133 let mut guard = self
134 .fatal
135 .lock()
136 .unwrap_or_else(std::sync::PoisonError::into_inner);
137 if guard.is_none() {
138 *guard = Some(message);
139 }
140 }
141
142 fn clear_fatal(&self) {
143 *self
144 .fatal
145 .lock()
146 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
147 }
148}
149
150struct Coordinator {
151 shared: Arc<Shared>,
152}
153
154static COORDINATOR: LazyLock<Mutex<Option<Coordinator>>> = LazyLock::new(|| Mutex::new(None));
155
156fn with_coordinator_mut<T>(f: impl FnOnce(&mut Option<Coordinator>) -> T) -> T {
157 let mut guard = COORDINATOR
158 .lock()
159 .unwrap_or_else(std::sync::PoisonError::into_inner);
160 f(&mut guard)
161}
162
163fn current_shared() -> Option<Arc<Shared>> {
164 with_coordinator_mut(|slot| slot.as_ref().map(|c| Arc::clone(&c.shared)))
165}
166
167#[must_use]
169pub fn is_stdout_taken_over() -> bool {
170 current_shared().is_some_and(|s| s.taken_over.load(Ordering::SeqCst))
171}
172
173pub fn take_over_stdout() -> Result<()> {
183 let shared = ensure_coordinator()?;
184 shared.taken_over.store(true, Ordering::SeqCst);
185 Ok(())
186}
187
188pub fn restore_stdout() {
192 if let Some(shared) = current_shared() {
193 shared.taken_over.store(false, Ordering::SeqCst);
194 }
195}
196
197pub async fn write_raw_stdout(text: impl AsRef<[u8]>) -> Result<()> {
211 let bytes = text.as_ref();
212 if bytes.is_empty() {
213 return Ok(());
214 }
215 let shared = ensure_coordinator()?;
216 if let Some(err) = shared.take_fatal() {
217 return Err(err);
218 }
219 let seq = shared.enqueued_seq.fetch_add(1, Ordering::SeqCst) + 1;
220 shared
221 .tx
222 .send(Command::Write {
223 bytes: bytes.to_vec(),
224 seq,
225 })
226 .await
227 .map_err(|_| OutputGuardError::WriterShutdown)?;
228 if let Some(err) = shared.take_fatal() {
229 return Err(err);
230 }
231 Ok(())
232}
233
234pub async fn wait_for_raw_stdout_backpressure() -> Result<()> {
244 let shared = ensure_coordinator()?;
245 if let Some(err) = shared.take_fatal() {
246 return Err(err);
247 }
248 let target_seq = shared.enqueued_seq.load(Ordering::SeqCst);
249 if target_seq == 0 || shared.completed_seq.load(Ordering::SeqCst) >= target_seq {
250 return shared.take_fatal().map_or(Ok(()), Err);
251 }
252 let (reply_tx, reply_rx) = oneshot::channel();
253 shared
254 .tx
255 .send(Command::Wait {
256 target_seq,
257 reply: reply_tx,
258 })
259 .await
260 .map_err(|_| OutputGuardError::WriterShutdown)?;
261 reply_rx
262 .await
263 .map_err(|_| OutputGuardError::WriterShutdown)?
264}
265
266pub async fn flush_raw_stdout() -> Result<()> {
272 let shared = ensure_coordinator()?;
273 if let Some(err) = shared.take_fatal() {
274 return Err(err);
275 }
276 let target_seq = shared.enqueued_seq.load(Ordering::SeqCst);
277 let (reply_tx, reply_rx) = oneshot::channel();
278 shared
279 .tx
280 .send(Command::Flush {
281 target_seq,
282 reply: reply_tx,
283 })
284 .await
285 .map_err(|_| OutputGuardError::WriterShutdown)?;
286 reply_rx
287 .await
288 .map_err(|_| OutputGuardError::WriterShutdown)?
289}
290
291pub async fn shutdown_raw_stdout() -> Result<()> {
300 let shared = with_coordinator_mut(|slot| slot.take().map(|c| c.shared));
304 let Some(shared) = shared else {
305 return Ok(());
306 };
307 let (reply_tx, reply_rx) = oneshot::channel();
308 if shared
309 .tx
310 .send(Command::Shutdown { reply: reply_tx })
311 .await
312 .is_err()
313 {
314 return Err(OutputGuardError::WriterShutdown);
315 }
316 reply_rx
317 .await
318 .map_err(|_| OutputGuardError::WriterShutdown)?
319}
320
321pub async fn install_raw_stdout_sink_for_test<W>(sink: W) -> Result<()>
330where
331 W: AsyncWrite + Unpin + Send + 'static,
332{
333 let shared = ensure_coordinator()?;
334 shared.clear_fatal();
335 let (reply_tx, reply_rx) = oneshot::channel();
336 shared
337 .tx
338 .send(Command::InstallSink {
339 sink: Box::new(sink),
340 reply: reply_tx,
341 })
342 .await
343 .map_err(|_| OutputGuardError::WriterShutdown)?;
344 reply_rx
345 .await
346 .map_err(|_| OutputGuardError::WriterShutdown)?
347}
348
349pub struct ProductOutput;
356
357impl ProductOutput {
358 pub fn write(text: &str) {
360 use std::io::Write;
361 if is_stdout_taken_over() {
362 let _ = std::io::stderr().write_all(text.as_bytes());
363 let _ = std::io::stderr().flush();
364 } else {
365 let _ = std::io::stdout().write_all(text.as_bytes());
366 let _ = std::io::stdout().flush();
367 }
368 }
369
370 pub fn writeln(text: &str) {
372 Self::write(text);
373 Self::write("\n");
374 }
375
376 pub fn write_fmt(args: fmt::Arguments<'_>) {
378 Self::write(&format!("{args}"));
379 }
380
381 pub fn writeln_fmt(args: fmt::Arguments<'_>) {
383 Self::writeln(&format!("{args}"));
384 }
385}
386
387fn ensure_coordinator() -> Result<Arc<Shared>> {
388 if let Some(shared) = current_shared() {
389 return Ok(shared);
390 }
391 let handle = tokio::runtime::Handle::try_current()
392 .map_err(|err| OutputGuardError::RuntimeUnavailable(err.to_string()))?;
393 with_coordinator_mut(|slot| {
394 if let Some(existing) = slot.as_ref() {
395 return Ok(Arc::clone(&existing.shared));
396 }
397 let (tx, rx) = mpsc::channel(RAW_STDOUT_QUEUE_CAPACITY);
398 let shared = Arc::new(Shared {
399 taken_over: AtomicBool::new(false),
400 enqueued_seq: AtomicU64::new(0),
401 completed_seq: AtomicU64::new(0),
402 fatal: Mutex::new(None),
403 tx,
404 });
405 let worker_shared = Arc::clone(&shared);
406 handle.spawn(async move {
407 writer_loop(worker_shared, rx, Box::new(tokio::io::stdout())).await;
408 });
409 *slot = Some(Coordinator {
410 shared: Arc::clone(&shared),
411 });
412 Ok(shared)
413 })
414}
415
416async fn writer_loop(
417 shared: Arc<Shared>,
418 mut rx: mpsc::Receiver<Command>,
419 mut sink: Box<dyn AsyncWrite + Unpin + Send>,
420) {
421 while let Some(cmd) = rx.recv().await {
422 match cmd {
423 Command::Write { bytes, seq } => {
424 if shared.take_fatal().is_some() {
425 shared.completed_seq.store(seq, Ordering::SeqCst);
426 continue;
427 }
428 match write_all_with_retry(sink.as_mut(), &bytes).await {
429 Ok(()) => {
430 shared.completed_seq.store(seq, Ordering::SeqCst);
431 }
432 Err(err) => {
433 shared.set_fatal(&err);
434 shared.completed_seq.store(seq, Ordering::SeqCst);
435 }
436 }
437 }
438 Command::Wait { target_seq, reply } => {
439 let result = wait_until_seq(&shared, target_seq).await;
440 let _ = reply.send(result);
441 }
442 Command::Flush { target_seq, reply } => {
443 let result = async {
444 wait_until_seq(&shared, target_seq).await?;
445 if let Some(err) = shared.take_fatal() {
446 return Err(err);
447 }
448 flush_with_retry(sink.as_mut()).await
449 }
450 .await;
451 if let Err(err) = &result
452 && !matches!(err, OutputGuardError::Latched(_))
453 {
454 shared.set_fatal(err);
455 }
456 let _ = reply.send(result);
457 }
458 Command::Shutdown { reply } => {
459 let target = shared.enqueued_seq.load(Ordering::SeqCst);
460 let result = wait_until_seq(&shared, target).await;
461 let _ = reply.send(result);
462 break;
463 }
464 Command::InstallSink {
465 sink: new_sink,
466 reply,
467 } => {
468 let target = shared.enqueued_seq.load(Ordering::SeqCst);
469 let result = wait_until_seq(&shared, target).await;
470 if result.is_ok() {
471 sink = new_sink;
472 shared.clear_fatal();
473 }
474 let _ = reply.send(result);
475 }
476 }
477 }
478}
479
480async fn wait_until_seq(shared: &Shared, target_seq: u64) -> Result<()> {
481 if target_seq == 0 {
482 return shared.take_fatal().map_or(Ok(()), Err);
483 }
484 loop {
488 if shared.completed_seq.load(Ordering::SeqCst) >= target_seq {
489 return shared.take_fatal().map_or(Ok(()), Err);
490 }
491 if let Some(err) = shared.take_fatal() {
492 if shared.completed_seq.load(Ordering::SeqCst) >= target_seq {
494 return Err(err);
495 }
496 }
497 tokio::task::yield_now().await;
498 sleep(Duration::from_millis(1)).await;
499 }
500}
501
502fn is_retryable_write_error(err: &io::Error) -> bool {
503 if matches!(
504 err.kind(),
505 io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
506 ) {
507 return true;
508 }
509 #[cfg(unix)]
510 {
511 match err.raw_os_error() {
512 Some(code)
513 if code == nix::libc::EAGAIN
514 || code == nix::libc::EWOULDBLOCK
515 || code == nix::libc::ENOBUFS =>
516 {
517 return true;
518 }
519 _ => {}
520 }
521 }
522 false
523}
524
525async fn write_all_with_retry(
526 sink: &mut (dyn AsyncWrite + Unpin + Send),
527 mut bytes: &[u8],
528) -> Result<()> {
529 while !bytes.is_empty() {
530 match sink.write(bytes).await {
531 Ok(0) => {
532 return Err(OutputGuardError::io(&io::Error::new(
533 io::ErrorKind::WriteZero,
534 "raw stdout write returned 0 bytes",
535 )));
536 }
537 Ok(n) => {
538 bytes = &bytes[n..];
539 }
540 Err(err) if is_retryable_write_error(&err) => {
541 sleep(RAW_STDOUT_RETRY_DELAY).await;
542 }
543 Err(err) => return Err(OutputGuardError::io(&err)),
544 }
545 }
546 Ok(())
547}
548
549async fn flush_with_retry(sink: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<()> {
550 loop {
551 match sink.flush().await {
552 Ok(()) => return Ok(()),
553 Err(err) if is_retryable_write_error(&err) => {
554 sleep(RAW_STDOUT_RETRY_DELAY).await;
555 }
556 Err(err) => return Err(OutputGuardError::io(&err)),
557 }
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use std::pin::Pin;
565 use std::sync::atomic::AtomicUsize;
566 use std::task::{Context, Poll};
567 use tokio::sync::Mutex as AsyncMutex;
568
569 type TestResult = std::result::Result<(), String>;
570
571 fn map_err(err: impl std::fmt::Display) -> String {
572 err.to_string()
573 }
574
575 static TEST_LOCK: LazyLock<AsyncMutex<()>> = LazyLock::new(|| AsyncMutex::new(()));
577
578 struct TestSink {
580 state: Arc<AsyncMutex<TestSinkState>>,
581 }
582
583 struct TestSinkState {
584 buf: Vec<u8>,
585 would_block_remaining: usize,
588 max_chunk: usize,
590 fatal_on_write: Option<io::ErrorKind>,
592 flush_count: usize,
593 write_calls: usize,
594 }
595
596 impl TestSink {
597 fn new(state: Arc<AsyncMutex<TestSinkState>>) -> Self {
598 Self { state }
599 }
600 }
601
602 impl AsyncWrite for TestSink {
603 fn poll_write(
604 self: Pin<&mut Self>,
605 cx: &mut Context<'_>,
606 buf: &[u8],
607 ) -> Poll<io::Result<usize>> {
608 let state = self.state.clone();
609 let Ok(mut guard) = state.try_lock() else {
611 cx.waker().wake_by_ref();
612 return Poll::Pending;
613 };
614 guard.write_calls += 1;
615 if let Some(kind) = guard.fatal_on_write.take() {
616 return Poll::Ready(Err(io::Error::from(kind)));
617 }
618 if guard.would_block_remaining > 0 {
619 guard.would_block_remaining -= 1;
620 cx.waker().wake_by_ref();
623 return Poll::Ready(Err(io::Error::from(io::ErrorKind::WouldBlock)));
624 }
625 if buf.is_empty() {
626 return Poll::Ready(Ok(0));
627 }
628 let n = if guard.max_chunk == 0 {
629 buf.len()
630 } else {
631 buf.len().min(guard.max_chunk)
632 };
633 guard.buf.extend_from_slice(&buf[..n]);
634 Poll::Ready(Ok(n))
635 }
636
637 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
638 let state = self.state.clone();
639 let Ok(mut guard) = state.try_lock() else {
640 cx.waker().wake_by_ref();
641 return Poll::Pending;
642 };
643 guard.flush_count += 1;
644 Poll::Ready(Ok(()))
645 }
646
647 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
648 self.poll_flush(cx)
649 }
650 }
651
652 async fn reset_coordinator() {
653 let _ = shutdown_raw_stdout().await;
654 with_coordinator_mut(|slot| {
655 *slot = None;
656 });
657 }
658
659 async fn install_sink(state: Arc<AsyncMutex<TestSinkState>>) -> TestResult {
660 reset_coordinator().await;
661 install_raw_stdout_sink_for_test(TestSink::new(state))
662 .await
663 .map_err(map_err)
664 }
665
666 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
667 async fn ordered_writes_preserve_fifo() -> TestResult {
668 let _guard = TEST_LOCK.lock().await;
669 let state = Arc::new(AsyncMutex::new(TestSinkState {
670 buf: Vec::new(),
671 would_block_remaining: 0,
672 max_chunk: 0,
673 fatal_on_write: None,
674 flush_count: 0,
675 write_calls: 0,
676 }));
677 install_sink(Arc::clone(&state)).await?;
678
679 write_raw_stdout("one\n").await.map_err(map_err)?;
680 write_raw_stdout("two\n").await.map_err(map_err)?;
681 write_raw_stdout("three\n").await.map_err(map_err)?;
682 wait_for_raw_stdout_backpressure().await.map_err(map_err)?;
683
684 let guard = state.lock().await;
685 let text = std::str::from_utf8(&guard.buf).map_err(map_err)?;
686 if text != "one\ntwo\nthree\n" {
687 return Err(format!("unexpected buffer: {text:?}"));
688 }
689 reset_coordinator().await;
690 Ok(())
691 }
692
693 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
694 async fn backpressure_waits_for_slow_partial_writes() -> TestResult {
695 let _guard = TEST_LOCK.lock().await;
696 let state = Arc::new(AsyncMutex::new(TestSinkState {
697 buf: Vec::new(),
698 would_block_remaining: 0,
699 max_chunk: 1,
700 fatal_on_write: None,
701 flush_count: 0,
702 write_calls: 0,
703 }));
704 install_sink(Arc::clone(&state)).await?;
705
706 write_raw_stdout("abcd").await.map_err(map_err)?;
707 wait_for_raw_stdout_backpressure().await.map_err(map_err)?;
708
709 let guard = state.lock().await;
710 let text = std::str::from_utf8(&guard.buf).map_err(map_err)?;
711 if text != "abcd" {
712 return Err(format!("unexpected buffer: {text:?}"));
713 }
714 if guard.write_calls < 4 {
715 return Err(format!(
716 "expected partial writes, got {}",
717 guard.write_calls
718 ));
719 }
720 reset_coordinator().await;
721 Ok(())
722 }
723
724 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
725 async fn would_block_is_retried() -> TestResult {
726 let _guard = TEST_LOCK.lock().await;
727 let state = Arc::new(AsyncMutex::new(TestSinkState {
728 buf: Vec::new(),
729 would_block_remaining: 3,
730 max_chunk: 0,
731 fatal_on_write: None,
732 flush_count: 0,
733 write_calls: 0,
734 }));
735 install_sink(Arc::clone(&state)).await?;
736
737 write_raw_stdout("ok").await.map_err(map_err)?;
738 wait_for_raw_stdout_backpressure().await.map_err(map_err)?;
739
740 let guard = state.lock().await;
741 let text = std::str::from_utf8(&guard.buf).map_err(map_err)?;
742 if text != "ok" {
743 return Err(format!("unexpected buffer: {text:?}"));
744 }
745 if guard.write_calls < 4 {
746 return Err(format!(
747 "expected would-block attempts plus success, got {}",
748 guard.write_calls
749 ));
750 }
751 reset_coordinator().await;
752 Ok(())
753 }
754
755 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
756 async fn flush_drains_and_flushes_sink() -> TestResult {
757 let _guard = TEST_LOCK.lock().await;
758 let state = Arc::new(AsyncMutex::new(TestSinkState {
759 buf: Vec::new(),
760 would_block_remaining: 0,
761 max_chunk: 0,
762 fatal_on_write: None,
763 flush_count: 0,
764 write_calls: 0,
765 }));
766 install_sink(Arc::clone(&state)).await?;
767
768 write_raw_stdout("flush-me").await.map_err(map_err)?;
769 flush_raw_stdout().await.map_err(map_err)?;
770
771 let guard = state.lock().await;
772 let text = std::str::from_utf8(&guard.buf).map_err(map_err)?;
773 if text != "flush-me" {
774 return Err(format!("unexpected buffer: {text:?}"));
775 }
776 if guard.flush_count < 1 {
777 return Err("expected at least one flush".to_owned());
778 }
779 reset_coordinator().await;
780 Ok(())
781 }
782
783 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
784 async fn takeover_is_idempotent_and_restorable() -> TestResult {
785 let _guard = TEST_LOCK.lock().await;
786 reset_coordinator().await;
787 if is_stdout_taken_over() {
788 return Err("expected not taken over".to_owned());
789 }
790
791 take_over_stdout().map_err(map_err)?;
792 if !is_stdout_taken_over() {
793 return Err("expected taken over".to_owned());
794 }
795 take_over_stdout().map_err(map_err)?;
796 if !is_stdout_taken_over() {
797 return Err("expected still taken over".to_owned());
798 }
799
800 restore_stdout();
801 if is_stdout_taken_over() {
802 return Err("expected restored".to_owned());
803 }
804 restore_stdout(); if is_stdout_taken_over() {
806 return Err("expected still restored".to_owned());
807 }
808 reset_coordinator().await;
809 Ok(())
810 }
811
812 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
813 async fn product_output_routes_to_stderr_when_taken_over() -> TestResult {
814 let _guard = TEST_LOCK.lock().await;
819 reset_coordinator().await;
820 take_over_stdout().map_err(map_err)?;
821 if !is_stdout_taken_over() {
822 return Err("expected taken over".to_owned());
823 }
824 ProductOutput::writeln("protocol-clean product line");
825 restore_stdout();
826 if is_stdout_taken_over() {
827 return Err("expected restored".to_owned());
828 }
829 ProductOutput::writeln("normal product line");
830 reset_coordinator().await;
831 Ok(())
832 }
833
834 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
835 async fn writer_failure_propagates_and_latches() -> TestResult {
836 let _guard = TEST_LOCK.lock().await;
837 let state = Arc::new(AsyncMutex::new(TestSinkState {
838 buf: Vec::new(),
839 would_block_remaining: 0,
840 max_chunk: 0,
841 fatal_on_write: Some(io::ErrorKind::BrokenPipe),
842 flush_count: 0,
843 write_calls: 0,
844 }));
845 install_sink(Arc::clone(&state)).await?;
846
847 write_raw_stdout("will-fail").await.map_err(map_err)?;
848 let err = match wait_for_raw_stdout_backpressure().await {
849 Ok(()) => return Err("expected writer failure".to_owned()),
850 Err(err) => err,
851 };
852 if !matches!(err, OutputGuardError::Io(_) | OutputGuardError::Latched(_)) {
853 return Err(format!("unexpected error: {err}"));
854 }
855
856 let latched = match write_raw_stdout("after-fail").await {
857 Ok(()) => return Err("expected latched fatal".to_owned()),
858 Err(err) => err,
859 };
860 if !matches!(latched, OutputGuardError::Latched(_)) {
861 return Err(format!("expected latched error, got {latched}"));
862 }
863 reset_coordinator().await;
864 Ok(())
865 }
866
867 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
868 async fn shutdown_cancels_further_writes() -> TestResult {
869 let _guard = TEST_LOCK.lock().await;
870 let state = Arc::new(AsyncMutex::new(TestSinkState {
871 buf: Vec::new(),
872 would_block_remaining: 0,
873 max_chunk: 0,
874 fatal_on_write: None,
875 flush_count: 0,
876 write_calls: 0,
877 }));
878 install_sink(Arc::clone(&state)).await?;
879
880 write_raw_stdout("before-shutdown").await.map_err(map_err)?;
881 shutdown_raw_stdout().await.map_err(map_err)?;
882
883 install_raw_stdout_sink_for_test(TestSink::new(Arc::clone(&state)))
886 .await
887 .map_err(map_err)?;
888 write_raw_stdout("after-restart").await.map_err(map_err)?;
889 wait_for_raw_stdout_backpressure().await.map_err(map_err)?;
890
891 let guard = state.lock().await;
892 let text = std::str::from_utf8(&guard.buf).map_err(map_err)?;
893 if !text.contains("before-shutdown") {
894 return Err(format!("missing before-shutdown in {text:?}"));
895 }
896 if !text.contains("after-restart") {
897 return Err(format!("missing after-restart in {text:?}"));
898 }
899 reset_coordinator().await;
900 Ok(())
901 }
902
903 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
904 async fn empty_write_is_noop() -> TestResult {
905 let _guard = TEST_LOCK.lock().await;
906 let state = Arc::new(AsyncMutex::new(TestSinkState {
907 buf: Vec::new(),
908 would_block_remaining: 0,
909 max_chunk: 0,
910 fatal_on_write: None,
911 flush_count: 0,
912 write_calls: 0,
913 }));
914 install_sink(Arc::clone(&state)).await?;
915
916 write_raw_stdout("").await.map_err(map_err)?;
917 wait_for_raw_stdout_backpressure().await.map_err(map_err)?;
918 let guard = state.lock().await;
919 if !guard.buf.is_empty() {
920 return Err(format!("expected empty buffer, got {:?}", guard.buf));
921 }
922 if guard.write_calls != 0 {
923 return Err(format!(
924 "expected zero write calls, got {}",
925 guard.write_calls
926 ));
927 }
928 reset_coordinator().await;
929 Ok(())
930 }
931
932 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
933 async fn concurrent_writers_stay_ordered_by_enqueue() -> TestResult {
934 let _guard = TEST_LOCK.lock().await;
935 let state = Arc::new(AsyncMutex::new(TestSinkState {
936 buf: Vec::new(),
937 would_block_remaining: 0,
938 max_chunk: 2,
939 fatal_on_write: None,
940 flush_count: 0,
941 write_calls: 0,
942 }));
943 install_sink(Arc::clone(&state)).await?;
944
945 let counter = Arc::new(AtomicUsize::new(0));
946 let mut handles = Vec::new();
947 for i in 0..8 {
948 let counter = Arc::clone(&counter);
949 handles.push(tokio::spawn(async move {
950 while counter.load(Ordering::SeqCst) != i {
953 tokio::task::yield_now().await;
954 }
955 let payload = format!("{i}");
956 write_raw_stdout(payload).await.map_err(map_err)?;
957 counter.fetch_add(1, Ordering::SeqCst);
958 Ok::<(), String>(())
959 }));
960 }
961 for handle in handles {
962 handle
963 .await
964 .map_err(|err| format!("join: {err}"))?
965 .map_err(map_err)?;
966 }
967 wait_for_raw_stdout_backpressure().await.map_err(map_err)?;
968
969 let guard = state.lock().await;
970 let text = std::str::from_utf8(&guard.buf).map_err(map_err)?;
971 if text != "01234567" {
972 return Err(format!("unexpected buffer: {text:?}"));
973 }
974 reset_coordinator().await;
975 Ok(())
976 }
977}