##// END OF EJS Templates
improve cleanup of connection files...
MinRK -
Show More
@@ -1,364 +1,383
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/frontend/qt/console/qtconsoleapp.py
6 6
7 7 Authors:
8 8
9 9 * Evan Patterson
10 10 * Min RK
11 11 * Erik Tollerud
12 12 * Fernando Perez
13 13 * Bussonnier Matthias
14 14 * Thomas Kluyver
15 15 * Paul Ivanov
16 16
17 17 """
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 # stdlib imports
24 import atexit
24 25 import json
25 26 import os
26 27 import signal
27 28 import sys
28 29 import uuid
29 30
30 31
31 32 # Local imports
32 33 from IPython.config.application import boolean_flag
33 34 from IPython.config.configurable import Configurable
34 35 from IPython.core.profiledir import ProfileDir
35 36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 37 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
37 38 from IPython.utils.path import filefind
38 39 from IPython.utils.py3compat import str_to_bytes
39 40 from IPython.utils.traitlets import (
40 41 Dict, List, Unicode, CUnicode, Int, CBool, Any
41 42 )
42 43 from IPython.zmq.ipkernel import (
43 44 flags as ipkernel_flags,
44 45 aliases as ipkernel_aliases,
45 46 IPKernelApp
46 47 )
47 48 from IPython.zmq.session import Session, default_secure
48 49 from IPython.zmq.zmqshell import ZMQInteractiveShell
49 50
50 51 #-----------------------------------------------------------------------------
51 52 # Network Constants
52 53 #-----------------------------------------------------------------------------
53 54
54 55 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
55 56
56 57 #-----------------------------------------------------------------------------
57 58 # Globals
58 59 #-----------------------------------------------------------------------------
59 60
60 61
61 62 #-----------------------------------------------------------------------------
62 63 # Aliases and Flags
63 64 #-----------------------------------------------------------------------------
64 65
65 66 flags = dict(ipkernel_flags)
66 67
67 68 # the flags that are specific to the frontend
68 69 # these must be scrubbed before being passed to the kernel,
69 70 # or it will raise an error on unrecognized flags
70 71 app_flags = {
71 72 'existing' : ({'IPythonMixinConsoleApp' : {'existing' : 'kernel*.json'}},
72 73 "Connect to an existing kernel. If no argument specified, guess most recent"),
73 74 }
74 75 app_flags.update(boolean_flag(
75 76 'confirm-exit', 'IPythonMixinConsoleApp.confirm_exit',
76 77 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
77 78 to force a direct exit without any confirmation.
78 79 """,
79 80 """Don't prompt the user when exiting. This will terminate the kernel
80 81 if it is owned by the frontend, and leave it alive if it is external.
81 82 """
82 83 ))
83 84 flags.update(app_flags)
84 85
85 86 aliases = dict(ipkernel_aliases)
86 87
87 88 # also scrub aliases from the frontend
88 89 app_aliases = dict(
89 90 hb = 'IPythonMixinConsoleApp.hb_port',
90 91 shell = 'IPythonMixinConsoleApp.shell_port',
91 92 iopub = 'IPythonMixinConsoleApp.iopub_port',
92 93 stdin = 'IPythonMixinConsoleApp.stdin_port',
93 94 ip = 'IPythonMixinConsoleApp.ip',
94 95 existing = 'IPythonMixinConsoleApp.existing',
95 96 f = 'IPythonMixinConsoleApp.connection_file',
96 97
97 98
98 99 ssh = 'IPythonMixinConsoleApp.sshserver',
99 100 )
100 101 aliases.update(app_aliases)
101 102
102 103 #-----------------------------------------------------------------------------
103 104 # Classes
104 105 #-----------------------------------------------------------------------------
105 106
106 107 #-----------------------------------------------------------------------------
107 108 # IPythonMixinConsole
108 109 #-----------------------------------------------------------------------------
109 110
110 111
111 112 class IPythonMixinConsoleApp(Configurable):
112 113 name = 'ipython-console-mixin'
113 114 default_config_file_name='ipython_config.py'
114 115
115 116 description = """
116 117 The IPython Mixin Console.
117 118
118 119 This class contains the common portions of console client (QtConsole,
119 120 ZMQ-based terminal console, etc). It is not a full console, in that
120 121 launched terminal subprocesses will not be able to accept input.
121 122
122 123 The Console using this mixing supports various extra features beyond
123 124 the single-process Terminal IPython shell, such as connecting to
124 125 existing kernel, via:
125 126
126 127 ipython <appname> --existing
127 128
128 129 as well as tunnel via SSH
129 130
130 131 """
131 132
132 133 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
133 134 flags = Dict(flags)
134 135 aliases = Dict(aliases)
135 136 kernel_manager_class = BlockingKernelManager
136 137
137 138 kernel_argv = List(Unicode)
138 139
139 140 pure = CBool(False, config=True,
140 141 help="Use a pure Python kernel instead of an IPython kernel.")
141 142 # create requested profiles by default, if they don't exist:
142 143 auto_create = CBool(True)
143 144 # connection info:
144 145 ip = Unicode(LOCALHOST, config=True,
145 146 help="""Set the kernel\'s IP address [default localhost].
146 147 If the IP address is something other than localhost, then
147 148 Consoles on other machines will be able to connect
148 149 to the Kernel, so be careful!"""
149 150 )
150 151
151 152 sshserver = Unicode('', config=True,
152 153 help="""The SSH server to use to connect to the kernel.""")
153 154 sshkey = Unicode('', config=True,
154 155 help="""Path to the ssh key to use for logging in to the ssh server.""")
155 156
156 157 hb_port = Int(0, config=True,
157 158 help="set the heartbeat port [default: random]")
158 159 shell_port = Int(0, config=True,
159 160 help="set the shell (XREP) port [default: random]")
160 161 iopub_port = Int(0, config=True,
161 162 help="set the iopub (PUB) port [default: random]")
162 163 stdin_port = Int(0, config=True,
163 164 help="set the stdin (XREQ) port [default: random]")
164 165 connection_file = Unicode('', config=True,
165 166 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
166 167
167 168 This file will contain the IP, ports, and authentication key needed to connect
168 169 clients to this kernel. By default, this file will be created in the security-dir
169 170 of the current profile, but can be specified by absolute path.
170 171 """)
171 172 def _connection_file_default(self):
172 173 return 'kernel-%i.json' % os.getpid()
173 174
174 175 existing = CUnicode('', config=True,
175 176 help="""Connect to an already running kernel""")
176 177
177 178 confirm_exit = CBool(True, config=True,
178 179 help="""
179 180 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
180 181 to force a direct exit without any confirmation.""",
181 182 )
182 183
183 184
184 185 def parse_command_line(self, argv=None):
185 186 #super(PythonBaseConsoleApp, self).parse_command_line(argv)
186 187 # make this stuff after this a function, in case the super stuff goes
187 188 # away. Also, Min notes that this functionality should be moved to a
188 189 # generic library of kernel stuff
189 190 self.swallow_args(app_aliases,app_flags,argv=argv)
190 191
191 192 def swallow_args(self, aliases,flags, argv=None):
192 193 if argv is None:
193 194 argv = sys.argv[1:]
194 195 self.kernel_argv = list(argv) # copy
195 196 # kernel should inherit default config file from frontend
196 197 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
197 198 # Scrub frontend-specific flags
198 199 swallow_next = False
199 200 was_flag = False
200 201 # copy again, in case some aliases have the same name as a flag
201 202 # argv = list(self.kernel_argv)
202 203 for a in argv:
203 204 if swallow_next:
204 205 swallow_next = False
205 206 # last arg was an alias, remove the next one
206 207 # *unless* the last alias has a no-arg flag version, in which
207 208 # case, don't swallow the next arg if it's also a flag:
208 209 if not (was_flag and a.startswith('-')):
209 210 self.kernel_argv.remove(a)
210 211 continue
211 212 if a.startswith('-'):
212 213 split = a.lstrip('-').split('=')
213 214 alias = split[0]
214 215 if alias in aliases:
215 216 self.kernel_argv.remove(a)
216 217 if len(split) == 1:
217 218 # alias passed with arg via space
218 219 swallow_next = True
219 220 # could have been a flag that matches an alias, e.g. `existing`
220 221 # in which case, we might not swallow the next arg
221 222 was_flag = alias in flags
222 223 elif alias in flags:
223 224 # strip flag, but don't swallow next, as flags don't take args
224 225 self.kernel_argv.remove(a)
225 226
226 227 def init_connection_file(self):
227 228 """find the connection file, and load the info if found.
228 229
229 230 The current working directory and the current profile's security
230 231 directory will be searched for the file if it is not given by
231 232 absolute path.
232 233
233 234 When attempting to connect to an existing kernel and the `--existing`
234 235 argument does not match an existing file, it will be interpreted as a
235 236 fileglob, and the matching file in the current profile's security dir
236 237 with the latest access time will be used.
238
239 After this method is called, self.connection_file contains the *full path*
240 to the connection file, never just its name.
237 241 """
238 242 if self.existing:
239 243 try:
240 244 cf = find_connection_file(self.existing)
241 245 except Exception:
242 246 self.log.critical("Could not find existing kernel connection file %s", self.existing)
243 247 self.exit(1)
244 248 self.log.info("Connecting to existing kernel: %s" % cf)
245 249 self.connection_file = cf
250 else:
251 # not existing, check if we are going to write the file
252 # and ensure that self.connection_file is a full path, not just the shortname
253 try:
254 cf = find_connection_file(self.connection_file)
255 except Exception:
256 # file might not exist
257 if self.connection_file == os.path.basename(self.connection_file):
258 # just shortname, put it in security dir
259 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
260 else:
261 cf = self.connection_file
262 self.connection_file = cf
263
246 264 # should load_connection_file only be used for existing?
247 265 # as it is now, this allows reusing ports if an existing
248 266 # file is requested
249 267 try:
250 268 self.load_connection_file()
251 269 except Exception:
252 270 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
253 271 self.exit(1)
254 272
255 273 def load_connection_file(self):
256 274 """load ip/port/hmac config from JSON connection file"""
257 275 # this is identical to KernelApp.load_connection_file
258 276 # perhaps it can be centralized somewhere?
259 277 try:
260 278 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
261 279 except IOError:
262 280 self.log.debug("Connection File not found: %s", self.connection_file)
263 281 return
264 282 self.log.debug(u"Loading connection file %s", fname)
265 283 with open(fname) as f:
266 284 s = f.read()
267 285 cfg = json.loads(s)
268 286 if self.ip == LOCALHOST and 'ip' in cfg:
269 287 # not overridden by config or cl_args
270 288 self.ip = cfg['ip']
271 289 for channel in ('hb', 'shell', 'iopub', 'stdin'):
272 290 name = channel + '_port'
273 291 if getattr(self, name) == 0 and name in cfg:
274 292 # not overridden by config or cl_args
275 293 setattr(self, name, cfg[name])
276 294 if 'key' in cfg:
277 295 self.config.Session.key = str_to_bytes(cfg['key'])
278 296
279 297 def init_ssh(self):
280 298 """set up ssh tunnels, if needed."""
281 299 if not self.sshserver and not self.sshkey:
282 300 return
283 301
284 302 if self.sshkey and not self.sshserver:
285 303 # specifying just the key implies that we are connecting directly
286 304 self.sshserver = self.ip
287 305 self.ip = LOCALHOST
288 306
289 307 # build connection dict for tunnels:
290 308 info = dict(ip=self.ip,
291 309 shell_port=self.shell_port,
292 310 iopub_port=self.iopub_port,
293 311 stdin_port=self.stdin_port,
294 312 hb_port=self.hb_port
295 313 )
296 314
297 315 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
298 316
299 317 # tunnels return a new set of ports, which will be on localhost:
300 318 self.ip = LOCALHOST
301 319 try:
302 320 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
303 321 except:
304 322 # even catch KeyboardInterrupt
305 323 self.log.error("Could not setup tunnels", exc_info=True)
306 324 self.exit(1)
307 325
308 326 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
309 327
310 328 cf = self.connection_file
311 329 base,ext = os.path.splitext(cf)
312 330 base = os.path.basename(base)
313 331 self.connection_file = os.path.basename(base)+'-ssh'+ext
314 332 self.log.critical("To connect another client via this tunnel, use:")
315 333 self.log.critical("--existing %s" % self.connection_file)
316 334
317 335 def _new_connection_file(self):
318 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
336 cf = ''
337 while not cf:
338 # we don't need a 128b id to distinguish kernels, use more readable
339 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
340 # kernels can subclass.
341 ident = str(uuid.uuid4()).split('-')[-1]
342 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
343 # only keep if it's actually new. Protect against unlikely collision
344 # in 48b random search space
345 cf = cf if not os.path.exists(cf) else ''
346 return cf
319 347
320 348 def init_kernel_manager(self):
321 349 # Don't let Qt or ZMQ swallow KeyboardInterupts.
322 350 signal.signal(signal.SIGINT, signal.SIG_DFL)
323 sec = self.profile_dir.security_dir
324 try:
325 cf = filefind(self.connection_file, ['.', sec])
326 except IOError:
327 # file might not exist
328 if self.connection_file == os.path.basename(self.connection_file):
329 # just shortname, put it in security dir
330 cf = os.path.join(sec, self.connection_file)
331 else:
332 cf = self.connection_file
333 351
334 352 # Create a KernelManager and start a kernel.
335 353 self.kernel_manager = self.kernel_manager_class(
336 354 ip=self.ip,
337 355 shell_port=self.shell_port,
338 356 iopub_port=self.iopub_port,
339 357 stdin_port=self.stdin_port,
340 358 hb_port=self.hb_port,
341 connection_file=cf,
359 connection_file=self.connection_file,
342 360 config=self.config,
343 361 )
344 362 # start the kernel
345 363 if not self.existing:
346 364 kwargs = dict(ipython=not self.pure)
347 365 kwargs['extra_arguments'] = self.kernel_argv
348 366 self.kernel_manager.start_kernel(**kwargs)
349 367 elif self.sshserver:
350 368 # ssh, write new connection file
351 369 self.kernel_manager.write_connection_file()
370 atexit.register(self.kernel_manager.cleanup_connection_file)
352 371 self.kernel_manager.start_channels()
353 372
354 373
355 374 def initialize(self, argv=None):
356 375 """
357 376 Classes which mix this class in should call:
358 377 IPythonMixinConsoleApp.initialize(self,argv)
359 378 """
360 379 self.init_connection_file()
361 380 default_secure(self.config)
362 381 self.init_ssh()
363 382 self.init_kernel_manager()
364 383
@@ -1,327 +1,341
1 1 """ A minimal application using the Qt console-style IPython frontend.
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.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14 * Paul Ivanov
15 15
16 16 """
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # stdlib imports
23 23 import json
24 24 import os
25 25 import signal
26 26 import sys
27 27 import uuid
28 28
29 29 # System library imports
30 from IPython.external.qt import QtGui
30 from IPython.external.qt import QtCore, QtGui
31 31
32 32 # Local imports
33 33 from IPython.config.application import boolean_flag, catch_config_error
34 34 from IPython.core.application import BaseIPythonApplication
35 35 from IPython.core.profiledir import ProfileDir
36 36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
37 37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
38 38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
39 39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
40 40 from IPython.frontend.qt.console import styles
41 41 from IPython.frontend.qt.console.mainwindow import MainWindow
42 42 from IPython.frontend.qt.kernelmanager import QtKernelManager
43 43 from IPython.utils.path import filefind
44 44 from IPython.utils.py3compat import str_to_bytes
45 45 from IPython.utils.traitlets import (
46 46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
47 47 )
48 48 from IPython.zmq.ipkernel import IPKernelApp
49 49 from IPython.zmq.session import Session, default_secure
50 50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 51
52 52 from IPython.frontend.kernelmixinapp import (
53 53 IPythonMixinConsoleApp, app_aliases, app_flags
54 54 )
55 55
56 56 #-----------------------------------------------------------------------------
57 57 # Network Constants
58 58 #-----------------------------------------------------------------------------
59 59
60 60 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
61 61
62 62 #-----------------------------------------------------------------------------
63 63 # Globals
64 64 #-----------------------------------------------------------------------------
65 65
66 66 _examples = """
67 67 ipython qtconsole # start the qtconsole
68 68 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
69 69 """
70 70
71 71 #-----------------------------------------------------------------------------
72 72 # Aliases and Flags
73 73 #-----------------------------------------------------------------------------
74 74
75 75 # XXX: the app_flags should really be flags from the mixin
76 76 flags = dict(app_flags)
77 77 qt_flags = {
78 78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 79 "Use a pure Python kernel instead of an IPython kernel."),
80 80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 81 "Disable rich text support."),
82 82 }
83 83 qt_flags.update(boolean_flag(
84 84 'gui-completion', 'ConsoleWidget.gui_completion',
85 85 "use a GUI widget for tab completion",
86 86 "use plaintext output for completion"
87 87 ))
88 88 flags.update(qt_flags)
89 89
90 90 aliases = dict(app_aliases)
91 91
92 92 qt_aliases = dict(
93 93
94 94 style = 'IPythonWidget.syntax_style',
95 95 stylesheet = 'IPythonQtConsoleApp.stylesheet',
96 96 colors = 'ZMQInteractiveShell.colors',
97 97
98 98 editor = 'IPythonWidget.editor',
99 99 paging = 'ConsoleWidget.paging',
100 100 )
101 101 aliases.update(qt_aliases)
102 102
103 103 #-----------------------------------------------------------------------------
104 104 # Classes
105 105 #-----------------------------------------------------------------------------
106 106
107 107 #-----------------------------------------------------------------------------
108 108 # IPythonQtConsole
109 109 #-----------------------------------------------------------------------------
110 110
111 111
112 112 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonMixinConsoleApp):
113 113 name = 'ipython-qtconsole'
114 114
115 115 description = """
116 116 The IPython QtConsole.
117 117
118 118 This launches a Console-style application using Qt. It is not a full
119 119 console, in that launched terminal subprocesses will not be able to accept
120 120 input.
121 121
122 122 The QtConsole supports various extra features beyond the Terminal IPython
123 123 shell, such as inline plotting with matplotlib, via:
124 124
125 125 ipython qtconsole --pylab=inline
126 126
127 127 as well as saving your session as HTML, and printing the output.
128 128
129 129 """
130 130 examples = _examples
131 131
132 132 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
133 133 flags = Dict(flags)
134 134 aliases = Dict(aliases)
135 135 kernel_manager_class = QtKernelManager
136 136
137 137 stylesheet = Unicode('', config=True,
138 138 help="path to a custom CSS stylesheet")
139 139
140 140 plain = CBool(False, config=True,
141 141 help="Use a plaintext widget instead of rich text (plain can't print/save).")
142 142
143 143 def _pure_changed(self, name, old, new):
144 144 kind = 'plain' if self.plain else 'rich'
145 145 self.config.ConsoleWidget.kind = kind
146 146 if self.pure:
147 147 self.widget_factory = FrontendWidget
148 148 elif self.plain:
149 149 self.widget_factory = IPythonWidget
150 150 else:
151 151 self.widget_factory = RichIPythonWidget
152 152
153 153 _plain_changed = _pure_changed
154 154
155 155 # the factory for creating a widget
156 156 widget_factory = Any(RichIPythonWidget)
157 157
158 158 def parse_command_line(self, argv=None):
159 159 super(IPythonQtConsoleApp, self).parse_command_line(argv)
160 160 IPythonMixinConsoleApp.parse_command_line(self,argv)
161 161 self.swallow_args(qt_aliases,qt_flags,argv=argv)
162 162
163 163
164 164
165 165
166 166 def new_frontend_master(self):
167 167 """ Create and return new frontend attached to new kernel, launched on localhost.
168 168 """
169 169 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
170 170 kernel_manager = QtKernelManager(
171 171 ip=ip,
172 172 connection_file=self._new_connection_file(),
173 173 config=self.config,
174 174 )
175 175 # start the kernel
176 176 kwargs = dict(ipython=not self.pure)
177 177 kwargs['extra_arguments'] = self.kernel_argv
178 178 kernel_manager.start_kernel(**kwargs)
179 179 kernel_manager.start_channels()
180 180 widget = self.widget_factory(config=self.config,
181 181 local_kernel=True)
182 182 widget.kernel_manager = kernel_manager
183 183 widget._existing = False
184 184 widget._may_close = True
185 185 widget._confirm_exit = self.confirm_exit
186 186 return widget
187 187
188 188 def new_frontend_slave(self, current_widget):
189 189 """Create and return a new frontend attached to an existing kernel.
190 190
191 191 Parameters
192 192 ----------
193 193 current_widget : IPythonWidget
194 194 The IPythonWidget whose kernel this frontend is to share
195 195 """
196 196 kernel_manager = QtKernelManager(
197 197 connection_file=current_widget.kernel_manager.connection_file,
198 198 config = self.config,
199 199 )
200 200 kernel_manager.load_connection_file()
201 201 kernel_manager.start_channels()
202 202 widget = self.widget_factory(config=self.config,
203 203 local_kernel=False)
204 204 widget._existing = True
205 205 widget._may_close = False
206 206 widget._confirm_exit = False
207 207 widget.kernel_manager = kernel_manager
208 208 return widget
209 209
210 210 def init_qt_elements(self):
211 211 # Create the widget.
212 212 self.app = QtGui.QApplication([])
213 213
214 214 base_path = os.path.abspath(os.path.dirname(__file__))
215 215 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
216 216 self.app.icon = QtGui.QIcon(icon_path)
217 217 QtGui.QApplication.setWindowIcon(self.app.icon)
218 218
219 219 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
220 220 self.widget = self.widget_factory(config=self.config,
221 221 local_kernel=local_kernel)
222 222 self.widget._existing = self.existing
223 223 self.widget._may_close = not self.existing
224 224 self.widget._confirm_exit = self.confirm_exit
225 225
226 226 self.widget.kernel_manager = self.kernel_manager
227 227 self.window = MainWindow(self.app,
228 228 confirm_exit=self.confirm_exit,
229 229 new_frontend_factory=self.new_frontend_master,
230 230 slave_frontend_factory=self.new_frontend_slave,
231 231 )
232 232 self.window.log = self.log
233 233 self.window.add_tab_with_frontend(self.widget)
234 234 self.window.init_menu_bar()
235 235
236 236 self.window.setWindowTitle('Python' if self.pure else 'IPython')
237 237
238 238 def init_colors(self):
239 239 """Configure the coloring of the widget"""
240 240 # Note: This will be dramatically simplified when colors
241 241 # are removed from the backend.
242 242
243 243 if self.pure:
244 244 # only IPythonWidget supports styling
245 245 return
246 246
247 247 # parse the colors arg down to current known labels
248 248 try:
249 249 colors = self.config.ZMQInteractiveShell.colors
250 250 except AttributeError:
251 251 colors = None
252 252 try:
253 253 style = self.config.IPythonWidget.syntax_style
254 254 except AttributeError:
255 255 style = None
256 256
257 257 # find the value for colors:
258 258 if colors:
259 259 colors=colors.lower()
260 260 if colors in ('lightbg', 'light'):
261 261 colors='lightbg'
262 262 elif colors in ('dark', 'linux'):
263 263 colors='linux'
264 264 else:
265 265 colors='nocolor'
266 266 elif style:
267 267 if style=='bw':
268 268 colors='nocolor'
269 269 elif styles.dark_style(style):
270 270 colors='linux'
271 271 else:
272 272 colors='lightbg'
273 273 else:
274 274 colors=None
275 275
276 276 # Configure the style.
277 277 widget = self.widget
278 278 if style:
279 279 widget.style_sheet = styles.sheet_from_template(style, colors)
280 280 widget.syntax_style = style
281 281 widget._syntax_style_changed()
282 282 widget._style_sheet_changed()
283 283 elif colors:
284 284 # use a default style
285 285 widget.set_default_style(colors=colors)
286 286 else:
287 287 # this is redundant for now, but allows the widget's
288 288 # defaults to change
289 289 widget.set_default_style()
290 290
291 291 if self.stylesheet:
292 292 # we got an expicit stylesheet
293 293 if os.path.isfile(self.stylesheet):
294 294 with open(self.stylesheet) as f:
295 295 sheet = f.read()
296 296 widget.style_sheet = sheet
297 297 widget._style_sheet_changed()
298 298 else:
299 299 raise IOError("Stylesheet %r not found."%self.stylesheet)
300 300
301 def init_signal(self):
302 """allow clean shutdown on sigint"""
303 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
304 # need a timer, so that QApplication doesn't block until a real
305 # Qt event fires (can require mouse movement)
306 # timer trick from http://stackoverflow.com/q/4938723/938949
307 timer = QtCore.QTimer()
308 # Let the interpreter run each 200 ms:
309 timer.timeout.connect(lambda: None)
310 timer.start(200)
311 # hold onto ref, so the timer doesn't get cleaned up
312 self._sigint_timer = timer
313
301 314 @catch_config_error
302 315 def initialize(self, argv=None):
303 316 super(IPythonQtConsoleApp, self).initialize(argv)
304 317 IPythonMixinConsoleApp.initialize(self,argv)
305 318 self.init_qt_elements()
306 319 self.init_colors()
320 self.init_signal()
307 321
308 322 def start(self):
309 323
310 324 # draw the window
311 325 self.window.show()
312 326
313 327 # Start the application main loop.
314 328 self.app.exec_()
315 329
316 330 #-----------------------------------------------------------------------------
317 331 # Main entry point
318 332 #-----------------------------------------------------------------------------
319 333
320 334 def main():
321 335 app = IPythonQtConsoleApp()
322 336 app.initialize()
323 337 app.start()
324 338
325 339
326 340 if __name__ == '__main__':
327 341 main()
@@ -1,947 +1,954
1 1 """Base classes to manage the interaction with a running kernel.
2 2
3 3 TODO
4 4 * Create logger to handle debugging and console messages.
5 5 """
6 6
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (C) 2008-2011 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING, distributed as part of this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17
18 18 # Standard library imports.
19 19 import errno
20 20 import json
21 21 from subprocess import Popen
22 22 import os
23 23 import signal
24 24 import sys
25 25 from threading import Thread
26 26 import time
27 27
28 28 # System library imports.
29 29 import zmq
30 30 from zmq.eventloop import ioloop, zmqstream
31 31
32 32 # Local imports.
33 33 from IPython.config.loader import Config
34 34 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
35 35 from IPython.utils.traitlets import (
36 36 HasTraits, Any, Instance, Type, Unicode, Integer, Bool
37 37 )
38 38 from IPython.utils.py3compat import str_to_bytes
39 39 from IPython.zmq.entry_point import write_connection_file
40 40 from session import Session
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # Constants and exceptions
44 44 #-----------------------------------------------------------------------------
45 45
46 46 class InvalidPortNumber(Exception):
47 47 pass
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Utility functions
51 51 #-----------------------------------------------------------------------------
52 52
53 53 # some utilities to validate message structure, these might get moved elsewhere
54 54 # if they prove to have more generic utility
55 55
56 56 def validate_string_list(lst):
57 57 """Validate that the input is a list of strings.
58 58
59 59 Raises ValueError if not."""
60 60 if not isinstance(lst, list):
61 61 raise ValueError('input %r must be a list' % lst)
62 62 for x in lst:
63 63 if not isinstance(x, basestring):
64 64 raise ValueError('element %r in list must be a string' % x)
65 65
66 66
67 67 def validate_string_dict(dct):
68 68 """Validate that the input is a dict with string keys and values.
69 69
70 70 Raises ValueError if not."""
71 71 for k,v in dct.iteritems():
72 72 if not isinstance(k, basestring):
73 73 raise ValueError('key %r in dict must be a string' % k)
74 74 if not isinstance(v, basestring):
75 75 raise ValueError('value %r in dict must be a string' % v)
76 76
77 77
78 78 #-----------------------------------------------------------------------------
79 79 # ZMQ Socket Channel classes
80 80 #-----------------------------------------------------------------------------
81 81
82 82 class ZMQSocketChannel(Thread):
83 83 """The base class for the channels that use ZMQ sockets.
84 84 """
85 85 context = None
86 86 session = None
87 87 socket = None
88 88 ioloop = None
89 89 stream = None
90 90 _address = None
91 91
92 92 def __init__(self, context, session, address):
93 93 """Create a channel
94 94
95 95 Parameters
96 96 ----------
97 97 context : :class:`zmq.Context`
98 98 The ZMQ context to use.
99 99 session : :class:`session.Session`
100 100 The session to use.
101 101 address : tuple
102 102 Standard (ip, port) tuple that the kernel is listening on.
103 103 """
104 104 super(ZMQSocketChannel, self).__init__()
105 105 self.daemon = True
106 106
107 107 self.context = context
108 108 self.session = session
109 109 if address[1] == 0:
110 110 message = 'The port number for a channel cannot be 0.'
111 111 raise InvalidPortNumber(message)
112 112 self._address = address
113 113
114 114 def _run_loop(self):
115 115 """Run my loop, ignoring EINTR events in the poller"""
116 116 while True:
117 117 try:
118 118 self.ioloop.start()
119 119 except zmq.ZMQError as e:
120 120 if e.errno == errno.EINTR:
121 121 continue
122 122 else:
123 123 raise
124 124 else:
125 125 break
126 126
127 127 def stop(self):
128 128 """Stop the channel's activity.
129 129
130 130 This calls :method:`Thread.join` and returns when the thread
131 131 terminates. :class:`RuntimeError` will be raised if
132 132 :method:`self.start` is called again.
133 133 """
134 134 self.join()
135 135
136 136 @property
137 137 def address(self):
138 138 """Get the channel's address as an (ip, port) tuple.
139 139
140 140 By the default, the address is (localhost, 0), where 0 means a random
141 141 port.
142 142 """
143 143 return self._address
144 144
145 145 def _queue_send(self, msg):
146 146 """Queue a message to be sent from the IOLoop's thread.
147 147
148 148 Parameters
149 149 ----------
150 150 msg : message to send
151 151
152 152 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
153 153 thread control of the action.
154 154 """
155 155 def thread_send():
156 156 self.session.send(self.stream, msg)
157 157 self.ioloop.add_callback(thread_send)
158 158
159 159 def _handle_recv(self, msg):
160 160 """callback for stream.on_recv
161 161
162 162 unpacks message, and calls handlers with it.
163 163 """
164 164 ident,smsg = self.session.feed_identities(msg)
165 165 self.call_handlers(self.session.unserialize(smsg))
166 166
167 167
168 168
169 169 class ShellSocketChannel(ZMQSocketChannel):
170 170 """The XREQ channel for issues request/replies to the kernel.
171 171 """
172 172
173 173 command_queue = None
174 174 # flag for whether execute requests should be allowed to call raw_input:
175 175 allow_stdin = True
176 176
177 177 def __init__(self, context, session, address):
178 178 super(ShellSocketChannel, self).__init__(context, session, address)
179 179 self.ioloop = ioloop.IOLoop()
180 180
181 181 def run(self):
182 182 """The thread's main activity. Call start() instead."""
183 183 self.socket = self.context.socket(zmq.DEALER)
184 184 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
185 185 self.socket.connect('tcp://%s:%i' % self.address)
186 186 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
187 187 self.stream.on_recv(self._handle_recv)
188 188 self._run_loop()
189 189
190 190 def stop(self):
191 191 self.ioloop.stop()
192 192 super(ShellSocketChannel, self).stop()
193 193
194 194 def call_handlers(self, msg):
195 195 """This method is called in the ioloop thread when a message arrives.
196 196
197 197 Subclasses should override this method to handle incoming messages.
198 198 It is important to remember that this method is called in the thread
199 199 so that some logic must be done to ensure that the application leve
200 200 handlers are called in the application thread.
201 201 """
202 202 raise NotImplementedError('call_handlers must be defined in a subclass.')
203 203
204 204 def execute(self, code, silent=False,
205 205 user_variables=None, user_expressions=None, allow_stdin=None):
206 206 """Execute code in the kernel.
207 207
208 208 Parameters
209 209 ----------
210 210 code : str
211 211 A string of Python code.
212 212
213 213 silent : bool, optional (default False)
214 214 If set, the kernel will execute the code as quietly possible.
215 215
216 216 user_variables : list, optional
217 217 A list of variable names to pull from the user's namespace. They
218 218 will come back as a dict with these names as keys and their
219 219 :func:`repr` as values.
220 220
221 221 user_expressions : dict, optional
222 222 A dict with string keys and to pull from the user's
223 223 namespace. They will come back as a dict with these names as keys
224 224 and their :func:`repr` as values.
225 225
226 226 allow_stdin : bool, optional
227 227 Flag for
228 228 A dict with string keys and to pull from the user's
229 229 namespace. They will come back as a dict with these names as keys
230 230 and their :func:`repr` as values.
231 231
232 232 Returns
233 233 -------
234 234 The msg_id of the message sent.
235 235 """
236 236 if user_variables is None:
237 237 user_variables = []
238 238 if user_expressions is None:
239 239 user_expressions = {}
240 240 if allow_stdin is None:
241 241 allow_stdin = self.allow_stdin
242 242
243 243
244 244 # Don't waste network traffic if inputs are invalid
245 245 if not isinstance(code, basestring):
246 246 raise ValueError('code %r must be a string' % code)
247 247 validate_string_list(user_variables)
248 248 validate_string_dict(user_expressions)
249 249
250 250 # Create class for content/msg creation. Related to, but possibly
251 251 # not in Session.
252 252 content = dict(code=code, silent=silent,
253 253 user_variables=user_variables,
254 254 user_expressions=user_expressions,
255 255 allow_stdin=allow_stdin,
256 256 )
257 257 msg = self.session.msg('execute_request', content)
258 258 self._queue_send(msg)
259 259 return msg['header']['msg_id']
260 260
261 261 def complete(self, text, line, cursor_pos, block=None):
262 262 """Tab complete text in the kernel's namespace.
263 263
264 264 Parameters
265 265 ----------
266 266 text : str
267 267 The text to complete.
268 268 line : str
269 269 The full line of text that is the surrounding context for the
270 270 text to complete.
271 271 cursor_pos : int
272 272 The position of the cursor in the line where the completion was
273 273 requested.
274 274 block : str, optional
275 275 The full block of code in which the completion is being requested.
276 276
277 277 Returns
278 278 -------
279 279 The msg_id of the message sent.
280 280 """
281 281 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
282 282 msg = self.session.msg('complete_request', content)
283 283 self._queue_send(msg)
284 284 return msg['header']['msg_id']
285 285
286 286 def object_info(self, oname):
287 287 """Get metadata information about an object.
288 288
289 289 Parameters
290 290 ----------
291 291 oname : str
292 292 A string specifying the object name.
293 293
294 294 Returns
295 295 -------
296 296 The msg_id of the message sent.
297 297 """
298 298 content = dict(oname=oname)
299 299 msg = self.session.msg('object_info_request', content)
300 300 self._queue_send(msg)
301 301 return msg['header']['msg_id']
302 302
303 303 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
304 304 """Get entries from the history list.
305 305
306 306 Parameters
307 307 ----------
308 308 raw : bool
309 309 If True, return the raw input.
310 310 output : bool
311 311 If True, then return the output as well.
312 312 hist_access_type : str
313 313 'range' (fill in session, start and stop params), 'tail' (fill in n)
314 314 or 'search' (fill in pattern param).
315 315
316 316 session : int
317 317 For a range request, the session from which to get lines. Session
318 318 numbers are positive integers; negative ones count back from the
319 319 current session.
320 320 start : int
321 321 The first line number of a history range.
322 322 stop : int
323 323 The final (excluded) line number of a history range.
324 324
325 325 n : int
326 326 The number of lines of history to get for a tail request.
327 327
328 328 pattern : str
329 329 The glob-syntax pattern for a search request.
330 330
331 331 Returns
332 332 -------
333 333 The msg_id of the message sent.
334 334 """
335 335 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
336 336 **kwargs)
337 337 msg = self.session.msg('history_request', content)
338 338 self._queue_send(msg)
339 339 return msg['header']['msg_id']
340 340
341 341 def shutdown(self, restart=False):
342 342 """Request an immediate kernel shutdown.
343 343
344 344 Upon receipt of the (empty) reply, client code can safely assume that
345 345 the kernel has shut down and it's safe to forcefully terminate it if
346 346 it's still alive.
347 347
348 348 The kernel will send the reply via a function registered with Python's
349 349 atexit module, ensuring it's truly done as the kernel is done with all
350 350 normal operation.
351 351 """
352 352 # Send quit message to kernel. Once we implement kernel-side setattr,
353 353 # this should probably be done that way, but for now this will do.
354 354 msg = self.session.msg('shutdown_request', {'restart':restart})
355 355 self._queue_send(msg)
356 356 return msg['header']['msg_id']
357 357
358 358
359 359
360 360 class SubSocketChannel(ZMQSocketChannel):
361 361 """The SUB channel which listens for messages that the kernel publishes.
362 362 """
363 363
364 364 def __init__(self, context, session, address):
365 365 super(SubSocketChannel, self).__init__(context, session, address)
366 366 self.ioloop = ioloop.IOLoop()
367 367
368 368 def run(self):
369 369 """The thread's main activity. Call start() instead."""
370 370 self.socket = self.context.socket(zmq.SUB)
371 371 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
372 372 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
373 373 self.socket.connect('tcp://%s:%i' % self.address)
374 374 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
375 375 self.stream.on_recv(self._handle_recv)
376 376 self._run_loop()
377 377
378 378 def stop(self):
379 379 self.ioloop.stop()
380 380 super(SubSocketChannel, self).stop()
381 381
382 382 def call_handlers(self, msg):
383 383 """This method is called in the ioloop thread when a message arrives.
384 384
385 385 Subclasses should override this method to handle incoming messages.
386 386 It is important to remember that this method is called in the thread
387 387 so that some logic must be done to ensure that the application leve
388 388 handlers are called in the application thread.
389 389 """
390 390 raise NotImplementedError('call_handlers must be defined in a subclass.')
391 391
392 392 def flush(self, timeout=1.0):
393 393 """Immediately processes all pending messages on the SUB channel.
394 394
395 395 Callers should use this method to ensure that :method:`call_handlers`
396 396 has been called for all messages that have been received on the
397 397 0MQ SUB socket of this channel.
398 398
399 399 This method is thread safe.
400 400
401 401 Parameters
402 402 ----------
403 403 timeout : float, optional
404 404 The maximum amount of time to spend flushing, in seconds. The
405 405 default is one second.
406 406 """
407 407 # We do the IOLoop callback process twice to ensure that the IOLoop
408 408 # gets to perform at least one full poll.
409 409 stop_time = time.time() + timeout
410 410 for i in xrange(2):
411 411 self._flushed = False
412 412 self.ioloop.add_callback(self._flush)
413 413 while not self._flushed and time.time() < stop_time:
414 414 time.sleep(0.01)
415 415
416 416 def _flush(self):
417 417 """Callback for :method:`self.flush`."""
418 418 self.stream.flush()
419 419 self._flushed = True
420 420
421 421
422 422 class StdInSocketChannel(ZMQSocketChannel):
423 423 """A reply channel to handle raw_input requests that the kernel makes."""
424 424
425 425 msg_queue = None
426 426
427 427 def __init__(self, context, session, address):
428 428 super(StdInSocketChannel, self).__init__(context, session, address)
429 429 self.ioloop = ioloop.IOLoop()
430 430
431 431 def run(self):
432 432 """The thread's main activity. Call start() instead."""
433 433 self.socket = self.context.socket(zmq.DEALER)
434 434 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
435 435 self.socket.connect('tcp://%s:%i' % self.address)
436 436 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
437 437 self.stream.on_recv(self._handle_recv)
438 438 self._run_loop()
439 439
440 440 def stop(self):
441 441 self.ioloop.stop()
442 442 super(StdInSocketChannel, self).stop()
443 443
444 444 def call_handlers(self, msg):
445 445 """This method is called in the ioloop thread when a message arrives.
446 446
447 447 Subclasses should override this method to handle incoming messages.
448 448 It is important to remember that this method is called in the thread
449 449 so that some logic must be done to ensure that the application leve
450 450 handlers are called in the application thread.
451 451 """
452 452 raise NotImplementedError('call_handlers must be defined in a subclass.')
453 453
454 454 def input(self, string):
455 455 """Send a string of raw input to the kernel."""
456 456 content = dict(value=string)
457 457 msg = self.session.msg('input_reply', content)
458 458 self._queue_send(msg)
459 459
460 460
461 461 class HBSocketChannel(ZMQSocketChannel):
462 462 """The heartbeat channel which monitors the kernel heartbeat.
463 463
464 464 Note that the heartbeat channel is paused by default. As long as you start
465 465 this channel, the kernel manager will ensure that it is paused and un-paused
466 466 as appropriate.
467 467 """
468 468
469 469 time_to_dead = 3.0
470 470 socket = None
471 471 poller = None
472 472 _running = None
473 473 _pause = None
474 474
475 475 def __init__(self, context, session, address):
476 476 super(HBSocketChannel, self).__init__(context, session, address)
477 477 self._running = False
478 478 self._pause = True
479 479
480 480 def _create_socket(self):
481 481 self.socket = self.context.socket(zmq.REQ)
482 482 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
483 483 self.socket.connect('tcp://%s:%i' % self.address)
484 484 self.poller = zmq.Poller()
485 485 self.poller.register(self.socket, zmq.POLLIN)
486 486
487 487 def run(self):
488 488 """The thread's main activity. Call start() instead."""
489 489 self._create_socket()
490 490 self._running = True
491 491 while self._running:
492 492 if self._pause:
493 493 time.sleep(self.time_to_dead)
494 494 else:
495 495 since_last_heartbeat = 0.0
496 496 request_time = time.time()
497 497 try:
498 498 #io.rprint('Ping from HB channel') # dbg
499 499 self.socket.send(b'ping')
500 500 except zmq.ZMQError, e:
501 501 #io.rprint('*** HB Error:', e) # dbg
502 502 if e.errno == zmq.EFSM:
503 503 #io.rprint('sleep...', self.time_to_dead) # dbg
504 504 time.sleep(self.time_to_dead)
505 505 self._create_socket()
506 506 else:
507 507 raise
508 508 else:
509 509 while True:
510 510 try:
511 511 self.socket.recv(zmq.NOBLOCK)
512 512 except zmq.ZMQError, e:
513 513 #io.rprint('*** HB Error 2:', e) # dbg
514 514 if e.errno == zmq.EAGAIN:
515 515 before_poll = time.time()
516 516 until_dead = self.time_to_dead - (before_poll -
517 517 request_time)
518 518
519 519 # When the return value of poll() is an empty
520 520 # list, that is when things have gone wrong
521 521 # (zeromq bug). As long as it is not an empty
522 522 # list, poll is working correctly even if it
523 523 # returns quickly. Note: poll timeout is in
524 524 # milliseconds.
525 525 if until_dead > 0.0:
526 526 while True:
527 527 try:
528 528 self.poller.poll(1000 * until_dead)
529 529 except zmq.ZMQError as e:
530 530 if e.errno == errno.EINTR:
531 531 continue
532 532 else:
533 533 raise
534 534 else:
535 535 break
536 536
537 537 since_last_heartbeat = time.time()-request_time
538 538 if since_last_heartbeat > self.time_to_dead:
539 539 self.call_handlers(since_last_heartbeat)
540 540 break
541 541 else:
542 542 # FIXME: We should probably log this instead.
543 543 raise
544 544 else:
545 545 until_dead = self.time_to_dead - (time.time() -
546 546 request_time)
547 547 if until_dead > 0.0:
548 548 #io.rprint('sleep...', self.time_to_dead) # dbg
549 549 time.sleep(until_dead)
550 550 break
551 551
552 552 def pause(self):
553 553 """Pause the heartbeat."""
554 554 self._pause = True
555 555
556 556 def unpause(self):
557 557 """Unpause the heartbeat."""
558 558 self._pause = False
559 559
560 560 def is_beating(self):
561 561 """Is the heartbeat running and not paused."""
562 562 if self.is_alive() and not self._pause:
563 563 return True
564 564 else:
565 565 return False
566 566
567 567 def stop(self):
568 568 self._running = False
569 569 super(HBSocketChannel, self).stop()
570 570
571 571 def call_handlers(self, since_last_heartbeat):
572 572 """This method is called in the ioloop thread when a message arrives.
573 573
574 574 Subclasses should override this method to handle incoming messages.
575 575 It is important to remember that this method is called in the thread
576 576 so that some logic must be done to ensure that the application leve
577 577 handlers are called in the application thread.
578 578 """
579 579 raise NotImplementedError('call_handlers must be defined in a subclass.')
580 580
581 581
582 582 #-----------------------------------------------------------------------------
583 583 # Main kernel manager class
584 584 #-----------------------------------------------------------------------------
585 585
586 586 class KernelManager(HasTraits):
587 587 """ Manages a kernel for a frontend.
588 588
589 589 The SUB channel is for the frontend to receive messages published by the
590 590 kernel.
591 591
592 592 The REQ channel is for the frontend to make requests of the kernel.
593 593
594 594 The REP channel is for the kernel to request stdin (raw_input) from the
595 595 frontend.
596 596 """
597 597 # config object for passing to child configurables
598 598 config = Instance(Config)
599 599
600 600 # The PyZMQ Context to use for communication with the kernel.
601 601 context = Instance(zmq.Context)
602 602 def _context_default(self):
603 603 return zmq.Context.instance()
604 604
605 605 # The Session to use for communication with the kernel.
606 606 session = Instance(Session)
607 607
608 608 # The kernel process with which the KernelManager is communicating.
609 609 kernel = Instance(Popen)
610 610
611 611 # The addresses for the communication channels.
612 612 connection_file = Unicode('')
613 613 ip = Unicode(LOCALHOST)
614 614 def _ip_changed(self, name, old, new):
615 615 if new == '*':
616 616 self.ip = '0.0.0.0'
617 617 shell_port = Integer(0)
618 618 iopub_port = Integer(0)
619 619 stdin_port = Integer(0)
620 620 hb_port = Integer(0)
621 621
622 622 # The classes to use for the various channels.
623 623 shell_channel_class = Type(ShellSocketChannel)
624 624 sub_channel_class = Type(SubSocketChannel)
625 625 stdin_channel_class = Type(StdInSocketChannel)
626 626 hb_channel_class = Type(HBSocketChannel)
627 627
628 628 # Protected traits.
629 629 _launch_args = Any
630 630 _shell_channel = Any
631 631 _sub_channel = Any
632 632 _stdin_channel = Any
633 633 _hb_channel = Any
634 634 _connection_file_written=Bool(False)
635 635
636 636 def __init__(self, **kwargs):
637 637 super(KernelManager, self).__init__(**kwargs)
638 638 if self.session is None:
639 639 self.session = Session(config=self.config)
640 640
641 641 def __del__(self):
642 if self._connection_file_written:
643 # cleanup connection files on full shutdown of kernel we started
644 self._connection_file_written = False
645 try:
646 os.remove(self.connection_file)
647 except IOError:
648 pass
642 self.cleanup_connection_file()
649 643
650 644
651 645 #--------------------------------------------------------------------------
652 646 # Channel management methods:
653 647 #--------------------------------------------------------------------------
654 648
655 649 def start_channels(self, shell=True, sub=True, stdin=True, hb=True):
656 650 """Starts the channels for this kernel.
657 651
658 652 This will create the channels if they do not exist and then start
659 653 them. If port numbers of 0 are being used (random ports) then you
660 654 must first call :method:`start_kernel`. If the channels have been
661 655 stopped and you call this, :class:`RuntimeError` will be raised.
662 656 """
663 657 if shell:
664 658 self.shell_channel.start()
665 659 if sub:
666 660 self.sub_channel.start()
667 661 if stdin:
668 662 self.stdin_channel.start()
669 663 self.shell_channel.allow_stdin = True
670 664 else:
671 665 self.shell_channel.allow_stdin = False
672 666 if hb:
673 667 self.hb_channel.start()
674 668
675 669 def stop_channels(self):
676 670 """Stops all the running channels for this kernel.
677 671 """
678 672 if self.shell_channel.is_alive():
679 673 self.shell_channel.stop()
680 674 if self.sub_channel.is_alive():
681 675 self.sub_channel.stop()
682 676 if self.stdin_channel.is_alive():
683 677 self.stdin_channel.stop()
684 678 if self.hb_channel.is_alive():
685 679 self.hb_channel.stop()
686 680
687 681 @property
688 682 def channels_running(self):
689 683 """Are any of the channels created and running?"""
690 684 return (self.shell_channel.is_alive() or self.sub_channel.is_alive() or
691 685 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
692 686
693 687 #--------------------------------------------------------------------------
694 688 # Kernel process management methods:
695 689 #--------------------------------------------------------------------------
696 690
691 def cleanup_connection_file(self):
692 """cleanup connection file *if we wrote it*
693
694 Will not raise if the connection file was already removed somehow.
695 """
696 if self._connection_file_written:
697 # cleanup connection files on full shutdown of kernel we started
698 self._connection_file_written = False
699 try:
700 os.remove(self.connection_file)
701 except OSError:
702 pass
703
697 704 def load_connection_file(self):
698 705 """load connection info from JSON dict in self.connection_file"""
699 706 with open(self.connection_file) as f:
700 707 cfg = json.loads(f.read())
701 708
702 709 self.ip = cfg['ip']
703 710 self.shell_port = cfg['shell_port']
704 711 self.stdin_port = cfg['stdin_port']
705 712 self.iopub_port = cfg['iopub_port']
706 713 self.hb_port = cfg['hb_port']
707 714 self.session.key = str_to_bytes(cfg['key'])
708 715
709 716 def write_connection_file(self):
710 717 """write connection info to JSON dict in self.connection_file"""
711 718 if self._connection_file_written:
712 719 return
713 720 self.connection_file,cfg = write_connection_file(self.connection_file,
714 721 ip=self.ip, key=self.session.key,
715 722 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
716 723 shell_port=self.shell_port, hb_port=self.hb_port)
717 724 # write_connection_file also sets default ports:
718 725 self.shell_port = cfg['shell_port']
719 726 self.stdin_port = cfg['stdin_port']
720 727 self.iopub_port = cfg['iopub_port']
721 728 self.hb_port = cfg['hb_port']
722 729
723 730 self._connection_file_written = True
724 731
725 732 def start_kernel(self, **kw):
726 733 """Starts a kernel process and configures the manager to use it.
727 734
728 735 If random ports (port=0) are being used, this method must be called
729 736 before the channels are created.
730 737
731 738 Parameters:
732 739 -----------
733 740 ipython : bool, optional (default True)
734 741 Whether to use an IPython kernel instead of a plain Python kernel.
735 742
736 743 launcher : callable, optional (default None)
737 744 A custom function for launching the kernel process (generally a
738 745 wrapper around ``entry_point.base_launch_kernel``). In most cases,
739 746 it should not be necessary to use this parameter.
740 747
741 748 **kw : optional
742 749 See respective options for IPython and Python kernels.
743 750 """
744 751 if self.ip not in LOCAL_IPS:
745 752 raise RuntimeError("Can only launch a kernel on a local interface. "
746 753 "Make sure that the '*_address' attributes are "
747 754 "configured properly. "
748 755 "Currently valid addresses are: %s"%LOCAL_IPS
749 756 )
750 757
751 758 # write connection file / get default ports
752 759 self.write_connection_file()
753 760
754 761 self._launch_args = kw.copy()
755 762 launch_kernel = kw.pop('launcher', None)
756 763 if launch_kernel is None:
757 764 if kw.pop('ipython', True):
758 765 from ipkernel import launch_kernel
759 766 else:
760 767 from pykernel import launch_kernel
761 768 self.kernel = launch_kernel(fname=self.connection_file, **kw)
762 769
763 770 def shutdown_kernel(self, restart=False):
764 771 """ Attempts to the stop the kernel process cleanly. If the kernel
765 772 cannot be stopped, it is killed, if possible.
766 773 """
767 774 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
768 775 if sys.platform == 'win32':
769 776 self.kill_kernel()
770 777 return
771 778
772 779 # Pause the heart beat channel if it exists.
773 780 if self._hb_channel is not None:
774 781 self._hb_channel.pause()
775 782
776 783 # Don't send any additional kernel kill messages immediately, to give
777 784 # the kernel a chance to properly execute shutdown actions. Wait for at
778 785 # most 1s, checking every 0.1s.
779 786 self.shell_channel.shutdown(restart=restart)
780 787 for i in range(10):
781 788 if self.is_alive:
782 789 time.sleep(0.1)
783 790 else:
784 791 break
785 792 else:
786 793 # OK, we've waited long enough.
787 794 if self.has_kernel:
788 795 self.kill_kernel()
789 796
790 797 if not restart and self._connection_file_written:
791 798 # cleanup connection files on full shutdown of kernel we started
792 799 self._connection_file_written = False
793 800 try:
794 801 os.remove(self.connection_file)
795 802 except IOError:
796 803 pass
797 804
798 805 def restart_kernel(self, now=False, **kw):
799 806 """Restarts a kernel with the arguments that were used to launch it.
800 807
801 808 If the old kernel was launched with random ports, the same ports will be
802 809 used for the new kernel.
803 810
804 811 Parameters
805 812 ----------
806 813 now : bool, optional
807 814 If True, the kernel is forcefully restarted *immediately*, without
808 815 having a chance to do any cleanup action. Otherwise the kernel is
809 816 given 1s to clean up before a forceful restart is issued.
810 817
811 818 In all cases the kernel is restarted, the only difference is whether
812 819 it is given a chance to perform a clean shutdown or not.
813 820
814 821 **kw : optional
815 822 Any options specified here will replace those used to launch the
816 823 kernel.
817 824 """
818 825 if self._launch_args is None:
819 826 raise RuntimeError("Cannot restart the kernel. "
820 827 "No previous call to 'start_kernel'.")
821 828 else:
822 829 # Stop currently running kernel.
823 830 if self.has_kernel:
824 831 if now:
825 832 self.kill_kernel()
826 833 else:
827 834 self.shutdown_kernel(restart=True)
828 835
829 836 # Start new kernel.
830 837 self._launch_args.update(kw)
831 838 self.start_kernel(**self._launch_args)
832 839
833 840 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
834 841 # unless there is some delay here.
835 842 if sys.platform == 'win32':
836 843 time.sleep(0.2)
837 844
838 845 @property
839 846 def has_kernel(self):
840 847 """Returns whether a kernel process has been specified for the kernel
841 848 manager.
842 849 """
843 850 return self.kernel is not None
844 851
845 852 def kill_kernel(self):
846 853 """ Kill the running kernel. """
847 854 if self.has_kernel:
848 855 # Pause the heart beat channel if it exists.
849 856 if self._hb_channel is not None:
850 857 self._hb_channel.pause()
851 858
852 859 # Attempt to kill the kernel.
853 860 try:
854 861 self.kernel.kill()
855 862 except OSError, e:
856 863 # In Windows, we will get an Access Denied error if the process
857 864 # has already terminated. Ignore it.
858 865 if sys.platform == 'win32':
859 866 if e.winerror != 5:
860 867 raise
861 868 # On Unix, we may get an ESRCH error if the process has already
862 869 # terminated. Ignore it.
863 870 else:
864 871 from errno import ESRCH
865 872 if e.errno != ESRCH:
866 873 raise
867 874 self.kernel = None
868 875 else:
869 876 raise RuntimeError("Cannot kill kernel. No kernel is running!")
870 877
871 878 def interrupt_kernel(self):
872 879 """ Interrupts the kernel. Unlike ``signal_kernel``, this operation is
873 880 well supported on all platforms.
874 881 """
875 882 if self.has_kernel:
876 883 if sys.platform == 'win32':
877 884 from parentpoller import ParentPollerWindows as Poller
878 885 Poller.send_interrupt(self.kernel.win32_interrupt_event)
879 886 else:
880 887 self.kernel.send_signal(signal.SIGINT)
881 888 else:
882 889 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
883 890
884 891 def signal_kernel(self, signum):
885 892 """ Sends a signal to the kernel. Note that since only SIGTERM is
886 893 supported on Windows, this function is only useful on Unix systems.
887 894 """
888 895 if self.has_kernel:
889 896 self.kernel.send_signal(signum)
890 897 else:
891 898 raise RuntimeError("Cannot signal kernel. No kernel is running!")
892 899
893 900 @property
894 901 def is_alive(self):
895 902 """Is the kernel process still running?"""
896 903 # FIXME: not using a heartbeat means this method is broken for any
897 904 # remote kernel, it's only capable of handling local kernels.
898 905 if self.has_kernel:
899 906 if self.kernel.poll() is None:
900 907 return True
901 908 else:
902 909 return False
903 910 else:
904 911 # We didn't start the kernel with this KernelManager so we don't
905 912 # know if it is running. We should use a heartbeat for this case.
906 913 return True
907 914
908 915 #--------------------------------------------------------------------------
909 916 # Channels used for communication with the kernel:
910 917 #--------------------------------------------------------------------------
911 918
912 919 @property
913 920 def shell_channel(self):
914 921 """Get the REQ socket channel object to make requests of the kernel."""
915 922 if self._shell_channel is None:
916 923 self._shell_channel = self.shell_channel_class(self.context,
917 924 self.session,
918 925 (self.ip, self.shell_port))
919 926 return self._shell_channel
920 927
921 928 @property
922 929 def sub_channel(self):
923 930 """Get the SUB socket channel object."""
924 931 if self._sub_channel is None:
925 932 self._sub_channel = self.sub_channel_class(self.context,
926 933 self.session,
927 934 (self.ip, self.iopub_port))
928 935 return self._sub_channel
929 936
930 937 @property
931 938 def stdin_channel(self):
932 939 """Get the REP socket channel object to handle stdin (raw_input)."""
933 940 if self._stdin_channel is None:
934 941 self._stdin_channel = self.stdin_channel_class(self.context,
935 942 self.session,
936 943 (self.ip, self.stdin_port))
937 944 return self._stdin_channel
938 945
939 946 @property
940 947 def hb_channel(self):
941 948 """Get the heartbeat socket channel object to check that the
942 949 kernel is alive."""
943 950 if self._hb_channel is None:
944 951 self._hb_channel = self.hb_channel_class(self.context,
945 952 self.session,
946 953 (self.ip, self.hb_port))
947 954 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now