##// 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 """ A minimal application base mixin for all ZMQ based IPython frontends.
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations. This is a
4 input, there is no real readline support, among other limitations. This is a
5 refactoring of what used to be the IPython/qt/console/qtconsoleapp.py
5 refactoring of what used to be the IPython/qt/console/qtconsoleapp.py
6 """
6 """
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 # stdlib imports
14 # stdlib imports
15 import atexit
15 import atexit
16 import os
16 import os
17 import signal
17 import signal
18 import sys
18 import sys
19 import uuid
19 import uuid
20
20
21
21
22 # Local imports
22 # Local imports
23 from IPython.config.application import boolean_flag
23 from IPython.config.application import boolean_flag
24 from IPython.core.profiledir import ProfileDir
24 from IPython.core.profiledir import ProfileDir
25 from IPython.kernel.blocking import BlockingKernelClient
25 from IPython.kernel.blocking import BlockingKernelClient
26 from IPython.kernel import KernelManager
26 from IPython.kernel import KernelManager
27 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
27 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
28 from IPython.kernel.kernelspec import NoSuchKernel
28 from IPython.kernel.kernelspec import NoSuchKernel
29 from IPython.utils.path import filefind
29 from IPython.utils.path import filefind
30 from IPython.utils.traitlets import (
30 from IPython.utils.traitlets import (
31 Dict, List, Unicode, CUnicode, CBool, Any
31 Dict, List, Unicode, CUnicode, CBool, Any
32 )
32 )
33 from IPython.kernel.zmq.kernelapp import (
33 from IPython.kernel.zmq.kernelapp import (
34 kernel_flags,
34 kernel_flags,
35 kernel_aliases,
35 kernel_aliases,
36 IPKernelApp
36 IPKernelApp
37 )
37 )
38 from IPython.kernel.zmq.pylab.config import InlineBackend
38 from IPython.kernel.zmq.pylab.config import InlineBackend
39 from IPython.kernel.zmq.session import Session, default_secure
39 from IPython.kernel.zmq.session import Session, default_secure
40 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
40 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
41 from IPython.kernel.connect import ConnectionFileMixin
41 from IPython.kernel.connect import ConnectionFileMixin
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Network Constants
44 # Network Constants
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 from IPython.utils.localinterfaces import localhost
47 from IPython.utils.localinterfaces import localhost
48
48
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 # Globals
50 # Globals
51 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
52
52
53
53
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55 # Aliases and Flags
55 # Aliases and Flags
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57
57
58 flags = dict(kernel_flags)
58 flags = dict(kernel_flags)
59
59
60 # the flags that are specific to the frontend
60 # the flags that are specific to the frontend
61 # these must be scrubbed before being passed to the kernel,
61 # these must be scrubbed before being passed to the kernel,
62 # or it will raise an error on unrecognized flags
62 # or it will raise an error on unrecognized flags
63 app_flags = {
63 app_flags = {
64 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
64 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
65 "Connect to an existing kernel. If no argument specified, guess most recent"),
65 "Connect to an existing kernel. If no argument specified, guess most recent"),
66 }
66 }
67 app_flags.update(boolean_flag(
67 app_flags.update(boolean_flag(
68 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
68 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
69 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
69 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
70 to force a direct exit without any confirmation.
70 to force a direct exit without any confirmation.
71 """,
71 """,
72 """Don't prompt the user when exiting. This will terminate the kernel
72 """Don't prompt the user when exiting. This will terminate the kernel
73 if it is owned by the frontend, and leave it alive if it is external.
73 if it is owned by the frontend, and leave it alive if it is external.
74 """
74 """
75 ))
75 ))
76 flags.update(app_flags)
76 flags.update(app_flags)
77
77
78 aliases = dict(kernel_aliases)
78 aliases = dict(kernel_aliases)
79
79
80 # also scrub aliases from the frontend
80 # also scrub aliases from the frontend
81 app_aliases = dict(
81 app_aliases = dict(
82 ip = 'IPythonConsoleApp.ip',
82 ip = 'IPythonConsoleApp.ip',
83 transport = 'IPythonConsoleApp.transport',
83 transport = 'IPythonConsoleApp.transport',
84 hb = 'IPythonConsoleApp.hb_port',
84 hb = 'IPythonConsoleApp.hb_port',
85 shell = 'IPythonConsoleApp.shell_port',
85 shell = 'IPythonConsoleApp.shell_port',
86 iopub = 'IPythonConsoleApp.iopub_port',
86 iopub = 'IPythonConsoleApp.iopub_port',
87 stdin = 'IPythonConsoleApp.stdin_port',
87 stdin = 'IPythonConsoleApp.stdin_port',
88 existing = 'IPythonConsoleApp.existing',
88 existing = 'IPythonConsoleApp.existing',
89 f = 'IPythonConsoleApp.connection_file',
89 f = 'IPythonConsoleApp.connection_file',
90
90
91 kernel = 'IPythonConsoleApp.kernel_name',
91 kernel = 'IPythonConsoleApp.kernel_name',
92
92
93 ssh = 'IPythonConsoleApp.sshserver',
93 ssh = 'IPythonConsoleApp.sshserver',
94 )
94 )
95 aliases.update(app_aliases)
95 aliases.update(app_aliases)
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Classes
98 # Classes
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 # IPythonConsole
102 # IPythonConsole
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104
104
105 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
105 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
106
106
107 class IPythonConsoleApp(ConnectionFileMixin):
107 class IPythonConsoleApp(ConnectionFileMixin):
108 name = 'ipython-console-mixin'
108 name = 'ipython-console-mixin'
109
109
110 description = """
110 description = """
111 The IPython Mixin Console.
111 The IPython Mixin Console.
112
112
113 This class contains the common portions of console client (QtConsole,
113 This class contains the common portions of console client (QtConsole,
114 ZMQ-based terminal console, etc). It is not a full console, in that
114 ZMQ-based terminal console, etc). It is not a full console, in that
115 launched terminal subprocesses will not be able to accept input.
115 launched terminal subprocesses will not be able to accept input.
116
116
117 The Console using this mixing supports various extra features beyond
117 The Console using this mixing supports various extra features beyond
118 the single-process Terminal IPython shell, such as connecting to
118 the single-process Terminal IPython shell, such as connecting to
119 existing kernel, via:
119 existing kernel, via:
120
120
121 ipython <appname> --existing
121 ipython <appname> --existing
122
122
123 as well as tunnel via SSH
123 as well as tunnel via SSH
124
124
125 """
125 """
126
126
127 classes = classes
127 classes = classes
128 flags = Dict(flags)
128 flags = Dict(flags)
129 aliases = Dict(aliases)
129 aliases = Dict(aliases)
130 kernel_manager_class = KernelManager
130 kernel_manager_class = KernelManager
131 kernel_client_class = BlockingKernelClient
131 kernel_client_class = BlockingKernelClient
132
132
133 kernel_argv = List(Unicode)
133 kernel_argv = List(Unicode)
134 # frontend flags&aliases to be stripped when building kernel_argv
134 # frontend flags&aliases to be stripped when building kernel_argv
135 frontend_flags = Any(app_flags)
135 frontend_flags = Any(app_flags)
136 frontend_aliases = Any(app_aliases)
136 frontend_aliases = Any(app_aliases)
137
137
138 # create requested profiles by default, if they don't exist:
138 # create requested profiles by default, if they don't exist:
139 auto_create = CBool(True)
139 auto_create = CBool(True)
140 # connection info:
140 # connection info:
141
141
142 sshserver = Unicode('', config=True,
142 sshserver = Unicode('', config=True,
143 help="""The SSH server to use to connect to the kernel.""")
143 help="""The SSH server to use to connect to the kernel.""")
144 sshkey = Unicode('', config=True,
144 sshkey = Unicode('', config=True,
145 help="""Path to the ssh key to use for logging in to the ssh server.""")
145 help="""Path to the ssh key to use for logging in to the ssh server.""")
146
146
147 def _connection_file_default(self):
147 def _connection_file_default(self):
148 return 'kernel-%i.json' % os.getpid()
148 return 'kernel-%i.json' % os.getpid()
149
149
150 existing = CUnicode('', config=True,
150 existing = CUnicode('', config=True,
151 help="""Connect to an already running kernel""")
151 help="""Connect to an already running kernel""")
152
152
153 kernel_name = Unicode('python', config=True,
153 kernel_name = Unicode('python', config=True,
154 help="""The name of the default kernel to start.""")
154 help="""The name of the default kernel to start.""")
155
155
156 confirm_exit = CBool(True, config=True,
156 confirm_exit = CBool(True, config=True,
157 help="""
157 help="""
158 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
158 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
159 to force a direct exit without any confirmation.""",
159 to force a direct exit without any confirmation.""",
160 )
160 )
161
161
162
162
163 def build_kernel_argv(self, argv=None):
163 def build_kernel_argv(self, argv=None):
164 """build argv to be passed to kernel subprocess"""
164 """build argv to be passed to kernel subprocess"""
165 if argv is None:
165 if argv is None:
166 argv = sys.argv[1:]
166 argv = sys.argv[1:]
167 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
167 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
168
168
169 def init_connection_file(self):
169 def init_connection_file(self):
170 """find the connection file, and load the info if found.
170 """find the connection file, and load the info if found.
171
171
172 The current working directory and the current profile's security
172 The current working directory and the current profile's security
173 directory will be searched for the file if it is not given by
173 directory will be searched for the file if it is not given by
174 absolute path.
174 absolute path.
175
175
176 When attempting to connect to an existing kernel and the `--existing`
176 When attempting to connect to an existing kernel and the `--existing`
177 argument does not match an existing file, it will be interpreted as a
177 argument does not match an existing file, it will be interpreted as a
178 fileglob, and the matching file in the current profile's security dir
178 fileglob, and the matching file in the current profile's security dir
179 with the latest access time will be used.
179 with the latest access time will be used.
180
180
181 After this method is called, self.connection_file contains the *full path*
181 After this method is called, self.connection_file contains the *full path*
182 to the connection file, never just its name.
182 to the connection file, never just its name.
183 """
183 """
184 if self.existing:
184 if self.existing:
185 try:
185 try:
186 cf = find_connection_file(self.existing)
186 cf = find_connection_file(self.existing)
187 except Exception:
187 except Exception:
188 self.log.critical("Could not find existing kernel connection file %s", self.existing)
188 self.log.critical("Could not find existing kernel connection file %s", self.existing)
189 self.exit(1)
189 self.exit(1)
190 self.log.debug("Connecting to existing kernel: %s" % cf)
190 self.log.debug("Connecting to existing kernel: %s" % cf)
191 self.connection_file = cf
191 self.connection_file = cf
192 else:
192 else:
193 # not existing, check if we are going to write the file
193 # not existing, check if we are going to write the file
194 # and ensure that self.connection_file is a full path, not just the shortname
194 # and ensure that self.connection_file is a full path, not just the shortname
195 try:
195 try:
196 cf = find_connection_file(self.connection_file)
196 cf = find_connection_file(self.connection_file)
197 except Exception:
197 except Exception:
198 # file might not exist
198 # file might not exist
199 if self.connection_file == os.path.basename(self.connection_file):
199 if self.connection_file == os.path.basename(self.connection_file):
200 # just shortname, put it in security dir
200 # just shortname, put it in security dir
201 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
201 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
202 else:
202 else:
203 cf = self.connection_file
203 cf = self.connection_file
204 self.connection_file = cf
204 self.connection_file = cf
205 try:
205 try:
206 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
206 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
207 except IOError:
207 except IOError:
208 self.log.debug("Connection File not found: %s", self.connection_file)
208 self.log.debug("Connection File not found: %s", self.connection_file)
209 return
209 return
210
210
211 # should load_connection_file only be used for existing?
211 # should load_connection_file only be used for existing?
212 # as it is now, this allows reusing ports if an existing
212 # as it is now, this allows reusing ports if an existing
213 # file is requested
213 # file is requested
214 try:
214 try:
215 self.load_connection_file()
215 self.load_connection_file()
216 except Exception:
216 except Exception:
217 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
217 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
218 self.exit(1)
218 self.exit(1)
219
219
220 def init_ssh(self):
220 def init_ssh(self):
221 """set up ssh tunnels, if needed."""
221 """set up ssh tunnels, if needed."""
222 if not self.existing or (not self.sshserver and not self.sshkey):
222 if not self.existing or (not self.sshserver and not self.sshkey):
223 return
223 return
224 self.load_connection_file()
224 self.load_connection_file()
225
225
226 transport = self.transport
226 transport = self.transport
227 ip = self.ip
227 ip = self.ip
228
228
229 if transport != 'tcp':
229 if transport != 'tcp':
230 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
230 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
231 sys.exit(-1)
231 sys.exit(-1)
232
232
233 if self.sshkey and not self.sshserver:
233 if self.sshkey and not self.sshserver:
234 # specifying just the key implies that we are connecting directly
234 # specifying just the key implies that we are connecting directly
235 self.sshserver = ip
235 self.sshserver = ip
236 ip = localhost()
236 ip = localhost()
237
237
238 # build connection dict for tunnels:
238 # build connection dict for tunnels:
239 info = dict(ip=ip,
239 info = dict(ip=ip,
240 shell_port=self.shell_port,
240 shell_port=self.shell_port,
241 iopub_port=self.iopub_port,
241 iopub_port=self.iopub_port,
242 stdin_port=self.stdin_port,
242 stdin_port=self.stdin_port,
243 hb_port=self.hb_port
243 hb_port=self.hb_port
244 )
244 )
245
245
246 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
246 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
247
247
248 # tunnels return a new set of ports, which will be on localhost:
248 # tunnels return a new set of ports, which will be on localhost:
249 self.ip = localhost()
249 self.ip = localhost()
250 try:
250 try:
251 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
251 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
252 except:
252 except:
253 # even catch KeyboardInterrupt
253 # even catch KeyboardInterrupt
254 self.log.error("Could not setup tunnels", exc_info=True)
254 self.log.error("Could not setup tunnels", exc_info=True)
255 self.exit(1)
255 self.exit(1)
256
256
257 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
257 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
258
258
259 cf = self.connection_file
259 cf = self.connection_file
260 base,ext = os.path.splitext(cf)
260 base,ext = os.path.splitext(cf)
261 base = os.path.basename(base)
261 base = os.path.basename(base)
262 self.connection_file = os.path.basename(base)+'-ssh'+ext
262 self.connection_file = os.path.basename(base)+'-ssh'+ext
263 self.log.info("To connect another client via this tunnel, use:")
263 self.log.info("To connect another client via this tunnel, use:")
264 self.log.info("--existing %s" % self.connection_file)
264 self.log.info("--existing %s" % self.connection_file)
265
265
266 def _new_connection_file(self):
266 def _new_connection_file(self):
267 cf = ''
267 cf = ''
268 while not cf:
268 while not cf:
269 # we don't need a 128b id to distinguish kernels, use more readable
269 # we don't need a 128b id to distinguish kernels, use more readable
270 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
270 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
271 # kernels can subclass.
271 # kernels can subclass.
272 ident = str(uuid.uuid4()).split('-')[-1]
272 ident = str(uuid.uuid4()).split('-')[-1]
273 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
273 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
274 # only keep if it's actually new. Protect against unlikely collision
274 # only keep if it's actually new. Protect against unlikely collision
275 # in 48b random search space
275 # in 48b random search space
276 cf = cf if not os.path.exists(cf) else ''
276 cf = cf if not os.path.exists(cf) else ''
277 return cf
277 return cf
278
278
279 def init_kernel_manager(self):
279 def init_kernel_manager(self):
280 # Don't let Qt or ZMQ swallow KeyboardInterupts.
280 # Don't let Qt or ZMQ swallow KeyboardInterupts.
281 if self.existing:
281 if self.existing:
282 self.kernel_manager = None
282 self.kernel_manager = None
283 return
283 return
284 signal.signal(signal.SIGINT, signal.SIG_DFL)
284 signal.signal(signal.SIGINT, signal.SIG_DFL)
285
285
286 # Create a KernelManager and start a kernel.
286 # Create a KernelManager and start a kernel.
287 try:
287 try:
288 self.kernel_manager = self.kernel_manager_class(
288 self.kernel_manager = self.kernel_manager_class(
289 ip=self.ip,
289 ip=self.ip,
290 session=self.session,
290 transport=self.transport,
291 transport=self.transport,
291 shell_port=self.shell_port,
292 shell_port=self.shell_port,
292 iopub_port=self.iopub_port,
293 iopub_port=self.iopub_port,
293 stdin_port=self.stdin_port,
294 stdin_port=self.stdin_port,
294 hb_port=self.hb_port,
295 hb_port=self.hb_port,
295 connection_file=self.connection_file,
296 connection_file=self.connection_file,
296 kernel_name=self.kernel_name,
297 kernel_name=self.kernel_name,
297 parent=self,
298 parent=self,
298 ipython_dir=self.ipython_dir,
299 ipython_dir=self.ipython_dir,
299 )
300 )
300 except NoSuchKernel:
301 except NoSuchKernel:
301 self.log.critical("Could not find kernel %s", self.kernel_name)
302 self.log.critical("Could not find kernel %s", self.kernel_name)
302 self.exit(1)
303 self.exit(1)
303
304
304 self.kernel_manager.client_factory = self.kernel_client_class
305 self.kernel_manager.client_factory = self.kernel_client_class
305 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
306 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
306 atexit.register(self.kernel_manager.cleanup_ipc_files)
307 atexit.register(self.kernel_manager.cleanup_ipc_files)
307
308
308 if self.sshserver:
309 if self.sshserver:
309 # ssh, write new connection file
310 # ssh, write new connection file
310 self.kernel_manager.write_connection_file()
311 self.kernel_manager.write_connection_file()
311
312
312 # in case KM defaults / ssh writing changes things:
313 # in case KM defaults / ssh writing changes things:
313 km = self.kernel_manager
314 km = self.kernel_manager
314 self.shell_port=km.shell_port
315 self.shell_port=km.shell_port
315 self.iopub_port=km.iopub_port
316 self.iopub_port=km.iopub_port
316 self.stdin_port=km.stdin_port
317 self.stdin_port=km.stdin_port
317 self.hb_port=km.hb_port
318 self.hb_port=km.hb_port
318 self.connection_file = km.connection_file
319 self.connection_file = km.connection_file
319
320
320 atexit.register(self.kernel_manager.cleanup_connection_file)
321 atexit.register(self.kernel_manager.cleanup_connection_file)
321
322
322 def init_kernel_client(self):
323 def init_kernel_client(self):
323 if self.kernel_manager is not None:
324 if self.kernel_manager is not None:
324 self.kernel_client = self.kernel_manager.client()
325 self.kernel_client = self.kernel_manager.client()
325 else:
326 else:
326 self.kernel_client = self.kernel_client_class(
327 self.kernel_client = self.kernel_client_class(
328 session=self.session,
327 ip=self.ip,
329 ip=self.ip,
328 transport=self.transport,
330 transport=self.transport,
329 shell_port=self.shell_port,
331 shell_port=self.shell_port,
330 iopub_port=self.iopub_port,
332 iopub_port=self.iopub_port,
331 stdin_port=self.stdin_port,
333 stdin_port=self.stdin_port,
332 hb_port=self.hb_port,
334 hb_port=self.hb_port,
333 connection_file=self.connection_file,
335 connection_file=self.connection_file,
334 parent=self,
336 parent=self,
335 )
337 )
336
338
337 self.kernel_client.start_channels()
339 self.kernel_client.start_channels()
338
340
339
341
340
342
341 def initialize(self, argv=None):
343 def initialize(self, argv=None):
342 """
344 """
343 Classes which mix this class in should call:
345 Classes which mix this class in should call:
344 IPythonConsoleApp.initialize(self,argv)
346 IPythonConsoleApp.initialize(self,argv)
345 """
347 """
346 self.init_connection_file()
348 self.init_connection_file()
347 default_secure(self.config)
349 default_secure(self.config)
348 self.init_ssh()
350 self.init_ssh()
349 self.init_kernel_manager()
351 self.init_kernel_manager()
350 self.init_kernel_client()
352 self.init_kernel_client()
351
353
General Comments 0
You need to be logged in to leave comments. Login now