@@ -513,49 +513,57 @@ class FDCaptureBinary:
513
513
514
514
def __init__ (self , targetfd , tmpfile = None ):
515
515
self .targetfd = targetfd
516
+
516
517
try :
517
- self . targetfd_save = os .dup ( self . targetfd )
518
+ os .fstat ( targetfd )
518
519
except OSError :
519
- self .start = lambda : None
520
- self .done = lambda : None
520
+ # FD capturing is conceptually simple -- create a temporary file,
521
+ # redirect the FD to it, redirect back when done. But when the
522
+ # target FD is invalid it throws a wrench into this loveley scheme.
523
+ #
524
+ # Tests themselves shouldn't care if the FD is valid, FD capturing
525
+ # should work regardless of external circumstances. So falling back
526
+ # to just sys capturing is not a good option.
527
+ #
528
+ # Further complications are the need to support suspend() and the
529
+ # possibility of FD reuse (e.g. the tmpfile getting the very same
530
+ # target FD). The following approach is robust, I believe.
531
+ self .targetfd_invalid = os .open (os .devnull , os .O_RDWR )
532
+ os .dup2 (self .targetfd_invalid , targetfd )
533
+ else :
534
+ self .targetfd_invalid = None
535
+ self .targetfd_save = os .dup (targetfd )
536
+
537
+ if targetfd == 0 :
538
+ assert not tmpfile , "cannot set tmpfile with stdin"
539
+ tmpfile = open (os .devnull )
540
+ self .syscapture = SysCapture (targetfd )
521
541
else :
522
- self .start = self ._start
523
- self .done = self ._done
524
- if targetfd == 0 :
525
- assert not tmpfile , "cannot set tmpfile with stdin"
526
- tmpfile = open (os .devnull )
527
- self .syscapture = SysCapture (targetfd )
542
+ if tmpfile is None :
543
+ tmpfile = EncodedFile (
544
+ TemporaryFile (buffering = 0 ),
545
+ encoding = "utf-8" ,
546
+ errors = "replace" ,
547
+ write_through = True ,
548
+ )
549
+ if targetfd in patchsysdict :
550
+ self .syscapture = SysCapture (targetfd , tmpfile )
528
551
else :
529
- if tmpfile is None :
530
- tmpfile = EncodedFile (
531
- TemporaryFile (buffering = 0 ),
532
- encoding = "utf-8" ,
533
- errors = "replace" ,
534
- write_through = True ,
535
- )
536
- if targetfd in patchsysdict :
537
- self .syscapture = SysCapture (targetfd , tmpfile )
538
- else :
539
- self .syscapture = NoCapture ()
540
- self .tmpfile = tmpfile
541
- self .tmpfile_fd = tmpfile .fileno ()
552
+ self .syscapture = NoCapture ()
553
+ self .tmpfile = tmpfile
542
554
543
555
def __repr__ (self ):
544
- return "<{} {} oldfd={} _state={!r} tmpfile={}>" .format (
556
+ return "<{} {} oldfd={} _state={!r} tmpfile={!r }>" .format (
545
557
self .__class__ .__name__ ,
546
558
self .targetfd ,
547
- getattr ( self , " targetfd_save" , "<UNSET>" ) ,
559
+ self . targetfd_save ,
548
560
self ._state ,
549
- hasattr ( self , "tmpfile" ) and repr ( self .tmpfile ) or "<UNSET>" ,
561
+ self .tmpfile ,
550
562
)
551
563
552
- def _start (self ):
564
+ def start (self ):
553
565
""" Start capturing on targetfd using memorized tmpfile. """
554
- try :
555
- os .fstat (self .targetfd_save )
556
- except (AttributeError , OSError ):
557
- raise ValueError ("saved filedescriptor not valid anymore" )
558
- os .dup2 (self .tmpfile_fd , self .targetfd )
566
+ os .dup2 (self .tmpfile .fileno (), self .targetfd )
559
567
self .syscapture .start ()
560
568
self ._state = "started"
561
569
@@ -566,12 +574,15 @@ def snap(self):
566
574
self .tmpfile .truncate ()
567
575
return res
568
576
569
- def _done (self ):
577
+ def done (self ):
570
578
""" stop capturing, restore streams, return original capture file,
571
579
seeked to position zero. """
572
- targetfd_save = self .__dict__ .pop ("targetfd_save" )
573
- os .dup2 (targetfd_save , self .targetfd )
574
- os .close (targetfd_save )
580
+ os .dup2 (self .targetfd_save , self .targetfd )
581
+ os .close (self .targetfd_save )
582
+ if self .targetfd_invalid is not None :
583
+ if self .targetfd_invalid != self .targetfd :
584
+ os .close (self .targetfd )
585
+ os .close (self .targetfd_invalid )
575
586
self .syscapture .done ()
576
587
self .tmpfile .close ()
577
588
self ._state = "done"
@@ -583,7 +594,7 @@ def suspend(self):
583
594
584
595
def resume (self ):
585
596
self .syscapture .resume ()
586
- os .dup2 (self .tmpfile_fd , self .targetfd )
597
+ os .dup2 (self .tmpfile . fileno () , self .targetfd )
587
598
self ._state = "resumed"
588
599
589
600
def writeorg (self , data ):
@@ -609,8 +620,7 @@ def snap(self):
609
620
610
621
def writeorg (self , data ):
611
622
""" write to original file descriptor. """
612
- data = data .encode ("utf-8" ) # XXX use encoding of original stream
613
- os .write (self .targetfd_save , data )
623
+ super ().writeorg (data .encode ("utf-8" )) # XXX use encoding of original stream
614
624
615
625
616
626
class SysCaptureBinary :
0 commit comments