##// END OF EJS Templates
support self.ports as dict or list in cleanup_ipc_files...
Brian Gough -
Show More
@@ -1,576 +1,581 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 # IPython imports
27 27 from IPython.config import LoggingConfigurable
28 28 from IPython.core.profiledir import ProfileDir
29 29 from IPython.utils.localinterfaces import localhost
30 30 from IPython.utils.path import filefind, get_ipython_dir
31 31 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
32 32 string_types)
33 33 from IPython.utils.traitlets import (
34 34 Bool, Integer, Unicode, CaselessStrEnum, Instance,
35 35 )
36 36
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Working with Connection Files
40 40 #-----------------------------------------------------------------------------
41 41
42 42 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
43 43 control_port=0, ip='', key=b'', transport='tcp',
44 44 signature_scheme='hmac-sha256',
45 45 ):
46 46 """Generates a JSON config file, including the selection of random ports.
47 47
48 48 Parameters
49 49 ----------
50 50
51 51 fname : unicode
52 52 The path to the file to write
53 53
54 54 shell_port : int, optional
55 55 The port to use for ROUTER (shell) channel.
56 56
57 57 iopub_port : int, optional
58 58 The port to use for the SUB channel.
59 59
60 60 stdin_port : int, optional
61 61 The port to use for the ROUTER (raw input) channel.
62 62
63 63 control_port : int, optional
64 64 The port to use for the ROUTER (control) channel.
65 65
66 66 hb_port : int, optional
67 67 The port to use for the heartbeat REP channel.
68 68
69 69 ip : str, optional
70 70 The ip address the kernel will bind to.
71 71
72 72 key : str, optional
73 73 The Session key used for message authentication.
74 74
75 75 signature_scheme : str, optional
76 76 The scheme used for message authentication.
77 77 This has the form 'digest-hash', where 'digest'
78 78 is the scheme used for digests, and 'hash' is the name of the hash function
79 79 used by the digest scheme.
80 80 Currently, 'hmac' is the only supported digest scheme,
81 81 and 'sha256' is the default hash function.
82 82
83 83 """
84 84 if not ip:
85 85 ip = localhost()
86 86 # default to temporary connector file
87 87 if not fname:
88 88 fd, fname = tempfile.mkstemp('.json')
89 89 os.close(fd)
90 90
91 91 # Find open ports as necessary.
92 92
93 93 ports = []
94 94 ports_needed = int(shell_port <= 0) + \
95 95 int(iopub_port <= 0) + \
96 96 int(stdin_port <= 0) + \
97 97 int(control_port <= 0) + \
98 98 int(hb_port <= 0)
99 99 if transport == 'tcp':
100 100 for i in range(ports_needed):
101 101 sock = socket.socket()
102 102 # struct.pack('ii', (0,0)) is 8 null bytes
103 103 sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8)
104 104 sock.bind(('', 0))
105 105 ports.append(sock)
106 106 for i, sock in enumerate(ports):
107 107 port = sock.getsockname()[1]
108 108 sock.close()
109 109 ports[i] = port
110 110 else:
111 111 N = 1
112 112 for i in range(ports_needed):
113 113 while os.path.exists("%s-%s" % (ip, str(N))):
114 114 N += 1
115 115 ports.append(N)
116 116 N += 1
117 117 if shell_port <= 0:
118 118 shell_port = ports.pop(0)
119 119 if iopub_port <= 0:
120 120 iopub_port = ports.pop(0)
121 121 if stdin_port <= 0:
122 122 stdin_port = ports.pop(0)
123 123 if control_port <= 0:
124 124 control_port = ports.pop(0)
125 125 if hb_port <= 0:
126 126 hb_port = ports.pop(0)
127 127
128 128 cfg = dict( shell_port=shell_port,
129 129 iopub_port=iopub_port,
130 130 stdin_port=stdin_port,
131 131 control_port=control_port,
132 132 hb_port=hb_port,
133 133 )
134 134 cfg['ip'] = ip
135 135 cfg['key'] = bytes_to_str(key)
136 136 cfg['transport'] = transport
137 137 cfg['signature_scheme'] = signature_scheme
138 138
139 139 with open(fname, 'w') as f:
140 140 f.write(json.dumps(cfg, indent=2))
141 141
142 142 return fname, cfg
143 143
144 144
145 145 def get_connection_file(app=None):
146 146 """Return the path to the connection file of an app
147 147
148 148 Parameters
149 149 ----------
150 150 app : IPKernelApp instance [optional]
151 151 If unspecified, the currently running app will be used
152 152 """
153 153 if app is None:
154 154 from IPython.kernel.zmq.kernelapp import IPKernelApp
155 155 if not IPKernelApp.initialized():
156 156 raise RuntimeError("app not specified, and not in a running Kernel")
157 157
158 158 app = IPKernelApp.instance()
159 159 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
160 160
161 161
162 162 def find_connection_file(filename='kernel-*.json', profile=None):
163 163 """find a connection file, and return its absolute path.
164 164
165 165 The current working directory and the profile's security
166 166 directory will be searched for the file if it is not given by
167 167 absolute path.
168 168
169 169 If profile is unspecified, then the current running application's
170 170 profile will be used, or 'default', if not run from IPython.
171 171
172 172 If the argument does not match an existing file, it will be interpreted as a
173 173 fileglob, and the matching file in the profile's security dir with
174 174 the latest access time will be used.
175 175
176 176 Parameters
177 177 ----------
178 178 filename : str
179 179 The connection file or fileglob to search for.
180 180 profile : str [optional]
181 181 The name of the profile to use when searching for the connection file,
182 182 if different from the current IPython session or 'default'.
183 183
184 184 Returns
185 185 -------
186 186 str : The absolute path of the connection file.
187 187 """
188 188 from IPython.core.application import BaseIPythonApplication as IPApp
189 189 try:
190 190 # quick check for absolute path, before going through logic
191 191 return filefind(filename)
192 192 except IOError:
193 193 pass
194 194
195 195 if profile is None:
196 196 # profile unspecified, check if running from an IPython app
197 197 if IPApp.initialized():
198 198 app = IPApp.instance()
199 199 profile_dir = app.profile_dir
200 200 else:
201 201 # not running in IPython, use default profile
202 202 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
203 203 else:
204 204 # find profiledir by profile name:
205 205 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
206 206 security_dir = profile_dir.security_dir
207 207
208 208 try:
209 209 # first, try explicit name
210 210 return filefind(filename, ['.', security_dir])
211 211 except IOError:
212 212 pass
213 213
214 214 # not found by full name
215 215
216 216 if '*' in filename:
217 217 # given as a glob already
218 218 pat = filename
219 219 else:
220 220 # accept any substring match
221 221 pat = '*%s*' % filename
222 222 matches = glob.glob( os.path.join(security_dir, pat) )
223 223 if not matches:
224 224 raise IOError("Could not find %r in %r" % (filename, security_dir))
225 225 elif len(matches) == 1:
226 226 return matches[0]
227 227 else:
228 228 # get most recent match, by access time:
229 229 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
230 230
231 231
232 232 def get_connection_info(connection_file=None, unpack=False, profile=None):
233 233 """Return the connection information for the current Kernel.
234 234
235 235 Parameters
236 236 ----------
237 237 connection_file : str [optional]
238 238 The connection file to be used. Can be given by absolute path, or
239 239 IPython will search in the security directory of a given profile.
240 240 If run from IPython,
241 241
242 242 If unspecified, the connection file for the currently running
243 243 IPython Kernel will be used, which is only allowed from inside a kernel.
244 244 unpack : bool [default: False]
245 245 if True, return the unpacked dict, otherwise just the string contents
246 246 of the file.
247 247 profile : str [optional]
248 248 The name of the profile to use when searching for the connection file,
249 249 if different from the current IPython session or 'default'.
250 250
251 251
252 252 Returns
253 253 -------
254 254 The connection dictionary of the current kernel, as string or dict,
255 255 depending on `unpack`.
256 256 """
257 257 if connection_file is None:
258 258 # get connection file from current kernel
259 259 cf = get_connection_file()
260 260 else:
261 261 # connection file specified, allow shortnames:
262 262 cf = find_connection_file(connection_file, profile=profile)
263 263
264 264 with open(cf) as f:
265 265 info = f.read()
266 266
267 267 if unpack:
268 268 info = json.loads(info)
269 269 # ensure key is bytes:
270 270 info['key'] = str_to_bytes(info.get('key', ''))
271 271 return info
272 272
273 273
274 274 def connect_qtconsole(connection_file=None, argv=None, profile=None):
275 275 """Connect a qtconsole to the current kernel.
276 276
277 277 This is useful for connecting a second qtconsole to a kernel, or to a
278 278 local notebook.
279 279
280 280 Parameters
281 281 ----------
282 282 connection_file : str [optional]
283 283 The connection file to be used. Can be given by absolute path, or
284 284 IPython will search in the security directory of a given profile.
285 285 If run from IPython,
286 286
287 287 If unspecified, the connection file for the currently running
288 288 IPython Kernel will be used, which is only allowed from inside a kernel.
289 289 argv : list [optional]
290 290 Any extra args to be passed to the console.
291 291 profile : str [optional]
292 292 The name of the profile to use when searching for the connection file,
293 293 if different from the current IPython session or 'default'.
294 294
295 295
296 296 Returns
297 297 -------
298 298 :class:`subprocess.Popen` instance running the qtconsole frontend
299 299 """
300 300 argv = [] if argv is None else argv
301 301
302 302 if connection_file is None:
303 303 # get connection file from current kernel
304 304 cf = get_connection_file()
305 305 else:
306 306 cf = find_connection_file(connection_file, profile=profile)
307 307
308 308 cmd = ';'.join([
309 309 "from IPython.qt.console import qtconsoleapp",
310 310 "qtconsoleapp.main()"
311 311 ])
312 312
313 313 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
314 314 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
315 315 )
316 316
317 317
318 318 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
319 319 """tunnel connections to a kernel via ssh
320 320
321 321 This will open four SSH tunnels from localhost on this machine to the
322 322 ports associated with the kernel. They can be either direct
323 323 localhost-localhost tunnels, or if an intermediate server is necessary,
324 324 the kernel must be listening on a public IP.
325 325
326 326 Parameters
327 327 ----------
328 328 connection_info : dict or str (path)
329 329 Either a connection dict, or the path to a JSON connection file
330 330 sshserver : str
331 331 The ssh sever to use to tunnel to the kernel. Can be a full
332 332 `user@server:port` string. ssh config aliases are respected.
333 333 sshkey : str [optional]
334 334 Path to file containing ssh key to use for authentication.
335 335 Only necessary if your ssh config does not already associate
336 336 a keyfile with the host.
337 337
338 338 Returns
339 339 -------
340 340
341 341 (shell, iopub, stdin, hb) : ints
342 342 The four ports on localhost that have been forwarded to the kernel.
343 343 """
344 344 from zmq.ssh import tunnel
345 345 if isinstance(connection_info, string_types):
346 346 # it's a path, unpack it
347 347 with open(connection_info) as f:
348 348 connection_info = json.loads(f.read())
349 349
350 350 cf = connection_info
351 351
352 352 lports = tunnel.select_random_ports(4)
353 353 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
354 354
355 355 remote_ip = cf['ip']
356 356
357 357 if tunnel.try_passwordless_ssh(sshserver, sshkey):
358 358 password=False
359 359 else:
360 360 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
361 361
362 362 for lp,rp in zip(lports, rports):
363 363 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
364 364
365 365 return tuple(lports)
366 366
367 367
368 368 #-----------------------------------------------------------------------------
369 369 # Mixin for classes that work with connection files
370 370 #-----------------------------------------------------------------------------
371 371
372 372 channel_socket_types = {
373 373 'hb' : zmq.REQ,
374 374 'shell' : zmq.DEALER,
375 375 'iopub' : zmq.SUB,
376 376 'stdin' : zmq.DEALER,
377 377 'control': zmq.DEALER,
378 378 }
379 379
380 380 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
381 381
382 382 class ConnectionFileMixin(LoggingConfigurable):
383 383 """Mixin for configurable classes that work with connection files"""
384 384
385 385 # The addresses for the communication channels
386 386 connection_file = Unicode('', config=True,
387 387 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
388 388
389 389 This file will contain the IP, ports, and authentication key needed to connect
390 390 clients to this kernel. By default, this file will be created in the security dir
391 391 of the current profile, but can be specified by absolute path.
392 392 """)
393 393 _connection_file_written = Bool(False)
394 394
395 395 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
396 396
397 397 ip = Unicode(config=True,
398 398 help="""Set the kernel\'s IP address [default localhost].
399 399 If the IP address is something other than localhost, then
400 400 Consoles on other machines will be able to connect
401 401 to the Kernel, so be careful!"""
402 402 )
403 403
404 404 def _ip_default(self):
405 405 if self.transport == 'ipc':
406 406 if self.connection_file:
407 407 return os.path.splitext(self.connection_file)[0] + '-ipc'
408 408 else:
409 409 return 'kernel-ipc'
410 410 else:
411 411 return localhost()
412 412
413 413 def _ip_changed(self, name, old, new):
414 414 if new == '*':
415 415 self.ip = '0.0.0.0'
416 416
417 417 # protected traits
418 418
419 419 hb_port = Integer(0, config=True,
420 420 help="set the heartbeat port [default: random]")
421 421 shell_port = Integer(0, config=True,
422 422 help="set the shell (ROUTER) port [default: random]")
423 423 iopub_port = Integer(0, config=True,
424 424 help="set the iopub (PUB) port [default: random]")
425 425 stdin_port = Integer(0, config=True,
426 426 help="set the stdin (ROUTER) port [default: random]")
427 427 control_port = Integer(0, config=True,
428 428 help="set the control (ROUTER) port [default: random]")
429 429
430 430 @property
431 431 def ports(self):
432 432 return [ getattr(self, name) for name in port_names ]
433 433
434 434 # The Session to use for communication with the kernel.
435 435 session = Instance('IPython.kernel.zmq.session.Session')
436 436 def _session_default(self):
437 437 from IPython.kernel.zmq.session import Session
438 438 return Session(parent=self)
439 439
440 440 #--------------------------------------------------------------------------
441 441 # Connection and ipc file management
442 442 #--------------------------------------------------------------------------
443 443
444 444 def get_connection_info(self):
445 445 """return the connection info as a dict"""
446 446 return dict(
447 447 transport=self.transport,
448 448 ip=self.ip,
449 449 shell_port=self.shell_port,
450 450 iopub_port=self.iopub_port,
451 451 stdin_port=self.stdin_port,
452 452 hb_port=self.hb_port,
453 453 control_port=self.control_port,
454 454 signature_scheme=self.session.signature_scheme,
455 455 key=self.session.key,
456 456 )
457 457
458 458 def cleanup_connection_file(self):
459 459 """Cleanup connection file *if we wrote it*
460 460
461 461 Will not raise if the connection file was already removed somehow.
462 462 """
463 463 if self._connection_file_written:
464 464 # cleanup connection files on full shutdown of kernel we started
465 465 self._connection_file_written = False
466 466 try:
467 467 os.remove(self.connection_file)
468 468 except (IOError, OSError, AttributeError):
469 469 pass
470 470
471 471 def cleanup_ipc_files(self):
472 472 """Cleanup ipc files if we wrote them."""
473 473 if self.transport != 'ipc':
474 474 return
475 for port in self.ports:
475 # handle self.ports as a list [port1, ...] or dictionary
476 # {label1: port1, ...}
477 ports = self.ports
478 if isinstance(ports, dict):
479 ports = ports.values()
480 for port in ports:
476 481 ipcfile = "%s-%i" % (self.ip, port)
477 482 try:
478 483 os.remove(ipcfile)
479 484 except (IOError, OSError):
480 485 pass
481 486
482 487 def write_connection_file(self):
483 488 """Write connection info to JSON dict in self.connection_file."""
484 489 if self._connection_file_written and os.path.exists(self.connection_file):
485 490 return
486 491
487 492 self.connection_file, cfg = write_connection_file(self.connection_file,
488 493 transport=self.transport, ip=self.ip, key=self.session.key,
489 494 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
490 495 shell_port=self.shell_port, hb_port=self.hb_port,
491 496 control_port=self.control_port,
492 497 signature_scheme=self.session.signature_scheme,
493 498 )
494 499 # write_connection_file also sets default ports:
495 500 for name in port_names:
496 501 setattr(self, name, cfg[name])
497 502
498 503 self._connection_file_written = True
499 504
500 505 def load_connection_file(self):
501 506 """Load connection info from JSON dict in self.connection_file."""
502 507 self.log.debug(u"Loading connection file %s", self.connection_file)
503 508 with open(self.connection_file) as f:
504 509 cfg = json.load(f)
505 510 self.transport = cfg.get('transport', self.transport)
506 511 self.ip = cfg.get('ip', self._ip_default())
507 512
508 513 for name in port_names:
509 514 if getattr(self, name) == 0 and name in cfg:
510 515 # not overridden by config or cl_args
511 516 setattr(self, name, cfg[name])
512 517
513 518 if 'key' in cfg:
514 519 self.session.key = str_to_bytes(cfg['key'])
515 520 if 'signature_scheme' in cfg:
516 521 self.session.signature_scheme = cfg['signature_scheme']
517 522
518 523 #--------------------------------------------------------------------------
519 524 # Creating connected sockets
520 525 #--------------------------------------------------------------------------
521 526
522 527 def _make_url(self, channel):
523 528 """Make a ZeroMQ URL for a given channel."""
524 529 transport = self.transport
525 530 ip = self.ip
526 531 port = getattr(self, '%s_port' % channel)
527 532
528 533 if transport == 'tcp':
529 534 return "tcp://%s:%i" % (ip, port)
530 535 else:
531 536 return "%s://%s-%s" % (transport, ip, port)
532 537
533 538 def _create_connected_socket(self, channel, identity=None):
534 539 """Create a zmq Socket and connect it to the kernel."""
535 540 url = self._make_url(channel)
536 541 socket_type = channel_socket_types[channel]
537 542 self.log.debug("Connecting to: %s" % url)
538 543 sock = self.context.socket(socket_type)
539 544 # set linger to 1s to prevent hangs at exit
540 545 sock.linger = 1000
541 546 if identity:
542 547 sock.identity = identity
543 548 sock.connect(url)
544 549 return sock
545 550
546 551 def connect_iopub(self, identity=None):
547 552 """return zmq Socket connected to the IOPub channel"""
548 553 sock = self._create_connected_socket('iopub', identity=identity)
549 554 sock.setsockopt(zmq.SUBSCRIBE, b'')
550 555 return sock
551 556
552 557 def connect_shell(self, identity=None):
553 558 """return zmq Socket connected to the Shell channel"""
554 559 return self._create_connected_socket('shell', identity=identity)
555 560
556 561 def connect_stdin(self, identity=None):
557 562 """return zmq Socket connected to the StdIn channel"""
558 563 return self._create_connected_socket('stdin', identity=identity)
559 564
560 565 def connect_hb(self, identity=None):
561 566 """return zmq Socket connected to the Heartbeat channel"""
562 567 return self._create_connected_socket('hb', identity=identity)
563 568
564 569 def connect_control(self, identity=None):
565 570 """return zmq Socket connected to the Control channel"""
566 571 return self._create_connected_socket('control', identity=identity)
567 572
568 573
569 574 __all__ = [
570 575 'write_connection_file',
571 576 'get_connection_file',
572 577 'find_connection_file',
573 578 'get_connection_info',
574 579 'connect_qtconsole',
575 580 'tunnel_to_kernel',
576 581 ]
General Comments 0
You need to be logged in to leave comments. Login now