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