Skip to content

Commit 316e878

Browse files
committed
Vendor fflush-less wurlitzer.
Based on wurlitzer 0.2.0, but without flushing in the forwarder loop to prevent blocking. Also removed non-essential API.
1 parent bfd8441 commit 316e878

File tree

4 files changed

+200
-3
lines changed

4 files changed

+200
-3
lines changed

LICENSE.txt

+25
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,28 @@ Redistribution and use in source and binary forms, with or without modification,
1010
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
1111

1212
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13+
14+
15+
wurlitzer was originally published under the following license:
16+
17+
The MIT License (MIT)
18+
19+
Copyright (c) 2016 Min RK
20+
21+
Permission is hereby granted, free of charge, to any person obtaining a copy
22+
of this software and associated documentation files (the "Software"), to deal
23+
in the Software without restriction, including without limitation the rights
24+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25+
copies of the Software, and to permit persons to whom the Software is
26+
furnished to do so, subject to the following conditions:
27+
28+
The above copyright notice and this permission notice shall be included in all
29+
copies or substantial portions of the Software.
30+
31+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37+
SOFTWARE.

matlab_kernel/kernel.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
from IPython.display import Image
1010
from metakernel import MetaKernel
11-
from wurlitzer import Wurlitzer
1211

1312
import matlab.engine
1413
from matlab.engine import MatlabExecutionError
1514

1615
from . import __version__
16+
from .wurlitzer import Wurlitzer
1717

1818

1919
class _PseudoStream:

matlab_kernel/wurlitzer.py

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Capture C-level FD output on pipes
2+
3+
Use `wurlitzer.pipes` or `wurlitzer.sys_pipes` as context managers.
4+
"""
5+
from __future__ import print_function
6+
7+
__version__ = '0.2.1.dev'
8+
9+
__all__ = [
10+
'Wurlitzer',
11+
]
12+
13+
from contextlib import contextmanager
14+
import ctypes
15+
from fcntl import fcntl, F_GETFL, F_SETFL
16+
import io
17+
import os
18+
import select
19+
import sys
20+
import threading
21+
22+
libc = ctypes.CDLL(None)
23+
24+
try:
25+
c_stdout_p = ctypes.c_void_p.in_dll(libc, 'stdout')
26+
c_stderr_p = ctypes.c_void_p.in_dll(libc, 'stderr')
27+
except ValueError: # pragma: no cover
28+
# libc.stdout is has a funny name on OS X
29+
c_stdout_p = ctypes.c_void_p.in_dll(libc, '__stdoutp') # pragma: no cover
30+
c_stderr_p = ctypes.c_void_p.in_dll(libc, '__stderrp') # pragma: no cover
31+
32+
STDOUT = 2
33+
PIPE = 3
34+
35+
_default_encoding = getattr(sys.stdin, 'encoding', None) or 'utf8'
36+
if _default_encoding.lower() == 'ascii':
37+
# don't respect ascii
38+
_default_encoding = 'utf8' # pragma: no cover
39+
40+
class Wurlitzer(object):
41+
"""Class for Capturing Process-level FD output via dup2
42+
43+
Typically used via `wurlitzer.capture`
44+
"""
45+
flush_interval = 0.2
46+
47+
def __init__(self, stdout=None, stderr=None, encoding=_default_encoding):
48+
"""
49+
Parameters
50+
----------
51+
stdout: stream or None
52+
The stream for forwarding stdout.
53+
stderr = stream or None
54+
The stream for forwarding stderr.
55+
encoding: str or None
56+
The encoding to use, if streams should be interpreted as text.
57+
"""
58+
self._stdout = stdout
59+
if stderr == STDOUT:
60+
self._stderr = self._stdout
61+
else:
62+
self._stderr = stderr
63+
self.encoding = encoding
64+
self._save_fds = {}
65+
self._real_fds = {}
66+
self._handlers = {}
67+
self._handlers['stderr'] = self._handle_stderr
68+
self._handlers['stdout'] = self._handle_stdout
69+
70+
def _setup_pipe(self, name):
71+
real_fd = getattr(sys, '__%s__' % name).fileno()
72+
save_fd = os.dup(real_fd)
73+
self._save_fds[name] = save_fd
74+
75+
pipe_out, pipe_in = os.pipe()
76+
os.dup2(pipe_in, real_fd)
77+
os.close(pipe_in)
78+
self._real_fds[name] = real_fd
79+
80+
# make pipe_out non-blocking
81+
flags = fcntl(pipe_out, F_GETFL)
82+
fcntl(pipe_out, F_SETFL, flags|os.O_NONBLOCK)
83+
return pipe_out
84+
85+
def _decode(self, data):
86+
"""Decode data, if any
87+
88+
Called before pasing to stdout/stderr streams
89+
"""
90+
if self.encoding:
91+
data = data.decode(self.encoding, 'replace')
92+
return data
93+
94+
def _handle_stdout(self, data):
95+
if self._stdout:
96+
self._stdout.write(self._decode(data))
97+
98+
def _handle_stderr(self, data):
99+
if self._stderr:
100+
self._stderr.write(self._decode(data))
101+
102+
def _setup_handle(self):
103+
"""Setup handle for output, if any"""
104+
self.handle = (self._stdout, self._stderr)
105+
106+
def _finish_handle(self):
107+
"""Finish handle, if anything should be done when it's all wrapped up."""
108+
pass
109+
110+
def __enter__(self):
111+
# flush anything out before starting
112+
libc.fflush(c_stdout_p)
113+
libc.fflush(c_stderr_p)
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+
# See Calysto/metakernel#5: flushing sometimes blocks.
134+
# libc.fflush(c_stdout_p)
135+
# libc.fflush(c_stderr_p)
136+
r, w, x = select.select(pipes, [], [], self.flush_interval)
137+
if not r:
138+
# nothing to read, next iteration will flush and check again
139+
continue
140+
for pipe in r:
141+
name = names[pipe]
142+
data = os.read(pipe, 1024)
143+
if not data:
144+
# pipe closed, stop polling
145+
pipes.remove(pipe)
146+
else:
147+
handler = getattr(self, '_handle_%s' % name)
148+
handler(data)
149+
if not pipes:
150+
# pipes closed, we are done
151+
break
152+
self.thread = threading.Thread(target=forwarder)
153+
self.thread.daemon = True
154+
self.thread.start()
155+
156+
return self.handle
157+
158+
def __exit__(self, exc_type, exc_value, traceback):
159+
# flush the underlying C buffers
160+
libc.fflush(c_stdout_p)
161+
libc.fflush(c_stderr_p)
162+
# close FDs, signaling output is complete
163+
for real_fd in self._real_fds.values():
164+
os.close(real_fd)
165+
self.thread.join()
166+
167+
# restore original state
168+
for name, real_fd in self._real_fds.items():
169+
save_fd = self._save_fds[name]
170+
os.dup2(save_fd, real_fd)
171+
os.close(save_fd)
172+
# finalize handle
173+
self._finish_handle()

setup.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,5 @@
2121
"Programming Language :: Python :: 3.5",
2222
"Topic :: System :: Shells",],
2323
packages=find_packages(include=["matlab_kernel", "matlab_kernel.*"]),
24-
install_requires=["metakernel>=0.13.1",
25-
"wurlitzer>=0.2.0",],
24+
install_requires=["metakernel>=0.13.1"],
2625
)

0 commit comments

Comments
 (0)