##// END OF EJS Templates
Merge pull request #3753 from ivanov/ctrl-c-for-kernel...
Paul Ivanov -
r11675:01322e25 merge
parent child Browse files
Show More
@@ -1,459 +1,470 b''
1 1 """An Application for launching a kernel
2 2
3 3 Authors
4 4 -------
5 5 * MinRK
6 6 """
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (C) 2011 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING.txt, distributed as part of this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17
18 18 from __future__ import print_function
19 19
20 20 # Standard library imports
21 21 import atexit
22 22 import json
23 23 import os
24 24 import sys
25 25 import signal
26 26
27 27 # System library imports
28 28 import zmq
29 29 from zmq.eventloop import ioloop
30 30 from zmq.eventloop.zmqstream import ZMQStream
31 31
32 32 # IPython imports
33 33 from IPython.core.ultratb import FormattedTB
34 34 from IPython.core.application import (
35 35 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
36 36 )
37 37 from IPython.core.profiledir import ProfileDir
38 38 from IPython.core.shellapp import (
39 39 InteractiveShellApp, shell_flags, shell_aliases
40 40 )
41 41 from IPython.utils import io
42 42 from IPython.utils.localinterfaces import LOCALHOST
43 43 from IPython.utils.path import filefind
44 44 from IPython.utils.py3compat import str_to_bytes
45 45 from IPython.utils.traitlets import (
46 46 Any, Instance, Dict, Unicode, Integer, Bool, CaselessStrEnum,
47 47 DottedObjectName,
48 48 )
49 49 from IPython.utils.importstring import import_item
50 50 from IPython.kernel import write_connection_file
51 51
52 52 # local imports
53 53 from heartbeat import Heartbeat
54 54 from ipkernel import Kernel
55 55 from parentpoller import ParentPollerUnix, ParentPollerWindows
56 56 from session import (
57 57 Session, session_flags, session_aliases, default_secure,
58 58 )
59 59 from zmqshell import ZMQInteractiveShell
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Flags and Aliases
63 63 #-----------------------------------------------------------------------------
64 64
65 65 kernel_aliases = dict(base_aliases)
66 66 kernel_aliases.update({
67 67 'ip' : 'IPKernelApp.ip',
68 68 'hb' : 'IPKernelApp.hb_port',
69 69 'shell' : 'IPKernelApp.shell_port',
70 70 'iopub' : 'IPKernelApp.iopub_port',
71 71 'stdin' : 'IPKernelApp.stdin_port',
72 72 'control' : 'IPKernelApp.control_port',
73 73 'f' : 'IPKernelApp.connection_file',
74 74 'parent': 'IPKernelApp.parent_handle',
75 75 'transport': 'IPKernelApp.transport',
76 76 })
77 77 if sys.platform.startswith('win'):
78 78 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
79 79
80 80 kernel_flags = dict(base_flags)
81 81 kernel_flags.update({
82 82 'no-stdout' : (
83 83 {'IPKernelApp' : {'no_stdout' : True}},
84 84 "redirect stdout to the null device"),
85 85 'no-stderr' : (
86 86 {'IPKernelApp' : {'no_stderr' : True}},
87 87 "redirect stderr to the null device"),
88 88 'pylab' : (
89 89 {'IPKernelApp' : {'pylab' : 'auto'}},
90 90 """Pre-load matplotlib and numpy for interactive use with
91 91 the default matplotlib backend."""),
92 92 })
93 93
94 94 # inherit flags&aliases for any IPython shell apps
95 95 kernel_aliases.update(shell_aliases)
96 96 kernel_flags.update(shell_flags)
97 97
98 98 # inherit flags&aliases for Sessions
99 99 kernel_aliases.update(session_aliases)
100 100 kernel_flags.update(session_flags)
101 101
102 _ctrl_c_message = """\
103 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
104
105 To exit, you will have to explicitly quit this process, by either sending
106 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
107
108 To read more about this, see https://github.com/ipython/ipython/issues/2049
109
110 """
111
102 112 #-----------------------------------------------------------------------------
103 113 # Application class for starting an IPython Kernel
104 114 #-----------------------------------------------------------------------------
105 115
106 116 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp):
107 117 name='ipkernel'
108 118 aliases = Dict(kernel_aliases)
109 119 flags = Dict(kernel_flags)
110 120 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
111 121 # the kernel class, as an importstring
112 122 kernel_class = DottedObjectName('IPython.kernel.zmq.ipkernel.Kernel', config=True,
113 123 help="""The Kernel subclass to be used.
114 124
115 125 This should allow easy re-use of the IPKernelApp entry point
116 126 to configure and launch kernels other than IPython's own.
117 127 """)
118 128 kernel = Any()
119 129 poller = Any() # don't restrict this even though current pollers are all Threads
120 130 heartbeat = Instance(Heartbeat)
121 131 session = Instance('IPython.kernel.zmq.session.Session')
122 132 ports = Dict()
123 133
124 134 # ipkernel doesn't get its own config file
125 135 def _config_file_name_default(self):
126 136 return 'ipython_config.py'
127 137
128 138 # inherit config file name from parent:
129 139 parent_appname = Unicode(config=True)
130 140 def _parent_appname_changed(self, name, old, new):
131 141 if self.config_file_specified:
132 142 # it was manually specified, ignore
133 143 return
134 144 self.config_file_name = new.replace('-','_') + u'_config.py'
135 145 # don't let this count as specifying the config file
136 146 self.config_file_specified.remove(self.config_file_name)
137 147
138 148 # connection info:
139 149 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
140 150 ip = Unicode(config=True,
141 151 help="Set the IP or interface on which the kernel will listen.")
142 152 def _ip_default(self):
143 153 if self.transport == 'ipc':
144 154 if self.connection_file:
145 155 return os.path.splitext(self.abs_connection_file)[0] + '-ipc'
146 156 else:
147 157 return 'kernel-ipc'
148 158 else:
149 159 return LOCALHOST
150 160 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
151 161 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
152 162 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
153 163 stdin_port = Integer(0, config=True, help="set the stdin (ROUTER) port [default: random]")
154 164 control_port = Integer(0, config=True, help="set the control (ROUTER) port [default: random]")
155 165 connection_file = Unicode('', config=True,
156 166 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
157 167
158 168 This file will contain the IP, ports, and authentication key needed to connect
159 169 clients to this kernel. By default, this file will be created in the security dir
160 170 of the current profile, but can be specified by absolute path.
161 171 """)
162 172 @property
163 173 def abs_connection_file(self):
164 174 if os.path.basename(self.connection_file) == self.connection_file:
165 175 return os.path.join(self.profile_dir.security_dir, self.connection_file)
166 176 else:
167 177 return self.connection_file
168 178
169 179
170 180 # streams, etc.
171 181 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
172 182 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
173 183 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
174 184 config=True, help="The importstring for the OutStream factory")
175 185 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
176 186 config=True, help="The importstring for the DisplayHook factory")
177 187
178 188 # polling
179 189 parent_handle = Integer(0, config=True,
180 190 help="""kill this process if its parent dies. On Windows, the argument
181 191 specifies the HANDLE of the parent process, otherwise it is simply boolean.
182 192 """)
183 193 interrupt = Integer(0, config=True,
184 194 help="""ONLY USED ON WINDOWS
185 195 Interrupt this process when the parent is signaled.
186 196 """)
187 197
188 198 def init_crash_handler(self):
189 199 # Install minimal exception handling
190 200 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
191 201 ostream=sys.__stdout__)
192 202
193 203 def init_poller(self):
194 204 if sys.platform == 'win32':
195 205 if self.interrupt or self.parent_handle:
196 206 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
197 207 elif self.parent_handle:
198 208 self.poller = ParentPollerUnix()
199 209
200 210 def _bind_socket(self, s, port):
201 211 iface = '%s://%s' % (self.transport, self.ip)
202 212 if self.transport == 'tcp':
203 213 if port <= 0:
204 214 port = s.bind_to_random_port(iface)
205 215 else:
206 216 s.bind("tcp://%s:%i" % (self.ip, port))
207 217 elif self.transport == 'ipc':
208 218 if port <= 0:
209 219 port = 1
210 220 path = "%s-%i" % (self.ip, port)
211 221 while os.path.exists(path):
212 222 port = port + 1
213 223 path = "%s-%i" % (self.ip, port)
214 224 else:
215 225 path = "%s-%i" % (self.ip, port)
216 226 s.bind("ipc://%s" % path)
217 227 return port
218 228
219 229 def load_connection_file(self):
220 230 """load ip/port/hmac config from JSON connection file"""
221 231 try:
222 232 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
223 233 except IOError:
224 234 self.log.debug("Connection file not found: %s", self.connection_file)
225 235 # This means I own it, so I will clean it up:
226 236 atexit.register(self.cleanup_connection_file)
227 237 return
228 238 self.log.debug(u"Loading connection file %s", fname)
229 239 with open(fname) as f:
230 240 s = f.read()
231 241 cfg = json.loads(s)
232 242 self.transport = cfg.get('transport', self.transport)
233 243 if self.ip == self._ip_default() and 'ip' in cfg:
234 244 # not overridden by config or cl_args
235 245 self.ip = cfg['ip']
236 246 for channel in ('hb', 'shell', 'iopub', 'stdin', 'control'):
237 247 name = channel + '_port'
238 248 if getattr(self, name) == 0 and name in cfg:
239 249 # not overridden by config or cl_args
240 250 setattr(self, name, cfg[name])
241 251 if 'key' in cfg:
242 252 self.config.Session.key = str_to_bytes(cfg['key'])
243 253
244 254 def write_connection_file(self):
245 255 """write connection info to JSON file"""
246 256 cf = self.abs_connection_file
247 257 self.log.debug("Writing connection file: %s", cf)
248 258 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
249 259 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
250 260 iopub_port=self.iopub_port, control_port=self.control_port)
251 261
252 262 def cleanup_connection_file(self):
253 263 cf = self.abs_connection_file
254 264 self.log.debug("Cleaning up connection file: %s", cf)
255 265 try:
256 266 os.remove(cf)
257 267 except (IOError, OSError):
258 268 pass
259 269
260 270 self.cleanup_ipc_files()
261 271
262 272 def cleanup_ipc_files(self):
263 273 """cleanup ipc files if we wrote them"""
264 274 if self.transport != 'ipc':
265 275 return
266 276 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port, self.control_port):
267 277 ipcfile = "%s-%i" % (self.ip, port)
268 278 try:
269 279 os.remove(ipcfile)
270 280 except (IOError, OSError):
271 281 pass
272 282
273 283 def init_connection_file(self):
274 284 if not self.connection_file:
275 285 self.connection_file = "kernel-%s.json"%os.getpid()
276 286 try:
277 287 self.load_connection_file()
278 288 except Exception:
279 289 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
280 290 self.exit(1)
281 291
282 292 def init_sockets(self):
283 293 # Create a context, a session, and the kernel sockets.
284 294 self.log.info("Starting the kernel at pid: %i", os.getpid())
285 295 context = zmq.Context.instance()
286 296 # Uncomment this to try closing the context.
287 297 # atexit.register(context.term)
288 298
289 299 self.shell_socket = context.socket(zmq.ROUTER)
290 300 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
291 301 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
292 302
293 303 self.iopub_socket = context.socket(zmq.PUB)
294 304 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
295 305 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
296 306
297 307 self.stdin_socket = context.socket(zmq.ROUTER)
298 308 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
299 309 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
300 310
301 311 self.control_socket = context.socket(zmq.ROUTER)
302 312 self.control_port = self._bind_socket(self.control_socket, self.control_port)
303 313 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
304 314
305 315 def init_heartbeat(self):
306 316 """start the heart beating"""
307 317 # heartbeat doesn't share context, because it mustn't be blocked
308 318 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
309 319 hb_ctx = zmq.Context()
310 320 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
311 321 self.hb_port = self.heartbeat.port
312 322 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
313 323 self.heartbeat.start()
314 324
315 325 def log_connection_info(self):
316 326 """display connection info, and store ports"""
317 327 basename = os.path.basename(self.connection_file)
318 328 if basename == self.connection_file or \
319 329 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
320 330 # use shortname
321 331 tail = basename
322 332 if self.profile != 'default':
323 333 tail += " --profile %s" % self.profile
324 334 else:
325 335 tail = self.connection_file
326 336 lines = [
327 337 "To connect another client to this kernel, use:",
328 338 " --existing %s" % tail,
329 339 ]
330 340 # log connection info
331 341 # info-level, so often not shown.
332 342 # frontends should use the %connect_info magic
333 343 # to see the connection info
334 344 for line in lines:
335 345 self.log.info(line)
336 346 # also raw print to the terminal if no parent_handle (`ipython kernel`)
337 347 if not self.parent_handle:
348 io.rprint(_ctrl_c_message)
338 349 for line in lines:
339 350 io.rprint(line)
340 351
341 352 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
342 353 stdin=self.stdin_port, hb=self.hb_port,
343 354 control=self.control_port)
344 355
345 356 def init_session(self):
346 357 """create our session object"""
347 358 default_secure(self.config)
348 359 self.session = Session(parent=self, username=u'kernel')
349 360
350 361 def init_blackhole(self):
351 362 """redirects stdout/stderr to devnull if necessary"""
352 363 if self.no_stdout or self.no_stderr:
353 364 blackhole = open(os.devnull, 'w')
354 365 if self.no_stdout:
355 366 sys.stdout = sys.__stdout__ = blackhole
356 367 if self.no_stderr:
357 368 sys.stderr = sys.__stderr__ = blackhole
358 369
359 370 def init_io(self):
360 371 """Redirect input streams and set a display hook."""
361 372 if self.outstream_class:
362 373 outstream_factory = import_item(str(self.outstream_class))
363 374 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
364 375 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
365 376 if self.displayhook_class:
366 377 displayhook_factory = import_item(str(self.displayhook_class))
367 378 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
368 379
369 380 def init_signal(self):
370 381 signal.signal(signal.SIGINT, signal.SIG_IGN)
371 382
372 383 def init_kernel(self):
373 384 """Create the Kernel object itself"""
374 385 shell_stream = ZMQStream(self.shell_socket)
375 386 control_stream = ZMQStream(self.control_socket)
376 387
377 388 kernel_factory = import_item(str(self.kernel_class))
378 389
379 390 kernel = kernel_factory(parent=self, session=self.session,
380 391 shell_streams=[shell_stream, control_stream],
381 392 iopub_socket=self.iopub_socket,
382 393 stdin_socket=self.stdin_socket,
383 394 log=self.log,
384 395 profile_dir=self.profile_dir,
385 396 )
386 397 kernel.record_ports(self.ports)
387 398 self.kernel = kernel
388 399
389 400 def init_gui_pylab(self):
390 401 """Enable GUI event loop integration, taking pylab into account."""
391 402
392 403 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
393 404 # to ensure that any exception is printed straight to stderr.
394 405 # Normally _showtraceback associates the reply with an execution,
395 406 # which means frontends will never draw it, as this exception
396 407 # is not associated with any execute request.
397 408
398 409 shell = self.shell
399 410 _showtraceback = shell._showtraceback
400 411 try:
401 412 # replace pyerr-sending traceback with stderr
402 413 def print_tb(etype, evalue, stb):
403 414 print ("GUI event loop or pylab initialization failed",
404 415 file=io.stderr)
405 416 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
406 417 shell._showtraceback = print_tb
407 418 InteractiveShellApp.init_gui_pylab(self)
408 419 finally:
409 420 shell._showtraceback = _showtraceback
410 421
411 422 def init_shell(self):
412 423 self.shell = self.kernel.shell
413 424 self.shell.configurables.append(self)
414 425
415 426 @catch_config_error
416 427 def initialize(self, argv=None):
417 428 super(IPKernelApp, self).initialize(argv)
418 429 self.init_blackhole()
419 430 self.init_connection_file()
420 431 self.init_session()
421 432 self.init_poller()
422 433 self.init_sockets()
423 434 self.init_heartbeat()
424 435 # writing/displaying connection info must be *after* init_sockets/heartbeat
425 436 self.log_connection_info()
426 437 self.write_connection_file()
427 438 self.init_io()
428 439 self.init_signal()
429 440 self.init_kernel()
430 441 # shell init steps
431 442 self.init_path()
432 443 self.init_shell()
433 444 self.init_gui_pylab()
434 445 self.init_extensions()
435 446 self.init_code()
436 447 # flush stdout/stderr, so that anything written to these streams during
437 448 # initialization do not get associated with the first execution request
438 449 sys.stdout.flush()
439 450 sys.stderr.flush()
440 451
441 452 def start(self):
442 453 if self.poller is not None:
443 454 self.poller.start()
444 455 self.kernel.start()
445 456 try:
446 457 ioloop.IOLoop.instance().start()
447 458 except KeyboardInterrupt:
448 459 pass
449 460
450 461
451 462 def main():
452 463 """Run an IPKernel as an application"""
453 464 app = IPKernelApp.instance()
454 465 app.initialize()
455 466 app.start()
456 467
457 468
458 469 if __name__ == '__main__':
459 470 main()
General Comments 0
You need to be logged in to leave comments. Login now