##// END OF EJS Templates
more DRYing out of consoleapp
Paul Ivanov -
Show More
@@ -1,368 +1,353 b''
1 1 """ A minimal application base mixin for all ZMQ based IPython frontends.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations. This is a
5 5 refactoring of what used to be the IPython/qt/console/qtconsoleapp.py
6 6 """
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 # stdlib imports
15 15 import atexit
16 16 import os
17 17 import signal
18 18 import sys
19 19 import uuid
20 20
21 21
22 22 # Local imports
23 23 from IPython.config.application import boolean_flag
24 24 from IPython.core.profiledir import ProfileDir
25 25 from IPython.kernel.blocking import BlockingKernelClient
26 26 from IPython.kernel import KernelManager
27 27 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
28 28 from IPython.kernel.kernelspec import NoSuchKernel
29 29 from IPython.utils.path import filefind
30 30 from IPython.utils.traitlets import (
31 Dict, List, Unicode, CUnicode, Int, CBool, Any
31 Dict, List, Unicode, CUnicode, CBool, Any
32 32 )
33 33 from IPython.kernel.zmq.kernelapp import (
34 34 kernel_flags,
35 35 kernel_aliases,
36 36 IPKernelApp
37 37 )
38 38 from IPython.kernel.zmq.pylab.config import InlineBackend
39 39 from IPython.kernel.zmq.session import Session, default_secure
40 40 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
41 41 from IPython.kernel.connect import ConnectionFileMixin
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Network Constants
45 45 #-----------------------------------------------------------------------------
46 46
47 47 from IPython.utils.localinterfaces import localhost
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Globals
51 51 #-----------------------------------------------------------------------------
52 52
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # Aliases and Flags
56 56 #-----------------------------------------------------------------------------
57 57
58 58 flags = dict(kernel_flags)
59 59
60 60 # the flags that are specific to the frontend
61 61 # these must be scrubbed before being passed to the kernel,
62 62 # or it will raise an error on unrecognized flags
63 63 app_flags = {
64 64 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
65 65 "Connect to an existing kernel. If no argument specified, guess most recent"),
66 66 }
67 67 app_flags.update(boolean_flag(
68 68 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
69 69 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
70 70 to force a direct exit without any confirmation.
71 71 """,
72 72 """Don't prompt the user when exiting. This will terminate the kernel
73 73 if it is owned by the frontend, and leave it alive if it is external.
74 74 """
75 75 ))
76 76 flags.update(app_flags)
77 77
78 78 aliases = dict(kernel_aliases)
79 79
80 80 # also scrub aliases from the frontend
81 81 app_aliases = dict(
82 82 ip = 'IPythonConsoleApp.ip',
83 83 transport = 'IPythonConsoleApp.transport',
84 84 hb = 'IPythonConsoleApp.hb_port',
85 85 shell = 'IPythonConsoleApp.shell_port',
86 86 iopub = 'IPythonConsoleApp.iopub_port',
87 87 stdin = 'IPythonConsoleApp.stdin_port',
88 88 existing = 'IPythonConsoleApp.existing',
89 89 f = 'IPythonConsoleApp.connection_file',
90 90
91 91 kernel = 'IPythonConsoleApp.kernel_name',
92 92
93 93 ssh = 'IPythonConsoleApp.sshserver',
94 94 )
95 95 aliases.update(app_aliases)
96 96
97 97 #-----------------------------------------------------------------------------
98 98 # Classes
99 99 #-----------------------------------------------------------------------------
100 100
101 101 #-----------------------------------------------------------------------------
102 102 # IPythonConsole
103 103 #-----------------------------------------------------------------------------
104 104
105 105 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
106 106
107 107 class IPythonConsoleApp(ConnectionFileMixin):
108 108 name = 'ipython-console-mixin'
109 109
110 110 description = """
111 111 The IPython Mixin Console.
112 112
113 113 This class contains the common portions of console client (QtConsole,
114 114 ZMQ-based terminal console, etc). It is not a full console, in that
115 115 launched terminal subprocesses will not be able to accept input.
116 116
117 117 The Console using this mixing supports various extra features beyond
118 118 the single-process Terminal IPython shell, such as connecting to
119 119 existing kernel, via:
120 120
121 121 ipython <appname> --existing
122 122
123 123 as well as tunnel via SSH
124 124
125 125 """
126 126
127 127 classes = classes
128 128 flags = Dict(flags)
129 129 aliases = Dict(aliases)
130 130 kernel_manager_class = KernelManager
131 131 kernel_client_class = BlockingKernelClient
132 132
133 133 kernel_argv = List(Unicode)
134 134 # frontend flags&aliases to be stripped when building kernel_argv
135 135 frontend_flags = Any(app_flags)
136 136 frontend_aliases = Any(app_aliases)
137 137
138 138 # create requested profiles by default, if they don't exist:
139 139 auto_create = CBool(True)
140 140 # connection info:
141 141
142 142 sshserver = Unicode('', config=True,
143 143 help="""The SSH server to use to connect to the kernel.""")
144 144 sshkey = Unicode('', config=True,
145 145 help="""Path to the ssh key to use for logging in to the ssh server.""")
146 146
147 hb_port = Int(0, config=True,
148 help="set the heartbeat port [default: random]")
149 shell_port = Int(0, config=True,
150 help="set the shell (ROUTER) port [default: random]")
151 iopub_port = Int(0, config=True,
152 help="set the iopub (PUB) port [default: random]")
153 stdin_port = Int(0, config=True,
154 help="set the stdin (DEALER) port [default: random]")
155 connection_file = Unicode('', config=True,
156 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
157
158 This file will contain the IP, ports, and authentication key needed to connect
159 clients to this kernel. By default, this file will be created in the security-dir
160 of the current profile, but can be specified by absolute path.
161 """)
162 147 def _connection_file_default(self):
163 148 return 'kernel-%i.json' % os.getpid()
164 149
165 150 existing = CUnicode('', config=True,
166 151 help="""Connect to an already running kernel""")
167 152
168 153 kernel_name = Unicode('python', config=True,
169 154 help="""The name of the default kernel to start.""")
170 155
171 156 confirm_exit = CBool(True, config=True,
172 157 help="""
173 158 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
174 159 to force a direct exit without any confirmation.""",
175 160 )
176 161
177 162
178 163 def build_kernel_argv(self, argv=None):
179 164 """build argv to be passed to kernel subprocess"""
180 165 if argv is None:
181 166 argv = sys.argv[1:]
182 167 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
183 168 # kernel should inherit default config file from frontend
184 169 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
185 170
186 171 def init_connection_file(self):
187 172 """find the connection file, and load the info if found.
188 173
189 174 The current working directory and the current profile's security
190 175 directory will be searched for the file if it is not given by
191 176 absolute path.
192 177
193 178 When attempting to connect to an existing kernel and the `--existing`
194 179 argument does not match an existing file, it will be interpreted as a
195 180 fileglob, and the matching file in the current profile's security dir
196 181 with the latest access time will be used.
197 182
198 183 After this method is called, self.connection_file contains the *full path*
199 184 to the connection file, never just its name.
200 185 """
201 186 if self.existing:
202 187 try:
203 188 cf = find_connection_file(self.existing)
204 189 except Exception:
205 190 self.log.critical("Could not find existing kernel connection file %s", self.existing)
206 191 self.exit(1)
207 192 self.log.debug("Connecting to existing kernel: %s" % cf)
208 193 self.connection_file = cf
209 194 else:
210 195 # not existing, check if we are going to write the file
211 196 # and ensure that self.connection_file is a full path, not just the shortname
212 197 try:
213 198 cf = find_connection_file(self.connection_file)
214 199 except Exception:
215 200 # file might not exist
216 201 if self.connection_file == os.path.basename(self.connection_file):
217 202 # just shortname, put it in security dir
218 203 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
219 204 else:
220 205 cf = self.connection_file
221 206 self.connection_file = cf
222 207 try:
223 208 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
224 209 except IOError:
225 210 self.log.debug("Connection File not found: %s", self.connection_file)
226 211 return
227 212
228 213 # should load_connection_file only be used for existing?
229 214 # as it is now, this allows reusing ports if an existing
230 215 # file is requested
231 216 try:
232 217 self.load_connection_file()
233 218 except Exception:
234 219 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
235 220 self.exit(1)
236 221
237 222 def init_ssh(self):
238 223 """set up ssh tunnels, if needed."""
239 224 if not self.existing or (not self.sshserver and not self.sshkey):
240 225 return
241 226 self.load_connection_file()
242 227
243 228 transport = self.transport
244 229 ip = self.ip
245 230
246 231 if transport != 'tcp':
247 232 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
248 233 sys.exit(-1)
249 234
250 235 if self.sshkey and not self.sshserver:
251 236 # specifying just the key implies that we are connecting directly
252 237 self.sshserver = ip
253 238 ip = localhost()
254 239
255 240 # build connection dict for tunnels:
256 241 info = dict(ip=ip,
257 242 shell_port=self.shell_port,
258 243 iopub_port=self.iopub_port,
259 244 stdin_port=self.stdin_port,
260 245 hb_port=self.hb_port
261 246 )
262 247
263 248 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
264 249
265 250 # tunnels return a new set of ports, which will be on localhost:
266 251 self.ip = localhost()
267 252 try:
268 253 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
269 254 except:
270 255 # even catch KeyboardInterrupt
271 256 self.log.error("Could not setup tunnels", exc_info=True)
272 257 self.exit(1)
273 258
274 259 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
275 260
276 261 cf = self.connection_file
277 262 base,ext = os.path.splitext(cf)
278 263 base = os.path.basename(base)
279 264 self.connection_file = os.path.basename(base)+'-ssh'+ext
280 265 self.log.info("To connect another client via this tunnel, use:")
281 266 self.log.info("--existing %s" % self.connection_file)
282 267
283 268 def _new_connection_file(self):
284 269 cf = ''
285 270 while not cf:
286 271 # we don't need a 128b id to distinguish kernels, use more readable
287 272 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
288 273 # kernels can subclass.
289 274 ident = str(uuid.uuid4()).split('-')[-1]
290 275 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
291 276 # only keep if it's actually new. Protect against unlikely collision
292 277 # in 48b random search space
293 278 cf = cf if not os.path.exists(cf) else ''
294 279 return cf
295 280
296 281 def init_kernel_manager(self):
297 282 # Don't let Qt or ZMQ swallow KeyboardInterupts.
298 283 if self.existing:
299 284 self.kernel_manager = None
300 285 return
301 286 signal.signal(signal.SIGINT, signal.SIG_DFL)
302 287
303 288 # Create a KernelManager and start a kernel.
304 289 try:
305 290 self.kernel_manager = self.kernel_manager_class(
306 291 ip=self.ip,
307 292 transport=self.transport,
308 293 shell_port=self.shell_port,
309 294 iopub_port=self.iopub_port,
310 295 stdin_port=self.stdin_port,
311 296 hb_port=self.hb_port,
312 297 connection_file=self.connection_file,
313 298 kernel_name=self.kernel_name,
314 299 parent=self,
315 300 ipython_dir=self.ipython_dir,
316 301 )
317 302 except NoSuchKernel:
318 303 self.log.critical("Could not find kernel %s", self.kernel_name)
319 304 self.exit(1)
320 305
321 306 self.kernel_manager.client_factory = self.kernel_client_class
322 307 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
323 308 atexit.register(self.kernel_manager.cleanup_ipc_files)
324 309
325 310 if self.sshserver:
326 311 # ssh, write new connection file
327 312 self.kernel_manager.write_connection_file()
328 313
329 314 # in case KM defaults / ssh writing changes things:
330 315 km = self.kernel_manager
331 316 self.shell_port=km.shell_port
332 317 self.iopub_port=km.iopub_port
333 318 self.stdin_port=km.stdin_port
334 319 self.hb_port=km.hb_port
335 320 self.connection_file = km.connection_file
336 321
337 322 atexit.register(self.kernel_manager.cleanup_connection_file)
338 323
339 324 def init_kernel_client(self):
340 325 if self.kernel_manager is not None:
341 326 self.kernel_client = self.kernel_manager.client()
342 327 else:
343 328 self.kernel_client = self.kernel_client_class(
344 329 ip=self.ip,
345 330 transport=self.transport,
346 331 shell_port=self.shell_port,
347 332 iopub_port=self.iopub_port,
348 333 stdin_port=self.stdin_port,
349 334 hb_port=self.hb_port,
350 335 connection_file=self.connection_file,
351 336 parent=self,
352 337 )
353 338
354 339 self.kernel_client.start_channels()
355 340
356 341
357 342
358 343 def initialize(self, argv=None):
359 344 """
360 345 Classes which mix this class in should call:
361 346 IPythonConsoleApp.initialize(self,argv)
362 347 """
363 348 self.init_connection_file()
364 349 default_secure(self.config)
365 350 self.init_ssh()
366 351 self.init_kernel_manager()
367 352 self.init_kernel_client()
368 353
@@ -1,564 +1,569 b''
1 1 """Utilities for connecting to kernels
2 2
3 3 Notable contents:
4 4 - ConnectionFileMixin class
5 5 encapsulates the logic related to writing and reading connections files.
6 6 """
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import glob
17 17 import json
18 18 import os
19 19 import socket
20 20 import sys
21 21 from getpass import getpass
22 22 from subprocess import Popen, PIPE
23 23 import tempfile
24 24
25 25 import zmq
26 26
27 27 # external imports
28 28 from IPython.external.ssh import tunnel
29 29
30 30 # IPython imports
31 31 from IPython.config import Configurable
32 32 from IPython.core.profiledir import ProfileDir
33 33 from IPython.utils.localinterfaces import localhost
34 34 from IPython.utils.path import filefind, get_ipython_dir
35 35 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
36 36 string_types)
37 37 from IPython.utils.traitlets import (
38 38 Bool, Integer, Unicode, CaselessStrEnum,
39 39 )
40 40
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # Working with Connection Files
44 44 #-----------------------------------------------------------------------------
45 45
46 46 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
47 47 control_port=0, ip='', key=b'', transport='tcp',
48 48 signature_scheme='hmac-sha256',
49 49 ):
50 50 """Generates a JSON config file, including the selection of random ports.
51 51
52 52 Parameters
53 53 ----------
54 54
55 55 fname : unicode
56 56 The path to the file to write
57 57
58 58 shell_port : int, optional
59 59 The port to use for ROUTER (shell) channel.
60 60
61 61 iopub_port : int, optional
62 62 The port to use for the SUB channel.
63 63
64 64 stdin_port : int, optional
65 65 The port to use for the ROUTER (raw input) channel.
66 66
67 67 control_port : int, optional
68 68 The port to use for the ROUTER (control) channel.
69 69
70 70 hb_port : int, optional
71 71 The port to use for the heartbeat REP channel.
72 72
73 73 ip : str, optional
74 74 The ip address the kernel will bind to.
75 75
76 76 key : str, optional
77 77 The Session key used for message authentication.
78 78
79 79 signature_scheme : str, optional
80 80 The scheme used for message authentication.
81 81 This has the form 'digest-hash', where 'digest'
82 82 is the scheme used for digests, and 'hash' is the name of the hash function
83 83 used by the digest scheme.
84 84 Currently, 'hmac' is the only supported digest scheme,
85 85 and 'sha256' is the default hash function.
86 86
87 87 """
88 88 if not ip:
89 89 ip = localhost()
90 90 # default to temporary connector file
91 91 if not fname:
92 92 fd, fname = tempfile.mkstemp('.json')
93 93 os.close(fd)
94 94
95 95 # Find open ports as necessary.
96 96
97 97 ports = []
98 98 ports_needed = int(shell_port <= 0) + \
99 99 int(iopub_port <= 0) + \
100 100 int(stdin_port <= 0) + \
101 101 int(control_port <= 0) + \
102 102 int(hb_port <= 0)
103 103 if transport == 'tcp':
104 104 for i in range(ports_needed):
105 105 sock = socket.socket()
106 106 # struct.pack('ii', (0,0)) is 8 null bytes
107 107 sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8)
108 108 sock.bind(('', 0))
109 109 ports.append(sock)
110 110 for i, sock in enumerate(ports):
111 111 port = sock.getsockname()[1]
112 112 sock.close()
113 113 ports[i] = port
114 114 else:
115 115 N = 1
116 116 for i in range(ports_needed):
117 117 while os.path.exists("%s-%s" % (ip, str(N))):
118 118 N += 1
119 119 ports.append(N)
120 120 N += 1
121 121 if shell_port <= 0:
122 122 shell_port = ports.pop(0)
123 123 if iopub_port <= 0:
124 124 iopub_port = ports.pop(0)
125 125 if stdin_port <= 0:
126 126 stdin_port = ports.pop(0)
127 127 if control_port <= 0:
128 128 control_port = ports.pop(0)
129 129 if hb_port <= 0:
130 130 hb_port = ports.pop(0)
131 131
132 132 cfg = dict( shell_port=shell_port,
133 133 iopub_port=iopub_port,
134 134 stdin_port=stdin_port,
135 135 control_port=control_port,
136 136 hb_port=hb_port,
137 137 )
138 138 cfg['ip'] = ip
139 139 cfg['key'] = bytes_to_str(key)
140 140 cfg['transport'] = transport
141 141 cfg['signature_scheme'] = signature_scheme
142 142
143 143 with open(fname, 'w') as f:
144 144 f.write(json.dumps(cfg, indent=2))
145 145
146 146 return fname, cfg
147 147
148 148
149 149 def get_connection_file(app=None):
150 150 """Return the path to the connection file of an app
151 151
152 152 Parameters
153 153 ----------
154 154 app : IPKernelApp instance [optional]
155 155 If unspecified, the currently running app will be used
156 156 """
157 157 if app is None:
158 158 from IPython.kernel.zmq.kernelapp import IPKernelApp
159 159 if not IPKernelApp.initialized():
160 160 raise RuntimeError("app not specified, and not in a running Kernel")
161 161
162 162 app = IPKernelApp.instance()
163 163 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
164 164
165 165
166 166 def find_connection_file(filename, profile=None):
167 167 """find a connection file, and return its absolute path.
168 168
169 169 The current working directory and the profile's security
170 170 directory will be searched for the file if it is not given by
171 171 absolute path.
172 172
173 173 If profile is unspecified, then the current running application's
174 174 profile will be used, or 'default', if not run from IPython.
175 175
176 176 If the argument does not match an existing file, it will be interpreted as a
177 177 fileglob, and the matching file in the profile's security dir with
178 178 the latest access time will be used.
179 179
180 180 Parameters
181 181 ----------
182 182 filename : str
183 183 The connection file or fileglob to search for.
184 184 profile : str [optional]
185 185 The name of the profile to use when searching for the connection file,
186 186 if different from the current IPython session or 'default'.
187 187
188 188 Returns
189 189 -------
190 190 str : The absolute path of the connection file.
191 191 """
192 192 from IPython.core.application import BaseIPythonApplication as IPApp
193 193 try:
194 194 # quick check for absolute path, before going through logic
195 195 return filefind(filename)
196 196 except IOError:
197 197 pass
198 198
199 199 if profile is None:
200 200 # profile unspecified, check if running from an IPython app
201 201 if IPApp.initialized():
202 202 app = IPApp.instance()
203 203 profile_dir = app.profile_dir
204 204 else:
205 205 # not running in IPython, use default profile
206 206 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
207 207 else:
208 208 # find profiledir by profile name:
209 209 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
210 210 security_dir = profile_dir.security_dir
211 211
212 212 try:
213 213 # first, try explicit name
214 214 return filefind(filename, ['.', security_dir])
215 215 except IOError:
216 216 pass
217 217
218 218 # not found by full name
219 219
220 220 if '*' in filename:
221 221 # given as a glob already
222 222 pat = filename
223 223 else:
224 224 # accept any substring match
225 225 pat = '*%s*' % filename
226 226 matches = glob.glob( os.path.join(security_dir, pat) )
227 227 if not matches:
228 228 raise IOError("Could not find %r in %r" % (filename, security_dir))
229 229 elif len(matches) == 1:
230 230 return matches[0]
231 231 else:
232 232 # get most recent match, by access time:
233 233 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
234 234
235 235
236 236 def get_connection_info(connection_file=None, unpack=False, profile=None):
237 237 """Return the connection information for the current Kernel.
238 238
239 239 Parameters
240 240 ----------
241 241 connection_file : str [optional]
242 242 The connection file to be used. Can be given by absolute path, or
243 243 IPython will search in the security directory of a given profile.
244 244 If run from IPython,
245 245
246 246 If unspecified, the connection file for the currently running
247 247 IPython Kernel will be used, which is only allowed from inside a kernel.
248 248 unpack : bool [default: False]
249 249 if True, return the unpacked dict, otherwise just the string contents
250 250 of the file.
251 251 profile : str [optional]
252 252 The name of the profile to use when searching for the connection file,
253 253 if different from the current IPython session or 'default'.
254 254
255 255
256 256 Returns
257 257 -------
258 258 The connection dictionary of the current kernel, as string or dict,
259 259 depending on `unpack`.
260 260 """
261 261 if connection_file is None:
262 262 # get connection file from current kernel
263 263 cf = get_connection_file()
264 264 else:
265 265 # connection file specified, allow shortnames:
266 266 cf = find_connection_file(connection_file, profile=profile)
267 267
268 268 with open(cf) as f:
269 269 info = f.read()
270 270
271 271 if unpack:
272 272 info = json.loads(info)
273 273 # ensure key is bytes:
274 274 info['key'] = str_to_bytes(info.get('key', ''))
275 275 return info
276 276
277 277
278 278 def connect_qtconsole(connection_file=None, argv=None, profile=None):
279 279 """Connect a qtconsole to the current kernel.
280 280
281 281 This is useful for connecting a second qtconsole to a kernel, or to a
282 282 local notebook.
283 283
284 284 Parameters
285 285 ----------
286 286 connection_file : str [optional]
287 287 The connection file to be used. Can be given by absolute path, or
288 288 IPython will search in the security directory of a given profile.
289 289 If run from IPython,
290 290
291 291 If unspecified, the connection file for the currently running
292 292 IPython Kernel will be used, which is only allowed from inside a kernel.
293 293 argv : list [optional]
294 294 Any extra args to be passed to the console.
295 295 profile : str [optional]
296 296 The name of the profile to use when searching for the connection file,
297 297 if different from the current IPython session or 'default'.
298 298
299 299
300 300 Returns
301 301 -------
302 302 subprocess.Popen instance running the qtconsole frontend
303 303 """
304 304 argv = [] if argv is None else argv
305 305
306 306 if connection_file is None:
307 307 # get connection file from current kernel
308 308 cf = get_connection_file()
309 309 else:
310 310 cf = find_connection_file(connection_file, profile=profile)
311 311
312 312 cmd = ';'.join([
313 313 "from IPython.qt.console import qtconsoleapp",
314 314 "qtconsoleapp.main()"
315 315 ])
316 316
317 317 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
318 318 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
319 319 )
320 320
321 321
322 322 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
323 323 """tunnel connections to a kernel via ssh
324 324
325 325 This will open four SSH tunnels from localhost on this machine to the
326 326 ports associated with the kernel. They can be either direct
327 327 localhost-localhost tunnels, or if an intermediate server is necessary,
328 328 the kernel must be listening on a public IP.
329 329
330 330 Parameters
331 331 ----------
332 332 connection_info : dict or str (path)
333 333 Either a connection dict, or the path to a JSON connection file
334 334 sshserver : str
335 335 The ssh sever to use to tunnel to the kernel. Can be a full
336 336 `user@server:port` string. ssh config aliases are respected.
337 337 sshkey : str [optional]
338 338 Path to file containing ssh key to use for authentication.
339 339 Only necessary if your ssh config does not already associate
340 340 a keyfile with the host.
341 341
342 342 Returns
343 343 -------
344 344
345 345 (shell, iopub, stdin, hb) : ints
346 346 The four ports on localhost that have been forwarded to the kernel.
347 347 """
348 348 if isinstance(connection_info, string_types):
349 349 # it's a path, unpack it
350 350 with open(connection_info) as f:
351 351 connection_info = json.loads(f.read())
352 352
353 353 cf = connection_info
354 354
355 355 lports = tunnel.select_random_ports(4)
356 356 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
357 357
358 358 remote_ip = cf['ip']
359 359
360 360 if tunnel.try_passwordless_ssh(sshserver, sshkey):
361 361 password=False
362 362 else:
363 363 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
364 364
365 365 for lp,rp in zip(lports, rports):
366 366 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
367 367
368 368 return tuple(lports)
369 369
370 370
371 371 #-----------------------------------------------------------------------------
372 372 # Mixin for classes that work with connection files
373 373 #-----------------------------------------------------------------------------
374 374
375 375 channel_socket_types = {
376 376 'hb' : zmq.REQ,
377 377 'shell' : zmq.DEALER,
378 378 'iopub' : zmq.SUB,
379 379 'stdin' : zmq.DEALER,
380 380 'control': zmq.DEALER,
381 381 }
382 382
383 383 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
384 384
385 385 class ConnectionFileMixin(Configurable):
386 386 """Mixin for configurable classes that work with connection files"""
387 387
388 388 # The addresses for the communication channels
389 389 connection_file = Unicode('', config=True,
390 390 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
391 391
392 392 This file will contain the IP, ports, and authentication key needed to connect
393 393 clients to this kernel. By default, this file will be created in the security dir
394 394 of the current profile, but can be specified by absolute path.
395 395 """)
396 396 _connection_file_written = Bool(False)
397 397
398 398 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
399 399
400 400 ip = Unicode(config=True,
401 401 help="""Set the kernel\'s IP address [default localhost].
402 402 If the IP address is something other than localhost, then
403 403 Consoles on other machines will be able to connect
404 404 to the Kernel, so be careful!"""
405 405 )
406 406
407 407 def _ip_default(self):
408 408 if self.transport == 'ipc':
409 409 if self.connection_file:
410 410 return os.path.splitext(self.connection_file)[0] + '-ipc'
411 411 else:
412 412 return 'kernel-ipc'
413 413 else:
414 414 return localhost()
415 415
416 416 def _ip_changed(self, name, old, new):
417 417 if new == '*':
418 418 self.ip = '0.0.0.0'
419 419
420 420 # protected traits
421 421
422 hb_port = Integer(0, config=True, help="set the heartbeat port [default: random]")
423 shell_port = Integer(0, config=True, help="set the shell (ROUTER) port [default: random]")
424 iopub_port = Integer(0, config=True, help="set the iopub (PUB) port [default: random]")
425 stdin_port = Integer(0, config=True, help="set the stdin (ROUTER) port [default: random]")
426 control_port = Integer(0, config=True, help="set the control (ROUTER) port [default: random]")
422 hb_port = Integer(0, config=True,
423 help="set the heartbeat port [default: random]")
424 shell_port = Integer(0, config=True,
425 help="set the shell (ROUTER) port [default: random]")
426 iopub_port = Integer(0, config=True,
427 help="set the iopub (PUB) port [default: random]")
428 stdin_port = Integer(0, config=True,
429 help="set the stdin (ROUTER) port [default: random]")
430 control_port = Integer(0, config=True,
431 help="set the control (ROUTER) port [default: random]")
427 432
428 433 @property
429 434 def ports(self):
430 435 return [ getattr(self, name) for name in port_names ]
431 436
432 437 #--------------------------------------------------------------------------
433 438 # Connection and ipc file management
434 439 #--------------------------------------------------------------------------
435 440
436 441 def get_connection_info(self):
437 442 """return the connection info as a dict"""
438 443 return dict(
439 444 transport=self.transport,
440 445 ip=self.ip,
441 446 shell_port=self.shell_port,
442 447 iopub_port=self.iopub_port,
443 448 stdin_port=self.stdin_port,
444 449 hb_port=self.hb_port,
445 450 control_port=self.control_port,
446 451 signature_scheme=self.session.signature_scheme,
447 452 key=self.session.key,
448 453 )
449 454
450 455 def cleanup_connection_file(self):
451 456 """Cleanup connection file *if we wrote it*
452 457
453 458 Will not raise if the connection file was already removed somehow.
454 459 """
455 460 if self._connection_file_written:
456 461 # cleanup connection files on full shutdown of kernel we started
457 462 self._connection_file_written = False
458 463 try:
459 464 os.remove(self.connection_file)
460 465 except (IOError, OSError, AttributeError):
461 466 pass
462 467
463 468 def cleanup_ipc_files(self):
464 469 """Cleanup ipc files if we wrote them."""
465 470 if self.transport != 'ipc':
466 471 return
467 472 for port in self.ports:
468 473 ipcfile = "%s-%i" % (self.ip, port)
469 474 try:
470 475 os.remove(ipcfile)
471 476 except (IOError, OSError):
472 477 pass
473 478
474 479 def write_connection_file(self):
475 480 """Write connection info to JSON dict in self.connection_file."""
476 481 if self._connection_file_written and os.path.exists(self.connection_file):
477 482 return
478 483
479 484 self.connection_file, cfg = write_connection_file(self.connection_file,
480 485 transport=self.transport, ip=self.ip, key=self.session.key,
481 486 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
482 487 shell_port=self.shell_port, hb_port=self.hb_port,
483 488 control_port=self.control_port,
484 489 signature_scheme=self.session.signature_scheme,
485 490 )
486 491 # write_connection_file also sets default ports:
487 492 for name in port_names:
488 493 setattr(self, name, cfg[name])
489 494
490 495 self._connection_file_written = True
491 496
492 497 def load_connection_file(self):
493 498 """Load connection info from JSON dict in self.connection_file."""
494 499 self.log.debug(u"Loading connection file %s", self.connection_file)
495 500 with open(self.connection_file) as f:
496 501 cfg = json.load(f)
497 502 self.transport = cfg.get('transport', self.transport)
498 503 self.ip = cfg.get('ip', self._ip_default())
499 504
500 505 for name in port_names:
501 506 if getattr(self, name) == 0 and name in cfg:
502 507 # not overridden by config or cl_args
503 508 setattr(self, name, cfg[name])
504 509 if 'key' in cfg:
505 510 self.config.Session.key = str_to_bytes(cfg['key'])
506 511 if 'signature_scheme' in cfg:
507 512 self.config.Session.signature_scheme = cfg['signature_scheme']
508 513 #--------------------------------------------------------------------------
509 514 # Creating connected sockets
510 515 #--------------------------------------------------------------------------
511 516
512 517 def _make_url(self, channel):
513 518 """Make a ZeroMQ URL for a given channel."""
514 519 transport = self.transport
515 520 ip = self.ip
516 521 port = getattr(self, '%s_port' % channel)
517 522
518 523 if transport == 'tcp':
519 524 return "tcp://%s:%i" % (ip, port)
520 525 else:
521 526 return "%s://%s-%s" % (transport, ip, port)
522 527
523 528 def _create_connected_socket(self, channel, identity=None):
524 529 """Create a zmq Socket and connect it to the kernel."""
525 530 url = self._make_url(channel)
526 531 socket_type = channel_socket_types[channel]
527 532 self.log.debug("Connecting to: %s" % url)
528 533 sock = self.context.socket(socket_type)
529 534 if identity:
530 535 sock.identity = identity
531 536 sock.connect(url)
532 537 return sock
533 538
534 539 def connect_iopub(self, identity=None):
535 540 """return zmq Socket connected to the IOPub channel"""
536 541 sock = self._create_connected_socket('iopub', identity=identity)
537 542 sock.setsockopt(zmq.SUBSCRIBE, b'')
538 543 return sock
539 544
540 545 def connect_shell(self, identity=None):
541 546 """return zmq Socket connected to the Shell channel"""
542 547 return self._create_connected_socket('shell', identity=identity)
543 548
544 549 def connect_stdin(self, identity=None):
545 550 """return zmq Socket connected to the StdIn channel"""
546 551 return self._create_connected_socket('stdin', identity=identity)
547 552
548 553 def connect_hb(self, identity=None):
549 554 """return zmq Socket connected to the Heartbeat channel"""
550 555 return self._create_connected_socket('hb', identity=identity)
551 556
552 557 def connect_control(self, identity=None):
553 558 """return zmq Socket connected to the Heartbeat channel"""
554 559 return self._create_connected_socket('control', identity=identity)
555 560
556 561
557 562 __all__ = [
558 563 'write_connection_file',
559 564 'get_connection_file',
560 565 'find_connection_file',
561 566 'get_connection_info',
562 567 'connect_qtconsole',
563 568 'tunnel_to_kernel',
564 569 ]
General Comments 0
You need to be logged in to leave comments. Login now