##// END OF EJS Templates
improve cleanup of connection files...
MinRK -
Show More
@@ -1,364 +1,383 b''
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 json
25 import json
25 import os
26 import os
26 import signal
27 import signal
27 import sys
28 import sys
28 import uuid
29 import uuid
29
30
30
31
31 # Local imports
32 # Local imports
32 from IPython.config.application import boolean_flag
33 from IPython.config.application import boolean_flag
33 from IPython.config.configurable import Configurable
34 from IPython.config.configurable import Configurable
34 from IPython.core.profiledir import ProfileDir
35 from IPython.core.profiledir import ProfileDir
35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
37 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
37 from IPython.utils.path import filefind
38 from IPython.utils.path import filefind
38 from IPython.utils.py3compat import str_to_bytes
39 from IPython.utils.py3compat import str_to_bytes
39 from IPython.utils.traitlets import (
40 from IPython.utils.traitlets import (
40 Dict, List, Unicode, CUnicode, Int, CBool, Any
41 Dict, List, Unicode, CUnicode, Int, CBool, Any
41 )
42 )
42 from IPython.zmq.ipkernel import (
43 from IPython.zmq.ipkernel import (
43 flags as ipkernel_flags,
44 flags as ipkernel_flags,
44 aliases as ipkernel_aliases,
45 aliases as ipkernel_aliases,
45 IPKernelApp
46 IPKernelApp
46 )
47 )
47 from IPython.zmq.session import Session, default_secure
48 from IPython.zmq.session import Session, default_secure
48 from IPython.zmq.zmqshell import ZMQInteractiveShell
49 from IPython.zmq.zmqshell import ZMQInteractiveShell
49
50
50 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
51 # Network Constants
52 # Network Constants
52 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
53
54
54 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
55 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
55
56
56 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
57 # Globals
58 # Globals
58 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
59
60
60
61
61 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
62 # Aliases and Flags
63 # Aliases and Flags
63 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
64
65
65 flags = dict(ipkernel_flags)
66 flags = dict(ipkernel_flags)
66
67
67 # the flags that are specific to the frontend
68 # the flags that are specific to the frontend
68 # these must be scrubbed before being passed to the kernel,
69 # these must be scrubbed before being passed to the kernel,
69 # or it will raise an error on unrecognized flags
70 # or it will raise an error on unrecognized flags
70 app_flags = {
71 app_flags = {
71 'existing' : ({'IPythonMixinConsoleApp' : {'existing' : 'kernel*.json'}},
72 'existing' : ({'IPythonMixinConsoleApp' : {'existing' : 'kernel*.json'}},
72 "Connect to an existing kernel. If no argument specified, guess most recent"),
73 "Connect to an existing kernel. If no argument specified, guess most recent"),
73 }
74 }
74 app_flags.update(boolean_flag(
75 app_flags.update(boolean_flag(
75 'confirm-exit', 'IPythonMixinConsoleApp.confirm_exit',
76 'confirm-exit', 'IPythonMixinConsoleApp.confirm_exit',
76 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
77 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
77 to force a direct exit without any confirmation.
78 to force a direct exit without any confirmation.
78 """,
79 """,
79 """Don't prompt the user when exiting. This will terminate the kernel
80 """Don't prompt the user when exiting. This will terminate the kernel
80 if it is owned by the frontend, and leave it alive if it is external.
81 if it is owned by the frontend, and leave it alive if it is external.
81 """
82 """
82 ))
83 ))
83 flags.update(app_flags)
84 flags.update(app_flags)
84
85
85 aliases = dict(ipkernel_aliases)
86 aliases = dict(ipkernel_aliases)
86
87
87 # also scrub aliases from the frontend
88 # also scrub aliases from the frontend
88 app_aliases = dict(
89 app_aliases = dict(
89 hb = 'IPythonMixinConsoleApp.hb_port',
90 hb = 'IPythonMixinConsoleApp.hb_port',
90 shell = 'IPythonMixinConsoleApp.shell_port',
91 shell = 'IPythonMixinConsoleApp.shell_port',
91 iopub = 'IPythonMixinConsoleApp.iopub_port',
92 iopub = 'IPythonMixinConsoleApp.iopub_port',
92 stdin = 'IPythonMixinConsoleApp.stdin_port',
93 stdin = 'IPythonMixinConsoleApp.stdin_port',
93 ip = 'IPythonMixinConsoleApp.ip',
94 ip = 'IPythonMixinConsoleApp.ip',
94 existing = 'IPythonMixinConsoleApp.existing',
95 existing = 'IPythonMixinConsoleApp.existing',
95 f = 'IPythonMixinConsoleApp.connection_file',
96 f = 'IPythonMixinConsoleApp.connection_file',
96
97
97
98
98 ssh = 'IPythonMixinConsoleApp.sshserver',
99 ssh = 'IPythonMixinConsoleApp.sshserver',
99 )
100 )
100 aliases.update(app_aliases)
101 aliases.update(app_aliases)
101
102
102 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
103 # Classes
104 # Classes
104 #-----------------------------------------------------------------------------
105 #-----------------------------------------------------------------------------
105
106
106 #-----------------------------------------------------------------------------
107 #-----------------------------------------------------------------------------
107 # IPythonMixinConsole
108 # IPythonMixinConsole
108 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
109
110
110
111
111 class IPythonMixinConsoleApp(Configurable):
112 class IPythonMixinConsoleApp(Configurable):
112 name = 'ipython-console-mixin'
113 name = 'ipython-console-mixin'
113 default_config_file_name='ipython_config.py'
114 default_config_file_name='ipython_config.py'
114
115
115 description = """
116 description = """
116 The IPython Mixin Console.
117 The IPython Mixin Console.
117
118
118 This class contains the common portions of console client (QtConsole,
119 This class contains the common portions of console client (QtConsole,
119 ZMQ-based terminal console, etc). It is not a full console, in that
120 ZMQ-based terminal console, etc). It is not a full console, in that
120 launched terminal subprocesses will not be able to accept input.
121 launched terminal subprocesses will not be able to accept input.
121
122
122 The Console using this mixing supports various extra features beyond
123 The Console using this mixing supports various extra features beyond
123 the single-process Terminal IPython shell, such as connecting to
124 the single-process Terminal IPython shell, such as connecting to
124 existing kernel, via:
125 existing kernel, via:
125
126
126 ipython <appname> --existing
127 ipython <appname> --existing
127
128
128 as well as tunnel via SSH
129 as well as tunnel via SSH
129
130
130 """
131 """
131
132
132 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
133 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
133 flags = Dict(flags)
134 flags = Dict(flags)
134 aliases = Dict(aliases)
135 aliases = Dict(aliases)
135 kernel_manager_class = BlockingKernelManager
136 kernel_manager_class = BlockingKernelManager
136
137
137 kernel_argv = List(Unicode)
138 kernel_argv = List(Unicode)
138
139
139 pure = CBool(False, config=True,
140 pure = CBool(False, config=True,
140 help="Use a pure Python kernel instead of an IPython kernel.")
141 help="Use a pure Python kernel instead of an IPython kernel.")
141 # create requested profiles by default, if they don't exist:
142 # create requested profiles by default, if they don't exist:
142 auto_create = CBool(True)
143 auto_create = CBool(True)
143 # connection info:
144 # connection info:
144 ip = Unicode(LOCALHOST, config=True,
145 ip = Unicode(LOCALHOST, config=True,
145 help="""Set the kernel\'s IP address [default localhost].
146 help="""Set the kernel\'s IP address [default localhost].
146 If the IP address is something other than localhost, then
147 If the IP address is something other than localhost, then
147 Consoles on other machines will be able to connect
148 Consoles on other machines will be able to connect
148 to the Kernel, so be careful!"""
149 to the Kernel, so be careful!"""
149 )
150 )
150
151
151 sshserver = Unicode('', config=True,
152 sshserver = Unicode('', config=True,
152 help="""The SSH server to use to connect to the kernel.""")
153 help="""The SSH server to use to connect to the kernel.""")
153 sshkey = Unicode('', config=True,
154 sshkey = Unicode('', config=True,
154 help="""Path to the ssh key to use for logging in to the ssh server.""")
155 help="""Path to the ssh key to use for logging in to the ssh server.""")
155
156
156 hb_port = Int(0, config=True,
157 hb_port = Int(0, config=True,
157 help="set the heartbeat port [default: random]")
158 help="set the heartbeat port [default: random]")
158 shell_port = Int(0, config=True,
159 shell_port = Int(0, config=True,
159 help="set the shell (XREP) port [default: random]")
160 help="set the shell (XREP) port [default: random]")
160 iopub_port = Int(0, config=True,
161 iopub_port = Int(0, config=True,
161 help="set the iopub (PUB) port [default: random]")
162 help="set the iopub (PUB) port [default: random]")
162 stdin_port = Int(0, config=True,
163 stdin_port = Int(0, config=True,
163 help="set the stdin (XREQ) port [default: random]")
164 help="set the stdin (XREQ) port [default: random]")
164 connection_file = Unicode('', config=True,
165 connection_file = Unicode('', config=True,
165 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
166 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
166
167
167 This file will contain the IP, ports, and authentication key needed to connect
168 This file will contain the IP, ports, and authentication key needed to connect
168 clients to this kernel. By default, this file will be created in the security-dir
169 clients to this kernel. By default, this file will be created in the security-dir
169 of the current profile, but can be specified by absolute path.
170 of the current profile, but can be specified by absolute path.
170 """)
171 """)
171 def _connection_file_default(self):
172 def _connection_file_default(self):
172 return 'kernel-%i.json' % os.getpid()
173 return 'kernel-%i.json' % os.getpid()
173
174
174 existing = CUnicode('', config=True,
175 existing = CUnicode('', config=True,
175 help="""Connect to an already running kernel""")
176 help="""Connect to an already running kernel""")
176
177
177 confirm_exit = CBool(True, config=True,
178 confirm_exit = CBool(True, config=True,
178 help="""
179 help="""
179 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
180 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
180 to force a direct exit without any confirmation.""",
181 to force a direct exit without any confirmation.""",
181 )
182 )
182
183
183
184
184 def parse_command_line(self, argv=None):
185 def parse_command_line(self, argv=None):
185 #super(PythonBaseConsoleApp, self).parse_command_line(argv)
186 #super(PythonBaseConsoleApp, self).parse_command_line(argv)
186 # make this stuff after this a function, in case the super stuff goes
187 # make this stuff after this a function, in case the super stuff goes
187 # away. Also, Min notes that this functionality should be moved to a
188 # away. Also, Min notes that this functionality should be moved to a
188 # generic library of kernel stuff
189 # generic library of kernel stuff
189 self.swallow_args(app_aliases,app_flags,argv=argv)
190 self.swallow_args(app_aliases,app_flags,argv=argv)
190
191
191 def swallow_args(self, aliases,flags, argv=None):
192 def swallow_args(self, aliases,flags, argv=None):
192 if argv is None:
193 if argv is None:
193 argv = sys.argv[1:]
194 argv = sys.argv[1:]
194 self.kernel_argv = list(argv) # copy
195 self.kernel_argv = list(argv) # copy
195 # kernel should inherit default config file from frontend
196 # kernel should inherit default config file from frontend
196 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
197 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
197 # Scrub frontend-specific flags
198 # Scrub frontend-specific flags
198 swallow_next = False
199 swallow_next = False
199 was_flag = False
200 was_flag = False
200 # copy again, in case some aliases have the same name as a flag
201 # copy again, in case some aliases have the same name as a flag
201 # argv = list(self.kernel_argv)
202 # argv = list(self.kernel_argv)
202 for a in argv:
203 for a in argv:
203 if swallow_next:
204 if swallow_next:
204 swallow_next = False
205 swallow_next = False
205 # last arg was an alias, remove the next one
206 # last arg was an alias, remove the next one
206 # *unless* the last alias has a no-arg flag version, in which
207 # *unless* the last alias has a no-arg flag version, in which
207 # case, don't swallow the next arg if it's also a flag:
208 # case, don't swallow the next arg if it's also a flag:
208 if not (was_flag and a.startswith('-')):
209 if not (was_flag and a.startswith('-')):
209 self.kernel_argv.remove(a)
210 self.kernel_argv.remove(a)
210 continue
211 continue
211 if a.startswith('-'):
212 if a.startswith('-'):
212 split = a.lstrip('-').split('=')
213 split = a.lstrip('-').split('=')
213 alias = split[0]
214 alias = split[0]
214 if alias in aliases:
215 if alias in aliases:
215 self.kernel_argv.remove(a)
216 self.kernel_argv.remove(a)
216 if len(split) == 1:
217 if len(split) == 1:
217 # alias passed with arg via space
218 # alias passed with arg via space
218 swallow_next = True
219 swallow_next = True
219 # could have been a flag that matches an alias, e.g. `existing`
220 # could have been a flag that matches an alias, e.g. `existing`
220 # in which case, we might not swallow the next arg
221 # in which case, we might not swallow the next arg
221 was_flag = alias in flags
222 was_flag = alias in flags
222 elif alias in flags:
223 elif alias in flags:
223 # strip flag, but don't swallow next, as flags don't take args
224 # strip flag, but don't swallow next, as flags don't take args
224 self.kernel_argv.remove(a)
225 self.kernel_argv.remove(a)
225
226
226 def init_connection_file(self):
227 def init_connection_file(self):
227 """find the connection file, and load the info if found.
228 """find the connection file, and load the info if found.
228
229
229 The current working directory and the current profile's security
230 The current working directory and the current profile's security
230 directory will be searched for the file if it is not given by
231 directory will be searched for the file if it is not given by
231 absolute path.
232 absolute path.
232
233
233 When attempting to connect to an existing kernel and the `--existing`
234 When attempting to connect to an existing kernel and the `--existing`
234 argument does not match an existing file, it will be interpreted as a
235 argument does not match an existing file, it will be interpreted as a
235 fileglob, and the matching file in the current profile's security dir
236 fileglob, and the matching file in the current profile's security dir
236 with the latest access time will be used.
237 with the latest access time will be used.
238
239 After this method is called, self.connection_file contains the *full path*
240 to the connection file, never just its name.
237 """
241 """
238 if self.existing:
242 if self.existing:
239 try:
243 try:
240 cf = find_connection_file(self.existing)
244 cf = find_connection_file(self.existing)
241 except Exception:
245 except Exception:
242 self.log.critical("Could not find existing kernel connection file %s", self.existing)
246 self.log.critical("Could not find existing kernel connection file %s", self.existing)
243 self.exit(1)
247 self.exit(1)
244 self.log.info("Connecting to existing kernel: %s" % cf)
248 self.log.info("Connecting to existing kernel: %s" % cf)
245 self.connection_file = cf
249 self.connection_file = cf
250 else:
251 # not existing, check if we are going to write the file
252 # and ensure that self.connection_file is a full path, not just the shortname
253 try:
254 cf = find_connection_file(self.connection_file)
255 except Exception:
256 # file might not exist
257 if self.connection_file == os.path.basename(self.connection_file):
258 # just shortname, put it in security dir
259 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
260 else:
261 cf = self.connection_file
262 self.connection_file = cf
263
246 # should load_connection_file only be used for existing?
264 # should load_connection_file only be used for existing?
247 # as it is now, this allows reusing ports if an existing
265 # as it is now, this allows reusing ports if an existing
248 # file is requested
266 # file is requested
249 try:
267 try:
250 self.load_connection_file()
268 self.load_connection_file()
251 except Exception:
269 except Exception:
252 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
270 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
253 self.exit(1)
271 self.exit(1)
254
272
255 def load_connection_file(self):
273 def load_connection_file(self):
256 """load ip/port/hmac config from JSON connection file"""
274 """load ip/port/hmac config from JSON connection file"""
257 # this is identical to KernelApp.load_connection_file
275 # this is identical to KernelApp.load_connection_file
258 # perhaps it can be centralized somewhere?
276 # perhaps it can be centralized somewhere?
259 try:
277 try:
260 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
278 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
261 except IOError:
279 except IOError:
262 self.log.debug("Connection File not found: %s", self.connection_file)
280 self.log.debug("Connection File not found: %s", self.connection_file)
263 return
281 return
264 self.log.debug(u"Loading connection file %s", fname)
282 self.log.debug(u"Loading connection file %s", fname)
265 with open(fname) as f:
283 with open(fname) as f:
266 s = f.read()
284 s = f.read()
267 cfg = json.loads(s)
285 cfg = json.loads(s)
268 if self.ip == LOCALHOST and 'ip' in cfg:
286 if self.ip == LOCALHOST and 'ip' in cfg:
269 # not overridden by config or cl_args
287 # not overridden by config or cl_args
270 self.ip = cfg['ip']
288 self.ip = cfg['ip']
271 for channel in ('hb', 'shell', 'iopub', 'stdin'):
289 for channel in ('hb', 'shell', 'iopub', 'stdin'):
272 name = channel + '_port'
290 name = channel + '_port'
273 if getattr(self, name) == 0 and name in cfg:
291 if getattr(self, name) == 0 and name in cfg:
274 # not overridden by config or cl_args
292 # not overridden by config or cl_args
275 setattr(self, name, cfg[name])
293 setattr(self, name, cfg[name])
276 if 'key' in cfg:
294 if 'key' in cfg:
277 self.config.Session.key = str_to_bytes(cfg['key'])
295 self.config.Session.key = str_to_bytes(cfg['key'])
278
296
279 def init_ssh(self):
297 def init_ssh(self):
280 """set up ssh tunnels, if needed."""
298 """set up ssh tunnels, if needed."""
281 if not self.sshserver and not self.sshkey:
299 if not self.sshserver and not self.sshkey:
282 return
300 return
283
301
284 if self.sshkey and not self.sshserver:
302 if self.sshkey and not self.sshserver:
285 # specifying just the key implies that we are connecting directly
303 # specifying just the key implies that we are connecting directly
286 self.sshserver = self.ip
304 self.sshserver = self.ip
287 self.ip = LOCALHOST
305 self.ip = LOCALHOST
288
306
289 # build connection dict for tunnels:
307 # build connection dict for tunnels:
290 info = dict(ip=self.ip,
308 info = dict(ip=self.ip,
291 shell_port=self.shell_port,
309 shell_port=self.shell_port,
292 iopub_port=self.iopub_port,
310 iopub_port=self.iopub_port,
293 stdin_port=self.stdin_port,
311 stdin_port=self.stdin_port,
294 hb_port=self.hb_port
312 hb_port=self.hb_port
295 )
313 )
296
314
297 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
315 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
298
316
299 # tunnels return a new set of ports, which will be on localhost:
317 # tunnels return a new set of ports, which will be on localhost:
300 self.ip = LOCALHOST
318 self.ip = LOCALHOST
301 try:
319 try:
302 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
320 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
303 except:
321 except:
304 # even catch KeyboardInterrupt
322 # even catch KeyboardInterrupt
305 self.log.error("Could not setup tunnels", exc_info=True)
323 self.log.error("Could not setup tunnels", exc_info=True)
306 self.exit(1)
324 self.exit(1)
307
325
308 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
326 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
309
327
310 cf = self.connection_file
328 cf = self.connection_file
311 base,ext = os.path.splitext(cf)
329 base,ext = os.path.splitext(cf)
312 base = os.path.basename(base)
330 base = os.path.basename(base)
313 self.connection_file = os.path.basename(base)+'-ssh'+ext
331 self.connection_file = os.path.basename(base)+'-ssh'+ext
314 self.log.critical("To connect another client via this tunnel, use:")
332 self.log.critical("To connect another client via this tunnel, use:")
315 self.log.critical("--existing %s" % self.connection_file)
333 self.log.critical("--existing %s" % self.connection_file)
316
334
317 def _new_connection_file(self):
335 def _new_connection_file(self):
318 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
336 cf = ''
337 while not cf:
338 # we don't need a 128b id to distinguish kernels, use more readable
339 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
340 # kernels can subclass.
341 ident = str(uuid.uuid4()).split('-')[-1]
342 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
343 # only keep if it's actually new. Protect against unlikely collision
344 # in 48b random search space
345 cf = cf if not os.path.exists(cf) else ''
346 return cf
319
347
320 def init_kernel_manager(self):
348 def init_kernel_manager(self):
321 # Don't let Qt or ZMQ swallow KeyboardInterupts.
349 # Don't let Qt or ZMQ swallow KeyboardInterupts.
322 signal.signal(signal.SIGINT, signal.SIG_DFL)
350 signal.signal(signal.SIGINT, signal.SIG_DFL)
323 sec = self.profile_dir.security_dir
324 try:
325 cf = filefind(self.connection_file, ['.', sec])
326 except IOError:
327 # file might not exist
328 if self.connection_file == os.path.basename(self.connection_file):
329 # just shortname, put it in security dir
330 cf = os.path.join(sec, self.connection_file)
331 else:
332 cf = self.connection_file
333
351
334 # Create a KernelManager and start a kernel.
352 # Create a KernelManager and start a kernel.
335 self.kernel_manager = self.kernel_manager_class(
353 self.kernel_manager = self.kernel_manager_class(
336 ip=self.ip,
354 ip=self.ip,
337 shell_port=self.shell_port,
355 shell_port=self.shell_port,
338 iopub_port=self.iopub_port,
356 iopub_port=self.iopub_port,
339 stdin_port=self.stdin_port,
357 stdin_port=self.stdin_port,
340 hb_port=self.hb_port,
358 hb_port=self.hb_port,
341 connection_file=cf,
359 connection_file=self.connection_file,
342 config=self.config,
360 config=self.config,
343 )
361 )
344 # start the kernel
362 # start the kernel
345 if not self.existing:
363 if not self.existing:
346 kwargs = dict(ipython=not self.pure)
364 kwargs = dict(ipython=not self.pure)
347 kwargs['extra_arguments'] = self.kernel_argv
365 kwargs['extra_arguments'] = self.kernel_argv
348 self.kernel_manager.start_kernel(**kwargs)
366 self.kernel_manager.start_kernel(**kwargs)
349 elif self.sshserver:
367 elif self.sshserver:
350 # ssh, write new connection file
368 # ssh, write new connection file
351 self.kernel_manager.write_connection_file()
369 self.kernel_manager.write_connection_file()
370 atexit.register(self.kernel_manager.cleanup_connection_file)
352 self.kernel_manager.start_channels()
371 self.kernel_manager.start_channels()
353
372
354
373
355 def initialize(self, argv=None):
374 def initialize(self, argv=None):
356 """
375 """
357 Classes which mix this class in should call:
376 Classes which mix this class in should call:
358 IPythonMixinConsoleApp.initialize(self,argv)
377 IPythonMixinConsoleApp.initialize(self,argv)
359 """
378 """
360 self.init_connection_file()
379 self.init_connection_file()
361 default_secure(self.config)
380 default_secure(self.config)
362 self.init_ssh()
381 self.init_ssh()
363 self.init_kernel_manager()
382 self.init_kernel_manager()
364
383
@@ -1,327 +1,341 b''
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 # System library imports
29 # System library imports
30 from IPython.external.qt import QtGui
30 from IPython.external.qt import QtCore, QtGui
31
31
32 # Local imports
32 # Local imports
33 from IPython.config.application import boolean_flag, catch_config_error
33 from IPython.config.application import boolean_flag, catch_config_error
34 from IPython.core.application import BaseIPythonApplication
34 from IPython.core.application import BaseIPythonApplication
35 from IPython.core.profiledir import ProfileDir
35 from IPython.core.profiledir import ProfileDir
36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
40 from IPython.frontend.qt.console import styles
40 from IPython.frontend.qt.console import styles
41 from IPython.frontend.qt.console.mainwindow import MainWindow
41 from IPython.frontend.qt.console.mainwindow import MainWindow
42 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 from IPython.frontend.qt.kernelmanager import QtKernelManager
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.py3compat import str_to_bytes
44 from IPython.utils.py3compat import str_to_bytes
45 from IPython.utils.traitlets import (
45 from IPython.utils.traitlets import (
46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
47 )
47 )
48 from IPython.zmq.ipkernel import IPKernelApp
48 from IPython.zmq.ipkernel import IPKernelApp
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 from IPython.frontend.kernelmixinapp import (
52 from IPython.frontend.kernelmixinapp import (
53 IPythonMixinConsoleApp, app_aliases, app_flags
53 IPythonMixinConsoleApp, app_aliases, app_flags
54 )
54 )
55
55
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57 # Network Constants
57 # Network Constants
58 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
59
59
60 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
61
61
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63 # Globals
63 # Globals
64 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
65
65
66 _examples = """
66 _examples = """
67 ipython qtconsole # start the qtconsole
67 ipython qtconsole # start the qtconsole
68 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
69 """
69 """
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # Aliases and Flags
72 # Aliases and Flags
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74
74
75 # XXX: the app_flags should really be flags from the mixin
75 # XXX: the app_flags should really be flags from the mixin
76 flags = dict(app_flags)
76 flags = dict(app_flags)
77 qt_flags = {
77 qt_flags = {
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 "Use a pure Python kernel instead of an IPython kernel."),
79 "Use a pure Python kernel instead of an IPython kernel."),
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 "Disable rich text support."),
81 "Disable rich text support."),
82 }
82 }
83 qt_flags.update(boolean_flag(
83 qt_flags.update(boolean_flag(
84 'gui-completion', 'ConsoleWidget.gui_completion',
84 'gui-completion', 'ConsoleWidget.gui_completion',
85 "use a GUI widget for tab completion",
85 "use a GUI widget for tab completion",
86 "use plaintext output for completion"
86 "use plaintext output for completion"
87 ))
87 ))
88 flags.update(qt_flags)
88 flags.update(qt_flags)
89
89
90 aliases = dict(app_aliases)
90 aliases = dict(app_aliases)
91
91
92 qt_aliases = dict(
92 qt_aliases = dict(
93
93
94 style = 'IPythonWidget.syntax_style',
94 style = 'IPythonWidget.syntax_style',
95 stylesheet = 'IPythonQtConsoleApp.stylesheet',
95 stylesheet = 'IPythonQtConsoleApp.stylesheet',
96 colors = 'ZMQInteractiveShell.colors',
96 colors = 'ZMQInteractiveShell.colors',
97
97
98 editor = 'IPythonWidget.editor',
98 editor = 'IPythonWidget.editor',
99 paging = 'ConsoleWidget.paging',
99 paging = 'ConsoleWidget.paging',
100 )
100 )
101 aliases.update(qt_aliases)
101 aliases.update(qt_aliases)
102
102
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104 # Classes
104 # Classes
105 #-----------------------------------------------------------------------------
105 #-----------------------------------------------------------------------------
106
106
107 #-----------------------------------------------------------------------------
107 #-----------------------------------------------------------------------------
108 # IPythonQtConsole
108 # IPythonQtConsole
109 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
110
110
111
111
112 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonMixinConsoleApp):
112 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonMixinConsoleApp):
113 name = 'ipython-qtconsole'
113 name = 'ipython-qtconsole'
114
114
115 description = """
115 description = """
116 The IPython QtConsole.
116 The IPython QtConsole.
117
117
118 This launches a Console-style application using Qt. It is not a full
118 This launches a Console-style application using Qt. It is not a full
119 console, in that launched terminal subprocesses will not be able to accept
119 console, in that launched terminal subprocesses will not be able to accept
120 input.
120 input.
121
121
122 The QtConsole supports various extra features beyond the Terminal IPython
122 The QtConsole supports various extra features beyond the Terminal IPython
123 shell, such as inline plotting with matplotlib, via:
123 shell, such as inline plotting with matplotlib, via:
124
124
125 ipython qtconsole --pylab=inline
125 ipython qtconsole --pylab=inline
126
126
127 as well as saving your session as HTML, and printing the output.
127 as well as saving your session as HTML, and printing the output.
128
128
129 """
129 """
130 examples = _examples
130 examples = _examples
131
131
132 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
132 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
133 flags = Dict(flags)
133 flags = Dict(flags)
134 aliases = Dict(aliases)
134 aliases = Dict(aliases)
135 kernel_manager_class = QtKernelManager
135 kernel_manager_class = QtKernelManager
136
136
137 stylesheet = Unicode('', config=True,
137 stylesheet = Unicode('', config=True,
138 help="path to a custom CSS stylesheet")
138 help="path to a custom CSS stylesheet")
139
139
140 plain = CBool(False, config=True,
140 plain = CBool(False, config=True,
141 help="Use a plaintext widget instead of rich text (plain can't print/save).")
141 help="Use a plaintext widget instead of rich text (plain can't print/save).")
142
142
143 def _pure_changed(self, name, old, new):
143 def _pure_changed(self, name, old, new):
144 kind = 'plain' if self.plain else 'rich'
144 kind = 'plain' if self.plain else 'rich'
145 self.config.ConsoleWidget.kind = kind
145 self.config.ConsoleWidget.kind = kind
146 if self.pure:
146 if self.pure:
147 self.widget_factory = FrontendWidget
147 self.widget_factory = FrontendWidget
148 elif self.plain:
148 elif self.plain:
149 self.widget_factory = IPythonWidget
149 self.widget_factory = IPythonWidget
150 else:
150 else:
151 self.widget_factory = RichIPythonWidget
151 self.widget_factory = RichIPythonWidget
152
152
153 _plain_changed = _pure_changed
153 _plain_changed = _pure_changed
154
154
155 # the factory for creating a widget
155 # the factory for creating a widget
156 widget_factory = Any(RichIPythonWidget)
156 widget_factory = Any(RichIPythonWidget)
157
157
158 def parse_command_line(self, argv=None):
158 def parse_command_line(self, argv=None):
159 super(IPythonQtConsoleApp, self).parse_command_line(argv)
159 super(IPythonQtConsoleApp, self).parse_command_line(argv)
160 IPythonMixinConsoleApp.parse_command_line(self,argv)
160 IPythonMixinConsoleApp.parse_command_line(self,argv)
161 self.swallow_args(qt_aliases,qt_flags,argv=argv)
161 self.swallow_args(qt_aliases,qt_flags,argv=argv)
162
162
163
163
164
164
165
165
166 def new_frontend_master(self):
166 def new_frontend_master(self):
167 """ Create and return new frontend attached to new kernel, launched on localhost.
167 """ Create and return new frontend attached to new kernel, launched on localhost.
168 """
168 """
169 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
169 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
170 kernel_manager = QtKernelManager(
170 kernel_manager = QtKernelManager(
171 ip=ip,
171 ip=ip,
172 connection_file=self._new_connection_file(),
172 connection_file=self._new_connection_file(),
173 config=self.config,
173 config=self.config,
174 )
174 )
175 # start the kernel
175 # start the kernel
176 kwargs = dict(ipython=not self.pure)
176 kwargs = dict(ipython=not self.pure)
177 kwargs['extra_arguments'] = self.kernel_argv
177 kwargs['extra_arguments'] = self.kernel_argv
178 kernel_manager.start_kernel(**kwargs)
178 kernel_manager.start_kernel(**kwargs)
179 kernel_manager.start_channels()
179 kernel_manager.start_channels()
180 widget = self.widget_factory(config=self.config,
180 widget = self.widget_factory(config=self.config,
181 local_kernel=True)
181 local_kernel=True)
182 widget.kernel_manager = kernel_manager
182 widget.kernel_manager = kernel_manager
183 widget._existing = False
183 widget._existing = False
184 widget._may_close = True
184 widget._may_close = True
185 widget._confirm_exit = self.confirm_exit
185 widget._confirm_exit = self.confirm_exit
186 return widget
186 return widget
187
187
188 def new_frontend_slave(self, current_widget):
188 def new_frontend_slave(self, current_widget):
189 """Create and return a new frontend attached to an existing kernel.
189 """Create and return a new frontend attached to an existing kernel.
190
190
191 Parameters
191 Parameters
192 ----------
192 ----------
193 current_widget : IPythonWidget
193 current_widget : IPythonWidget
194 The IPythonWidget whose kernel this frontend is to share
194 The IPythonWidget whose kernel this frontend is to share
195 """
195 """
196 kernel_manager = QtKernelManager(
196 kernel_manager = QtKernelManager(
197 connection_file=current_widget.kernel_manager.connection_file,
197 connection_file=current_widget.kernel_manager.connection_file,
198 config = self.config,
198 config = self.config,
199 )
199 )
200 kernel_manager.load_connection_file()
200 kernel_manager.load_connection_file()
201 kernel_manager.start_channels()
201 kernel_manager.start_channels()
202 widget = self.widget_factory(config=self.config,
202 widget = self.widget_factory(config=self.config,
203 local_kernel=False)
203 local_kernel=False)
204 widget._existing = True
204 widget._existing = True
205 widget._may_close = False
205 widget._may_close = False
206 widget._confirm_exit = False
206 widget._confirm_exit = False
207 widget.kernel_manager = kernel_manager
207 widget.kernel_manager = kernel_manager
208 return widget
208 return widget
209
209
210 def init_qt_elements(self):
210 def init_qt_elements(self):
211 # Create the widget.
211 # Create the widget.
212 self.app = QtGui.QApplication([])
212 self.app = QtGui.QApplication([])
213
213
214 base_path = os.path.abspath(os.path.dirname(__file__))
214 base_path = os.path.abspath(os.path.dirname(__file__))
215 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
215 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
216 self.app.icon = QtGui.QIcon(icon_path)
216 self.app.icon = QtGui.QIcon(icon_path)
217 QtGui.QApplication.setWindowIcon(self.app.icon)
217 QtGui.QApplication.setWindowIcon(self.app.icon)
218
218
219 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
219 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
220 self.widget = self.widget_factory(config=self.config,
220 self.widget = self.widget_factory(config=self.config,
221 local_kernel=local_kernel)
221 local_kernel=local_kernel)
222 self.widget._existing = self.existing
222 self.widget._existing = self.existing
223 self.widget._may_close = not self.existing
223 self.widget._may_close = not self.existing
224 self.widget._confirm_exit = self.confirm_exit
224 self.widget._confirm_exit = self.confirm_exit
225
225
226 self.widget.kernel_manager = self.kernel_manager
226 self.widget.kernel_manager = self.kernel_manager
227 self.window = MainWindow(self.app,
227 self.window = MainWindow(self.app,
228 confirm_exit=self.confirm_exit,
228 confirm_exit=self.confirm_exit,
229 new_frontend_factory=self.new_frontend_master,
229 new_frontend_factory=self.new_frontend_master,
230 slave_frontend_factory=self.new_frontend_slave,
230 slave_frontend_factory=self.new_frontend_slave,
231 )
231 )
232 self.window.log = self.log
232 self.window.log = self.log
233 self.window.add_tab_with_frontend(self.widget)
233 self.window.add_tab_with_frontend(self.widget)
234 self.window.init_menu_bar()
234 self.window.init_menu_bar()
235
235
236 self.window.setWindowTitle('Python' if self.pure else 'IPython')
236 self.window.setWindowTitle('Python' if self.pure else 'IPython')
237
237
238 def init_colors(self):
238 def init_colors(self):
239 """Configure the coloring of the widget"""
239 """Configure the coloring of the widget"""
240 # Note: This will be dramatically simplified when colors
240 # Note: This will be dramatically simplified when colors
241 # are removed from the backend.
241 # are removed from the backend.
242
242
243 if self.pure:
243 if self.pure:
244 # only IPythonWidget supports styling
244 # only IPythonWidget supports styling
245 return
245 return
246
246
247 # parse the colors arg down to current known labels
247 # parse the colors arg down to current known labels
248 try:
248 try:
249 colors = self.config.ZMQInteractiveShell.colors
249 colors = self.config.ZMQInteractiveShell.colors
250 except AttributeError:
250 except AttributeError:
251 colors = None
251 colors = None
252 try:
252 try:
253 style = self.config.IPythonWidget.syntax_style
253 style = self.config.IPythonWidget.syntax_style
254 except AttributeError:
254 except AttributeError:
255 style = None
255 style = None
256
256
257 # find the value for colors:
257 # find the value for colors:
258 if colors:
258 if colors:
259 colors=colors.lower()
259 colors=colors.lower()
260 if colors in ('lightbg', 'light'):
260 if colors in ('lightbg', 'light'):
261 colors='lightbg'
261 colors='lightbg'
262 elif colors in ('dark', 'linux'):
262 elif colors in ('dark', 'linux'):
263 colors='linux'
263 colors='linux'
264 else:
264 else:
265 colors='nocolor'
265 colors='nocolor'
266 elif style:
266 elif style:
267 if style=='bw':
267 if style=='bw':
268 colors='nocolor'
268 colors='nocolor'
269 elif styles.dark_style(style):
269 elif styles.dark_style(style):
270 colors='linux'
270 colors='linux'
271 else:
271 else:
272 colors='lightbg'
272 colors='lightbg'
273 else:
273 else:
274 colors=None
274 colors=None
275
275
276 # Configure the style.
276 # Configure the style.
277 widget = self.widget
277 widget = self.widget
278 if style:
278 if style:
279 widget.style_sheet = styles.sheet_from_template(style, colors)
279 widget.style_sheet = styles.sheet_from_template(style, colors)
280 widget.syntax_style = style
280 widget.syntax_style = style
281 widget._syntax_style_changed()
281 widget._syntax_style_changed()
282 widget._style_sheet_changed()
282 widget._style_sheet_changed()
283 elif colors:
283 elif colors:
284 # use a default style
284 # use a default style
285 widget.set_default_style(colors=colors)
285 widget.set_default_style(colors=colors)
286 else:
286 else:
287 # this is redundant for now, but allows the widget's
287 # this is redundant for now, but allows the widget's
288 # defaults to change
288 # defaults to change
289 widget.set_default_style()
289 widget.set_default_style()
290
290
291 if self.stylesheet:
291 if self.stylesheet:
292 # we got an expicit stylesheet
292 # we got an expicit stylesheet
293 if os.path.isfile(self.stylesheet):
293 if os.path.isfile(self.stylesheet):
294 with open(self.stylesheet) as f:
294 with open(self.stylesheet) as f:
295 sheet = f.read()
295 sheet = f.read()
296 widget.style_sheet = sheet
296 widget.style_sheet = sheet
297 widget._style_sheet_changed()
297 widget._style_sheet_changed()
298 else:
298 else:
299 raise IOError("Stylesheet %r not found."%self.stylesheet)
299 raise IOError("Stylesheet %r not found."%self.stylesheet)
300
300
301 def init_signal(self):
302 """allow clean shutdown on sigint"""
303 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
304 # need a timer, so that QApplication doesn't block until a real
305 # Qt event fires (can require mouse movement)
306 # timer trick from http://stackoverflow.com/q/4938723/938949
307 timer = QtCore.QTimer()
308 # Let the interpreter run each 200 ms:
309 timer.timeout.connect(lambda: None)
310 timer.start(200)
311 # hold onto ref, so the timer doesn't get cleaned up
312 self._sigint_timer = timer
313
301 @catch_config_error
314 @catch_config_error
302 def initialize(self, argv=None):
315 def initialize(self, argv=None):
303 super(IPythonQtConsoleApp, self).initialize(argv)
316 super(IPythonQtConsoleApp, self).initialize(argv)
304 IPythonMixinConsoleApp.initialize(self,argv)
317 IPythonMixinConsoleApp.initialize(self,argv)
305 self.init_qt_elements()
318 self.init_qt_elements()
306 self.init_colors()
319 self.init_colors()
320 self.init_signal()
307
321
308 def start(self):
322 def start(self):
309
323
310 # draw the window
324 # draw the window
311 self.window.show()
325 self.window.show()
312
326
313 # Start the application main loop.
327 # Start the application main loop.
314 self.app.exec_()
328 self.app.exec_()
315
329
316 #-----------------------------------------------------------------------------
330 #-----------------------------------------------------------------------------
317 # Main entry point
331 # Main entry point
318 #-----------------------------------------------------------------------------
332 #-----------------------------------------------------------------------------
319
333
320 def main():
334 def main():
321 app = IPythonQtConsoleApp()
335 app = IPythonQtConsoleApp()
322 app.initialize()
336 app.initialize()
323 app.start()
337 app.start()
324
338
325
339
326 if __name__ == '__main__':
340 if __name__ == '__main__':
327 main()
341 main()
@@ -1,947 +1,954 b''
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 errno
19 import errno
20 import json
20 import json
21 from subprocess import Popen
21 from subprocess import Popen
22 import os
22 import os
23 import signal
23 import signal
24 import sys
24 import sys
25 from threading import Thread
25 from threading import Thread
26 import time
26 import time
27
27
28 # System library imports.
28 # System library imports.
29 import zmq
29 import zmq
30 from zmq.eventloop import ioloop, zmqstream
30 from zmq.eventloop import ioloop, zmqstream
31
31
32 # Local imports.
32 # Local imports.
33 from IPython.config.loader import Config
33 from IPython.config.loader import Config
34 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
34 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
35 from IPython.utils.traitlets import (
35 from IPython.utils.traitlets import (
36 HasTraits, Any, Instance, Type, Unicode, Integer, Bool
36 HasTraits, Any, Instance, Type, Unicode, Integer, Bool
37 )
37 )
38 from IPython.utils.py3compat import str_to_bytes
38 from IPython.utils.py3compat import str_to_bytes
39 from IPython.zmq.entry_point import write_connection_file
39 from IPython.zmq.entry_point import write_connection_file
40 from session import Session
40 from session import Session
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Constants and exceptions
43 # Constants and exceptions
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 class InvalidPortNumber(Exception):
46 class InvalidPortNumber(Exception):
47 pass
47 pass
48
48
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 # Utility functions
50 # Utility functions
51 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
52
52
53 # some utilities to validate message structure, these might get moved elsewhere
53 # some utilities to validate message structure, these might get moved elsewhere
54 # if they prove to have more generic utility
54 # if they prove to have more generic utility
55
55
56 def validate_string_list(lst):
56 def validate_string_list(lst):
57 """Validate that the input is a list of strings.
57 """Validate that the input is a list of strings.
58
58
59 Raises ValueError if not."""
59 Raises ValueError if not."""
60 if not isinstance(lst, list):
60 if not isinstance(lst, list):
61 raise ValueError('input %r must be a list' % lst)
61 raise ValueError('input %r must be a list' % lst)
62 for x in lst:
62 for x in lst:
63 if not isinstance(x, basestring):
63 if not isinstance(x, basestring):
64 raise ValueError('element %r in list must be a string' % x)
64 raise ValueError('element %r in list must be a string' % x)
65
65
66
66
67 def validate_string_dict(dct):
67 def validate_string_dict(dct):
68 """Validate that the input is a dict with string keys and values.
68 """Validate that the input is a dict with string keys and values.
69
69
70 Raises ValueError if not."""
70 Raises ValueError if not."""
71 for k,v in dct.iteritems():
71 for k,v in dct.iteritems():
72 if not isinstance(k, basestring):
72 if not isinstance(k, basestring):
73 raise ValueError('key %r in dict must be a string' % k)
73 raise ValueError('key %r in dict must be a string' % k)
74 if not isinstance(v, basestring):
74 if not isinstance(v, basestring):
75 raise ValueError('value %r in dict must be a string' % v)
75 raise ValueError('value %r in dict must be a string' % v)
76
76
77
77
78 #-----------------------------------------------------------------------------
78 #-----------------------------------------------------------------------------
79 # ZMQ Socket Channel classes
79 # ZMQ Socket Channel classes
80 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
81
81
82 class ZMQSocketChannel(Thread):
82 class ZMQSocketChannel(Thread):
83 """The base class for the channels that use ZMQ sockets.
83 """The base class for the channels that use ZMQ sockets.
84 """
84 """
85 context = None
85 context = None
86 session = None
86 session = None
87 socket = None
87 socket = None
88 ioloop = None
88 ioloop = None
89 stream = None
89 stream = None
90 _address = None
90 _address = None
91
91
92 def __init__(self, context, session, address):
92 def __init__(self, context, session, address):
93 """Create a channel
93 """Create a channel
94
94
95 Parameters
95 Parameters
96 ----------
96 ----------
97 context : :class:`zmq.Context`
97 context : :class:`zmq.Context`
98 The ZMQ context to use.
98 The ZMQ context to use.
99 session : :class:`session.Session`
99 session : :class:`session.Session`
100 The session to use.
100 The session to use.
101 address : tuple
101 address : tuple
102 Standard (ip, port) tuple that the kernel is listening on.
102 Standard (ip, port) tuple that the kernel is listening on.
103 """
103 """
104 super(ZMQSocketChannel, self).__init__()
104 super(ZMQSocketChannel, self).__init__()
105 self.daemon = True
105 self.daemon = True
106
106
107 self.context = context
107 self.context = context
108 self.session = session
108 self.session = session
109 if address[1] == 0:
109 if address[1] == 0:
110 message = 'The port number for a channel cannot be 0.'
110 message = 'The port number for a channel cannot be 0.'
111 raise InvalidPortNumber(message)
111 raise InvalidPortNumber(message)
112 self._address = address
112 self._address = address
113
113
114 def _run_loop(self):
114 def _run_loop(self):
115 """Run my loop, ignoring EINTR events in the poller"""
115 """Run my loop, ignoring EINTR events in the poller"""
116 while True:
116 while True:
117 try:
117 try:
118 self.ioloop.start()
118 self.ioloop.start()
119 except zmq.ZMQError as e:
119 except zmq.ZMQError as e:
120 if e.errno == errno.EINTR:
120 if e.errno == errno.EINTR:
121 continue
121 continue
122 else:
122 else:
123 raise
123 raise
124 else:
124 else:
125 break
125 break
126
126
127 def stop(self):
127 def stop(self):
128 """Stop the channel's activity.
128 """Stop the channel's activity.
129
129
130 This calls :method:`Thread.join` and returns when the thread
130 This calls :method:`Thread.join` and returns when the thread
131 terminates. :class:`RuntimeError` will be raised if
131 terminates. :class:`RuntimeError` will be raised if
132 :method:`self.start` is called again.
132 :method:`self.start` is called again.
133 """
133 """
134 self.join()
134 self.join()
135
135
136 @property
136 @property
137 def address(self):
137 def address(self):
138 """Get the channel's address as an (ip, port) tuple.
138 """Get the channel's address as an (ip, port) tuple.
139
139
140 By the default, the address is (localhost, 0), where 0 means a random
140 By the default, the address is (localhost, 0), where 0 means a random
141 port.
141 port.
142 """
142 """
143 return self._address
143 return self._address
144
144
145 def _queue_send(self, msg):
145 def _queue_send(self, msg):
146 """Queue a message to be sent from the IOLoop's thread.
146 """Queue a message to be sent from the IOLoop's thread.
147
147
148 Parameters
148 Parameters
149 ----------
149 ----------
150 msg : message to send
150 msg : message to send
151
151
152 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
152 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
153 thread control of the action.
153 thread control of the action.
154 """
154 """
155 def thread_send():
155 def thread_send():
156 self.session.send(self.stream, msg)
156 self.session.send(self.stream, msg)
157 self.ioloop.add_callback(thread_send)
157 self.ioloop.add_callback(thread_send)
158
158
159 def _handle_recv(self, msg):
159 def _handle_recv(self, msg):
160 """callback for stream.on_recv
160 """callback for stream.on_recv
161
161
162 unpacks message, and calls handlers with it.
162 unpacks message, and calls handlers with it.
163 """
163 """
164 ident,smsg = self.session.feed_identities(msg)
164 ident,smsg = self.session.feed_identities(msg)
165 self.call_handlers(self.session.unserialize(smsg))
165 self.call_handlers(self.session.unserialize(smsg))
166
166
167
167
168
168
169 class ShellSocketChannel(ZMQSocketChannel):
169 class ShellSocketChannel(ZMQSocketChannel):
170 """The XREQ channel for issues request/replies to the kernel.
170 """The XREQ channel for issues request/replies to the kernel.
171 """
171 """
172
172
173 command_queue = None
173 command_queue = None
174 # flag for whether execute requests should be allowed to call raw_input:
174 # flag for whether execute requests should be allowed to call raw_input:
175 allow_stdin = True
175 allow_stdin = True
176
176
177 def __init__(self, context, session, address):
177 def __init__(self, context, session, address):
178 super(ShellSocketChannel, self).__init__(context, session, address)
178 super(ShellSocketChannel, self).__init__(context, session, address)
179 self.ioloop = ioloop.IOLoop()
179 self.ioloop = ioloop.IOLoop()
180
180
181 def run(self):
181 def run(self):
182 """The thread's main activity. Call start() instead."""
182 """The thread's main activity. Call start() instead."""
183 self.socket = self.context.socket(zmq.DEALER)
183 self.socket = self.context.socket(zmq.DEALER)
184 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
184 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
185 self.socket.connect('tcp://%s:%i' % self.address)
185 self.socket.connect('tcp://%s:%i' % self.address)
186 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
186 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
187 self.stream.on_recv(self._handle_recv)
187 self.stream.on_recv(self._handle_recv)
188 self._run_loop()
188 self._run_loop()
189
189
190 def stop(self):
190 def stop(self):
191 self.ioloop.stop()
191 self.ioloop.stop()
192 super(ShellSocketChannel, self).stop()
192 super(ShellSocketChannel, self).stop()
193
193
194 def call_handlers(self, msg):
194 def call_handlers(self, msg):
195 """This method is called in the ioloop thread when a message arrives.
195 """This method is called in the ioloop thread when a message arrives.
196
196
197 Subclasses should override this method to handle incoming messages.
197 Subclasses should override this method to handle incoming messages.
198 It is important to remember that this method is called in the thread
198 It is important to remember that this method is called in the thread
199 so that some logic must be done to ensure that the application leve
199 so that some logic must be done to ensure that the application leve
200 handlers are called in the application thread.
200 handlers are called in the application thread.
201 """
201 """
202 raise NotImplementedError('call_handlers must be defined in a subclass.')
202 raise NotImplementedError('call_handlers must be defined in a subclass.')
203
203
204 def execute(self, code, silent=False,
204 def execute(self, code, silent=False,
205 user_variables=None, user_expressions=None, allow_stdin=None):
205 user_variables=None, user_expressions=None, allow_stdin=None):
206 """Execute code in the kernel.
206 """Execute code in the kernel.
207
207
208 Parameters
208 Parameters
209 ----------
209 ----------
210 code : str
210 code : str
211 A string of Python code.
211 A string of Python code.
212
212
213 silent : bool, optional (default False)
213 silent : bool, optional (default False)
214 If set, the kernel will execute the code as quietly possible.
214 If set, the kernel will execute the code as quietly possible.
215
215
216 user_variables : list, optional
216 user_variables : list, optional
217 A list of variable names to pull from the user's namespace. They
217 A list of variable names to pull from the user's namespace. They
218 will come back as a dict with these names as keys and their
218 will come back as a dict with these names as keys and their
219 :func:`repr` as values.
219 :func:`repr` as values.
220
220
221 user_expressions : dict, optional
221 user_expressions : dict, optional
222 A dict with string keys and to pull from the user's
222 A dict with string keys and to pull from the user's
223 namespace. They will come back as a dict with these names as keys
223 namespace. They will come back as a dict with these names as keys
224 and their :func:`repr` as values.
224 and their :func:`repr` as values.
225
225
226 allow_stdin : bool, optional
226 allow_stdin : bool, optional
227 Flag for
227 Flag for
228 A dict with string keys and to pull from the user's
228 A dict with string keys and to pull from the user's
229 namespace. They will come back as a dict with these names as keys
229 namespace. They will come back as a dict with these names as keys
230 and their :func:`repr` as values.
230 and their :func:`repr` as values.
231
231
232 Returns
232 Returns
233 -------
233 -------
234 The msg_id of the message sent.
234 The msg_id of the message sent.
235 """
235 """
236 if user_variables is None:
236 if user_variables is None:
237 user_variables = []
237 user_variables = []
238 if user_expressions is None:
238 if user_expressions is None:
239 user_expressions = {}
239 user_expressions = {}
240 if allow_stdin is None:
240 if allow_stdin is None:
241 allow_stdin = self.allow_stdin
241 allow_stdin = self.allow_stdin
242
242
243
243
244 # Don't waste network traffic if inputs are invalid
244 # Don't waste network traffic if inputs are invalid
245 if not isinstance(code, basestring):
245 if not isinstance(code, basestring):
246 raise ValueError('code %r must be a string' % code)
246 raise ValueError('code %r must be a string' % code)
247 validate_string_list(user_variables)
247 validate_string_list(user_variables)
248 validate_string_dict(user_expressions)
248 validate_string_dict(user_expressions)
249
249
250 # Create class for content/msg creation. Related to, but possibly
250 # Create class for content/msg creation. Related to, but possibly
251 # not in Session.
251 # not in Session.
252 content = dict(code=code, silent=silent,
252 content = dict(code=code, silent=silent,
253 user_variables=user_variables,
253 user_variables=user_variables,
254 user_expressions=user_expressions,
254 user_expressions=user_expressions,
255 allow_stdin=allow_stdin,
255 allow_stdin=allow_stdin,
256 )
256 )
257 msg = self.session.msg('execute_request', content)
257 msg = self.session.msg('execute_request', content)
258 self._queue_send(msg)
258 self._queue_send(msg)
259 return msg['header']['msg_id']
259 return msg['header']['msg_id']
260
260
261 def complete(self, text, line, cursor_pos, block=None):
261 def complete(self, text, line, cursor_pos, block=None):
262 """Tab complete text in the kernel's namespace.
262 """Tab complete text in the kernel's namespace.
263
263
264 Parameters
264 Parameters
265 ----------
265 ----------
266 text : str
266 text : str
267 The text to complete.
267 The text to complete.
268 line : str
268 line : str
269 The full line of text that is the surrounding context for the
269 The full line of text that is the surrounding context for the
270 text to complete.
270 text to complete.
271 cursor_pos : int
271 cursor_pos : int
272 The position of the cursor in the line where the completion was
272 The position of the cursor in the line where the completion was
273 requested.
273 requested.
274 block : str, optional
274 block : str, optional
275 The full block of code in which the completion is being requested.
275 The full block of code in which the completion is being requested.
276
276
277 Returns
277 Returns
278 -------
278 -------
279 The msg_id of the message sent.
279 The msg_id of the message sent.
280 """
280 """
281 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
281 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
282 msg = self.session.msg('complete_request', content)
282 msg = self.session.msg('complete_request', content)
283 self._queue_send(msg)
283 self._queue_send(msg)
284 return msg['header']['msg_id']
284 return msg['header']['msg_id']
285
285
286 def object_info(self, oname):
286 def object_info(self, oname):
287 """Get metadata information about an object.
287 """Get metadata information about an object.
288
288
289 Parameters
289 Parameters
290 ----------
290 ----------
291 oname : str
291 oname : str
292 A string specifying the object name.
292 A string specifying the object name.
293
293
294 Returns
294 Returns
295 -------
295 -------
296 The msg_id of the message sent.
296 The msg_id of the message sent.
297 """
297 """
298 content = dict(oname=oname)
298 content = dict(oname=oname)
299 msg = self.session.msg('object_info_request', content)
299 msg = self.session.msg('object_info_request', content)
300 self._queue_send(msg)
300 self._queue_send(msg)
301 return msg['header']['msg_id']
301 return msg['header']['msg_id']
302
302
303 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
303 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
304 """Get entries from the history list.
304 """Get entries from the history list.
305
305
306 Parameters
306 Parameters
307 ----------
307 ----------
308 raw : bool
308 raw : bool
309 If True, return the raw input.
309 If True, return the raw input.
310 output : bool
310 output : bool
311 If True, then return the output as well.
311 If True, then return the output as well.
312 hist_access_type : str
312 hist_access_type : str
313 'range' (fill in session, start and stop params), 'tail' (fill in n)
313 'range' (fill in session, start and stop params), 'tail' (fill in n)
314 or 'search' (fill in pattern param).
314 or 'search' (fill in pattern param).
315
315
316 session : int
316 session : int
317 For a range request, the session from which to get lines. Session
317 For a range request, the session from which to get lines. Session
318 numbers are positive integers; negative ones count back from the
318 numbers are positive integers; negative ones count back from the
319 current session.
319 current session.
320 start : int
320 start : int
321 The first line number of a history range.
321 The first line number of a history range.
322 stop : int
322 stop : int
323 The final (excluded) line number of a history range.
323 The final (excluded) line number of a history range.
324
324
325 n : int
325 n : int
326 The number of lines of history to get for a tail request.
326 The number of lines of history to get for a tail request.
327
327
328 pattern : str
328 pattern : str
329 The glob-syntax pattern for a search request.
329 The glob-syntax pattern for a search request.
330
330
331 Returns
331 Returns
332 -------
332 -------
333 The msg_id of the message sent.
333 The msg_id of the message sent.
334 """
334 """
335 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
335 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
336 **kwargs)
336 **kwargs)
337 msg = self.session.msg('history_request', content)
337 msg = self.session.msg('history_request', content)
338 self._queue_send(msg)
338 self._queue_send(msg)
339 return msg['header']['msg_id']
339 return msg['header']['msg_id']
340
340
341 def shutdown(self, restart=False):
341 def shutdown(self, restart=False):
342 """Request an immediate kernel shutdown.
342 """Request an immediate kernel shutdown.
343
343
344 Upon receipt of the (empty) reply, client code can safely assume that
344 Upon receipt of the (empty) reply, client code can safely assume that
345 the kernel has shut down and it's safe to forcefully terminate it if
345 the kernel has shut down and it's safe to forcefully terminate it if
346 it's still alive.
346 it's still alive.
347
347
348 The kernel will send the reply via a function registered with Python's
348 The kernel will send the reply via a function registered with Python's
349 atexit module, ensuring it's truly done as the kernel is done with all
349 atexit module, ensuring it's truly done as the kernel is done with all
350 normal operation.
350 normal operation.
351 """
351 """
352 # Send quit message to kernel. Once we implement kernel-side setattr,
352 # Send quit message to kernel. Once we implement kernel-side setattr,
353 # this should probably be done that way, but for now this will do.
353 # this should probably be done that way, but for now this will do.
354 msg = self.session.msg('shutdown_request', {'restart':restart})
354 msg = self.session.msg('shutdown_request', {'restart':restart})
355 self._queue_send(msg)
355 self._queue_send(msg)
356 return msg['header']['msg_id']
356 return msg['header']['msg_id']
357
357
358
358
359
359
360 class SubSocketChannel(ZMQSocketChannel):
360 class SubSocketChannel(ZMQSocketChannel):
361 """The SUB channel which listens for messages that the kernel publishes.
361 """The SUB channel which listens for messages that the kernel publishes.
362 """
362 """
363
363
364 def __init__(self, context, session, address):
364 def __init__(self, context, session, address):
365 super(SubSocketChannel, self).__init__(context, session, address)
365 super(SubSocketChannel, self).__init__(context, session, address)
366 self.ioloop = ioloop.IOLoop()
366 self.ioloop = ioloop.IOLoop()
367
367
368 def run(self):
368 def run(self):
369 """The thread's main activity. Call start() instead."""
369 """The thread's main activity. Call start() instead."""
370 self.socket = self.context.socket(zmq.SUB)
370 self.socket = self.context.socket(zmq.SUB)
371 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
371 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
372 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
372 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
373 self.socket.connect('tcp://%s:%i' % self.address)
373 self.socket.connect('tcp://%s:%i' % self.address)
374 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
374 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
375 self.stream.on_recv(self._handle_recv)
375 self.stream.on_recv(self._handle_recv)
376 self._run_loop()
376 self._run_loop()
377
377
378 def stop(self):
378 def stop(self):
379 self.ioloop.stop()
379 self.ioloop.stop()
380 super(SubSocketChannel, self).stop()
380 super(SubSocketChannel, self).stop()
381
381
382 def call_handlers(self, msg):
382 def call_handlers(self, msg):
383 """This method is called in the ioloop thread when a message arrives.
383 """This method is called in the ioloop thread when a message arrives.
384
384
385 Subclasses should override this method to handle incoming messages.
385 Subclasses should override this method to handle incoming messages.
386 It is important to remember that this method is called in the thread
386 It is important to remember that this method is called in the thread
387 so that some logic must be done to ensure that the application leve
387 so that some logic must be done to ensure that the application leve
388 handlers are called in the application thread.
388 handlers are called in the application thread.
389 """
389 """
390 raise NotImplementedError('call_handlers must be defined in a subclass.')
390 raise NotImplementedError('call_handlers must be defined in a subclass.')
391
391
392 def flush(self, timeout=1.0):
392 def flush(self, timeout=1.0):
393 """Immediately processes all pending messages on the SUB channel.
393 """Immediately processes all pending messages on the SUB channel.
394
394
395 Callers should use this method to ensure that :method:`call_handlers`
395 Callers should use this method to ensure that :method:`call_handlers`
396 has been called for all messages that have been received on the
396 has been called for all messages that have been received on the
397 0MQ SUB socket of this channel.
397 0MQ SUB socket of this channel.
398
398
399 This method is thread safe.
399 This method is thread safe.
400
400
401 Parameters
401 Parameters
402 ----------
402 ----------
403 timeout : float, optional
403 timeout : float, optional
404 The maximum amount of time to spend flushing, in seconds. The
404 The maximum amount of time to spend flushing, in seconds. The
405 default is one second.
405 default is one second.
406 """
406 """
407 # We do the IOLoop callback process twice to ensure that the IOLoop
407 # We do the IOLoop callback process twice to ensure that the IOLoop
408 # gets to perform at least one full poll.
408 # gets to perform at least one full poll.
409 stop_time = time.time() + timeout
409 stop_time = time.time() + timeout
410 for i in xrange(2):
410 for i in xrange(2):
411 self._flushed = False
411 self._flushed = False
412 self.ioloop.add_callback(self._flush)
412 self.ioloop.add_callback(self._flush)
413 while not self._flushed and time.time() < stop_time:
413 while not self._flushed and time.time() < stop_time:
414 time.sleep(0.01)
414 time.sleep(0.01)
415
415
416 def _flush(self):
416 def _flush(self):
417 """Callback for :method:`self.flush`."""
417 """Callback for :method:`self.flush`."""
418 self.stream.flush()
418 self.stream.flush()
419 self._flushed = True
419 self._flushed = True
420
420
421
421
422 class StdInSocketChannel(ZMQSocketChannel):
422 class StdInSocketChannel(ZMQSocketChannel):
423 """A reply channel to handle raw_input requests that the kernel makes."""
423 """A reply channel to handle raw_input requests that the kernel makes."""
424
424
425 msg_queue = None
425 msg_queue = None
426
426
427 def __init__(self, context, session, address):
427 def __init__(self, context, session, address):
428 super(StdInSocketChannel, self).__init__(context, session, address)
428 super(StdInSocketChannel, self).__init__(context, session, address)
429 self.ioloop = ioloop.IOLoop()
429 self.ioloop = ioloop.IOLoop()
430
430
431 def run(self):
431 def run(self):
432 """The thread's main activity. Call start() instead."""
432 """The thread's main activity. Call start() instead."""
433 self.socket = self.context.socket(zmq.DEALER)
433 self.socket = self.context.socket(zmq.DEALER)
434 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
434 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
435 self.socket.connect('tcp://%s:%i' % self.address)
435 self.socket.connect('tcp://%s:%i' % self.address)
436 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
436 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
437 self.stream.on_recv(self._handle_recv)
437 self.stream.on_recv(self._handle_recv)
438 self._run_loop()
438 self._run_loop()
439
439
440 def stop(self):
440 def stop(self):
441 self.ioloop.stop()
441 self.ioloop.stop()
442 super(StdInSocketChannel, self).stop()
442 super(StdInSocketChannel, self).stop()
443
443
444 def call_handlers(self, msg):
444 def call_handlers(self, msg):
445 """This method is called in the ioloop thread when a message arrives.
445 """This method is called in the ioloop thread when a message arrives.
446
446
447 Subclasses should override this method to handle incoming messages.
447 Subclasses should override this method to handle incoming messages.
448 It is important to remember that this method is called in the thread
448 It is important to remember that this method is called in the thread
449 so that some logic must be done to ensure that the application leve
449 so that some logic must be done to ensure that the application leve
450 handlers are called in the application thread.
450 handlers are called in the application thread.
451 """
451 """
452 raise NotImplementedError('call_handlers must be defined in a subclass.')
452 raise NotImplementedError('call_handlers must be defined in a subclass.')
453
453
454 def input(self, string):
454 def input(self, string):
455 """Send a string of raw input to the kernel."""
455 """Send a string of raw input to the kernel."""
456 content = dict(value=string)
456 content = dict(value=string)
457 msg = self.session.msg('input_reply', content)
457 msg = self.session.msg('input_reply', content)
458 self._queue_send(msg)
458 self._queue_send(msg)
459
459
460
460
461 class HBSocketChannel(ZMQSocketChannel):
461 class HBSocketChannel(ZMQSocketChannel):
462 """The heartbeat channel which monitors the kernel heartbeat.
462 """The heartbeat channel which monitors the kernel heartbeat.
463
463
464 Note that the heartbeat channel is paused by default. As long as you start
464 Note that the heartbeat channel is paused by default. As long as you start
465 this channel, the kernel manager will ensure that it is paused and un-paused
465 this channel, the kernel manager will ensure that it is paused and un-paused
466 as appropriate.
466 as appropriate.
467 """
467 """
468
468
469 time_to_dead = 3.0
469 time_to_dead = 3.0
470 socket = None
470 socket = None
471 poller = None
471 poller = None
472 _running = None
472 _running = None
473 _pause = None
473 _pause = None
474
474
475 def __init__(self, context, session, address):
475 def __init__(self, context, session, address):
476 super(HBSocketChannel, self).__init__(context, session, address)
476 super(HBSocketChannel, self).__init__(context, session, address)
477 self._running = False
477 self._running = False
478 self._pause = True
478 self._pause = True
479
479
480 def _create_socket(self):
480 def _create_socket(self):
481 self.socket = self.context.socket(zmq.REQ)
481 self.socket = self.context.socket(zmq.REQ)
482 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
482 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
483 self.socket.connect('tcp://%s:%i' % self.address)
483 self.socket.connect('tcp://%s:%i' % self.address)
484 self.poller = zmq.Poller()
484 self.poller = zmq.Poller()
485 self.poller.register(self.socket, zmq.POLLIN)
485 self.poller.register(self.socket, zmq.POLLIN)
486
486
487 def run(self):
487 def run(self):
488 """The thread's main activity. Call start() instead."""
488 """The thread's main activity. Call start() instead."""
489 self._create_socket()
489 self._create_socket()
490 self._running = True
490 self._running = True
491 while self._running:
491 while self._running:
492 if self._pause:
492 if self._pause:
493 time.sleep(self.time_to_dead)
493 time.sleep(self.time_to_dead)
494 else:
494 else:
495 since_last_heartbeat = 0.0
495 since_last_heartbeat = 0.0
496 request_time = time.time()
496 request_time = time.time()
497 try:
497 try:
498 #io.rprint('Ping from HB channel') # dbg
498 #io.rprint('Ping from HB channel') # dbg
499 self.socket.send(b'ping')
499 self.socket.send(b'ping')
500 except zmq.ZMQError, e:
500 except zmq.ZMQError, e:
501 #io.rprint('*** HB Error:', e) # dbg
501 #io.rprint('*** HB Error:', e) # dbg
502 if e.errno == zmq.EFSM:
502 if e.errno == zmq.EFSM:
503 #io.rprint('sleep...', self.time_to_dead) # dbg
503 #io.rprint('sleep...', self.time_to_dead) # dbg
504 time.sleep(self.time_to_dead)
504 time.sleep(self.time_to_dead)
505 self._create_socket()
505 self._create_socket()
506 else:
506 else:
507 raise
507 raise
508 else:
508 else:
509 while True:
509 while True:
510 try:
510 try:
511 self.socket.recv(zmq.NOBLOCK)
511 self.socket.recv(zmq.NOBLOCK)
512 except zmq.ZMQError, e:
512 except zmq.ZMQError, e:
513 #io.rprint('*** HB Error 2:', e) # dbg
513 #io.rprint('*** HB Error 2:', e) # dbg
514 if e.errno == zmq.EAGAIN:
514 if e.errno == zmq.EAGAIN:
515 before_poll = time.time()
515 before_poll = time.time()
516 until_dead = self.time_to_dead - (before_poll -
516 until_dead = self.time_to_dead - (before_poll -
517 request_time)
517 request_time)
518
518
519 # When the return value of poll() is an empty
519 # When the return value of poll() is an empty
520 # list, that is when things have gone wrong
520 # list, that is when things have gone wrong
521 # (zeromq bug). As long as it is not an empty
521 # (zeromq bug). As long as it is not an empty
522 # list, poll is working correctly even if it
522 # list, poll is working correctly even if it
523 # returns quickly. Note: poll timeout is in
523 # returns quickly. Note: poll timeout is in
524 # milliseconds.
524 # milliseconds.
525 if until_dead > 0.0:
525 if until_dead > 0.0:
526 while True:
526 while True:
527 try:
527 try:
528 self.poller.poll(1000 * until_dead)
528 self.poller.poll(1000 * until_dead)
529 except zmq.ZMQError as e:
529 except zmq.ZMQError as e:
530 if e.errno == errno.EINTR:
530 if e.errno == errno.EINTR:
531 continue
531 continue
532 else:
532 else:
533 raise
533 raise
534 else:
534 else:
535 break
535 break
536
536
537 since_last_heartbeat = time.time()-request_time
537 since_last_heartbeat = time.time()-request_time
538 if since_last_heartbeat > self.time_to_dead:
538 if since_last_heartbeat > self.time_to_dead:
539 self.call_handlers(since_last_heartbeat)
539 self.call_handlers(since_last_heartbeat)
540 break
540 break
541 else:
541 else:
542 # FIXME: We should probably log this instead.
542 # FIXME: We should probably log this instead.
543 raise
543 raise
544 else:
544 else:
545 until_dead = self.time_to_dead - (time.time() -
545 until_dead = self.time_to_dead - (time.time() -
546 request_time)
546 request_time)
547 if until_dead > 0.0:
547 if until_dead > 0.0:
548 #io.rprint('sleep...', self.time_to_dead) # dbg
548 #io.rprint('sleep...', self.time_to_dead) # dbg
549 time.sleep(until_dead)
549 time.sleep(until_dead)
550 break
550 break
551
551
552 def pause(self):
552 def pause(self):
553 """Pause the heartbeat."""
553 """Pause the heartbeat."""
554 self._pause = True
554 self._pause = True
555
555
556 def unpause(self):
556 def unpause(self):
557 """Unpause the heartbeat."""
557 """Unpause the heartbeat."""
558 self._pause = False
558 self._pause = False
559
559
560 def is_beating(self):
560 def is_beating(self):
561 """Is the heartbeat running and not paused."""
561 """Is the heartbeat running and not paused."""
562 if self.is_alive() and not self._pause:
562 if self.is_alive() and not self._pause:
563 return True
563 return True
564 else:
564 else:
565 return False
565 return False
566
566
567 def stop(self):
567 def stop(self):
568 self._running = False
568 self._running = False
569 super(HBSocketChannel, self).stop()
569 super(HBSocketChannel, self).stop()
570
570
571 def call_handlers(self, since_last_heartbeat):
571 def call_handlers(self, since_last_heartbeat):
572 """This method is called in the ioloop thread when a message arrives.
572 """This method is called in the ioloop thread when a message arrives.
573
573
574 Subclasses should override this method to handle incoming messages.
574 Subclasses should override this method to handle incoming messages.
575 It is important to remember that this method is called in the thread
575 It is important to remember that this method is called in the thread
576 so that some logic must be done to ensure that the application leve
576 so that some logic must be done to ensure that the application leve
577 handlers are called in the application thread.
577 handlers are called in the application thread.
578 """
578 """
579 raise NotImplementedError('call_handlers must be defined in a subclass.')
579 raise NotImplementedError('call_handlers must be defined in a subclass.')
580
580
581
581
582 #-----------------------------------------------------------------------------
582 #-----------------------------------------------------------------------------
583 # Main kernel manager class
583 # Main kernel manager class
584 #-----------------------------------------------------------------------------
584 #-----------------------------------------------------------------------------
585
585
586 class KernelManager(HasTraits):
586 class KernelManager(HasTraits):
587 """ Manages a kernel for a frontend.
587 """ Manages a kernel for a frontend.
588
588
589 The SUB channel is for the frontend to receive messages published by the
589 The SUB channel is for the frontend to receive messages published by the
590 kernel.
590 kernel.
591
591
592 The REQ channel is for the frontend to make requests of the kernel.
592 The REQ channel is for the frontend to make requests of the kernel.
593
593
594 The REP channel is for the kernel to request stdin (raw_input) from the
594 The REP channel is for the kernel to request stdin (raw_input) from the
595 frontend.
595 frontend.
596 """
596 """
597 # config object for passing to child configurables
597 # config object for passing to child configurables
598 config = Instance(Config)
598 config = Instance(Config)
599
599
600 # The PyZMQ Context to use for communication with the kernel.
600 # The PyZMQ Context to use for communication with the kernel.
601 context = Instance(zmq.Context)
601 context = Instance(zmq.Context)
602 def _context_default(self):
602 def _context_default(self):
603 return zmq.Context.instance()
603 return zmq.Context.instance()
604
604
605 # The Session to use for communication with the kernel.
605 # The Session to use for communication with the kernel.
606 session = Instance(Session)
606 session = Instance(Session)
607
607
608 # The kernel process with which the KernelManager is communicating.
608 # The kernel process with which the KernelManager is communicating.
609 kernel = Instance(Popen)
609 kernel = Instance(Popen)
610
610
611 # The addresses for the communication channels.
611 # The addresses for the communication channels.
612 connection_file = Unicode('')
612 connection_file = Unicode('')
613 ip = Unicode(LOCALHOST)
613 ip = Unicode(LOCALHOST)
614 def _ip_changed(self, name, old, new):
614 def _ip_changed(self, name, old, new):
615 if new == '*':
615 if new == '*':
616 self.ip = '0.0.0.0'
616 self.ip = '0.0.0.0'
617 shell_port = Integer(0)
617 shell_port = Integer(0)
618 iopub_port = Integer(0)
618 iopub_port = Integer(0)
619 stdin_port = Integer(0)
619 stdin_port = Integer(0)
620 hb_port = Integer(0)
620 hb_port = Integer(0)
621
621
622 # The classes to use for the various channels.
622 # The classes to use for the various channels.
623 shell_channel_class = Type(ShellSocketChannel)
623 shell_channel_class = Type(ShellSocketChannel)
624 sub_channel_class = Type(SubSocketChannel)
624 sub_channel_class = Type(SubSocketChannel)
625 stdin_channel_class = Type(StdInSocketChannel)
625 stdin_channel_class = Type(StdInSocketChannel)
626 hb_channel_class = Type(HBSocketChannel)
626 hb_channel_class = Type(HBSocketChannel)
627
627
628 # Protected traits.
628 # Protected traits.
629 _launch_args = Any
629 _launch_args = Any
630 _shell_channel = Any
630 _shell_channel = Any
631 _sub_channel = Any
631 _sub_channel = Any
632 _stdin_channel = Any
632 _stdin_channel = Any
633 _hb_channel = Any
633 _hb_channel = Any
634 _connection_file_written=Bool(False)
634 _connection_file_written=Bool(False)
635
635
636 def __init__(self, **kwargs):
636 def __init__(self, **kwargs):
637 super(KernelManager, self).__init__(**kwargs)
637 super(KernelManager, self).__init__(**kwargs)
638 if self.session is None:
638 if self.session is None:
639 self.session = Session(config=self.config)
639 self.session = Session(config=self.config)
640
640
641 def __del__(self):
641 def __del__(self):
642 if self._connection_file_written:
642 self.cleanup_connection_file()
643 # cleanup connection files on full shutdown of kernel we started
643
644 self._connection_file_written = False
645 try:
646 os.remove(self.connection_file)
647 except IOError:
648 pass
649
650
644
651 #--------------------------------------------------------------------------
645 #--------------------------------------------------------------------------
652 # Channel management methods:
646 # Channel management methods:
653 #--------------------------------------------------------------------------
647 #--------------------------------------------------------------------------
654
648
655 def start_channels(self, shell=True, sub=True, stdin=True, hb=True):
649 def start_channels(self, shell=True, sub=True, stdin=True, hb=True):
656 """Starts the channels for this kernel.
650 """Starts the channels for this kernel.
657
651
658 This will create the channels if they do not exist and then start
652 This will create the channels if they do not exist and then start
659 them. If port numbers of 0 are being used (random ports) then you
653 them. If port numbers of 0 are being used (random ports) then you
660 must first call :method:`start_kernel`. If the channels have been
654 must first call :method:`start_kernel`. If the channels have been
661 stopped and you call this, :class:`RuntimeError` will be raised.
655 stopped and you call this, :class:`RuntimeError` will be raised.
662 """
656 """
663 if shell:
657 if shell:
664 self.shell_channel.start()
658 self.shell_channel.start()
665 if sub:
659 if sub:
666 self.sub_channel.start()
660 self.sub_channel.start()
667 if stdin:
661 if stdin:
668 self.stdin_channel.start()
662 self.stdin_channel.start()
669 self.shell_channel.allow_stdin = True
663 self.shell_channel.allow_stdin = True
670 else:
664 else:
671 self.shell_channel.allow_stdin = False
665 self.shell_channel.allow_stdin = False
672 if hb:
666 if hb:
673 self.hb_channel.start()
667 self.hb_channel.start()
674
668
675 def stop_channels(self):
669 def stop_channels(self):
676 """Stops all the running channels for this kernel.
670 """Stops all the running channels for this kernel.
677 """
671 """
678 if self.shell_channel.is_alive():
672 if self.shell_channel.is_alive():
679 self.shell_channel.stop()
673 self.shell_channel.stop()
680 if self.sub_channel.is_alive():
674 if self.sub_channel.is_alive():
681 self.sub_channel.stop()
675 self.sub_channel.stop()
682 if self.stdin_channel.is_alive():
676 if self.stdin_channel.is_alive():
683 self.stdin_channel.stop()
677 self.stdin_channel.stop()
684 if self.hb_channel.is_alive():
678 if self.hb_channel.is_alive():
685 self.hb_channel.stop()
679 self.hb_channel.stop()
686
680
687 @property
681 @property
688 def channels_running(self):
682 def channels_running(self):
689 """Are any of the channels created and running?"""
683 """Are any of the channels created and running?"""
690 return (self.shell_channel.is_alive() or self.sub_channel.is_alive() or
684 return (self.shell_channel.is_alive() or self.sub_channel.is_alive() or
691 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
685 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
692
686
693 #--------------------------------------------------------------------------
687 #--------------------------------------------------------------------------
694 # Kernel process management methods:
688 # Kernel process management methods:
695 #--------------------------------------------------------------------------
689 #--------------------------------------------------------------------------
696
690
691 def cleanup_connection_file(self):
692 """cleanup connection file *if we wrote it*
693
694 Will not raise if the connection file was already removed somehow.
695 """
696 if self._connection_file_written:
697 # cleanup connection files on full shutdown of kernel we started
698 self._connection_file_written = False
699 try:
700 os.remove(self.connection_file)
701 except OSError:
702 pass
703
697 def load_connection_file(self):
704 def load_connection_file(self):
698 """load connection info from JSON dict in self.connection_file"""
705 """load connection info from JSON dict in self.connection_file"""
699 with open(self.connection_file) as f:
706 with open(self.connection_file) as f:
700 cfg = json.loads(f.read())
707 cfg = json.loads(f.read())
701
708
702 self.ip = cfg['ip']
709 self.ip = cfg['ip']
703 self.shell_port = cfg['shell_port']
710 self.shell_port = cfg['shell_port']
704 self.stdin_port = cfg['stdin_port']
711 self.stdin_port = cfg['stdin_port']
705 self.iopub_port = cfg['iopub_port']
712 self.iopub_port = cfg['iopub_port']
706 self.hb_port = cfg['hb_port']
713 self.hb_port = cfg['hb_port']
707 self.session.key = str_to_bytes(cfg['key'])
714 self.session.key = str_to_bytes(cfg['key'])
708
715
709 def write_connection_file(self):
716 def write_connection_file(self):
710 """write connection info to JSON dict in self.connection_file"""
717 """write connection info to JSON dict in self.connection_file"""
711 if self._connection_file_written:
718 if self._connection_file_written:
712 return
719 return
713 self.connection_file,cfg = write_connection_file(self.connection_file,
720 self.connection_file,cfg = write_connection_file(self.connection_file,
714 ip=self.ip, key=self.session.key,
721 ip=self.ip, key=self.session.key,
715 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
722 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
716 shell_port=self.shell_port, hb_port=self.hb_port)
723 shell_port=self.shell_port, hb_port=self.hb_port)
717 # write_connection_file also sets default ports:
724 # write_connection_file also sets default ports:
718 self.shell_port = cfg['shell_port']
725 self.shell_port = cfg['shell_port']
719 self.stdin_port = cfg['stdin_port']
726 self.stdin_port = cfg['stdin_port']
720 self.iopub_port = cfg['iopub_port']
727 self.iopub_port = cfg['iopub_port']
721 self.hb_port = cfg['hb_port']
728 self.hb_port = cfg['hb_port']
722
729
723 self._connection_file_written = True
730 self._connection_file_written = True
724
731
725 def start_kernel(self, **kw):
732 def start_kernel(self, **kw):
726 """Starts a kernel process and configures the manager to use it.
733 """Starts a kernel process and configures the manager to use it.
727
734
728 If random ports (port=0) are being used, this method must be called
735 If random ports (port=0) are being used, this method must be called
729 before the channels are created.
736 before the channels are created.
730
737
731 Parameters:
738 Parameters:
732 -----------
739 -----------
733 ipython : bool, optional (default True)
740 ipython : bool, optional (default True)
734 Whether to use an IPython kernel instead of a plain Python kernel.
741 Whether to use an IPython kernel instead of a plain Python kernel.
735
742
736 launcher : callable, optional (default None)
743 launcher : callable, optional (default None)
737 A custom function for launching the kernel process (generally a
744 A custom function for launching the kernel process (generally a
738 wrapper around ``entry_point.base_launch_kernel``). In most cases,
745 wrapper around ``entry_point.base_launch_kernel``). In most cases,
739 it should not be necessary to use this parameter.
746 it should not be necessary to use this parameter.
740
747
741 **kw : optional
748 **kw : optional
742 See respective options for IPython and Python kernels.
749 See respective options for IPython and Python kernels.
743 """
750 """
744 if self.ip not in LOCAL_IPS:
751 if self.ip not in LOCAL_IPS:
745 raise RuntimeError("Can only launch a kernel on a local interface. "
752 raise RuntimeError("Can only launch a kernel on a local interface. "
746 "Make sure that the '*_address' attributes are "
753 "Make sure that the '*_address' attributes are "
747 "configured properly. "
754 "configured properly. "
748 "Currently valid addresses are: %s"%LOCAL_IPS
755 "Currently valid addresses are: %s"%LOCAL_IPS
749 )
756 )
750
757
751 # write connection file / get default ports
758 # write connection file / get default ports
752 self.write_connection_file()
759 self.write_connection_file()
753
760
754 self._launch_args = kw.copy()
761 self._launch_args = kw.copy()
755 launch_kernel = kw.pop('launcher', None)
762 launch_kernel = kw.pop('launcher', None)
756 if launch_kernel is None:
763 if launch_kernel is None:
757 if kw.pop('ipython', True):
764 if kw.pop('ipython', True):
758 from ipkernel import launch_kernel
765 from ipkernel import launch_kernel
759 else:
766 else:
760 from pykernel import launch_kernel
767 from pykernel import launch_kernel
761 self.kernel = launch_kernel(fname=self.connection_file, **kw)
768 self.kernel = launch_kernel(fname=self.connection_file, **kw)
762
769
763 def shutdown_kernel(self, restart=False):
770 def shutdown_kernel(self, restart=False):
764 """ Attempts to the stop the kernel process cleanly. If the kernel
771 """ Attempts to the stop the kernel process cleanly. If the kernel
765 cannot be stopped, it is killed, if possible.
772 cannot be stopped, it is killed, if possible.
766 """
773 """
767 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
774 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
768 if sys.platform == 'win32':
775 if sys.platform == 'win32':
769 self.kill_kernel()
776 self.kill_kernel()
770 return
777 return
771
778
772 # Pause the heart beat channel if it exists.
779 # Pause the heart beat channel if it exists.
773 if self._hb_channel is not None:
780 if self._hb_channel is not None:
774 self._hb_channel.pause()
781 self._hb_channel.pause()
775
782
776 # Don't send any additional kernel kill messages immediately, to give
783 # Don't send any additional kernel kill messages immediately, to give
777 # the kernel a chance to properly execute shutdown actions. Wait for at
784 # the kernel a chance to properly execute shutdown actions. Wait for at
778 # most 1s, checking every 0.1s.
785 # most 1s, checking every 0.1s.
779 self.shell_channel.shutdown(restart=restart)
786 self.shell_channel.shutdown(restart=restart)
780 for i in range(10):
787 for i in range(10):
781 if self.is_alive:
788 if self.is_alive:
782 time.sleep(0.1)
789 time.sleep(0.1)
783 else:
790 else:
784 break
791 break
785 else:
792 else:
786 # OK, we've waited long enough.
793 # OK, we've waited long enough.
787 if self.has_kernel:
794 if self.has_kernel:
788 self.kill_kernel()
795 self.kill_kernel()
789
796
790 if not restart and self._connection_file_written:
797 if not restart and self._connection_file_written:
791 # cleanup connection files on full shutdown of kernel we started
798 # cleanup connection files on full shutdown of kernel we started
792 self._connection_file_written = False
799 self._connection_file_written = False
793 try:
800 try:
794 os.remove(self.connection_file)
801 os.remove(self.connection_file)
795 except IOError:
802 except IOError:
796 pass
803 pass
797
804
798 def restart_kernel(self, now=False, **kw):
805 def restart_kernel(self, now=False, **kw):
799 """Restarts a kernel with the arguments that were used to launch it.
806 """Restarts a kernel with the arguments that were used to launch it.
800
807
801 If the old kernel was launched with random ports, the same ports will be
808 If the old kernel was launched with random ports, the same ports will be
802 used for the new kernel.
809 used for the new kernel.
803
810
804 Parameters
811 Parameters
805 ----------
812 ----------
806 now : bool, optional
813 now : bool, optional
807 If True, the kernel is forcefully restarted *immediately*, without
814 If True, the kernel is forcefully restarted *immediately*, without
808 having a chance to do any cleanup action. Otherwise the kernel is
815 having a chance to do any cleanup action. Otherwise the kernel is
809 given 1s to clean up before a forceful restart is issued.
816 given 1s to clean up before a forceful restart is issued.
810
817
811 In all cases the kernel is restarted, the only difference is whether
818 In all cases the kernel is restarted, the only difference is whether
812 it is given a chance to perform a clean shutdown or not.
819 it is given a chance to perform a clean shutdown or not.
813
820
814 **kw : optional
821 **kw : optional
815 Any options specified here will replace those used to launch the
822 Any options specified here will replace those used to launch the
816 kernel.
823 kernel.
817 """
824 """
818 if self._launch_args is None:
825 if self._launch_args is None:
819 raise RuntimeError("Cannot restart the kernel. "
826 raise RuntimeError("Cannot restart the kernel. "
820 "No previous call to 'start_kernel'.")
827 "No previous call to 'start_kernel'.")
821 else:
828 else:
822 # Stop currently running kernel.
829 # Stop currently running kernel.
823 if self.has_kernel:
830 if self.has_kernel:
824 if now:
831 if now:
825 self.kill_kernel()
832 self.kill_kernel()
826 else:
833 else:
827 self.shutdown_kernel(restart=True)
834 self.shutdown_kernel(restart=True)
828
835
829 # Start new kernel.
836 # Start new kernel.
830 self._launch_args.update(kw)
837 self._launch_args.update(kw)
831 self.start_kernel(**self._launch_args)
838 self.start_kernel(**self._launch_args)
832
839
833 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
840 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
834 # unless there is some delay here.
841 # unless there is some delay here.
835 if sys.platform == 'win32':
842 if sys.platform == 'win32':
836 time.sleep(0.2)
843 time.sleep(0.2)
837
844
838 @property
845 @property
839 def has_kernel(self):
846 def has_kernel(self):
840 """Returns whether a kernel process has been specified for the kernel
847 """Returns whether a kernel process has been specified for the kernel
841 manager.
848 manager.
842 """
849 """
843 return self.kernel is not None
850 return self.kernel is not None
844
851
845 def kill_kernel(self):
852 def kill_kernel(self):
846 """ Kill the running kernel. """
853 """ Kill the running kernel. """
847 if self.has_kernel:
854 if self.has_kernel:
848 # Pause the heart beat channel if it exists.
855 # Pause the heart beat channel if it exists.
849 if self._hb_channel is not None:
856 if self._hb_channel is not None:
850 self._hb_channel.pause()
857 self._hb_channel.pause()
851
858
852 # Attempt to kill the kernel.
859 # Attempt to kill the kernel.
853 try:
860 try:
854 self.kernel.kill()
861 self.kernel.kill()
855 except OSError, e:
862 except OSError, e:
856 # In Windows, we will get an Access Denied error if the process
863 # In Windows, we will get an Access Denied error if the process
857 # has already terminated. Ignore it.
864 # has already terminated. Ignore it.
858 if sys.platform == 'win32':
865 if sys.platform == 'win32':
859 if e.winerror != 5:
866 if e.winerror != 5:
860 raise
867 raise
861 # On Unix, we may get an ESRCH error if the process has already
868 # On Unix, we may get an ESRCH error if the process has already
862 # terminated. Ignore it.
869 # terminated. Ignore it.
863 else:
870 else:
864 from errno import ESRCH
871 from errno import ESRCH
865 if e.errno != ESRCH:
872 if e.errno != ESRCH:
866 raise
873 raise
867 self.kernel = None
874 self.kernel = None
868 else:
875 else:
869 raise RuntimeError("Cannot kill kernel. No kernel is running!")
876 raise RuntimeError("Cannot kill kernel. No kernel is running!")
870
877
871 def interrupt_kernel(self):
878 def interrupt_kernel(self):
872 """ Interrupts the kernel. Unlike ``signal_kernel``, this operation is
879 """ Interrupts the kernel. Unlike ``signal_kernel``, this operation is
873 well supported on all platforms.
880 well supported on all platforms.
874 """
881 """
875 if self.has_kernel:
882 if self.has_kernel:
876 if sys.platform == 'win32':
883 if sys.platform == 'win32':
877 from parentpoller import ParentPollerWindows as Poller
884 from parentpoller import ParentPollerWindows as Poller
878 Poller.send_interrupt(self.kernel.win32_interrupt_event)
885 Poller.send_interrupt(self.kernel.win32_interrupt_event)
879 else:
886 else:
880 self.kernel.send_signal(signal.SIGINT)
887 self.kernel.send_signal(signal.SIGINT)
881 else:
888 else:
882 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
889 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
883
890
884 def signal_kernel(self, signum):
891 def signal_kernel(self, signum):
885 """ Sends a signal to the kernel. Note that since only SIGTERM is
892 """ Sends a signal to the kernel. Note that since only SIGTERM is
886 supported on Windows, this function is only useful on Unix systems.
893 supported on Windows, this function is only useful on Unix systems.
887 """
894 """
888 if self.has_kernel:
895 if self.has_kernel:
889 self.kernel.send_signal(signum)
896 self.kernel.send_signal(signum)
890 else:
897 else:
891 raise RuntimeError("Cannot signal kernel. No kernel is running!")
898 raise RuntimeError("Cannot signal kernel. No kernel is running!")
892
899
893 @property
900 @property
894 def is_alive(self):
901 def is_alive(self):
895 """Is the kernel process still running?"""
902 """Is the kernel process still running?"""
896 # FIXME: not using a heartbeat means this method is broken for any
903 # FIXME: not using a heartbeat means this method is broken for any
897 # remote kernel, it's only capable of handling local kernels.
904 # remote kernel, it's only capable of handling local kernels.
898 if self.has_kernel:
905 if self.has_kernel:
899 if self.kernel.poll() is None:
906 if self.kernel.poll() is None:
900 return True
907 return True
901 else:
908 else:
902 return False
909 return False
903 else:
910 else:
904 # We didn't start the kernel with this KernelManager so we don't
911 # We didn't start the kernel with this KernelManager so we don't
905 # know if it is running. We should use a heartbeat for this case.
912 # know if it is running. We should use a heartbeat for this case.
906 return True
913 return True
907
914
908 #--------------------------------------------------------------------------
915 #--------------------------------------------------------------------------
909 # Channels used for communication with the kernel:
916 # Channels used for communication with the kernel:
910 #--------------------------------------------------------------------------
917 #--------------------------------------------------------------------------
911
918
912 @property
919 @property
913 def shell_channel(self):
920 def shell_channel(self):
914 """Get the REQ socket channel object to make requests of the kernel."""
921 """Get the REQ socket channel object to make requests of the kernel."""
915 if self._shell_channel is None:
922 if self._shell_channel is None:
916 self._shell_channel = self.shell_channel_class(self.context,
923 self._shell_channel = self.shell_channel_class(self.context,
917 self.session,
924 self.session,
918 (self.ip, self.shell_port))
925 (self.ip, self.shell_port))
919 return self._shell_channel
926 return self._shell_channel
920
927
921 @property
928 @property
922 def sub_channel(self):
929 def sub_channel(self):
923 """Get the SUB socket channel object."""
930 """Get the SUB socket channel object."""
924 if self._sub_channel is None:
931 if self._sub_channel is None:
925 self._sub_channel = self.sub_channel_class(self.context,
932 self._sub_channel = self.sub_channel_class(self.context,
926 self.session,
933 self.session,
927 (self.ip, self.iopub_port))
934 (self.ip, self.iopub_port))
928 return self._sub_channel
935 return self._sub_channel
929
936
930 @property
937 @property
931 def stdin_channel(self):
938 def stdin_channel(self):
932 """Get the REP socket channel object to handle stdin (raw_input)."""
939 """Get the REP socket channel object to handle stdin (raw_input)."""
933 if self._stdin_channel is None:
940 if self._stdin_channel is None:
934 self._stdin_channel = self.stdin_channel_class(self.context,
941 self._stdin_channel = self.stdin_channel_class(self.context,
935 self.session,
942 self.session,
936 (self.ip, self.stdin_port))
943 (self.ip, self.stdin_port))
937 return self._stdin_channel
944 return self._stdin_channel
938
945
939 @property
946 @property
940 def hb_channel(self):
947 def hb_channel(self):
941 """Get the heartbeat socket channel object to check that the
948 """Get the heartbeat socket channel object to check that the
942 kernel is alive."""
949 kernel is alive."""
943 if self._hb_channel is None:
950 if self._hb_channel is None:
944 self._hb_channel = self.hb_channel_class(self.context,
951 self._hb_channel = self.hb_channel_class(self.context,
945 self.session,
952 self.session,
946 (self.ip, self.hb_port))
953 (self.ip, self.hb_port))
947 return self._hb_channel
954 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now