##// END OF EJS Templates
Merge pull request #6092 from minrk/client-session...
Thomas Kluyver -
r17167:ecd0de49 merge
parent child Browse files
Show More
@@ -1,351 +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 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 147 def _connection_file_default(self):
148 148 return 'kernel-%i.json' % os.getpid()
149 149
150 150 existing = CUnicode('', config=True,
151 151 help="""Connect to an already running kernel""")
152 152
153 153 kernel_name = Unicode('python', config=True,
154 154 help="""The name of the default kernel to start.""")
155 155
156 156 confirm_exit = CBool(True, config=True,
157 157 help="""
158 158 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
159 159 to force a direct exit without any confirmation.""",
160 160 )
161 161
162 162
163 163 def build_kernel_argv(self, argv=None):
164 164 """build argv to be passed to kernel subprocess"""
165 165 if argv is None:
166 166 argv = sys.argv[1:]
167 167 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
168 168
169 169 def init_connection_file(self):
170 170 """find the connection file, and load the info if found.
171 171
172 172 The current working directory and the current profile's security
173 173 directory will be searched for the file if it is not given by
174 174 absolute path.
175 175
176 176 When attempting to connect to an existing kernel and the `--existing`
177 177 argument does not match an existing file, it will be interpreted as a
178 178 fileglob, and the matching file in the current profile's security dir
179 179 with the latest access time will be used.
180 180
181 181 After this method is called, self.connection_file contains the *full path*
182 182 to the connection file, never just its name.
183 183 """
184 184 if self.existing:
185 185 try:
186 186 cf = find_connection_file(self.existing)
187 187 except Exception:
188 188 self.log.critical("Could not find existing kernel connection file %s", self.existing)
189 189 self.exit(1)
190 190 self.log.debug("Connecting to existing kernel: %s" % cf)
191 191 self.connection_file = cf
192 192 else:
193 193 # not existing, check if we are going to write the file
194 194 # and ensure that self.connection_file is a full path, not just the shortname
195 195 try:
196 196 cf = find_connection_file(self.connection_file)
197 197 except Exception:
198 198 # file might not exist
199 199 if self.connection_file == os.path.basename(self.connection_file):
200 200 # just shortname, put it in security dir
201 201 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
202 202 else:
203 203 cf = self.connection_file
204 204 self.connection_file = cf
205 205 try:
206 206 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
207 207 except IOError:
208 208 self.log.debug("Connection File not found: %s", self.connection_file)
209 209 return
210 210
211 211 # should load_connection_file only be used for existing?
212 212 # as it is now, this allows reusing ports if an existing
213 213 # file is requested
214 214 try:
215 215 self.load_connection_file()
216 216 except Exception:
217 217 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
218 218 self.exit(1)
219 219
220 220 def init_ssh(self):
221 221 """set up ssh tunnels, if needed."""
222 222 if not self.existing or (not self.sshserver and not self.sshkey):
223 223 return
224 224 self.load_connection_file()
225 225
226 226 transport = self.transport
227 227 ip = self.ip
228 228
229 229 if transport != 'tcp':
230 230 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
231 231 sys.exit(-1)
232 232
233 233 if self.sshkey and not self.sshserver:
234 234 # specifying just the key implies that we are connecting directly
235 235 self.sshserver = ip
236 236 ip = localhost()
237 237
238 238 # build connection dict for tunnels:
239 239 info = dict(ip=ip,
240 240 shell_port=self.shell_port,
241 241 iopub_port=self.iopub_port,
242 242 stdin_port=self.stdin_port,
243 243 hb_port=self.hb_port
244 244 )
245 245
246 246 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
247 247
248 248 # tunnels return a new set of ports, which will be on localhost:
249 249 self.ip = localhost()
250 250 try:
251 251 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
252 252 except:
253 253 # even catch KeyboardInterrupt
254 254 self.log.error("Could not setup tunnels", exc_info=True)
255 255 self.exit(1)
256 256
257 257 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
258 258
259 259 cf = self.connection_file
260 260 base,ext = os.path.splitext(cf)
261 261 base = os.path.basename(base)
262 262 self.connection_file = os.path.basename(base)+'-ssh'+ext
263 263 self.log.info("To connect another client via this tunnel, use:")
264 264 self.log.info("--existing %s" % self.connection_file)
265 265
266 266 def _new_connection_file(self):
267 267 cf = ''
268 268 while not cf:
269 269 # we don't need a 128b id to distinguish kernels, use more readable
270 270 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
271 271 # kernels can subclass.
272 272 ident = str(uuid.uuid4()).split('-')[-1]
273 273 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
274 274 # only keep if it's actually new. Protect against unlikely collision
275 275 # in 48b random search space
276 276 cf = cf if not os.path.exists(cf) else ''
277 277 return cf
278 278
279 279 def init_kernel_manager(self):
280 280 # Don't let Qt or ZMQ swallow KeyboardInterupts.
281 281 if self.existing:
282 282 self.kernel_manager = None
283 283 return
284 284 signal.signal(signal.SIGINT, signal.SIG_DFL)
285 285
286 286 # Create a KernelManager and start a kernel.
287 287 try:
288 288 self.kernel_manager = self.kernel_manager_class(
289 289 ip=self.ip,
290 session=self.session,
290 291 transport=self.transport,
291 292 shell_port=self.shell_port,
292 293 iopub_port=self.iopub_port,
293 294 stdin_port=self.stdin_port,
294 295 hb_port=self.hb_port,
295 296 connection_file=self.connection_file,
296 297 kernel_name=self.kernel_name,
297 298 parent=self,
298 299 ipython_dir=self.ipython_dir,
299 300 )
300 301 except NoSuchKernel:
301 302 self.log.critical("Could not find kernel %s", self.kernel_name)
302 303 self.exit(1)
303 304
304 305 self.kernel_manager.client_factory = self.kernel_client_class
305 306 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
306 307 atexit.register(self.kernel_manager.cleanup_ipc_files)
307 308
308 309 if self.sshserver:
309 310 # ssh, write new connection file
310 311 self.kernel_manager.write_connection_file()
311 312
312 313 # in case KM defaults / ssh writing changes things:
313 314 km = self.kernel_manager
314 315 self.shell_port=km.shell_port
315 316 self.iopub_port=km.iopub_port
316 317 self.stdin_port=km.stdin_port
317 318 self.hb_port=km.hb_port
318 319 self.connection_file = km.connection_file
319 320
320 321 atexit.register(self.kernel_manager.cleanup_connection_file)
321 322
322 323 def init_kernel_client(self):
323 324 if self.kernel_manager is not None:
324 325 self.kernel_client = self.kernel_manager.client()
325 326 else:
326 327 self.kernel_client = self.kernel_client_class(
328 session=self.session,
327 329 ip=self.ip,
328 330 transport=self.transport,
329 331 shell_port=self.shell_port,
330 332 iopub_port=self.iopub_port,
331 333 stdin_port=self.stdin_port,
332 334 hb_port=self.hb_port,
333 335 connection_file=self.connection_file,
334 336 parent=self,
335 337 )
336 338
337 339 self.kernel_client.start_channels()
338 340
339 341
340 342
341 343 def initialize(self, argv=None):
342 344 """
343 345 Classes which mix this class in should call:
344 346 IPythonConsoleApp.initialize(self,argv)
345 347 """
346 348 self.init_connection_file()
347 349 default_secure(self.config)
348 350 self.init_ssh()
349 351 self.init_kernel_manager()
350 352 self.init_kernel_client()
351 353
General Comments 0
You need to be logged in to leave comments. Login now