##// END OF EJS Templates
Merge pull request #2811 from minrk/ipc_defaults...
Min RK -
r9245:8c1236c4 merge
parent child Browse files
Show More
@@ -1,378 +1,361
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations. This is a
4 input, there is no real readline support, among other limitations. This is a
5 refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py
5 refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py
6
6
7 Authors:
7 Authors:
8
8
9 * Evan Patterson
9 * Evan Patterson
10 * Min RK
10 * Min RK
11 * Erik Tollerud
11 * Erik Tollerud
12 * Fernando Perez
12 * Fernando Perez
13 * Bussonnier Matthias
13 * Bussonnier Matthias
14 * Thomas Kluyver
14 * Thomas Kluyver
15 * Paul Ivanov
15 * Paul Ivanov
16
16
17 """
17 """
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 # stdlib imports
23 # stdlib imports
24 import atexit
24 import atexit
25 import json
25 import json
26 import os
26 import os
27 import shutil
27 import shutil
28 import signal
28 import signal
29 import sys
29 import sys
30 import uuid
30 import uuid
31
31
32
32
33 # Local imports
33 # Local imports
34 from IPython.config.application import boolean_flag
34 from IPython.config.application import boolean_flag
35 from IPython.config.configurable import Configurable
35 from IPython.config.configurable import Configurable
36 from IPython.core.profiledir import ProfileDir
36 from IPython.core.profiledir import ProfileDir
37 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
37 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
38 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
38 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
39 from IPython.utils.path import filefind
39 from IPython.utils.path import filefind
40 from IPython.utils.py3compat import str_to_bytes
40 from IPython.utils.py3compat import str_to_bytes
41 from IPython.utils.traitlets import (
41 from IPython.utils.traitlets import (
42 Dict, List, Unicode, CUnicode, Int, CBool, Any, CaselessStrEnum
42 Dict, List, Unicode, CUnicode, Int, CBool, Any, CaselessStrEnum
43 )
43 )
44 from IPython.zmq.ipkernel import (
44 from IPython.zmq.ipkernel import (
45 flags as ipkernel_flags,
45 flags as ipkernel_flags,
46 aliases as ipkernel_aliases,
46 aliases as ipkernel_aliases,
47 IPKernelApp
47 IPKernelApp
48 )
48 )
49 from IPython.zmq.session import Session, default_secure
49 from IPython.zmq.session import Session, default_secure
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # Network Constants
53 # Network Constants
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
56 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
57
57
58 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
59 # Globals
59 # Globals
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61
61
62
62
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64 # Aliases and Flags
64 # Aliases and Flags
65 #-----------------------------------------------------------------------------
65 #-----------------------------------------------------------------------------
66
66
67 flags = dict(ipkernel_flags)
67 flags = dict(ipkernel_flags)
68
68
69 # the flags that are specific to the frontend
69 # the flags that are specific to the frontend
70 # these must be scrubbed before being passed to the kernel,
70 # these must be scrubbed before being passed to the kernel,
71 # or it will raise an error on unrecognized flags
71 # or it will raise an error on unrecognized flags
72 app_flags = {
72 app_flags = {
73 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
73 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
74 "Connect to an existing kernel. If no argument specified, guess most recent"),
74 "Connect to an existing kernel. If no argument specified, guess most recent"),
75 }
75 }
76 app_flags.update(boolean_flag(
76 app_flags.update(boolean_flag(
77 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
77 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
78 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
78 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
79 to force a direct exit without any confirmation.
79 to force a direct exit without any confirmation.
80 """,
80 """,
81 """Don't prompt the user when exiting. This will terminate the kernel
81 """Don't prompt the user when exiting. This will terminate the kernel
82 if it is owned by the frontend, and leave it alive if it is external.
82 if it is owned by the frontend, and leave it alive if it is external.
83 """
83 """
84 ))
84 ))
85 flags.update(app_flags)
85 flags.update(app_flags)
86
86
87 aliases = dict(ipkernel_aliases)
87 aliases = dict(ipkernel_aliases)
88
88
89 # also scrub aliases from the frontend
89 # also scrub aliases from the frontend
90 app_aliases = dict(
90 app_aliases = dict(
91 ip = 'KernelManager.ip',
92 transport = 'KernelManager.transport',
91 hb = 'IPythonConsoleApp.hb_port',
93 hb = 'IPythonConsoleApp.hb_port',
92 shell = 'IPythonConsoleApp.shell_port',
94 shell = 'IPythonConsoleApp.shell_port',
93 iopub = 'IPythonConsoleApp.iopub_port',
95 iopub = 'IPythonConsoleApp.iopub_port',
94 stdin = 'IPythonConsoleApp.stdin_port',
96 stdin = 'IPythonConsoleApp.stdin_port',
95 ip = 'IPythonConsoleApp.ip',
96 existing = 'IPythonConsoleApp.existing',
97 existing = 'IPythonConsoleApp.existing',
97 f = 'IPythonConsoleApp.connection_file',
98 f = 'IPythonConsoleApp.connection_file',
98
99
99
100
100 ssh = 'IPythonConsoleApp.sshserver',
101 ssh = 'IPythonConsoleApp.sshserver',
101 )
102 )
102 aliases.update(app_aliases)
103 aliases.update(app_aliases)
103
104
104 #-----------------------------------------------------------------------------
105 #-----------------------------------------------------------------------------
105 # Classes
106 # Classes
106 #-----------------------------------------------------------------------------
107 #-----------------------------------------------------------------------------
107
108
108 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
109 # IPythonConsole
110 # IPythonConsole
110 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
111
112
112 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
113 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
113
114
114 try:
115 try:
115 from IPython.zmq.pylab.backend_inline import InlineBackend
116 from IPython.zmq.pylab.backend_inline import InlineBackend
116 except ImportError:
117 except ImportError:
117 pass
118 pass
118 else:
119 else:
119 classes.append(InlineBackend)
120 classes.append(InlineBackend)
120
121
121 class IPythonConsoleApp(Configurable):
122 class IPythonConsoleApp(Configurable):
122 name = 'ipython-console-mixin'
123 name = 'ipython-console-mixin'
123 default_config_file_name='ipython_config.py'
124 default_config_file_name='ipython_config.py'
124
125
125 description = """
126 description = """
126 The IPython Mixin Console.
127 The IPython Mixin Console.
127
128
128 This class contains the common portions of console client (QtConsole,
129 This class contains the common portions of console client (QtConsole,
129 ZMQ-based terminal console, etc). It is not a full console, in that
130 ZMQ-based terminal console, etc). It is not a full console, in that
130 launched terminal subprocesses will not be able to accept input.
131 launched terminal subprocesses will not be able to accept input.
131
132
132 The Console using this mixing supports various extra features beyond
133 The Console using this mixing supports various extra features beyond
133 the single-process Terminal IPython shell, such as connecting to
134 the single-process Terminal IPython shell, such as connecting to
134 existing kernel, via:
135 existing kernel, via:
135
136
136 ipython <appname> --existing
137 ipython <appname> --existing
137
138
138 as well as tunnel via SSH
139 as well as tunnel via SSH
139
140
140 """
141 """
141
142
142 classes = classes
143 classes = classes
143 flags = Dict(flags)
144 flags = Dict(flags)
144 aliases = Dict(aliases)
145 aliases = Dict(aliases)
145 kernel_manager_class = BlockingKernelManager
146 kernel_manager_class = BlockingKernelManager
146
147
147 kernel_argv = List(Unicode)
148 kernel_argv = List(Unicode)
148 # frontend flags&aliases to be stripped when building kernel_argv
149 # frontend flags&aliases to be stripped when building kernel_argv
149 frontend_flags = Any(app_flags)
150 frontend_flags = Any(app_flags)
150 frontend_aliases = Any(app_aliases)
151 frontend_aliases = Any(app_aliases)
151
152
152 # create requested profiles by default, if they don't exist:
153 # create requested profiles by default, if they don't exist:
153 auto_create = CBool(True)
154 auto_create = CBool(True)
154 # connection info:
155 # connection info:
155
156
156 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
157
158 ip = Unicode(config=True,
159 help="""Set the kernel\'s IP address [default localhost].
160 If the IP address is something other than localhost, then
161 Consoles on other machines will be able to connect
162 to the Kernel, so be careful!"""
163 )
164 def _ip_default(self):
165 if self.transport == 'tcp':
166 return LOCALHOST
167 else:
168 # this can fire early if ip is given,
169 # in which case our return value is meaningless
170 if not hasattr(self, 'profile_dir'):
171 return ''
172 ipcdir = os.path.join(self.profile_dir.security_dir, 'kernel-%s' % os.getpid())
173 os.makedirs(ipcdir)
174 atexit.register(lambda : shutil.rmtree(ipcdir))
175 return os.path.join(ipcdir, 'ipc')
176
177 sshserver = Unicode('', config=True,
157 sshserver = Unicode('', config=True,
178 help="""The SSH server to use to connect to the kernel.""")
158 help="""The SSH server to use to connect to the kernel.""")
179 sshkey = Unicode('', config=True,
159 sshkey = Unicode('', config=True,
180 help="""Path to the ssh key to use for logging in to the ssh server.""")
160 help="""Path to the ssh key to use for logging in to the ssh server.""")
181
161
182 hb_port = Int(0, config=True,
162 hb_port = Int(0, config=True,
183 help="set the heartbeat port [default: random]")
163 help="set the heartbeat port [default: random]")
184 shell_port = Int(0, config=True,
164 shell_port = Int(0, config=True,
185 help="set the shell (ROUTER) port [default: random]")
165 help="set the shell (ROUTER) port [default: random]")
186 iopub_port = Int(0, config=True,
166 iopub_port = Int(0, config=True,
187 help="set the iopub (PUB) port [default: random]")
167 help="set the iopub (PUB) port [default: random]")
188 stdin_port = Int(0, config=True,
168 stdin_port = Int(0, config=True,
189 help="set the stdin (DEALER) port [default: random]")
169 help="set the stdin (DEALER) port [default: random]")
190 connection_file = Unicode('', config=True,
170 connection_file = Unicode('', config=True,
191 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
171 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
192
172
193 This file will contain the IP, ports, and authentication key needed to connect
173 This file will contain the IP, ports, and authentication key needed to connect
194 clients to this kernel. By default, this file will be created in the security-dir
174 clients to this kernel. By default, this file will be created in the security-dir
195 of the current profile, but can be specified by absolute path.
175 of the current profile, but can be specified by absolute path.
196 """)
176 """)
197 def _connection_file_default(self):
177 def _connection_file_default(self):
198 return 'kernel-%i.json' % os.getpid()
178 return 'kernel-%i.json' % os.getpid()
199
179
200 existing = CUnicode('', config=True,
180 existing = CUnicode('', config=True,
201 help="""Connect to an already running kernel""")
181 help="""Connect to an already running kernel""")
202
182
203 confirm_exit = CBool(True, config=True,
183 confirm_exit = CBool(True, config=True,
204 help="""
184 help="""
205 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
185 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
206 to force a direct exit without any confirmation.""",
186 to force a direct exit without any confirmation.""",
207 )
187 )
208
188
209
189
210 def build_kernel_argv(self, argv=None):
190 def build_kernel_argv(self, argv=None):
211 """build argv to be passed to kernel subprocess"""
191 """build argv to be passed to kernel subprocess"""
212 if argv is None:
192 if argv is None:
213 argv = sys.argv[1:]
193 argv = sys.argv[1:]
214 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
194 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
215 # kernel should inherit default config file from frontend
195 # kernel should inherit default config file from frontend
216 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
196 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
217
197
218 def init_connection_file(self):
198 def init_connection_file(self):
219 """find the connection file, and load the info if found.
199 """find the connection file, and load the info if found.
220
200
221 The current working directory and the current profile's security
201 The current working directory and the current profile's security
222 directory will be searched for the file if it is not given by
202 directory will be searched for the file if it is not given by
223 absolute path.
203 absolute path.
224
204
225 When attempting to connect to an existing kernel and the `--existing`
205 When attempting to connect to an existing kernel and the `--existing`
226 argument does not match an existing file, it will be interpreted as a
206 argument does not match an existing file, it will be interpreted as a
227 fileglob, and the matching file in the current profile's security dir
207 fileglob, and the matching file in the current profile's security dir
228 with the latest access time will be used.
208 with the latest access time will be used.
229
209
230 After this method is called, self.connection_file contains the *full path*
210 After this method is called, self.connection_file contains the *full path*
231 to the connection file, never just its name.
211 to the connection file, never just its name.
232 """
212 """
233 if self.existing:
213 if self.existing:
234 try:
214 try:
235 cf = find_connection_file(self.existing)
215 cf = find_connection_file(self.existing)
236 except Exception:
216 except Exception:
237 self.log.critical("Could not find existing kernel connection file %s", self.existing)
217 self.log.critical("Could not find existing kernel connection file %s", self.existing)
238 self.exit(1)
218 self.exit(1)
239 self.log.info("Connecting to existing kernel: %s" % cf)
219 self.log.info("Connecting to existing kernel: %s" % cf)
240 self.connection_file = cf
220 self.connection_file = cf
241 else:
221 else:
242 # not existing, check if we are going to write the file
222 # not existing, check if we are going to write the file
243 # and ensure that self.connection_file is a full path, not just the shortname
223 # and ensure that self.connection_file is a full path, not just the shortname
244 try:
224 try:
245 cf = find_connection_file(self.connection_file)
225 cf = find_connection_file(self.connection_file)
246 except Exception:
226 except Exception:
247 # file might not exist
227 # file might not exist
248 if self.connection_file == os.path.basename(self.connection_file):
228 if self.connection_file == os.path.basename(self.connection_file):
249 # just shortname, put it in security dir
229 # just shortname, put it in security dir
250 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
230 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
251 else:
231 else:
252 cf = self.connection_file
232 cf = self.connection_file
253 self.connection_file = cf
233 self.connection_file = cf
254
234
255 # should load_connection_file only be used for existing?
235 # should load_connection_file only be used for existing?
256 # as it is now, this allows reusing ports if an existing
236 # as it is now, this allows reusing ports if an existing
257 # file is requested
237 # file is requested
258 try:
238 try:
259 self.load_connection_file()
239 self.load_connection_file()
260 except Exception:
240 except Exception:
261 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
241 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
262 self.exit(1)
242 self.exit(1)
263
243
264 def load_connection_file(self):
244 def load_connection_file(self):
265 """load ip/port/hmac config from JSON connection file"""
245 """load ip/port/hmac config from JSON connection file"""
266 # this is identical to KernelApp.load_connection_file
246 # this is identical to KernelApp.load_connection_file
267 # perhaps it can be centralized somewhere?
247 # perhaps it can be centralized somewhere?
268 try:
248 try:
269 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
249 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
270 except IOError:
250 except IOError:
271 self.log.debug("Connection File not found: %s", self.connection_file)
251 self.log.debug("Connection File not found: %s", self.connection_file)
272 return
252 return
273 self.log.debug(u"Loading connection file %s", fname)
253 self.log.debug(u"Loading connection file %s", fname)
274 with open(fname) as f:
254 with open(fname) as f:
275 cfg = json.load(f)
255 cfg = json.load(f)
276
256
277 self.transport = cfg.get('transport', 'tcp')
257 self.config.KernelManager.transport = cfg.get('transport', 'tcp')
278 if 'ip' in cfg:
258 self.config.KernelManager.ip = cfg.get('ip', LOCALHOST)
279 self.ip = cfg['ip']
259
280 for channel in ('hb', 'shell', 'iopub', 'stdin'):
260 for channel in ('hb', 'shell', 'iopub', 'stdin'):
281 name = channel + '_port'
261 name = channel + '_port'
282 if getattr(self, name) == 0 and name in cfg:
262 if getattr(self, name) == 0 and name in cfg:
283 # not overridden by config or cl_args
263 # not overridden by config or cl_args
284 setattr(self, name, cfg[name])
264 setattr(self, name, cfg[name])
285 if 'key' in cfg:
265 if 'key' in cfg:
286 self.config.Session.key = str_to_bytes(cfg['key'])
266 self.config.Session.key = str_to_bytes(cfg['key'])
287
288
267
289 def init_ssh(self):
268 def init_ssh(self):
290 """set up ssh tunnels, if needed."""
269 """set up ssh tunnels, if needed."""
291 if not self.sshserver and not self.sshkey:
270 if not self.existing or (not self.sshserver and not self.sshkey):
292 return
271 return
293
272
294 if self.transport != 'tcp':
273 self.load_connection_file()
295 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", self.transport)
274
296 return
275 transport = self.config.KernelManager.transport
276 ip = self.config.KernelManager.ip
277
278 if transport != 'tcp':
279 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
280 sys.exit(-1)
297
281
298 if self.sshkey and not self.sshserver:
282 if self.sshkey and not self.sshserver:
299 # specifying just the key implies that we are connecting directly
283 # specifying just the key implies that we are connecting directly
300 self.sshserver = self.ip
284 self.sshserver = ip
301 self.ip = LOCALHOST
285 ip = LOCALHOST
302
286
303 # build connection dict for tunnels:
287 # build connection dict for tunnels:
304 info = dict(ip=self.ip,
288 info = dict(ip=ip,
305 shell_port=self.shell_port,
289 shell_port=self.shell_port,
306 iopub_port=self.iopub_port,
290 iopub_port=self.iopub_port,
307 stdin_port=self.stdin_port,
291 stdin_port=self.stdin_port,
308 hb_port=self.hb_port
292 hb_port=self.hb_port
309 )
293 )
310
294
311 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
295 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
312
296
313 # tunnels return a new set of ports, which will be on localhost:
297 # tunnels return a new set of ports, which will be on localhost:
314 self.ip = LOCALHOST
298 self.config.KernelManager.ip = LOCALHOST
315 try:
299 try:
316 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
300 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
317 except:
301 except:
318 # even catch KeyboardInterrupt
302 # even catch KeyboardInterrupt
319 self.log.error("Could not setup tunnels", exc_info=True)
303 self.log.error("Could not setup tunnels", exc_info=True)
320 self.exit(1)
304 self.exit(1)
321
305
322 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
306 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
323
307
324 cf = self.connection_file
308 cf = self.connection_file
325 base,ext = os.path.splitext(cf)
309 base,ext = os.path.splitext(cf)
326 base = os.path.basename(base)
310 base = os.path.basename(base)
327 self.connection_file = os.path.basename(base)+'-ssh'+ext
311 self.connection_file = os.path.basename(base)+'-ssh'+ext
328 self.log.critical("To connect another client via this tunnel, use:")
312 self.log.critical("To connect another client via this tunnel, use:")
329 self.log.critical("--existing %s" % self.connection_file)
313 self.log.critical("--existing %s" % self.connection_file)
330
314
331 def _new_connection_file(self):
315 def _new_connection_file(self):
332 cf = ''
316 cf = ''
333 while not cf:
317 while not cf:
334 # we don't need a 128b id to distinguish kernels, use more readable
318 # we don't need a 128b id to distinguish kernels, use more readable
335 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
319 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
336 # kernels can subclass.
320 # kernels can subclass.
337 ident = str(uuid.uuid4()).split('-')[-1]
321 ident = str(uuid.uuid4()).split('-')[-1]
338 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
322 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
339 # only keep if it's actually new. Protect against unlikely collision
323 # only keep if it's actually new. Protect against unlikely collision
340 # in 48b random search space
324 # in 48b random search space
341 cf = cf if not os.path.exists(cf) else ''
325 cf = cf if not os.path.exists(cf) else ''
342 return cf
326 return cf
343
327
344 def init_kernel_manager(self):
328 def init_kernel_manager(self):
345 # Don't let Qt or ZMQ swallow KeyboardInterupts.
329 # Don't let Qt or ZMQ swallow KeyboardInterupts.
346 signal.signal(signal.SIGINT, signal.SIG_DFL)
330 signal.signal(signal.SIGINT, signal.SIG_DFL)
347
331
348 # Create a KernelManager and start a kernel.
332 # Create a KernelManager and start a kernel.
349 self.kernel_manager = self.kernel_manager_class(
333 self.kernel_manager = self.kernel_manager_class(
350 transport=self.transport,
351 ip=self.ip,
352 shell_port=self.shell_port,
334 shell_port=self.shell_port,
353 iopub_port=self.iopub_port,
335 iopub_port=self.iopub_port,
354 stdin_port=self.stdin_port,
336 stdin_port=self.stdin_port,
355 hb_port=self.hb_port,
337 hb_port=self.hb_port,
356 connection_file=self.connection_file,
338 connection_file=self.connection_file,
357 config=self.config,
339 config=self.config,
358 )
340 )
359 # start the kernel
341 # start the kernel
360 if not self.existing:
342 if not self.existing:
361 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
343 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
344 atexit.register(self.kernel_manager.cleanup_ipc_files)
362 elif self.sshserver:
345 elif self.sshserver:
363 # ssh, write new connection file
346 # ssh, write new connection file
364 self.kernel_manager.write_connection_file()
347 self.kernel_manager.write_connection_file()
365 atexit.register(self.kernel_manager.cleanup_connection_file)
348 atexit.register(self.kernel_manager.cleanup_connection_file)
366 self.kernel_manager.start_channels()
349 self.kernel_manager.start_channels()
367
350
368
351
369 def initialize(self, argv=None):
352 def initialize(self, argv=None):
370 """
353 """
371 Classes which mix this class in should call:
354 Classes which mix this class in should call:
372 IPythonConsoleApp.initialize(self,argv)
355 IPythonConsoleApp.initialize(self,argv)
373 """
356 """
374 self.init_connection_file()
357 self.init_connection_file()
375 default_secure(self.config)
358 default_secure(self.config)
376 self.init_ssh()
359 self.init_ssh()
377 self.init_kernel_manager()
360 self.init_kernel_manager()
378
361
@@ -1,644 +1,645
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import uuid
31 import uuid
32 import webbrowser
32 import webbrowser
33
33
34 # Third party
34 # Third party
35 import zmq
35 import zmq
36 from jinja2 import Environment, FileSystemLoader
36 from jinja2 import Environment, FileSystemLoader
37
37
38 # Install the pyzmq ioloop. This has to be done before anything else from
38 # Install the pyzmq ioloop. This has to be done before anything else from
39 # tornado is imported.
39 # tornado is imported.
40 from zmq.eventloop import ioloop
40 from zmq.eventloop import ioloop
41 ioloop.install()
41 ioloop.install()
42
42
43 from tornado import httpserver
43 from tornado import httpserver
44 from tornado import web
44 from tornado import web
45
45
46 # Our own libraries
46 # Our own libraries
47 from .kernelmanager import MappingKernelManager
47 from .kernelmanager import MappingKernelManager
48 from .handlers import (LoginHandler, LogoutHandler,
48 from .handlers import (LoginHandler, LogoutHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
54 FileFindHandler,
54 FileFindHandler,
55 )
55 )
56 from .nbmanager import NotebookManager
56 from .nbmanager import NotebookManager
57 from .filenbmanager import FileNotebookManager
57 from .filenbmanager import FileNotebookManager
58 from .clustermanager import ClusterManager
58 from .clustermanager import ClusterManager
59
59
60 from IPython.config.application import catch_config_error, boolean_flag
60 from IPython.config.application import catch_config_error, boolean_flag
61 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.application import BaseIPythonApplication
62 from IPython.core.profiledir import ProfileDir
62 from IPython.core.profiledir import ProfileDir
63 from IPython.frontend.consoleapp import IPythonConsoleApp
63 from IPython.frontend.consoleapp import IPythonConsoleApp
64 from IPython.lib.kernel import swallow_argv
64 from IPython.lib.kernel import swallow_argv
65 from IPython.zmq.session import Session, default_secure
65 from IPython.zmq.session import Session, default_secure
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
67 from IPython.zmq.ipkernel import (
67 from IPython.zmq.ipkernel import (
68 flags as ipkernel_flags,
68 flags as ipkernel_flags,
69 aliases as ipkernel_aliases,
69 aliases as ipkernel_aliases,
70 IPKernelApp
70 IPKernelApp
71 )
71 )
72 from IPython.utils.importstring import import_item
72 from IPython.utils.importstring import import_item
73 from IPython.utils.traitlets import (
73 from IPython.utils.traitlets import (
74 Dict, Unicode, Integer, List, Enum, Bool,
74 Dict, Unicode, Integer, List, Enum, Bool,
75 DottedObjectName
75 DottedObjectName
76 )
76 )
77 from IPython.utils import py3compat
77 from IPython.utils import py3compat
78 from IPython.utils.path import filefind
78 from IPython.utils.path import filefind
79
79
80 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
81 # Module globals
81 # Module globals
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83
83
84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
88 _cluster_action_regex = r"(?P<action>start|stop)"
88 _cluster_action_regex = r"(?P<action>start|stop)"
89
89
90
90
91 LOCALHOST = '127.0.0.1'
91 LOCALHOST = '127.0.0.1'
92
92
93 _examples = """
93 _examples = """
94 ipython notebook # start the notebook
94 ipython notebook # start the notebook
95 ipython notebook --profile=sympy # use the sympy profile
95 ipython notebook --profile=sympy # use the sympy profile
96 ipython notebook --pylab=inline # pylab in inline plotting mode
96 ipython notebook --pylab=inline # pylab in inline plotting mode
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
99 """
99 """
100
100
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 # Helper functions
102 # Helper functions
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104
104
105 def url_path_join(a,b):
105 def url_path_join(a,b):
106 if a.endswith('/') and b.startswith('/'):
106 if a.endswith('/') and b.startswith('/'):
107 return a[:-1]+b
107 return a[:-1]+b
108 else:
108 else:
109 return a+b
109 return a+b
110
110
111 def random_ports(port, n):
111 def random_ports(port, n):
112 """Generate a list of n random ports near the given port.
112 """Generate a list of n random ports near the given port.
113
113
114 The first 5 ports will be sequential, and the remaining n-5 will be
114 The first 5 ports will be sequential, and the remaining n-5 will be
115 randomly selected in the range [port-2*n, port+2*n].
115 randomly selected in the range [port-2*n, port+2*n].
116 """
116 """
117 for i in range(min(5, n)):
117 for i in range(min(5, n)):
118 yield port + i
118 yield port + i
119 for i in range(n-5):
119 for i in range(n-5):
120 yield port + random.randint(-2*n, 2*n)
120 yield port + random.randint(-2*n, 2*n)
121
121
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123 # The Tornado web application
123 # The Tornado web application
124 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
125
125
126 class NotebookWebApplication(web.Application):
126 class NotebookWebApplication(web.Application):
127
127
128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
129 cluster_manager, log,
129 cluster_manager, log,
130 base_project_url, settings_overrides):
130 base_project_url, settings_overrides):
131 handlers = [
131 handlers = [
132 (r"/", ProjectDashboardHandler),
132 (r"/", ProjectDashboardHandler),
133 (r"/login", LoginHandler),
133 (r"/login", LoginHandler),
134 (r"/logout", LogoutHandler),
134 (r"/logout", LogoutHandler),
135 (r"/new", NewHandler),
135 (r"/new", NewHandler),
136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
139 (r"/kernels", MainKernelHandler),
139 (r"/kernels", MainKernelHandler),
140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
144 (r"/notebooks", NotebookRootHandler),
144 (r"/notebooks", NotebookRootHandler),
145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
146 (r"/rstservice/render", RSTHandler),
146 (r"/rstservice/render", RSTHandler),
147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
148 (r"/clusters", MainClusterHandler),
148 (r"/clusters", MainClusterHandler),
149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
151 ]
151 ]
152
152
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # base_project_url will always be unicode, which will in turn
154 # base_project_url will always be unicode, which will in turn
155 # make the patterns unicode, and ultimately result in unicode
155 # make the patterns unicode, and ultimately result in unicode
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # This enforces that base_project_url be ascii in that situation.
157 # This enforces that base_project_url be ascii in that situation.
158 #
158 #
159 # Note that the URLs these patterns check against are escaped,
159 # Note that the URLs these patterns check against are escaped,
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162
162
163 settings = dict(
163 settings = dict(
164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
165 static_path=ipython_app.static_file_path,
165 static_path=ipython_app.static_file_path,
166 static_handler_class = FileFindHandler,
166 static_handler_class = FileFindHandler,
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 cookie_secret=os.urandom(1024),
168 cookie_secret=os.urandom(1024),
169 login_url=url_path_join(base_project_url,'/login'),
169 login_url=url_path_join(base_project_url,'/login'),
170 cookie_name='username-%s' % uuid.uuid4(),
170 cookie_name='username-%s' % uuid.uuid4(),
171 )
171 )
172
172
173 # allow custom overrides for the tornado web app.
173 # allow custom overrides for the tornado web app.
174 settings.update(settings_overrides)
174 settings.update(settings_overrides)
175
175
176 # prepend base_project_url onto the patterns that we match
176 # prepend base_project_url onto the patterns that we match
177 new_handlers = []
177 new_handlers = []
178 for handler in handlers:
178 for handler in handlers:
179 pattern = url_path_join(base_project_url, handler[0])
179 pattern = url_path_join(base_project_url, handler[0])
180 new_handler = tuple([pattern]+list(handler[1:]))
180 new_handler = tuple([pattern]+list(handler[1:]))
181 new_handlers.append( new_handler )
181 new_handlers.append( new_handler )
182
182
183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
184
184
185 self.kernel_manager = kernel_manager
185 self.kernel_manager = kernel_manager
186 self.notebook_manager = notebook_manager
186 self.notebook_manager = notebook_manager
187 self.cluster_manager = cluster_manager
187 self.cluster_manager = cluster_manager
188 self.ipython_app = ipython_app
188 self.ipython_app = ipython_app
189 self.read_only = self.ipython_app.read_only
189 self.read_only = self.ipython_app.read_only
190 self.config = self.ipython_app.config
190 self.config = self.ipython_app.config
191 self.log = log
191 self.log = log
192 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
192 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
193
193
194
194
195
195
196 #-----------------------------------------------------------------------------
196 #-----------------------------------------------------------------------------
197 # Aliases and Flags
197 # Aliases and Flags
198 #-----------------------------------------------------------------------------
198 #-----------------------------------------------------------------------------
199
199
200 flags = dict(ipkernel_flags)
200 flags = dict(ipkernel_flags)
201 flags['no-browser']=(
201 flags['no-browser']=(
202 {'NotebookApp' : {'open_browser' : False}},
202 {'NotebookApp' : {'open_browser' : False}},
203 "Don't open the notebook in a browser after startup."
203 "Don't open the notebook in a browser after startup."
204 )
204 )
205 flags['no-mathjax']=(
205 flags['no-mathjax']=(
206 {'NotebookApp' : {'enable_mathjax' : False}},
206 {'NotebookApp' : {'enable_mathjax' : False}},
207 """Disable MathJax
207 """Disable MathJax
208
208
209 MathJax is the javascript library IPython uses to render math/LaTeX. It is
209 MathJax is the javascript library IPython uses to render math/LaTeX. It is
210 very large, so you may want to disable it if you have a slow internet
210 very large, so you may want to disable it if you have a slow internet
211 connection, or for offline use of the notebook.
211 connection, or for offline use of the notebook.
212
212
213 When disabled, equations etc. will appear as their untransformed TeX source.
213 When disabled, equations etc. will appear as their untransformed TeX source.
214 """
214 """
215 )
215 )
216 flags['read-only'] = (
216 flags['read-only'] = (
217 {'NotebookApp' : {'read_only' : True}},
217 {'NotebookApp' : {'read_only' : True}},
218 """Allow read-only access to notebooks.
218 """Allow read-only access to notebooks.
219
219
220 When using a password to protect the notebook server, this flag
220 When using a password to protect the notebook server, this flag
221 allows unauthenticated clients to view the notebook list, and
221 allows unauthenticated clients to view the notebook list, and
222 individual notebooks, but not edit them, start kernels, or run
222 individual notebooks, but not edit them, start kernels, or run
223 code.
223 code.
224
224
225 If no password is set, the server will be entirely read-only.
225 If no password is set, the server will be entirely read-only.
226 """
226 """
227 )
227 )
228
228
229 # Add notebook manager flags
229 # Add notebook manager flags
230 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
230 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
231 'Auto-save a .py script everytime the .ipynb notebook is saved',
231 'Auto-save a .py script everytime the .ipynb notebook is saved',
232 'Do not auto-save .py scripts for every notebook'))
232 'Do not auto-save .py scripts for every notebook'))
233
233
234 # the flags that are specific to the frontend
234 # the flags that are specific to the frontend
235 # these must be scrubbed before being passed to the kernel,
235 # these must be scrubbed before being passed to the kernel,
236 # or it will raise an error on unrecognized flags
236 # or it will raise an error on unrecognized flags
237 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
237 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
238
238
239 aliases = dict(ipkernel_aliases)
239 aliases = dict(ipkernel_aliases)
240
240
241 aliases.update({
241 aliases.update({
242 'ip': 'NotebookApp.ip',
242 'ip': 'NotebookApp.ip',
243 'port': 'NotebookApp.port',
243 'port': 'NotebookApp.port',
244 'port-retries': 'NotebookApp.port_retries',
244 'port-retries': 'NotebookApp.port_retries',
245 'transport': 'KernelManager.transport',
245 'keyfile': 'NotebookApp.keyfile',
246 'keyfile': 'NotebookApp.keyfile',
246 'certfile': 'NotebookApp.certfile',
247 'certfile': 'NotebookApp.certfile',
247 'notebook-dir': 'NotebookManager.notebook_dir',
248 'notebook-dir': 'NotebookManager.notebook_dir',
248 'browser': 'NotebookApp.browser',
249 'browser': 'NotebookApp.browser',
249 })
250 })
250
251
251 # remove ipkernel flags that are singletons, and don't make sense in
252 # remove ipkernel flags that are singletons, and don't make sense in
252 # multi-kernel evironment:
253 # multi-kernel evironment:
253 aliases.pop('f', None)
254 aliases.pop('f', None)
254
255
255 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
256 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
256 u'notebook-dir']
257 u'notebook-dir']
257
258
258 #-----------------------------------------------------------------------------
259 #-----------------------------------------------------------------------------
259 # NotebookApp
260 # NotebookApp
260 #-----------------------------------------------------------------------------
261 #-----------------------------------------------------------------------------
261
262
262 class NotebookApp(BaseIPythonApplication):
263 class NotebookApp(BaseIPythonApplication):
263
264
264 name = 'ipython-notebook'
265 name = 'ipython-notebook'
265 default_config_file_name='ipython_notebook_config.py'
266 default_config_file_name='ipython_notebook_config.py'
266
267
267 description = """
268 description = """
268 The IPython HTML Notebook.
269 The IPython HTML Notebook.
269
270
270 This launches a Tornado based HTML Notebook Server that serves up an
271 This launches a Tornado based HTML Notebook Server that serves up an
271 HTML5/Javascript Notebook client.
272 HTML5/Javascript Notebook client.
272 """
273 """
273 examples = _examples
274 examples = _examples
274
275
275 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
276 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
276 FileNotebookManager]
277 FileNotebookManager]
277 flags = Dict(flags)
278 flags = Dict(flags)
278 aliases = Dict(aliases)
279 aliases = Dict(aliases)
279
280
280 kernel_argv = List(Unicode)
281 kernel_argv = List(Unicode)
281
282
282 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
283 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
283 default_value=logging.INFO,
284 default_value=logging.INFO,
284 config=True,
285 config=True,
285 help="Set the log level by value or name.")
286 help="Set the log level by value or name.")
286
287
287 # create requested profiles by default, if they don't exist:
288 # create requested profiles by default, if they don't exist:
288 auto_create = Bool(True)
289 auto_create = Bool(True)
289
290
290 # file to be opened in the notebook server
291 # file to be opened in the notebook server
291 file_to_run = Unicode('')
292 file_to_run = Unicode('')
292
293
293 # Network related information.
294 # Network related information.
294
295
295 ip = Unicode(LOCALHOST, config=True,
296 ip = Unicode(LOCALHOST, config=True,
296 help="The IP address the notebook server will listen on."
297 help="The IP address the notebook server will listen on."
297 )
298 )
298
299
299 def _ip_changed(self, name, old, new):
300 def _ip_changed(self, name, old, new):
300 if new == u'*': self.ip = u''
301 if new == u'*': self.ip = u''
301
302
302 port = Integer(8888, config=True,
303 port = Integer(8888, config=True,
303 help="The port the notebook server will listen on."
304 help="The port the notebook server will listen on."
304 )
305 )
305 port_retries = Integer(50, config=True,
306 port_retries = Integer(50, config=True,
306 help="The number of additional ports to try if the specified port is not available."
307 help="The number of additional ports to try if the specified port is not available."
307 )
308 )
308
309
309 certfile = Unicode(u'', config=True,
310 certfile = Unicode(u'', config=True,
310 help="""The full path to an SSL/TLS certificate file."""
311 help="""The full path to an SSL/TLS certificate file."""
311 )
312 )
312
313
313 keyfile = Unicode(u'', config=True,
314 keyfile = Unicode(u'', config=True,
314 help="""The full path to a private key file for usage with SSL/TLS."""
315 help="""The full path to a private key file for usage with SSL/TLS."""
315 )
316 )
316
317
317 password = Unicode(u'', config=True,
318 password = Unicode(u'', config=True,
318 help="""Hashed password to use for web authentication.
319 help="""Hashed password to use for web authentication.
319
320
320 To generate, type in a python/IPython shell:
321 To generate, type in a python/IPython shell:
321
322
322 from IPython.lib import passwd; passwd()
323 from IPython.lib import passwd; passwd()
323
324
324 The string should be of the form type:salt:hashed-password.
325 The string should be of the form type:salt:hashed-password.
325 """
326 """
326 )
327 )
327
328
328 open_browser = Bool(True, config=True,
329 open_browser = Bool(True, config=True,
329 help="""Whether to open in a browser after starting.
330 help="""Whether to open in a browser after starting.
330 The specific browser used is platform dependent and
331 The specific browser used is platform dependent and
331 determined by the python standard library `webbrowser`
332 determined by the python standard library `webbrowser`
332 module, unless it is overridden using the --browser
333 module, unless it is overridden using the --browser
333 (NotebookApp.browser) configuration option.
334 (NotebookApp.browser) configuration option.
334 """)
335 """)
335
336
336 browser = Unicode(u'', config=True,
337 browser = Unicode(u'', config=True,
337 help="""Specify what command to use to invoke a web
338 help="""Specify what command to use to invoke a web
338 browser when opening the notebook. If not specified, the
339 browser when opening the notebook. If not specified, the
339 default browser will be determined by the `webbrowser`
340 default browser will be determined by the `webbrowser`
340 standard library module, which allows setting of the
341 standard library module, which allows setting of the
341 BROWSER environment variable to override it.
342 BROWSER environment variable to override it.
342 """)
343 """)
343
344
344 read_only = Bool(False, config=True,
345 read_only = Bool(False, config=True,
345 help="Whether to prevent editing/execution of notebooks."
346 help="Whether to prevent editing/execution of notebooks."
346 )
347 )
347
348
348 webapp_settings = Dict(config=True,
349 webapp_settings = Dict(config=True,
349 help="Supply overrides for the tornado.web.Application that the "
350 help="Supply overrides for the tornado.web.Application that the "
350 "IPython notebook uses.")
351 "IPython notebook uses.")
351
352
352 enable_mathjax = Bool(True, config=True,
353 enable_mathjax = Bool(True, config=True,
353 help="""Whether to enable MathJax for typesetting math/TeX
354 help="""Whether to enable MathJax for typesetting math/TeX
354
355
355 MathJax is the javascript library IPython uses to render math/LaTeX. It is
356 MathJax is the javascript library IPython uses to render math/LaTeX. It is
356 very large, so you may want to disable it if you have a slow internet
357 very large, so you may want to disable it if you have a slow internet
357 connection, or for offline use of the notebook.
358 connection, or for offline use of the notebook.
358
359
359 When disabled, equations etc. will appear as their untransformed TeX source.
360 When disabled, equations etc. will appear as their untransformed TeX source.
360 """
361 """
361 )
362 )
362 def _enable_mathjax_changed(self, name, old, new):
363 def _enable_mathjax_changed(self, name, old, new):
363 """set mathjax url to empty if mathjax is disabled"""
364 """set mathjax url to empty if mathjax is disabled"""
364 if not new:
365 if not new:
365 self.mathjax_url = u''
366 self.mathjax_url = u''
366
367
367 base_project_url = Unicode('/', config=True,
368 base_project_url = Unicode('/', config=True,
368 help='''The base URL for the notebook server.
369 help='''The base URL for the notebook server.
369
370
370 Leading and trailing slashes can be omitted,
371 Leading and trailing slashes can be omitted,
371 and will automatically be added.
372 and will automatically be added.
372 ''')
373 ''')
373 def _base_project_url_changed(self, name, old, new):
374 def _base_project_url_changed(self, name, old, new):
374 if not new.startswith('/'):
375 if not new.startswith('/'):
375 self.base_project_url = '/'+new
376 self.base_project_url = '/'+new
376 elif not new.endswith('/'):
377 elif not new.endswith('/'):
377 self.base_project_url = new+'/'
378 self.base_project_url = new+'/'
378
379
379 base_kernel_url = Unicode('/', config=True,
380 base_kernel_url = Unicode('/', config=True,
380 help='''The base URL for the kernel server
381 help='''The base URL for the kernel server
381
382
382 Leading and trailing slashes can be omitted,
383 Leading and trailing slashes can be omitted,
383 and will automatically be added.
384 and will automatically be added.
384 ''')
385 ''')
385 def _base_kernel_url_changed(self, name, old, new):
386 def _base_kernel_url_changed(self, name, old, new):
386 if not new.startswith('/'):
387 if not new.startswith('/'):
387 self.base_kernel_url = '/'+new
388 self.base_kernel_url = '/'+new
388 elif not new.endswith('/'):
389 elif not new.endswith('/'):
389 self.base_kernel_url = new+'/'
390 self.base_kernel_url = new+'/'
390
391
391 websocket_host = Unicode("", config=True,
392 websocket_host = Unicode("", config=True,
392 help="""The hostname for the websocket server."""
393 help="""The hostname for the websocket server."""
393 )
394 )
394
395
395 extra_static_paths = List(Unicode, config=True,
396 extra_static_paths = List(Unicode, config=True,
396 help="""Extra paths to search for serving static files.
397 help="""Extra paths to search for serving static files.
397
398
398 This allows adding javascript/css to be available from the notebook server machine,
399 This allows adding javascript/css to be available from the notebook server machine,
399 or overriding individual files in the IPython"""
400 or overriding individual files in the IPython"""
400 )
401 )
401 def _extra_static_paths_default(self):
402 def _extra_static_paths_default(self):
402 return [os.path.join(self.profile_dir.location, 'static')]
403 return [os.path.join(self.profile_dir.location, 'static')]
403
404
404 @property
405 @property
405 def static_file_path(self):
406 def static_file_path(self):
406 """return extra paths + the default location"""
407 """return extra paths + the default location"""
407 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
408 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
408
409
409 mathjax_url = Unicode("", config=True,
410 mathjax_url = Unicode("", config=True,
410 help="""The url for MathJax.js."""
411 help="""The url for MathJax.js."""
411 )
412 )
412 def _mathjax_url_default(self):
413 def _mathjax_url_default(self):
413 if not self.enable_mathjax:
414 if not self.enable_mathjax:
414 return u''
415 return u''
415 static_url_prefix = self.webapp_settings.get("static_url_prefix",
416 static_url_prefix = self.webapp_settings.get("static_url_prefix",
416 "/static/")
417 "/static/")
417 try:
418 try:
418 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
419 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
419 except IOError:
420 except IOError:
420 if self.certfile:
421 if self.certfile:
421 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
422 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
422 base = u"https://c328740.ssl.cf1.rackcdn.com"
423 base = u"https://c328740.ssl.cf1.rackcdn.com"
423 else:
424 else:
424 base = u"http://cdn.mathjax.org"
425 base = u"http://cdn.mathjax.org"
425
426
426 url = base + u"/mathjax/latest/MathJax.js"
427 url = base + u"/mathjax/latest/MathJax.js"
427 self.log.info("Using MathJax from CDN: %s", url)
428 self.log.info("Using MathJax from CDN: %s", url)
428 return url
429 return url
429 else:
430 else:
430 self.log.info("Using local MathJax from %s" % mathjax)
431 self.log.info("Using local MathJax from %s" % mathjax)
431 return static_url_prefix+u"mathjax/MathJax.js"
432 return static_url_prefix+u"mathjax/MathJax.js"
432
433
433 def _mathjax_url_changed(self, name, old, new):
434 def _mathjax_url_changed(self, name, old, new):
434 if new and not self.enable_mathjax:
435 if new and not self.enable_mathjax:
435 # enable_mathjax=False overrides mathjax_url
436 # enable_mathjax=False overrides mathjax_url
436 self.mathjax_url = u''
437 self.mathjax_url = u''
437 else:
438 else:
438 self.log.info("Using MathJax: %s", new)
439 self.log.info("Using MathJax: %s", new)
439
440
440 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
441 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
441 config=True,
442 config=True,
442 help='The notebook manager class to use.')
443 help='The notebook manager class to use.')
443
444
444 def parse_command_line(self, argv=None):
445 def parse_command_line(self, argv=None):
445 super(NotebookApp, self).parse_command_line(argv)
446 super(NotebookApp, self).parse_command_line(argv)
446 if argv is None:
447 if argv is None:
447 argv = sys.argv[1:]
448 argv = sys.argv[1:]
448
449
449 # Scrub frontend-specific flags
450 # Scrub frontend-specific flags
450 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
451 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
451 # Kernel should inherit default config file from frontend
452 # Kernel should inherit default config file from frontend
452 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
453 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
453
454
454 if self.extra_args:
455 if self.extra_args:
455 f = os.path.abspath(self.extra_args[0])
456 f = os.path.abspath(self.extra_args[0])
456 if os.path.isdir(f):
457 if os.path.isdir(f):
457 nbdir = f
458 nbdir = f
458 else:
459 else:
459 self.file_to_run = f
460 self.file_to_run = f
460 nbdir = os.path.dirname(f)
461 nbdir = os.path.dirname(f)
461 self.config.NotebookManager.notebook_dir = nbdir
462 self.config.NotebookManager.notebook_dir = nbdir
462
463
463 def init_configurables(self):
464 def init_configurables(self):
464 # force Session default to be secure
465 # force Session default to be secure
465 default_secure(self.config)
466 default_secure(self.config)
466 self.kernel_manager = MappingKernelManager(
467 self.kernel_manager = MappingKernelManager(
467 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
468 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
468 connection_dir = self.profile_dir.security_dir,
469 connection_dir = self.profile_dir.security_dir,
469 )
470 )
470 kls = import_item(self.notebook_manager_class)
471 kls = import_item(self.notebook_manager_class)
471 self.notebook_manager = kls(config=self.config, log=self.log)
472 self.notebook_manager = kls(config=self.config, log=self.log)
472 self.notebook_manager.log_info()
473 self.notebook_manager.log_info()
473 self.notebook_manager.load_notebook_names()
474 self.notebook_manager.load_notebook_names()
474 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
475 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
475 self.cluster_manager.update_profiles()
476 self.cluster_manager.update_profiles()
476
477
477 def init_logging(self):
478 def init_logging(self):
478 # This prevents double log messages because tornado use a root logger that
479 # This prevents double log messages because tornado use a root logger that
479 # self.log is a child of. The logging module dipatches log messages to a log
480 # self.log is a child of. The logging module dipatches log messages to a log
480 # and all of its ancenstors until propagate is set to False.
481 # and all of its ancenstors until propagate is set to False.
481 self.log.propagate = False
482 self.log.propagate = False
482
483
483 def init_webapp(self):
484 def init_webapp(self):
484 """initialize tornado webapp and httpserver"""
485 """initialize tornado webapp and httpserver"""
485 self.web_app = NotebookWebApplication(
486 self.web_app = NotebookWebApplication(
486 self, self.kernel_manager, self.notebook_manager,
487 self, self.kernel_manager, self.notebook_manager,
487 self.cluster_manager, self.log,
488 self.cluster_manager, self.log,
488 self.base_project_url, self.webapp_settings
489 self.base_project_url, self.webapp_settings
489 )
490 )
490 if self.certfile:
491 if self.certfile:
491 ssl_options = dict(certfile=self.certfile)
492 ssl_options = dict(certfile=self.certfile)
492 if self.keyfile:
493 if self.keyfile:
493 ssl_options['keyfile'] = self.keyfile
494 ssl_options['keyfile'] = self.keyfile
494 else:
495 else:
495 ssl_options = None
496 ssl_options = None
496 self.web_app.password = self.password
497 self.web_app.password = self.password
497 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
498 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
498 if not self.ip:
499 if not self.ip:
499 warning = "WARNING: The notebook server is listening on all IP addresses"
500 warning = "WARNING: The notebook server is listening on all IP addresses"
500 if ssl_options is None:
501 if ssl_options is None:
501 self.log.critical(warning + " and not using encryption. This"
502 self.log.critical(warning + " and not using encryption. This"
502 "is not recommended.")
503 "is not recommended.")
503 if not self.password and not self.read_only:
504 if not self.password and not self.read_only:
504 self.log.critical(warning + "and not using authentication."
505 self.log.critical(warning + "and not using authentication."
505 "This is highly insecure and not recommended.")
506 "This is highly insecure and not recommended.")
506 success = None
507 success = None
507 for port in random_ports(self.port, self.port_retries+1):
508 for port in random_ports(self.port, self.port_retries+1):
508 try:
509 try:
509 self.http_server.listen(port, self.ip)
510 self.http_server.listen(port, self.ip)
510 except socket.error as e:
511 except socket.error as e:
511 if e.errno != errno.EADDRINUSE:
512 if e.errno != errno.EADDRINUSE:
512 raise
513 raise
513 self.log.info('The port %i is already in use, trying another random port.' % port)
514 self.log.info('The port %i is already in use, trying another random port.' % port)
514 else:
515 else:
515 self.port = port
516 self.port = port
516 success = True
517 success = True
517 break
518 break
518 if not success:
519 if not success:
519 self.log.critical('ERROR: the notebook server could not be started because '
520 self.log.critical('ERROR: the notebook server could not be started because '
520 'no available port could be found.')
521 'no available port could be found.')
521 self.exit(1)
522 self.exit(1)
522
523
523 def init_signal(self):
524 def init_signal(self):
524 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
525 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
525 # safely extract zmq version info:
526 # safely extract zmq version info:
526 try:
527 try:
527 zmq_v = zmq.pyzmq_version_info()
528 zmq_v = zmq.pyzmq_version_info()
528 except AttributeError:
529 except AttributeError:
529 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
530 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
530 if 'dev' in zmq.__version__:
531 if 'dev' in zmq.__version__:
531 zmq_v.append(999)
532 zmq_v.append(999)
532 zmq_v = tuple(zmq_v)
533 zmq_v = tuple(zmq_v)
533 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
534 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
534 # This won't work with 2.1.7 and
535 # This won't work with 2.1.7 and
535 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
536 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
536 # but it will work
537 # but it will work
537 signal.signal(signal.SIGINT, self._handle_sigint)
538 signal.signal(signal.SIGINT, self._handle_sigint)
538 signal.signal(signal.SIGTERM, self._signal_stop)
539 signal.signal(signal.SIGTERM, self._signal_stop)
539
540
540 def _handle_sigint(self, sig, frame):
541 def _handle_sigint(self, sig, frame):
541 """SIGINT handler spawns confirmation dialog"""
542 """SIGINT handler spawns confirmation dialog"""
542 # register more forceful signal handler for ^C^C case
543 # register more forceful signal handler for ^C^C case
543 signal.signal(signal.SIGINT, self._signal_stop)
544 signal.signal(signal.SIGINT, self._signal_stop)
544 # request confirmation dialog in bg thread, to avoid
545 # request confirmation dialog in bg thread, to avoid
545 # blocking the App
546 # blocking the App
546 thread = threading.Thread(target=self._confirm_exit)
547 thread = threading.Thread(target=self._confirm_exit)
547 thread.daemon = True
548 thread.daemon = True
548 thread.start()
549 thread.start()
549
550
550 def _restore_sigint_handler(self):
551 def _restore_sigint_handler(self):
551 """callback for restoring original SIGINT handler"""
552 """callback for restoring original SIGINT handler"""
552 signal.signal(signal.SIGINT, self._handle_sigint)
553 signal.signal(signal.SIGINT, self._handle_sigint)
553
554
554 def _confirm_exit(self):
555 def _confirm_exit(self):
555 """confirm shutdown on ^C
556 """confirm shutdown on ^C
556
557
557 A second ^C, or answering 'y' within 5s will cause shutdown,
558 A second ^C, or answering 'y' within 5s will cause shutdown,
558 otherwise original SIGINT handler will be restored.
559 otherwise original SIGINT handler will be restored.
559
560
560 This doesn't work on Windows.
561 This doesn't work on Windows.
561 """
562 """
562 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
563 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
563 time.sleep(0.1)
564 time.sleep(0.1)
564 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
565 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
565 sys.stdout.flush()
566 sys.stdout.flush()
566 r,w,x = select.select([sys.stdin], [], [], 5)
567 r,w,x = select.select([sys.stdin], [], [], 5)
567 if r:
568 if r:
568 line = sys.stdin.readline()
569 line = sys.stdin.readline()
569 if line.lower().startswith('y'):
570 if line.lower().startswith('y'):
570 self.log.critical("Shutdown confirmed")
571 self.log.critical("Shutdown confirmed")
571 ioloop.IOLoop.instance().stop()
572 ioloop.IOLoop.instance().stop()
572 return
573 return
573 else:
574 else:
574 print "No answer for 5s:",
575 print "No answer for 5s:",
575 print "resuming operation..."
576 print "resuming operation..."
576 # no answer, or answer is no:
577 # no answer, or answer is no:
577 # set it back to original SIGINT handler
578 # set it back to original SIGINT handler
578 # use IOLoop.add_callback because signal.signal must be called
579 # use IOLoop.add_callback because signal.signal must be called
579 # from main thread
580 # from main thread
580 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
581 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
581
582
582 def _signal_stop(self, sig, frame):
583 def _signal_stop(self, sig, frame):
583 self.log.critical("received signal %s, stopping", sig)
584 self.log.critical("received signal %s, stopping", sig)
584 ioloop.IOLoop.instance().stop()
585 ioloop.IOLoop.instance().stop()
585
586
586 @catch_config_error
587 @catch_config_error
587 def initialize(self, argv=None):
588 def initialize(self, argv=None):
588 self.init_logging()
589 self.init_logging()
589 super(NotebookApp, self).initialize(argv)
590 super(NotebookApp, self).initialize(argv)
590 self.init_configurables()
591 self.init_configurables()
591 self.init_webapp()
592 self.init_webapp()
592 self.init_signal()
593 self.init_signal()
593
594
594 def cleanup_kernels(self):
595 def cleanup_kernels(self):
595 """Shutdown all kernels.
596 """Shutdown all kernels.
596
597
597 The kernels will shutdown themselves when this process no longer exists,
598 The kernels will shutdown themselves when this process no longer exists,
598 but explicit shutdown allows the KernelManagers to cleanup the connection files.
599 but explicit shutdown allows the KernelManagers to cleanup the connection files.
599 """
600 """
600 self.log.info('Shutting down kernels')
601 self.log.info('Shutting down kernels')
601 self.kernel_manager.shutdown_all()
602 self.kernel_manager.shutdown_all()
602
603
603 def start(self):
604 def start(self):
604 ip = self.ip if self.ip else '[all ip addresses on your system]'
605 ip = self.ip if self.ip else '[all ip addresses on your system]'
605 proto = 'https' if self.certfile else 'http'
606 proto = 'https' if self.certfile else 'http'
606 info = self.log.info
607 info = self.log.info
607 info("The IPython Notebook is running at: %s://%s:%i%s" %
608 info("The IPython Notebook is running at: %s://%s:%i%s" %
608 (proto, ip, self.port,self.base_project_url) )
609 (proto, ip, self.port,self.base_project_url) )
609 info("Use Control-C to stop this server and shut down all kernels.")
610 info("Use Control-C to stop this server and shut down all kernels.")
610
611
611 if self.open_browser or self.file_to_run:
612 if self.open_browser or self.file_to_run:
612 ip = self.ip or '127.0.0.1'
613 ip = self.ip or '127.0.0.1'
613 try:
614 try:
614 browser = webbrowser.get(self.browser or None)
615 browser = webbrowser.get(self.browser or None)
615 except webbrowser.Error as e:
616 except webbrowser.Error as e:
616 self.log.warn('No web browser found: %s.' % e)
617 self.log.warn('No web browser found: %s.' % e)
617 browser = None
618 browser = None
618
619
619 if self.file_to_run:
620 if self.file_to_run:
620 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
621 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
621 url = self.notebook_manager.rev_mapping.get(name, '')
622 url = self.notebook_manager.rev_mapping.get(name, '')
622 else:
623 else:
623 url = ''
624 url = ''
624 if browser:
625 if browser:
625 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
626 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
626 self.port, self.base_project_url, url), new=2)
627 self.port, self.base_project_url, url), new=2)
627 threading.Thread(target=b).start()
628 threading.Thread(target=b).start()
628 try:
629 try:
629 ioloop.IOLoop.instance().start()
630 ioloop.IOLoop.instance().start()
630 except KeyboardInterrupt:
631 except KeyboardInterrupt:
631 info("Interrupted...")
632 info("Interrupted...")
632 finally:
633 finally:
633 self.cleanup_kernels()
634 self.cleanup_kernels()
634
635
635
636
636 #-----------------------------------------------------------------------------
637 #-----------------------------------------------------------------------------
637 # Main entry point
638 # Main entry point
638 #-----------------------------------------------------------------------------
639 #-----------------------------------------------------------------------------
639
640
640 def launch_new_instance():
641 def launch_new_instance():
641 app = NotebookApp.instance()
642 app = NotebookApp.instance()
642 app.initialize()
643 app.initialize()
643 app.start()
644 app.start()
644
645
@@ -1,368 +1,370
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14 * Paul Ivanov
14 * Paul Ivanov
15
15
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 # stdlib imports
22 # stdlib imports
23 import json
23 import json
24 import os
24 import os
25 import signal
25 import signal
26 import sys
26 import sys
27 import uuid
27 import uuid
28
28
29 # If run on Windows, install an exception hook which pops up a
29 # If run on Windows, install an exception hook which pops up a
30 # message box. Pythonw.exe hides the console, so without this
30 # message box. Pythonw.exe hides the console, so without this
31 # the application silently fails to load.
31 # the application silently fails to load.
32 #
32 #
33 # We always install this handler, because the expectation is for
33 # We always install this handler, because the expectation is for
34 # qtconsole to bring up a GUI even if called from the console.
34 # qtconsole to bring up a GUI even if called from the console.
35 # The old handler is called, so the exception is printed as well.
35 # The old handler is called, so the exception is printed as well.
36 # If desired, check for pythonw with an additional condition
36 # If desired, check for pythonw with an additional condition
37 # (sys.executable.lower().find('pythonw.exe') >= 0).
37 # (sys.executable.lower().find('pythonw.exe') >= 0).
38 if os.name == 'nt':
38 if os.name == 'nt':
39 old_excepthook = sys.excepthook
39 old_excepthook = sys.excepthook
40
40
41 def gui_excepthook(exctype, value, tb):
41 def gui_excepthook(exctype, value, tb):
42 try:
42 try:
43 import ctypes, traceback
43 import ctypes, traceback
44 MB_ICONERROR = 0x00000010L
44 MB_ICONERROR = 0x00000010L
45 title = u'Error starting IPython QtConsole'
45 title = u'Error starting IPython QtConsole'
46 msg = u''.join(traceback.format_exception(exctype, value, tb))
46 msg = u''.join(traceback.format_exception(exctype, value, tb))
47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
48 finally:
48 finally:
49 # Also call the old exception hook to let it do
49 # Also call the old exception hook to let it do
50 # its thing too.
50 # its thing too.
51 old_excepthook(exctype, value, tb)
51 old_excepthook(exctype, value, tb)
52
52
53 sys.excepthook = gui_excepthook
53 sys.excepthook = gui_excepthook
54
54
55 # System library imports
55 # System library imports
56 from IPython.external.qt import QtCore, QtGui
56 from IPython.external.qt import QtCore, QtGui
57
57
58 # Local imports
58 # Local imports
59 from IPython.config.application import boolean_flag, catch_config_error
59 from IPython.config.application import boolean_flag, catch_config_error
60 from IPython.core.application import BaseIPythonApplication
60 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.profiledir import ProfileDir
61 from IPython.core.profiledir import ProfileDir
62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
66 from IPython.frontend.qt.console import styles
66 from IPython.frontend.qt.console import styles
67 from IPython.frontend.qt.console.mainwindow import MainWindow
67 from IPython.frontend.qt.console.mainwindow import MainWindow
68 from IPython.frontend.qt.kernelmanager import QtKernelManager
68 from IPython.frontend.qt.kernelmanager import QtKernelManager
69 from IPython.utils.path import filefind
69 from IPython.utils.path import filefind
70 from IPython.utils.py3compat import str_to_bytes
70 from IPython.utils.py3compat import str_to_bytes
71 from IPython.utils.traitlets import (
71 from IPython.utils.traitlets import (
72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
73 )
73 )
74 from IPython.zmq.ipkernel import IPKernelApp
74 from IPython.zmq.ipkernel import IPKernelApp
75 from IPython.zmq.session import Session, default_secure
75 from IPython.zmq.session import Session, default_secure
76 from IPython.zmq.zmqshell import ZMQInteractiveShell
76 from IPython.zmq.zmqshell import ZMQInteractiveShell
77
77
78 from IPython.frontend.consoleapp import (
78 from IPython.frontend.consoleapp import (
79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
80 )
80 )
81
81
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83 # Network Constants
83 # Network Constants
84 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
85
85
86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Globals
89 # Globals
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 _examples = """
92 _examples = """
93 ipython qtconsole # start the qtconsole
93 ipython qtconsole # start the qtconsole
94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
95 """
95 """
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Aliases and Flags
98 # Aliases and Flags
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 # start with copy of flags
101 # start with copy of flags
102 flags = dict(flags)
102 flags = dict(flags)
103 qt_flags = {
103 qt_flags = {
104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
105 "Disable rich text support."),
105 "Disable rich text support."),
106 }
106 }
107
107
108 # and app_flags from the Console Mixin
108 # and app_flags from the Console Mixin
109 qt_flags.update(app_flags)
109 qt_flags.update(app_flags)
110 # add frontend flags to the full set
110 # add frontend flags to the full set
111 flags.update(qt_flags)
111 flags.update(qt_flags)
112
112
113 # start with copy of front&backend aliases list
113 # start with copy of front&backend aliases list
114 aliases = dict(aliases)
114 aliases = dict(aliases)
115 qt_aliases = dict(
115 qt_aliases = dict(
116 style = 'IPythonWidget.syntax_style',
116 style = 'IPythonWidget.syntax_style',
117 stylesheet = 'IPythonQtConsoleApp.stylesheet',
117 stylesheet = 'IPythonQtConsoleApp.stylesheet',
118 colors = 'ZMQInteractiveShell.colors',
118 colors = 'ZMQInteractiveShell.colors',
119
119
120 editor = 'IPythonWidget.editor',
120 editor = 'IPythonWidget.editor',
121 paging = 'ConsoleWidget.paging',
121 paging = 'ConsoleWidget.paging',
122 )
122 )
123 # and app_aliases from the Console Mixin
123 # and app_aliases from the Console Mixin
124 qt_aliases.update(app_aliases)
124 qt_aliases.update(app_aliases)
125 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
125 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
126 # add frontend aliases to the full set
126 # add frontend aliases to the full set
127 aliases.update(qt_aliases)
127 aliases.update(qt_aliases)
128
128
129 # get flags&aliases into sets, and remove a couple that
129 # get flags&aliases into sets, and remove a couple that
130 # shouldn't be scrubbed from backend flags:
130 # shouldn't be scrubbed from backend flags:
131 qt_aliases = set(qt_aliases.keys())
131 qt_aliases = set(qt_aliases.keys())
132 qt_aliases.remove('colors')
132 qt_aliases.remove('colors')
133 qt_flags = set(qt_flags.keys())
133 qt_flags = set(qt_flags.keys())
134
134
135 #-----------------------------------------------------------------------------
135 #-----------------------------------------------------------------------------
136 # Classes
136 # Classes
137 #-----------------------------------------------------------------------------
137 #-----------------------------------------------------------------------------
138
138
139 #-----------------------------------------------------------------------------
139 #-----------------------------------------------------------------------------
140 # IPythonQtConsole
140 # IPythonQtConsole
141 #-----------------------------------------------------------------------------
141 #-----------------------------------------------------------------------------
142
142
143
143
144 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
144 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
145 name = 'ipython-qtconsole'
145 name = 'ipython-qtconsole'
146
146
147 description = """
147 description = """
148 The IPython QtConsole.
148 The IPython QtConsole.
149
149
150 This launches a Console-style application using Qt. It is not a full
150 This launches a Console-style application using Qt. It is not a full
151 console, in that launched terminal subprocesses will not be able to accept
151 console, in that launched terminal subprocesses will not be able to accept
152 input.
152 input.
153
153
154 The QtConsole supports various extra features beyond the Terminal IPython
154 The QtConsole supports various extra features beyond the Terminal IPython
155 shell, such as inline plotting with matplotlib, via:
155 shell, such as inline plotting with matplotlib, via:
156
156
157 ipython qtconsole --pylab=inline
157 ipython qtconsole --pylab=inline
158
158
159 as well as saving your session as HTML, and printing the output.
159 as well as saving your session as HTML, and printing the output.
160
160
161 """
161 """
162 examples = _examples
162 examples = _examples
163
163
164 classes = [IPythonWidget] + IPythonConsoleApp.classes
164 classes = [IPythonWidget] + IPythonConsoleApp.classes
165 flags = Dict(flags)
165 flags = Dict(flags)
166 aliases = Dict(aliases)
166 aliases = Dict(aliases)
167 frontend_flags = Any(qt_flags)
167 frontend_flags = Any(qt_flags)
168 frontend_aliases = Any(qt_aliases)
168 frontend_aliases = Any(qt_aliases)
169 kernel_manager_class = QtKernelManager
169 kernel_manager_class = QtKernelManager
170
170
171 stylesheet = Unicode('', config=True,
171 stylesheet = Unicode('', config=True,
172 help="path to a custom CSS stylesheet")
172 help="path to a custom CSS stylesheet")
173
173
174 plain = CBool(False, config=True,
174 plain = CBool(False, config=True,
175 help="Use a plaintext widget instead of rich text (plain can't print/save).")
175 help="Use a plaintext widget instead of rich text (plain can't print/save).")
176
176
177 def _plain_changed(self, name, old, new):
177 def _plain_changed(self, name, old, new):
178 kind = 'plain' if new else 'rich'
178 kind = 'plain' if new else 'rich'
179 self.config.ConsoleWidget.kind = kind
179 self.config.ConsoleWidget.kind = kind
180 if new:
180 if new:
181 self.widget_factory = IPythonWidget
181 self.widget_factory = IPythonWidget
182 else:
182 else:
183 self.widget_factory = RichIPythonWidget
183 self.widget_factory = RichIPythonWidget
184
184
185 # the factory for creating a widget
185 # the factory for creating a widget
186 widget_factory = Any(RichIPythonWidget)
186 widget_factory = Any(RichIPythonWidget)
187
187
188 def parse_command_line(self, argv=None):
188 def parse_command_line(self, argv=None):
189 super(IPythonQtConsoleApp, self).parse_command_line(argv)
189 super(IPythonQtConsoleApp, self).parse_command_line(argv)
190 self.build_kernel_argv(argv)
190 self.build_kernel_argv(argv)
191
191
192
192
193 def new_frontend_master(self):
193 def new_frontend_master(self):
194 """ Create and return new frontend attached to new kernel, launched on localhost.
194 """ Create and return new frontend attached to new kernel, launched on localhost.
195 """
195 """
196 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
197 kernel_manager = self.kernel_manager_class(
196 kernel_manager = self.kernel_manager_class(
198 ip=ip,
199 connection_file=self._new_connection_file(),
197 connection_file=self._new_connection_file(),
200 config=self.config,
198 config=self.config,
201 )
199 )
202 # start the kernel
200 # start the kernel
203 kwargs = dict()
201 kwargs = dict()
204 kwargs['extra_arguments'] = self.kernel_argv
202 kwargs['extra_arguments'] = self.kernel_argv
205 kernel_manager.start_kernel(**kwargs)
203 kernel_manager.start_kernel(**kwargs)
206 kernel_manager.start_channels()
204 kernel_manager.start_channels()
207 widget = self.widget_factory(config=self.config,
205 widget = self.widget_factory(config=self.config,
208 local_kernel=True)
206 local_kernel=True)
209 self.init_colors(widget)
207 self.init_colors(widget)
210 widget.kernel_manager = kernel_manager
208 widget.kernel_manager = kernel_manager
211 widget._existing = False
209 widget._existing = False
212 widget._may_close = True
210 widget._may_close = True
213 widget._confirm_exit = self.confirm_exit
211 widget._confirm_exit = self.confirm_exit
214 return widget
212 return widget
215
213
216 def new_frontend_slave(self, current_widget):
214 def new_frontend_slave(self, current_widget):
217 """Create and return a new frontend attached to an existing kernel.
215 """Create and return a new frontend attached to an existing kernel.
218
216
219 Parameters
217 Parameters
220 ----------
218 ----------
221 current_widget : IPythonWidget
219 current_widget : IPythonWidget
222 The IPythonWidget whose kernel this frontend is to share
220 The IPythonWidget whose kernel this frontend is to share
223 """
221 """
224 kernel_manager = self.kernel_manager_class(
222 kernel_manager = self.kernel_manager_class(
225 connection_file=current_widget.kernel_manager.connection_file,
223 connection_file=current_widget.kernel_manager.connection_file,
226 config = self.config,
224 config = self.config,
227 )
225 )
228 kernel_manager.load_connection_file()
226 kernel_manager.load_connection_file()
229 kernel_manager.start_channels()
227 kernel_manager.start_channels()
230 widget = self.widget_factory(config=self.config,
228 widget = self.widget_factory(config=self.config,
231 local_kernel=False)
229 local_kernel=False)
232 self.init_colors(widget)
230 self.init_colors(widget)
233 widget._existing = True
231 widget._existing = True
234 widget._may_close = False
232 widget._may_close = False
235 widget._confirm_exit = False
233 widget._confirm_exit = False
236 widget.kernel_manager = kernel_manager
234 widget.kernel_manager = kernel_manager
237 return widget
235 return widget
238
236
239 def init_qt_elements(self):
237 def init_qt_elements(self):
240 # Create the widget.
238 # Create the widget.
241 self.app = QtGui.QApplication([])
239 self.app = QtGui.QApplication([])
242
240
243 base_path = os.path.abspath(os.path.dirname(__file__))
241 base_path = os.path.abspath(os.path.dirname(__file__))
244 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
242 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
245 self.app.icon = QtGui.QIcon(icon_path)
243 self.app.icon = QtGui.QIcon(icon_path)
246 QtGui.QApplication.setWindowIcon(self.app.icon)
244 QtGui.QApplication.setWindowIcon(self.app.icon)
247
245
248 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
246 try:
247 ip = self.config.KernelManager.ip
248 except AttributeError:
249 ip = LOCALHOST
250 local_kernel = (not self.existing) or ip in LOCAL_IPS
249 self.widget = self.widget_factory(config=self.config,
251 self.widget = self.widget_factory(config=self.config,
250 local_kernel=local_kernel)
252 local_kernel=local_kernel)
251 self.init_colors(self.widget)
253 self.init_colors(self.widget)
252 self.widget._existing = self.existing
254 self.widget._existing = self.existing
253 self.widget._may_close = not self.existing
255 self.widget._may_close = not self.existing
254 self.widget._confirm_exit = self.confirm_exit
256 self.widget._confirm_exit = self.confirm_exit
255
257
256 self.widget.kernel_manager = self.kernel_manager
258 self.widget.kernel_manager = self.kernel_manager
257 self.window = MainWindow(self.app,
259 self.window = MainWindow(self.app,
258 confirm_exit=self.confirm_exit,
260 confirm_exit=self.confirm_exit,
259 new_frontend_factory=self.new_frontend_master,
261 new_frontend_factory=self.new_frontend_master,
260 slave_frontend_factory=self.new_frontend_slave,
262 slave_frontend_factory=self.new_frontend_slave,
261 )
263 )
262 self.window.log = self.log
264 self.window.log = self.log
263 self.window.add_tab_with_frontend(self.widget)
265 self.window.add_tab_with_frontend(self.widget)
264 self.window.init_menu_bar()
266 self.window.init_menu_bar()
265
267
266 self.window.setWindowTitle('IPython')
268 self.window.setWindowTitle('IPython')
267
269
268 def init_colors(self, widget):
270 def init_colors(self, widget):
269 """Configure the coloring of the widget"""
271 """Configure the coloring of the widget"""
270 # Note: This will be dramatically simplified when colors
272 # Note: This will be dramatically simplified when colors
271 # are removed from the backend.
273 # are removed from the backend.
272
274
273 # parse the colors arg down to current known labels
275 # parse the colors arg down to current known labels
274 try:
276 try:
275 colors = self.config.ZMQInteractiveShell.colors
277 colors = self.config.ZMQInteractiveShell.colors
276 except AttributeError:
278 except AttributeError:
277 colors = None
279 colors = None
278 try:
280 try:
279 style = self.config.IPythonWidget.syntax_style
281 style = self.config.IPythonWidget.syntax_style
280 except AttributeError:
282 except AttributeError:
281 style = None
283 style = None
282 try:
284 try:
283 sheet = self.config.IPythonWidget.style_sheet
285 sheet = self.config.IPythonWidget.style_sheet
284 except AttributeError:
286 except AttributeError:
285 sheet = None
287 sheet = None
286
288
287 # find the value for colors:
289 # find the value for colors:
288 if colors:
290 if colors:
289 colors=colors.lower()
291 colors=colors.lower()
290 if colors in ('lightbg', 'light'):
292 if colors in ('lightbg', 'light'):
291 colors='lightbg'
293 colors='lightbg'
292 elif colors in ('dark', 'linux'):
294 elif colors in ('dark', 'linux'):
293 colors='linux'
295 colors='linux'
294 else:
296 else:
295 colors='nocolor'
297 colors='nocolor'
296 elif style:
298 elif style:
297 if style=='bw':
299 if style=='bw':
298 colors='nocolor'
300 colors='nocolor'
299 elif styles.dark_style(style):
301 elif styles.dark_style(style):
300 colors='linux'
302 colors='linux'
301 else:
303 else:
302 colors='lightbg'
304 colors='lightbg'
303 else:
305 else:
304 colors=None
306 colors=None
305
307
306 # Configure the style
308 # Configure the style
307 if style:
309 if style:
308 widget.style_sheet = styles.sheet_from_template(style, colors)
310 widget.style_sheet = styles.sheet_from_template(style, colors)
309 widget.syntax_style = style
311 widget.syntax_style = style
310 widget._syntax_style_changed()
312 widget._syntax_style_changed()
311 widget._style_sheet_changed()
313 widget._style_sheet_changed()
312 elif colors:
314 elif colors:
313 # use a default dark/light/bw style
315 # use a default dark/light/bw style
314 widget.set_default_style(colors=colors)
316 widget.set_default_style(colors=colors)
315
317
316 if self.stylesheet:
318 if self.stylesheet:
317 # we got an explicit stylesheet
319 # we got an explicit stylesheet
318 if os.path.isfile(self.stylesheet):
320 if os.path.isfile(self.stylesheet):
319 with open(self.stylesheet) as f:
321 with open(self.stylesheet) as f:
320 sheet = f.read()
322 sheet = f.read()
321 else:
323 else:
322 raise IOError("Stylesheet %r not found." % self.stylesheet)
324 raise IOError("Stylesheet %r not found." % self.stylesheet)
323 if sheet:
325 if sheet:
324 widget.style_sheet = sheet
326 widget.style_sheet = sheet
325 widget._style_sheet_changed()
327 widget._style_sheet_changed()
326
328
327
329
328 def init_signal(self):
330 def init_signal(self):
329 """allow clean shutdown on sigint"""
331 """allow clean shutdown on sigint"""
330 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
332 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
331 # need a timer, so that QApplication doesn't block until a real
333 # need a timer, so that QApplication doesn't block until a real
332 # Qt event fires (can require mouse movement)
334 # Qt event fires (can require mouse movement)
333 # timer trick from http://stackoverflow.com/q/4938723/938949
335 # timer trick from http://stackoverflow.com/q/4938723/938949
334 timer = QtCore.QTimer()
336 timer = QtCore.QTimer()
335 # Let the interpreter run each 200 ms:
337 # Let the interpreter run each 200 ms:
336 timer.timeout.connect(lambda: None)
338 timer.timeout.connect(lambda: None)
337 timer.start(200)
339 timer.start(200)
338 # hold onto ref, so the timer doesn't get cleaned up
340 # hold onto ref, so the timer doesn't get cleaned up
339 self._sigint_timer = timer
341 self._sigint_timer = timer
340
342
341 @catch_config_error
343 @catch_config_error
342 def initialize(self, argv=None):
344 def initialize(self, argv=None):
343 super(IPythonQtConsoleApp, self).initialize(argv)
345 super(IPythonQtConsoleApp, self).initialize(argv)
344 IPythonConsoleApp.initialize(self,argv)
346 IPythonConsoleApp.initialize(self,argv)
345 self.init_qt_elements()
347 self.init_qt_elements()
346 self.init_signal()
348 self.init_signal()
347
349
348 def start(self):
350 def start(self):
349
351
350 # draw the window
352 # draw the window
351 self.window.show()
353 self.window.show()
352 self.window.raise_()
354 self.window.raise_()
353
355
354 # Start the application main loop.
356 # Start the application main loop.
355 self.app.exec_()
357 self.app.exec_()
356
358
357 #-----------------------------------------------------------------------------
359 #-----------------------------------------------------------------------------
358 # Main entry point
360 # Main entry point
359 #-----------------------------------------------------------------------------
361 #-----------------------------------------------------------------------------
360
362
361 def main():
363 def main():
362 app = IPythonQtConsoleApp()
364 app = IPythonQtConsoleApp()
363 app.initialize()
365 app.initialize()
364 app.start()
366 app.start()
365
367
366
368
367 if __name__ == '__main__':
369 if __name__ == '__main__':
368 main()
370 main()
@@ -1,56 +1,57
1 """The client and server for a basic ping-pong style heartbeat.
1 """The client and server for a basic ping-pong style heartbeat.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2008-2011 The IPython Development Team
5 # Copyright (C) 2008-2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 import os
15 import os
16 import socket
16 import socket
17 import sys
17 import sys
18 from threading import Thread
18 from threading import Thread
19
19
20 import zmq
20 import zmq
21
21
22 from IPython.utils.localinterfaces import LOCALHOST
22 from IPython.utils.localinterfaces import LOCALHOST
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Code
25 # Code
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28
28
29 class Heartbeat(Thread):
29 class Heartbeat(Thread):
30 "A simple ping-pong style heartbeat that runs in a thread."
30 "A simple ping-pong style heartbeat that runs in a thread."
31
31
32 def __init__(self, context, addr=('tcp', LOCALHOST, 0)):
32 def __init__(self, context, addr=('tcp', LOCALHOST, 0)):
33 Thread.__init__(self)
33 Thread.__init__(self)
34 self.context = context
34 self.context = context
35 self.transport, self.ip, self.port = addr
35 self.transport, self.ip, self.port = addr
36 if self.port == 0:
36 if self.port == 0:
37 if addr[0] == 'tcp':
37 if addr[0] == 'tcp':
38 s = socket.socket()
38 s = socket.socket()
39 # '*' means all interfaces to 0MQ, which is '' to socket.socket
39 # '*' means all interfaces to 0MQ, which is '' to socket.socket
40 s.bind(('' if self.ip == '*' else self.ip, 0))
40 s.bind(('' if self.ip == '*' else self.ip, 0))
41 self.port = s.getsockname()[1]
41 self.port = s.getsockname()[1]
42 s.close()
42 s.close()
43 elif addr[0] == 'ipc':
43 elif addr[0] == 'ipc':
44 while os.path.exists(self.ip + '-' + self.port):
44 self.port = 1
45 while os.path.exists("%s-%s" % (self.ip, self.port)):
45 self.port = self.port + 1
46 self.port = self.port + 1
46 else:
47 else:
47 raise ValueError("Unrecognized zmq transport: %s" % addr[0])
48 raise ValueError("Unrecognized zmq transport: %s" % addr[0])
48 self.addr = (self.ip, self.port)
49 self.addr = (self.ip, self.port)
49 self.daemon = True
50 self.daemon = True
50
51
51 def run(self):
52 def run(self):
52 self.socket = self.context.socket(zmq.REP)
53 self.socket = self.context.socket(zmq.REP)
53 c = ':' if self.transport == 'tcp' else '-'
54 c = ':' if self.transport == 'tcp' else '-'
54 self.socket.bind('%s://%s' % (self.transport, self.ip) + c + str(self.port))
55 self.socket.bind('%s://%s' % (self.transport, self.ip) + c + str(self.port))
55 zmq.device(zmq.FORWARDER, self.socket, self.socket)
56 zmq.device(zmq.FORWARDER, self.socket, self.socket)
56
57
@@ -1,351 +1,372
1 """An Application for launching a kernel
1 """An Application for launching a kernel
2
2
3 Authors
3 Authors
4 -------
4 -------
5 * MinRK
5 * MinRK
6 """
6 """
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2011 The IPython Development Team
8 # Copyright (C) 2011 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING.txt, distributed as part of this software.
11 # the file COPYING.txt, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 # Standard library imports
18 # Standard library imports
19 import atexit
19 import atexit
20 import json
20 import json
21 import os
21 import os
22 import sys
22 import sys
23 import signal
23 import signal
24
24
25 # System library imports
25 # System library imports
26 import zmq
26 import zmq
27 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
28
28
29 # IPython imports
29 # IPython imports
30 from IPython.core.ultratb import FormattedTB
30 from IPython.core.ultratb import FormattedTB
31 from IPython.core.application import (
31 from IPython.core.application import (
32 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
32 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
33 )
33 )
34 from IPython.utils import io
34 from IPython.utils import io
35 from IPython.utils.localinterfaces import LOCALHOST
35 from IPython.utils.localinterfaces import LOCALHOST
36 from IPython.utils.path import filefind
36 from IPython.utils.path import filefind
37 from IPython.utils.py3compat import str_to_bytes
37 from IPython.utils.py3compat import str_to_bytes
38 from IPython.utils.traitlets import (
38 from IPython.utils.traitlets import (
39 Any, Instance, Dict, Unicode, Integer, Bool, CaselessStrEnum,
39 Any, Instance, Dict, Unicode, Integer, Bool, CaselessStrEnum,
40 DottedObjectName,
40 DottedObjectName,
41 )
41 )
42 from IPython.utils.importstring import import_item
42 from IPython.utils.importstring import import_item
43 # local imports
43 # local imports
44 from IPython.zmq.entry_point import write_connection_file
44 from IPython.zmq.entry_point import write_connection_file
45 from IPython.zmq.heartbeat import Heartbeat
45 from IPython.zmq.heartbeat import Heartbeat
46 from IPython.zmq.parentpoller import ParentPollerUnix, ParentPollerWindows
46 from IPython.zmq.parentpoller import ParentPollerUnix, ParentPollerWindows
47 from IPython.zmq.session import (
47 from IPython.zmq.session import (
48 Session, session_flags, session_aliases, default_secure,
48 Session, session_flags, session_aliases, default_secure,
49 )
49 )
50
50
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # Flags and Aliases
53 # Flags and Aliases
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56 kernel_aliases = dict(base_aliases)
56 kernel_aliases = dict(base_aliases)
57 kernel_aliases.update({
57 kernel_aliases.update({
58 'ip' : 'KernelApp.ip',
58 'ip' : 'KernelApp.ip',
59 'hb' : 'KernelApp.hb_port',
59 'hb' : 'KernelApp.hb_port',
60 'shell' : 'KernelApp.shell_port',
60 'shell' : 'KernelApp.shell_port',
61 'iopub' : 'KernelApp.iopub_port',
61 'iopub' : 'KernelApp.iopub_port',
62 'stdin' : 'KernelApp.stdin_port',
62 'stdin' : 'KernelApp.stdin_port',
63 'f' : 'KernelApp.connection_file',
63 'f' : 'KernelApp.connection_file',
64 'parent': 'KernelApp.parent',
64 'parent': 'KernelApp.parent',
65 'transport': 'KernelApp.transport',
65 })
66 })
66 if sys.platform.startswith('win'):
67 if sys.platform.startswith('win'):
67 kernel_aliases['interrupt'] = 'KernelApp.interrupt'
68 kernel_aliases['interrupt'] = 'KernelApp.interrupt'
68
69
69 kernel_flags = dict(base_flags)
70 kernel_flags = dict(base_flags)
70 kernel_flags.update({
71 kernel_flags.update({
71 'no-stdout' : (
72 'no-stdout' : (
72 {'KernelApp' : {'no_stdout' : True}},
73 {'KernelApp' : {'no_stdout' : True}},
73 "redirect stdout to the null device"),
74 "redirect stdout to the null device"),
74 'no-stderr' : (
75 'no-stderr' : (
75 {'KernelApp' : {'no_stderr' : True}},
76 {'KernelApp' : {'no_stderr' : True}},
76 "redirect stderr to the null device"),
77 "redirect stderr to the null device"),
77 })
78 })
78
79
79 # inherit flags&aliases for Sessions
80 # inherit flags&aliases for Sessions
80 kernel_aliases.update(session_aliases)
81 kernel_aliases.update(session_aliases)
81 kernel_flags.update(session_flags)
82 kernel_flags.update(session_flags)
82
83
83
84
84
85
85 #-----------------------------------------------------------------------------
86 #-----------------------------------------------------------------------------
86 # Application class for starting a Kernel
87 # Application class for starting a Kernel
87 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
88
89
89 class KernelApp(BaseIPythonApplication):
90 class KernelApp(BaseIPythonApplication):
90 name='ipkernel'
91 name='ipkernel'
91 aliases = Dict(kernel_aliases)
92 aliases = Dict(kernel_aliases)
92 flags = Dict(kernel_flags)
93 flags = Dict(kernel_flags)
93 classes = [Session]
94 classes = [Session]
94 # the kernel class, as an importstring
95 # the kernel class, as an importstring
95 kernel_class = DottedObjectName('IPython.zmq.ipkernel.Kernel')
96 kernel_class = DottedObjectName('IPython.zmq.ipkernel.Kernel')
96 kernel = Any()
97 kernel = Any()
97 poller = Any() # don't restrict this even though current pollers are all Threads
98 poller = Any() # don't restrict this even though current pollers are all Threads
98 heartbeat = Instance(Heartbeat)
99 heartbeat = Instance(Heartbeat)
99 session = Instance('IPython.zmq.session.Session')
100 session = Instance('IPython.zmq.session.Session')
100 ports = Dict()
101 ports = Dict()
101 _full_connection_file = Unicode()
102
102
103 # inherit config file name from parent:
103 # inherit config file name from parent:
104 parent_appname = Unicode(config=True)
104 parent_appname = Unicode(config=True)
105 def _parent_appname_changed(self, name, old, new):
105 def _parent_appname_changed(self, name, old, new):
106 if self.config_file_specified:
106 if self.config_file_specified:
107 # it was manually specified, ignore
107 # it was manually specified, ignore
108 return
108 return
109 self.config_file_name = new.replace('-','_') + u'_config.py'
109 self.config_file_name = new.replace('-','_') + u'_config.py'
110 # don't let this count as specifying the config file
110 # don't let this count as specifying the config file
111 self.config_file_specified = False
111 self.config_file_specified = False
112
112
113 # connection info:
113 # connection info:
114 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
114 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
115 ip = Unicode(LOCALHOST, config=True,
115 ip = Unicode(config=True,
116 help="Set the IP or interface on which the kernel will listen.")
116 help="Set the IP or interface on which the kernel will listen.")
117 def _ip_default(self):
118 if self.transport == 'ipc':
119 if self.connection_file:
120 return os.path.splitext(self.abs_connection_file)[0] + '-ipc'
121 else:
122 return 'kernel-ipc'
123 else:
124 return LOCALHOST
117 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
125 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
118 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
126 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
119 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
127 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
120 stdin_port = Integer(0, config=True, help="set the stdin (DEALER) port [default: random]")
128 stdin_port = Integer(0, config=True, help="set the stdin (DEALER) port [default: random]")
121 connection_file = Unicode('', config=True,
129 connection_file = Unicode('', config=True,
122 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
130 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
123
131
124 This file will contain the IP, ports, and authentication key needed to connect
132 This file will contain the IP, ports, and authentication key needed to connect
125 clients to this kernel. By default, this file will be created in the security-dir
133 clients to this kernel. By default, this file will be created in the security dir
126 of the current profile, but can be specified by absolute path.
134 of the current profile, but can be specified by absolute path.
127 """)
135 """)
136 @property
137 def abs_connection_file(self):
138 if os.path.basename(self.connection_file) == self.connection_file:
139 return os.path.join(self.profile_dir.security_dir, self.connection_file)
140 else:
141 return self.connection_file
142
128
143
129 # streams, etc.
144 # streams, etc.
130 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
145 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
131 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
146 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
132 outstream_class = DottedObjectName('IPython.zmq.iostream.OutStream',
147 outstream_class = DottedObjectName('IPython.zmq.iostream.OutStream',
133 config=True, help="The importstring for the OutStream factory")
148 config=True, help="The importstring for the OutStream factory")
134 displayhook_class = DottedObjectName('IPython.zmq.displayhook.ZMQDisplayHook',
149 displayhook_class = DottedObjectName('IPython.zmq.displayhook.ZMQDisplayHook',
135 config=True, help="The importstring for the DisplayHook factory")
150 config=True, help="The importstring for the DisplayHook factory")
136
151
137 # polling
152 # polling
138 parent = Integer(0, config=True,
153 parent = Integer(0, config=True,
139 help="""kill this process if its parent dies. On Windows, the argument
154 help="""kill this process if its parent dies. On Windows, the argument
140 specifies the HANDLE of the parent process, otherwise it is simply boolean.
155 specifies the HANDLE of the parent process, otherwise it is simply boolean.
141 """)
156 """)
142 interrupt = Integer(0, config=True,
157 interrupt = Integer(0, config=True,
143 help="""ONLY USED ON WINDOWS
158 help="""ONLY USED ON WINDOWS
144 Interrupt this process when the parent is signalled.
159 Interrupt this process when the parent is signaled.
145 """)
160 """)
146
161
147 def init_crash_handler(self):
162 def init_crash_handler(self):
148 # Install minimal exception handling
163 # Install minimal exception handling
149 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
164 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
150 ostream=sys.__stdout__)
165 ostream=sys.__stdout__)
151
166
152 def init_poller(self):
167 def init_poller(self):
153 if sys.platform == 'win32':
168 if sys.platform == 'win32':
154 if self.interrupt or self.parent:
169 if self.interrupt or self.parent:
155 self.poller = ParentPollerWindows(self.interrupt, self.parent)
170 self.poller = ParentPollerWindows(self.interrupt, self.parent)
156 elif self.parent:
171 elif self.parent:
157 self.poller = ParentPollerUnix()
172 self.poller = ParentPollerUnix()
158
173
159 def _bind_socket(self, s, port):
174 def _bind_socket(self, s, port):
160 iface = '%s://%s' % (self.transport, self.ip)
175 iface = '%s://%s' % (self.transport, self.ip)
161 if port <= 0 and self.transport == 'tcp':
176 if self.transport == 'tcp':
162 port = s.bind_to_random_port(iface)
177 if port <= 0:
163 else:
178 port = s.bind_to_random_port(iface)
164 c = ':' if self.transport == 'tcp' else '-'
179 else:
165 s.bind(iface + c + str(port))
180 s.bind("tcp://%s:%i" % (self.ip, port))
181 elif self.transport == 'ipc':
182 if port <= 0:
183 port = 1
184 path = "%s-%i" % (self.ip, port)
185 while os.path.exists(path):
186 port = port + 1
187 path = "%s-%i" % (self.ip, port)
188 else:
189 path = "%s-%i" % (self.ip, port)
190 s.bind("ipc://%s" % path)
166 return port
191 return port
167
192
168 def load_connection_file(self):
193 def load_connection_file(self):
169 """load ip/port/hmac config from JSON connection file"""
194 """load ip/port/hmac config from JSON connection file"""
170 try:
195 try:
171 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
196 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
172 except IOError:
197 except IOError:
173 self.log.debug("Connection file not found: %s", self.connection_file)
198 self.log.debug("Connection file not found: %s", self.connection_file)
174 # This means I own it, so I will clean it up:
199 # This means I own it, so I will clean it up:
175 atexit.register(self.cleanup_connection_file)
200 atexit.register(self.cleanup_connection_file)
176 return
201 return
177 self.log.debug(u"Loading connection file %s", fname)
202 self.log.debug(u"Loading connection file %s", fname)
178 with open(fname) as f:
203 with open(fname) as f:
179 s = f.read()
204 s = f.read()
180 cfg = json.loads(s)
205 cfg = json.loads(s)
181 self.transport = cfg.get('transport', self.transport)
206 self.transport = cfg.get('transport', self.transport)
182 if self.ip == LOCALHOST and 'ip' in cfg:
207 if self.ip == self._ip_default() and 'ip' in cfg:
183 # not overridden by config or cl_args
208 # not overridden by config or cl_args
184 self.ip = cfg['ip']
209 self.ip = cfg['ip']
185 for channel in ('hb', 'shell', 'iopub', 'stdin'):
210 for channel in ('hb', 'shell', 'iopub', 'stdin'):
186 name = channel + '_port'
211 name = channel + '_port'
187 if getattr(self, name) == 0 and name in cfg:
212 if getattr(self, name) == 0 and name in cfg:
188 # not overridden by config or cl_args
213 # not overridden by config or cl_args
189 setattr(self, name, cfg[name])
214 setattr(self, name, cfg[name])
190 if 'key' in cfg:
215 if 'key' in cfg:
191 self.config.Session.key = str_to_bytes(cfg['key'])
216 self.config.Session.key = str_to_bytes(cfg['key'])
192
217
193 def write_connection_file(self):
218 def write_connection_file(self):
194 """write connection info to JSON file"""
219 """write connection info to JSON file"""
195 if os.path.basename(self.connection_file) == self.connection_file:
220 cf = self.abs_connection_file
196 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
221 self.log.debug("Writing connection file: %s", cf)
197 else:
198 cf = self.connection_file
199 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
222 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
200 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
223 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
201 iopub_port=self.iopub_port)
224 iopub_port=self.iopub_port)
202
203 self._full_connection_file = cf
204
225
205 def cleanup_connection_file(self):
226 def cleanup_connection_file(self):
206 cf = self._full_connection_file
227 cf = self.abs_connection_file
207 self.log.debug("cleaning up connection file: %r", cf)
228 self.log.debug("Cleaning up connection file: %s", cf)
208 try:
229 try:
209 os.remove(cf)
230 os.remove(cf)
210 except (IOError, OSError):
231 except (IOError, OSError):
211 pass
232 pass
212
233
213 self.cleanup_ipc_files()
234 self.cleanup_ipc_files()
214
235
215 def cleanup_ipc_files(self):
236 def cleanup_ipc_files(self):
216 """cleanup ipc files if we wrote them"""
237 """cleanup ipc files if we wrote them"""
217 if self.transport != 'ipc':
238 if self.transport != 'ipc':
218 return
239 return
219 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port):
240 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port):
220 ipcfile = "%s-%i" % (self.ip, port)
241 ipcfile = "%s-%i" % (self.ip, port)
221 try:
242 try:
222 os.remove(ipcfile)
243 os.remove(ipcfile)
223 except (IOError, OSError):
244 except (IOError, OSError):
224 pass
245 pass
225
246
226 def init_connection_file(self):
247 def init_connection_file(self):
227 if not self.connection_file:
248 if not self.connection_file:
228 self.connection_file = "kernel-%s.json"%os.getpid()
249 self.connection_file = "kernel-%s.json"%os.getpid()
229 try:
250 try:
230 self.load_connection_file()
251 self.load_connection_file()
231 except Exception:
252 except Exception:
232 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
253 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
233 self.exit(1)
254 self.exit(1)
234
255
235 def init_sockets(self):
256 def init_sockets(self):
236 # Create a context, a session, and the kernel sockets.
257 # Create a context, a session, and the kernel sockets.
237 self.log.info("Starting the kernel at pid: %i", os.getpid())
258 self.log.info("Starting the kernel at pid: %i", os.getpid())
238 context = zmq.Context.instance()
259 context = zmq.Context.instance()
239 # Uncomment this to try closing the context.
260 # Uncomment this to try closing the context.
240 # atexit.register(context.term)
261 # atexit.register(context.term)
241
262
242 self.shell_socket = context.socket(zmq.ROUTER)
263 self.shell_socket = context.socket(zmq.ROUTER)
243 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
264 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
244 self.log.debug("shell ROUTER Channel on port: %i"%self.shell_port)
265 self.log.debug("shell ROUTER Channel on port: %i"%self.shell_port)
245
266
246 self.iopub_socket = context.socket(zmq.PUB)
267 self.iopub_socket = context.socket(zmq.PUB)
247 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
268 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
248 self.log.debug("iopub PUB Channel on port: %i"%self.iopub_port)
269 self.log.debug("iopub PUB Channel on port: %i"%self.iopub_port)
249
270
250 self.stdin_socket = context.socket(zmq.ROUTER)
271 self.stdin_socket = context.socket(zmq.ROUTER)
251 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
272 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
252 self.log.debug("stdin ROUTER Channel on port: %i"%self.stdin_port)
273 self.log.debug("stdin ROUTER Channel on port: %i"%self.stdin_port)
253
274
254 def init_heartbeat(self):
275 def init_heartbeat(self):
255 """start the heart beating"""
276 """start the heart beating"""
256 # heartbeat doesn't share context, because it mustn't be blocked
277 # heartbeat doesn't share context, because it mustn't be blocked
257 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
278 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
258 hb_ctx = zmq.Context()
279 hb_ctx = zmq.Context()
259 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
280 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
260 self.hb_port = self.heartbeat.port
281 self.hb_port = self.heartbeat.port
261 self.log.debug("Heartbeat REP Channel on port: %i"%self.hb_port)
282 self.log.debug("Heartbeat REP Channel on port: %i"%self.hb_port)
262 self.heartbeat.start()
283 self.heartbeat.start()
263
284
264 # Helper to make it easier to connect to an existing kernel.
285 # Helper to make it easier to connect to an existing kernel.
265 # set log-level to critical, to make sure it is output
286 # set log-level to critical, to make sure it is output
266 self.log.critical("To connect another client to this kernel, use:")
287 self.log.critical("To connect another client to this kernel, use:")
267
288
268 def log_connection_info(self):
289 def log_connection_info(self):
269 """display connection info, and store ports"""
290 """display connection info, and store ports"""
270 basename = os.path.basename(self.connection_file)
291 basename = os.path.basename(self.connection_file)
271 if basename == self.connection_file or \
292 if basename == self.connection_file or \
272 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
293 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
273 # use shortname
294 # use shortname
274 tail = basename
295 tail = basename
275 if self.profile != 'default':
296 if self.profile != 'default':
276 tail += " --profile %s" % self.profile
297 tail += " --profile %s" % self.profile
277 else:
298 else:
278 tail = self.connection_file
299 tail = self.connection_file
279 self.log.critical("--existing %s", tail)
300 self.log.critical("--existing %s", tail)
280
301
281
302
282 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
303 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
283 stdin=self.stdin_port, hb=self.hb_port)
304 stdin=self.stdin_port, hb=self.hb_port)
284
305
285 def init_session(self):
306 def init_session(self):
286 """create our session object"""
307 """create our session object"""
287 default_secure(self.config)
308 default_secure(self.config)
288 self.session = Session(config=self.config, username=u'kernel')
309 self.session = Session(config=self.config, username=u'kernel')
289
310
290 def init_blackhole(self):
311 def init_blackhole(self):
291 """redirects stdout/stderr to devnull if necessary"""
312 """redirects stdout/stderr to devnull if necessary"""
292 if self.no_stdout or self.no_stderr:
313 if self.no_stdout or self.no_stderr:
293 blackhole = open(os.devnull, 'w')
314 blackhole = open(os.devnull, 'w')
294 if self.no_stdout:
315 if self.no_stdout:
295 sys.stdout = sys.__stdout__ = blackhole
316 sys.stdout = sys.__stdout__ = blackhole
296 if self.no_stderr:
317 if self.no_stderr:
297 sys.stderr = sys.__stderr__ = blackhole
318 sys.stderr = sys.__stderr__ = blackhole
298
319
299 def init_io(self):
320 def init_io(self):
300 """Redirect input streams and set a display hook."""
321 """Redirect input streams and set a display hook."""
301 if self.outstream_class:
322 if self.outstream_class:
302 outstream_factory = import_item(str(self.outstream_class))
323 outstream_factory = import_item(str(self.outstream_class))
303 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
324 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
304 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
325 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
305 if self.displayhook_class:
326 if self.displayhook_class:
306 displayhook_factory = import_item(str(self.displayhook_class))
327 displayhook_factory = import_item(str(self.displayhook_class))
307 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
328 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
308
329
309 def init_signal(self):
330 def init_signal(self):
310 signal.signal(signal.SIGINT, signal.SIG_IGN)
331 signal.signal(signal.SIGINT, signal.SIG_IGN)
311
332
312 def init_kernel(self):
333 def init_kernel(self):
313 """Create the Kernel object itself"""
334 """Create the Kernel object itself"""
314 kernel_factory = import_item(str(self.kernel_class))
335 kernel_factory = import_item(str(self.kernel_class))
315 self.kernel = kernel_factory(config=self.config, session=self.session,
336 self.kernel = kernel_factory(config=self.config, session=self.session,
316 shell_socket=self.shell_socket,
337 shell_socket=self.shell_socket,
317 iopub_socket=self.iopub_socket,
338 iopub_socket=self.iopub_socket,
318 stdin_socket=self.stdin_socket,
339 stdin_socket=self.stdin_socket,
319 log=self.log
340 log=self.log
320 )
341 )
321 self.kernel.record_ports(self.ports)
342 self.kernel.record_ports(self.ports)
322
343
323 @catch_config_error
344 @catch_config_error
324 def initialize(self, argv=None):
345 def initialize(self, argv=None):
325 super(KernelApp, self).initialize(argv)
346 super(KernelApp, self).initialize(argv)
326 self.init_blackhole()
347 self.init_blackhole()
327 self.init_connection_file()
348 self.init_connection_file()
328 self.init_session()
349 self.init_session()
329 self.init_poller()
350 self.init_poller()
330 self.init_sockets()
351 self.init_sockets()
331 self.init_heartbeat()
352 self.init_heartbeat()
332 # writing/displaying connection info must be *after* init_sockets/heartbeat
353 # writing/displaying connection info must be *after* init_sockets/heartbeat
333 self.log_connection_info()
354 self.log_connection_info()
334 self.write_connection_file()
355 self.write_connection_file()
335 self.init_io()
356 self.init_io()
336 self.init_signal()
357 self.init_signal()
337 self.init_kernel()
358 self.init_kernel()
338 # flush stdout/stderr, so that anything written to these streams during
359 # flush stdout/stderr, so that anything written to these streams during
339 # initialization do not get associated with the first execution request
360 # initialization do not get associated with the first execution request
340 sys.stdout.flush()
361 sys.stdout.flush()
341 sys.stderr.flush()
362 sys.stderr.flush()
342
363
343 def start(self):
364 def start(self):
344 if self.poller is not None:
365 if self.poller is not None:
345 self.poller.start()
366 self.poller.start()
346 self.kernel.start()
367 self.kernel.start()
347 try:
368 try:
348 ioloop.IOLoop.instance().start()
369 ioloop.IOLoop.instance().start()
349 except KeyboardInterrupt:
370 except KeyboardInterrupt:
350 pass
371 pass
351
372
@@ -1,1081 +1,1094
1 """Base classes to manage the interaction with a running kernel.
1 """Base classes to manage the interaction with a running kernel.
2
2
3 TODO
3 TODO
4 * Create logger to handle debugging and console messages.
4 * Create logger to handle debugging and console messages.
5 """
5 """
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2008-2011 The IPython Development Team
8 # Copyright (C) 2008-2011 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 # Standard library imports.
18 # Standard library imports.
19 import atexit
19 import atexit
20 import errno
20 import errno
21 import json
21 import json
22 from subprocess import Popen
22 from subprocess import Popen
23 import os
23 import os
24 import signal
24 import signal
25 import sys
25 import sys
26 from threading import Thread
26 from threading import Thread
27 import time
27 import time
28
28
29 # System library imports.
29 # System library imports.
30 import zmq
30 import zmq
31 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
31 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
32 # during garbage collection of threads at exit:
32 # during garbage collection of threads at exit:
33 from zmq import ZMQError
33 from zmq import ZMQError
34 from zmq.eventloop import ioloop, zmqstream
34 from zmq.eventloop import ioloop, zmqstream
35
35
36 # Local imports.
36 # Local imports.
37 from IPython.config.configurable import Configurable
37 from IPython.config.configurable import Configurable
38 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
38 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
39 from IPython.utils.traitlets import (
39 from IPython.utils.traitlets import (
40 Any, Instance, Type, Unicode, Integer, Bool, CaselessStrEnum
40 Any, Instance, Type, Unicode, Integer, Bool, CaselessStrEnum
41 )
41 )
42 from IPython.utils.py3compat import str_to_bytes
42 from IPython.utils.py3compat import str_to_bytes
43 from IPython.zmq.entry_point import write_connection_file
43 from IPython.zmq.entry_point import write_connection_file
44 from session import Session
44 from session import Session
45 from IPython.zmq.kernelmanagerabc import (
45 from IPython.zmq.kernelmanagerabc import (
46 ShellChannelABC, IOPubChannelABC,
46 ShellChannelABC, IOPubChannelABC,
47 HBChannelABC, StdInChannelABC,
47 HBChannelABC, StdInChannelABC,
48 KernelManagerABC
48 KernelManagerABC
49 )
49 )
50
50
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # Constants and exceptions
53 # Constants and exceptions
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56 class InvalidPortNumber(Exception):
56 class InvalidPortNumber(Exception):
57 pass
57 pass
58
58
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60 # Utility functions
60 # Utility functions
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62
62
63 # some utilities to validate message structure, these might get moved elsewhere
63 # some utilities to validate message structure, these might get moved elsewhere
64 # if they prove to have more generic utility
64 # if they prove to have more generic utility
65
65
66 def validate_string_list(lst):
66 def validate_string_list(lst):
67 """Validate that the input is a list of strings.
67 """Validate that the input is a list of strings.
68
68
69 Raises ValueError if not."""
69 Raises ValueError if not."""
70 if not isinstance(lst, list):
70 if not isinstance(lst, list):
71 raise ValueError('input %r must be a list' % lst)
71 raise ValueError('input %r must be a list' % lst)
72 for x in lst:
72 for x in lst:
73 if not isinstance(x, basestring):
73 if not isinstance(x, basestring):
74 raise ValueError('element %r in list must be a string' % x)
74 raise ValueError('element %r in list must be a string' % x)
75
75
76
76
77 def validate_string_dict(dct):
77 def validate_string_dict(dct):
78 """Validate that the input is a dict with string keys and values.
78 """Validate that the input is a dict with string keys and values.
79
79
80 Raises ValueError if not."""
80 Raises ValueError if not."""
81 for k,v in dct.iteritems():
81 for k,v in dct.iteritems():
82 if not isinstance(k, basestring):
82 if not isinstance(k, basestring):
83 raise ValueError('key %r in dict must be a string' % k)
83 raise ValueError('key %r in dict must be a string' % k)
84 if not isinstance(v, basestring):
84 if not isinstance(v, basestring):
85 raise ValueError('value %r in dict must be a string' % v)
85 raise ValueError('value %r in dict must be a string' % v)
86
86
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # ZMQ Socket Channel classes
89 # ZMQ Socket Channel classes
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 class ZMQSocketChannel(Thread):
92 class ZMQSocketChannel(Thread):
93 """The base class for the channels that use ZMQ sockets."""
93 """The base class for the channels that use ZMQ sockets."""
94 context = None
94 context = None
95 session = None
95 session = None
96 socket = None
96 socket = None
97 ioloop = None
97 ioloop = None
98 stream = None
98 stream = None
99 _address = None
99 _address = None
100 _exiting = False
100 _exiting = False
101
101
102 def __init__(self, context, session, address):
102 def __init__(self, context, session, address):
103 """Create a channel.
103 """Create a channel.
104
104
105 Parameters
105 Parameters
106 ----------
106 ----------
107 context : :class:`zmq.Context`
107 context : :class:`zmq.Context`
108 The ZMQ context to use.
108 The ZMQ context to use.
109 session : :class:`session.Session`
109 session : :class:`session.Session`
110 The session to use.
110 The session to use.
111 address : zmq url
111 address : zmq url
112 Standard (ip, port) tuple that the kernel is listening on.
112 Standard (ip, port) tuple that the kernel is listening on.
113 """
113 """
114 super(ZMQSocketChannel, self).__init__()
114 super(ZMQSocketChannel, self).__init__()
115 self.daemon = True
115 self.daemon = True
116
116
117 self.context = context
117 self.context = context
118 self.session = session
118 self.session = session
119 if isinstance(address, tuple):
119 if isinstance(address, tuple):
120 if address[1] == 0:
120 if address[1] == 0:
121 message = 'The port number for a channel cannot be 0.'
121 message = 'The port number for a channel cannot be 0.'
122 raise InvalidPortNumber(message)
122 raise InvalidPortNumber(message)
123 address = "tcp://%s:%i" % address
123 address = "tcp://%s:%i" % address
124 self._address = address
124 self._address = address
125 atexit.register(self._notice_exit)
125 atexit.register(self._notice_exit)
126
126
127 def _notice_exit(self):
127 def _notice_exit(self):
128 self._exiting = True
128 self._exiting = True
129
129
130 def _run_loop(self):
130 def _run_loop(self):
131 """Run my loop, ignoring EINTR events in the poller"""
131 """Run my loop, ignoring EINTR events in the poller"""
132 while True:
132 while True:
133 try:
133 try:
134 self.ioloop.start()
134 self.ioloop.start()
135 except ZMQError as e:
135 except ZMQError as e:
136 if e.errno == errno.EINTR:
136 if e.errno == errno.EINTR:
137 continue
137 continue
138 else:
138 else:
139 raise
139 raise
140 except Exception:
140 except Exception:
141 if self._exiting:
141 if self._exiting:
142 break
142 break
143 else:
143 else:
144 raise
144 raise
145 else:
145 else:
146 break
146 break
147
147
148 def stop(self):
148 def stop(self):
149 """Stop the channel's event loop and join its thread.
149 """Stop the channel's event loop and join its thread.
150
150
151 This calls :method:`Thread.join` and returns when the thread
151 This calls :method:`Thread.join` and returns when the thread
152 terminates. :class:`RuntimeError` will be raised if
152 terminates. :class:`RuntimeError` will be raised if
153 :method:`self.start` is called again.
153 :method:`self.start` is called again.
154 """
154 """
155 self.join()
155 self.join()
156
156
157 @property
157 @property
158 def address(self):
158 def address(self):
159 """Get the channel's address as a zmq url string.
159 """Get the channel's address as a zmq url string.
160
160
161 These URLS have the form: 'tcp://127.0.0.1:5555'.
161 These URLS have the form: 'tcp://127.0.0.1:5555'.
162 """
162 """
163 return self._address
163 return self._address
164
164
165 def _queue_send(self, msg):
165 def _queue_send(self, msg):
166 """Queue a message to be sent from the IOLoop's thread.
166 """Queue a message to be sent from the IOLoop's thread.
167
167
168 Parameters
168 Parameters
169 ----------
169 ----------
170 msg : message to send
170 msg : message to send
171
171
172 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
172 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
173 thread control of the action.
173 thread control of the action.
174 """
174 """
175 def thread_send():
175 def thread_send():
176 self.session.send(self.stream, msg)
176 self.session.send(self.stream, msg)
177 self.ioloop.add_callback(thread_send)
177 self.ioloop.add_callback(thread_send)
178
178
179 def _handle_recv(self, msg):
179 def _handle_recv(self, msg):
180 """Callback for stream.on_recv.
180 """Callback for stream.on_recv.
181
181
182 Unpacks message, and calls handlers with it.
182 Unpacks message, and calls handlers with it.
183 """
183 """
184 ident,smsg = self.session.feed_identities(msg)
184 ident,smsg = self.session.feed_identities(msg)
185 self.call_handlers(self.session.unserialize(smsg))
185 self.call_handlers(self.session.unserialize(smsg))
186
186
187
187
188
188
189 class ShellChannel(ZMQSocketChannel):
189 class ShellChannel(ZMQSocketChannel):
190 """The shell channel for issuing request/replies to the kernel."""
190 """The shell channel for issuing request/replies to the kernel."""
191
191
192 command_queue = None
192 command_queue = None
193 # flag for whether execute requests should be allowed to call raw_input:
193 # flag for whether execute requests should be allowed to call raw_input:
194 allow_stdin = True
194 allow_stdin = True
195
195
196 def __init__(self, context, session, address):
196 def __init__(self, context, session, address):
197 super(ShellChannel, self).__init__(context, session, address)
197 super(ShellChannel, self).__init__(context, session, address)
198 self.ioloop = ioloop.IOLoop()
198 self.ioloop = ioloop.IOLoop()
199
199
200 def run(self):
200 def run(self):
201 """The thread's main activity. Call start() instead."""
201 """The thread's main activity. Call start() instead."""
202 self.socket = self.context.socket(zmq.DEALER)
202 self.socket = self.context.socket(zmq.DEALER)
203 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
203 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
204 self.socket.connect(self.address)
204 self.socket.connect(self.address)
205 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
205 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
206 self.stream.on_recv(self._handle_recv)
206 self.stream.on_recv(self._handle_recv)
207 self._run_loop()
207 self._run_loop()
208 try:
208 try:
209 self.socket.close()
209 self.socket.close()
210 except:
210 except:
211 pass
211 pass
212
212
213 def stop(self):
213 def stop(self):
214 """Stop the channel's event loop and join its thread."""
214 """Stop the channel's event loop and join its thread."""
215 self.ioloop.stop()
215 self.ioloop.stop()
216 super(ShellChannel, self).stop()
216 super(ShellChannel, self).stop()
217
217
218 def call_handlers(self, msg):
218 def call_handlers(self, msg):
219 """This method is called in the ioloop thread when a message arrives.
219 """This method is called in the ioloop thread when a message arrives.
220
220
221 Subclasses should override this method to handle incoming messages.
221 Subclasses should override this method to handle incoming messages.
222 It is important to remember that this method is called in the thread
222 It is important to remember that this method is called in the thread
223 so that some logic must be done to ensure that the application leve
223 so that some logic must be done to ensure that the application leve
224 handlers are called in the application thread.
224 handlers are called in the application thread.
225 """
225 """
226 raise NotImplementedError('call_handlers must be defined in a subclass.')
226 raise NotImplementedError('call_handlers must be defined in a subclass.')
227
227
228 def execute(self, code, silent=False, store_history=True,
228 def execute(self, code, silent=False, store_history=True,
229 user_variables=None, user_expressions=None, allow_stdin=None):
229 user_variables=None, user_expressions=None, allow_stdin=None):
230 """Execute code in the kernel.
230 """Execute code in the kernel.
231
231
232 Parameters
232 Parameters
233 ----------
233 ----------
234 code : str
234 code : str
235 A string of Python code.
235 A string of Python code.
236
236
237 silent : bool, optional (default False)
237 silent : bool, optional (default False)
238 If set, the kernel will execute the code as quietly possible, and
238 If set, the kernel will execute the code as quietly possible, and
239 will force store_history to be False.
239 will force store_history to be False.
240
240
241 store_history : bool, optional (default True)
241 store_history : bool, optional (default True)
242 If set, the kernel will store command history. This is forced
242 If set, the kernel will store command history. This is forced
243 to be False if silent is True.
243 to be False if silent is True.
244
244
245 user_variables : list, optional
245 user_variables : list, optional
246 A list of variable names to pull from the user's namespace. They
246 A list of variable names to pull from the user's namespace. They
247 will come back as a dict with these names as keys and their
247 will come back as a dict with these names as keys and their
248 :func:`repr` as values.
248 :func:`repr` as values.
249
249
250 user_expressions : dict, optional
250 user_expressions : dict, optional
251 A dict mapping names to expressions to be evaluated in the user's
251 A dict mapping names to expressions to be evaluated in the user's
252 dict. The expression values are returned as strings formatted using
252 dict. The expression values are returned as strings formatted using
253 :func:`repr`.
253 :func:`repr`.
254
254
255 allow_stdin : bool, optional (default self.allow_stdin)
255 allow_stdin : bool, optional (default self.allow_stdin)
256 Flag for whether the kernel can send stdin requests to frontends.
256 Flag for whether the kernel can send stdin requests to frontends.
257
257
258 Some frontends (e.g. the Notebook) do not support stdin requests.
258 Some frontends (e.g. the Notebook) do not support stdin requests.
259 If raw_input is called from code executed from such a frontend, a
259 If raw_input is called from code executed from such a frontend, a
260 StdinNotImplementedError will be raised.
260 StdinNotImplementedError will be raised.
261
261
262 Returns
262 Returns
263 -------
263 -------
264 The msg_id of the message sent.
264 The msg_id of the message sent.
265 """
265 """
266 if user_variables is None:
266 if user_variables is None:
267 user_variables = []
267 user_variables = []
268 if user_expressions is None:
268 if user_expressions is None:
269 user_expressions = {}
269 user_expressions = {}
270 if allow_stdin is None:
270 if allow_stdin is None:
271 allow_stdin = self.allow_stdin
271 allow_stdin = self.allow_stdin
272
272
273
273
274 # Don't waste network traffic if inputs are invalid
274 # Don't waste network traffic if inputs are invalid
275 if not isinstance(code, basestring):
275 if not isinstance(code, basestring):
276 raise ValueError('code %r must be a string' % code)
276 raise ValueError('code %r must be a string' % code)
277 validate_string_list(user_variables)
277 validate_string_list(user_variables)
278 validate_string_dict(user_expressions)
278 validate_string_dict(user_expressions)
279
279
280 # Create class for content/msg creation. Related to, but possibly
280 # Create class for content/msg creation. Related to, but possibly
281 # not in Session.
281 # not in Session.
282 content = dict(code=code, silent=silent, store_history=store_history,
282 content = dict(code=code, silent=silent, store_history=store_history,
283 user_variables=user_variables,
283 user_variables=user_variables,
284 user_expressions=user_expressions,
284 user_expressions=user_expressions,
285 allow_stdin=allow_stdin,
285 allow_stdin=allow_stdin,
286 )
286 )
287 msg = self.session.msg('execute_request', content)
287 msg = self.session.msg('execute_request', content)
288 self._queue_send(msg)
288 self._queue_send(msg)
289 return msg['header']['msg_id']
289 return msg['header']['msg_id']
290
290
291 def complete(self, text, line, cursor_pos, block=None):
291 def complete(self, text, line, cursor_pos, block=None):
292 """Tab complete text in the kernel's namespace.
292 """Tab complete text in the kernel's namespace.
293
293
294 Parameters
294 Parameters
295 ----------
295 ----------
296 text : str
296 text : str
297 The text to complete.
297 The text to complete.
298 line : str
298 line : str
299 The full line of text that is the surrounding context for the
299 The full line of text that is the surrounding context for the
300 text to complete.
300 text to complete.
301 cursor_pos : int
301 cursor_pos : int
302 The position of the cursor in the line where the completion was
302 The position of the cursor in the line where the completion was
303 requested.
303 requested.
304 block : str, optional
304 block : str, optional
305 The full block of code in which the completion is being requested.
305 The full block of code in which the completion is being requested.
306
306
307 Returns
307 Returns
308 -------
308 -------
309 The msg_id of the message sent.
309 The msg_id of the message sent.
310 """
310 """
311 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
311 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
312 msg = self.session.msg('complete_request', content)
312 msg = self.session.msg('complete_request', content)
313 self._queue_send(msg)
313 self._queue_send(msg)
314 return msg['header']['msg_id']
314 return msg['header']['msg_id']
315
315
316 def object_info(self, oname, detail_level=0):
316 def object_info(self, oname, detail_level=0):
317 """Get metadata information about an object in the kernel's namespace.
317 """Get metadata information about an object in the kernel's namespace.
318
318
319 Parameters
319 Parameters
320 ----------
320 ----------
321 oname : str
321 oname : str
322 A string specifying the object name.
322 A string specifying the object name.
323 detail_level : int, optional
323 detail_level : int, optional
324 The level of detail for the introspection (0-2)
324 The level of detail for the introspection (0-2)
325
325
326 Returns
326 Returns
327 -------
327 -------
328 The msg_id of the message sent.
328 The msg_id of the message sent.
329 """
329 """
330 content = dict(oname=oname, detail_level=detail_level)
330 content = dict(oname=oname, detail_level=detail_level)
331 msg = self.session.msg('object_info_request', content)
331 msg = self.session.msg('object_info_request', content)
332 self._queue_send(msg)
332 self._queue_send(msg)
333 return msg['header']['msg_id']
333 return msg['header']['msg_id']
334
334
335 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
335 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
336 """Get entries from the kernel's history list.
336 """Get entries from the kernel's history list.
337
337
338 Parameters
338 Parameters
339 ----------
339 ----------
340 raw : bool
340 raw : bool
341 If True, return the raw input.
341 If True, return the raw input.
342 output : bool
342 output : bool
343 If True, then return the output as well.
343 If True, then return the output as well.
344 hist_access_type : str
344 hist_access_type : str
345 'range' (fill in session, start and stop params), 'tail' (fill in n)
345 'range' (fill in session, start and stop params), 'tail' (fill in n)
346 or 'search' (fill in pattern param).
346 or 'search' (fill in pattern param).
347
347
348 session : int
348 session : int
349 For a range request, the session from which to get lines. Session
349 For a range request, the session from which to get lines. Session
350 numbers are positive integers; negative ones count back from the
350 numbers are positive integers; negative ones count back from the
351 current session.
351 current session.
352 start : int
352 start : int
353 The first line number of a history range.
353 The first line number of a history range.
354 stop : int
354 stop : int
355 The final (excluded) line number of a history range.
355 The final (excluded) line number of a history range.
356
356
357 n : int
357 n : int
358 The number of lines of history to get for a tail request.
358 The number of lines of history to get for a tail request.
359
359
360 pattern : str
360 pattern : str
361 The glob-syntax pattern for a search request.
361 The glob-syntax pattern for a search request.
362
362
363 Returns
363 Returns
364 -------
364 -------
365 The msg_id of the message sent.
365 The msg_id of the message sent.
366 """
366 """
367 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
367 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
368 **kwargs)
368 **kwargs)
369 msg = self.session.msg('history_request', content)
369 msg = self.session.msg('history_request', content)
370 self._queue_send(msg)
370 self._queue_send(msg)
371 return msg['header']['msg_id']
371 return msg['header']['msg_id']
372
372
373 def kernel_info(self):
373 def kernel_info(self):
374 """Request kernel info."""
374 """Request kernel info."""
375 msg = self.session.msg('kernel_info_request')
375 msg = self.session.msg('kernel_info_request')
376 self._queue_send(msg)
376 self._queue_send(msg)
377 return msg['header']['msg_id']
377 return msg['header']['msg_id']
378
378
379 def shutdown(self, restart=False):
379 def shutdown(self, restart=False):
380 """Request an immediate kernel shutdown.
380 """Request an immediate kernel shutdown.
381
381
382 Upon receipt of the (empty) reply, client code can safely assume that
382 Upon receipt of the (empty) reply, client code can safely assume that
383 the kernel has shut down and it's safe to forcefully terminate it if
383 the kernel has shut down and it's safe to forcefully terminate it if
384 it's still alive.
384 it's still alive.
385
385
386 The kernel will send the reply via a function registered with Python's
386 The kernel will send the reply via a function registered with Python's
387 atexit module, ensuring it's truly done as the kernel is done with all
387 atexit module, ensuring it's truly done as the kernel is done with all
388 normal operation.
388 normal operation.
389 """
389 """
390 # Send quit message to kernel. Once we implement kernel-side setattr,
390 # Send quit message to kernel. Once we implement kernel-side setattr,
391 # this should probably be done that way, but for now this will do.
391 # this should probably be done that way, but for now this will do.
392 msg = self.session.msg('shutdown_request', {'restart':restart})
392 msg = self.session.msg('shutdown_request', {'restart':restart})
393 self._queue_send(msg)
393 self._queue_send(msg)
394 return msg['header']['msg_id']
394 return msg['header']['msg_id']
395
395
396
396
397
397
398 class IOPubChannel(ZMQSocketChannel):
398 class IOPubChannel(ZMQSocketChannel):
399 """The iopub channel which listens for messages that the kernel publishes.
399 """The iopub channel which listens for messages that the kernel publishes.
400
400
401 This channel is where all output is published to frontends.
401 This channel is where all output is published to frontends.
402 """
402 """
403
403
404 def __init__(self, context, session, address):
404 def __init__(self, context, session, address):
405 super(IOPubChannel, self).__init__(context, session, address)
405 super(IOPubChannel, self).__init__(context, session, address)
406 self.ioloop = ioloop.IOLoop()
406 self.ioloop = ioloop.IOLoop()
407
407
408 def run(self):
408 def run(self):
409 """The thread's main activity. Call start() instead."""
409 """The thread's main activity. Call start() instead."""
410 self.socket = self.context.socket(zmq.SUB)
410 self.socket = self.context.socket(zmq.SUB)
411 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
411 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
412 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
412 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
413 self.socket.connect(self.address)
413 self.socket.connect(self.address)
414 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
414 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
415 self.stream.on_recv(self._handle_recv)
415 self.stream.on_recv(self._handle_recv)
416 self._run_loop()
416 self._run_loop()
417 try:
417 try:
418 self.socket.close()
418 self.socket.close()
419 except:
419 except:
420 pass
420 pass
421
421
422 def stop(self):
422 def stop(self):
423 """Stop the channel's event loop and join its thread."""
423 """Stop the channel's event loop and join its thread."""
424 self.ioloop.stop()
424 self.ioloop.stop()
425 super(IOPubChannel, self).stop()
425 super(IOPubChannel, self).stop()
426
426
427 def call_handlers(self, msg):
427 def call_handlers(self, msg):
428 """This method is called in the ioloop thread when a message arrives.
428 """This method is called in the ioloop thread when a message arrives.
429
429
430 Subclasses should override this method to handle incoming messages.
430 Subclasses should override this method to handle incoming messages.
431 It is important to remember that this method is called in the thread
431 It is important to remember that this method is called in the thread
432 so that some logic must be done to ensure that the application leve
432 so that some logic must be done to ensure that the application leve
433 handlers are called in the application thread.
433 handlers are called in the application thread.
434 """
434 """
435 raise NotImplementedError('call_handlers must be defined in a subclass.')
435 raise NotImplementedError('call_handlers must be defined in a subclass.')
436
436
437 def flush(self, timeout=1.0):
437 def flush(self, timeout=1.0):
438 """Immediately processes all pending messages on the iopub channel.
438 """Immediately processes all pending messages on the iopub channel.
439
439
440 Callers should use this method to ensure that :method:`call_handlers`
440 Callers should use this method to ensure that :method:`call_handlers`
441 has been called for all messages that have been received on the
441 has been called for all messages that have been received on the
442 0MQ SUB socket of this channel.
442 0MQ SUB socket of this channel.
443
443
444 This method is thread safe.
444 This method is thread safe.
445
445
446 Parameters
446 Parameters
447 ----------
447 ----------
448 timeout : float, optional
448 timeout : float, optional
449 The maximum amount of time to spend flushing, in seconds. The
449 The maximum amount of time to spend flushing, in seconds. The
450 default is one second.
450 default is one second.
451 """
451 """
452 # We do the IOLoop callback process twice to ensure that the IOLoop
452 # We do the IOLoop callback process twice to ensure that the IOLoop
453 # gets to perform at least one full poll.
453 # gets to perform at least one full poll.
454 stop_time = time.time() + timeout
454 stop_time = time.time() + timeout
455 for i in xrange(2):
455 for i in xrange(2):
456 self._flushed = False
456 self._flushed = False
457 self.ioloop.add_callback(self._flush)
457 self.ioloop.add_callback(self._flush)
458 while not self._flushed and time.time() < stop_time:
458 while not self._flushed and time.time() < stop_time:
459 time.sleep(0.01)
459 time.sleep(0.01)
460
460
461 def _flush(self):
461 def _flush(self):
462 """Callback for :method:`self.flush`."""
462 """Callback for :method:`self.flush`."""
463 self.stream.flush()
463 self.stream.flush()
464 self._flushed = True
464 self._flushed = True
465
465
466
466
467 class StdInChannel(ZMQSocketChannel):
467 class StdInChannel(ZMQSocketChannel):
468 """The stdin channel to handle raw_input requests that the kernel makes."""
468 """The stdin channel to handle raw_input requests that the kernel makes."""
469
469
470 msg_queue = None
470 msg_queue = None
471
471
472 def __init__(self, context, session, address):
472 def __init__(self, context, session, address):
473 super(StdInChannel, self).__init__(context, session, address)
473 super(StdInChannel, self).__init__(context, session, address)
474 self.ioloop = ioloop.IOLoop()
474 self.ioloop = ioloop.IOLoop()
475
475
476 def run(self):
476 def run(self):
477 """The thread's main activity. Call start() instead."""
477 """The thread's main activity. Call start() instead."""
478 self.socket = self.context.socket(zmq.DEALER)
478 self.socket = self.context.socket(zmq.DEALER)
479 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
479 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
480 self.socket.connect(self.address)
480 self.socket.connect(self.address)
481 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
481 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
482 self.stream.on_recv(self._handle_recv)
482 self.stream.on_recv(self._handle_recv)
483 self._run_loop()
483 self._run_loop()
484 try:
484 try:
485 self.socket.close()
485 self.socket.close()
486 except:
486 except:
487 pass
487 pass
488
488
489 def stop(self):
489 def stop(self):
490 """Stop the channel's event loop and join its thread."""
490 """Stop the channel's event loop and join its thread."""
491 self.ioloop.stop()
491 self.ioloop.stop()
492 super(StdInChannel, self).stop()
492 super(StdInChannel, self).stop()
493
493
494 def call_handlers(self, msg):
494 def call_handlers(self, msg):
495 """This method is called in the ioloop thread when a message arrives.
495 """This method is called in the ioloop thread when a message arrives.
496
496
497 Subclasses should override this method to handle incoming messages.
497 Subclasses should override this method to handle incoming messages.
498 It is important to remember that this method is called in the thread
498 It is important to remember that this method is called in the thread
499 so that some logic must be done to ensure that the application leve
499 so that some logic must be done to ensure that the application leve
500 handlers are called in the application thread.
500 handlers are called in the application thread.
501 """
501 """
502 raise NotImplementedError('call_handlers must be defined in a subclass.')
502 raise NotImplementedError('call_handlers must be defined in a subclass.')
503
503
504 def input(self, string):
504 def input(self, string):
505 """Send a string of raw input to the kernel."""
505 """Send a string of raw input to the kernel."""
506 content = dict(value=string)
506 content = dict(value=string)
507 msg = self.session.msg('input_reply', content)
507 msg = self.session.msg('input_reply', content)
508 self._queue_send(msg)
508 self._queue_send(msg)
509
509
510
510
511 class HBChannel(ZMQSocketChannel):
511 class HBChannel(ZMQSocketChannel):
512 """The heartbeat channel which monitors the kernel heartbeat.
512 """The heartbeat channel which monitors the kernel heartbeat.
513
513
514 Note that the heartbeat channel is paused by default. As long as you start
514 Note that the heartbeat channel is paused by default. As long as you start
515 this channel, the kernel manager will ensure that it is paused and un-paused
515 this channel, the kernel manager will ensure that it is paused and un-paused
516 as appropriate.
516 as appropriate.
517 """
517 """
518
518
519 time_to_dead = 3.0
519 time_to_dead = 3.0
520 socket = None
520 socket = None
521 poller = None
521 poller = None
522 _running = None
522 _running = None
523 _pause = None
523 _pause = None
524 _beating = None
524 _beating = None
525
525
526 def __init__(self, context, session, address):
526 def __init__(self, context, session, address):
527 super(HBChannel, self).__init__(context, session, address)
527 super(HBChannel, self).__init__(context, session, address)
528 self._running = False
528 self._running = False
529 self._pause =True
529 self._pause =True
530 self.poller = zmq.Poller()
530 self.poller = zmq.Poller()
531
531
532 def _create_socket(self):
532 def _create_socket(self):
533 if self.socket is not None:
533 if self.socket is not None:
534 # close previous socket, before opening a new one
534 # close previous socket, before opening a new one
535 self.poller.unregister(self.socket)
535 self.poller.unregister(self.socket)
536 self.socket.close()
536 self.socket.close()
537 self.socket = self.context.socket(zmq.REQ)
537 self.socket = self.context.socket(zmq.REQ)
538 self.socket.setsockopt(zmq.LINGER, 0)
538 self.socket.setsockopt(zmq.LINGER, 0)
539 self.socket.connect(self.address)
539 self.socket.connect(self.address)
540
540
541 self.poller.register(self.socket, zmq.POLLIN)
541 self.poller.register(self.socket, zmq.POLLIN)
542
542
543 def _poll(self, start_time):
543 def _poll(self, start_time):
544 """poll for heartbeat replies until we reach self.time_to_dead.
544 """poll for heartbeat replies until we reach self.time_to_dead.
545
545
546 Ignores interrupts, and returns the result of poll(), which
546 Ignores interrupts, and returns the result of poll(), which
547 will be an empty list if no messages arrived before the timeout,
547 will be an empty list if no messages arrived before the timeout,
548 or the event tuple if there is a message to receive.
548 or the event tuple if there is a message to receive.
549 """
549 """
550
550
551 until_dead = self.time_to_dead - (time.time() - start_time)
551 until_dead = self.time_to_dead - (time.time() - start_time)
552 # ensure poll at least once
552 # ensure poll at least once
553 until_dead = max(until_dead, 1e-3)
553 until_dead = max(until_dead, 1e-3)
554 events = []
554 events = []
555 while True:
555 while True:
556 try:
556 try:
557 events = self.poller.poll(1000 * until_dead)
557 events = self.poller.poll(1000 * until_dead)
558 except ZMQError as e:
558 except ZMQError as e:
559 if e.errno == errno.EINTR:
559 if e.errno == errno.EINTR:
560 # ignore interrupts during heartbeat
560 # ignore interrupts during heartbeat
561 # this may never actually happen
561 # this may never actually happen
562 until_dead = self.time_to_dead - (time.time() - start_time)
562 until_dead = self.time_to_dead - (time.time() - start_time)
563 until_dead = max(until_dead, 1e-3)
563 until_dead = max(until_dead, 1e-3)
564 pass
564 pass
565 else:
565 else:
566 raise
566 raise
567 except Exception:
567 except Exception:
568 if self._exiting:
568 if self._exiting:
569 break
569 break
570 else:
570 else:
571 raise
571 raise
572 else:
572 else:
573 break
573 break
574 return events
574 return events
575
575
576 def run(self):
576 def run(self):
577 """The thread's main activity. Call start() instead."""
577 """The thread's main activity. Call start() instead."""
578 self._create_socket()
578 self._create_socket()
579 self._running = True
579 self._running = True
580 self._beating = True
580 self._beating = True
581
581
582 while self._running:
582 while self._running:
583 if self._pause:
583 if self._pause:
584 # just sleep, and skip the rest of the loop
584 # just sleep, and skip the rest of the loop
585 time.sleep(self.time_to_dead)
585 time.sleep(self.time_to_dead)
586 continue
586 continue
587
587
588 since_last_heartbeat = 0.0
588 since_last_heartbeat = 0.0
589 # io.rprint('Ping from HB channel') # dbg
589 # io.rprint('Ping from HB channel') # dbg
590 # no need to catch EFSM here, because the previous event was
590 # no need to catch EFSM here, because the previous event was
591 # either a recv or connect, which cannot be followed by EFSM
591 # either a recv or connect, which cannot be followed by EFSM
592 self.socket.send(b'ping')
592 self.socket.send(b'ping')
593 request_time = time.time()
593 request_time = time.time()
594 ready = self._poll(request_time)
594 ready = self._poll(request_time)
595 if ready:
595 if ready:
596 self._beating = True
596 self._beating = True
597 # the poll above guarantees we have something to recv
597 # the poll above guarantees we have something to recv
598 self.socket.recv()
598 self.socket.recv()
599 # sleep the remainder of the cycle
599 # sleep the remainder of the cycle
600 remainder = self.time_to_dead - (time.time() - request_time)
600 remainder = self.time_to_dead - (time.time() - request_time)
601 if remainder > 0:
601 if remainder > 0:
602 time.sleep(remainder)
602 time.sleep(remainder)
603 continue
603 continue
604 else:
604 else:
605 # nothing was received within the time limit, signal heart failure
605 # nothing was received within the time limit, signal heart failure
606 self._beating = False
606 self._beating = False
607 since_last_heartbeat = time.time() - request_time
607 since_last_heartbeat = time.time() - request_time
608 self.call_handlers(since_last_heartbeat)
608 self.call_handlers(since_last_heartbeat)
609 # and close/reopen the socket, because the REQ/REP cycle has been broken
609 # and close/reopen the socket, because the REQ/REP cycle has been broken
610 self._create_socket()
610 self._create_socket()
611 continue
611 continue
612 try:
612 try:
613 self.socket.close()
613 self.socket.close()
614 except:
614 except:
615 pass
615 pass
616
616
617 def pause(self):
617 def pause(self):
618 """Pause the heartbeat."""
618 """Pause the heartbeat."""
619 self._pause = True
619 self._pause = True
620
620
621 def unpause(self):
621 def unpause(self):
622 """Unpause the heartbeat."""
622 """Unpause the heartbeat."""
623 self._pause = False
623 self._pause = False
624
624
625 def is_beating(self):
625 def is_beating(self):
626 """Is the heartbeat running and responsive (and not paused)."""
626 """Is the heartbeat running and responsive (and not paused)."""
627 if self.is_alive() and not self._pause and self._beating:
627 if self.is_alive() and not self._pause and self._beating:
628 return True
628 return True
629 else:
629 else:
630 return False
630 return False
631
631
632 def stop(self):
632 def stop(self):
633 """Stop the channel's event loop and join its thread."""
633 """Stop the channel's event loop and join its thread."""
634 self._running = False
634 self._running = False
635 super(HBChannel, self).stop()
635 super(HBChannel, self).stop()
636
636
637 def call_handlers(self, since_last_heartbeat):
637 def call_handlers(self, since_last_heartbeat):
638 """This method is called in the ioloop thread when a message arrives.
638 """This method is called in the ioloop thread when a message arrives.
639
639
640 Subclasses should override this method to handle incoming messages.
640 Subclasses should override this method to handle incoming messages.
641 It is important to remember that this method is called in the thread
641 It is important to remember that this method is called in the thread
642 so that some logic must be done to ensure that the application level
642 so that some logic must be done to ensure that the application level
643 handlers are called in the application thread.
643 handlers are called in the application thread.
644 """
644 """
645 raise NotImplementedError('call_handlers must be defined in a subclass.')
645 raise NotImplementedError('call_handlers must be defined in a subclass.')
646
646
647
647
648 #-----------------------------------------------------------------------------
648 #-----------------------------------------------------------------------------
649 # Main kernel manager class
649 # Main kernel manager class
650 #-----------------------------------------------------------------------------
650 #-----------------------------------------------------------------------------
651
651
652 class KernelManager(Configurable):
652 class KernelManager(Configurable):
653 """Manages a single kernel on this host along with its channels.
653 """Manages a single kernel on this host along with its channels.
654
654
655 There are four channels associated with each kernel:
655 There are four channels associated with each kernel:
656
656
657 * shell: for request/reply calls to the kernel.
657 * shell: for request/reply calls to the kernel.
658 * iopub: for the kernel to publish results to frontends.
658 * iopub: for the kernel to publish results to frontends.
659 * hb: for monitoring the kernel's heartbeat.
659 * hb: for monitoring the kernel's heartbeat.
660 * stdin: for frontends to reply to raw_input calls in the kernel.
660 * stdin: for frontends to reply to raw_input calls in the kernel.
661
661
662 The usage of the channels that this class manages is optional. It is
662 The usage of the channels that this class manages is optional. It is
663 entirely possible to connect to the kernels directly using ZeroMQ
663 entirely possible to connect to the kernels directly using ZeroMQ
664 sockets. These channels are useful primarily for talking to a kernel
664 sockets. These channels are useful primarily for talking to a kernel
665 whose :class:`KernelManager` is in the same process.
665 whose :class:`KernelManager` is in the same process.
666
666
667 This version manages kernels started using Popen.
667 This version manages kernels started using Popen.
668 """
668 """
669 # The PyZMQ Context to use for communication with the kernel.
669 # The PyZMQ Context to use for communication with the kernel.
670 context = Instance(zmq.Context)
670 context = Instance(zmq.Context)
671 def _context_default(self):
671 def _context_default(self):
672 return zmq.Context.instance()
672 return zmq.Context.instance()
673
673
674 # The Session to use for communication with the kernel.
674 # The Session to use for communication with the kernel.
675 session = Instance(Session)
675 session = Instance(Session)
676 def _session_default(self):
676 def _session_default(self):
677 return Session(config=self.config)
677 return Session(config=self.config)
678
678
679 # The kernel process with which the KernelManager is communicating.
679 # The kernel process with which the KernelManager is communicating.
680 kernel = Instance(Popen)
680 kernel = Instance(Popen)
681
681
682 # The addresses for the communication channels.
682 # The addresses for the communication channels.
683 connection_file = Unicode('')
683 connection_file = Unicode('')
684
684
685 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
685 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
686
686
687 ip = Unicode(LOCALHOST, config=True)
687 ip = Unicode(LOCALHOST, config=True,
688 help="""Set the kernel\'s IP address [default localhost].
689 If the IP address is something other than localhost, then
690 Consoles on other machines will be able to connect
691 to the Kernel, so be careful!"""
692 )
693 def _ip_default(self):
694 if self.transport == 'ipc':
695 if self.connection_file:
696 return os.path.splitext(self.connection_file)[0] + '-ipc'
697 else:
698 return 'kernel-ipc'
699 else:
700 return LOCALHOST
688 def _ip_changed(self, name, old, new):
701 def _ip_changed(self, name, old, new):
689 if new == '*':
702 if new == '*':
690 self.ip = '0.0.0.0'
703 self.ip = '0.0.0.0'
691 shell_port = Integer(0)
704 shell_port = Integer(0)
692 iopub_port = Integer(0)
705 iopub_port = Integer(0)
693 stdin_port = Integer(0)
706 stdin_port = Integer(0)
694 hb_port = Integer(0)
707 hb_port = Integer(0)
695
708
696 # The classes to use for the various channels.
709 # The classes to use for the various channels.
697 shell_channel_class = Type(ShellChannel)
710 shell_channel_class = Type(ShellChannel)
698 iopub_channel_class = Type(IOPubChannel)
711 iopub_channel_class = Type(IOPubChannel)
699 stdin_channel_class = Type(StdInChannel)
712 stdin_channel_class = Type(StdInChannel)
700 hb_channel_class = Type(HBChannel)
713 hb_channel_class = Type(HBChannel)
701
714
702 # Protected traits.
715 # Protected traits.
703 _launch_args = Any
716 _launch_args = Any
704 _shell_channel = Any
717 _shell_channel = Any
705 _iopub_channel = Any
718 _iopub_channel = Any
706 _stdin_channel = Any
719 _stdin_channel = Any
707 _hb_channel = Any
720 _hb_channel = Any
708 _connection_file_written=Bool(False)
721 _connection_file_written=Bool(False)
709
722
710 def __del__(self):
723 def __del__(self):
711 self.cleanup_connection_file()
724 self.cleanup_connection_file()
712
725
713 #--------------------------------------------------------------------------
726 #--------------------------------------------------------------------------
714 # Channel management methods:
727 # Channel management methods:
715 #--------------------------------------------------------------------------
728 #--------------------------------------------------------------------------
716
729
717 def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
730 def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
718 """Starts the channels for this kernel.
731 """Starts the channels for this kernel.
719
732
720 This will create the channels if they do not exist and then start
733 This will create the channels if they do not exist and then start
721 them (their activity runs in a thread). If port numbers of 0 are
734 them (their activity runs in a thread). If port numbers of 0 are
722 being used (random ports) then you must first call
735 being used (random ports) then you must first call
723 :method:`start_kernel`. If the channels have been stopped and you
736 :method:`start_kernel`. If the channels have been stopped and you
724 call this, :class:`RuntimeError` will be raised.
737 call this, :class:`RuntimeError` will be raised.
725 """
738 """
726 if shell:
739 if shell:
727 self.shell_channel.start()
740 self.shell_channel.start()
728 if iopub:
741 if iopub:
729 self.iopub_channel.start()
742 self.iopub_channel.start()
730 if stdin:
743 if stdin:
731 self.stdin_channel.start()
744 self.stdin_channel.start()
732 self.shell_channel.allow_stdin = True
745 self.shell_channel.allow_stdin = True
733 else:
746 else:
734 self.shell_channel.allow_stdin = False
747 self.shell_channel.allow_stdin = False
735 if hb:
748 if hb:
736 self.hb_channel.start()
749 self.hb_channel.start()
737
750
738 def stop_channels(self):
751 def stop_channels(self):
739 """Stops all the running channels for this kernel.
752 """Stops all the running channels for this kernel.
740
753
741 This stops their event loops and joins their threads.
754 This stops their event loops and joins their threads.
742 """
755 """
743 if self.shell_channel.is_alive():
756 if self.shell_channel.is_alive():
744 self.shell_channel.stop()
757 self.shell_channel.stop()
745 if self.iopub_channel.is_alive():
758 if self.iopub_channel.is_alive():
746 self.iopub_channel.stop()
759 self.iopub_channel.stop()
747 if self.stdin_channel.is_alive():
760 if self.stdin_channel.is_alive():
748 self.stdin_channel.stop()
761 self.stdin_channel.stop()
749 if self.hb_channel.is_alive():
762 if self.hb_channel.is_alive():
750 self.hb_channel.stop()
763 self.hb_channel.stop()
751
764
752 @property
765 @property
753 def channels_running(self):
766 def channels_running(self):
754 """Are any of the channels created and running?"""
767 """Are any of the channels created and running?"""
755 return (self.shell_channel.is_alive() or self.iopub_channel.is_alive() or
768 return (self.shell_channel.is_alive() or self.iopub_channel.is_alive() or
756 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
769 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
757
770
758 def _make_url(self, port):
771 def _make_url(self, port):
759 """Make a zmq url with a port.
772 """Make a zmq url with a port.
760
773
761 There are two cases that this handles:
774 There are two cases that this handles:
762
775
763 * tcp: tcp://ip:port
776 * tcp: tcp://ip:port
764 * ipc: ipc://ip-port
777 * ipc: ipc://ip-port
765 """
778 """
766 if self.transport == 'tcp':
779 if self.transport == 'tcp':
767 return "tcp://%s:%i" % (self.ip, port)
780 return "tcp://%s:%i" % (self.ip, port)
768 else:
781 else:
769 return "%s://%s-%s" % (self.transport, self.ip, port)
782 return "%s://%s-%s" % (self.transport, self.ip, port)
770
783
771 @property
784 @property
772 def shell_channel(self):
785 def shell_channel(self):
773 """Get the shell channel object for this kernel."""
786 """Get the shell channel object for this kernel."""
774 if self._shell_channel is None:
787 if self._shell_channel is None:
775 self._shell_channel = self.shell_channel_class(
788 self._shell_channel = self.shell_channel_class(
776 self.context, self.session, self._make_url(self.shell_port)
789 self.context, self.session, self._make_url(self.shell_port)
777 )
790 )
778 return self._shell_channel
791 return self._shell_channel
779
792
780 @property
793 @property
781 def iopub_channel(self):
794 def iopub_channel(self):
782 """Get the iopub channel object for this kernel."""
795 """Get the iopub channel object for this kernel."""
783 if self._iopub_channel is None:
796 if self._iopub_channel is None:
784 self._iopub_channel = self.iopub_channel_class(
797 self._iopub_channel = self.iopub_channel_class(
785 self.context, self.session, self._make_url(self.iopub_port)
798 self.context, self.session, self._make_url(self.iopub_port)
786 )
799 )
787 return self._iopub_channel
800 return self._iopub_channel
788
801
789 @property
802 @property
790 def stdin_channel(self):
803 def stdin_channel(self):
791 """Get the stdin channel object for this kernel."""
804 """Get the stdin channel object for this kernel."""
792 if self._stdin_channel is None:
805 if self._stdin_channel is None:
793 self._stdin_channel = self.stdin_channel_class(
806 self._stdin_channel = self.stdin_channel_class(
794 self.context, self.session, self._make_url(self.stdin_port)
807 self.context, self.session, self._make_url(self.stdin_port)
795 )
808 )
796 return self._stdin_channel
809 return self._stdin_channel
797
810
798 @property
811 @property
799 def hb_channel(self):
812 def hb_channel(self):
800 """Get the hb channel object for this kernel."""
813 """Get the hb channel object for this kernel."""
801 if self._hb_channel is None:
814 if self._hb_channel is None:
802 self._hb_channel = self.hb_channel_class(
815 self._hb_channel = self.hb_channel_class(
803 self.context, self.session, self._make_url(self.hb_port)
816 self.context, self.session, self._make_url(self.hb_port)
804 )
817 )
805 return self._hb_channel
818 return self._hb_channel
806
819
807 #--------------------------------------------------------------------------
820 #--------------------------------------------------------------------------
808 # Connection and ipc file management
821 # Connection and ipc file management
809 #--------------------------------------------------------------------------
822 #--------------------------------------------------------------------------
810
823
811 def cleanup_connection_file(self):
824 def cleanup_connection_file(self):
812 """Cleanup connection file *if we wrote it*
825 """Cleanup connection file *if we wrote it*
813
826
814 Will not raise if the connection file was already removed somehow.
827 Will not raise if the connection file was already removed somehow.
815 """
828 """
816 if self._connection_file_written:
829 if self._connection_file_written:
817 # cleanup connection files on full shutdown of kernel we started
830 # cleanup connection files on full shutdown of kernel we started
818 self._connection_file_written = False
831 self._connection_file_written = False
819 try:
832 try:
820 os.remove(self.connection_file)
833 os.remove(self.connection_file)
821 except (IOError, OSError):
834 except (IOError, OSError):
822 pass
835 pass
823
836
824 def cleanup_ipc_files(self):
837 def cleanup_ipc_files(self):
825 """Cleanup ipc files if we wrote them."""
838 """Cleanup ipc files if we wrote them."""
826 if self.transport != 'ipc':
839 if self.transport != 'ipc':
827 return
840 return
828 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port):
841 for port in (self.shell_port, self.iopub_port, self.stdin_port, self.hb_port):
829 ipcfile = "%s-%i" % (self.ip, port)
842 ipcfile = "%s-%i" % (self.ip, port)
830 try:
843 try:
831 os.remove(ipcfile)
844 os.remove(ipcfile)
832 except (IOError, OSError):
845 except (IOError, OSError):
833 pass
846 pass
834
847
835 def load_connection_file(self):
848 def load_connection_file(self):
836 """Load connection info from JSON dict in self.connection_file."""
849 """Load connection info from JSON dict in self.connection_file."""
837 with open(self.connection_file) as f:
850 with open(self.connection_file) as f:
838 cfg = json.loads(f.read())
851 cfg = json.loads(f.read())
839
852
840 from pprint import pprint
853 from pprint import pprint
841 pprint(cfg)
854 pprint(cfg)
842 self.transport = cfg.get('transport', 'tcp')
855 self.transport = cfg.get('transport', 'tcp')
843 self.ip = cfg['ip']
856 self.ip = cfg['ip']
844 self.shell_port = cfg['shell_port']
857 self.shell_port = cfg['shell_port']
845 self.stdin_port = cfg['stdin_port']
858 self.stdin_port = cfg['stdin_port']
846 self.iopub_port = cfg['iopub_port']
859 self.iopub_port = cfg['iopub_port']
847 self.hb_port = cfg['hb_port']
860 self.hb_port = cfg['hb_port']
848 self.session.key = str_to_bytes(cfg['key'])
861 self.session.key = str_to_bytes(cfg['key'])
849
862
850 def write_connection_file(self):
863 def write_connection_file(self):
851 """Write connection info to JSON dict in self.connection_file."""
864 """Write connection info to JSON dict in self.connection_file."""
852 if self._connection_file_written:
865 if self._connection_file_written:
853 return
866 return
854 self.connection_file,cfg = write_connection_file(self.connection_file,
867 self.connection_file,cfg = write_connection_file(self.connection_file,
855 transport=self.transport, ip=self.ip, key=self.session.key,
868 transport=self.transport, ip=self.ip, key=self.session.key,
856 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
869 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
857 shell_port=self.shell_port, hb_port=self.hb_port)
870 shell_port=self.shell_port, hb_port=self.hb_port)
858 # write_connection_file also sets default ports:
871 # write_connection_file also sets default ports:
859 self.shell_port = cfg['shell_port']
872 self.shell_port = cfg['shell_port']
860 self.stdin_port = cfg['stdin_port']
873 self.stdin_port = cfg['stdin_port']
861 self.iopub_port = cfg['iopub_port']
874 self.iopub_port = cfg['iopub_port']
862 self.hb_port = cfg['hb_port']
875 self.hb_port = cfg['hb_port']
863
876
864 self._connection_file_written = True
877 self._connection_file_written = True
865
878
866 #--------------------------------------------------------------------------
879 #--------------------------------------------------------------------------
867 # Kernel management
880 # Kernel management
868 #--------------------------------------------------------------------------
881 #--------------------------------------------------------------------------
869
882
870 def start_kernel(self, **kw):
883 def start_kernel(self, **kw):
871 """Starts a kernel on this host in a separate process.
884 """Starts a kernel on this host in a separate process.
872
885
873 If random ports (port=0) are being used, this method must be called
886 If random ports (port=0) are being used, this method must be called
874 before the channels are created.
887 before the channels are created.
875
888
876 Parameters:
889 Parameters:
877 -----------
890 -----------
878 launcher : callable, optional (default None)
891 launcher : callable, optional (default None)
879 A custom function for launching the kernel process (generally a
892 A custom function for launching the kernel process (generally a
880 wrapper around ``entry_point.base_launch_kernel``). In most cases,
893 wrapper around ``entry_point.base_launch_kernel``). In most cases,
881 it should not be necessary to use this parameter.
894 it should not be necessary to use this parameter.
882
895
883 **kw : optional
896 **kw : optional
884 keyword arguments that are passed down into the launcher
897 keyword arguments that are passed down into the launcher
885 callable.
898 callable.
886 """
899 """
887 if self.transport == 'tcp' and self.ip not in LOCAL_IPS:
900 if self.transport == 'tcp' and self.ip not in LOCAL_IPS:
888 raise RuntimeError("Can only launch a kernel on a local interface. "
901 raise RuntimeError("Can only launch a kernel on a local interface. "
889 "Make sure that the '*_address' attributes are "
902 "Make sure that the '*_address' attributes are "
890 "configured properly. "
903 "configured properly. "
891 "Currently valid addresses are: %s"%LOCAL_IPS
904 "Currently valid addresses are: %s"%LOCAL_IPS
892 )
905 )
893
906
894 # write connection file / get default ports
907 # write connection file / get default ports
895 self.write_connection_file()
908 self.write_connection_file()
896
909
897 self._launch_args = kw.copy()
910 self._launch_args = kw.copy()
898 launch_kernel = kw.pop('launcher', None)
911 launch_kernel = kw.pop('launcher', None)
899 if launch_kernel is None:
912 if launch_kernel is None:
900 from ipkernel import launch_kernel
913 from ipkernel import launch_kernel
901 self.kernel = launch_kernel(fname=self.connection_file, **kw)
914 self.kernel = launch_kernel(fname=self.connection_file, **kw)
902
915
903 def shutdown_kernel(self, now=False, restart=False):
916 def shutdown_kernel(self, now=False, restart=False):
904 """Attempts to the stop the kernel process cleanly.
917 """Attempts to the stop the kernel process cleanly.
905
918
906 This attempts to shutdown the kernels cleanly by:
919 This attempts to shutdown the kernels cleanly by:
907
920
908 1. Sending it a shutdown message over the shell channel.
921 1. Sending it a shutdown message over the shell channel.
909 2. If that fails, the kernel is shutdown forcibly by sending it
922 2. If that fails, the kernel is shutdown forcibly by sending it
910 a signal.
923 a signal.
911
924
912 Parameters:
925 Parameters:
913 -----------
926 -----------
914 now : bool
927 now : bool
915 Should the kernel be forcible killed *now*. This skips the
928 Should the kernel be forcible killed *now*. This skips the
916 first, nice shutdown attempt.
929 first, nice shutdown attempt.
917 restart: bool
930 restart: bool
918 Will this kernel be restarted after it is shutdown. When this
931 Will this kernel be restarted after it is shutdown. When this
919 is True, connection files will not be cleaned up.
932 is True, connection files will not be cleaned up.
920 """
933 """
921 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
934 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
922 if sys.platform == 'win32':
935 if sys.platform == 'win32':
923 self._kill_kernel()
936 self._kill_kernel()
924 return
937 return
925
938
926 # Pause the heart beat channel if it exists.
939 # Pause the heart beat channel if it exists.
927 if self._hb_channel is not None:
940 if self._hb_channel is not None:
928 self._hb_channel.pause()
941 self._hb_channel.pause()
929
942
930 if now:
943 if now:
931 if self.has_kernel:
944 if self.has_kernel:
932 self._kill_kernel()
945 self._kill_kernel()
933 else:
946 else:
934 # Don't send any additional kernel kill messages immediately, to give
947 # Don't send any additional kernel kill messages immediately, to give
935 # the kernel a chance to properly execute shutdown actions. Wait for at
948 # the kernel a chance to properly execute shutdown actions. Wait for at
936 # most 1s, checking every 0.1s.
949 # most 1s, checking every 0.1s.
937 self.shell_channel.shutdown(restart=restart)
950 self.shell_channel.shutdown(restart=restart)
938 for i in range(10):
951 for i in range(10):
939 if self.is_alive:
952 if self.is_alive:
940 time.sleep(0.1)
953 time.sleep(0.1)
941 else:
954 else:
942 break
955 break
943 else:
956 else:
944 # OK, we've waited long enough.
957 # OK, we've waited long enough.
945 if self.has_kernel:
958 if self.has_kernel:
946 self._kill_kernel()
959 self._kill_kernel()
947
960
948 if not restart:
961 if not restart:
949 self.cleanup_connection_file()
962 self.cleanup_connection_file()
950 self.cleanup_ipc_files()
963 self.cleanup_ipc_files()
951 else:
964 else:
952 self.cleanup_ipc_files()
965 self.cleanup_ipc_files()
953
966
954 def restart_kernel(self, now=False, **kw):
967 def restart_kernel(self, now=False, **kw):
955 """Restarts a kernel with the arguments that were used to launch it.
968 """Restarts a kernel with the arguments that were used to launch it.
956
969
957 If the old kernel was launched with random ports, the same ports will be
970 If the old kernel was launched with random ports, the same ports will be
958 used for the new kernel. The same connection file is used again.
971 used for the new kernel. The same connection file is used again.
959
972
960 Parameters
973 Parameters
961 ----------
974 ----------
962 now : bool, optional
975 now : bool, optional
963 If True, the kernel is forcefully restarted *immediately*, without
976 If True, the kernel is forcefully restarted *immediately*, without
964 having a chance to do any cleanup action. Otherwise the kernel is
977 having a chance to do any cleanup action. Otherwise the kernel is
965 given 1s to clean up before a forceful restart is issued.
978 given 1s to clean up before a forceful restart is issued.
966
979
967 In all cases the kernel is restarted, the only difference is whether
980 In all cases the kernel is restarted, the only difference is whether
968 it is given a chance to perform a clean shutdown or not.
981 it is given a chance to perform a clean shutdown or not.
969
982
970 **kw : optional
983 **kw : optional
971 Any options specified here will overwrite those used to launch the
984 Any options specified here will overwrite those used to launch the
972 kernel.
985 kernel.
973 """
986 """
974 if self._launch_args is None:
987 if self._launch_args is None:
975 raise RuntimeError("Cannot restart the kernel. "
988 raise RuntimeError("Cannot restart the kernel. "
976 "No previous call to 'start_kernel'.")
989 "No previous call to 'start_kernel'.")
977 else:
990 else:
978 # Stop currently running kernel.
991 # Stop currently running kernel.
979 self.shutdown_kernel(now=now, restart=True)
992 self.shutdown_kernel(now=now, restart=True)
980
993
981 # Start new kernel.
994 # Start new kernel.
982 self._launch_args.update(kw)
995 self._launch_args.update(kw)
983 self.start_kernel(**self._launch_args)
996 self.start_kernel(**self._launch_args)
984
997
985 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
998 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
986 # unless there is some delay here.
999 # unless there is some delay here.
987 if sys.platform == 'win32':
1000 if sys.platform == 'win32':
988 time.sleep(0.2)
1001 time.sleep(0.2)
989
1002
990 @property
1003 @property
991 def has_kernel(self):
1004 def has_kernel(self):
992 """Has a kernel been started that we are managing."""
1005 """Has a kernel been started that we are managing."""
993 return self.kernel is not None
1006 return self.kernel is not None
994
1007
995 def _kill_kernel(self):
1008 def _kill_kernel(self):
996 """Kill the running kernel.
1009 """Kill the running kernel.
997
1010
998 This is a private method, callers should use shutdown_kernel(now=True).
1011 This is a private method, callers should use shutdown_kernel(now=True).
999 """
1012 """
1000 if self.has_kernel:
1013 if self.has_kernel:
1001 # Pause the heart beat channel if it exists.
1014 # Pause the heart beat channel if it exists.
1002 if self._hb_channel is not None:
1015 if self._hb_channel is not None:
1003 self._hb_channel.pause()
1016 self._hb_channel.pause()
1004
1017
1005 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
1018 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
1006 # TerminateProcess() on Win32).
1019 # TerminateProcess() on Win32).
1007 try:
1020 try:
1008 self.kernel.kill()
1021 self.kernel.kill()
1009 except OSError as e:
1022 except OSError as e:
1010 # In Windows, we will get an Access Denied error if the process
1023 # In Windows, we will get an Access Denied error if the process
1011 # has already terminated. Ignore it.
1024 # has already terminated. Ignore it.
1012 if sys.platform == 'win32':
1025 if sys.platform == 'win32':
1013 if e.winerror != 5:
1026 if e.winerror != 5:
1014 raise
1027 raise
1015 # On Unix, we may get an ESRCH error if the process has already
1028 # On Unix, we may get an ESRCH error if the process has already
1016 # terminated. Ignore it.
1029 # terminated. Ignore it.
1017 else:
1030 else:
1018 from errno import ESRCH
1031 from errno import ESRCH
1019 if e.errno != ESRCH:
1032 if e.errno != ESRCH:
1020 raise
1033 raise
1021
1034
1022 # Block until the kernel terminates.
1035 # Block until the kernel terminates.
1023 self.kernel.wait()
1036 self.kernel.wait()
1024 self.kernel = None
1037 self.kernel = None
1025 else:
1038 else:
1026 raise RuntimeError("Cannot kill kernel. No kernel is running!")
1039 raise RuntimeError("Cannot kill kernel. No kernel is running!")
1027
1040
1028 def interrupt_kernel(self):
1041 def interrupt_kernel(self):
1029 """Interrupts the kernel by sending it a signal.
1042 """Interrupts the kernel by sending it a signal.
1030
1043
1031 Unlike ``signal_kernel``, this operation is well supported on all
1044 Unlike ``signal_kernel``, this operation is well supported on all
1032 platforms.
1045 platforms.
1033 """
1046 """
1034 if self.has_kernel:
1047 if self.has_kernel:
1035 if sys.platform == 'win32':
1048 if sys.platform == 'win32':
1036 from parentpoller import ParentPollerWindows as Poller
1049 from parentpoller import ParentPollerWindows as Poller
1037 Poller.send_interrupt(self.kernel.win32_interrupt_event)
1050 Poller.send_interrupt(self.kernel.win32_interrupt_event)
1038 else:
1051 else:
1039 self.kernel.send_signal(signal.SIGINT)
1052 self.kernel.send_signal(signal.SIGINT)
1040 else:
1053 else:
1041 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
1054 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
1042
1055
1043 def signal_kernel(self, signum):
1056 def signal_kernel(self, signum):
1044 """Sends a signal to the kernel.
1057 """Sends a signal to the kernel.
1045
1058
1046 Note that since only SIGTERM is supported on Windows, this function is
1059 Note that since only SIGTERM is supported on Windows, this function is
1047 only useful on Unix systems.
1060 only useful on Unix systems.
1048 """
1061 """
1049 if self.has_kernel:
1062 if self.has_kernel:
1050 self.kernel.send_signal(signum)
1063 self.kernel.send_signal(signum)
1051 else:
1064 else:
1052 raise RuntimeError("Cannot signal kernel. No kernel is running!")
1065 raise RuntimeError("Cannot signal kernel. No kernel is running!")
1053
1066
1054 @property
1067 @property
1055 def is_alive(self):
1068 def is_alive(self):
1056 """Is the kernel process still running?"""
1069 """Is the kernel process still running?"""
1057 if self.has_kernel:
1070 if self.has_kernel:
1058 if self.kernel.poll() is None:
1071 if self.kernel.poll() is None:
1059 return True
1072 return True
1060 else:
1073 else:
1061 return False
1074 return False
1062 elif self._hb_channel is not None:
1075 elif self._hb_channel is not None:
1063 # We didn't start the kernel with this KernelManager so we
1076 # We didn't start the kernel with this KernelManager so we
1064 # use the heartbeat.
1077 # use the heartbeat.
1065 return self._hb_channel.is_beating()
1078 return self._hb_channel.is_beating()
1066 else:
1079 else:
1067 # no heartbeat and not local, we can't tell if it's running,
1080 # no heartbeat and not local, we can't tell if it's running,
1068 # so naively return True
1081 # so naively return True
1069 return True
1082 return True
1070
1083
1071
1084
1072 #-----------------------------------------------------------------------------
1085 #-----------------------------------------------------------------------------
1073 # ABC Registration
1086 # ABC Registration
1074 #-----------------------------------------------------------------------------
1087 #-----------------------------------------------------------------------------
1075
1088
1076 ShellChannelABC.register(ShellChannel)
1089 ShellChannelABC.register(ShellChannel)
1077 IOPubChannelABC.register(IOPubChannel)
1090 IOPubChannelABC.register(IOPubChannel)
1078 HBChannelABC.register(HBChannel)
1091 HBChannelABC.register(HBChannel)
1079 StdInChannelABC.register(StdInChannel)
1092 StdInChannelABC.register(StdInChannel)
1080 KernelManagerABC.register(KernelManager)
1093 KernelManagerABC.register(KernelManager)
1081
1094
General Comments 0
You need to be logged in to leave comments. Login now