@@ -144,7 +144,10 @@ use std::iter;
144144use std:: os:: unix:: ffi:: { OsStrExt , OsStringExt } ;
145145use std:: str;
146146use std:: str:: Utf8Chunk ;
147- use std:: sync:: { LazyLock , atomic:: Ordering } ;
147+ use std:: sync:: {
148+ LazyLock ,
149+ atomic:: { AtomicBool , Ordering } ,
150+ } ;
148151
149152/// Disables the custom signal handlers installed by Rust for stack-overflow handling. With those custom signal handlers processes ignore the first SIGBUS and SIGSEGV signal they receive.
150153/// See <https://github.com/rust-lang/rust/blob/8ac1525e091d3db28e67adcbbd6db1e1deaa37fb/src/libstd/sys/unix/stack_overflow.rs#L71-L92> for details.
@@ -169,19 +172,118 @@ pub fn disable_rust_signal_handlers() -> Result<(), Errno> {
169172pub fn stdout_is_closed ( ) -> bool {
170173 #[ cfg( unix) ]
171174 {
172- use nix:: fcntl:: { FcntlArg , fcntl} ;
173- match fcntl ( std:: io:: stdout ( ) , FcntlArg :: F_GETFL ) {
174- Ok ( _) => false ,
175- Err ( nix:: errno:: Errno :: EBADF ) => true ,
176- Err ( _) => false ,
175+ let res = unsafe { nix:: libc:: fcntl ( nix:: libc:: STDOUT_FILENO , nix:: libc:: F_GETFL ) } ;
176+ if res != -1 {
177+ return false ;
177178 }
179+ matches ! ( nix:: errno:: Errno :: last( ) , nix:: errno:: Errno :: EBADF )
178180 }
179181 #[ cfg( not( unix) ) ]
180182 {
181183 false
182184 }
183185}
184186
187+ static STDOUT_WRITTEN : AtomicBool = AtomicBool :: new ( false ) ;
188+ static STDOUT_WAS_CLOSED : AtomicBool = AtomicBool :: new ( false ) ;
189+ static STDOUT_WAS_CLOSED_SET : AtomicBool = AtomicBool :: new ( false ) ;
190+
191+ /// Reset the stdout-written flag for the current process.
192+ pub fn reset_stdout_written ( ) {
193+ STDOUT_WRITTEN . store ( false , Ordering :: Relaxed ) ;
194+ }
195+
196+ /// Record whether stdout was closed at process start.
197+ pub fn set_stdout_was_closed ( value : bool ) {
198+ STDOUT_WAS_CLOSED . store ( value, Ordering :: Relaxed ) ;
199+ STDOUT_WAS_CLOSED_SET . store ( true , Ordering :: Relaxed ) ;
200+ }
201+
202+ /// Returns true if stdout was closed at process start.
203+ pub fn stdout_was_closed ( ) -> bool {
204+ STDOUT_WAS_CLOSED . load ( Ordering :: Relaxed )
205+ }
206+
207+ /// Initialize stdout state if it wasn't set by early startup code.
208+ pub fn init_stdout_state ( ) {
209+ if !STDOUT_WAS_CLOSED_SET . load ( Ordering :: Relaxed ) {
210+ set_stdout_was_closed ( stdout_is_closed ( ) ) ;
211+ }
212+ }
213+
214+ /// Mark that at least one non-empty write to stdout was attempted.
215+ pub fn mark_stdout_written ( ) {
216+ STDOUT_WRITTEN . store ( true , Ordering :: Relaxed ) ;
217+ }
218+
219+ /// Returns true if any write to stdout was attempted.
220+ pub fn stdout_was_written ( ) -> bool {
221+ STDOUT_WRITTEN . load ( Ordering :: Relaxed )
222+ }
223+
224+ #[ cfg( unix) ]
225+ mod early_stdout_state {
226+ use super :: set_stdout_was_closed;
227+
228+ extern "C" fn init ( ) {
229+ set_stdout_was_closed ( super :: stdout_is_closed ( ) ) ;
230+ }
231+
232+ #[ used]
233+ #[ cfg_attr(
234+ target_os = "macos" ,
235+ unsafe ( link_section = "__DATA,__mod_init_func" )
236+ ) ]
237+ #[ cfg_attr( not( target_os = "macos" ) , unsafe ( link_section = ".init_array" ) ) ]
238+ static INIT : extern "C" fn ( ) = init;
239+ }
240+
241+ /// Record a pending stdout write and error if stdout was closed at startup.
242+ pub fn check_stdout_write ( len : usize ) -> std:: io:: Result < ( ) > {
243+ if len == 0 {
244+ return Ok ( ( ) ) ;
245+ }
246+ mark_stdout_written ( ) ;
247+ if stdout_was_closed ( ) {
248+ #[ cfg( unix) ]
249+ {
250+ return Err ( std:: io:: Error :: from_raw_os_error (
251+ nix:: errno:: Errno :: EBADF as i32 ,
252+ ) ) ;
253+ }
254+ #[ cfg( not( unix) ) ]
255+ {
256+ return Err ( std:: io:: Error :: new (
257+ std:: io:: ErrorKind :: BrokenPipe ,
258+ "stdout was closed" ,
259+ ) ) ;
260+ }
261+ }
262+ Ok ( ( ) )
263+ }
264+
265+ /// A writer wrapper that marks stdout as written when non-empty data is written.
266+ pub struct TrackingWriter < W > {
267+ inner : W ,
268+ }
269+
270+ impl < W > TrackingWriter < W > {
271+ pub fn new ( inner : W ) -> Self {
272+ Self { inner }
273+ }
274+ }
275+
276+ impl < W : std:: io:: Write > std:: io:: Write for TrackingWriter < W > {
277+ fn write ( & mut self , buf : & [ u8 ] ) -> std:: io:: Result < usize > {
278+ check_stdout_write ( buf. len ( ) ) ?;
279+ self . inner . write ( buf)
280+ }
281+
282+ fn flush ( & mut self ) -> std:: io:: Result < ( ) > {
283+ self . inner . flush ( )
284+ }
285+ }
286+
185287/// Returns true if the error corresponds to writing to a closed stdout.
186288pub fn is_closed_stdout_error ( err : & std:: io:: Error , stdout_was_closed : bool ) -> bool {
187289 if !stdout_was_closed {
@@ -228,6 +330,12 @@ macro_rules! bin {
228330 use std:: io:: Write ;
229331 use uucore:: locale;
230332
333+ // Capture whether stdout was already closed before we run the utility.
334+ // This must happen before any setup that might open files and reuse fd 1.
335+ uucore:: init_stdout_state( ) ;
336+ uucore:: reset_stdout_written( ) ;
337+ let stdout_was_closed = uucore:: stdout_was_closed( ) ;
338+
231339 // Preserve inherited SIGPIPE settings (e.g., from env --default-signal=PIPE)
232340 uucore:: panic:: preserve_inherited_sigpipe( ) ;
233341
@@ -245,17 +353,15 @@ macro_rules! bin {
245353 std:: process:: exit( 99 )
246354 } ) ;
247355
248- // Capture whether stdout was already closed before we run the utility.
249- // This avoids treating a closed stdout as a flush failure for "silent" commands.
250- let stdout_was_closed = uucore:: stdout_is_closed( ) ;
251-
252356 // execute utility code
253357 let mut code = $util:: uumain( uucore:: args_os( ) ) ;
254358 // (defensively) flush stdout for utility prior to exit; see <https://github.com/rust-lang/rust/issues/23818>
255359 if let Err ( e) = std:: io:: stdout( ) . flush( ) {
256360 // Treat write errors as a failure, but ignore BrokenPipe to avoid
257361 // breaking utilities that intentionally silence it (e.g., seq).
258- let ignore_closed_stdout = uucore:: is_closed_stdout_error( & e, stdout_was_closed) ;
362+ let stdout_was_written = uucore:: stdout_was_written( ) ;
363+ let ignore_closed_stdout = uucore:: is_closed_stdout_error( & e, stdout_was_closed)
364+ && !stdout_was_written;
259365 if e. kind( ) != std:: io:: ErrorKind :: BrokenPipe && !ignore_closed_stdout {
260366 eprintln!( "Error flushing stdout: {e}" ) ;
261367 if code == 0 {
0 commit comments