You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

511 lines
20 KiB

4 years ago
  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 print_function
  5. import atexit
  6. import os
  7. import sys
  8. import signal
  9. import traceback
  10. import logging
  11. from tornado import ioloop
  12. import zmq
  13. from zmq.eventloop import ioloop as zmq_ioloop
  14. from zmq.eventloop.zmqstream import ZMQStream
  15. from IPython.core.application import (
  16. BaseIPythonApplication, base_flags, base_aliases, catch_config_error
  17. )
  18. from IPython.core.profiledir import ProfileDir
  19. from IPython.core.shellapp import (
  20. InteractiveShellApp, shell_flags, shell_aliases
  21. )
  22. from IPython.utils import io
  23. from ipython_genutils.path import filefind, ensure_dir_exists
  24. from traitlets import (
  25. Any, Instance, Dict, Unicode, Integer, Bool, DottedObjectName, Type, default
  26. )
  27. from ipython_genutils.importstring import import_item
  28. from jupyter_core.paths import jupyter_runtime_dir
  29. from jupyter_client import write_connection_file
  30. from jupyter_client.connect import ConnectionFileMixin
  31. # local imports
  32. from .iostream import IOPubThread
  33. from .heartbeat import Heartbeat
  34. from .ipkernel import IPythonKernel
  35. from .parentpoller import ParentPollerUnix, ParentPollerWindows
  36. from jupyter_client.session import (
  37. Session, session_flags, session_aliases,
  38. )
  39. from .zmqshell import ZMQInteractiveShell
  40. #-----------------------------------------------------------------------------
  41. # Flags and Aliases
  42. #-----------------------------------------------------------------------------
  43. kernel_aliases = dict(base_aliases)
  44. kernel_aliases.update({
  45. 'ip' : 'IPKernelApp.ip',
  46. 'hb' : 'IPKernelApp.hb_port',
  47. 'shell' : 'IPKernelApp.shell_port',
  48. 'iopub' : 'IPKernelApp.iopub_port',
  49. 'stdin' : 'IPKernelApp.stdin_port',
  50. 'control' : 'IPKernelApp.control_port',
  51. 'f' : 'IPKernelApp.connection_file',
  52. 'transport': 'IPKernelApp.transport',
  53. })
  54. kernel_flags = dict(base_flags)
  55. kernel_flags.update({
  56. 'no-stdout' : (
  57. {'IPKernelApp' : {'no_stdout' : True}},
  58. "redirect stdout to the null device"),
  59. 'no-stderr' : (
  60. {'IPKernelApp' : {'no_stderr' : True}},
  61. "redirect stderr to the null device"),
  62. 'pylab' : (
  63. {'IPKernelApp' : {'pylab' : 'auto'}},
  64. """Pre-load matplotlib and numpy for interactive use with
  65. the default matplotlib backend."""),
  66. })
  67. # inherit flags&aliases for any IPython shell apps
  68. kernel_aliases.update(shell_aliases)
  69. kernel_flags.update(shell_flags)
  70. # inherit flags&aliases for Sessions
  71. kernel_aliases.update(session_aliases)
  72. kernel_flags.update(session_flags)
  73. _ctrl_c_message = """\
  74. NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
  75. To exit, you will have to explicitly quit this process, by either sending
  76. "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
  77. To read more about this, see https://github.com/ipython/ipython/issues/2049
  78. """
  79. #-----------------------------------------------------------------------------
  80. # Application class for starting an IPython Kernel
  81. #-----------------------------------------------------------------------------
  82. class IPKernelApp(BaseIPythonApplication, InteractiveShellApp,
  83. ConnectionFileMixin):
  84. name='ipython-kernel'
  85. aliases = Dict(kernel_aliases)
  86. flags = Dict(kernel_flags)
  87. classes = [IPythonKernel, ZMQInteractiveShell, ProfileDir, Session]
  88. # the kernel class, as an importstring
  89. kernel_class = Type('ipykernel.ipkernel.IPythonKernel',
  90. klass='ipykernel.kernelbase.Kernel',
  91. help="""The Kernel subclass to be used.
  92. This should allow easy re-use of the IPKernelApp entry point
  93. to configure and launch kernels other than IPython's own.
  94. """).tag(config=True)
  95. kernel = Any()
  96. poller = Any() # don't restrict this even though current pollers are all Threads
  97. heartbeat = Instance(Heartbeat, allow_none=True)
  98. ports = Dict()
  99. subcommands = {
  100. 'install': (
  101. 'ipykernel.kernelspec.InstallIPythonKernelSpecApp',
  102. 'Install the IPython kernel'
  103. ),
  104. }
  105. # connection info:
  106. connection_dir = Unicode()
  107. @default('connection_dir')
  108. def _default_connection_dir(self):
  109. return jupyter_runtime_dir()
  110. @property
  111. def abs_connection_file(self):
  112. if os.path.basename(self.connection_file) == self.connection_file:
  113. return os.path.join(self.connection_dir, self.connection_file)
  114. else:
  115. return self.connection_file
  116. # streams, etc.
  117. no_stdout = Bool(False, help="redirect stdout to the null device").tag(config=True)
  118. no_stderr = Bool(False, help="redirect stderr to the null device").tag(config=True)
  119. quiet = Bool(True, help="Only send stdout/stderr to output stream").tag(config=True)
  120. outstream_class = DottedObjectName('ipykernel.iostream.OutStream',
  121. help="The importstring for the OutStream factory").tag(config=True)
  122. displayhook_class = DottedObjectName('ipykernel.displayhook.ZMQDisplayHook',
  123. help="The importstring for the DisplayHook factory").tag(config=True)
  124. # polling
  125. parent_handle = Integer(int(os.environ.get('JPY_PARENT_PID') or 0),
  126. help="""kill this process if its parent dies. On Windows, the argument
  127. specifies the HANDLE of the parent process, otherwise it is simply boolean.
  128. """).tag(config=True)
  129. interrupt = Integer(int(os.environ.get('JPY_INTERRUPT_EVENT') or 0),
  130. help="""ONLY USED ON WINDOWS
  131. Interrupt this process when the parent is signaled.
  132. """).tag(config=True)
  133. def init_crash_handler(self):
  134. sys.excepthook = self.excepthook
  135. def excepthook(self, etype, evalue, tb):
  136. # write uncaught traceback to 'real' stderr, not zmq-forwarder
  137. traceback.print_exception(etype, evalue, tb, file=sys.__stderr__)
  138. def init_poller(self):
  139. if sys.platform == 'win32':
  140. if self.interrupt or self.parent_handle:
  141. self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
  142. elif self.parent_handle and self.parent_handle != 1:
  143. # PID 1 (init) is special and will never go away,
  144. # only be reassigned.
  145. # Parent polling doesn't work if ppid == 1 to start with.
  146. self.poller = ParentPollerUnix()
  147. def _bind_socket(self, s, port):
  148. iface = '%s://%s' % (self.transport, self.ip)
  149. if self.transport == 'tcp':
  150. if port <= 0:
  151. port = s.bind_to_random_port(iface)
  152. else:
  153. s.bind("tcp://%s:%i" % (self.ip, port))
  154. elif self.transport == 'ipc':
  155. if port <= 0:
  156. port = 1
  157. path = "%s-%i" % (self.ip, port)
  158. while os.path.exists(path):
  159. port = port + 1
  160. path = "%s-%i" % (self.ip, port)
  161. else:
  162. path = "%s-%i" % (self.ip, port)
  163. s.bind("ipc://%s" % path)
  164. return port
  165. def write_connection_file(self):
  166. """write connection info to JSON file"""
  167. cf = self.abs_connection_file
  168. self.log.debug("Writing connection file: %s", cf)
  169. write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
  170. shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
  171. iopub_port=self.iopub_port, control_port=self.control_port)
  172. def cleanup_connection_file(self):
  173. cf = self.abs_connection_file
  174. self.log.debug("Cleaning up connection file: %s", cf)
  175. try:
  176. os.remove(cf)
  177. except (IOError, OSError):
  178. pass
  179. self.cleanup_ipc_files()
  180. def init_connection_file(self):
  181. if not self.connection_file:
  182. self.connection_file = "kernel-%s.json"%os.getpid()
  183. try:
  184. self.connection_file = filefind(self.connection_file, ['.', self.connection_dir])
  185. except IOError:
  186. self.log.debug("Connection file not found: %s", self.connection_file)
  187. # This means I own it, and I'll create it in this directory:
  188. ensure_dir_exists(os.path.dirname(self.abs_connection_file), 0o700)
  189. # Also, I will clean it up:
  190. atexit.register(self.cleanup_connection_file)
  191. return
  192. try:
  193. self.load_connection_file()
  194. except Exception:
  195. self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
  196. self.exit(1)
  197. def init_sockets(self):
  198. # Create a context, a session, and the kernel sockets.
  199. self.log.info("Starting the kernel at pid: %i", os.getpid())
  200. context = zmq.Context.instance()
  201. # Uncomment this to try closing the context.
  202. # atexit.register(context.term)
  203. self.shell_socket = context.socket(zmq.ROUTER)
  204. self.shell_socket.linger = 1000
  205. self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
  206. self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
  207. self.stdin_socket = context.socket(zmq.ROUTER)
  208. self.stdin_socket.linger = 1000
  209. self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
  210. self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
  211. self.control_socket = context.socket(zmq.ROUTER)
  212. self.control_socket.linger = 1000
  213. self.control_port = self._bind_socket(self.control_socket, self.control_port)
  214. self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
  215. if hasattr(zmq, 'ROUTER_HANDOVER'):
  216. # set router-handover to workaround zeromq reconnect problems
  217. # in certain rare circumstances
  218. # see ipython/ipykernel#270 and zeromq/libzmq#2892
  219. self.shell_socket.router_handover = \
  220. self.control_socket.router_handover = \
  221. self.stdin_socket.router_handover = 1
  222. self.init_iopub(context)
  223. def init_iopub(self, context):
  224. self.iopub_socket = context.socket(zmq.PUB)
  225. self.iopub_socket.linger = 1000
  226. self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
  227. self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
  228. self.configure_tornado_logger()
  229. self.iopub_thread = IOPubThread(self.iopub_socket, pipe=True)
  230. self.iopub_thread.start()
  231. # backward-compat: wrap iopub socket API in background thread
  232. self.iopub_socket = self.iopub_thread.background_socket
  233. def init_heartbeat(self):
  234. """start the heart beating"""
  235. # heartbeat doesn't share context, because it mustn't be blocked
  236. # by the GIL, which is accessed by libzmq when freeing zero-copy messages
  237. hb_ctx = zmq.Context()
  238. self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
  239. self.hb_port = self.heartbeat.port
  240. self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
  241. self.heartbeat.start()
  242. def log_connection_info(self):
  243. """display connection info, and store ports"""
  244. basename = os.path.basename(self.connection_file)
  245. if basename == self.connection_file or \
  246. os.path.dirname(self.connection_file) == self.connection_dir:
  247. # use shortname
  248. tail = basename
  249. else:
  250. tail = self.connection_file
  251. lines = [
  252. "To connect another client to this kernel, use:",
  253. " --existing %s" % tail,
  254. ]
  255. # log connection info
  256. # info-level, so often not shown.
  257. # frontends should use the %connect_info magic
  258. # to see the connection info
  259. for line in lines:
  260. self.log.info(line)
  261. # also raw print to the terminal if no parent_handle (`ipython kernel`)
  262. # unless log-level is CRITICAL (--quiet)
  263. if not self.parent_handle and self.log_level < logging.CRITICAL:
  264. io.rprint(_ctrl_c_message)
  265. for line in lines:
  266. io.rprint(line)
  267. self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
  268. stdin=self.stdin_port, hb=self.hb_port,
  269. control=self.control_port)
  270. def init_blackhole(self):
  271. """redirects stdout/stderr to devnull if necessary"""
  272. if self.no_stdout or self.no_stderr:
  273. blackhole = open(os.devnull, 'w')
  274. if self.no_stdout:
  275. sys.stdout = sys.__stdout__ = blackhole
  276. if self.no_stderr:
  277. sys.stderr = sys.__stderr__ = blackhole
  278. def init_io(self):
  279. """Redirect input streams and set a display hook."""
  280. if self.outstream_class:
  281. outstream_factory = import_item(str(self.outstream_class))
  282. sys.stdout.flush()
  283. e_stdout = None if self.quiet else sys.__stdout__
  284. e_stderr = None if self.quiet else sys.__stderr__
  285. sys.stdout = outstream_factory(self.session, self.iopub_thread,
  286. u'stdout',
  287. echo=e_stdout)
  288. sys.stderr.flush()
  289. sys.stderr = outstream_factory(self.session, self.iopub_thread,
  290. u'stderr',
  291. echo=e_stderr)
  292. if self.displayhook_class:
  293. displayhook_factory = import_item(str(self.displayhook_class))
  294. self.displayhook = displayhook_factory(self.session, self.iopub_socket)
  295. sys.displayhook = self.displayhook
  296. self.patch_io()
  297. def patch_io(self):
  298. """Patch important libraries that can't handle sys.stdout forwarding"""
  299. try:
  300. import faulthandler
  301. except ImportError:
  302. pass
  303. else:
  304. # Warning: this is a monkeypatch of `faulthandler.enable`, watch for possible
  305. # updates to the upstream API and update accordingly (up-to-date as of Python 3.5):
  306. # https://docs.python.org/3/library/faulthandler.html#faulthandler.enable
  307. # change default file to __stderr__ from forwarded stderr
  308. faulthandler_enable = faulthandler.enable
  309. def enable(file=sys.__stderr__, all_threads=True, **kwargs):
  310. return faulthandler_enable(file=file, all_threads=all_threads, **kwargs)
  311. faulthandler.enable = enable
  312. if hasattr(faulthandler, 'register'):
  313. faulthandler_register = faulthandler.register
  314. def register(signum, file=sys.__stderr__, all_threads=True, chain=False, **kwargs):
  315. return faulthandler_register(signum, file=file, all_threads=all_threads,
  316. chain=chain, **kwargs)
  317. faulthandler.register = register
  318. def init_signal(self):
  319. signal.signal(signal.SIGINT, signal.SIG_IGN)
  320. def init_kernel(self):
  321. """Create the Kernel object itself"""
  322. shell_stream = ZMQStream(self.shell_socket)
  323. control_stream = ZMQStream(self.control_socket)
  324. kernel_factory = self.kernel_class.instance
  325. kernel = kernel_factory(parent=self, session=self.session,
  326. shell_streams=[shell_stream, control_stream],
  327. iopub_thread=self.iopub_thread,
  328. iopub_socket=self.iopub_socket,
  329. stdin_socket=self.stdin_socket,
  330. log=self.log,
  331. profile_dir=self.profile_dir,
  332. user_ns=self.user_ns,
  333. )
  334. kernel.record_ports({
  335. name + '_port': port for name, port in self.ports.items()
  336. })
  337. self.kernel = kernel
  338. # Allow the displayhook to get the execution count
  339. self.displayhook.get_execution_count = lambda: kernel.execution_count
  340. def init_gui_pylab(self):
  341. """Enable GUI event loop integration, taking pylab into account."""
  342. # Register inline backend as default
  343. # this is higher priority than matplotlibrc,
  344. # but lower priority than anything else (mpl.use() for instance).
  345. # This only affects matplotlib >= 1.5
  346. if not os.environ.get('MPLBACKEND'):
  347. os.environ['MPLBACKEND'] = 'module://ipykernel.pylab.backend_inline'
  348. # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
  349. # to ensure that any exception is printed straight to stderr.
  350. # Normally _showtraceback associates the reply with an execution,
  351. # which means frontends will never draw it, as this exception
  352. # is not associated with any execute request.
  353. shell = self.shell
  354. _showtraceback = shell._showtraceback
  355. try:
  356. # replace error-sending traceback with stderr
  357. def print_tb(etype, evalue, stb):
  358. print ("GUI event loop or pylab initialization failed",
  359. file=sys.stderr)
  360. print (shell.InteractiveTB.stb2text(stb), file=sys.stderr)
  361. shell._showtraceback = print_tb
  362. InteractiveShellApp.init_gui_pylab(self)
  363. finally:
  364. shell._showtraceback = _showtraceback
  365. def init_shell(self):
  366. self.shell = getattr(self.kernel, 'shell', None)
  367. if self.shell:
  368. self.shell.configurables.append(self)
  369. def init_extensions(self):
  370. super(IPKernelApp, self).init_extensions()
  371. # BEGIN HARDCODED WIDGETS HACK
  372. # Ensure ipywidgets extension is loaded if available
  373. extension_man = self.shell.extension_manager
  374. if 'ipywidgets' not in extension_man.loaded:
  375. try:
  376. extension_man.load_extension('ipywidgets')
  377. except ImportError as e:
  378. self.log.debug('ipywidgets package not installed. Widgets will not be available.')
  379. # END HARDCODED WIDGETS HACK
  380. def configure_tornado_logger(self):
  381. """ Configure the tornado logging.Logger.
  382. Must set up the tornado logger or else tornado will call
  383. basicConfig for the root logger which makes the root logger
  384. go to the real sys.stderr instead of the capture streams.
  385. This function mimics the setup of logging.basicConfig.
  386. """
  387. logger = logging.getLogger('tornado')
  388. handler = logging.StreamHandler()
  389. formatter = logging.Formatter(logging.BASIC_FORMAT)
  390. handler.setFormatter(formatter)
  391. logger.addHandler(handler)
  392. @catch_config_error
  393. def initialize(self, argv=None):
  394. super(IPKernelApp, self).initialize(argv)
  395. if self.subapp is not None:
  396. return
  397. # register zmq IOLoop with tornado
  398. zmq_ioloop.install()
  399. self.init_blackhole()
  400. self.init_connection_file()
  401. self.init_poller()
  402. self.init_sockets()
  403. self.init_heartbeat()
  404. # writing/displaying connection info must be *after* init_sockets/heartbeat
  405. self.write_connection_file()
  406. # Log connection info after writing connection file, so that the connection
  407. # file is definitely available at the time someone reads the log.
  408. self.log_connection_info()
  409. self.init_io()
  410. self.init_signal()
  411. self.init_kernel()
  412. # shell init steps
  413. self.init_path()
  414. self.init_shell()
  415. if self.shell:
  416. self.init_gui_pylab()
  417. self.init_extensions()
  418. self.init_code()
  419. # flush stdout/stderr, so that anything written to these streams during
  420. # initialization do not get associated with the first execution request
  421. sys.stdout.flush()
  422. sys.stderr.flush()
  423. def start(self):
  424. if self.subapp is not None:
  425. return self.subapp.start()
  426. if self.poller is not None:
  427. self.poller.start()
  428. self.kernel.start()
  429. self.io_loop = ioloop.IOLoop.current()
  430. try:
  431. self.io_loop.start()
  432. except KeyboardInterrupt:
  433. pass
  434. launch_new_instance = IPKernelApp.launch_instance
  435. def main():
  436. """Run an IPKernel as an application"""
  437. app = IPKernelApp.instance()
  438. app.initialize()
  439. app.start()
  440. if __name__ == '__main__':
  441. main()