@@ -79,6 +79,27 @@ def writelines(self, data):
79
79
self .flush ()
80
80
81
81
82
+ class _fd_closer (object ):
83
+ """A context manager to handle closing a specified file descriptor
84
+
85
+ Ideally we would use `os.fdopen(... closefd=True)`; however, it
86
+ appears that Python ignores `closefd` on Windows. This would
87
+ eventually lead to the process exceeding the maximum number of open
88
+ files (see Pyomo/pyomo#3587). So, we will explicitly manage closing
89
+ the file descriptors that we open using this context manager.
90
+
91
+ """
92
+
93
+ def __init__ (self , fd ):
94
+ self .fd = fd
95
+
96
+ def __enter__ (self ):
97
+ return self .fd
98
+
99
+ def __exit__ (self , et , ev , tb ):
100
+ os .close (self .fd )
101
+
102
+
82
103
class redirect_fd (object ):
83
104
"""Redirect a file descriptor to a new file or file descriptor.
84
105
@@ -256,9 +277,11 @@ def _exit_context_stack(self, et, ev, tb):
256
277
FAIL = []
257
278
while self .context_stack :
258
279
try :
259
- self .context_stack .pop ().__exit__ (et , ev , tb )
280
+ cm = self .context_stack .pop ()
281
+ cm .__exit__ (et , ev , tb )
260
282
except :
261
- FAIL .append (str (sys .exc_info ()[1 ]))
283
+ _stack = self .context_stack
284
+ FAIL .append (f"{ sys .exc_info ()[1 ]} ({ len (_stack )+ 1 } : { cm } @{ id (cm ):x} )" )
262
285
return FAIL
263
286
264
287
def __enter__ (self ):
@@ -286,8 +309,15 @@ def __enter__(self):
286
309
# overwrite it when we get to redirect_fd below). If
287
310
# sys.stderr doesn't have a file descriptor, we will
288
311
# fall back on the process stderr (FD=2).
312
+ #
313
+ # Note that we would like to use closefd=True, but can't
314
+ # (see _fd_closer docs)
289
315
log_stream = self ._enter_context (
290
- os .fdopen (os .dup (old_fd [1 ] or 2 ), mode = "w" , closefd = True )
316
+ os .fdopen (
317
+ self ._enter_context (_fd_closer (os .dup (old_fd [1 ] or 2 ))),
318
+ mode = "w" ,
319
+ closefd = False ,
320
+ )
291
321
)
292
322
else :
293
323
log_stream = self .old [1 ]
@@ -340,11 +370,17 @@ def __enter__(self):
340
370
# loop that we really want to break. Undo
341
371
# the redirect by pointing our output stream
342
372
# back to the original file descriptor.
373
+ #
374
+ # Note that we would like to use closefd=True, but can't
375
+ # (see _fd_closer docs)
343
376
stream = self ._enter_context (
344
377
os .fdopen (
345
- os .dup (fd_redirect [fd ].original_fd ),
378
+ self ._enter_context (
379
+ _fd_closer (os .dup (fd_redirect [fd ].original_fd )),
380
+ prior_to = self .tee ,
381
+ ),
346
382
mode = "w" ,
347
- closefd = True ,
383
+ closefd = False ,
348
384
),
349
385
prior_to = self .tee ,
350
386
)
@@ -366,10 +402,16 @@ def __enter__(self):
366
402
def __exit__ (self , et , ev , tb ):
367
403
# Check that we were nested correctly
368
404
FAIL = []
369
- if self .tee .STDOUT is not sys .stdout :
370
- FAIL .append ('Captured output does not match sys.stdout.' )
371
- if self .tee .STDERR is not sys .stderr :
372
- FAIL .append ('Captured output does not match sys.stderr.' )
405
+ if self .tee ._stdout is not None and self .tee .STDOUT is not sys .stdout :
406
+ FAIL .append (
407
+ 'Captured output (%s) does not match sys.stdout (%s).'
408
+ % (self .tee ._stdout , sys .stdout )
409
+ )
410
+ if self .tee ._stderr is not None and self .tee .STDERR is not sys .stderr :
411
+ FAIL .append (
412
+ 'Captured output (%s) does not match sys.stderr (%s).'
413
+ % (self .tee ._stdout , sys .stdout )
414
+ )
373
415
# Exit all context managers. This includes
374
416
# - Restore any file descriptors we commandeered
375
417
# - Close / join the TeeStream
@@ -449,8 +491,9 @@ def close(self):
449
491
# Close both the file and the underlying file descriptor. Note
450
492
# that this may get called more than once.
451
493
if self .write_file is not None :
452
- self .write_file .flush ()
453
- self .write_file .close ()
494
+ if not self .write_file .closed :
495
+ self .write_file .flush ()
496
+ self .write_file .close ()
454
497
self .write_file = None
455
498
456
499
if self .write_pipe is not None :
@@ -572,6 +615,7 @@ def __init__(self, *ostreams, encoding=None, buffering=-1):
572
615
self ._handles = []
573
616
self ._active_handles = []
574
617
self ._threads = []
618
+ self ._enter_count = 0
575
619
576
620
@property
577
621
def STDOUT (self ):
@@ -634,7 +678,10 @@ def close(self, in_exception=False):
634
678
if _poll_timeout <= _poll < 2 * _poll_timeout :
635
679
if in_exception :
636
680
# We are already processing an exception: no reason
637
- # to trigger another, nor to deadlock for an extended time
681
+ # to trigger another, nor to deadlock for an
682
+ # extended time. Silently clean everything up
683
+ # (because emitting logger messages could trigger
684
+ # yet another exception and mask the true cause).
638
685
break
639
686
logger .warning (
640
687
"Significant delay observed waiting to join reader "
@@ -659,9 +706,13 @@ def close(self, in_exception=False):
659
706
raise RuntimeError ("TeeStream: deadlock observed joining reader threads" )
660
707
661
708
def __enter__ (self ):
709
+ self ._enter_count += 1
662
710
return self
663
711
664
712
def __exit__ (self , et , ev , tb ):
713
+ if not self ._enter_count :
714
+ raise RuntimeError ("TeeStream: exiting a context that was not entered" )
715
+ self ._enter_count -= 1
665
716
self .close (et is not None )
666
717
667
718
def __del__ (self ):
0 commit comments