1
+ """Capture C-level FD output on pipes
2
+
3
+ Use `duper.capture` or `duper.redirect_to_sys` as context managers.
4
+ """
5
+ from __future__ import print_function
6
+
7
+ __version__ = '0.0.1'
8
+
9
+ __all__ = [
10
+ 'capture' ,
11
+ 'redirect_to_sys' ,
12
+ 'Duper' ,
13
+ ]
14
+
15
+ from contextlib import contextmanager
16
+ import ctypes
17
+ from fcntl import fcntl , F_GETFL , F_SETFL
18
+ import io
19
+ import os
20
+ import select
21
+ import sys
22
+ import threading
23
+
24
+ libc = ctypes .CDLL (None )
25
+
26
+ try :
27
+ c_stdout_p = ctypes .c_void_p .in_dll (libc , 'stdout' )
28
+ c_stderr_p = ctypes .c_void_p .in_dll (libc , 'stderr' )
29
+ except ValueError :
30
+ # libc.stdout is has a funny name on OS X
31
+ c_stdout_p = ctypes .c_void_p .in_dll (libc , '__stdoutp' )
32
+ c_stderr_p = ctypes .c_void_p .in_dll (libc , '__stderrp' )
33
+
34
+ STDOUT = 2
35
+ PIPE = 3
36
+
37
+ _default_encoding = getattr (sys .stdin , 'encoding' , None ) or 'utf8'
38
+ if _default_encoding .lower () == 'ascii' :
39
+ # don't respect ascii
40
+ _default_encoding = 'utf8'
41
+
42
+ class Duper (object ):
43
+ """Class for Capturing Process-level FD output via dup2
44
+
45
+ Typically used via `duper.capture`
46
+ """
47
+ flush_interval = 0.2
48
+
49
+ def __init__ (self , stdout = None , stderr = None , encoding = _default_encoding ):
50
+ """
51
+ Parameters
52
+ ----------
53
+ stdout: stream or None
54
+ The stream for forwarding stdout.
55
+ stderr = stream or None
56
+ The stream for forwarding stderr.
57
+ encoding: str or None
58
+ The encoding to use, if streams should be interpreted as text.
59
+ """
60
+ self ._stdout = stdout
61
+ if stderr == STDOUT :
62
+ self ._stderr = self ._stdout
63
+ else :
64
+ self ._stderr = stderr
65
+ self .encoding = encoding
66
+ self ._save_fds = {}
67
+ self ._real_fds = {}
68
+ self ._out_pipes = {}
69
+ self ._handlers = {}
70
+ self ._handlers ['stderr' ] = self ._handle_stderr
71
+ self ._handlers ['stdout' ] = self ._handle_stdout
72
+
73
+ def _setup_pipe (self , name ):
74
+ real_fd = getattr (sys , '__%s__' % name ).fileno ()
75
+ save_fd = os .dup (real_fd )
76
+ self ._save_fds [name ] = save_fd
77
+
78
+ pipe_out , pipe_in = os .pipe ()
79
+ os .dup2 (pipe_in , real_fd )
80
+ os .close (pipe_in )
81
+ self ._real_fds [name ] = real_fd
82
+
83
+ # make pipe_out non-blocking
84
+ flags = fcntl (pipe_out , F_GETFL )
85
+ fcntl (pipe_out , F_SETFL , flags | os .O_NONBLOCK )
86
+ return pipe_out
87
+
88
+ def _decode (self , data ):
89
+ """Decode data, if any
90
+
91
+ Called before pasing to stdout/stderr streams
92
+ """
93
+ if self .encoding :
94
+ data = data .decode (self .encoding , 'replace' )
95
+ return data
96
+
97
+ def _handle_stdout (self , data ):
98
+ if self ._stdout :
99
+ self ._stdout .write (self ._decode (data ))
100
+
101
+ def _handle_stderr (self , data ):
102
+ if self ._stderr :
103
+ self ._stderr .write (self ._decode (data ))
104
+
105
+ def _setup_handle (self ):
106
+ """Setup handle for output, if any"""
107
+ self .handle = (self ._stdout , self ._stderr )
108
+
109
+ def _finish_handle (self ):
110
+ """Finish handle, if anything should be done when it's all wrapped up."""
111
+ pass
112
+
113
+ def __enter__ (self ):
114
+ # setup handle
115
+ self ._setup_handle ()
116
+
117
+ # create pipe for stdout
118
+ pipes = []
119
+ names = {}
120
+ if self ._stdout :
121
+ pipe = self ._setup_pipe ('stdout' )
122
+ pipes .append (pipe )
123
+ names [pipe ] = 'stdout'
124
+ if self ._stderr :
125
+ pipe = self ._setup_pipe ('stderr' )
126
+ pipes .append (pipe )
127
+ names [pipe ] = 'stderr'
128
+
129
+ def forwarder ():
130
+ """Forward bytes on a pipe to stream messages"""
131
+ while True :
132
+ # flush libc's buffers before calling select
133
+ libc .fflush (c_stdout_p )
134
+ libc .fflush (c_stderr_p )
135
+ r , w , x = select .select (pipes , [], [], self .flush_interval )
136
+ if not r :
137
+ # nothing to read, next iteration will flush and check again
138
+ continue
139
+ for pipe in r :
140
+ name = names [pipe ]
141
+ data = os .read (pipe , 1024 )
142
+ if not data :
143
+ # pipe closed, stop polling
144
+ pipes .remove (pipe )
145
+ else :
146
+ handler = getattr (self , '_handle_%s' % name )
147
+ handler (data )
148
+ if not pipes :
149
+ # pipes closed, we are done
150
+ break
151
+ self .thread = threading .Thread (target = forwarder )
152
+ self .thread .daemon = True
153
+ self .thread .start ()
154
+
155
+ return self .handle
156
+
157
+ def __exit__ (self , exc_type , exc_value , traceback ):
158
+ # flush the underlying C buffers
159
+ libc .fflush (c_stdout_p )
160
+ libc .fflush (c_stderr_p )
161
+ # close FDs, signaling output is complete
162
+ for real_fd in self ._real_fds .values ():
163
+ os .close (real_fd )
164
+ self .thread .join ()
165
+
166
+ # close finished pipes
167
+ for pipe_out in self ._out_pipes .values ():
168
+ os .close (pipe_out )
169
+
170
+ # restore original state
171
+ for name , real_fd in self ._real_fds .items ():
172
+ save_fd = self ._save_fds [name ]
173
+ os .dup2 (save_fd , real_fd )
174
+ os .close (save_fd )
175
+ # finalize handle
176
+ self ._finish_handle ()
177
+
178
+
179
+ @contextmanager
180
+ def capture (stdout = PIPE , stderr = PIPE , encoding = _default_encoding ):
181
+ """Capture C-level stdout/stderr in a context manager.
182
+
183
+ The return value for the context manager is (stdout, stderr).
184
+
185
+ Examples
186
+ --------
187
+
188
+ >>> with capture() as (stdout, stderr):
189
+ ... printf("C-level stdout")
190
+ ... output = stdout.read()
191
+ """
192
+ stdout_pipe = stderr_pipe = False
193
+ # setup stdout
194
+ if stdout == PIPE :
195
+ stdout_r , stdout_w = os .pipe ()
196
+ stdout_w = os .fdopen (stdout_w , 'wb' )
197
+ stdout_r = os .fdopen (stdout_r , 'rb' )
198
+ if encoding :
199
+ stdout_r = io .TextIOWrapper (stdout_r , encoding = encoding )
200
+ stdout_pipe = True
201
+ else :
202
+ stdout_r = stdout_w = stdout
203
+ # setup stderr
204
+ if stderr == STDOUT :
205
+ stderr_r = None
206
+ stderr_w = stdout_w
207
+ elif stderr == PIPE :
208
+ stderr_r , stderr_w = os .pipe ()
209
+ stderr_w = os .fdopen (stderr_w , 'wb' )
210
+ stderr_r = os .fdopen (stderr_r , 'rb' )
211
+ if encoding :
212
+ stderr_r = io .TextIOWrapper (stderr_r , encoding = encoding )
213
+ stderr_pipe = True
214
+ else :
215
+ stderr_r = stderr_w = stderr
216
+ if stdout_pipe or stderr_pipe :
217
+ capture_encoding = None
218
+ else :
219
+ capture_encoding = encoding
220
+ duper = Duper (stdout = stdout_w , stderr = stderr_w , encoding = capture_encoding )
221
+ try :
222
+ with duper :
223
+ yield stdout_r , stderr_r
224
+ finally :
225
+ # close pipes
226
+ if stdout_pipe :
227
+ stdout_w .close ()
228
+ if stderr_pipe :
229
+ stderr_w .close ()
230
+
231
+
232
+ def redirect_to_sys (encoding = _default_encoding ):
233
+ """Redirect C-level stdout/stderr to sys.stdout/stderr
234
+
235
+ This is useful of sys.sdout/stderr are already being forwarded somewhere.
236
+
237
+ DO NOT USE THIS if sys.stdout and sys.stderr are not already being forwarded.
238
+ """
239
+ return capture (sys .stdout , sys .stderr , encoding = encoding )
240
+
241
+ def redirect_everything_to_sys (encoding = _default_encoding ):
242
+ """Redirect all C output to sys.stdout/err
243
+
244
+ This does *not*
245
+ """
0 commit comments