##// END OF EJS Templates
ConnectionFileMixin is a LoggingConfigurable
MinRK -
Show More
@@ -1,201 +1,186 b''
1 """Base class to manage the interaction with a running kernel
2 """
1 """Base class to manage the interaction with a running kernel"""
3 2
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2013 The IPython Development Team
6 #
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
10
11 #-----------------------------------------------------------------------------
12 # Imports
13 #-----------------------------------------------------------------------------
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
14 5
15 6 from __future__ import absolute_import
16 7
17 8 import zmq
18 9
19 # Local imports
20 from IPython.config.configurable import LoggingConfigurable
21 10 from IPython.utils.traitlets import (
22 11 Any, Instance, Type,
23 12 )
24 13
25 14 from .zmq.session import Session
26 15 from .channels import (
27 16 ShellChannel, IOPubChannel,
28 17 HBChannel, StdInChannel,
29 18 )
30 19 from .clientabc import KernelClientABC
31 20 from .connect import ConnectionFileMixin
32 21
33 22
34 #-----------------------------------------------------------------------------
35 # Main kernel client class
36 #-----------------------------------------------------------------------------
37
38 class KernelClient(LoggingConfigurable, ConnectionFileMixin):
23 class KernelClient(ConnectionFileMixin):
39 24 """Communicates with a single kernel on any host via zmq channels.
40 25
41 26 There are four channels associated with each kernel:
42 27
43 28 * shell: for request/reply calls to the kernel.
44 29 * iopub: for the kernel to publish results to frontends.
45 30 * hb: for monitoring the kernel's heartbeat.
46 31 * stdin: for frontends to reply to raw_input calls in the kernel.
47 32
48 33 The methods of the channels are exposed as methods of the client itself
49 34 (KernelClient.execute, complete, history, etc.).
50 35 See the channels themselves for documentation of these methods.
51 36
52 37 """
53 38
54 39 # The PyZMQ Context to use for communication with the kernel.
55 40 context = Instance(zmq.Context)
56 41 def _context_default(self):
57 42 return zmq.Context.instance()
58 43
59 44 # The classes to use for the various channels
60 45 shell_channel_class = Type(ShellChannel)
61 46 iopub_channel_class = Type(IOPubChannel)
62 47 stdin_channel_class = Type(StdInChannel)
63 48 hb_channel_class = Type(HBChannel)
64 49
65 50 # Protected traits
66 51 _shell_channel = Any
67 52 _iopub_channel = Any
68 53 _stdin_channel = Any
69 54 _hb_channel = Any
70 55
71 56 #--------------------------------------------------------------------------
72 57 # Channel proxy methods
73 58 #--------------------------------------------------------------------------
74 59
75 60 def _get_msg(channel, *args, **kwargs):
76 61 return channel.get_msg(*args, **kwargs)
77 62
78 63 def get_shell_msg(self, *args, **kwargs):
79 64 """Get a message from the shell channel"""
80 65 return self.shell_channel.get_msg(*args, **kwargs)
81 66
82 67 def get_iopub_msg(self, *args, **kwargs):
83 68 """Get a message from the iopub channel"""
84 69 return self.iopub_channel.get_msg(*args, **kwargs)
85 70
86 71 def get_stdin_msg(self, *args, **kwargs):
87 72 """Get a message from the stdin channel"""
88 73 return self.stdin_channel.get_msg(*args, **kwargs)
89 74
90 75 #--------------------------------------------------------------------------
91 76 # Channel management methods
92 77 #--------------------------------------------------------------------------
93 78
94 79 def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
95 80 """Starts the channels for this kernel.
96 81
97 82 This will create the channels if they do not exist and then start
98 83 them (their activity runs in a thread). If port numbers of 0 are
99 84 being used (random ports) then you must first call
100 85 :meth:`start_kernel`. If the channels have been stopped and you
101 86 call this, :class:`RuntimeError` will be raised.
102 87 """
103 88 if shell:
104 89 self.shell_channel.start()
105 90 for method in self.shell_channel.proxy_methods:
106 91 setattr(self, method, getattr(self.shell_channel, method))
107 92 if iopub:
108 93 self.iopub_channel.start()
109 94 for method in self.iopub_channel.proxy_methods:
110 95 setattr(self, method, getattr(self.iopub_channel, method))
111 96 if stdin:
112 97 self.stdin_channel.start()
113 98 for method in self.stdin_channel.proxy_methods:
114 99 setattr(self, method, getattr(self.stdin_channel, method))
115 100 self.shell_channel.allow_stdin = True
116 101 else:
117 102 self.shell_channel.allow_stdin = False
118 103 if hb:
119 104 self.hb_channel.start()
120 105
121 106 def stop_channels(self):
122 107 """Stops all the running channels for this kernel.
123 108
124 109 This stops their event loops and joins their threads.
125 110 """
126 111 if self.shell_channel.is_alive():
127 112 self.shell_channel.stop()
128 113 if self.iopub_channel.is_alive():
129 114 self.iopub_channel.stop()
130 115 if self.stdin_channel.is_alive():
131 116 self.stdin_channel.stop()
132 117 if self.hb_channel.is_alive():
133 118 self.hb_channel.stop()
134 119
135 120 @property
136 121 def channels_running(self):
137 122 """Are any of the channels created and running?"""
138 123 return (self.shell_channel.is_alive() or self.iopub_channel.is_alive() or
139 124 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
140 125
141 126 @property
142 127 def shell_channel(self):
143 128 """Get the shell channel object for this kernel."""
144 129 if self._shell_channel is None:
145 130 url = self._make_url('shell')
146 131 self.log.debug("connecting shell channel to %s", url)
147 132 self._shell_channel = self.shell_channel_class(
148 133 self.context, self.session, url
149 134 )
150 135 return self._shell_channel
151 136
152 137 @property
153 138 def iopub_channel(self):
154 139 """Get the iopub channel object for this kernel."""
155 140 if self._iopub_channel is None:
156 141 url = self._make_url('iopub')
157 142 self.log.debug("connecting iopub channel to %s", url)
158 143 self._iopub_channel = self.iopub_channel_class(
159 144 self.context, self.session, url
160 145 )
161 146 return self._iopub_channel
162 147
163 148 @property
164 149 def stdin_channel(self):
165 150 """Get the stdin channel object for this kernel."""
166 151 if self._stdin_channel is None:
167 152 url = self._make_url('stdin')
168 153 self.log.debug("connecting stdin channel to %s", url)
169 154 self._stdin_channel = self.stdin_channel_class(
170 155 self.context, self.session, url
171 156 )
172 157 return self._stdin_channel
173 158
174 159 @property
175 160 def hb_channel(self):
176 161 """Get the hb channel object for this kernel."""
177 162 if self._hb_channel is None:
178 163 url = self._make_url('hb')
179 164 self.log.debug("connecting heartbeat channel to %s", url)
180 165 self._hb_channel = self.hb_channel_class(
181 166 self.context, self.session, url
182 167 )
183 168 return self._hb_channel
184 169
185 170 def is_alive(self):
186 171 """Is the kernel process still running?"""
187 172 if self._hb_channel is not None:
188 173 # We didn't start the kernel with this KernelManager so we
189 174 # use the heartbeat.
190 175 return self._hb_channel.is_beating()
191 176 else:
192 177 # no heartbeat and not local, we can't tell if it's running,
193 178 # so naively return True
194 179 return True
195 180
196 181
197 182 #-----------------------------------------------------------------------------
198 183 # ABC Registration
199 184 #-----------------------------------------------------------------------------
200 185
201 186 KernelClientABC.register(KernelClient)
@@ -1,578 +1,578 b''
1 1 """Utilities for connecting to kernels
2 2
3 3 The :class:`ConnectionFileMixin` class in this module encapsulates the logic
4 4 related to writing and reading connections files.
5 5 """
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 from __future__ import absolute_import
14 14
15 15 import glob
16 16 import json
17 17 import os
18 18 import socket
19 19 import sys
20 20 from getpass import getpass
21 21 from subprocess import Popen, PIPE
22 22 import tempfile
23 23
24 24 import zmq
25 25
26 26 # external imports
27 27 from IPython.external.ssh import tunnel
28 28
29 29 # IPython imports
30 from IPython.config import Configurable
30 from IPython.config import LoggingConfigurable
31 31 from IPython.core.profiledir import ProfileDir
32 32 from IPython.utils.localinterfaces import localhost
33 33 from IPython.utils.path import filefind, get_ipython_dir
34 34 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
35 35 string_types)
36 36 from IPython.utils.traitlets import (
37 37 Bool, Integer, Unicode, CaselessStrEnum, Instance,
38 38 )
39 39
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Working with Connection Files
43 43 #-----------------------------------------------------------------------------
44 44
45 45 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
46 46 control_port=0, ip='', key=b'', transport='tcp',
47 47 signature_scheme='hmac-sha256',
48 48 ):
49 49 """Generates a JSON config file, including the selection of random ports.
50 50
51 51 Parameters
52 52 ----------
53 53
54 54 fname : unicode
55 55 The path to the file to write
56 56
57 57 shell_port : int, optional
58 58 The port to use for ROUTER (shell) channel.
59 59
60 60 iopub_port : int, optional
61 61 The port to use for the SUB channel.
62 62
63 63 stdin_port : int, optional
64 64 The port to use for the ROUTER (raw input) channel.
65 65
66 66 control_port : int, optional
67 67 The port to use for the ROUTER (control) channel.
68 68
69 69 hb_port : int, optional
70 70 The port to use for the heartbeat REP channel.
71 71
72 72 ip : str, optional
73 73 The ip address the kernel will bind to.
74 74
75 75 key : str, optional
76 76 The Session key used for message authentication.
77 77
78 78 signature_scheme : str, optional
79 79 The scheme used for message authentication.
80 80 This has the form 'digest-hash', where 'digest'
81 81 is the scheme used for digests, and 'hash' is the name of the hash function
82 82 used by the digest scheme.
83 83 Currently, 'hmac' is the only supported digest scheme,
84 84 and 'sha256' is the default hash function.
85 85
86 86 """
87 87 if not ip:
88 88 ip = localhost()
89 89 # default to temporary connector file
90 90 if not fname:
91 91 fd, fname = tempfile.mkstemp('.json')
92 92 os.close(fd)
93 93
94 94 # Find open ports as necessary.
95 95
96 96 ports = []
97 97 ports_needed = int(shell_port <= 0) + \
98 98 int(iopub_port <= 0) + \
99 99 int(stdin_port <= 0) + \
100 100 int(control_port <= 0) + \
101 101 int(hb_port <= 0)
102 102 if transport == 'tcp':
103 103 for i in range(ports_needed):
104 104 sock = socket.socket()
105 105 # struct.pack('ii', (0,0)) is 8 null bytes
106 106 sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8)
107 107 sock.bind(('', 0))
108 108 ports.append(sock)
109 109 for i, sock in enumerate(ports):
110 110 port = sock.getsockname()[1]
111 111 sock.close()
112 112 ports[i] = port
113 113 else:
114 114 N = 1
115 115 for i in range(ports_needed):
116 116 while os.path.exists("%s-%s" % (ip, str(N))):
117 117 N += 1
118 118 ports.append(N)
119 119 N += 1
120 120 if shell_port <= 0:
121 121 shell_port = ports.pop(0)
122 122 if iopub_port <= 0:
123 123 iopub_port = ports.pop(0)
124 124 if stdin_port <= 0:
125 125 stdin_port = ports.pop(0)
126 126 if control_port <= 0:
127 127 control_port = ports.pop(0)
128 128 if hb_port <= 0:
129 129 hb_port = ports.pop(0)
130 130
131 131 cfg = dict( shell_port=shell_port,
132 132 iopub_port=iopub_port,
133 133 stdin_port=stdin_port,
134 134 control_port=control_port,
135 135 hb_port=hb_port,
136 136 )
137 137 cfg['ip'] = ip
138 138 cfg['key'] = bytes_to_str(key)
139 139 cfg['transport'] = transport
140 140 cfg['signature_scheme'] = signature_scheme
141 141
142 142 with open(fname, 'w') as f:
143 143 f.write(json.dumps(cfg, indent=2))
144 144
145 145 return fname, cfg
146 146
147 147
148 148 def get_connection_file(app=None):
149 149 """Return the path to the connection file of an app
150 150
151 151 Parameters
152 152 ----------
153 153 app : IPKernelApp instance [optional]
154 154 If unspecified, the currently running app will be used
155 155 """
156 156 if app is None:
157 157 from IPython.kernel.zmq.kernelapp import IPKernelApp
158 158 if not IPKernelApp.initialized():
159 159 raise RuntimeError("app not specified, and not in a running Kernel")
160 160
161 161 app = IPKernelApp.instance()
162 162 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
163 163
164 164
165 165 def find_connection_file(filename, profile=None):
166 166 """find a connection file, and return its absolute path.
167 167
168 168 The current working directory and the profile's security
169 169 directory will be searched for the file if it is not given by
170 170 absolute path.
171 171
172 172 If profile is unspecified, then the current running application's
173 173 profile will be used, or 'default', if not run from IPython.
174 174
175 175 If the argument does not match an existing file, it will be interpreted as a
176 176 fileglob, and the matching file in the profile's security dir with
177 177 the latest access time will be used.
178 178
179 179 Parameters
180 180 ----------
181 181 filename : str
182 182 The connection file or fileglob to search for.
183 183 profile : str [optional]
184 184 The name of the profile to use when searching for the connection file,
185 185 if different from the current IPython session or 'default'.
186 186
187 187 Returns
188 188 -------
189 189 str : The absolute path of the connection file.
190 190 """
191 191 from IPython.core.application import BaseIPythonApplication as IPApp
192 192 try:
193 193 # quick check for absolute path, before going through logic
194 194 return filefind(filename)
195 195 except IOError:
196 196 pass
197 197
198 198 if profile is None:
199 199 # profile unspecified, check if running from an IPython app
200 200 if IPApp.initialized():
201 201 app = IPApp.instance()
202 202 profile_dir = app.profile_dir
203 203 else:
204 204 # not running in IPython, use default profile
205 205 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
206 206 else:
207 207 # find profiledir by profile name:
208 208 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
209 209 security_dir = profile_dir.security_dir
210 210
211 211 try:
212 212 # first, try explicit name
213 213 return filefind(filename, ['.', security_dir])
214 214 except IOError:
215 215 pass
216 216
217 217 # not found by full name
218 218
219 219 if '*' in filename:
220 220 # given as a glob already
221 221 pat = filename
222 222 else:
223 223 # accept any substring match
224 224 pat = '*%s*' % filename
225 225 matches = glob.glob( os.path.join(security_dir, pat) )
226 226 if not matches:
227 227 raise IOError("Could not find %r in %r" % (filename, security_dir))
228 228 elif len(matches) == 1:
229 229 return matches[0]
230 230 else:
231 231 # get most recent match, by access time:
232 232 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
233 233
234 234
235 235 def get_connection_info(connection_file=None, unpack=False, profile=None):
236 236 """Return the connection information for the current Kernel.
237 237
238 238 Parameters
239 239 ----------
240 240 connection_file : str [optional]
241 241 The connection file to be used. Can be given by absolute path, or
242 242 IPython will search in the security directory of a given profile.
243 243 If run from IPython,
244 244
245 245 If unspecified, the connection file for the currently running
246 246 IPython Kernel will be used, which is only allowed from inside a kernel.
247 247 unpack : bool [default: False]
248 248 if True, return the unpacked dict, otherwise just the string contents
249 249 of the file.
250 250 profile : str [optional]
251 251 The name of the profile to use when searching for the connection file,
252 252 if different from the current IPython session or 'default'.
253 253
254 254
255 255 Returns
256 256 -------
257 257 The connection dictionary of the current kernel, as string or dict,
258 258 depending on `unpack`.
259 259 """
260 260 if connection_file is None:
261 261 # get connection file from current kernel
262 262 cf = get_connection_file()
263 263 else:
264 264 # connection file specified, allow shortnames:
265 265 cf = find_connection_file(connection_file, profile=profile)
266 266
267 267 with open(cf) as f:
268 268 info = f.read()
269 269
270 270 if unpack:
271 271 info = json.loads(info)
272 272 # ensure key is bytes:
273 273 info['key'] = str_to_bytes(info.get('key', ''))
274 274 return info
275 275
276 276
277 277 def connect_qtconsole(connection_file=None, argv=None, profile=None):
278 278 """Connect a qtconsole to the current kernel.
279 279
280 280 This is useful for connecting a second qtconsole to a kernel, or to a
281 281 local notebook.
282 282
283 283 Parameters
284 284 ----------
285 285 connection_file : str [optional]
286 286 The connection file to be used. Can be given by absolute path, or
287 287 IPython will search in the security directory of a given profile.
288 288 If run from IPython,
289 289
290 290 If unspecified, the connection file for the currently running
291 291 IPython Kernel will be used, which is only allowed from inside a kernel.
292 292 argv : list [optional]
293 293 Any extra args to be passed to the console.
294 294 profile : str [optional]
295 295 The name of the profile to use when searching for the connection file,
296 296 if different from the current IPython session or 'default'.
297 297
298 298
299 299 Returns
300 300 -------
301 301 :class:`subprocess.Popen` instance running the qtconsole frontend
302 302 """
303 303 argv = [] if argv is None else argv
304 304
305 305 if connection_file is None:
306 306 # get connection file from current kernel
307 307 cf = get_connection_file()
308 308 else:
309 309 cf = find_connection_file(connection_file, profile=profile)
310 310
311 311 cmd = ';'.join([
312 312 "from IPython.qt.console import qtconsoleapp",
313 313 "qtconsoleapp.main()"
314 314 ])
315 315
316 316 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
317 317 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
318 318 )
319 319
320 320
321 321 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
322 322 """tunnel connections to a kernel via ssh
323 323
324 324 This will open four SSH tunnels from localhost on this machine to the
325 325 ports associated with the kernel. They can be either direct
326 326 localhost-localhost tunnels, or if an intermediate server is necessary,
327 327 the kernel must be listening on a public IP.
328 328
329 329 Parameters
330 330 ----------
331 331 connection_info : dict or str (path)
332 332 Either a connection dict, or the path to a JSON connection file
333 333 sshserver : str
334 334 The ssh sever to use to tunnel to the kernel. Can be a full
335 335 `user@server:port` string. ssh config aliases are respected.
336 336 sshkey : str [optional]
337 337 Path to file containing ssh key to use for authentication.
338 338 Only necessary if your ssh config does not already associate
339 339 a keyfile with the host.
340 340
341 341 Returns
342 342 -------
343 343
344 344 (shell, iopub, stdin, hb) : ints
345 345 The four ports on localhost that have been forwarded to the kernel.
346 346 """
347 347 if isinstance(connection_info, string_types):
348 348 # it's a path, unpack it
349 349 with open(connection_info) as f:
350 350 connection_info = json.loads(f.read())
351 351
352 352 cf = connection_info
353 353
354 354 lports = tunnel.select_random_ports(4)
355 355 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
356 356
357 357 remote_ip = cf['ip']
358 358
359 359 if tunnel.try_passwordless_ssh(sshserver, sshkey):
360 360 password=False
361 361 else:
362 362 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
363 363
364 364 for lp,rp in zip(lports, rports):
365 365 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
366 366
367 367 return tuple(lports)
368 368
369 369
370 370 #-----------------------------------------------------------------------------
371 371 # Mixin for classes that work with connection files
372 372 #-----------------------------------------------------------------------------
373 373
374 374 channel_socket_types = {
375 375 'hb' : zmq.REQ,
376 376 'shell' : zmq.DEALER,
377 377 'iopub' : zmq.SUB,
378 378 'stdin' : zmq.DEALER,
379 379 'control': zmq.DEALER,
380 380 }
381 381
382 382 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
383 383
384 class ConnectionFileMixin(Configurable):
384 class ConnectionFileMixin(LoggingConfigurable):
385 385 """Mixin for configurable classes that work with connection files"""
386 386
387 387 # The addresses for the communication channels
388 388 connection_file = Unicode('', config=True,
389 389 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
390 390
391 391 This file will contain the IP, ports, and authentication key needed to connect
392 392 clients to this kernel. By default, this file will be created in the security dir
393 393 of the current profile, but can be specified by absolute path.
394 394 """)
395 395 _connection_file_written = Bool(False)
396 396
397 397 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
398 398
399 399 ip = Unicode(config=True,
400 400 help="""Set the kernel\'s IP address [default localhost].
401 401 If the IP address is something other than localhost, then
402 402 Consoles on other machines will be able to connect
403 403 to the Kernel, so be careful!"""
404 404 )
405 405
406 406 def _ip_default(self):
407 407 if self.transport == 'ipc':
408 408 if self.connection_file:
409 409 return os.path.splitext(self.connection_file)[0] + '-ipc'
410 410 else:
411 411 return 'kernel-ipc'
412 412 else:
413 413 return localhost()
414 414
415 415 def _ip_changed(self, name, old, new):
416 416 if new == '*':
417 417 self.ip = '0.0.0.0'
418 418
419 419 # protected traits
420 420
421 421 hb_port = Integer(0, config=True,
422 422 help="set the heartbeat port [default: random]")
423 423 shell_port = Integer(0, config=True,
424 424 help="set the shell (ROUTER) port [default: random]")
425 425 iopub_port = Integer(0, config=True,
426 426 help="set the iopub (PUB) port [default: random]")
427 427 stdin_port = Integer(0, config=True,
428 428 help="set the stdin (ROUTER) port [default: random]")
429 429 control_port = Integer(0, config=True,
430 430 help="set the control (ROUTER) port [default: random]")
431 431
432 432 @property
433 433 def ports(self):
434 434 return [ getattr(self, name) for name in port_names ]
435 435
436 436 # The Session to use for communication with the kernel.
437 437 session = Instance('IPython.kernel.zmq.session.Session')
438 438 def _session_default(self):
439 439 from IPython.kernel.zmq.session import Session
440 440 return Session(parent=self)
441 441
442 442 #--------------------------------------------------------------------------
443 443 # Connection and ipc file management
444 444 #--------------------------------------------------------------------------
445 445
446 446 def get_connection_info(self):
447 447 """return the connection info as a dict"""
448 448 return dict(
449 449 transport=self.transport,
450 450 ip=self.ip,
451 451 shell_port=self.shell_port,
452 452 iopub_port=self.iopub_port,
453 453 stdin_port=self.stdin_port,
454 454 hb_port=self.hb_port,
455 455 control_port=self.control_port,
456 456 signature_scheme=self.session.signature_scheme,
457 457 key=self.session.key,
458 458 )
459 459
460 460 def cleanup_connection_file(self):
461 461 """Cleanup connection file *if we wrote it*
462 462
463 463 Will not raise if the connection file was already removed somehow.
464 464 """
465 465 if self._connection_file_written:
466 466 # cleanup connection files on full shutdown of kernel we started
467 467 self._connection_file_written = False
468 468 try:
469 469 os.remove(self.connection_file)
470 470 except (IOError, OSError, AttributeError):
471 471 pass
472 472
473 473 def cleanup_ipc_files(self):
474 474 """Cleanup ipc files if we wrote them."""
475 475 if self.transport != 'ipc':
476 476 return
477 477 for port in self.ports:
478 478 ipcfile = "%s-%i" % (self.ip, port)
479 479 try:
480 480 os.remove(ipcfile)
481 481 except (IOError, OSError):
482 482 pass
483 483
484 484 def write_connection_file(self):
485 485 """Write connection info to JSON dict in self.connection_file."""
486 486 if self._connection_file_written and os.path.exists(self.connection_file):
487 487 return
488 488
489 489 self.connection_file, cfg = write_connection_file(self.connection_file,
490 490 transport=self.transport, ip=self.ip, key=self.session.key,
491 491 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
492 492 shell_port=self.shell_port, hb_port=self.hb_port,
493 493 control_port=self.control_port,
494 494 signature_scheme=self.session.signature_scheme,
495 495 )
496 496 # write_connection_file also sets default ports:
497 497 for name in port_names:
498 498 setattr(self, name, cfg[name])
499 499
500 500 self._connection_file_written = True
501 501
502 502 def load_connection_file(self):
503 503 """Load connection info from JSON dict in self.connection_file."""
504 504 self.log.debug(u"Loading connection file %s", self.connection_file)
505 505 with open(self.connection_file) as f:
506 506 cfg = json.load(f)
507 507 self.transport = cfg.get('transport', self.transport)
508 508 self.ip = cfg.get('ip', self._ip_default())
509 509
510 510 for name in port_names:
511 511 if getattr(self, name) == 0 and name in cfg:
512 512 # not overridden by config or cl_args
513 513 setattr(self, name, cfg[name])
514 514
515 515 if 'key' in cfg:
516 516 self.session.key = str_to_bytes(cfg['key'])
517 517 if 'signature_scheme' in cfg:
518 518 self.session.signature_scheme = cfg['signature_scheme']
519 519
520 520 #--------------------------------------------------------------------------
521 521 # Creating connected sockets
522 522 #--------------------------------------------------------------------------
523 523
524 524 def _make_url(self, channel):
525 525 """Make a ZeroMQ URL for a given channel."""
526 526 transport = self.transport
527 527 ip = self.ip
528 528 port = getattr(self, '%s_port' % channel)
529 529
530 530 if transport == 'tcp':
531 531 return "tcp://%s:%i" % (ip, port)
532 532 else:
533 533 return "%s://%s-%s" % (transport, ip, port)
534 534
535 535 def _create_connected_socket(self, channel, identity=None):
536 536 """Create a zmq Socket and connect it to the kernel."""
537 537 url = self._make_url(channel)
538 538 socket_type = channel_socket_types[channel]
539 539 self.log.debug("Connecting to: %s" % url)
540 540 sock = self.context.socket(socket_type)
541 541 # set linger to 1s to prevent hangs at exit
542 542 sock.linger = 1000
543 543 if identity:
544 544 sock.identity = identity
545 545 sock.connect(url)
546 546 return sock
547 547
548 548 def connect_iopub(self, identity=None):
549 549 """return zmq Socket connected to the IOPub channel"""
550 550 sock = self._create_connected_socket('iopub', identity=identity)
551 551 sock.setsockopt(zmq.SUBSCRIBE, b'')
552 552 return sock
553 553
554 554 def connect_shell(self, identity=None):
555 555 """return zmq Socket connected to the Shell channel"""
556 556 return self._create_connected_socket('shell', identity=identity)
557 557
558 558 def connect_stdin(self, identity=None):
559 559 """return zmq Socket connected to the StdIn channel"""
560 560 return self._create_connected_socket('stdin', identity=identity)
561 561
562 562 def connect_hb(self, identity=None):
563 563 """return zmq Socket connected to the Heartbeat channel"""
564 564 return self._create_connected_socket('hb', identity=identity)
565 565
566 566 def connect_control(self, identity=None):
567 567 """return zmq Socket connected to the Heartbeat channel"""
568 568 return self._create_connected_socket('control', identity=identity)
569 569
570 570
571 571 __all__ = [
572 572 'write_connection_file',
573 573 'get_connection_file',
574 574 'find_connection_file',
575 575 'get_connection_info',
576 576 'connect_qtconsole',
577 577 'tunnel_to_kernel',
578 578 ]
@@ -1,415 +1,412 b''
1 1 """Base class to manage a running kernel"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import absolute_import
7 7
8 # Standard library imports
9 8 import os
10 9 import re
11 10 import signal
12 11 import sys
13 12 import time
14 13 import warnings
15 14
16 15 import zmq
17 16
18 # Local imports
19 from IPython.config.configurable import LoggingConfigurable
20 17 from IPython.utils.importstring import import_item
21 18 from IPython.utils.localinterfaces import is_local_ip, local_ips
22 19 from IPython.utils.path import get_ipython_dir
23 20 from IPython.utils.traitlets import (
24 21 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
25 22 )
26 23 from IPython.kernel import (
27 24 make_ipkernel_cmd,
28 25 launch_kernel,
29 26 kernelspec,
30 27 )
31 28 from .connect import ConnectionFileMixin
32 29 from .zmq.session import Session
33 30 from .managerabc import (
34 31 KernelManagerABC
35 32 )
36 33
37 34
38 class KernelManager(LoggingConfigurable, ConnectionFileMixin):
35 class KernelManager(ConnectionFileMixin):
39 36 """Manages a single kernel in a subprocess on this host.
40 37
41 38 This version starts kernels with Popen.
42 39 """
43 40
44 41 # The PyZMQ Context to use for communication with the kernel.
45 42 context = Instance(zmq.Context)
46 43 def _context_default(self):
47 44 return zmq.Context.instance()
48 45
49 46 # the class to create with our `client` method
50 47 client_class = DottedObjectName('IPython.kernel.blocking.BlockingKernelClient')
51 48 client_factory = Type()
52 49 def _client_class_changed(self, name, old, new):
53 50 self.client_factory = import_item(str(new))
54 51
55 52 # The kernel process with which the KernelManager is communicating.
56 53 # generally a Popen instance
57 54 kernel = Any()
58 55
59 56 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
60 57
61 58 def _kernel_spec_manager_default(self):
62 59 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
63 60
64 61 kernel_name = Unicode('python')
65 62
66 63 kernel_spec = Instance(kernelspec.KernelSpec)
67 64
68 65 def _kernel_spec_default(self):
69 66 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
70 67
71 68 def _kernel_name_changed(self, name, old, new):
72 69 self.kernel_spec = self.kernel_spec_manager.get_kernel_spec(new)
73 70 self.ipython_kernel = new in {'python', 'python2', 'python3'}
74 71
75 72 kernel_cmd = List(Unicode, config=True,
76 73 help="""DEPRECATED: Use kernel_name instead.
77 74
78 75 The Popen Command to launch the kernel.
79 76 Override this if you have a custom kernel.
80 77 If kernel_cmd is specified in a configuration file,
81 78 IPython does not pass any arguments to the kernel,
82 79 because it cannot make any assumptions about the
83 80 arguments that the kernel understands. In particular,
84 81 this means that the kernel does not receive the
85 82 option --debug if it given on the IPython command line.
86 83 """
87 84 )
88 85
89 86 def _kernel_cmd_changed(self, name, old, new):
90 87 warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
91 88 "start different kernels.")
92 89 self.ipython_kernel = False
93 90
94 91 ipython_kernel = Bool(True)
95 92
96 93 ipython_dir = Unicode()
97 94 def _ipython_dir_default(self):
98 95 return get_ipython_dir()
99 96
100 97 # Protected traits
101 98 _launch_args = Any()
102 99 _control_socket = Any()
103 100
104 101 _restarter = Any()
105 102
106 103 autorestart = Bool(False, config=True,
107 104 help="""Should we autorestart the kernel if it dies."""
108 105 )
109 106
110 107 def __del__(self):
111 108 self._close_control_socket()
112 109 self.cleanup_connection_file()
113 110
114 111 #--------------------------------------------------------------------------
115 112 # Kernel restarter
116 113 #--------------------------------------------------------------------------
117 114
118 115 def start_restarter(self):
119 116 pass
120 117
121 118 def stop_restarter(self):
122 119 pass
123 120
124 121 def add_restart_callback(self, callback, event='restart'):
125 122 """register a callback to be called when a kernel is restarted"""
126 123 if self._restarter is None:
127 124 return
128 125 self._restarter.add_callback(callback, event)
129 126
130 127 def remove_restart_callback(self, callback, event='restart'):
131 128 """unregister a callback to be called when a kernel is restarted"""
132 129 if self._restarter is None:
133 130 return
134 131 self._restarter.remove_callback(callback, event)
135 132
136 133 #--------------------------------------------------------------------------
137 134 # create a Client connected to our Kernel
138 135 #--------------------------------------------------------------------------
139 136
140 137 def client(self, **kwargs):
141 138 """Create a client configured to connect to our kernel"""
142 139 if self.client_factory is None:
143 140 self.client_factory = import_item(self.client_class)
144 141
145 142 kw = {}
146 143 kw.update(self.get_connection_info())
147 144 kw.update(dict(
148 145 connection_file=self.connection_file,
149 146 session=self.session,
150 147 parent=self,
151 148 ))
152 149
153 150 # add kwargs last, for manual overrides
154 151 kw.update(kwargs)
155 152 return self.client_factory(**kw)
156 153
157 154 #--------------------------------------------------------------------------
158 155 # Kernel management
159 156 #--------------------------------------------------------------------------
160 157
161 158 def format_kernel_cmd(self, **kw):
162 159 """replace templated args (e.g. {connection_file})"""
163 160 if self.kernel_cmd:
164 161 cmd = self.kernel_cmd
165 162 elif self.kernel_name == 'python':
166 163 # The native kernel gets special handling
167 164 cmd = make_ipkernel_cmd(
168 165 'from IPython.kernel.zmq.kernelapp import main; main()',
169 166 **kw
170 167 )
171 168 else:
172 169 cmd = self.kernel_spec.argv
173 170
174 171 ns = dict(connection_file=self.connection_file)
175 172 ns.update(self._launch_args)
176 173
177 174 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
178 175 def from_ns(match):
179 176 """Get the key out of ns if it's there, otherwise no change."""
180 177 return ns.get(match.group(1), match.group())
181 178
182 179 return [ pat.sub(from_ns, arg) for arg in cmd ]
183 180
184 181 def _launch_kernel(self, kernel_cmd, **kw):
185 182 """actually launch the kernel
186 183
187 184 override in a subclass to launch kernel subprocesses differently
188 185 """
189 186 return launch_kernel(kernel_cmd, **kw)
190 187
191 188 # Control socket used for polite kernel shutdown
192 189
193 190 def _connect_control_socket(self):
194 191 if self._control_socket is None:
195 192 self._control_socket = self.connect_control()
196 193 self._control_socket.linger = 100
197 194
198 195 def _close_control_socket(self):
199 196 if self._control_socket is None:
200 197 return
201 198 self._control_socket.close()
202 199 self._control_socket = None
203 200
204 201 def start_kernel(self, **kw):
205 202 """Starts a kernel on this host in a separate process.
206 203
207 204 If random ports (port=0) are being used, this method must be called
208 205 before the channels are created.
209 206
210 207 Parameters
211 208 ----------
212 209 **kw : optional
213 210 keyword arguments that are passed down to build the kernel_cmd
214 211 and launching the kernel (e.g. Popen kwargs).
215 212 """
216 213 if self.transport == 'tcp' and not is_local_ip(self.ip):
217 214 raise RuntimeError("Can only launch a kernel on a local interface. "
218 215 "Make sure that the '*_address' attributes are "
219 216 "configured properly. "
220 217 "Currently valid addresses are: %s" % local_ips()
221 218 )
222 219
223 220 # write connection file / get default ports
224 221 self.write_connection_file()
225 222
226 223 # save kwargs for use in restart
227 224 self._launch_args = kw.copy()
228 225 # build the Popen cmd
229 226 kernel_cmd = self.format_kernel_cmd(**kw)
230 227 if self.kernel_cmd:
231 228 # If kernel_cmd has been set manually, don't refer to a kernel spec
232 229 env = os.environ
233 230 else:
234 231 # Environment variables from kernel spec are added to os.environ
235 232 env = os.environ.copy()
236 233 env.update(self.kernel_spec.env or {})
237 234 # launch the kernel subprocess
238 235 self.kernel = self._launch_kernel(kernel_cmd, env=env,
239 236 ipython_kernel=self.ipython_kernel,
240 237 **kw)
241 238 self.start_restarter()
242 239 self._connect_control_socket()
243 240
244 241 def request_shutdown(self, restart=False):
245 242 """Send a shutdown request via control channel
246 243
247 244 On Windows, this just kills kernels instead, because the shutdown
248 245 messages don't work.
249 246 """
250 247 content = dict(restart=restart)
251 248 msg = self.session.msg("shutdown_request", content=content)
252 249 self.session.send(self._control_socket, msg)
253 250
254 251 def finish_shutdown(self, waittime=1, pollinterval=0.1):
255 252 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
256 253
257 254 This does not send shutdown requests - use :meth:`request_shutdown`
258 255 first.
259 256 """
260 257 for i in range(int(waittime/pollinterval)):
261 258 if self.is_alive():
262 259 time.sleep(pollinterval)
263 260 else:
264 261 break
265 262 else:
266 263 # OK, we've waited long enough.
267 264 if self.has_kernel:
268 265 self._kill_kernel()
269 266
270 267 def cleanup(self, connection_file=True):
271 268 """Clean up resources when the kernel is shut down"""
272 269 if connection_file:
273 270 self.cleanup_connection_file()
274 271
275 272 self.cleanup_ipc_files()
276 273 self._close_control_socket()
277 274
278 275 def shutdown_kernel(self, now=False, restart=False):
279 276 """Attempts to the stop the kernel process cleanly.
280 277
281 278 This attempts to shutdown the kernels cleanly by:
282 279
283 280 1. Sending it a shutdown message over the shell channel.
284 281 2. If that fails, the kernel is shutdown forcibly by sending it
285 282 a signal.
286 283
287 284 Parameters
288 285 ----------
289 286 now : bool
290 287 Should the kernel be forcible killed *now*. This skips the
291 288 first, nice shutdown attempt.
292 289 restart: bool
293 290 Will this kernel be restarted after it is shutdown. When this
294 291 is True, connection files will not be cleaned up.
295 292 """
296 293 # Stop monitoring for restarting while we shutdown.
297 294 self.stop_restarter()
298 295
299 296 if now:
300 297 self._kill_kernel()
301 298 else:
302 299 self.request_shutdown(restart=restart)
303 300 # Don't send any additional kernel kill messages immediately, to give
304 301 # the kernel a chance to properly execute shutdown actions. Wait for at
305 302 # most 1s, checking every 0.1s.
306 303 self.finish_shutdown()
307 304
308 305 self.cleanup(connection_file=not restart)
309 306
310 307 def restart_kernel(self, now=False, **kw):
311 308 """Restarts a kernel with the arguments that were used to launch it.
312 309
313 310 If the old kernel was launched with random ports, the same ports will be
314 311 used for the new kernel. The same connection file is used again.
315 312
316 313 Parameters
317 314 ----------
318 315 now : bool, optional
319 316 If True, the kernel is forcefully restarted *immediately*, without
320 317 having a chance to do any cleanup action. Otherwise the kernel is
321 318 given 1s to clean up before a forceful restart is issued.
322 319
323 320 In all cases the kernel is restarted, the only difference is whether
324 321 it is given a chance to perform a clean shutdown or not.
325 322
326 323 **kw : optional
327 324 Any options specified here will overwrite those used to launch the
328 325 kernel.
329 326 """
330 327 if self._launch_args is None:
331 328 raise RuntimeError("Cannot restart the kernel. "
332 329 "No previous call to 'start_kernel'.")
333 330 else:
334 331 # Stop currently running kernel.
335 332 self.shutdown_kernel(now=now, restart=True)
336 333
337 334 # Start new kernel.
338 335 self._launch_args.update(kw)
339 336 self.start_kernel(**self._launch_args)
340 337
341 338 @property
342 339 def has_kernel(self):
343 340 """Has a kernel been started that we are managing."""
344 341 return self.kernel is not None
345 342
346 343 def _kill_kernel(self):
347 344 """Kill the running kernel.
348 345
349 346 This is a private method, callers should use shutdown_kernel(now=True).
350 347 """
351 348 if self.has_kernel:
352 349
353 350 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
354 351 # TerminateProcess() on Win32).
355 352 try:
356 353 self.kernel.kill()
357 354 except OSError as e:
358 355 # In Windows, we will get an Access Denied error if the process
359 356 # has already terminated. Ignore it.
360 357 if sys.platform == 'win32':
361 358 if e.winerror != 5:
362 359 raise
363 360 # On Unix, we may get an ESRCH error if the process has already
364 361 # terminated. Ignore it.
365 362 else:
366 363 from errno import ESRCH
367 364 if e.errno != ESRCH:
368 365 raise
369 366
370 367 # Block until the kernel terminates.
371 368 self.kernel.wait()
372 369 self.kernel = None
373 370 else:
374 371 raise RuntimeError("Cannot kill kernel. No kernel is running!")
375 372
376 373 def interrupt_kernel(self):
377 374 """Interrupts the kernel by sending it a signal.
378 375
379 376 Unlike ``signal_kernel``, this operation is well supported on all
380 377 platforms.
381 378 """
382 379 if self.has_kernel:
383 380 if sys.platform == 'win32':
384 381 from .zmq.parentpoller import ParentPollerWindows as Poller
385 382 Poller.send_interrupt(self.kernel.win32_interrupt_event)
386 383 else:
387 384 self.kernel.send_signal(signal.SIGINT)
388 385 else:
389 386 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
390 387
391 388 def signal_kernel(self, signum):
392 389 """Sends a signal to the kernel.
393 390
394 391 Note that since only SIGTERM is supported on Windows, this function is
395 392 only useful on Unix systems.
396 393 """
397 394 if self.has_kernel:
398 395 self.kernel.send_signal(signum)
399 396 else:
400 397 raise RuntimeError("Cannot signal kernel. No kernel is running!")
401 398
402 399 def is_alive(self):
403 400 """Is the kernel process still running?"""
404 401 if self.has_kernel:
405 402 if self.kernel.poll() is None:
406 403 return True
407 404 else:
408 405 return False
409 406 else:
410 407 # we don't have a kernel
411 408 return False
412 409
413 410
414 411 KernelManagerABC.register(KernelManager)
415 412
General Comments 0
You need to be logged in to leave comments. Login now