Skip to content

Commit 637c35c

Browse files
committed
Try to capture stdout and err going directly to 1/2 filedescriptor.
This try to fix a long standing issue that stdout and stderr going directly to the filedescriptor are not shown in notebooks. This is annoying when using wrappers around c-libraries, or calling system commands as those will not be seen from within notebook. Here we redirect and split the filedescriptor and watch those in threads and redirect both to the original FD (terminal), and ZMQ (notebook). Thus output sent to fd 1 & 2 will be shown BOTH in terminal that launched the notebook server and in notebook themselves. One of the concern is that now logs and errors internal to ipykernel may appear in the notebook themselves, so may confuse user; though these should be limited to error and debug; and we can workaround this by setting the log handler to not be stdout/err. This still seem like a big hack to me, and I don't like thread. I did not manage to make reading the FD non-blocking; so this cannot be put in the io-thread – at least I'm not sure how. So adds 2 extra threads to the kernel. This might need to be turn off by default for now until further testing. Locally this seem to work with things like: - os.system("echo HELLO WORLD") - c-extensions writing directly to fd 1 and 2 (when properly flushed). I have no clue how filedescriptor work on windows, so this only change behavior on linux and mac.
1 parent 7e85756 commit 637c35c

File tree

2 files changed

+73
-6
lines changed

2 files changed

+73
-6
lines changed

ipykernel/iostream.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import threading
1313
import warnings
1414
from weakref import WeakSet
15+
import traceback
1516
from io import StringIO, TextIOBase
1617

1718
import zmq
@@ -285,7 +286,44 @@ class OutStream(TextIOBase):
285286
topic = None
286287
encoding = 'UTF-8'
287288

288-
def __init__(self, session, pub_thread, name, pipe=None, echo=None):
289+
def _watch_pipe_fd(self):
290+
"""
291+
We've redirected standards steams 0 and 1 into a pipe.
292+
293+
We need to watch in a thread and redirect them to the right places.
294+
295+
1) the ZMQ channels to show in notebook interfaces,
296+
2) the original stdout/err, to capture errors in terminals.
297+
298+
We cannot schedule this on the ioloop thread, as this might be blocking.
299+
300+
"""
301+
302+
try:
303+
bts = os.read(self._fid, 1000)
304+
while bts and self._should_watch:
305+
self.write(bts.decode())
306+
os.write(self._original_stdstream_copy, bts)
307+
bts = os.read(self._fid, 1000)
308+
except Exception:
309+
self._exc = sys.exc_info()
310+
311+
def __init__(
312+
self, session, pub_thread, name, pipe=None, echo=None, *, watchfd=True
313+
):
314+
"""
315+
Parameters
316+
----------
317+
name : str {'stderr', 'stdout'}
318+
the name of the standard stream to replace
319+
watchfd : bool (default, True)
320+
Watch the file descripttor corresponding to the replaced stream.
321+
This is useful if you know some underlying code will write directly
322+
the file descriptor by its number. It will spawn a watching thread,
323+
that will swap the give file descriptor for a pipe, read from the
324+
pipe, and insert this into the current Stream.
325+
326+
"""
289327
if pipe is not None:
290328
warnings.warn(
291329
"pipe argument to OutStream is deprecated and ignored",
@@ -297,8 +335,12 @@ def __init__(self, session, pub_thread, name, pipe=None, echo=None):
297335
self.session = session
298336
if not isinstance(pub_thread, IOPubThread):
299337
# Backward-compat: given socket, not thread. Wrap in a thread.
300-
warnings.warn("OutStream should be created with IOPubThread, not %r" % pub_thread,
301-
DeprecationWarning, stacklevel=2)
338+
warnings.warn(
339+
"Since IPykernel 4.3, OutStream should be created with "
340+
"IOPubThread, not %r" % pub_thread,
341+
DeprecationWarning,
342+
stacklevel=2,
343+
)
302344
pub_thread = IOPubThread(pub_thread)
303345
pub_thread.start()
304346
self.pub_thread = pub_thread
@@ -312,19 +354,44 @@ def __init__(self, session, pub_thread, name, pipe=None, echo=None):
312354
self._new_buffer()
313355
self.echo = None
314356

357+
if watchfd and (
358+
sys.platform.startswith("linux") or sys.platform.startswith("darwin")
359+
):
360+
self._should_watch = True
361+
self._setup_stream_redirects(name)
362+
315363
if echo:
316364
if hasattr(echo, 'read') and hasattr(echo, 'write'):
317365
self.echo = echo
318366
else:
319367
raise ValueError("echo argument must be a file like object")
320368

369+
def _setup_stream_redirects(self, name):
370+
pr, pw = os.pipe()
371+
fno = getattr(sys, name).fileno()
372+
self._original_stdstream_copy = os.dup(fno)
373+
os.dup2(pw, fno)
374+
375+
self._fid = pr
376+
377+
self._exc = None
378+
self.watch_fd_thread = threading.Thread(target=self._watch_pipe_fd)
379+
self.watch_fd_thread.daemon = True
380+
self.watch_fd_thread.start()
381+
321382
def _is_master_process(self):
322383
return os.getpid() == self._master_pid
323384

324385
def set_parent(self, parent):
325386
self.parent_header = extract_header(parent)
326387

327388
def close(self):
389+
if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
390+
self._should_watch = False
391+
self.watch_fd_thread.join()
392+
if self._exc:
393+
etype, value, tb = self._exc
394+
traceback.print_exception(etype, value, tb)
328395
self.pub_thread = None
329396

330397
@property

ipykernel/kernelbase.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,9 +825,9 @@ def _send_abort_reply(self, stream, msg, idents):
825825
"""Send a reply to an aborted request"""
826826
self.log.info("Aborting:")
827827
self.log.info("%s", msg)
828-
reply_type = msg['header']['msg_type'].rsplit('_', 1)[0] + '_reply'
829-
status = {'status': 'aborted'}
830-
md = {'engine': self.ident}
828+
reply_type = msg["header"]["msg_type"].rsplit("_", 1)[0] + "_reply"
829+
status = {"status": "aborted"}
830+
md = {"engine": self.ident}
831831
md.update(status)
832832
self.session.send(
833833
stream, reply_type, metadata=md,

0 commit comments

Comments
 (0)