kernelapp.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. """An Application for launching a kernel"""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import annotations
  5. import atexit
  6. import errno
  7. import logging
  8. import os
  9. import signal
  10. import sys
  11. import traceback
  12. import typing as t
  13. from functools import partial
  14. from io import FileIO, TextIOWrapper
  15. from logging import StreamHandler
  16. from pathlib import Path
  17. import zmq
  18. from IPython.core.application import ( # type:ignore[attr-defined]
  19. BaseIPythonApplication,
  20. base_aliases,
  21. base_flags,
  22. catch_config_error,
  23. )
  24. from IPython.core.profiledir import ProfileDir
  25. from IPython.core.shellapp import InteractiveShellApp, shell_aliases, shell_flags
  26. from jupyter_client.connect import ConnectionFileMixin
  27. from jupyter_client.session import Session, session_aliases, session_flags
  28. from jupyter_core.paths import jupyter_runtime_dir
  29. from tornado import ioloop
  30. from traitlets.traitlets import (
  31. Any,
  32. Bool,
  33. Dict,
  34. DottedObjectName,
  35. Instance,
  36. Integer,
  37. Type,
  38. Unicode,
  39. default,
  40. )
  41. from traitlets.utils import filefind
  42. from traitlets.utils.importstring import import_item
  43. from zmq.eventloop.zmqstream import ZMQStream
  44. from .connect import get_connection_info, write_connection_file
  45. # local imports
  46. from .control import ControlThread
  47. from .heartbeat import Heartbeat
  48. from .iostream import IOPubThread
  49. from .ipkernel import IPythonKernel
  50. from .parentpoller import ParentPollerUnix, ParentPollerWindows
  51. from .shellchannel import ShellChannelThread
  52. from .zmqshell import ZMQInteractiveShell
  53. # -----------------------------------------------------------------------------
  54. # Flags and Aliases
  55. # -----------------------------------------------------------------------------
  56. kernel_aliases = dict(base_aliases)
  57. kernel_aliases.update(
  58. {
  59. "ip": "IPKernelApp.ip",
  60. "hb": "IPKernelApp.hb_port",
  61. "shell": "IPKernelApp.shell_port",
  62. "iopub": "IPKernelApp.iopub_port",
  63. "stdin": "IPKernelApp.stdin_port",
  64. "control": "IPKernelApp.control_port",
  65. "f": "IPKernelApp.connection_file",
  66. "transport": "IPKernelApp.transport",
  67. }
  68. )
  69. kernel_flags = dict(base_flags)
  70. kernel_flags.update(
  71. {
  72. "no-stdout": ({"IPKernelApp": {"no_stdout": True}}, "redirect stdout to the null device"),
  73. "no-stderr": ({"IPKernelApp": {"no_stderr": True}}, "redirect stderr to the null device"),
  74. "pylab": (
  75. {"IPKernelApp": {"pylab": "auto"}},
  76. """Pre-load matplotlib and numpy for interactive use with
  77. the default matplotlib backend.""",
  78. ),
  79. "trio-loop": (
  80. {"InteractiveShell": {"trio_loop": False}},
  81. "Enable Trio as main event loop.",
  82. ),
  83. }
  84. )
  85. # inherit flags&aliases for any IPython shell apps
  86. kernel_aliases.update(shell_aliases)
  87. kernel_flags.update(shell_flags)
  88. # inherit flags&aliases for Sessions
  89. kernel_aliases.update(session_aliases)
  90. kernel_flags.update(session_flags)
  91. _ctrl_c_message = """\
  92. NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
  93. To exit, you will have to explicitly quit this process, by either sending
  94. "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
  95. To read more about this, see https://github.com/ipython/ipython/issues/2049
  96. """
  97. # -----------------------------------------------------------------------------
  98. # Application class for starting an IPython Kernel
  99. # -----------------------------------------------------------------------------
  100. class IPKernelApp(BaseIPythonApplication, InteractiveShellApp, ConnectionFileMixin):
  101. """The IPYKernel application class."""
  102. name = "ipython-kernel"
  103. aliases = Dict(kernel_aliases)
  104. flags = Dict(kernel_flags)
  105. classes = [IPythonKernel, ZMQInteractiveShell, ProfileDir, Session] # type:ignore[assignment]
  106. # the kernel class, as an importstring
  107. kernel_class = Type(
  108. "ipykernel.ipkernel.IPythonKernel",
  109. klass="ipykernel.kernelbase.Kernel",
  110. help="""The Kernel subclass to be used.
  111. This should allow easy reuse of the IPKernelApp entry point
  112. to configure and launch kernels other than IPython's own.
  113. """,
  114. ).tag(config=True)
  115. kernel = Any()
  116. poller = Any() # don't restrict this even though current pollers are all Threads
  117. heartbeat = Instance(Heartbeat, allow_none=True)
  118. context: zmq.Context[t.Any] | None = Any() # type:ignore[assignment]
  119. shell_socket = Any()
  120. control_socket = Any()
  121. debugpy_socket = Any()
  122. debug_shell_socket = Any()
  123. stdin_socket = Any()
  124. iopub_socket = Any()
  125. iopub_thread = Any()
  126. control_thread = Any()
  127. shell_channel_thread = Any()
  128. _ports = Dict()
  129. subcommands = {
  130. "install": (
  131. "ipykernel.kernelspec.InstallIPythonKernelSpecApp",
  132. "Install the IPython kernel",
  133. ),
  134. }
  135. # connection info:
  136. connection_dir = Unicode()
  137. @default("connection_dir")
  138. def _default_connection_dir(self):
  139. return jupyter_runtime_dir()
  140. @property
  141. def abs_connection_file(self):
  142. if Path(self.connection_file).name == self.connection_file and self.connection_dir:
  143. return str(Path(str(self.connection_dir)) / self.connection_file)
  144. return self.connection_file
  145. # streams, etc.
  146. no_stdout = Bool(False, help="redirect stdout to the null device").tag(config=True)
  147. no_stderr = Bool(False, help="redirect stderr to the null device").tag(config=True)
  148. trio_loop = Bool(False, help="Set main event loop.").tag(config=True)
  149. quiet = Bool(True, help="Only send stdout/stderr to output stream").tag(config=True)
  150. outstream_class = DottedObjectName(
  151. "ipykernel.iostream.OutStream",
  152. help="The importstring for the OutStream factory",
  153. allow_none=True,
  154. ).tag(config=True)
  155. displayhook_class = DottedObjectName(
  156. "ipykernel.displayhook.ZMQDisplayHook", help="The importstring for the DisplayHook factory"
  157. ).tag(config=True)
  158. capture_fd_output = Bool(
  159. True,
  160. help="""Attempt to capture and forward low-level output, e.g. produced by Extension libraries.
  161. """,
  162. ).tag(config=True)
  163. # polling
  164. parent_handle = Integer(
  165. int(os.environ.get("JPY_PARENT_PID") or 0),
  166. help="""kill this process if its parent dies. On Windows, the argument
  167. specifies the HANDLE of the parent process, otherwise it is simply boolean.
  168. """,
  169. ).tag(config=True)
  170. interrupt = Integer(
  171. int(os.environ.get("JPY_INTERRUPT_EVENT") or 0),
  172. help="""ONLY USED ON WINDOWS
  173. Interrupt this process when the parent is signaled.
  174. """,
  175. ).tag(config=True)
  176. def init_crash_handler(self):
  177. """Initialize the crash handler."""
  178. sys.excepthook = self.excepthook
  179. def excepthook(self, etype, evalue, tb):
  180. """Handle an exception."""
  181. # write uncaught traceback to 'real' stderr, not zmq-forwarder
  182. traceback.print_exception(etype, evalue, tb, file=sys.__stderr__)
  183. def init_poller(self):
  184. """Initialize the poller."""
  185. if sys.platform == "win32":
  186. if self.interrupt or self.parent_handle:
  187. self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
  188. elif self.parent_handle and self.parent_handle != 1:
  189. # PID 1 (init) is special and will never go away,
  190. # only be reassigned.
  191. # Parent polling doesn't work if ppid == 1 to start with.
  192. self.poller = ParentPollerUnix(parent_pid=self.parent_handle)
  193. def _try_bind_socket(self, s, port):
  194. iface = f"{self.transport}://{self.ip}"
  195. if self.transport == "tcp":
  196. if port <= 0:
  197. port = s.bind_to_random_port(iface)
  198. else:
  199. s.bind("tcp://%s:%i" % (self.ip, port))
  200. elif self.transport == "ipc":
  201. if port <= 0:
  202. port = 1
  203. path = "%s-%i" % (self.ip, port)
  204. while Path(path).exists():
  205. port = port + 1
  206. path = "%s-%i" % (self.ip, port)
  207. else:
  208. path = "%s-%i" % (self.ip, port)
  209. s.bind("ipc://%s" % path)
  210. return port
  211. def _bind_socket(self, s, port):
  212. try:
  213. win_in_use = errno.WSAEADDRINUSE # type:ignore[attr-defined]
  214. except AttributeError:
  215. win_in_use = None
  216. # Try up to 100 times to bind a port when in conflict to avoid
  217. # infinite attempts in bad setups
  218. max_attempts = 1 if port else 100
  219. for attempt in range(max_attempts):
  220. try:
  221. return self._try_bind_socket(s, port)
  222. except zmq.ZMQError as ze:
  223. # Raise if we have any error not related to socket binding
  224. if ze.errno != errno.EADDRINUSE and ze.errno != win_in_use:
  225. raise
  226. if attempt == max_attempts - 1:
  227. raise
  228. return None
  229. def write_connection_file(self, **kwargs: Any) -> None:
  230. """write connection info to JSON file"""
  231. cf = self.abs_connection_file
  232. connection_info = dict(
  233. ip=self.ip,
  234. key=self.session.key,
  235. transport=self.transport,
  236. shell_port=self.shell_port,
  237. stdin_port=self.stdin_port,
  238. hb_port=self.hb_port,
  239. iopub_port=self.iopub_port,
  240. control_port=self.control_port,
  241. )
  242. if Path(cf).exists():
  243. # If the file exists, merge our info into it. For example, if the
  244. # original file had port number 0, we update with the actual port
  245. # used.
  246. existing_connection_info = get_connection_info(cf, unpack=True)
  247. assert isinstance(existing_connection_info, dict)
  248. connection_info = dict(existing_connection_info, **connection_info)
  249. if connection_info == existing_connection_info:
  250. self.log.debug("Connection file %s with current information already exists", cf)
  251. return
  252. self.log.debug("Writing connection file: %s", cf)
  253. write_connection_file(cf, **connection_info)
  254. def cleanup_connection_file(self):
  255. """Clean up our connection file."""
  256. cf = self.abs_connection_file
  257. self.log.debug("Cleaning up connection file: %s", cf)
  258. try:
  259. Path(cf).unlink()
  260. except OSError:
  261. pass
  262. self.cleanup_ipc_files()
  263. def init_connection_file(self):
  264. """Initialize our connection file."""
  265. if not self.connection_file:
  266. self.connection_file = "kernel-%s.json" % os.getpid()
  267. try:
  268. self.connection_file = filefind(self.connection_file, [".", self.connection_dir])
  269. except OSError:
  270. self.log.debug("Connection file not found: %s", self.connection_file)
  271. # This means I own it, and I'll create it in this directory:
  272. Path(self.abs_connection_file).parent.mkdir(mode=0o700, exist_ok=True, parents=True)
  273. # Also, I will clean it up:
  274. atexit.register(self.cleanup_connection_file)
  275. return
  276. try:
  277. self.load_connection_file()
  278. except Exception:
  279. self.log.error( # noqa: G201
  280. "Failed to load connection file: %r", self.connection_file, exc_info=True
  281. )
  282. self.exit(1)
  283. def init_sockets(self):
  284. """Create a context, a session, and the kernel sockets."""
  285. self.log.info("Starting the kernel at pid: %i", os.getpid())
  286. assert self.context is None, "init_sockets cannot be called twice!"
  287. self.context = context = zmq.Context()
  288. atexit.register(self.close)
  289. self.shell_socket = context.socket(zmq.ROUTER)
  290. self.shell_socket.linger = 1000
  291. self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
  292. self.log.debug("shell ROUTER Channel on port: %i", self.shell_port)
  293. self.stdin_socket = context.socket(zmq.ROUTER)
  294. self.stdin_socket.linger = 1000
  295. self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
  296. self.log.debug("stdin ROUTER Channel on port: %i", self.stdin_port)
  297. if hasattr(zmq, "ROUTER_HANDOVER"):
  298. # set router-handover to workaround zeromq reconnect problems
  299. # in certain rare circumstances
  300. # see ipython/ipykernel#270 and zeromq/libzmq#2892
  301. self.shell_socket.router_handover = self.stdin_socket.router_handover = 1
  302. self.init_control(context)
  303. self.init_iopub(context)
  304. def init_control(self, context):
  305. """Initialize the control channel."""
  306. self.control_socket = context.socket(zmq.ROUTER)
  307. self.control_socket.linger = 1000
  308. self.control_port = self._bind_socket(self.control_socket, self.control_port)
  309. self.log.debug("control ROUTER Channel on port: %i", self.control_port)
  310. self.debugpy_socket = context.socket(zmq.STREAM)
  311. self.debugpy_socket.linger = 1000
  312. self.debug_shell_socket = context.socket(zmq.DEALER)
  313. self.debug_shell_socket.linger = 1000
  314. if self.shell_socket.getsockopt(zmq.LAST_ENDPOINT):
  315. self.debug_shell_socket.connect(self.shell_socket.getsockopt(zmq.LAST_ENDPOINT))
  316. if hasattr(zmq, "ROUTER_HANDOVER"):
  317. # set router-handover to workaround zeromq reconnect problems
  318. # in certain rare circumstances
  319. # see ipython/ipykernel#270 and zeromq/libzmq#2892
  320. self.control_socket.router_handover = 1
  321. self.control_thread = ControlThread(daemon=True)
  322. self.shell_channel_thread = ShellChannelThread(
  323. context,
  324. self.shell_socket,
  325. daemon=True,
  326. )
  327. def init_iopub(self, context):
  328. """Initialize the iopub channel."""
  329. self.iopub_socket = context.socket(zmq.XPUB)
  330. self.iopub_socket.linger = 1000
  331. self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
  332. self.log.debug("iopub PUB Channel on port: %i", self.iopub_port)
  333. self.configure_tornado_logger()
  334. self.iopub_thread = IOPubThread(self.iopub_socket, pipe=True, session=self.session)
  335. self.iopub_thread.start()
  336. # backward-compat: wrap iopub socket API in background thread
  337. self.iopub_socket = self.iopub_thread.background_socket
  338. def init_heartbeat(self):
  339. """start the heart beating"""
  340. # heartbeat doesn't share context, because it mustn't be blocked
  341. # by the GIL, which is accessed by libzmq when freeing zero-copy messages
  342. hb_ctx = zmq.Context()
  343. self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
  344. self.hb_port = self.heartbeat.port
  345. self.log.debug("Heartbeat REP Channel on port: %i", self.hb_port)
  346. self.heartbeat.start()
  347. def close(self):
  348. """Close zmq sockets in an orderly fashion"""
  349. # un-capture IO before we start closing channels
  350. self.reset_io()
  351. self.log.info("Cleaning up sockets")
  352. if self.heartbeat:
  353. self.log.debug("Closing heartbeat channel")
  354. self.heartbeat.context.term()
  355. if self.iopub_thread:
  356. self.log.debug("Closing iopub channel")
  357. self.iopub_thread.stop()
  358. self.iopub_thread.close()
  359. if self.control_thread and self.control_thread.is_alive():
  360. self.log.debug("Closing control thread")
  361. self.control_thread.stop()
  362. self.control_thread.join()
  363. if self.shell_channel_thread and self.shell_channel_thread.is_alive():
  364. self.log.debug("Closing shell channel thread")
  365. self.shell_channel_thread.stop()
  366. self.shell_channel_thread.join()
  367. if self.debugpy_socket and not self.debugpy_socket.closed:
  368. self.debugpy_socket.close()
  369. if self.debug_shell_socket and not self.debug_shell_socket.closed:
  370. self.debug_shell_socket.close()
  371. for channel in ("shell", "control", "stdin"):
  372. self.log.debug("Closing %s channel", channel)
  373. socket = getattr(self, channel + "_socket", None)
  374. if socket and not socket.closed:
  375. socket.close()
  376. self.log.debug("Terminating zmq context")
  377. if self.context:
  378. self.context.term()
  379. self.log.debug("Terminated zmq context")
  380. def log_connection_info(self):
  381. """display connection info, and store ports"""
  382. basename = Path(self.connection_file).name
  383. if (
  384. basename == self.connection_file
  385. or str(Path(self.connection_file).parent) == self.connection_dir
  386. ):
  387. # use shortname
  388. tail = basename
  389. else:
  390. tail = self.connection_file
  391. lines = [
  392. "To connect another client to this kernel, use:",
  393. " --existing %s" % tail,
  394. ]
  395. # log connection info
  396. # info-level, so often not shown.
  397. # frontends should use the %connect_info magic
  398. # to see the connection info
  399. for line in lines:
  400. self.log.info(line)
  401. # also raw print to the terminal if no parent_handle (`ipython kernel`)
  402. # unless log-level is CRITICAL (--quiet)
  403. if not self.parent_handle and int(self.log_level) < logging.CRITICAL: # type:ignore[call-overload]
  404. print(_ctrl_c_message, file=sys.__stdout__)
  405. for line in lines:
  406. print(line, file=sys.__stdout__)
  407. self._ports = dict(
  408. shell=self.shell_port,
  409. iopub=self.iopub_port,
  410. stdin=self.stdin_port,
  411. hb=self.hb_port,
  412. control=self.control_port,
  413. )
  414. def init_blackhole(self):
  415. """redirects stdout/stderr to devnull if necessary"""
  416. if self.no_stdout or self.no_stderr:
  417. blackhole = open(os.devnull, "w") # noqa: SIM115
  418. if self.no_stdout:
  419. sys.stdout = sys.__stdout__ = blackhole # type:ignore[misc]
  420. if self.no_stderr:
  421. sys.stderr = sys.__stderr__ = blackhole # type:ignore[misc]
  422. def init_io(self):
  423. """Redirect input streams and set a display hook."""
  424. if self.outstream_class:
  425. outstream_factory = import_item(str(self.outstream_class))
  426. if sys.stdout is not None:
  427. sys.stdout.flush()
  428. e_stdout = None if self.quiet else sys.__stdout__
  429. e_stderr = None if self.quiet else sys.__stderr__
  430. if not self.capture_fd_output:
  431. outstream_factory = partial(outstream_factory, watchfd=False)
  432. sys.stdout = outstream_factory(self.session, self.iopub_thread, "stdout", echo=e_stdout)
  433. if sys.stderr is not None:
  434. sys.stderr.flush()
  435. sys.stderr = outstream_factory(self.session, self.iopub_thread, "stderr", echo=e_stderr)
  436. if hasattr(sys.stderr, "_original_stdstream_copy"):
  437. for handler in self.log.handlers:
  438. if isinstance(handler, StreamHandler) and (handler.stream.buffer.fileno() == 2):
  439. self.log.debug("Seeing logger to stderr, rerouting to raw filedescriptor.")
  440. handler.stream = TextIOWrapper(
  441. FileIO(
  442. sys.stderr._original_stdstream_copy,
  443. "w",
  444. )
  445. )
  446. if self.displayhook_class:
  447. displayhook_factory = import_item(str(self.displayhook_class))
  448. self.displayhook = displayhook_factory(self.session, self.iopub_socket)
  449. sys.displayhook = self.displayhook
  450. self.patch_io()
  451. def reset_io(self):
  452. """restore original io
  453. restores state after init_io
  454. """
  455. sys.stdout = sys.__stdout__
  456. sys.stderr = sys.__stderr__
  457. sys.displayhook = sys.__displayhook__
  458. def patch_io(self):
  459. """Patch important libraries that can't handle sys.stdout forwarding"""
  460. try:
  461. import faulthandler
  462. except ImportError:
  463. pass
  464. else:
  465. # Warning: this is a monkeypatch of `faulthandler.enable`, watch for possible
  466. # updates to the upstream API and update accordingly (up-to-date as of Python 3.5):
  467. # https://docs.python.org/3/library/faulthandler.html#faulthandler.enable
  468. # change default file to __stderr__ from forwarded stderr
  469. faulthandler_enable = faulthandler.enable
  470. def enable(file=sys.__stderr__, all_threads=True, **kwargs):
  471. return faulthandler_enable(file=file, all_threads=all_threads, **kwargs)
  472. faulthandler.enable = enable
  473. if hasattr(faulthandler, "register"):
  474. faulthandler_register = faulthandler.register
  475. def register(signum, file=sys.__stderr__, all_threads=True, chain=False, **kwargs):
  476. return faulthandler_register(
  477. signum, file=file, all_threads=all_threads, chain=chain, **kwargs
  478. )
  479. faulthandler.register = register
  480. def init_signal(self):
  481. """Initialize the signal handler."""
  482. signal.signal(signal.SIGINT, signal.SIG_IGN)
  483. def init_kernel(self):
  484. """Create the Kernel object itself"""
  485. if self.shell_channel_thread:
  486. shell_stream = ZMQStream(self.shell_socket, self.shell_channel_thread.io_loop)
  487. else:
  488. shell_stream = ZMQStream(self.shell_socket)
  489. control_stream = ZMQStream(self.control_socket, self.control_thread.io_loop)
  490. debugpy_stream = ZMQStream(self.debugpy_socket, self.control_thread.io_loop)
  491. self.control_thread.start()
  492. if self.shell_channel_thread:
  493. self.shell_channel_thread.start()
  494. kernel_factory = self.kernel_class.instance # type:ignore[attr-defined]
  495. kernel = kernel_factory(
  496. parent=self,
  497. session=self.session,
  498. control_stream=control_stream,
  499. debugpy_stream=debugpy_stream,
  500. debug_shell_socket=self.debug_shell_socket,
  501. shell_stream=shell_stream,
  502. control_thread=self.control_thread,
  503. shell_channel_thread=self.shell_channel_thread,
  504. iopub_thread=self.iopub_thread,
  505. iopub_socket=self.iopub_socket,
  506. stdin_socket=self.stdin_socket,
  507. log=self.log,
  508. profile_dir=self.profile_dir,
  509. user_ns=self.user_ns,
  510. )
  511. kernel.record_ports({name + "_port": port for name, port in self._ports.items()})
  512. self.kernel = kernel
  513. # Allow the displayhook to get the execution count
  514. self.displayhook.get_execution_count = lambda: kernel.execution_count
  515. def init_gui_pylab(self):
  516. """Enable GUI event loop integration, taking pylab into account."""
  517. # Register inline backend as default
  518. # this is higher priority than matplotlibrc,
  519. # but lower priority than anything else (mpl.use() for instance).
  520. # This only affects matplotlib >= 1.5
  521. if not os.environ.get("MPLBACKEND"):
  522. os.environ["MPLBACKEND"] = "module://matplotlib_inline.backend_inline"
  523. # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
  524. # to ensure that any exception is printed straight to stderr.
  525. # Normally _showtraceback associates the reply with an execution,
  526. # which means frontends will never draw it, as this exception
  527. # is not associated with any execute request.
  528. shell = self.shell
  529. assert shell is not None
  530. _showtraceback = shell._showtraceback
  531. try:
  532. # replace error-sending traceback with stderr
  533. def print_tb(etype, evalue, stb):
  534. print("GUI event loop or pylab initialization failed", file=sys.stderr)
  535. assert shell is not None
  536. print(shell.InteractiveTB.stb2text(stb), file=sys.stderr)
  537. shell._showtraceback = print_tb
  538. InteractiveShellApp.init_gui_pylab(self)
  539. finally:
  540. shell._showtraceback = _showtraceback
  541. def init_shell(self):
  542. """Initialize the shell channel."""
  543. self.shell = getattr(self.kernel, "shell", None)
  544. if self.shell:
  545. self.shell.configurables.append(self)
  546. def configure_tornado_logger(self):
  547. """Configure the tornado logging.Logger.
  548. Must set up the tornado logger or else tornado will call
  549. basicConfig for the root logger which makes the root logger
  550. go to the real sys.stderr instead of the capture streams.
  551. This function mimics the setup of logging.basicConfig.
  552. """
  553. logger = logging.getLogger("tornado")
  554. handler = logging.StreamHandler()
  555. formatter = logging.Formatter(logging.BASIC_FORMAT)
  556. handler.setFormatter(formatter)
  557. logger.addHandler(handler)
  558. def _init_asyncio_patch(self):
  559. """set default asyncio policy to be compatible with tornado
  560. Tornado 6 (at least) is not compatible with the default
  561. asyncio implementation on Windows
  562. Pick the older SelectorEventLoopPolicy on Windows
  563. if the known-incompatible default policy is in use.
  564. Support for Proactor via a background thread is available in tornado 6.1,
  565. but it is still preferable to run the Selector in the main thread
  566. instead of the background.
  567. do this as early as possible to make it a low priority and overridable
  568. ref: https://github.com/tornadoweb/tornado/issues/2608
  569. FIXME: if/when tornado supports the defaults in asyncio without threads,
  570. remove and bump tornado requirement for py38.
  571. Most likely, this will mean a new Python version
  572. where asyncio.ProactorEventLoop supports add_reader and friends.
  573. """
  574. if sys.platform.startswith("win"):
  575. import asyncio
  576. try:
  577. from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy
  578. except ImportError:
  579. pass
  580. # not affected
  581. else:
  582. if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
  583. # WindowsProactorEventLoopPolicy is not compatible with tornado 6
  584. # fallback to the pre-3.8 default of Selector
  585. asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
  586. def init_pdb(self):
  587. """Replace pdb with IPython's version that is interruptible.
  588. With the non-interruptible version, stopping pdb() locks up the kernel in a
  589. non-recoverable state.
  590. """
  591. import pdb
  592. from IPython.core import debugger
  593. if hasattr(debugger, "InterruptiblePdb"):
  594. # Only available in newer IPython releases:
  595. debugger.Pdb = debugger.InterruptiblePdb # type:ignore[misc]
  596. pdb.Pdb = debugger.Pdb # type:ignore[assignment,misc]
  597. pdb.set_trace = debugger.set_trace
  598. @catch_config_error
  599. def initialize(self, argv=None):
  600. """Initialize the application."""
  601. self._init_asyncio_patch()
  602. super().initialize(argv)
  603. if self.subapp is not None:
  604. return
  605. self.init_pdb()
  606. self.init_blackhole()
  607. self.init_connection_file()
  608. self.init_poller()
  609. self.init_sockets()
  610. self.init_heartbeat()
  611. # writing/displaying connection info must be *after* init_sockets/heartbeat
  612. self.write_connection_file()
  613. # Log connection info after writing connection file, so that the connection
  614. # file is definitely available at the time someone reads the log.
  615. self.log_connection_info()
  616. self.init_io()
  617. try:
  618. self.init_signal()
  619. except Exception:
  620. # Catch exception when initializing signal fails, eg when running the
  621. # kernel on a separate thread
  622. if int(self.log_level) < logging.CRITICAL: # type:ignore[call-overload]
  623. self.log.error("Unable to initialize signal:", exc_info=True) # noqa: G201
  624. self.init_kernel()
  625. # shell init steps
  626. self.init_path()
  627. self.init_shell()
  628. if self.shell:
  629. self.init_gui_pylab()
  630. self.init_extensions()
  631. self.init_code()
  632. # flush stdout/stderr, so that anything written to these streams during
  633. # initialization do not get associated with the first execution request
  634. sys.stdout.flush()
  635. sys.stderr.flush()
  636. def start(self):
  637. """Start the application."""
  638. if self.subapp is not None:
  639. return self.subapp.start()
  640. if self.poller is not None:
  641. self.poller.start()
  642. self.kernel.start()
  643. self.io_loop = ioloop.IOLoop.current()
  644. if self.trio_loop:
  645. from ipykernel.trio_runner import TrioRunner
  646. tr = TrioRunner()
  647. tr.initialize(self.kernel, self.io_loop)
  648. try:
  649. tr.run()
  650. except KeyboardInterrupt:
  651. pass
  652. else:
  653. try:
  654. self.io_loop.start()
  655. except KeyboardInterrupt:
  656. pass
  657. launch_new_instance = IPKernelApp.launch_instance
  658. def main(): # pragma: no cover
  659. """Run an IPKernel as an application"""
  660. app = IPKernelApp.instance()
  661. app.initialize()
  662. app.start()
  663. if __name__ == "__main__":
  664. main()