##// END OF EJS Templates
ip/transport live in KernelManager now...
MinRK -
Show More
@@ -1,378 +1,361 b''
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations. This is a
4 input, there is no real readline support, among other limitations. This is a
5 refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py
5 refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py
6
6
7 Authors:
7 Authors:
8
8
9 * Evan Patterson
9 * Evan Patterson
10 * Min RK
10 * Min RK
11 * Erik Tollerud
11 * Erik Tollerud
12 * Fernando Perez
12 * Fernando Perez
13 * Bussonnier Matthias
13 * Bussonnier Matthias
14 * Thomas Kluyver
14 * Thomas Kluyver
15 * Paul Ivanov
15 * Paul Ivanov
16
16
17 """
17 """
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 # stdlib imports
23 # stdlib imports
24 import atexit
24 import atexit
25 import json
25 import json
26 import os
26 import os
27 import shutil
27 import shutil
28 import signal
28 import signal
29 import sys
29 import sys
30 import uuid
30 import uuid
31
31
32
32
33 # Local imports
33 # Local imports
34 from IPython.config.application import boolean_flag
34 from IPython.config.application import boolean_flag
35 from IPython.config.configurable import Configurable
35 from IPython.config.configurable import Configurable
36 from IPython.core.profiledir import ProfileDir
36 from IPython.core.profiledir import ProfileDir
37 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
37 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
38 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
38 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
39 from IPython.utils.path import filefind
39 from IPython.utils.path import filefind
40 from IPython.utils.py3compat import str_to_bytes
40 from IPython.utils.py3compat import str_to_bytes
41 from IPython.utils.traitlets import (
41 from IPython.utils.traitlets import (
42 Dict, List, Unicode, CUnicode, Int, CBool, Any, CaselessStrEnum
42 Dict, List, Unicode, CUnicode, Int, CBool, Any, CaselessStrEnum
43 )
43 )
44 from IPython.zmq.ipkernel import (
44 from IPython.zmq.ipkernel import (
45 flags as ipkernel_flags,
45 flags as ipkernel_flags,
46 aliases as ipkernel_aliases,
46 aliases as ipkernel_aliases,
47 IPKernelApp
47 IPKernelApp
48 )
48 )
49 from IPython.zmq.session import Session, default_secure
49 from IPython.zmq.session import Session, default_secure
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # Network Constants
53 # Network Constants
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
56 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
57
57
58 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
59 # Globals
59 # Globals
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61
61
62
62
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64 # Aliases and Flags
64 # Aliases and Flags
65 #-----------------------------------------------------------------------------
65 #-----------------------------------------------------------------------------
66
66
67 flags = dict(ipkernel_flags)
67 flags = dict(ipkernel_flags)
68
68
69 # the flags that are specific to the frontend
69 # the flags that are specific to the frontend
70 # these must be scrubbed before being passed to the kernel,
70 # these must be scrubbed before being passed to the kernel,
71 # or it will raise an error on unrecognized flags
71 # or it will raise an error on unrecognized flags
72 app_flags = {
72 app_flags = {
73 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
73 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
74 "Connect to an existing kernel. If no argument specified, guess most recent"),
74 "Connect to an existing kernel. If no argument specified, guess most recent"),
75 }
75 }
76 app_flags.update(boolean_flag(
76 app_flags.update(boolean_flag(
77 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
77 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
78 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
78 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
79 to force a direct exit without any confirmation.
79 to force a direct exit without any confirmation.
80 """,
80 """,
81 """Don't prompt the user when exiting. This will terminate the kernel
81 """Don't prompt the user when exiting. This will terminate the kernel
82 if it is owned by the frontend, and leave it alive if it is external.
82 if it is owned by the frontend, and leave it alive if it is external.
83 """
83 """
84 ))
84 ))
85 flags.update(app_flags)
85 flags.update(app_flags)
86
86
87 aliases = dict(ipkernel_aliases)
87 aliases = dict(ipkernel_aliases)
88
88
89 # also scrub aliases from the frontend
89 # also scrub aliases from the frontend
90 app_aliases = dict(
90 app_aliases = dict(
91 ip = 'KernelManager.ip',
92 transport = 'KernelManager.transport',
91 hb = 'IPythonConsoleApp.hb_port',
93 hb = 'IPythonConsoleApp.hb_port',
92 shell = 'IPythonConsoleApp.shell_port',
94 shell = 'IPythonConsoleApp.shell_port',
93 iopub = 'IPythonConsoleApp.iopub_port',
95 iopub = 'IPythonConsoleApp.iopub_port',
94 stdin = 'IPythonConsoleApp.stdin_port',
96 stdin = 'IPythonConsoleApp.stdin_port',
95 ip = 'IPythonConsoleApp.ip',
96 existing = 'IPythonConsoleApp.existing',
97 existing = 'IPythonConsoleApp.existing',
97 f = 'IPythonConsoleApp.connection_file',
98 f = 'IPythonConsoleApp.connection_file',
98
99
99
100
100 ssh = 'IPythonConsoleApp.sshserver',
101 ssh = 'IPythonConsoleApp.sshserver',
101 )
102 )
102 aliases.update(app_aliases)
103 aliases.update(app_aliases)
103
104
104 #-----------------------------------------------------------------------------
105 #-----------------------------------------------------------------------------
105 # Classes
106 # Classes
106 #-----------------------------------------------------------------------------
107 #-----------------------------------------------------------------------------
107
108
108 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
109 # IPythonConsole
110 # IPythonConsole
110 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
111
112
112 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
113 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
113
114
114 try:
115 try:
115 from IPython.zmq.pylab.backend_inline import InlineBackend
116 from IPython.zmq.pylab.backend_inline import InlineBackend
116 except ImportError:
117 except ImportError:
117 pass
118 pass
118 else:
119 else:
119 classes.append(InlineBackend)
120 classes.append(InlineBackend)
120
121
121 class IPythonConsoleApp(Configurable):
122 class IPythonConsoleApp(Configurable):
122 name = 'ipython-console-mixin'
123 name = 'ipython-console-mixin'
123 default_config_file_name='ipython_config.py'
124 default_config_file_name='ipython_config.py'
124
125
125 description = """
126 description = """
126 The IPython Mixin Console.
127 The IPython Mixin Console.
127
128
128 This class contains the common portions of console client (QtConsole,
129 This class contains the common portions of console client (QtConsole,
129 ZMQ-based terminal console, etc). It is not a full console, in that
130 ZMQ-based terminal console, etc). It is not a full console, in that
130 launched terminal subprocesses will not be able to accept input.
131 launched terminal subprocesses will not be able to accept input.
131
132
132 The Console using this mixing supports various extra features beyond
133 The Console using this mixing supports various extra features beyond
133 the single-process Terminal IPython shell, such as connecting to
134 the single-process Terminal IPython shell, such as connecting to
134 existing kernel, via:
135 existing kernel, via:
135
136
136 ipython <appname> --existing
137 ipython <appname> --existing
137
138
138 as well as tunnel via SSH
139 as well as tunnel via SSH
139
140
140 """
141 """
141
142
142 classes = classes
143 classes = classes
143 flags = Dict(flags)
144 flags = Dict(flags)
144 aliases = Dict(aliases)
145 aliases = Dict(aliases)
145 kernel_manager_class = BlockingKernelManager
146 kernel_manager_class = BlockingKernelManager
146
147
147 kernel_argv = List(Unicode)
148 kernel_argv = List(Unicode)
148 # frontend flags&aliases to be stripped when building kernel_argv
149 # frontend flags&aliases to be stripped when building kernel_argv
149 frontend_flags = Any(app_flags)
150 frontend_flags = Any(app_flags)
150 frontend_aliases = Any(app_aliases)
151 frontend_aliases = Any(app_aliases)
151
152
152 # create requested profiles by default, if they don't exist:
153 # create requested profiles by default, if they don't exist:
153 auto_create = CBool(True)
154 auto_create = CBool(True)
154 # connection info:
155 # connection info:
155
156
156 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
157
158 ip = Unicode(config=True,
159 help="""Set the kernel\'s IP address [default localhost].
160 If the IP address is something other than localhost, then
161 Consoles on other machines will be able to connect
162 to the Kernel, so be careful!"""
163 )
164 def _ip_default(self):
165 if self.transport == 'tcp':
166 return LOCALHOST
167 else:
168 # this can fire early if ip is given,
169 # in which case our return value is meaningless
170 if not hasattr(self, 'profile_dir'):
171 return ''
172 ipcdir = os.path.join(self.profile_dir.security_dir, 'kernel-%s' % os.getpid())
173 os.makedirs(ipcdir)
174 atexit.register(lambda : shutil.rmtree(ipcdir))
175 return os.path.join(ipcdir, 'ipc')
176
177 sshserver = Unicode('', config=True,
157 sshserver = Unicode('', config=True,
178 help="""The SSH server to use to connect to the kernel.""")
158 help="""The SSH server to use to connect to the kernel.""")
179 sshkey = Unicode('', config=True,
159 sshkey = Unicode('', config=True,
180 help="""Path to the ssh key to use for logging in to the ssh server.""")
160 help="""Path to the ssh key to use for logging in to the ssh server.""")
181
161
182 hb_port = Int(0, config=True,
162 hb_port = Int(0, config=True,
183 help="set the heartbeat port [default: random]")
163 help="set the heartbeat port [default: random]")
184 shell_port = Int(0, config=True,
164 shell_port = Int(0, config=True,
185 help="set the shell (ROUTER) port [default: random]")
165 help="set the shell (ROUTER) port [default: random]")
186 iopub_port = Int(0, config=True,
166 iopub_port = Int(0, config=True,
187 help="set the iopub (PUB) port [default: random]")
167 help="set the iopub (PUB) port [default: random]")
188 stdin_port = Int(0, config=True,
168 stdin_port = Int(0, config=True,
189 help="set the stdin (DEALER) port [default: random]")
169 help="set the stdin (DEALER) port [default: random]")
190 connection_file = Unicode('', config=True,
170 connection_file = Unicode('', config=True,
191 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
171 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
192
172
193 This file will contain the IP, ports, and authentication key needed to connect
173 This file will contain the IP, ports, and authentication key needed to connect
194 clients to this kernel. By default, this file will be created in the security-dir
174 clients to this kernel. By default, this file will be created in the security-dir
195 of the current profile, but can be specified by absolute path.
175 of the current profile, but can be specified by absolute path.
196 """)
176 """)
197 def _connection_file_default(self):
177 def _connection_file_default(self):
198 return 'kernel-%i.json' % os.getpid()
178 return 'kernel-%i.json' % os.getpid()
199
179
200 existing = CUnicode('', config=True,
180 existing = CUnicode('', config=True,
201 help="""Connect to an already running kernel""")
181 help="""Connect to an already running kernel""")
202
182
203 confirm_exit = CBool(True, config=True,
183 confirm_exit = CBool(True, config=True,
204 help="""
184 help="""
205 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
185 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
206 to force a direct exit without any confirmation.""",
186 to force a direct exit without any confirmation.""",
207 )
187 )
208
188
209
189
210 def build_kernel_argv(self, argv=None):
190 def build_kernel_argv(self, argv=None):
211 """build argv to be passed to kernel subprocess"""
191 """build argv to be passed to kernel subprocess"""
212 if argv is None:
192 if argv is None:
213 argv = sys.argv[1:]
193 argv = sys.argv[1:]
214 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
194 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
215 # kernel should inherit default config file from frontend
195 # kernel should inherit default config file from frontend
216 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
196 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
217
197
218 def init_connection_file(self):
198 def init_connection_file(self):
219 """find the connection file, and load the info if found.
199 """find the connection file, and load the info if found.
220
200
221 The current working directory and the current profile's security
201 The current working directory and the current profile's security
222 directory will be searched for the file if it is not given by
202 directory will be searched for the file if it is not given by
223 absolute path.
203 absolute path.
224
204
225 When attempting to connect to an existing kernel and the `--existing`
205 When attempting to connect to an existing kernel and the `--existing`
226 argument does not match an existing file, it will be interpreted as a
206 argument does not match an existing file, it will be interpreted as a
227 fileglob, and the matching file in the current profile's security dir
207 fileglob, and the matching file in the current profile's security dir
228 with the latest access time will be used.
208 with the latest access time will be used.
229
209
230 After this method is called, self.connection_file contains the *full path*
210 After this method is called, self.connection_file contains the *full path*
231 to the connection file, never just its name.
211 to the connection file, never just its name.
232 """
212 """
233 if self.existing:
213 if self.existing:
234 try:
214 try:
235 cf = find_connection_file(self.existing)
215 cf = find_connection_file(self.existing)
236 except Exception:
216 except Exception:
237 self.log.critical("Could not find existing kernel connection file %s", self.existing)
217 self.log.critical("Could not find existing kernel connection file %s", self.existing)
238 self.exit(1)
218 self.exit(1)
239 self.log.info("Connecting to existing kernel: %s" % cf)
219 self.log.info("Connecting to existing kernel: %s" % cf)
240 self.connection_file = cf
220 self.connection_file = cf
241 else:
221 else:
242 # not existing, check if we are going to write the file
222 # not existing, check if we are going to write the file
243 # and ensure that self.connection_file is a full path, not just the shortname
223 # and ensure that self.connection_file is a full path, not just the shortname
244 try:
224 try:
245 cf = find_connection_file(self.connection_file)
225 cf = find_connection_file(self.connection_file)
246 except Exception:
226 except Exception:
247 # file might not exist
227 # file might not exist
248 if self.connection_file == os.path.basename(self.connection_file):
228 if self.connection_file == os.path.basename(self.connection_file):
249 # just shortname, put it in security dir
229 # just shortname, put it in security dir
250 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
230 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
251 else:
231 else:
252 cf = self.connection_file
232 cf = self.connection_file
253 self.connection_file = cf
233 self.connection_file = cf
254
234
255 # should load_connection_file only be used for existing?
235 # should load_connection_file only be used for existing?
256 # as it is now, this allows reusing ports if an existing
236 # as it is now, this allows reusing ports if an existing
257 # file is requested
237 # file is requested
258 try:
238 try:
259 self.load_connection_file()
239 self.load_connection_file()
260 except Exception:
240 except Exception:
261 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
241 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
262 self.exit(1)
242 self.exit(1)
263
243
264 def load_connection_file(self):
244 def load_connection_file(self):
265 """load ip/port/hmac config from JSON connection file"""
245 """load ip/port/hmac config from JSON connection file"""
266 # this is identical to KernelApp.load_connection_file
246 # this is identical to KernelApp.load_connection_file
267 # perhaps it can be centralized somewhere?
247 # perhaps it can be centralized somewhere?
268 try:
248 try:
269 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
249 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
270 except IOError:
250 except IOError:
271 self.log.debug("Connection File not found: %s", self.connection_file)
251 self.log.debug("Connection File not found: %s", self.connection_file)
272 return
252 return
273 self.log.debug(u"Loading connection file %s", fname)
253 self.log.debug(u"Loading connection file %s", fname)
274 with open(fname) as f:
254 with open(fname) as f:
275 cfg = json.load(f)
255 cfg = json.load(f)
276
256
277 self.transport = cfg.get('transport', 'tcp')
257 self.config.KernelManager.transport = cfg.get('transport', 'tcp')
278 if 'ip' in cfg:
258 self.config.KernelManager.ip = cfg.get('ip', LOCALHOST)
279 self.ip = cfg['ip']
259
280 for channel in ('hb', 'shell', 'iopub', 'stdin'):
260 for channel in ('hb', 'shell', 'iopub', 'stdin'):
281 name = channel + '_port'
261 name = channel + '_port'
282 if getattr(self, name) == 0 and name in cfg:
262 if getattr(self, name) == 0 and name in cfg:
283 # not overridden by config or cl_args
263 # not overridden by config or cl_args
284 setattr(self, name, cfg[name])
264 setattr(self, name, cfg[name])
285 if 'key' in cfg:
265 if 'key' in cfg:
286 self.config.Session.key = str_to_bytes(cfg['key'])
266 self.config.Session.key = str_to_bytes(cfg['key'])
287
267
288
289 def init_ssh(self):
268 def init_ssh(self):
290 """set up ssh tunnels, if needed."""
269 """set up ssh tunnels, if needed."""
291 if not self.sshserver and not self.sshkey:
270 if not self.existing or (not self.sshserver and not self.sshkey):
292 return
271 return
293
272
294 if self.transport != 'tcp':
273 self.load_connection_file()
295 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", self.transport)
274
296 return
275 transport = self.config.KernelManager.transport
276 ip = self.config.KernelManager.ip
277
278 if transport != 'tcp':
279 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
280 sys.exit(-1)
297
281
298 if self.sshkey and not self.sshserver:
282 if self.sshkey and not self.sshserver:
299 # specifying just the key implies that we are connecting directly
283 # specifying just the key implies that we are connecting directly
300 self.sshserver = self.ip
284 self.sshserver = ip
301 self.ip = LOCALHOST
285 ip = LOCALHOST
302
286
303 # build connection dict for tunnels:
287 # build connection dict for tunnels:
304 info = dict(ip=self.ip,
288 info = dict(ip=ip,
305 shell_port=self.shell_port,
289 shell_port=self.shell_port,
306 iopub_port=self.iopub_port,
290 iopub_port=self.iopub_port,
307 stdin_port=self.stdin_port,
291 stdin_port=self.stdin_port,
308 hb_port=self.hb_port
292 hb_port=self.hb_port
309 )
293 )
310
294
311 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
295 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
312
296
313 # tunnels return a new set of ports, which will be on localhost:
297 # tunnels return a new set of ports, which will be on localhost:
314 self.ip = LOCALHOST
298 self.config.KernelManager.ip = LOCALHOST
315 try:
299 try:
316 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
300 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
317 except:
301 except:
318 # even catch KeyboardInterrupt
302 # even catch KeyboardInterrupt
319 self.log.error("Could not setup tunnels", exc_info=True)
303 self.log.error("Could not setup tunnels", exc_info=True)
320 self.exit(1)
304 self.exit(1)
321
305
322 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
306 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
323
307
324 cf = self.connection_file
308 cf = self.connection_file
325 base,ext = os.path.splitext(cf)
309 base,ext = os.path.splitext(cf)
326 base = os.path.basename(base)
310 base = os.path.basename(base)
327 self.connection_file = os.path.basename(base)+'-ssh'+ext
311 self.connection_file = os.path.basename(base)+'-ssh'+ext
328 self.log.critical("To connect another client via this tunnel, use:")
312 self.log.critical("To connect another client via this tunnel, use:")
329 self.log.critical("--existing %s" % self.connection_file)
313 self.log.critical("--existing %s" % self.connection_file)
330
314
331 def _new_connection_file(self):
315 def _new_connection_file(self):
332 cf = ''
316 cf = ''
333 while not cf:
317 while not cf:
334 # we don't need a 128b id to distinguish kernels, use more readable
318 # we don't need a 128b id to distinguish kernels, use more readable
335 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
319 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
336 # kernels can subclass.
320 # kernels can subclass.
337 ident = str(uuid.uuid4()).split('-')[-1]
321 ident = str(uuid.uuid4()).split('-')[-1]
338 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
322 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
339 # only keep if it's actually new. Protect against unlikely collision
323 # only keep if it's actually new. Protect against unlikely collision
340 # in 48b random search space
324 # in 48b random search space
341 cf = cf if not os.path.exists(cf) else ''
325 cf = cf if not os.path.exists(cf) else ''
342 return cf
326 return cf
343
327
344 def init_kernel_manager(self):
328 def init_kernel_manager(self):
345 # Don't let Qt or ZMQ swallow KeyboardInterupts.
329 # Don't let Qt or ZMQ swallow KeyboardInterupts.
346 signal.signal(signal.SIGINT, signal.SIG_DFL)
330 signal.signal(signal.SIGINT, signal.SIG_DFL)
347
331
348 # Create a KernelManager and start a kernel.
332 # Create a KernelManager and start a kernel.
349 self.kernel_manager = self.kernel_manager_class(
333 self.kernel_manager = self.kernel_manager_class(
350 transport=self.transport,
351 ip=self.ip,
352 shell_port=self.shell_port,
334 shell_port=self.shell_port,
353 iopub_port=self.iopub_port,
335 iopub_port=self.iopub_port,
354 stdin_port=self.stdin_port,
336 stdin_port=self.stdin_port,
355 hb_port=self.hb_port,
337 hb_port=self.hb_port,
356 connection_file=self.connection_file,
338 connection_file=self.connection_file,
357 config=self.config,
339 config=self.config,
358 )
340 )
359 # start the kernel
341 # start the kernel
360 if not self.existing:
342 if not self.existing:
361 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
343 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
344 atexit.register(self.kernel_manager.cleanup_ipc_files)
362 elif self.sshserver:
345 elif self.sshserver:
363 # ssh, write new connection file
346 # ssh, write new connection file
364 self.kernel_manager.write_connection_file()
347 self.kernel_manager.write_connection_file()
365 atexit.register(self.kernel_manager.cleanup_connection_file)
348 atexit.register(self.kernel_manager.cleanup_connection_file)
366 self.kernel_manager.start_channels()
349 self.kernel_manager.start_channels()
367
350
368
351
369 def initialize(self, argv=None):
352 def initialize(self, argv=None):
370 """
353 """
371 Classes which mix this class in should call:
354 Classes which mix this class in should call:
372 IPythonConsoleApp.initialize(self,argv)
355 IPythonConsoleApp.initialize(self,argv)
373 """
356 """
374 self.init_connection_file()
357 self.init_connection_file()
375 default_secure(self.config)
358 default_secure(self.config)
376 self.init_ssh()
359 self.init_ssh()
377 self.init_kernel_manager()
360 self.init_kernel_manager()
378
361
@@ -1,644 +1,645 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import uuid
31 import uuid
32 import webbrowser
32 import webbrowser
33
33
34 # Third party
34 # Third party
35 import zmq
35 import zmq
36 from jinja2 import Environment, FileSystemLoader
36 from jinja2 import Environment, FileSystemLoader
37
37
38 # Install the pyzmq ioloop. This has to be done before anything else from
38 # Install the pyzmq ioloop. This has to be done before anything else from
39 # tornado is imported.
39 # tornado is imported.
40 from zmq.eventloop import ioloop
40 from zmq.eventloop import ioloop
41 ioloop.install()
41 ioloop.install()
42
42
43 from tornado import httpserver
43 from tornado import httpserver
44 from tornado import web
44 from tornado import web
45
45
46 # Our own libraries
46 # Our own libraries
47 from .kernelmanager import MappingKernelManager
47 from .kernelmanager import MappingKernelManager
48 from .handlers import (LoginHandler, LogoutHandler,
48 from .handlers import (LoginHandler, LogoutHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
54 FileFindHandler,
54 FileFindHandler,
55 )
55 )
56 from .nbmanager import NotebookManager
56 from .nbmanager import NotebookManager
57 from .filenbmanager import FileNotebookManager
57 from .filenbmanager import FileNotebookManager
58 from .clustermanager import ClusterManager
58 from .clustermanager import ClusterManager
59
59
60 from IPython.config.application import catch_config_error, boolean_flag
60 from IPython.config.application import catch_config_error, boolean_flag
61 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.application import BaseIPythonApplication
62 from IPython.core.profiledir import ProfileDir
62 from IPython.core.profiledir import ProfileDir
63 from IPython.frontend.consoleapp import IPythonConsoleApp
63 from IPython.frontend.consoleapp import IPythonConsoleApp
64 from IPython.lib.kernel import swallow_argv
64 from IPython.lib.kernel import swallow_argv
65 from IPython.zmq.session import Session, default_secure
65 from IPython.zmq.session import Session, default_secure
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
67 from IPython.zmq.ipkernel import (
67 from IPython.zmq.ipkernel import (
68 flags as ipkernel_flags,
68 flags as ipkernel_flags,
69 aliases as ipkernel_aliases,
69 aliases as ipkernel_aliases,
70 IPKernelApp
70 IPKernelApp
71 )
71 )
72 from IPython.utils.importstring import import_item
72 from IPython.utils.importstring import import_item
73 from IPython.utils.traitlets import (
73 from IPython.utils.traitlets import (
74 Dict, Unicode, Integer, List, Enum, Bool,
74 Dict, Unicode, Integer, List, Enum, Bool,
75 DottedObjectName
75 DottedObjectName
76 )
76 )
77 from IPython.utils import py3compat
77 from IPython.utils import py3compat
78 from IPython.utils.path import filefind
78 from IPython.utils.path import filefind
79
79
80 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
81 # Module globals
81 # Module globals
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83
83
84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
88 _cluster_action_regex = r"(?P<action>start|stop)"
88 _cluster_action_regex = r"(?P<action>start|stop)"
89
89
90
90
91 LOCALHOST = '127.0.0.1'
91 LOCALHOST = '127.0.0.1'
92
92
93 _examples = """
93 _examples = """
94 ipython notebook # start the notebook
94 ipython notebook # start the notebook
95 ipython notebook --profile=sympy # use the sympy profile
95 ipython notebook --profile=sympy # use the sympy profile
96 ipython notebook --pylab=inline # pylab in inline plotting mode
96 ipython notebook --pylab=inline # pylab in inline plotting mode
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
99 """
99 """
100
100
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 # Helper functions
102 # Helper functions
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104
104
105 def url_path_join(a,b):
105 def url_path_join(a,b):
106 if a.endswith('/') and b.startswith('/'):
106 if a.endswith('/') and b.startswith('/'):
107 return a[:-1]+b
107 return a[:-1]+b
108 else:
108 else:
109 return a+b
109 return a+b
110
110
111 def random_ports(port, n):
111 def random_ports(port, n):
112 """Generate a list of n random ports near the given port.
112 """Generate a list of n random ports near the given port.
113
113
114 The first 5 ports will be sequential, and the remaining n-5 will be
114 The first 5 ports will be sequential, and the remaining n-5 will be
115 randomly selected in the range [port-2*n, port+2*n].
115 randomly selected in the range [port-2*n, port+2*n].
116 """
116 """
117 for i in range(min(5, n)):
117 for i in range(min(5, n)):
118 yield port + i
118 yield port + i
119 for i in range(n-5):
119 for i in range(n-5):
120 yield port + random.randint(-2*n, 2*n)
120 yield port + random.randint(-2*n, 2*n)
121
121
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123 # The Tornado web application
123 # The Tornado web application
124 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
125
125
126 class NotebookWebApplication(web.Application):
126 class NotebookWebApplication(web.Application):
127
127
128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
129 cluster_manager, log,
129 cluster_manager, log,
130 base_project_url, settings_overrides):
130 base_project_url, settings_overrides):
131 handlers = [
131 handlers = [
132 (r"/", ProjectDashboardHandler),
132 (r"/", ProjectDashboardHandler),
133 (r"/login", LoginHandler),
133 (r"/login", LoginHandler),
134 (r"/logout", LogoutHandler),
134 (r"/logout", LogoutHandler),
135 (r"/new", NewHandler),
135 (r"/new", NewHandler),
136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
139 (r"/kernels", MainKernelHandler),
139 (r"/kernels", MainKernelHandler),
140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
144 (r"/notebooks", NotebookRootHandler),
144 (r"/notebooks", NotebookRootHandler),
145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
146 (r"/rstservice/render", RSTHandler),
146 (r"/rstservice/render", RSTHandler),
147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
148 (r"/clusters", MainClusterHandler),
148 (r"/clusters", MainClusterHandler),
149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
151 ]
151 ]
152
152
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # base_project_url will always be unicode, which will in turn
154 # base_project_url will always be unicode, which will in turn
155 # make the patterns unicode, and ultimately result in unicode
155 # make the patterns unicode, and ultimately result in unicode
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # This enforces that base_project_url be ascii in that situation.
157 # This enforces that base_project_url be ascii in that situation.
158 #
158 #
159 # Note that the URLs these patterns check against are escaped,
159 # Note that the URLs these patterns check against are escaped,
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162
162
163 settings = dict(
163 settings = dict(
164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
165 static_path=ipython_app.static_file_path,
165 static_path=ipython_app.static_file_path,
166 static_handler_class = FileFindHandler,
166 static_handler_class = FileFindHandler,
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 cookie_secret=os.urandom(1024),
168 cookie_secret=os.urandom(1024),
169 login_url=url_path_join(base_project_url,'/login'),
169 login_url=url_path_join(base_project_url,'/login'),
170 cookie_name='username-%s' % uuid.uuid4(),
170 cookie_name='username-%s' % uuid.uuid4(),
171 )
171 )
172
172
173 # allow custom overrides for the tornado web app.
173 # allow custom overrides for the tornado web app.
174 settings.update(settings_overrides)
174 settings.update(settings_overrides)
175
175
176 # prepend base_project_url onto the patterns that we match
176 # prepend base_project_url onto the patterns that we match
177 new_handlers = []
177 new_handlers = []
178 for handler in handlers:
178 for handler in handlers:
179 pattern = url_path_join(base_project_url, handler[0])
179 pattern = url_path_join(base_project_url, handler[0])
180 new_handler = tuple([pattern]+list(handler[1:]))
180 new_handler = tuple([pattern]+list(handler[1:]))
181 new_handlers.append( new_handler )
181 new_handlers.append( new_handler )
182
182
183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
184
184
185 self.kernel_manager = kernel_manager
185 self.kernel_manager = kernel_manager
186 self.notebook_manager = notebook_manager
186 self.notebook_manager = notebook_manager
187 self.cluster_manager = cluster_manager
187 self.cluster_manager = cluster_manager
188 self.ipython_app = ipython_app
188 self.ipython_app = ipython_app
189 self.read_only = self.ipython_app.read_only
189 self.read_only = self.ipython_app.read_only
190 self.config = self.ipython_app.config
190 self.config = self.ipython_app.config
191 self.log = log
191 self.log = log
192 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
192 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
193
193
194
194
195
195
196 #-----------------------------------------------------------------------------
196 #-----------------------------------------------------------------------------
197 # Aliases and Flags
197 # Aliases and Flags
198 #-----------------------------------------------------------------------------
198 #-----------------------------------------------------------------------------
199
199
200 flags = dict(ipkernel_flags)
200 flags = dict(ipkernel_flags)
201 flags['no-browser']=(
201 flags['no-browser']=(
202 {'NotebookApp' : {'open_browser' : False}},
202 {'NotebookApp' : {'open_browser' : False}},
203 "Don't open the notebook in a browser after startup."
203 "Don't open the notebook in a browser after startup."
204 )
204 )
205 flags['no-mathjax']=(
205 flags['no-mathjax']=(
206 {'NotebookApp' : {'enable_mathjax' : False}},
206 {'NotebookApp' : {'enable_mathjax' : False}},
207 """Disable MathJax
207 """Disable MathJax
208
208
209 MathJax is the javascript library IPython uses to render math/LaTeX. It is
209 MathJax is the javascript library IPython uses to render math/LaTeX. It is
210 very large, so you may want to disable it if you have a slow internet
210 very large, so you may want to disable it if you have a slow internet
211 connection, or for offline use of the notebook.
211 connection, or for offline use of the notebook.
212
212
213 When disabled, equations etc. will appear as their untransformed TeX source.
213 When disabled, equations etc. will appear as their untransformed TeX source.
214 """
214 """
215 )
215 )
216 flags['read-only'] = (
216 flags['read-only'] = (
217 {'NotebookApp' : {'read_only' : True}},
217 {'NotebookApp' : {'read_only' : True}},
218 """Allow read-only access to notebooks.
218 """Allow read-only access to notebooks.
219
219
220 When using a password to protect the notebook server, this flag
220 When using a password to protect the notebook server, this flag
221 allows unauthenticated clients to view the notebook list, and
221 allows unauthenticated clients to view the notebook list, and
222 individual notebooks, but not edit them, start kernels, or run
222 individual notebooks, but not edit them, start kernels, or run
223 code.
223 code.
224
224
225 If no password is set, the server will be entirely read-only.
225 If no password is set, the server will be entirely read-only.
226 """
226 """
227 )
227 )
228
228
229 # Add notebook manager flags
229 # Add notebook manager flags
230 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
230 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
231 'Auto-save a .py script everytime the .ipynb notebook is saved',
231 'Auto-save a .py script everytime the .ipynb notebook is saved',
232 'Do not auto-save .py scripts for every notebook'))
232 'Do not auto-save .py scripts for every notebook'))
233
233
234 # the flags that are specific to the frontend
234 # the flags that are specific to the frontend
235 # these must be scrubbed before being passed to the kernel,
235 # these must be scrubbed before being passed to the kernel,
236 # or it will raise an error on unrecognized flags
236 # or it will raise an error on unrecognized flags
237 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
237 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
238
238
239 aliases = dict(ipkernel_aliases)
239 aliases = dict(ipkernel_aliases)
240
240
241 aliases.update({
241 aliases.update({
242 'ip': 'NotebookApp.ip',
242 'ip': 'NotebookApp.ip',
243 'port': 'NotebookApp.port',
243 'port': 'NotebookApp.port',
244 'port-retries': 'NotebookApp.port_retries',
244 'port-retries': 'NotebookApp.port_retries',
245 'transport': 'KernelManager.transport',
245 'keyfile': 'NotebookApp.keyfile',
246 'keyfile': 'NotebookApp.keyfile',
246 'certfile': 'NotebookApp.certfile',
247 'certfile': 'NotebookApp.certfile',
247 'notebook-dir': 'NotebookManager.notebook_dir',
248 'notebook-dir': 'NotebookManager.notebook_dir',
248 'browser': 'NotebookApp.browser',
249 'browser': 'NotebookApp.browser',
249 })
250 })
250
251
251 # remove ipkernel flags that are singletons, and don't make sense in
252 # remove ipkernel flags that are singletons, and don't make sense in
252 # multi-kernel evironment:
253 # multi-kernel evironment:
253 aliases.pop('f', None)
254 aliases.pop('f', None)
254
255
255 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
256 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
256 u'notebook-dir']
257 u'notebook-dir']
257
258
258 #-----------------------------------------------------------------------------
259 #-----------------------------------------------------------------------------
259 # NotebookApp
260 # NotebookApp
260 #-----------------------------------------------------------------------------
261 #-----------------------------------------------------------------------------
261
262
262 class NotebookApp(BaseIPythonApplication):
263 class NotebookApp(BaseIPythonApplication):
263
264
264 name = 'ipython-notebook'
265 name = 'ipython-notebook'
265 default_config_file_name='ipython_notebook_config.py'
266 default_config_file_name='ipython_notebook_config.py'
266
267
267 description = """
268 description = """
268 The IPython HTML Notebook.
269 The IPython HTML Notebook.
269
270
270 This launches a Tornado based HTML Notebook Server that serves up an
271 This launches a Tornado based HTML Notebook Server that serves up an
271 HTML5/Javascript Notebook client.
272 HTML5/Javascript Notebook client.
272 """
273 """
273 examples = _examples
274 examples = _examples
274
275
275 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
276 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
276 FileNotebookManager]
277 FileNotebookManager]
277 flags = Dict(flags)
278 flags = Dict(flags)
278 aliases = Dict(aliases)
279 aliases = Dict(aliases)
279
280
280 kernel_argv = List(Unicode)
281 kernel_argv = List(Unicode)
281
282
282 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
283 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
283 default_value=logging.INFO,
284 default_value=logging.INFO,
284 config=True,
285 config=True,
285 help="Set the log level by value or name.")
286 help="Set the log level by value or name.")
286
287
287 # create requested profiles by default, if they don't exist:
288 # create requested profiles by default, if they don't exist:
288 auto_create = Bool(True)
289 auto_create = Bool(True)
289
290
290 # file to be opened in the notebook server
291 # file to be opened in the notebook server
291 file_to_run = Unicode('')
292 file_to_run = Unicode('')
292
293
293 # Network related information.
294 # Network related information.
294
295
295 ip = Unicode(LOCALHOST, config=True,
296 ip = Unicode(LOCALHOST, config=True,
296 help="The IP address the notebook server will listen on."
297 help="The IP address the notebook server will listen on."
297 )
298 )
298
299
299 def _ip_changed(self, name, old, new):
300 def _ip_changed(self, name, old, new):
300 if new == u'*': self.ip = u''
301 if new == u'*': self.ip = u''
301
302
302 port = Integer(8888, config=True,
303 port = Integer(8888, config=True,
303 help="The port the notebook server will listen on."
304 help="The port the notebook server will listen on."
304 )
305 )
305 port_retries = Integer(50, config=True,
306 port_retries = Integer(50, config=True,
306 help="The number of additional ports to try if the specified port is not available."
307 help="The number of additional ports to try if the specified port is not available."
307 )
308 )
308
309
309 certfile = Unicode(u'', config=True,
310 certfile = Unicode(u'', config=True,
310 help="""The full path to an SSL/TLS certificate file."""
311 help="""The full path to an SSL/TLS certificate file."""
311 )
312 )
312
313
313 keyfile = Unicode(u'', config=True,
314 keyfile = Unicode(u'', config=True,
314 help="""The full path to a private key file for usage with SSL/TLS."""
315 help="""The full path to a private key file for usage with SSL/TLS."""
315 )
316 )
316
317
317 password = Unicode(u'', config=True,
318 password = Unicode(u'', config=True,
318 help="""Hashed password to use for web authentication.
319 help="""Hashed password to use for web authentication.
319
320
320 To generate, type in a python/IPython shell:
321 To generate, type in a python/IPython shell:
321
322
322 from IPython.lib import passwd; passwd()
323 from IPython.lib import passwd; passwd()
323
324
324 The string should be of the form type:salt:hashed-password.
325 The string should be of the form type:salt:hashed-password.
325 """
326 """
326 )
327 )
327
328
328 open_browser = Bool(True, config=True,
329 open_browser = Bool(True, config=True,
329 help="""Whether to open in a browser after starting.
330 help="""Whether to open in a browser after starting.
330 The specific browser used is platform dependent and
331 The specific browser used is platform dependent and
331 determined by the python standard library `webbrowser`
332 determined by the python standard library `webbrowser`
332 module, unless it is overridden using the --browser
333 module, unless it is overridden using the --browser
333 (NotebookApp.browser) configuration option.
334 (NotebookApp.browser) configuration option.
334 """)
335 """)
335
336
336 browser = Unicode(u'', config=True,
337 browser = Unicode(u'', config=True,
337 help="""Specify what command to use to invoke a web
338 help="""Specify what command to use to invoke a web
338 browser when opening the notebook. If not specified, the
339 browser when opening the notebook. If not specified, the
339 default browser will be determined by the `webbrowser`
340 default browser will be determined by the `webbrowser`
340 standard library module, which allows setting of the
341 standard library module, which allows setting of the
341 BROWSER environment variable to override it.
342 BROWSER environment variable to override it.
342 """)
343 """)
343
344
344 read_only = Bool(False, config=True,
345 read_only = Bool(False, config=True,
345 help="Whether to prevent editing/execution of notebooks."
346 help="Whether to prevent editing/execution of notebooks."
346 )
347 )
347
348
348 webapp_settings = Dict(config=True,
349 webapp_settings = Dict(config=True,
349 help="Supply overrides for the tornado.web.Application that the "
350 help="Supply overrides for the tornado.web.Application that the "
350 "IPython notebook uses.")
351 "IPython notebook uses.")
351
352
352 enable_mathjax = Bool(True, config=True,
353 enable_mathjax = Bool(True, config=True,
353 help="""Whether to enable MathJax for typesetting math/TeX
354 help="""Whether to enable MathJax for typesetting math/TeX
354
355
355 MathJax is the javascript library IPython uses to render math/LaTeX. It is
356 MathJax is the javascript library IPython uses to render math/LaTeX. It is
356 very large, so you may want to disable it if you have a slow internet
357 very large, so you may want to disable it if you have a slow internet
357 connection, or for offline use of the notebook.
358 connection, or for offline use of the notebook.
358
359
359 When disabled, equations etc. will appear as their untransformed TeX source.
360 When disabled, equations etc. will appear as their untransformed TeX source.
360 """
361 """
361 )
362 )
362 def _enable_mathjax_changed(self, name, old, new):
363 def _enable_mathjax_changed(self, name, old, new):
363 """set mathjax url to empty if mathjax is disabled"""
364 """set mathjax url to empty if mathjax is disabled"""
364 if not new:
365 if not new:
365 self.mathjax_url = u''
366 self.mathjax_url = u''
366
367
367 base_project_url = Unicode('/', config=True,
368 base_project_url = Unicode('/', config=True,
368 help='''The base URL for the notebook server.
369 help='''The base URL for the notebook server.
369
370
370 Leading and trailing slashes can be omitted,
371 Leading and trailing slashes can be omitted,
371 and will automatically be added.
372 and will automatically be added.
372 ''')
373 ''')
373 def _base_project_url_changed(self, name, old, new):
374 def _base_project_url_changed(self, name, old, new):
374 if not new.startswith('/'):
375 if not new.startswith('/'):
375 self.base_project_url = '/'+new
376 self.base_project_url = '/'+new
376 elif not new.endswith('/'):
377 elif not new.endswith('/'):
377 self.base_project_url = new+'/'
378 self.base_project_url = new+'/'
378
379
379 base_kernel_url = Unicode('/', config=True,
380 base_kernel_url = Unicode('/', config=True,
380 help='''The base URL for the kernel server
381 help='''The base URL for the kernel server
381
382
382 Leading and trailing slashes can be omitted,
383 Leading and trailing slashes can be omitted,
383 and will automatically be added.
384 and will automatically be added.
384 ''')
385 ''')
385 def _base_kernel_url_changed(self, name, old, new):
386 def _base_kernel_url_changed(self, name, old, new):
386 if not new.startswith('/'):
387 if not new.startswith('/'):
387 self.base_kernel_url = '/'+new
388 self.base_kernel_url = '/'+new
388 elif not new.endswith('/'):
389 elif not new.endswith('/'):
389 self.base_kernel_url = new+'/'
390 self.base_kernel_url = new+'/'
390
391
391 websocket_host = Unicode("", config=True,
392 websocket_host = Unicode("", config=True,
392 help="""The hostname for the websocket server."""
393 help="""The hostname for the websocket server."""
393 )
394 )
394
395
395 extra_static_paths = List(Unicode, config=True,
396 extra_static_paths = List(Unicode, config=True,
396 help="""Extra paths to search for serving static files.
397 help="""Extra paths to search for serving static files.
397
398
398 This allows adding javascript/css to be available from the notebook server machine,
399 This allows adding javascript/css to be available from the notebook server machine,
399 or overriding individual files in the IPython"""
400 or overriding individual files in the IPython"""
400 )
401 )
401 def _extra_static_paths_default(self):
402 def _extra_static_paths_default(self):
402 return [os.path.join(self.profile_dir.location, 'static')]
403 return [os.path.join(self.profile_dir.location, 'static')]
403
404
404 @property
405 @property
405 def static_file_path(self):
406 def static_file_path(self):
406 """return extra paths + the default location"""
407 """return extra paths + the default location"""
407 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
408 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
408
409
409 mathjax_url = Unicode("", config=True,
410 mathjax_url = Unicode("", config=True,
410 help="""The url for MathJax.js."""
411 help="""The url for MathJax.js."""
411 )
412 )
412 def _mathjax_url_default(self):
413 def _mathjax_url_default(self):
413 if not self.enable_mathjax:
414 if not self.enable_mathjax:
414 return u''
415 return u''
415 static_url_prefix = self.webapp_settings.get("static_url_prefix",
416 static_url_prefix = self.webapp_settings.get("static_url_prefix",
416 "/static/")
417 "/static/")
417 try:
418 try:
418 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
419 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
419 except IOError:
420 except IOError:
420 if self.certfile:
421 if self.certfile:
421 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
422 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
422 base = u"https://c328740.ssl.cf1.rackcdn.com"
423 base = u"https://c328740.ssl.cf1.rackcdn.com"
423 else:
424 else:
424 base = u"http://cdn.mathjax.org"
425 base = u"http://cdn.mathjax.org"
425
426
426 url = base + u"/mathjax/latest/MathJax.js"
427 url = base + u"/mathjax/latest/MathJax.js"
427 self.log.info("Using MathJax from CDN: %s", url)
428 self.log.info("Using MathJax from CDN: %s", url)
428 return url
429 return url
429 else:
430 else:
430 self.log.info("Using local MathJax from %s" % mathjax)
431 self.log.info("Using local MathJax from %s" % mathjax)
431 return static_url_prefix+u"mathjax/MathJax.js"
432 return static_url_prefix+u"mathjax/MathJax.js"
432
433
433 def _mathjax_url_changed(self, name, old, new):
434 def _mathjax_url_changed(self, name, old, new):
434 if new and not self.enable_mathjax:
435 if new and not self.enable_mathjax:
435 # enable_mathjax=False overrides mathjax_url
436 # enable_mathjax=False overrides mathjax_url
436 self.mathjax_url = u''
437 self.mathjax_url = u''
437 else:
438 else:
438 self.log.info("Using MathJax: %s", new)
439 self.log.info("Using MathJax: %s", new)
439
440
440 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
441 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
441 config=True,
442 config=True,
442 help='The notebook manager class to use.')
443 help='The notebook manager class to use.')
443
444
444 def parse_command_line(self, argv=None):
445 def parse_command_line(self, argv=None):
445 super(NotebookApp, self).parse_command_line(argv)
446 super(NotebookApp, self).parse_command_line(argv)
446 if argv is None:
447 if argv is None:
447 argv = sys.argv[1:]
448 argv = sys.argv[1:]
448
449
449 # Scrub frontend-specific flags
450 # Scrub frontend-specific flags
450 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
451 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
451 # Kernel should inherit default config file from frontend
452 # Kernel should inherit default config file from frontend
452 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
453 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
453
454
454 if self.extra_args:
455 if self.extra_args:
455 f = os.path.abspath(self.extra_args[0])
456 f = os.path.abspath(self.extra_args[0])
456 if os.path.isdir(f):
457 if os.path.isdir(f):
457 nbdir = f
458 nbdir = f
458 else:
459 else:
459 self.file_to_run = f
460 self.file_to_run = f
460 nbdir = os.path.dirname(f)
461 nbdir = os.path.dirname(f)
461 self.config.NotebookManager.notebook_dir = nbdir
462 self.config.NotebookManager.notebook_dir = nbdir
462
463
463 def init_configurables(self):
464 def init_configurables(self):
464 # force Session default to be secure
465 # force Session default to be secure
465 default_secure(self.config)
466 default_secure(self.config)
466 self.kernel_manager = MappingKernelManager(
467 self.kernel_manager = MappingKernelManager(
467 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
468 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
468 connection_dir = self.profile_dir.security_dir,
469 connection_dir = self.profile_dir.security_dir,
469 )
470 )
470 kls = import_item(self.notebook_manager_class)
471 kls = import_item(self.notebook_manager_class)
471 self.notebook_manager = kls(config=self.config, log=self.log)
472 self.notebook_manager = kls(config=self.config, log=self.log)
472 self.notebook_manager.log_info()
473 self.notebook_manager.log_info()
473 self.notebook_manager.load_notebook_names()
474 self.notebook_manager.load_notebook_names()
474 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
475 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
475 self.cluster_manager.update_profiles()
476 self.cluster_manager.update_profiles()
476
477
477 def init_logging(self):
478 def init_logging(self):
478 # This prevents double log messages because tornado use a root logger that
479 # This prevents double log messages because tornado use a root logger that
479 # self.log is a child of. The logging module dipatches log messages to a log
480 # self.log is a child of. The logging module dipatches log messages to a log
480 # and all of its ancenstors until propagate is set to False.
481 # and all of its ancenstors until propagate is set to False.
481 self.log.propagate = False
482 self.log.propagate = False
482
483
483 def init_webapp(self):
484 def init_webapp(self):
484 """initialize tornado webapp and httpserver"""
485 """initialize tornado webapp and httpserver"""
485 self.web_app = NotebookWebApplication(
486 self.web_app = NotebookWebApplication(
486 self, self.kernel_manager, self.notebook_manager,
487 self, self.kernel_manager, self.notebook_manager,
487 self.cluster_manager, self.log,
488 self.cluster_manager, self.log,
488 self.base_project_url, self.webapp_settings
489 self.base_project_url, self.webapp_settings
489 )
490 )
490 if self.certfile:
491 if self.certfile:
491 ssl_options = dict(certfile=self.certfile)
492 ssl_options = dict(certfile=self.certfile)
492 if self.keyfile:
493 if self.keyfile:
493 ssl_options['keyfile'] = self.keyfile
494 ssl_options['keyfile'] = self.keyfile
494 else:
495 else:
495 ssl_options = None
496 ssl_options = None
496 self.web_app.password = self.password
497 self.web_app.password = self.password
497 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
498 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
498 if not self.ip:
499 if not self.ip:
499 warning = "WARNING: The notebook server is listening on all IP addresses"
500 warning = "WARNING: The notebook server is listening on all IP addresses"
500 if ssl_options is None:
501 if ssl_options is None:
501 self.log.critical(warning + " and not using encryption. This"
502 self.log.critical(warning + " and not using encryption. This"
502 "is not recommended.")
503 "is not recommended.")
503 if not self.password and not self.read_only:
504 if not self.password and not self.read_only:
504 self.log.critical(warning + "and not using authentication."
505 self.log.critical(warning + "and not using authentication."
505 "This is highly insecure and not recommended.")
506 "This is highly insecure and not recommended.")
506 success = None
507 success = None
507 for port in random_ports(self.port, self.port_retries+1):
508 for port in random_ports(self.port, self.port_retries+1):
508 try:
509 try:
509 self.http_server.listen(port, self.ip)
510 self.http_server.listen(port, self.ip)
510 except socket.error as e:
511 except socket.error as e:
511 if e.errno != errno.EADDRINUSE:
512 if e.errno != errno.EADDRINUSE:
512 raise
513 raise
513 self.log.info('The port %i is already in use, trying another random port.' % port)
514 self.log.info('The port %i is already in use, trying another random port.' % port)
514 else:
515 else:
515 self.port = port
516 self.port = port
516 success = True
517 success = True
517 break
518 break
518 if not success:
519 if not success:
519 self.log.critical('ERROR: the notebook server could not be started because '
520 self.log.critical('ERROR: the notebook server could not be started because '
520 'no available port could be found.')
521 'no available port could be found.')
521 self.exit(1)
522 self.exit(1)
522
523
523 def init_signal(self):
524 def init_signal(self):
524 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
525 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
525 # safely extract zmq version info:
526 # safely extract zmq version info:
526 try:
527 try:
527 zmq_v = zmq.pyzmq_version_info()
528 zmq_v = zmq.pyzmq_version_info()
528 except AttributeError:
529 except AttributeError:
529 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
530 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
530 if 'dev' in zmq.__version__:
531 if 'dev' in zmq.__version__:
531 zmq_v.append(999)
532 zmq_v.append(999)
532 zmq_v = tuple(zmq_v)
533 zmq_v = tuple(zmq_v)
533 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
534 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
534 # This won't work with 2.1.7 and
535 # This won't work with 2.1.7 and
535 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
536 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
536 # but it will work
537 # but it will work
537 signal.signal(signal.SIGINT, self._handle_sigint)
538 signal.signal(signal.SIGINT, self._handle_sigint)
538 signal.signal(signal.SIGTERM, self._signal_stop)
539 signal.signal(signal.SIGTERM, self._signal_stop)
539
540
540 def _handle_sigint(self, sig, frame):
541 def _handle_sigint(self, sig, frame):
541 """SIGINT handler spawns confirmation dialog"""
542 """SIGINT handler spawns confirmation dialog"""
542 # register more forceful signal handler for ^C^C case
543 # register more forceful signal handler for ^C^C case
543 signal.signal(signal.SIGINT, self._signal_stop)
544 signal.signal(signal.SIGINT, self._signal_stop)
544 # request confirmation dialog in bg thread, to avoid
545 # request confirmation dialog in bg thread, to avoid
545 # blocking the App
546 # blocking the App
546 thread = threading.Thread(target=self._confirm_exit)
547 thread = threading.Thread(target=self._confirm_exit)
547 thread.daemon = True
548 thread.daemon = True
548 thread.start()
549 thread.start()
549
550
550 def _restore_sigint_handler(self):
551 def _restore_sigint_handler(self):
551 """callback for restoring original SIGINT handler"""
552 """callback for restoring original SIGINT handler"""
552 signal.signal(signal.SIGINT, self._handle_sigint)
553 signal.signal(signal.SIGINT, self._handle_sigint)
553
554
554 def _confirm_exit(self):
555 def _confirm_exit(self):
555 """confirm shutdown on ^C
556 """confirm shutdown on ^C
556
557
557 A second ^C, or answering 'y' within 5s will cause shutdown,
558 A second ^C, or answering 'y' within 5s will cause shutdown,
558 otherwise original SIGINT handler will be restored.
559 otherwise original SIGINT handler will be restored.
559
560
560 This doesn't work on Windows.
561 This doesn't work on Windows.
561 """
562 """
562 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
563 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
563 time.sleep(0.1)
564 time.sleep(0.1)
564 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
565 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
565 sys.stdout.flush()
566 sys.stdout.flush()
566 r,w,x = select.select([sys.stdin], [], [], 5)
567 r,w,x = select.select([sys.stdin], [], [], 5)
567 if r:
568 if r:
568 line = sys.stdin.readline()
569 line = sys.stdin.readline()
569 if line.lower().startswith('y'):
570 if line.lower().startswith('y'):
570 self.log.critical("Shutdown confirmed")
571 self.log.critical("Shutdown confirmed")
571 ioloop.IOLoop.instance().stop()
572 ioloop.IOLoop.instance().stop()
572 return
573 return
573 else:
574 else:
574 print "No answer for 5s:",
575 print "No answer for 5s:",
575 print "resuming operation..."
576 print "resuming operation..."
576 # no answer, or answer is no:
577 # no answer, or answer is no:
577 # set it back to original SIGINT handler
578 # set it back to original SIGINT handler
578 # use IOLoop.add_callback because signal.signal must be called
579 # use IOLoop.add_callback because signal.signal must be called
579 # from main thread
580 # from main thread
580 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
581 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
581
582
582 def _signal_stop(self, sig, frame):
583 def _signal_stop(self, sig, frame):
583 self.log.critical("received signal %s, stopping", sig)
584 self.log.critical("received signal %s, stopping", sig)
584 ioloop.IOLoop.instance().stop()
585 ioloop.IOLoop.instance().stop()
585
586
586 @catch_config_error
587 @catch_config_error
587 def initialize(self, argv=None):
588 def initialize(self, argv=None):
588 self.init_logging()
589 self.init_logging()
589 super(NotebookApp, self).initialize(argv)
590 super(NotebookApp, self).initialize(argv)
590 self.init_configurables()
591 self.init_configurables()
591 self.init_webapp()
592 self.init_webapp()
592 self.init_signal()
593 self.init_signal()
593
594
594 def cleanup_kernels(self):
595 def cleanup_kernels(self):
595 """Shutdown all kernels.
596 """Shutdown all kernels.
596
597
597 The kernels will shutdown themselves when this process no longer exists,
598 The kernels will shutdown themselves when this process no longer exists,
598 but explicit shutdown allows the KernelManagers to cleanup the connection files.
599 but explicit shutdown allows the KernelManagers to cleanup the connection files.
599 """
600 """
600 self.log.info('Shutting down kernels')
601 self.log.info('Shutting down kernels')
601 self.kernel_manager.shutdown_all()
602 self.kernel_manager.shutdown_all()
602
603
603 def start(self):
604 def start(self):
604 ip = self.ip if self.ip else '[all ip addresses on your system]'
605 ip = self.ip if self.ip else '[all ip addresses on your system]'
605 proto = 'https' if self.certfile else 'http'
606 proto = 'https' if self.certfile else 'http'
606 info = self.log.info
607 info = self.log.info
607 info("The IPython Notebook is running at: %s://%s:%i%s" %
608 info("The IPython Notebook is running at: %s://%s:%i%s" %
608 (proto, ip, self.port,self.base_project_url) )
609 (proto, ip, self.port,self.base_project_url) )
609 info("Use Control-C to stop this server and shut down all kernels.")
610 info("Use Control-C to stop this server and shut down all kernels.")
610
611
611 if self.open_browser or self.file_to_run:
612 if self.open_browser or self.file_to_run:
612 ip = self.ip or '127.0.0.1'
613 ip = self.ip or '127.0.0.1'
613 try:
614 try:
614 browser = webbrowser.get(self.browser or None)
615 browser = webbrowser.get(self.browser or None)
615 except webbrowser.Error as e:
616 except webbrowser.Error as e:
616 self.log.warn('No web browser found: %s.' % e)
617 self.log.warn('No web browser found: %s.' % e)
617 browser = None
618 browser = None
618
619
619 if self.file_to_run:
620 if self.file_to_run:
620 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
621 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
621 url = self.notebook_manager.rev_mapping.get(name, '')
622 url = self.notebook_manager.rev_mapping.get(name, '')
622 else:
623 else:
623 url = ''
624 url = ''
624 if browser:
625 if browser:
625 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
626 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
626 self.port, self.base_project_url, url), new=2)
627 self.port, self.base_project_url, url), new=2)
627 threading.Thread(target=b).start()
628 threading.Thread(target=b).start()
628 try:
629 try:
629 ioloop.IOLoop.instance().start()
630 ioloop.IOLoop.instance().start()
630 except KeyboardInterrupt:
631 except KeyboardInterrupt:
631 info("Interrupted...")
632 info("Interrupted...")
632 finally:
633 finally:
633 self.cleanup_kernels()
634 self.cleanup_kernels()
634
635
635
636
636 #-----------------------------------------------------------------------------
637 #-----------------------------------------------------------------------------
637 # Main entry point
638 # Main entry point
638 #-----------------------------------------------------------------------------
639 #-----------------------------------------------------------------------------
639
640
640 def launch_new_instance():
641 def launch_new_instance():
641 app = NotebookApp.instance()
642 app = NotebookApp.instance()
642 app.initialize()
643 app.initialize()
643 app.start()
644 app.start()
644
645
@@ -1,368 +1,370 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14 * Paul Ivanov
14 * Paul Ivanov
15
15
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 # stdlib imports
22 # stdlib imports
23 import json
23 import json
24 import os
24 import os
25 import signal
25 import signal
26 import sys
26 import sys
27 import uuid
27 import uuid
28
28
29 # If run on Windows, install an exception hook which pops up a
29 # If run on Windows, install an exception hook which pops up a
30 # message box. Pythonw.exe hides the console, so without this
30 # message box. Pythonw.exe hides the console, so without this
31 # the application silently fails to load.
31 # the application silently fails to load.
32 #
32 #
33 # We always install this handler, because the expectation is for
33 # We always install this handler, because the expectation is for
34 # qtconsole to bring up a GUI even if called from the console.
34 # qtconsole to bring up a GUI even if called from the console.
35 # The old handler is called, so the exception is printed as well.
35 # The old handler is called, so the exception is printed as well.
36 # If desired, check for pythonw with an additional condition
36 # If desired, check for pythonw with an additional condition
37 # (sys.executable.lower().find('pythonw.exe') >= 0).
37 # (sys.executable.lower().find('pythonw.exe') >= 0).
38 if os.name == 'nt':
38 if os.name == 'nt':
39 old_excepthook = sys.excepthook
39 old_excepthook = sys.excepthook
40
40
41 def gui_excepthook(exctype, value, tb):
41 def gui_excepthook(exctype, value, tb):
42 try:
42 try:
43 import ctypes, traceback
43 import ctypes, traceback
44 MB_ICONERROR = 0x00000010L
44 MB_ICONERROR = 0x00000010L
45 title = u'Error starting IPython QtConsole'
45 title = u'Error starting IPython QtConsole'
46 msg = u''.join(traceback.format_exception(exctype, value, tb))
46 msg = u''.join(traceback.format_exception(exctype, value, tb))
47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
48 finally:
48 finally:
49 # Also call the old exception hook to let it do
49 # Also call the old exception hook to let it do
50 # its thing too.
50 # its thing too.
51 old_excepthook(exctype, value, tb)
51 old_excepthook(exctype, value, tb)
52
52
53 sys.excepthook = gui_excepthook
53 sys.excepthook = gui_excepthook
54
54
55 # System library imports
55 # System library imports
56 from IPython.external.qt import QtCore, QtGui
56 from IPython.external.qt import QtCore, QtGui
57
57
58 # Local imports
58 # Local imports
59 from IPython.config.application import boolean_flag, catch_config_error
59 from IPython.config.application import boolean_flag, catch_config_error
60 from IPython.core.application import BaseIPythonApplication
60 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.profiledir import ProfileDir
61 from IPython.core.profiledir import ProfileDir
62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
66 from IPython.frontend.qt.console import styles
66 from IPython.frontend.qt.console import styles
67 from IPython.frontend.qt.console.mainwindow import MainWindow
67 from IPython.frontend.qt.console.mainwindow import MainWindow
68 from IPython.frontend.qt.kernelmanager import QtKernelManager
68 from IPython.frontend.qt.kernelmanager import QtKernelManager
69 from IPython.utils.path import filefind
69 from IPython.utils.path import filefind
70 from IPython.utils.py3compat import str_to_bytes
70 from IPython.utils.py3compat import str_to_bytes
71 from IPython.utils.traitlets import (
71 from IPython.utils.traitlets import (
72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
73 )
73 )
74 from IPython.zmq.ipkernel import IPKernelApp
74 from IPython.zmq.ipkernel import IPKernelApp
75 from IPython.zmq.session import Session, default_secure
75 from IPython.zmq.session import Session, default_secure
76 from IPython.zmq.zmqshell import ZMQInteractiveShell
76 from IPython.zmq.zmqshell import ZMQInteractiveShell
77
77
78 from IPython.frontend.consoleapp import (
78 from IPython.frontend.consoleapp import (
79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
80 )
80 )
81
81
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83 # Network Constants
83 # Network Constants
84 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
85
85
86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Globals
89 # Globals
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 _examples = """
92 _examples = """
93 ipython qtconsole # start the qtconsole
93 ipython qtconsole # start the qtconsole
94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
95 """
95 """
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Aliases and Flags
98 # Aliases and Flags
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 # start with copy of flags
101 # start with copy of flags
102 flags = dict(flags)
102 flags = dict(flags)
103 qt_flags = {
103 qt_flags = {
104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
105 "Disable rich text support."),
105 "Disable rich text support."),
106 }
106 }
107
107
108 # and app_flags from the Console Mixin
108 # and app_flags from the Console Mixin
109 qt_flags.update(app_flags)
109 qt_flags.update(app_flags)
110 # add frontend flags to the full set
110 # add frontend flags to the full set
111 flags.update(qt_flags)
111 flags.update(qt_flags)
112
112
113 # start with copy of front&backend aliases list
113 # start with copy of front&backend aliases list
114 aliases = dict(aliases)
114 aliases = dict(aliases)
115 qt_aliases = dict(
115 qt_aliases = dict(
116 style = 'IPythonWidget.syntax_style',
116 style = 'IPythonWidget.syntax_style',
117 stylesheet = 'IPythonQtConsoleApp.stylesheet',
117 stylesheet = 'IPythonQtConsoleApp.stylesheet',
118 colors = 'ZMQInteractiveShell.colors',
118 colors = 'ZMQInteractiveShell.colors',
119
119
120 editor = 'IPythonWidget.editor',
120 editor = 'IPythonWidget.editor',
121 paging = 'ConsoleWidget.paging',
121 paging = 'ConsoleWidget.paging',
122 )
122 )
123 # and app_aliases from the Console Mixin
123 # and app_aliases from the Console Mixin
124 qt_aliases.update(app_aliases)
124 qt_aliases.update(app_aliases)
125 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
125 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
126 # add frontend aliases to the full set
126 # add frontend aliases to the full set
127 aliases.update(qt_aliases)
127 aliases.update(qt_aliases)
128
128
129 # get flags&aliases into sets, and remove a couple that
129 # get flags&aliases into sets, and remove a couple that
130 # shouldn't be scrubbed from backend flags:
130 # shouldn't be scrubbed from backend flags:
131 qt_aliases = set(qt_aliases.keys())
131 qt_aliases = set(qt_aliases.keys())
132 qt_aliases.remove('colors')
132 qt_aliases.remove('colors')
133 qt_flags = set(qt_flags.keys())
133 qt_flags = set(qt_flags.keys())
134
134
135 #-----------------------------------------------------------------------------
135 #-----------------------------------------------------------------------------
136 # Classes
136 # Classes
137 #-----------------------------------------------------------------------------
137 #-----------------------------------------------------------------------------
138
138
139 #-----------------------------------------------------------------------------
139 #-----------------------------------------------------------------------------
140 # IPythonQtConsole
140 # IPythonQtConsole
141 #-----------------------------------------------------------------------------
141 #-----------------------------------------------------------------------------
142
142
143
143
144 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
144 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
145 name = 'ipython-qtconsole'
145 name = 'ipython-qtconsole'
146
146
147 description = """
147 description = """
148 The IPython QtConsole.
148 The IPython QtConsole.
149
149
150 This launches a Console-style application using Qt. It is not a full
150 This launches a Console-style application using Qt. It is not a full
151 console, in that launched terminal subprocesses will not be able to accept
151 console, in that launched terminal subprocesses will not be able to accept
152 input.
152 input.
153
153
154 The QtConsole supports various extra features beyond the Terminal IPython
154 The QtConsole supports various extra features beyond the Terminal IPython
155 shell, such as inline plotting with matplotlib, via:
155 shell, such as inline plotting with matplotlib, via:
156
156
157 ipython qtconsole --pylab=inline
157 ipython qtconsole --pylab=inline
158
158
159 as well as saving your session as HTML, and printing the output.
159 as well as saving your session as HTML, and printing the output.
160
160
161 """
161 """
162 examples = _examples
162 examples = _examples
163
163
164 classes = [IPythonWidget] + IPythonConsoleApp.classes
164 classes = [IPythonWidget] + IPythonConsoleApp.classes
165 flags = Dict(flags)
165 flags = Dict(flags)
166 aliases = Dict(aliases)
166 aliases = Dict(aliases)
167 frontend_flags = Any(qt_flags)
167 frontend_flags = Any(qt_flags)
168 frontend_aliases = Any(qt_aliases)
168 frontend_aliases = Any(qt_aliases)
169 kernel_manager_class = QtKernelManager
169 kernel_manager_class = QtKernelManager
170
170
171 stylesheet = Unicode('', config=True,
171 stylesheet = Unicode('', config=True,
172 help="path to a custom CSS stylesheet")
172 help="path to a custom CSS stylesheet")
173
173
174 plain = CBool(False, config=True,
174 plain = CBool(False, config=True,
175 help="Use a plaintext widget instead of rich text (plain can't print/save).")
175 help="Use a plaintext widget instead of rich text (plain can't print/save).")
176
176
177 def _plain_changed(self, name, old, new):
177 def _plain_changed(self, name, old, new):
178 kind = 'plain' if new else 'rich'
178 kind = 'plain' if new else 'rich'
179 self.config.ConsoleWidget.kind = kind
179 self.config.ConsoleWidget.kind = kind
180 if new:
180 if new:
181 self.widget_factory = IPythonWidget
181 self.widget_factory = IPythonWidget
182 else:
182 else:
183 self.widget_factory = RichIPythonWidget
183 self.widget_factory = RichIPythonWidget
184
184
185 # the factory for creating a widget
185 # the factory for creating a widget
186 widget_factory = Any(RichIPythonWidget)
186 widget_factory = Any(RichIPythonWidget)
187
187
188 def parse_command_line(self, argv=None):
188 def parse_command_line(self, argv=None):
189 super(IPythonQtConsoleApp, self).parse_command_line(argv)
189 super(IPythonQtConsoleApp, self).parse_command_line(argv)
190 self.build_kernel_argv(argv)
190 self.build_kernel_argv(argv)
191
191
192
192
193 def new_frontend_master(self):
193 def new_frontend_master(self):
194 """ Create and return new frontend attached to new kernel, launched on localhost.
194 """ Create and return new frontend attached to new kernel, launched on localhost.
195 """
195 """
196 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
197 kernel_manager = self.kernel_manager_class(
196 kernel_manager = self.kernel_manager_class(
198 ip=ip,
199 connection_file=self._new_connection_file(),
197 connection_file=self._new_connection_file(),
200 config=self.config,
198 config=self.config,
201 )
199 )
202 # start the kernel
200 # start the kernel
203 kwargs = dict()
201 kwargs = dict()
204 kwargs['extra_arguments'] = self.kernel_argv
202 kwargs['extra_arguments'] = self.kernel_argv
205 kernel_manager.start_kernel(**kwargs)
203 kernel_manager.start_kernel(**kwargs)
206 kernel_manager.start_channels()
204 kernel_manager.start_channels()
207 widget = self.widget_factory(config=self.config,
205 widget = self.widget_factory(config=self.config,
208 local_kernel=True)
206 local_kernel=True)
209 self.init_colors(widget)
207 self.init_colors(widget)
210 widget.kernel_manager = kernel_manager
208 widget.kernel_manager = kernel_manager
211 widget._existing = False
209 widget._existing = False
212 widget._may_close = True
210 widget._may_close = True
213 widget._confirm_exit = self.confirm_exit
211 widget._confirm_exit = self.confirm_exit
214 return widget
212 return widget
215
213
216 def new_frontend_slave(self, current_widget):
214 def new_frontend_slave(self, current_widget):
217 """Create and return a new frontend attached to an existing kernel.
215 """Create and return a new frontend attached to an existing kernel.
218
216
219 Parameters
217 Parameters
220 ----------
218 ----------
221 current_widget : IPythonWidget
219 current_widget : IPythonWidget
222 The IPythonWidget whose kernel this frontend is to share
220 The IPythonWidget whose kernel this frontend is to share
223 """
221 """
224 kernel_manager = self.kernel_manager_class(
222 kernel_manager = self.kernel_manager_class(
225 connection_file=current_widget.kernel_manager.connection_file,
223 connection_file=current_widget.kernel_manager.connection_file,
226 config = self.config,
224 config = self.config,
227 )
225 )
228 kernel_manager.load_connection_file()
226 kernel_manager.load_connection_file()
229 kernel_manager.start_channels()
227 kernel_manager.start_channels()
230 widget = self.widget_factory(config=self.config,
228 widget = self.widget_factory(config=self.config,
231 local_kernel=False)
229 local_kernel=False)
232 self.init_colors(widget)
230 self.init_colors(widget)
233 widget._existing = True
231 widget._existing = True
234 widget._may_close = False
232 widget._may_close = False
235 widget._confirm_exit = False
233 widget._confirm_exit = False
236 widget.kernel_manager = kernel_manager
234 widget.kernel_manager = kernel_manager
237 return widget
235 return widget
238
236
239 def init_qt_elements(self):
237 def init_qt_elements(self):
240 # Create the widget.
238 # Create the widget.
241 self.app = QtGui.QApplication([])
239 self.app = QtGui.QApplication([])
242
240
243 base_path = os.path.abspath(os.path.dirname(__file__))
241 base_path = os.path.abspath(os.path.dirname(__file__))
244 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
242 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
245 self.app.icon = QtGui.QIcon(icon_path)
243 self.app.icon = QtGui.QIcon(icon_path)
246 QtGui.QApplication.setWindowIcon(self.app.icon)
244 QtGui.QApplication.setWindowIcon(self.app.icon)
247
245
248 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
246 try:
247 ip = self.config.KernelManager.ip
248 except AttributeError:
249 ip = LOCALHOST
250 local_kernel = (not self.existing) or ip in LOCAL_IPS
249 self.widget = self.widget_factory(config=self.config,
251 self.widget = self.widget_factory(config=self.config,
250 local_kernel=local_kernel)
252 local_kernel=local_kernel)
251 self.init_colors(self.widget)
253 self.init_colors(self.widget)
252 self.widget._existing = self.existing
254 self.widget._existing = self.existing
253 self.widget._may_close = not self.existing
255 self.widget._may_close = not self.existing
254 self.widget._confirm_exit = self.confirm_exit
256 self.widget._confirm_exit = self.confirm_exit
255
257
256 self.widget.kernel_manager = self.kernel_manager
258 self.widget.kernel_manager = self.kernel_manager
257 self.window = MainWindow(self.app,
259 self.window = MainWindow(self.app,
258 confirm_exit=self.confirm_exit,
260 confirm_exit=self.confirm_exit,
259 new_frontend_factory=self.new_frontend_master,
261 new_frontend_factory=self.new_frontend_master,
260 slave_frontend_factory=self.new_frontend_slave,
262 slave_frontend_factory=self.new_frontend_slave,
261 )
263 )
262 self.window.log = self.log
264 self.window.log = self.log
263 self.window.add_tab_with_frontend(self.widget)
265 self.window.add_tab_with_frontend(self.widget)
264 self.window.init_menu_bar()
266 self.window.init_menu_bar()
265
267
266 self.window.setWindowTitle('IPython')
268 self.window.setWindowTitle('IPython')
267
269
268 def init_colors(self, widget):
270 def init_colors(self, widget):
269 """Configure the coloring of the widget"""
271 """Configure the coloring of the widget"""
270 # Note: This will be dramatically simplified when colors
272 # Note: This will be dramatically simplified when colors
271 # are removed from the backend.
273 # are removed from the backend.
272
274
273 # parse the colors arg down to current known labels
275 # parse the colors arg down to current known labels
274 try:
276 try:
275 colors = self.config.ZMQInteractiveShell.colors
277 colors = self.config.ZMQInteractiveShell.colors
276 except AttributeError:
278 except AttributeError:
277 colors = None
279 colors = None
278 try:
280 try:
279 style = self.config.IPythonWidget.syntax_style
281 style = self.config.IPythonWidget.syntax_style
280 except AttributeError:
282 except AttributeError:
281 style = None
283 style = None
282 try:
284 try:
283 sheet = self.config.IPythonWidget.style_sheet
285 sheet = self.config.IPythonWidget.style_sheet
284 except AttributeError:
286 except AttributeError:
285 sheet = None
287 sheet = None
286
288
287 # find the value for colors:
289 # find the value for colors:
288 if colors:
290 if colors:
289 colors=colors.lower()
291 colors=colors.lower()
290 if colors in ('lightbg', 'light'):
292 if colors in ('lightbg', 'light'):
291 colors='lightbg'
293 colors='lightbg'
292 elif colors in ('dark', 'linux'):
294 elif colors in ('dark', 'linux'):
293 colors='linux'
295 colors='linux'
294 else:
296 else:
295 colors='nocolor'
297 colors='nocolor'
296 elif style:
298 elif style:
297 if style=='bw':
299 if style=='bw':
298 colors='nocolor'
300 colors='nocolor'
299 elif styles.dark_style(style):
301 elif styles.dark_style(style):
300 colors='linux'
302 colors='linux'
301 else:
303 else:
302 colors='lightbg'
304 colors='lightbg'
303 else:
305 else:
304 colors=None
306 colors=None
305
307
306 # Configure the style
308 # Configure the style
307 if style:
309 if style:
308 widget.style_sheet = styles.sheet_from_template(style, colors)
310 widget.style_sheet = styles.sheet_from_template(style, colors)
309 widget.syntax_style = style
311 widget.syntax_style = style
310 widget._syntax_style_changed()
312 widget._syntax_style_changed()
311 widget._style_sheet_changed()
313 widget._style_sheet_changed()
312 elif colors:
314 elif colors:
313 # use a default dark/light/bw style
315 # use a default dark/light/bw style
314 widget.set_default_style(colors=colors)
316 widget.set_default_style(colors=colors)
315
317
316 if self.stylesheet:
318 if self.stylesheet:
317 # we got an explicit stylesheet
319 # we got an explicit stylesheet
318 if os.path.isfile(self.stylesheet):
320 if os.path.isfile(self.stylesheet):
319 with open(self.stylesheet) as f:
321 with open(self.stylesheet) as f:
320 sheet = f.read()
322 sheet = f.read()
321 else:
323 else:
322 raise IOError("Stylesheet %r not found." % self.stylesheet)
324 raise IOError("Stylesheet %r not found." % self.stylesheet)
323 if sheet:
325 if sheet:
324 widget.style_sheet = sheet
326 widget.style_sheet = sheet
325 widget._style_sheet_changed()
327 widget._style_sheet_changed()
326
328
327
329
328 def init_signal(self):
330 def init_signal(self):
329 """allow clean shutdown on sigint"""
331 """allow clean shutdown on sigint"""
330 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
332 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
331 # need a timer, so that QApplication doesn't block until a real
333 # need a timer, so that QApplication doesn't block until a real
332 # Qt event fires (can require mouse movement)
334 # Qt event fires (can require mouse movement)
333 # timer trick from http://stackoverflow.com/q/4938723/938949
335 # timer trick from http://stackoverflow.com/q/4938723/938949
334 timer = QtCore.QTimer()
336 timer = QtCore.QTimer()
335 # Let the interpreter run each 200 ms:
337 # Let the interpreter run each 200 ms:
336 timer.timeout.connect(lambda: None)
338 timer.timeout.connect(lambda: None)
337 timer.start(200)
339 timer.start(200)
338 # hold onto ref, so the timer doesn't get cleaned up
340 # hold onto ref, so the timer doesn't get cleaned up
339 self._sigint_timer = timer
341 self._sigint_timer = timer
340
342
341 @catch_config_error
343 @catch_config_error
342 def initialize(self, argv=None):
344 def initialize(self, argv=None):
343 super(IPythonQtConsoleApp, self).initialize(argv)
345 super(IPythonQtConsoleApp, self).initialize(argv)
344 IPythonConsoleApp.initialize(self,argv)
346 IPythonConsoleApp.initialize(self,argv)
345 self.init_qt_elements()
347 self.init_qt_elements()
346 self.init_signal()
348 self.init_signal()
347
349
348 def start(self):
350 def start(self):
349
351
350 # draw the window
352 # draw the window
351 self.window.show()
353 self.window.show()
352 self.window.raise_()
354 self.window.raise_()
353
355
354 # Start the application main loop.
356 # Start the application main loop.
355 self.app.exec_()
357 self.app.exec_()
356
358
357 #-----------------------------------------------------------------------------
359 #-----------------------------------------------------------------------------
358 # Main entry point
360 # Main entry point
359 #-----------------------------------------------------------------------------
361 #-----------------------------------------------------------------------------
360
362
361 def main():
363 def main():
362 app = IPythonQtConsoleApp()
364 app = IPythonQtConsoleApp()
363 app.initialize()
365 app.initialize()
364 app.start()
366 app.start()
365
367
366
368
367 if __name__ == '__main__':
369 if __name__ == '__main__':
368 main()
370 main()
General Comments 0
You need to be logged in to leave comments. Login now