##// END OF EJS Templates
add ssh tunnel support to qtconsole
MinRK -
Show More
@@ -1,446 +1,483 b''
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
13 13 """
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib imports
20 20 import os
21 21 import signal
22 22 import sys
23 from getpass import getpass
23 24
24 25 # System library imports
25 26 from IPython.external.qt import QtGui
26 27 from pygments.styles import get_all_styles
27 28
29 # from IPython.external.ssh import tunnel
28 30 # Local imports
29 31 from IPython.config.application import boolean_flag
30 32 from IPython.core.application import BaseIPythonApplication
31 33 from IPython.core.profiledir import ProfileDir
32 34 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
33 35 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
34 36 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
35 37 from IPython.frontend.qt.console import styles
36 38 from IPython.frontend.qt.kernelmanager import QtKernelManager
39 from IPython.parallel.util import select_random_ports
37 40 from IPython.utils.traitlets import (
38 41 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
39 42 )
40 43 from IPython.zmq.ipkernel import (
41 44 flags as ipkernel_flags,
42 45 aliases as ipkernel_aliases,
43 46 IPKernelApp
44 47 )
45 48 from IPython.zmq.session import Session
46 49 from IPython.zmq.zmqshell import ZMQInteractiveShell
47 50
48 51
49 52 #-----------------------------------------------------------------------------
50 53 # Network Constants
51 54 #-----------------------------------------------------------------------------
52 55
53 56 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
54 57
55 58 #-----------------------------------------------------------------------------
56 59 # Globals
57 60 #-----------------------------------------------------------------------------
58 61
59 62 _examples = """
60 63 ipython qtconsole # start the qtconsole
61 64 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
62 65 """
63 66
64 67 #-----------------------------------------------------------------------------
65 68 # Classes
66 69 #-----------------------------------------------------------------------------
67 70
68 71 class MainWindow(QtGui.QMainWindow):
69 72
70 73 #---------------------------------------------------------------------------
71 74 # 'object' interface
72 75 #---------------------------------------------------------------------------
73 76
74 77 def __init__(self, app, frontend, existing=False, may_close=True,
75 78 confirm_exit=True):
76 79 """ Create a MainWindow for the specified FrontendWidget.
77 80
78 81 The app is passed as an argument to allow for different
79 82 closing behavior depending on whether we are the Kernel's parent.
80 83
81 84 If existing is True, then this Console does not own the Kernel.
82 85
83 86 If may_close is True, then this Console is permitted to close the kernel
84 87 """
85 88 super(MainWindow, self).__init__()
86 89 self._app = app
87 90 self._frontend = frontend
88 91 self._existing = existing
89 92 if existing:
90 93 self._may_close = may_close
91 94 else:
92 95 self._may_close = True
93 96 self._frontend.exit_requested.connect(self.close)
94 97 self._confirm_exit = confirm_exit
95 98 self.setCentralWidget(frontend)
96 99
97 100 #---------------------------------------------------------------------------
98 101 # QWidget interface
99 102 #---------------------------------------------------------------------------
100 103
101 104 def closeEvent(self, event):
102 105 """ Close the window and the kernel (if necessary).
103 106
104 107 This will prompt the user if they are finished with the kernel, and if
105 108 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
106 109 it closes without prompt.
107 110 """
108 111 keepkernel = None #Use the prompt by default
109 112 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
110 113 keepkernel = self._frontend._keep_kernel_on_exit
111 114
112 115 kernel_manager = self._frontend.kernel_manager
113 116
114 117 if keepkernel is None and not self._confirm_exit:
115 118 # don't prompt, just terminate the kernel if we own it
116 119 # or leave it alone if we don't
117 120 keepkernel = not self._existing
118 121
119 122 if keepkernel is None: #show prompt
120 123 if kernel_manager and kernel_manager.channels_running:
121 124 title = self.window().windowTitle()
122 125 cancel = QtGui.QMessageBox.Cancel
123 126 okay = QtGui.QMessageBox.Ok
124 127 if self._may_close:
125 128 msg = "You are closing this Console window."
126 129 info = "Would you like to quit the Kernel and all attached Consoles as well?"
127 130 justthis = QtGui.QPushButton("&No, just this Console", self)
128 131 justthis.setShortcut('N')
129 132 closeall = QtGui.QPushButton("&Yes, quit everything", self)
130 133 closeall.setShortcut('Y')
131 134 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
132 135 title, msg)
133 136 box.setInformativeText(info)
134 137 box.addButton(cancel)
135 138 box.addButton(justthis, QtGui.QMessageBox.NoRole)
136 139 box.addButton(closeall, QtGui.QMessageBox.YesRole)
137 140 box.setDefaultButton(closeall)
138 141 box.setEscapeButton(cancel)
139 142 reply = box.exec_()
140 143 if reply == 1: # close All
141 144 kernel_manager.shutdown_kernel()
142 145 #kernel_manager.stop_channels()
143 146 event.accept()
144 147 elif reply == 0: # close Console
145 148 if not self._existing:
146 149 # Have kernel: don't quit, just close the window
147 150 self._app.setQuitOnLastWindowClosed(False)
148 151 self.deleteLater()
149 152 event.accept()
150 153 else:
151 154 event.ignore()
152 155 else:
153 156 reply = QtGui.QMessageBox.question(self, title,
154 157 "Are you sure you want to close this Console?"+
155 158 "\nThe Kernel and other Consoles will remain active.",
156 159 okay|cancel,
157 160 defaultButton=okay
158 161 )
159 162 if reply == okay:
160 163 event.accept()
161 164 else:
162 165 event.ignore()
163 166 elif keepkernel: #close console but leave kernel running (no prompt)
164 167 if kernel_manager and kernel_manager.channels_running:
165 168 if not self._existing:
166 169 # I have the kernel: don't quit, just close the window
167 170 self._app.setQuitOnLastWindowClosed(False)
168 171 event.accept()
169 172 else: #close console and kernel (no prompt)
170 173 if kernel_manager and kernel_manager.channels_running:
171 174 kernel_manager.shutdown_kernel()
172 175 event.accept()
173 176
174 177 #-----------------------------------------------------------------------------
175 178 # Aliases and Flags
176 179 #-----------------------------------------------------------------------------
177 180
178 181 flags = dict(ipkernel_flags)
179 182 qt_flags = {
180 183 'existing' : ({'IPythonQtConsoleApp' : {'existing' : True}},
181 184 "Connect to an existing kernel."),
182 185 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
183 186 "Use a pure Python kernel instead of an IPython kernel."),
184 187 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
185 188 "Disable rich text support."),
186 189 }
187 190 qt_flags.update(boolean_flag(
188 191 'gui-completion', 'ConsoleWidget.gui_completion',
189 192 "use a GUI widget for tab completion",
190 193 "use plaintext output for completion"
191 194 ))
192 195 qt_flags.update(boolean_flag(
193 196 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
194 197 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
195 198 to force a direct exit without any confirmation.
196 199 """,
197 200 """Don't prompt the user when exiting. This will terminate the kernel
198 201 if it is owned by the frontend, and leave it alive if it is external.
199 202 """
200 203 ))
201 204 flags.update(qt_flags)
202 205 # the flags that are specific to the frontend
203 206 # these must be scrubbed before being passed to the kernel,
204 207 # or it will raise an error on unrecognized flags
205 208 qt_flags = qt_flags.keys()
206 209
207 210 aliases = dict(ipkernel_aliases)
208 211
209 212 qt_aliases = dict(
210 213 hb = 'IPythonQtConsoleApp.hb_port',
211 214 shell = 'IPythonQtConsoleApp.shell_port',
212 215 iopub = 'IPythonQtConsoleApp.iopub_port',
213 216 stdin = 'IPythonQtConsoleApp.stdin_port',
214 217 ip = 'IPythonQtConsoleApp.ip',
215 218
216 219 style = 'IPythonWidget.syntax_style',
217 220 stylesheet = 'IPythonQtConsoleApp.stylesheet',
218 221 colors = 'ZMQInteractiveShell.colors',
219 222
220 223 editor = 'IPythonWidget.editor',
221 224 paging = 'ConsoleWidget.paging',
225 ssh = 'IPythonQtConsoleApp.sshserver',
222 226 )
223 227 aliases.update(qt_aliases)
224 228 # also scrub aliases from the frontend
225 229 qt_flags.extend(qt_aliases.keys())
226 230
227 231
228 232 #-----------------------------------------------------------------------------
229 233 # IPythonQtConsole
230 234 #-----------------------------------------------------------------------------
231 235
232 236
233 237 class IPythonQtConsoleApp(BaseIPythonApplication):
234 238 name = 'ipython-qtconsole'
235 239 default_config_file_name='ipython_config.py'
236 240
237 241 description = """
238 242 The IPython QtConsole.
239 243
240 244 This launches a Console-style application using Qt. It is not a full
241 245 console, in that launched terminal subprocesses will not be able to accept
242 246 input.
243 247
244 248 The QtConsole supports various extra features beyond the Terminal IPython
245 249 shell, such as inline plotting with matplotlib, via:
246 250
247 251 ipython qtconsole --pylab=inline
248 252
249 253 as well as saving your session as HTML, and printing the output.
250 254
251 255 """
252 256 examples = _examples
253 257
254 258 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
255 259 flags = Dict(flags)
256 260 aliases = Dict(aliases)
257 261
258 262 kernel_argv = List(Unicode)
259 263
260 264 # create requested profiles by default, if they don't exist:
261 265 auto_create = CBool(True)
262 266 # connection info:
263 267 ip = Unicode(LOCALHOST, config=True,
264 268 help="""Set the kernel\'s IP address [default localhost].
265 269 If the IP address is something other than localhost, then
266 270 Consoles on other machines will be able to connect
267 271 to the Kernel, so be careful!"""
268 272 )
273
274 sshserver = Unicode('', config=True,
275 help="""The SSH server to use to connect to the kernel.""")
276 sshkey = Unicode('', config=True,
277 help="""Path to the ssh key to use for logging in to the ssh server.""")
278
269 279 hb_port = Int(0, config=True,
270 280 help="set the heartbeat port [default: random]")
271 281 shell_port = Int(0, config=True,
272 282 help="set the shell (XREP) port [default: random]")
273 283 iopub_port = Int(0, config=True,
274 284 help="set the iopub (PUB) port [default: random]")
275 285 stdin_port = Int(0, config=True,
276 286 help="set the stdin (XREQ) port [default: random]")
277 287
278 288 existing = CBool(False, config=True,
279 289 help="Whether to connect to an already running Kernel.")
280 290
281 291 stylesheet = Unicode('', config=True,
282 292 help="path to a custom CSS stylesheet")
283 293
284 294 pure = CBool(False, config=True,
285 295 help="Use a pure Python kernel instead of an IPython kernel.")
286 296 plain = CBool(False, config=True,
287 297 help="Use a plaintext widget instead of rich text (plain can't print/save).")
288 298
289 299 def _pure_changed(self, name, old, new):
290 300 kind = 'plain' if self.plain else 'rich'
291 301 self.config.ConsoleWidget.kind = kind
292 302 if self.pure:
293 303 self.widget_factory = FrontendWidget
294 304 elif self.plain:
295 305 self.widget_factory = IPythonWidget
296 306 else:
297 307 self.widget_factory = RichIPythonWidget
298 308
299 309 _plain_changed = _pure_changed
300 310
301 311 confirm_exit = CBool(True, config=True,
302 312 help="""
303 313 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
304 314 to force a direct exit without any confirmation.""",
305 315 )
306 316
307 317 # the factory for creating a widget
308 318 widget_factory = Any(RichIPythonWidget)
309 319
310 320 def parse_command_line(self, argv=None):
311 321 super(IPythonQtConsoleApp, self).parse_command_line(argv)
312 322 if argv is None:
313 323 argv = sys.argv[1:]
314 324
315 325 self.kernel_argv = list(argv) # copy
316 326 # kernel should inherit default config file from frontend
317 327 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
318 328 # scrub frontend-specific flags
319 329 for a in argv:
320 330
321 331 if a.startswith('-'):
322 332 key = a.lstrip('-').split('=')[0]
323 333 if key in qt_flags:
324 334 self.kernel_argv.remove(a)
325
335
336 def init_ssh(self):
337 # import here, to prevent circular import
338 from IPython.external.ssh import tunnel
339 if not self.sshserver and not self.sshkey:
340 return
341
342 if self.sshkey and not self.sshserver:
343 self.sshserver = self.ip
344 self.ip=LOCALHOST
345
346 lports = select_random_ports(4)
347 rports = self.shell_port, self.iopub_port, self.stdin_port, self.hb_port
348 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = lports
349
350 remote_ip = self.ip
351 self.ip = LOCALHOST
352 self.log.info("Forwarding connections to %s via %s"%(remote_ip, self.sshserver))
353
354 if tunnel.try_passwordless_ssh(self.sshserver, self.sshkey):
355 password=False
356 else:
357 password = getpass("SSH Password for %s: "%self.sshserver)
358
359 for lp,rp in zip(lports, rports):
360 tunnel.ssh_tunnel(lp, rp, self.sshserver, remote_ip, self.sshkey, password)
361
326 362 def init_kernel_manager(self):
327 363 # Don't let Qt or ZMQ swallow KeyboardInterupts.
328 364 signal.signal(signal.SIGINT, signal.SIG_DFL)
329 365
330 366 # Create a KernelManager and start a kernel.
331 367 self.kernel_manager = QtKernelManager(
332 368 shell_address=(self.ip, self.shell_port),
333 369 sub_address=(self.ip, self.iopub_port),
334 370 stdin_address=(self.ip, self.stdin_port),
335 371 hb_address=(self.ip, self.hb_port),
336 372 config=self.config
337 373 )
338 374 # start the kernel
339 375 if not self.existing:
340 376 kwargs = dict(ip=self.ip, ipython=not self.pure)
341 377 kwargs['extra_arguments'] = self.kernel_argv
342 378 self.kernel_manager.start_kernel(**kwargs)
343 379 self.kernel_manager.start_channels()
344 380
345 381
346 382 def init_qt_elements(self):
347 383 # Create the widget.
348 384 self.app = QtGui.QApplication([])
349 385 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
350 386 self.widget = self.widget_factory(config=self.config,
351 387 local_kernel=local_kernel)
352 388 self.widget.kernel_manager = self.kernel_manager
353 389 self.window = MainWindow(self.app, self.widget, self.existing,
354 390 may_close=local_kernel,
355 391 confirm_exit=self.confirm_exit)
356 392 self.window.setWindowTitle('Python' if self.pure else 'IPython')
357 393
358 394 def init_colors(self):
359 395 """Configure the coloring of the widget"""
360 396 # Note: This will be dramatically simplified when colors
361 397 # are removed from the backend.
362 398
363 399 if self.pure:
364 400 # only IPythonWidget supports styling
365 401 return
366 402
367 403 # parse the colors arg down to current known labels
368 404 try:
369 405 colors = self.config.ZMQInteractiveShell.colors
370 406 except AttributeError:
371 407 colors = None
372 408 try:
373 409 style = self.config.IPythonWidget.colors
374 410 except AttributeError:
375 411 style = None
376 412
377 413 # find the value for colors:
378 414 if colors:
379 415 colors=colors.lower()
380 416 if colors in ('lightbg', 'light'):
381 417 colors='lightbg'
382 418 elif colors in ('dark', 'linux'):
383 419 colors='linux'
384 420 else:
385 421 colors='nocolor'
386 422 elif style:
387 423 if style=='bw':
388 424 colors='nocolor'
389 425 elif styles.dark_style(style):
390 426 colors='linux'
391 427 else:
392 428 colors='lightbg'
393 429 else:
394 430 colors=None
395 431
396 432 # Configure the style.
397 433 widget = self.widget
398 434 if style:
399 435 widget.style_sheet = styles.sheet_from_template(style, colors)
400 436 widget.syntax_style = style
401 437 widget._syntax_style_changed()
402 438 widget._style_sheet_changed()
403 439 elif colors:
404 440 # use a default style
405 441 widget.set_default_style(colors=colors)
406 442 else:
407 443 # this is redundant for now, but allows the widget's
408 444 # defaults to change
409 445 widget.set_default_style()
410 446
411 447 if self.stylesheet:
412 448 # we got an expicit stylesheet
413 449 if os.path.isfile(self.stylesheet):
414 450 with open(self.stylesheet) as f:
415 451 sheet = f.read()
416 452 widget.style_sheet = sheet
417 453 widget._style_sheet_changed()
418 454 else:
419 455 raise IOError("Stylesheet %r not found."%self.stylesheet)
420 456
421 457 def initialize(self, argv=None):
422 458 super(IPythonQtConsoleApp, self).initialize(argv)
459 self.init_ssh()
423 460 self.init_kernel_manager()
424 461 self.init_qt_elements()
425 462 self.init_colors()
426 463
427 464 def start(self):
428 465
429 466 # draw the window
430 467 self.window.show()
431 468
432 469 # Start the application main loop.
433 470 self.app.exec_()
434 471
435 472 #-----------------------------------------------------------------------------
436 473 # Main entry point
437 474 #-----------------------------------------------------------------------------
438 475
439 476 def main():
440 477 app = IPythonQtConsoleApp()
441 478 app.initialize()
442 479 app.start()
443 480
444 481
445 482 if __name__ == '__main__':
446 483 main()
General Comments 0
You need to be logged in to leave comments. Login now