consoleapp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. """ A minimal application base mixin for all ZMQ based IPython frontends.
  2. This is not a complete console app, as subprocess will not be able to receive
  3. input, there is no real readline support, among other limitations. This is a
  4. refactoring of what used to be the IPython/qt/console/qtconsoleapp.py
  5. """
  6. # Copyright (c) Jupyter Development Team.
  7. # Distributed under the terms of the Modified BSD License.
  8. import atexit
  9. import os
  10. import signal
  11. import sys
  12. import uuid
  13. import warnings
  14. from traitlets.config.application import boolean_flag
  15. from ipython_genutils.path import filefind
  16. from traitlets import (
  17. Dict, List, Unicode, CUnicode, CBool, Any
  18. )
  19. from jupyter_core.application import base_flags, base_aliases
  20. from .blocking import BlockingKernelClient
  21. from .restarter import KernelRestarter
  22. from . import KernelManager, tunnel_to_kernel, find_connection_file, connect
  23. from .kernelspec import NoSuchKernel
  24. from .session import Session
  25. ConnectionFileMixin = connect.ConnectionFileMixin
  26. from .localinterfaces import localhost
  27. #-----------------------------------------------------------------------------
  28. # Aliases and Flags
  29. #-----------------------------------------------------------------------------
  30. flags = {}
  31. flags.update(base_flags)
  32. # the flags that are specific to the frontend
  33. # these must be scrubbed before being passed to the kernel,
  34. # or it will raise an error on unrecognized flags
  35. app_flags = {
  36. 'existing' : ({'JupyterConsoleApp' : {'existing' : 'kernel*.json'}},
  37. "Connect to an existing kernel. If no argument specified, guess most recent"),
  38. }
  39. app_flags.update(boolean_flag(
  40. 'confirm-exit', 'JupyterConsoleApp.confirm_exit',
  41. """Set to display confirmation dialog on exit. You can always use 'exit' or
  42. 'quit', to force a direct exit without any confirmation. This can also
  43. be set in the config file by setting
  44. `c.JupyterConsoleApp.confirm_exit`.
  45. """,
  46. """Don't prompt the user when exiting. This will terminate the kernel
  47. if it is owned by the frontend, and leave it alive if it is external.
  48. This can also be set in the config file by setting
  49. `c.JupyterConsoleApp.confirm_exit`.
  50. """
  51. ))
  52. flags.update(app_flags)
  53. aliases = {}
  54. aliases.update(base_aliases)
  55. # also scrub aliases from the frontend
  56. app_aliases = dict(
  57. ip = 'JupyterConsoleApp.ip',
  58. transport = 'JupyterConsoleApp.transport',
  59. hb = 'JupyterConsoleApp.hb_port',
  60. shell = 'JupyterConsoleApp.shell_port',
  61. iopub = 'JupyterConsoleApp.iopub_port',
  62. stdin = 'JupyterConsoleApp.stdin_port',
  63. existing = 'JupyterConsoleApp.existing',
  64. f = 'JupyterConsoleApp.connection_file',
  65. kernel = 'JupyterConsoleApp.kernel_name',
  66. ssh = 'JupyterConsoleApp.sshserver',
  67. )
  68. aliases.update(app_aliases)
  69. #-----------------------------------------------------------------------------
  70. # Classes
  71. #-----------------------------------------------------------------------------
  72. classes = [KernelManager, KernelRestarter, Session]
  73. class JupyterConsoleApp(ConnectionFileMixin):
  74. name = 'jupyter-console-mixin'
  75. description = """
  76. The Jupyter Console Mixin.
  77. This class contains the common portions of console client (QtConsole,
  78. ZMQ-based terminal console, etc). It is not a full console, in that
  79. launched terminal subprocesses will not be able to accept input.
  80. The Console using this mixing supports various extra features beyond
  81. the single-process Terminal IPython shell, such as connecting to
  82. existing kernel, via:
  83. jupyter console <appname> --existing
  84. as well as tunnel via SSH
  85. """
  86. classes = classes
  87. flags = Dict(flags)
  88. aliases = Dict(aliases)
  89. kernel_manager_class = KernelManager
  90. kernel_client_class = BlockingKernelClient
  91. kernel_argv = List(Unicode())
  92. # connection info:
  93. sshserver = Unicode('', config=True,
  94. help="""The SSH server to use to connect to the kernel.""")
  95. sshkey = Unicode('', config=True,
  96. help="""Path to the ssh key to use for logging in to the ssh server.""")
  97. def _connection_file_default(self):
  98. return 'kernel-%i.json' % os.getpid()
  99. existing = CUnicode('', config=True,
  100. help="""Connect to an already running kernel""")
  101. kernel_name = Unicode('python', config=True,
  102. help="""The name of the default kernel to start.""")
  103. confirm_exit = CBool(True, config=True,
  104. help="""
  105. Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
  106. to force a direct exit without any confirmation.""",
  107. )
  108. def build_kernel_argv(self, argv=None):
  109. """build argv to be passed to kernel subprocess
  110. Override in subclasses if any args should be passed to the kernel
  111. """
  112. self.kernel_argv = self.extra_args
  113. def init_connection_file(self):
  114. """find the connection file, and load the info if found.
  115. The current working directory and the current profile's security
  116. directory will be searched for the file if it is not given by
  117. absolute path.
  118. When attempting to connect to an existing kernel and the `--existing`
  119. argument does not match an existing file, it will be interpreted as a
  120. fileglob, and the matching file in the current profile's security dir
  121. with the latest access time will be used.
  122. After this method is called, self.connection_file contains the *full path*
  123. to the connection file, never just its name.
  124. """
  125. if self.existing:
  126. try:
  127. cf = find_connection_file(self.existing, ['.', self.runtime_dir])
  128. except Exception:
  129. self.log.critical("Could not find existing kernel connection file %s", self.existing)
  130. self.exit(1)
  131. self.log.debug("Connecting to existing kernel: %s" % cf)
  132. self.connection_file = cf
  133. else:
  134. # not existing, check if we are going to write the file
  135. # and ensure that self.connection_file is a full path, not just the shortname
  136. try:
  137. cf = find_connection_file(self.connection_file, [self.runtime_dir])
  138. except Exception:
  139. # file might not exist
  140. if self.connection_file == os.path.basename(self.connection_file):
  141. # just shortname, put it in security dir
  142. cf = os.path.join(self.runtime_dir, self.connection_file)
  143. else:
  144. cf = self.connection_file
  145. self.connection_file = cf
  146. try:
  147. self.connection_file = filefind(self.connection_file, ['.', self.runtime_dir])
  148. except IOError:
  149. self.log.debug("Connection File not found: %s", self.connection_file)
  150. return
  151. # should load_connection_file only be used for existing?
  152. # as it is now, this allows reusing ports if an existing
  153. # file is requested
  154. try:
  155. self.load_connection_file()
  156. except Exception:
  157. self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
  158. self.exit(1)
  159. def init_ssh(self):
  160. """set up ssh tunnels, if needed."""
  161. if not self.existing or (not self.sshserver and not self.sshkey):
  162. return
  163. self.load_connection_file()
  164. transport = self.transport
  165. ip = self.ip
  166. if transport != 'tcp':
  167. self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
  168. sys.exit(-1)
  169. if self.sshkey and not self.sshserver:
  170. # specifying just the key implies that we are connecting directly
  171. self.sshserver = ip
  172. ip = localhost()
  173. # build connection dict for tunnels:
  174. info = dict(ip=ip,
  175. shell_port=self.shell_port,
  176. iopub_port=self.iopub_port,
  177. stdin_port=self.stdin_port,
  178. hb_port=self.hb_port
  179. )
  180. self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
  181. # tunnels return a new set of ports, which will be on localhost:
  182. self.ip = localhost()
  183. try:
  184. newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
  185. except:
  186. # even catch KeyboardInterrupt
  187. self.log.error("Could not setup tunnels", exc_info=True)
  188. self.exit(1)
  189. self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
  190. cf = self.connection_file
  191. root, ext = os.path.splitext(cf)
  192. self.connection_file = root + '-ssh' + ext
  193. self.write_connection_file() # write the new connection file
  194. self.log.info("To connect another client via this tunnel, use:")
  195. self.log.info("--existing %s" % os.path.basename(self.connection_file))
  196. def _new_connection_file(self):
  197. cf = ''
  198. while not cf:
  199. # we don't need a 128b id to distinguish kernels, use more readable
  200. # 48b node segment (12 hex chars). Users running more than 32k simultaneous
  201. # kernels can subclass.
  202. ident = str(uuid.uuid4()).split('-')[-1]
  203. cf = os.path.join(self.runtime_dir, 'kernel-%s.json' % ident)
  204. # only keep if it's actually new. Protect against unlikely collision
  205. # in 48b random search space
  206. cf = cf if not os.path.exists(cf) else ''
  207. return cf
  208. def init_kernel_manager(self):
  209. # Don't let Qt or ZMQ swallow KeyboardInterupts.
  210. if self.existing:
  211. self.kernel_manager = None
  212. return
  213. signal.signal(signal.SIGINT, signal.SIG_DFL)
  214. # Create a KernelManager and start a kernel.
  215. try:
  216. self.kernel_manager = self.kernel_manager_class(
  217. ip=self.ip,
  218. session=self.session,
  219. transport=self.transport,
  220. shell_port=self.shell_port,
  221. iopub_port=self.iopub_port,
  222. stdin_port=self.stdin_port,
  223. hb_port=self.hb_port,
  224. connection_file=self.connection_file,
  225. kernel_name=self.kernel_name,
  226. parent=self,
  227. data_dir=self.data_dir,
  228. )
  229. except NoSuchKernel:
  230. self.log.critical("Could not find kernel %s", self.kernel_name)
  231. self.exit(1)
  232. self.kernel_manager.client_factory = self.kernel_client_class
  233. # FIXME: remove special treatment of IPython kernels
  234. kwargs = {}
  235. if self.kernel_manager.ipykernel:
  236. kwargs['extra_arguments'] = self.kernel_argv
  237. self.kernel_manager.start_kernel(**kwargs)
  238. atexit.register(self.kernel_manager.cleanup_ipc_files)
  239. if self.sshserver:
  240. # ssh, write new connection file
  241. self.kernel_manager.write_connection_file()
  242. # in case KM defaults / ssh writing changes things:
  243. km = self.kernel_manager
  244. self.shell_port=km.shell_port
  245. self.iopub_port=km.iopub_port
  246. self.stdin_port=km.stdin_port
  247. self.hb_port=km.hb_port
  248. self.connection_file = km.connection_file
  249. atexit.register(self.kernel_manager.cleanup_connection_file)
  250. def init_kernel_client(self):
  251. if self.kernel_manager is not None:
  252. self.kernel_client = self.kernel_manager.client()
  253. else:
  254. self.kernel_client = self.kernel_client_class(
  255. session=self.session,
  256. ip=self.ip,
  257. transport=self.transport,
  258. shell_port=self.shell_port,
  259. iopub_port=self.iopub_port,
  260. stdin_port=self.stdin_port,
  261. hb_port=self.hb_port,
  262. connection_file=self.connection_file,
  263. parent=self,
  264. )
  265. self.kernel_client.start_channels()
  266. def initialize(self, argv=None):
  267. """
  268. Classes which mix this class in should call:
  269. JupyterConsoleApp.initialize(self,argv)
  270. """
  271. if self._dispatching:
  272. return
  273. self.init_connection_file()
  274. self.init_ssh()
  275. self.init_kernel_manager()
  276. self.init_kernel_client()
  277. class IPythonConsoleApp(JupyterConsoleApp):
  278. def __init__(self, *args, **kwargs):
  279. warnings.warn("IPythonConsoleApp is deprecated. Use JupyterConsoleApp")
  280. super(IPythonConsoleApp, self).__init__(*args, **kwargs)