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