##// END OF EJS Templates
Merge pull request #3307 from minrk/wsproto...
Matthias Bussonnier -
r10847:3c2f44d2 merge
parent child Browse files
Show More
@@ -1,455 +1,450 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 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
19
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import stat
26 import stat
27 import threading
27 import threading
28
28
29 from tornado import web
29 from tornado import web
30 from tornado import websocket
30 from tornado import websocket
31
31
32 try:
32 try:
33 from tornado.log import app_log
33 from tornado.log import app_log
34 except ImportError:
34 except ImportError:
35 app_log = logging.getLogger()
35 app_log = logging.getLogger()
36
36
37 from IPython.config import Application
37 from IPython.config import Application
38 from IPython.external.decorator import decorator
38 from IPython.external.decorator import decorator
39 from IPython.utils.path import filefind
39 from IPython.utils.path import filefind
40
40
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44
44
45 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 # Google Chrome, as of release 16, changed its websocket protocol number. The
46 # parts tornado cares about haven't really changed, so it's OK to continue
46 # parts tornado cares about haven't really changed, so it's OK to continue
47 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
48 # version as of Oct 30/2011) the version check fails, see the issue report:
48 # version as of Oct 30/2011) the version check fails, see the issue report:
49
49
50 # https://github.com/facebook/tornado/issues/385
50 # https://github.com/facebook/tornado/issues/385
51
51
52 # This issue has been fixed in Tornado post 2.1.1:
52 # This issue has been fixed in Tornado post 2.1.1:
53
53
54 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
55
55
56 # Here we manually apply the same patch as above so that users of IPython can
56 # Here we manually apply the same patch as above so that users of IPython can
57 # continue to work with an officially released Tornado. We make the
57 # continue to work with an officially released Tornado. We make the
58 # monkeypatch version check as narrow as possible to limit its effects; once
58 # monkeypatch version check as narrow as possible to limit its effects; once
59 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
60
60
61 import tornado
61 import tornado
62
62
63 if tornado.version_info <= (2,1,1):
63 if tornado.version_info <= (2,1,1):
64
64
65 def _execute(self, transforms, *args, **kwargs):
65 def _execute(self, transforms, *args, **kwargs):
66 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
67
67
68 self.open_args = args
68 self.open_args = args
69 self.open_kwargs = kwargs
69 self.open_kwargs = kwargs
70
70
71 # The difference between version 8 and 13 is that in 8 the
71 # The difference between version 8 and 13 is that in 8 the
72 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 # client sends a "Sec-Websocket-Origin" header and in 13 it's
73 # simply "Origin".
73 # simply "Origin".
74 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
75 self.ws_connection = WebSocketProtocol8(self)
75 self.ws_connection = WebSocketProtocol8(self)
76 self.ws_connection.accept_connection()
76 self.ws_connection.accept_connection()
77
77
78 elif self.request.headers.get("Sec-WebSocket-Version"):
78 elif self.request.headers.get("Sec-WebSocket-Version"):
79 self.stream.write(tornado.escape.utf8(
79 self.stream.write(tornado.escape.utf8(
80 "HTTP/1.1 426 Upgrade Required\r\n"
80 "HTTP/1.1 426 Upgrade Required\r\n"
81 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 "Sec-WebSocket-Version: 8\r\n\r\n"))
82 self.stream.close()
82 self.stream.close()
83
83
84 else:
84 else:
85 self.ws_connection = WebSocketProtocol76(self)
85 self.ws_connection = WebSocketProtocol76(self)
86 self.ws_connection.accept_connection()
86 self.ws_connection.accept_connection()
87
87
88 websocket.WebSocketHandler._execute = _execute
88 websocket.WebSocketHandler._execute = _execute
89 del _execute
89 del _execute
90
90
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92 # Decorator for disabling read-only handlers
92 # Decorator for disabling read-only handlers
93 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
94
94
95 @decorator
95 @decorator
96 def not_if_readonly(f, self, *args, **kwargs):
96 def not_if_readonly(f, self, *args, **kwargs):
97 if self.settings.get('read_only', False):
97 if self.settings.get('read_only', False):
98 raise web.HTTPError(403, "Notebook server is read-only")
98 raise web.HTTPError(403, "Notebook server is read-only")
99 else:
99 else:
100 return f(self, *args, **kwargs)
100 return f(self, *args, **kwargs)
101
101
102 @decorator
102 @decorator
103 def authenticate_unless_readonly(f, self, *args, **kwargs):
103 def authenticate_unless_readonly(f, self, *args, **kwargs):
104 """authenticate this page *unless* readonly view is active.
104 """authenticate this page *unless* readonly view is active.
105
105
106 In read-only mode, the notebook list and print view should
106 In read-only mode, the notebook list and print view should
107 be accessible without authentication.
107 be accessible without authentication.
108 """
108 """
109
109
110 @web.authenticated
110 @web.authenticated
111 def auth_f(self, *args, **kwargs):
111 def auth_f(self, *args, **kwargs):
112 return f(self, *args, **kwargs)
112 return f(self, *args, **kwargs)
113
113
114 if self.settings.get('read_only', False):
114 if self.settings.get('read_only', False):
115 return f(self, *args, **kwargs)
115 return f(self, *args, **kwargs)
116 else:
116 else:
117 return auth_f(self, *args, **kwargs)
117 return auth_f(self, *args, **kwargs)
118
118
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120 # Top-level handlers
120 # Top-level handlers
121 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
122
122
123 class RequestHandler(web.RequestHandler):
123 class RequestHandler(web.RequestHandler):
124 """RequestHandler with default variable setting."""
124 """RequestHandler with default variable setting."""
125
125
126 def render(*args, **kwargs):
126 def render(*args, **kwargs):
127 kwargs.setdefault('message', '')
127 kwargs.setdefault('message', '')
128 return web.RequestHandler.render(*args, **kwargs)
128 return web.RequestHandler.render(*args, **kwargs)
129
129
130 class AuthenticatedHandler(RequestHandler):
130 class AuthenticatedHandler(RequestHandler):
131 """A RequestHandler with an authenticated user."""
131 """A RequestHandler with an authenticated user."""
132
132
133 def clear_login_cookie(self):
133 def clear_login_cookie(self):
134 self.clear_cookie(self.cookie_name)
134 self.clear_cookie(self.cookie_name)
135
135
136 def get_current_user(self):
136 def get_current_user(self):
137 user_id = self.get_secure_cookie(self.cookie_name)
137 user_id = self.get_secure_cookie(self.cookie_name)
138 # For now the user_id should not return empty, but it could eventually
138 # For now the user_id should not return empty, but it could eventually
139 if user_id == '':
139 if user_id == '':
140 user_id = 'anonymous'
140 user_id = 'anonymous'
141 if user_id is None:
141 if user_id is None:
142 # prevent extra Invalid cookie sig warnings:
142 # prevent extra Invalid cookie sig warnings:
143 self.clear_login_cookie()
143 self.clear_login_cookie()
144 if not self.read_only and not self.login_available:
144 if not self.read_only and not self.login_available:
145 user_id = 'anonymous'
145 user_id = 'anonymous'
146 return user_id
146 return user_id
147
147
148 @property
148 @property
149 def cookie_name(self):
149 def cookie_name(self):
150 default_cookie_name = 'username-{host}'.format(
150 default_cookie_name = 'username-{host}'.format(
151 host=self.request.host,
151 host=self.request.host,
152 ).replace(':', '-')
152 ).replace(':', '-')
153 return self.settings.get('cookie_name', default_cookie_name)
153 return self.settings.get('cookie_name', default_cookie_name)
154
154
155 @property
155 @property
156 def password(self):
156 def password(self):
157 """our password"""
157 """our password"""
158 return self.settings.get('password', '')
158 return self.settings.get('password', '')
159
159
160 @property
160 @property
161 def logged_in(self):
161 def logged_in(self):
162 """Is a user currently logged in?
162 """Is a user currently logged in?
163
163
164 """
164 """
165 user = self.get_current_user()
165 user = self.get_current_user()
166 return (user and not user == 'anonymous')
166 return (user and not user == 'anonymous')
167
167
168 @property
168 @property
169 def login_available(self):
169 def login_available(self):
170 """May a user proceed to log in?
170 """May a user proceed to log in?
171
171
172 This returns True if login capability is available, irrespective of
172 This returns True if login capability is available, irrespective of
173 whether the user is already logged in or not.
173 whether the user is already logged in or not.
174
174
175 """
175 """
176 return bool(self.settings.get('password', ''))
176 return bool(self.settings.get('password', ''))
177
177
178 @property
178 @property
179 def read_only(self):
179 def read_only(self):
180 """Is the notebook read-only?
180 """Is the notebook read-only?
181
181
182 """
182 """
183 return self.settings.get('read_only', False)
183 return self.settings.get('read_only', False)
184
184
185
185
186 class IPythonHandler(AuthenticatedHandler):
186 class IPythonHandler(AuthenticatedHandler):
187 """IPython-specific extensions to authenticated handling
187 """IPython-specific extensions to authenticated handling
188
188
189 Mostly property shortcuts to IPython-specific settings.
189 Mostly property shortcuts to IPython-specific settings.
190 """
190 """
191
191
192 @property
192 @property
193 def config(self):
193 def config(self):
194 return self.settings.get('config', None)
194 return self.settings.get('config', None)
195
195
196 @property
196 @property
197 def log(self):
197 def log(self):
198 """use the IPython log by default, falling back on tornado's logger"""
198 """use the IPython log by default, falling back on tornado's logger"""
199 if Application.initialized():
199 if Application.initialized():
200 return Application.instance().log
200 return Application.instance().log
201 else:
201 else:
202 return app_log
202 return app_log
203
203
204 @property
204 @property
205 def use_less(self):
205 def use_less(self):
206 """Use less instead of css in templates"""
206 """Use less instead of css in templates"""
207 return self.settings.get('use_less', False)
207 return self.settings.get('use_less', False)
208
208
209 #---------------------------------------------------------------
209 #---------------------------------------------------------------
210 # URLs
210 # URLs
211 #---------------------------------------------------------------
211 #---------------------------------------------------------------
212
212
213 @property
213 @property
214 def ws_url(self):
214 def ws_url(self):
215 """websocket url matching the current request
215 """websocket url matching the current request
216
216
217 turns http[s]://host[:port] into
217 By default, this is just `''`, indicating that it should match
218 ws[s]://host[:port]
218 the same host, protocol, port, etc.
219 """
219 """
220 proto = self.request.protocol.replace('http', 'ws')
220 return self.settings.get('websocket_url', '')
221 host = self.settings.get('websocket_host', '')
222 # default to config value
223 if host == '':
224 host = self.request.host # get from request
225 return "%s://%s" % (proto, host)
226
221
227 @property
222 @property
228 def mathjax_url(self):
223 def mathjax_url(self):
229 return self.settings.get('mathjax_url', '')
224 return self.settings.get('mathjax_url', '')
230
225
231 @property
226 @property
232 def base_project_url(self):
227 def base_project_url(self):
233 return self.settings.get('base_project_url', '/')
228 return self.settings.get('base_project_url', '/')
234
229
235 @property
230 @property
236 def base_kernel_url(self):
231 def base_kernel_url(self):
237 return self.settings.get('base_kernel_url', '/')
232 return self.settings.get('base_kernel_url', '/')
238
233
239 #---------------------------------------------------------------
234 #---------------------------------------------------------------
240 # Manager objects
235 # Manager objects
241 #---------------------------------------------------------------
236 #---------------------------------------------------------------
242
237
243 @property
238 @property
244 def kernel_manager(self):
239 def kernel_manager(self):
245 return self.settings['kernel_manager']
240 return self.settings['kernel_manager']
246
241
247 @property
242 @property
248 def notebook_manager(self):
243 def notebook_manager(self):
249 return self.settings['notebook_manager']
244 return self.settings['notebook_manager']
250
245
251 @property
246 @property
252 def cluster_manager(self):
247 def cluster_manager(self):
253 return self.settings['cluster_manager']
248 return self.settings['cluster_manager']
254
249
255 @property
250 @property
256 def project(self):
251 def project(self):
257 return self.notebook_manager.notebook_dir
252 return self.notebook_manager.notebook_dir
258
253
259 #---------------------------------------------------------------
254 #---------------------------------------------------------------
260 # template rendering
255 # template rendering
261 #---------------------------------------------------------------
256 #---------------------------------------------------------------
262
257
263 def get_template(self, name):
258 def get_template(self, name):
264 """Return the jinja template object for a given name"""
259 """Return the jinja template object for a given name"""
265 return self.settings['jinja2_env'].get_template(name)
260 return self.settings['jinja2_env'].get_template(name)
266
261
267 def render_template(self, name, **ns):
262 def render_template(self, name, **ns):
268 ns.update(self.template_namespace)
263 ns.update(self.template_namespace)
269 template = self.get_template(name)
264 template = self.get_template(name)
270 return template.render(**ns)
265 return template.render(**ns)
271
266
272 @property
267 @property
273 def template_namespace(self):
268 def template_namespace(self):
274 return dict(
269 return dict(
275 base_project_url=self.base_project_url,
270 base_project_url=self.base_project_url,
276 base_kernel_url=self.base_kernel_url,
271 base_kernel_url=self.base_kernel_url,
277 read_only=self.read_only,
272 read_only=self.read_only,
278 logged_in=self.logged_in,
273 logged_in=self.logged_in,
279 login_available=self.login_available,
274 login_available=self.login_available,
280 use_less=self.use_less,
275 use_less=self.use_less,
281 )
276 )
282
277
283 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
278 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
284 """static files should only be accessible when logged in"""
279 """static files should only be accessible when logged in"""
285
280
286 @authenticate_unless_readonly
281 @authenticate_unless_readonly
287 def get(self, path):
282 def get(self, path):
288 return web.StaticFileHandler.get(self, path)
283 return web.StaticFileHandler.get(self, path)
289
284
290
285
291 #-----------------------------------------------------------------------------
286 #-----------------------------------------------------------------------------
292 # File handler
287 # File handler
293 #-----------------------------------------------------------------------------
288 #-----------------------------------------------------------------------------
294
289
295 # to minimize subclass changes:
290 # to minimize subclass changes:
296 HTTPError = web.HTTPError
291 HTTPError = web.HTTPError
297
292
298 class FileFindHandler(web.StaticFileHandler):
293 class FileFindHandler(web.StaticFileHandler):
299 """subclass of StaticFileHandler for serving files from a search path"""
294 """subclass of StaticFileHandler for serving files from a search path"""
300
295
301 _static_paths = {}
296 _static_paths = {}
302 # _lock is needed for tornado < 2.2.0 compat
297 # _lock is needed for tornado < 2.2.0 compat
303 _lock = threading.Lock() # protects _static_hashes
298 _lock = threading.Lock() # protects _static_hashes
304
299
305 def initialize(self, path, default_filename=None):
300 def initialize(self, path, default_filename=None):
306 if isinstance(path, basestring):
301 if isinstance(path, basestring):
307 path = [path]
302 path = [path]
308 self.roots = tuple(
303 self.roots = tuple(
309 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
304 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
310 )
305 )
311 self.default_filename = default_filename
306 self.default_filename = default_filename
312
307
313 @classmethod
308 @classmethod
314 def locate_file(cls, path, roots):
309 def locate_file(cls, path, roots):
315 """locate a file to serve on our static file search path"""
310 """locate a file to serve on our static file search path"""
316 with cls._lock:
311 with cls._lock:
317 if path in cls._static_paths:
312 if path in cls._static_paths:
318 return cls._static_paths[path]
313 return cls._static_paths[path]
319 try:
314 try:
320 abspath = os.path.abspath(filefind(path, roots))
315 abspath = os.path.abspath(filefind(path, roots))
321 except IOError:
316 except IOError:
322 # empty string should always give exists=False
317 # empty string should always give exists=False
323 return ''
318 return ''
324
319
325 # os.path.abspath strips a trailing /
320 # os.path.abspath strips a trailing /
326 # it needs to be temporarily added back for requests to root/
321 # it needs to be temporarily added back for requests to root/
327 if not (abspath + os.path.sep).startswith(roots):
322 if not (abspath + os.path.sep).startswith(roots):
328 raise HTTPError(403, "%s is not in root static directory", path)
323 raise HTTPError(403, "%s is not in root static directory", path)
329
324
330 cls._static_paths[path] = abspath
325 cls._static_paths[path] = abspath
331 return abspath
326 return abspath
332
327
333 def get(self, path, include_body=True):
328 def get(self, path, include_body=True):
334 path = self.parse_url_path(path)
329 path = self.parse_url_path(path)
335
330
336 # begin subclass override
331 # begin subclass override
337 abspath = self.locate_file(path, self.roots)
332 abspath = self.locate_file(path, self.roots)
338 # end subclass override
333 # end subclass override
339
334
340 if os.path.isdir(abspath) and self.default_filename is not None:
335 if os.path.isdir(abspath) and self.default_filename is not None:
341 # need to look at the request.path here for when path is empty
336 # need to look at the request.path here for when path is empty
342 # but there is some prefix to the path that was already
337 # but there is some prefix to the path that was already
343 # trimmed by the routing
338 # trimmed by the routing
344 if not self.request.path.endswith("/"):
339 if not self.request.path.endswith("/"):
345 self.redirect(self.request.path + "/")
340 self.redirect(self.request.path + "/")
346 return
341 return
347 abspath = os.path.join(abspath, self.default_filename)
342 abspath = os.path.join(abspath, self.default_filename)
348 if not os.path.exists(abspath):
343 if not os.path.exists(abspath):
349 raise HTTPError(404)
344 raise HTTPError(404)
350 if not os.path.isfile(abspath):
345 if not os.path.isfile(abspath):
351 raise HTTPError(403, "%s is not a file", path)
346 raise HTTPError(403, "%s is not a file", path)
352
347
353 stat_result = os.stat(abspath)
348 stat_result = os.stat(abspath)
354 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
349 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
355
350
356 self.set_header("Last-Modified", modified)
351 self.set_header("Last-Modified", modified)
357
352
358 mime_type, encoding = mimetypes.guess_type(abspath)
353 mime_type, encoding = mimetypes.guess_type(abspath)
359 if mime_type:
354 if mime_type:
360 self.set_header("Content-Type", mime_type)
355 self.set_header("Content-Type", mime_type)
361
356
362 cache_time = self.get_cache_time(path, modified, mime_type)
357 cache_time = self.get_cache_time(path, modified, mime_type)
363
358
364 if cache_time > 0:
359 if cache_time > 0:
365 self.set_header("Expires", datetime.datetime.utcnow() + \
360 self.set_header("Expires", datetime.datetime.utcnow() + \
366 datetime.timedelta(seconds=cache_time))
361 datetime.timedelta(seconds=cache_time))
367 self.set_header("Cache-Control", "max-age=" + str(cache_time))
362 self.set_header("Cache-Control", "max-age=" + str(cache_time))
368 else:
363 else:
369 self.set_header("Cache-Control", "public")
364 self.set_header("Cache-Control", "public")
370
365
371 self.set_extra_headers(path)
366 self.set_extra_headers(path)
372
367
373 # Check the If-Modified-Since, and don't send the result if the
368 # Check the If-Modified-Since, and don't send the result if the
374 # content has not been modified
369 # content has not been modified
375 ims_value = self.request.headers.get("If-Modified-Since")
370 ims_value = self.request.headers.get("If-Modified-Since")
376 if ims_value is not None:
371 if ims_value is not None:
377 date_tuple = email.utils.parsedate(ims_value)
372 date_tuple = email.utils.parsedate(ims_value)
378 if_since = datetime.datetime(*date_tuple[:6])
373 if_since = datetime.datetime(*date_tuple[:6])
379 if if_since >= modified:
374 if if_since >= modified:
380 self.set_status(304)
375 self.set_status(304)
381 return
376 return
382
377
383 with open(abspath, "rb") as file:
378 with open(abspath, "rb") as file:
384 data = file.read()
379 data = file.read()
385 hasher = hashlib.sha1()
380 hasher = hashlib.sha1()
386 hasher.update(data)
381 hasher.update(data)
387 self.set_header("Etag", '"%s"' % hasher.hexdigest())
382 self.set_header("Etag", '"%s"' % hasher.hexdigest())
388 if include_body:
383 if include_body:
389 self.write(data)
384 self.write(data)
390 else:
385 else:
391 assert self.request.method == "HEAD"
386 assert self.request.method == "HEAD"
392 self.set_header("Content-Length", len(data))
387 self.set_header("Content-Length", len(data))
393
388
394 @classmethod
389 @classmethod
395 def get_version(cls, settings, path):
390 def get_version(cls, settings, path):
396 """Generate the version string to be used in static URLs.
391 """Generate the version string to be used in static URLs.
397
392
398 This method may be overridden in subclasses (but note that it
393 This method may be overridden in subclasses (but note that it
399 is a class method rather than a static method). The default
394 is a class method rather than a static method). The default
400 implementation uses a hash of the file's contents.
395 implementation uses a hash of the file's contents.
401
396
402 ``settings`` is the `Application.settings` dictionary and ``path``
397 ``settings`` is the `Application.settings` dictionary and ``path``
403 is the relative location of the requested asset on the filesystem.
398 is the relative location of the requested asset on the filesystem.
404 The returned value should be a string, or ``None`` if no version
399 The returned value should be a string, or ``None`` if no version
405 could be determined.
400 could be determined.
406 """
401 """
407 # begin subclass override:
402 # begin subclass override:
408 static_paths = settings['static_path']
403 static_paths = settings['static_path']
409 if isinstance(static_paths, basestring):
404 if isinstance(static_paths, basestring):
410 static_paths = [static_paths]
405 static_paths = [static_paths]
411 roots = tuple(
406 roots = tuple(
412 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
407 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
413 )
408 )
414
409
415 try:
410 try:
416 abs_path = filefind(path, roots)
411 abs_path = filefind(path, roots)
417 except IOError:
412 except IOError:
418 app_log.error("Could not find static file %r", path)
413 app_log.error("Could not find static file %r", path)
419 return None
414 return None
420
415
421 # end subclass override
416 # end subclass override
422
417
423 with cls._lock:
418 with cls._lock:
424 hashes = cls._static_hashes
419 hashes = cls._static_hashes
425 if abs_path not in hashes:
420 if abs_path not in hashes:
426 try:
421 try:
427 f = open(abs_path, "rb")
422 f = open(abs_path, "rb")
428 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
423 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
429 f.close()
424 f.close()
430 except Exception:
425 except Exception:
431 app_log.error("Could not open static file %r", path)
426 app_log.error("Could not open static file %r", path)
432 hashes[abs_path] = None
427 hashes[abs_path] = None
433 hsh = hashes.get(abs_path)
428 hsh = hashes.get(abs_path)
434 if hsh:
429 if hsh:
435 return hsh[:5]
430 return hsh[:5]
436 return None
431 return None
437
432
438
433
439 def parse_url_path(self, url_path):
434 def parse_url_path(self, url_path):
440 """Converts a static URL path into a filesystem path.
435 """Converts a static URL path into a filesystem path.
441
436
442 ``url_path`` is the path component of the URL with
437 ``url_path`` is the path component of the URL with
443 ``static_url_prefix`` removed. The return value should be
438 ``static_url_prefix`` removed. The return value should be
444 filesystem path relative to ``static_path``.
439 filesystem path relative to ``static_path``.
445 """
440 """
446 if os.path.sep != "/":
441 if os.path.sep != "/":
447 url_path = url_path.replace("/", os.path.sep)
442 url_path = url_path.replace("/", os.path.sep)
448 return url_path
443 return url_path
449
444
450 #-----------------------------------------------------------------------------
445 #-----------------------------------------------------------------------------
451 # URL to handler mappings
446 # URL to handler mappings
452 #-----------------------------------------------------------------------------
447 #-----------------------------------------------------------------------------
453
448
454
449
455 default_handlers = []
450 default_handlers = []
@@ -1,737 +1,741 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) 2013 The IPython Development Team
9 # Copyright (C) 2013 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 select
24 import select
25 import signal
25 import signal
26 import socket
26 import socket
27 import sys
27 import sys
28 import threading
28 import threading
29 import time
29 import time
30 import uuid
30 import uuid
31 import webbrowser
31 import webbrowser
32
32
33
33
34 # Third party
34 # Third party
35 # check for pyzmq 2.1.11
35 # check for pyzmq 2.1.11
36 from IPython.utils.zmqrelated import check_for_zmq
36 from IPython.utils.zmqrelated import check_for_zmq
37 check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
37 check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
38
38
39 import zmq
39 import zmq
40 from jinja2 import Environment, FileSystemLoader
40 from jinja2 import Environment, FileSystemLoader
41
41
42 # Install the pyzmq ioloop. This has to be done before anything else from
42 # Install the pyzmq ioloop. This has to be done before anything else from
43 # tornado is imported.
43 # tornado is imported.
44 from zmq.eventloop import ioloop
44 from zmq.eventloop import ioloop
45 ioloop.install()
45 ioloop.install()
46
46
47 # check for tornado 2.1.0
47 # check for tornado 2.1.0
48 msg = "The IPython Notebook requires tornado >= 2.1.0"
48 msg = "The IPython Notebook requires tornado >= 2.1.0"
49 try:
49 try:
50 import tornado
50 import tornado
51 except ImportError:
51 except ImportError:
52 raise ImportError(msg)
52 raise ImportError(msg)
53 try:
53 try:
54 version_info = tornado.version_info
54 version_info = tornado.version_info
55 except AttributeError:
55 except AttributeError:
56 raise ImportError(msg + ", but you have < 1.1.0")
56 raise ImportError(msg + ", but you have < 1.1.0")
57 if version_info < (2,1,0):
57 if version_info < (2,1,0):
58 raise ImportError(msg + ", but you have %s" % tornado.version)
58 raise ImportError(msg + ", but you have %s" % tornado.version)
59
59
60 from tornado import httpserver
60 from tornado import httpserver
61 from tornado import web
61 from tornado import web
62
62
63 # Our own libraries
63 # Our own libraries
64 from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
64 from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
65
65
66 from .services.kernels.kernelmanager import MappingKernelManager
66 from .services.kernels.kernelmanager import MappingKernelManager
67 from .services.notebooks.nbmanager import NotebookManager
67 from .services.notebooks.nbmanager import NotebookManager
68 from .services.notebooks.filenbmanager import FileNotebookManager
68 from .services.notebooks.filenbmanager import FileNotebookManager
69 from .services.clusters.clustermanager import ClusterManager
69 from .services.clusters.clustermanager import ClusterManager
70
70
71 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
71 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
72
72
73 from IPython.config.application import catch_config_error, boolean_flag
73 from IPython.config.application import catch_config_error, boolean_flag
74 from IPython.core.application import BaseIPythonApplication
74 from IPython.core.application import BaseIPythonApplication
75 from IPython.frontend.consoleapp import IPythonConsoleApp
75 from IPython.frontend.consoleapp import IPythonConsoleApp
76 from IPython.kernel import swallow_argv
76 from IPython.kernel import swallow_argv
77 from IPython.kernel.zmq.session import default_secure
77 from IPython.kernel.zmq.session import default_secure
78 from IPython.kernel.zmq.kernelapp import (
78 from IPython.kernel.zmq.kernelapp import (
79 kernel_flags,
79 kernel_flags,
80 kernel_aliases,
80 kernel_aliases,
81 )
81 )
82 from IPython.utils.importstring import import_item
82 from IPython.utils.importstring import import_item
83 from IPython.utils.localinterfaces import LOCALHOST
83 from IPython.utils.localinterfaces import LOCALHOST
84 from IPython.utils import submodule
84 from IPython.utils import submodule
85 from IPython.utils.traitlets import (
85 from IPython.utils.traitlets import (
86 Dict, Unicode, Integer, List, Bool,
86 Dict, Unicode, Integer, List, Bool,
87 DottedObjectName
87 DottedObjectName
88 )
88 )
89 from IPython.utils import py3compat
89 from IPython.utils import py3compat
90 from IPython.utils.path import filefind
90 from IPython.utils.path import filefind
91
91
92 from .utils import url_path_join
92 from .utils import url_path_join
93
93
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95 # Module globals
95 # Module globals
96 #-----------------------------------------------------------------------------
96 #-----------------------------------------------------------------------------
97
97
98 _examples = """
98 _examples = """
99 ipython notebook # start the notebook
99 ipython notebook # start the notebook
100 ipython notebook --profile=sympy # use the sympy profile
100 ipython notebook --profile=sympy # use the sympy profile
101 ipython notebook --pylab=inline # pylab in inline plotting mode
101 ipython notebook --pylab=inline # pylab in inline plotting mode
102 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
102 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
103 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
103 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
104 """
104 """
105
105
106 #-----------------------------------------------------------------------------
106 #-----------------------------------------------------------------------------
107 # Helper functions
107 # Helper functions
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109
109
110 def random_ports(port, n):
110 def random_ports(port, n):
111 """Generate a list of n random ports near the given port.
111 """Generate a list of n random ports near the given port.
112
112
113 The first 5 ports will be sequential, and the remaining n-5 will be
113 The first 5 ports will be sequential, and the remaining n-5 will be
114 randomly selected in the range [port-2*n, port+2*n].
114 randomly selected in the range [port-2*n, port+2*n].
115 """
115 """
116 for i in range(min(5, n)):
116 for i in range(min(5, n)):
117 yield port + i
117 yield port + i
118 for i in range(n-5):
118 for i in range(n-5):
119 yield port + random.randint(-2*n, 2*n)
119 yield port + random.randint(-2*n, 2*n)
120
120
121 def load_handlers(name):
121 def load_handlers(name):
122 """Load the (URL pattern, handler) tuples for each component."""
122 """Load the (URL pattern, handler) tuples for each component."""
123 name = 'IPython.frontend.html.notebook.' + name
123 name = 'IPython.frontend.html.notebook.' + name
124 mod = __import__(name, fromlist=['default_handlers'])
124 mod = __import__(name, fromlist=['default_handlers'])
125 return mod.default_handlers
125 return mod.default_handlers
126
126
127 #-----------------------------------------------------------------------------
127 #-----------------------------------------------------------------------------
128 # The Tornado web application
128 # The Tornado web application
129 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
130
130
131 class NotebookWebApplication(web.Application):
131 class NotebookWebApplication(web.Application):
132
132
133 def __init__(self, ipython_app, kernel_manager, notebook_manager,
133 def __init__(self, ipython_app, kernel_manager, notebook_manager,
134 cluster_manager, log,
134 cluster_manager, log,
135 base_project_url, settings_overrides):
135 base_project_url, settings_overrides):
136
136
137 settings = self.init_settings(
137 settings = self.init_settings(
138 ipython_app, kernel_manager, notebook_manager, cluster_manager,
138 ipython_app, kernel_manager, notebook_manager, cluster_manager,
139 log, base_project_url, settings_overrides)
139 log, base_project_url, settings_overrides)
140 handlers = self.init_handlers(settings)
140 handlers = self.init_handlers(settings)
141
141
142 super(NotebookWebApplication, self).__init__(handlers, **settings)
142 super(NotebookWebApplication, self).__init__(handlers, **settings)
143
143
144 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
144 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
145 cluster_manager, log,
145 cluster_manager, log,
146 base_project_url, settings_overrides):
146 base_project_url, settings_overrides):
147 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
147 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
148 # base_project_url will always be unicode, which will in turn
148 # base_project_url will always be unicode, which will in turn
149 # make the patterns unicode, and ultimately result in unicode
149 # make the patterns unicode, and ultimately result in unicode
150 # keys in kwargs to handler._execute(**kwargs) in tornado.
150 # keys in kwargs to handler._execute(**kwargs) in tornado.
151 # This enforces that base_project_url be ascii in that situation.
151 # This enforces that base_project_url be ascii in that situation.
152 #
152 #
153 # Note that the URLs these patterns check against are escaped,
153 # Note that the URLs these patterns check against are escaped,
154 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
154 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
155 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
155 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
156 template_path = os.path.join(os.path.dirname(__file__), "templates")
156 template_path = os.path.join(os.path.dirname(__file__), "templates")
157 settings = dict(
157 settings = dict(
158 # basics
158 # basics
159 base_project_url=base_project_url,
159 base_project_url=base_project_url,
160 base_kernel_url=ipython_app.base_kernel_url,
160 base_kernel_url=ipython_app.base_kernel_url,
161 template_path=template_path,
161 template_path=template_path,
162 static_path=ipython_app.static_file_path,
162 static_path=ipython_app.static_file_path,
163 static_handler_class = FileFindHandler,
163 static_handler_class = FileFindHandler,
164 static_url_prefix = url_path_join(base_project_url,'/static/'),
164 static_url_prefix = url_path_join(base_project_url,'/static/'),
165
165
166 # authentication
166 # authentication
167 cookie_secret=os.urandom(1024),
167 cookie_secret=os.urandom(1024),
168 login_url=url_path_join(base_project_url,'/login'),
168 login_url=url_path_join(base_project_url,'/login'),
169 read_only=ipython_app.read_only,
169 read_only=ipython_app.read_only,
170 password=ipython_app.password,
170 password=ipython_app.password,
171
171
172 # managers
172 # managers
173 kernel_manager=kernel_manager,
173 kernel_manager=kernel_manager,
174 notebook_manager=notebook_manager,
174 notebook_manager=notebook_manager,
175 cluster_manager=cluster_manager,
175 cluster_manager=cluster_manager,
176
176
177 # IPython stuff
177 # IPython stuff
178 mathjax_url=ipython_app.mathjax_url,
178 mathjax_url=ipython_app.mathjax_url,
179 max_msg_size=ipython_app.max_msg_size,
179 max_msg_size=ipython_app.max_msg_size,
180 config=ipython_app.config,
180 config=ipython_app.config,
181 use_less=ipython_app.use_less,
181 use_less=ipython_app.use_less,
182 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
182 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
183 )
183 )
184
184
185 # allow custom overrides for the tornado web app.
185 # allow custom overrides for the tornado web app.
186 settings.update(settings_overrides)
186 settings.update(settings_overrides)
187 return settings
187 return settings
188
188
189 def init_handlers(self, settings):
189 def init_handlers(self, settings):
190 # Load the (URL pattern, handler) tuples for each component.
190 # Load the (URL pattern, handler) tuples for each component.
191 handlers = []
191 handlers = []
192 handlers.extend(load_handlers('base.handlers'))
192 handlers.extend(load_handlers('base.handlers'))
193 handlers.extend(load_handlers('tree.handlers'))
193 handlers.extend(load_handlers('tree.handlers'))
194 handlers.extend(load_handlers('auth.login'))
194 handlers.extend(load_handlers('auth.login'))
195 handlers.extend(load_handlers('auth.logout'))
195 handlers.extend(load_handlers('auth.logout'))
196 handlers.extend(load_handlers('notebook.handlers'))
196 handlers.extend(load_handlers('notebook.handlers'))
197 handlers.extend(load_handlers('services.kernels.handlers'))
197 handlers.extend(load_handlers('services.kernels.handlers'))
198 handlers.extend(load_handlers('services.notebooks.handlers'))
198 handlers.extend(load_handlers('services.notebooks.handlers'))
199 handlers.extend(load_handlers('services.clusters.handlers'))
199 handlers.extend(load_handlers('services.clusters.handlers'))
200 handlers.extend([
200 handlers.extend([
201 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
201 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
202 ])
202 ])
203 # prepend base_project_url onto the patterns that we match
203 # prepend base_project_url onto the patterns that we match
204 new_handlers = []
204 new_handlers = []
205 for handler in handlers:
205 for handler in handlers:
206 pattern = url_path_join(settings['base_project_url'], handler[0])
206 pattern = url_path_join(settings['base_project_url'], handler[0])
207 new_handler = tuple([pattern] + list(handler[1:]))
207 new_handler = tuple([pattern] + list(handler[1:]))
208 new_handlers.append(new_handler)
208 new_handlers.append(new_handler)
209 return new_handlers
209 return new_handlers
210
210
211
211
212
212
213 #-----------------------------------------------------------------------------
213 #-----------------------------------------------------------------------------
214 # Aliases and Flags
214 # Aliases and Flags
215 #-----------------------------------------------------------------------------
215 #-----------------------------------------------------------------------------
216
216
217 flags = dict(kernel_flags)
217 flags = dict(kernel_flags)
218 flags['no-browser']=(
218 flags['no-browser']=(
219 {'NotebookApp' : {'open_browser' : False}},
219 {'NotebookApp' : {'open_browser' : False}},
220 "Don't open the notebook in a browser after startup."
220 "Don't open the notebook in a browser after startup."
221 )
221 )
222 flags['no-mathjax']=(
222 flags['no-mathjax']=(
223 {'NotebookApp' : {'enable_mathjax' : False}},
223 {'NotebookApp' : {'enable_mathjax' : False}},
224 """Disable MathJax
224 """Disable MathJax
225
225
226 MathJax is the javascript library IPython uses to render math/LaTeX. It is
226 MathJax is the javascript library IPython uses to render math/LaTeX. It is
227 very large, so you may want to disable it if you have a slow internet
227 very large, so you may want to disable it if you have a slow internet
228 connection, or for offline use of the notebook.
228 connection, or for offline use of the notebook.
229
229
230 When disabled, equations etc. will appear as their untransformed TeX source.
230 When disabled, equations etc. will appear as their untransformed TeX source.
231 """
231 """
232 )
232 )
233 flags['read-only'] = (
233 flags['read-only'] = (
234 {'NotebookApp' : {'read_only' : True}},
234 {'NotebookApp' : {'read_only' : True}},
235 """Allow read-only access to notebooks.
235 """Allow read-only access to notebooks.
236
236
237 When using a password to protect the notebook server, this flag
237 When using a password to protect the notebook server, this flag
238 allows unauthenticated clients to view the notebook list, and
238 allows unauthenticated clients to view the notebook list, and
239 individual notebooks, but not edit them, start kernels, or run
239 individual notebooks, but not edit them, start kernels, or run
240 code.
240 code.
241
241
242 If no password is set, the server will be entirely read-only.
242 If no password is set, the server will be entirely read-only.
243 """
243 """
244 )
244 )
245
245
246 # Add notebook manager flags
246 # Add notebook manager flags
247 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
247 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
248 'Auto-save a .py script everytime the .ipynb notebook is saved',
248 'Auto-save a .py script everytime the .ipynb notebook is saved',
249 'Do not auto-save .py scripts for every notebook'))
249 'Do not auto-save .py scripts for every notebook'))
250
250
251 # the flags that are specific to the frontend
251 # the flags that are specific to the frontend
252 # these must be scrubbed before being passed to the kernel,
252 # these must be scrubbed before being passed to the kernel,
253 # or it will raise an error on unrecognized flags
253 # or it will raise an error on unrecognized flags
254 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
254 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
255
255
256 aliases = dict(kernel_aliases)
256 aliases = dict(kernel_aliases)
257
257
258 aliases.update({
258 aliases.update({
259 'ip': 'NotebookApp.ip',
259 'ip': 'NotebookApp.ip',
260 'port': 'NotebookApp.port',
260 'port': 'NotebookApp.port',
261 'port-retries': 'NotebookApp.port_retries',
261 'port-retries': 'NotebookApp.port_retries',
262 'transport': 'KernelManager.transport',
262 'transport': 'KernelManager.transport',
263 'keyfile': 'NotebookApp.keyfile',
263 'keyfile': 'NotebookApp.keyfile',
264 'certfile': 'NotebookApp.certfile',
264 'certfile': 'NotebookApp.certfile',
265 'notebook-dir': 'NotebookManager.notebook_dir',
265 'notebook-dir': 'NotebookManager.notebook_dir',
266 'browser': 'NotebookApp.browser',
266 'browser': 'NotebookApp.browser',
267 })
267 })
268
268
269 # remove ipkernel flags that are singletons, and don't make sense in
269 # remove ipkernel flags that are singletons, and don't make sense in
270 # multi-kernel evironment:
270 # multi-kernel evironment:
271 aliases.pop('f', None)
271 aliases.pop('f', None)
272
272
273 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
273 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
274 u'notebook-dir']
274 u'notebook-dir']
275
275
276 #-----------------------------------------------------------------------------
276 #-----------------------------------------------------------------------------
277 # NotebookApp
277 # NotebookApp
278 #-----------------------------------------------------------------------------
278 #-----------------------------------------------------------------------------
279
279
280 class NotebookApp(BaseIPythonApplication):
280 class NotebookApp(BaseIPythonApplication):
281
281
282 name = 'ipython-notebook'
282 name = 'ipython-notebook'
283 default_config_file_name='ipython_notebook_config.py'
283 default_config_file_name='ipython_notebook_config.py'
284
284
285 description = """
285 description = """
286 The IPython HTML Notebook.
286 The IPython HTML Notebook.
287
287
288 This launches a Tornado based HTML Notebook Server that serves up an
288 This launches a Tornado based HTML Notebook Server that serves up an
289 HTML5/Javascript Notebook client.
289 HTML5/Javascript Notebook client.
290 """
290 """
291 examples = _examples
291 examples = _examples
292
292
293 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
293 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
294 FileNotebookManager]
294 FileNotebookManager]
295 flags = Dict(flags)
295 flags = Dict(flags)
296 aliases = Dict(aliases)
296 aliases = Dict(aliases)
297
297
298 kernel_argv = List(Unicode)
298 kernel_argv = List(Unicode)
299
299
300 max_msg_size = Integer(65536, config=True, help="""
300 max_msg_size = Integer(65536, config=True, help="""
301 The max raw message size accepted from the browser
301 The max raw message size accepted from the browser
302 over a WebSocket connection.
302 over a WebSocket connection.
303 """)
303 """)
304
304
305 def _log_level_default(self):
305 def _log_level_default(self):
306 return logging.INFO
306 return logging.INFO
307
307
308 def _log_format_default(self):
308 def _log_format_default(self):
309 """override default log format to include time"""
309 """override default log format to include time"""
310 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
310 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
311
311
312 # create requested profiles by default, if they don't exist:
312 # create requested profiles by default, if they don't exist:
313 auto_create = Bool(True)
313 auto_create = Bool(True)
314
314
315 # file to be opened in the notebook server
315 # file to be opened in the notebook server
316 file_to_run = Unicode('')
316 file_to_run = Unicode('')
317
317
318 # Network related information.
318 # Network related information.
319
319
320 ip = Unicode(LOCALHOST, config=True,
320 ip = Unicode(LOCALHOST, config=True,
321 help="The IP address the notebook server will listen on."
321 help="The IP address the notebook server will listen on."
322 )
322 )
323
323
324 def _ip_changed(self, name, old, new):
324 def _ip_changed(self, name, old, new):
325 if new == u'*': self.ip = u''
325 if new == u'*': self.ip = u''
326
326
327 port = Integer(8888, config=True,
327 port = Integer(8888, config=True,
328 help="The port the notebook server will listen on."
328 help="The port the notebook server will listen on."
329 )
329 )
330 port_retries = Integer(50, config=True,
330 port_retries = Integer(50, config=True,
331 help="The number of additional ports to try if the specified port is not available."
331 help="The number of additional ports to try if the specified port is not available."
332 )
332 )
333
333
334 certfile = Unicode(u'', config=True,
334 certfile = Unicode(u'', config=True,
335 help="""The full path to an SSL/TLS certificate file."""
335 help="""The full path to an SSL/TLS certificate file."""
336 )
336 )
337
337
338 keyfile = Unicode(u'', config=True,
338 keyfile = Unicode(u'', config=True,
339 help="""The full path to a private key file for usage with SSL/TLS."""
339 help="""The full path to a private key file for usage with SSL/TLS."""
340 )
340 )
341
341
342 password = Unicode(u'', config=True,
342 password = Unicode(u'', config=True,
343 help="""Hashed password to use for web authentication.
343 help="""Hashed password to use for web authentication.
344
344
345 To generate, type in a python/IPython shell:
345 To generate, type in a python/IPython shell:
346
346
347 from IPython.lib import passwd; passwd()
347 from IPython.lib import passwd; passwd()
348
348
349 The string should be of the form type:salt:hashed-password.
349 The string should be of the form type:salt:hashed-password.
350 """
350 """
351 )
351 )
352
352
353 open_browser = Bool(True, config=True,
353 open_browser = Bool(True, config=True,
354 help="""Whether to open in a browser after starting.
354 help="""Whether to open in a browser after starting.
355 The specific browser used is platform dependent and
355 The specific browser used is platform dependent and
356 determined by the python standard library `webbrowser`
356 determined by the python standard library `webbrowser`
357 module, unless it is overridden using the --browser
357 module, unless it is overridden using the --browser
358 (NotebookApp.browser) configuration option.
358 (NotebookApp.browser) configuration option.
359 """)
359 """)
360
360
361 browser = Unicode(u'', config=True,
361 browser = Unicode(u'', config=True,
362 help="""Specify what command to use to invoke a web
362 help="""Specify what command to use to invoke a web
363 browser when opening the notebook. If not specified, the
363 browser when opening the notebook. If not specified, the
364 default browser will be determined by the `webbrowser`
364 default browser will be determined by the `webbrowser`
365 standard library module, which allows setting of the
365 standard library module, which allows setting of the
366 BROWSER environment variable to override it.
366 BROWSER environment variable to override it.
367 """)
367 """)
368
368
369 read_only = Bool(False, config=True,
369 read_only = Bool(False, config=True,
370 help="Whether to prevent editing/execution of notebooks."
370 help="Whether to prevent editing/execution of notebooks."
371 )
371 )
372
372
373 use_less = Bool(False, config=True,
373 use_less = Bool(False, config=True,
374 help="""Wether to use Browser Side less-css parsing
374 help="""Wether to use Browser Side less-css parsing
375 instead of compiled css version in templates that allows
375 instead of compiled css version in templates that allows
376 it. This is mainly convenient when working on the less
376 it. This is mainly convenient when working on the less
377 file to avoid a build step, or if user want to overwrite
377 file to avoid a build step, or if user want to overwrite
378 some of the less variables without having to recompile
378 some of the less variables without having to recompile
379 everything.
379 everything.
380
380
381 You will need to install the less.js component in the static directory
381 You will need to install the less.js component in the static directory
382 either in the source tree or in your profile folder.
382 either in the source tree or in your profile folder.
383 """)
383 """)
384
384
385 webapp_settings = Dict(config=True,
385 webapp_settings = Dict(config=True,
386 help="Supply overrides for the tornado.web.Application that the "
386 help="Supply overrides for the tornado.web.Application that the "
387 "IPython notebook uses.")
387 "IPython notebook uses.")
388
388
389 enable_mathjax = Bool(True, config=True,
389 enable_mathjax = Bool(True, config=True,
390 help="""Whether to enable MathJax for typesetting math/TeX
390 help="""Whether to enable MathJax for typesetting math/TeX
391
391
392 MathJax is the javascript library IPython uses to render math/LaTeX. It is
392 MathJax is the javascript library IPython uses to render math/LaTeX. It is
393 very large, so you may want to disable it if you have a slow internet
393 very large, so you may want to disable it if you have a slow internet
394 connection, or for offline use of the notebook.
394 connection, or for offline use of the notebook.
395
395
396 When disabled, equations etc. will appear as their untransformed TeX source.
396 When disabled, equations etc. will appear as their untransformed TeX source.
397 """
397 """
398 )
398 )
399 def _enable_mathjax_changed(self, name, old, new):
399 def _enable_mathjax_changed(self, name, old, new):
400 """set mathjax url to empty if mathjax is disabled"""
400 """set mathjax url to empty if mathjax is disabled"""
401 if not new:
401 if not new:
402 self.mathjax_url = u''
402 self.mathjax_url = u''
403
403
404 base_project_url = Unicode('/', config=True,
404 base_project_url = Unicode('/', config=True,
405 help='''The base URL for the notebook server.
405 help='''The base URL for the notebook server.
406
406
407 Leading and trailing slashes can be omitted,
407 Leading and trailing slashes can be omitted,
408 and will automatically be added.
408 and will automatically be added.
409 ''')
409 ''')
410 def _base_project_url_changed(self, name, old, new):
410 def _base_project_url_changed(self, name, old, new):
411 if not new.startswith('/'):
411 if not new.startswith('/'):
412 self.base_project_url = '/'+new
412 self.base_project_url = '/'+new
413 elif not new.endswith('/'):
413 elif not new.endswith('/'):
414 self.base_project_url = new+'/'
414 self.base_project_url = new+'/'
415
415
416 base_kernel_url = Unicode('/', config=True,
416 base_kernel_url = Unicode('/', config=True,
417 help='''The base URL for the kernel server
417 help='''The base URL for the kernel server
418
418
419 Leading and trailing slashes can be omitted,
419 Leading and trailing slashes can be omitted,
420 and will automatically be added.
420 and will automatically be added.
421 ''')
421 ''')
422 def _base_kernel_url_changed(self, name, old, new):
422 def _base_kernel_url_changed(self, name, old, new):
423 if not new.startswith('/'):
423 if not new.startswith('/'):
424 self.base_kernel_url = '/'+new
424 self.base_kernel_url = '/'+new
425 elif not new.endswith('/'):
425 elif not new.endswith('/'):
426 self.base_kernel_url = new+'/'
426 self.base_kernel_url = new+'/'
427
427
428 websocket_host = Unicode("", config=True,
428 websocket_url = Unicode("", config=True,
429 help="""The hostname for the websocket server."""
429 help="""The base URL for the websocket server,
430 if it differs from the HTTP server (hint: it almost certainly doesn't).
431
432 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
433 """
430 )
434 )
431
435
432 extra_static_paths = List(Unicode, config=True,
436 extra_static_paths = List(Unicode, config=True,
433 help="""Extra paths to search for serving static files.
437 help="""Extra paths to search for serving static files.
434
438
435 This allows adding javascript/css to be available from the notebook server machine,
439 This allows adding javascript/css to be available from the notebook server machine,
436 or overriding individual files in the IPython"""
440 or overriding individual files in the IPython"""
437 )
441 )
438 def _extra_static_paths_default(self):
442 def _extra_static_paths_default(self):
439 return [os.path.join(self.profile_dir.location, 'static')]
443 return [os.path.join(self.profile_dir.location, 'static')]
440
444
441 @property
445 @property
442 def static_file_path(self):
446 def static_file_path(self):
443 """return extra paths + the default location"""
447 """return extra paths + the default location"""
444 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
448 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
445
449
446 mathjax_url = Unicode("", config=True,
450 mathjax_url = Unicode("", config=True,
447 help="""The url for MathJax.js."""
451 help="""The url for MathJax.js."""
448 )
452 )
449 def _mathjax_url_default(self):
453 def _mathjax_url_default(self):
450 if not self.enable_mathjax:
454 if not self.enable_mathjax:
451 return u''
455 return u''
452 static_url_prefix = self.webapp_settings.get("static_url_prefix",
456 static_url_prefix = self.webapp_settings.get("static_url_prefix",
453 "/static/")
457 "/static/")
454 try:
458 try:
455 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
459 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
456 except IOError:
460 except IOError:
457 if self.certfile:
461 if self.certfile:
458 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
462 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
459 base = u"https://c328740.ssl.cf1.rackcdn.com"
463 base = u"https://c328740.ssl.cf1.rackcdn.com"
460 else:
464 else:
461 base = u"http://cdn.mathjax.org"
465 base = u"http://cdn.mathjax.org"
462
466
463 url = base + u"/mathjax/latest/MathJax.js"
467 url = base + u"/mathjax/latest/MathJax.js"
464 self.log.info("Using MathJax from CDN: %s", url)
468 self.log.info("Using MathJax from CDN: %s", url)
465 return url
469 return url
466 else:
470 else:
467 self.log.info("Using local MathJax from %s" % mathjax)
471 self.log.info("Using local MathJax from %s" % mathjax)
468 return static_url_prefix+u"mathjax/MathJax.js"
472 return static_url_prefix+u"mathjax/MathJax.js"
469
473
470 def _mathjax_url_changed(self, name, old, new):
474 def _mathjax_url_changed(self, name, old, new):
471 if new and not self.enable_mathjax:
475 if new and not self.enable_mathjax:
472 # enable_mathjax=False overrides mathjax_url
476 # enable_mathjax=False overrides mathjax_url
473 self.mathjax_url = u''
477 self.mathjax_url = u''
474 else:
478 else:
475 self.log.info("Using MathJax: %s", new)
479 self.log.info("Using MathJax: %s", new)
476
480
477 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.services.notebooks.filenbmanager.FileNotebookManager',
481 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.services.notebooks.filenbmanager.FileNotebookManager',
478 config=True,
482 config=True,
479 help='The notebook manager class to use.')
483 help='The notebook manager class to use.')
480
484
481 trust_xheaders = Bool(False, config=True,
485 trust_xheaders = Bool(False, config=True,
482 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
486 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
483 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
487 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
484 )
488 )
485
489
486 def parse_command_line(self, argv=None):
490 def parse_command_line(self, argv=None):
487 super(NotebookApp, self).parse_command_line(argv)
491 super(NotebookApp, self).parse_command_line(argv)
488 if argv is None:
492 if argv is None:
489 argv = sys.argv[1:]
493 argv = sys.argv[1:]
490
494
491 # Scrub frontend-specific flags
495 # Scrub frontend-specific flags
492 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
496 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
493 # Kernel should inherit default config file from frontend
497 # Kernel should inherit default config file from frontend
494 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
498 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
495
499
496 if self.extra_args:
500 if self.extra_args:
497 f = os.path.abspath(self.extra_args[0])
501 f = os.path.abspath(self.extra_args[0])
498 if os.path.isdir(f):
502 if os.path.isdir(f):
499 nbdir = f
503 nbdir = f
500 else:
504 else:
501 self.file_to_run = f
505 self.file_to_run = f
502 nbdir = os.path.dirname(f)
506 nbdir = os.path.dirname(f)
503 self.config.NotebookManager.notebook_dir = nbdir
507 self.config.NotebookManager.notebook_dir = nbdir
504
508
505 def init_configurables(self):
509 def init_configurables(self):
506 # force Session default to be secure
510 # force Session default to be secure
507 default_secure(self.config)
511 default_secure(self.config)
508 self.kernel_manager = MappingKernelManager(
512 self.kernel_manager = MappingKernelManager(
509 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
513 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
510 connection_dir = self.profile_dir.security_dir,
514 connection_dir = self.profile_dir.security_dir,
511 )
515 )
512 kls = import_item(self.notebook_manager_class)
516 kls = import_item(self.notebook_manager_class)
513 self.notebook_manager = kls(config=self.config, log=self.log)
517 self.notebook_manager = kls(config=self.config, log=self.log)
514 self.notebook_manager.load_notebook_names()
518 self.notebook_manager.load_notebook_names()
515 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
519 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
516 self.cluster_manager.update_profiles()
520 self.cluster_manager.update_profiles()
517
521
518 def init_logging(self):
522 def init_logging(self):
519 # This prevents double log messages because tornado use a root logger that
523 # This prevents double log messages because tornado use a root logger that
520 # self.log is a child of. The logging module dipatches log messages to a log
524 # self.log is a child of. The logging module dipatches log messages to a log
521 # and all of its ancenstors until propagate is set to False.
525 # and all of its ancenstors until propagate is set to False.
522 self.log.propagate = False
526 self.log.propagate = False
523
527
524 # hook up tornado 3's loggers to our app handlers
528 # hook up tornado 3's loggers to our app handlers
525 for name in ('access', 'application', 'general'):
529 for name in ('access', 'application', 'general'):
526 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
530 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
527
531
528 def init_webapp(self):
532 def init_webapp(self):
529 """initialize tornado webapp and httpserver"""
533 """initialize tornado webapp and httpserver"""
530 self.web_app = NotebookWebApplication(
534 self.web_app = NotebookWebApplication(
531 self, self.kernel_manager, self.notebook_manager,
535 self, self.kernel_manager, self.notebook_manager,
532 self.cluster_manager, self.log,
536 self.cluster_manager, self.log,
533 self.base_project_url, self.webapp_settings
537 self.base_project_url, self.webapp_settings
534 )
538 )
535 if self.certfile:
539 if self.certfile:
536 ssl_options = dict(certfile=self.certfile)
540 ssl_options = dict(certfile=self.certfile)
537 if self.keyfile:
541 if self.keyfile:
538 ssl_options['keyfile'] = self.keyfile
542 ssl_options['keyfile'] = self.keyfile
539 else:
543 else:
540 ssl_options = None
544 ssl_options = None
541 self.web_app.password = self.password
545 self.web_app.password = self.password
542 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
546 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
543 xheaders=self.trust_xheaders)
547 xheaders=self.trust_xheaders)
544 if not self.ip:
548 if not self.ip:
545 warning = "WARNING: The notebook server is listening on all IP addresses"
549 warning = "WARNING: The notebook server is listening on all IP addresses"
546 if ssl_options is None:
550 if ssl_options is None:
547 self.log.critical(warning + " and not using encryption. This "
551 self.log.critical(warning + " and not using encryption. This "
548 "is not recommended.")
552 "is not recommended.")
549 if not self.password and not self.read_only:
553 if not self.password and not self.read_only:
550 self.log.critical(warning + " and not using authentication. "
554 self.log.critical(warning + " and not using authentication. "
551 "This is highly insecure and not recommended.")
555 "This is highly insecure and not recommended.")
552 success = None
556 success = None
553 for port in random_ports(self.port, self.port_retries+1):
557 for port in random_ports(self.port, self.port_retries+1):
554 try:
558 try:
555 self.http_server.listen(port, self.ip)
559 self.http_server.listen(port, self.ip)
556 except socket.error as e:
560 except socket.error as e:
557 # XXX: remove the e.errno == -9 block when we require
561 # XXX: remove the e.errno == -9 block when we require
558 # tornado >= 3.0
562 # tornado >= 3.0
559 if e.errno == -9 and tornado.version_info[0] < 3:
563 if e.errno == -9 and tornado.version_info[0] < 3:
560 # The flags passed to socket.getaddrinfo from
564 # The flags passed to socket.getaddrinfo from
561 # tornado.netutils.bind_sockets can cause "gaierror:
565 # tornado.netutils.bind_sockets can cause "gaierror:
562 # [Errno -9] Address family for hostname not supported"
566 # [Errno -9] Address family for hostname not supported"
563 # when the interface is not associated, for example.
567 # when the interface is not associated, for example.
564 # Changing the flags to exclude socket.AI_ADDRCONFIG does
568 # Changing the flags to exclude socket.AI_ADDRCONFIG does
565 # not cause this error, but the only way to do this is to
569 # not cause this error, but the only way to do this is to
566 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
570 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
567 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
571 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
568 self.log.warn('Monkeypatching socket to fix tornado bug')
572 self.log.warn('Monkeypatching socket to fix tornado bug')
569 del(socket.AI_ADDRCONFIG)
573 del(socket.AI_ADDRCONFIG)
570 try:
574 try:
571 # retry the tornado call without AI_ADDRCONFIG flags
575 # retry the tornado call without AI_ADDRCONFIG flags
572 self.http_server.listen(port, self.ip)
576 self.http_server.listen(port, self.ip)
573 except socket.error as e2:
577 except socket.error as e2:
574 e = e2
578 e = e2
575 else:
579 else:
576 self.port = port
580 self.port = port
577 success = True
581 success = True
578 break
582 break
579 # restore the monekypatch
583 # restore the monekypatch
580 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
584 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
581 if e.errno != errno.EADDRINUSE:
585 if e.errno != errno.EADDRINUSE:
582 raise
586 raise
583 self.log.info('The port %i is already in use, trying another random port.' % port)
587 self.log.info('The port %i is already in use, trying another random port.' % port)
584 else:
588 else:
585 self.port = port
589 self.port = port
586 success = True
590 success = True
587 break
591 break
588 if not success:
592 if not success:
589 self.log.critical('ERROR: the notebook server could not be started because '
593 self.log.critical('ERROR: the notebook server could not be started because '
590 'no available port could be found.')
594 'no available port could be found.')
591 self.exit(1)
595 self.exit(1)
592
596
593 def init_signal(self):
597 def init_signal(self):
594 if not sys.platform.startswith('win'):
598 if not sys.platform.startswith('win'):
595 signal.signal(signal.SIGINT, self._handle_sigint)
599 signal.signal(signal.SIGINT, self._handle_sigint)
596 signal.signal(signal.SIGTERM, self._signal_stop)
600 signal.signal(signal.SIGTERM, self._signal_stop)
597 if hasattr(signal, 'SIGUSR1'):
601 if hasattr(signal, 'SIGUSR1'):
598 # Windows doesn't support SIGUSR1
602 # Windows doesn't support SIGUSR1
599 signal.signal(signal.SIGUSR1, self._signal_info)
603 signal.signal(signal.SIGUSR1, self._signal_info)
600 if hasattr(signal, 'SIGINFO'):
604 if hasattr(signal, 'SIGINFO'):
601 # only on BSD-based systems
605 # only on BSD-based systems
602 signal.signal(signal.SIGINFO, self._signal_info)
606 signal.signal(signal.SIGINFO, self._signal_info)
603
607
604 def _handle_sigint(self, sig, frame):
608 def _handle_sigint(self, sig, frame):
605 """SIGINT handler spawns confirmation dialog"""
609 """SIGINT handler spawns confirmation dialog"""
606 # register more forceful signal handler for ^C^C case
610 # register more forceful signal handler for ^C^C case
607 signal.signal(signal.SIGINT, self._signal_stop)
611 signal.signal(signal.SIGINT, self._signal_stop)
608 # request confirmation dialog in bg thread, to avoid
612 # request confirmation dialog in bg thread, to avoid
609 # blocking the App
613 # blocking the App
610 thread = threading.Thread(target=self._confirm_exit)
614 thread = threading.Thread(target=self._confirm_exit)
611 thread.daemon = True
615 thread.daemon = True
612 thread.start()
616 thread.start()
613
617
614 def _restore_sigint_handler(self):
618 def _restore_sigint_handler(self):
615 """callback for restoring original SIGINT handler"""
619 """callback for restoring original SIGINT handler"""
616 signal.signal(signal.SIGINT, self._handle_sigint)
620 signal.signal(signal.SIGINT, self._handle_sigint)
617
621
618 def _confirm_exit(self):
622 def _confirm_exit(self):
619 """confirm shutdown on ^C
623 """confirm shutdown on ^C
620
624
621 A second ^C, or answering 'y' within 5s will cause shutdown,
625 A second ^C, or answering 'y' within 5s will cause shutdown,
622 otherwise original SIGINT handler will be restored.
626 otherwise original SIGINT handler will be restored.
623
627
624 This doesn't work on Windows.
628 This doesn't work on Windows.
625 """
629 """
626 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
630 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
627 time.sleep(0.1)
631 time.sleep(0.1)
628 info = self.log.info
632 info = self.log.info
629 info('interrupted')
633 info('interrupted')
630 print self.notebook_info()
634 print self.notebook_info()
631 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
635 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
632 sys.stdout.flush()
636 sys.stdout.flush()
633 r,w,x = select.select([sys.stdin], [], [], 5)
637 r,w,x = select.select([sys.stdin], [], [], 5)
634 if r:
638 if r:
635 line = sys.stdin.readline()
639 line = sys.stdin.readline()
636 if line.lower().startswith('y'):
640 if line.lower().startswith('y'):
637 self.log.critical("Shutdown confirmed")
641 self.log.critical("Shutdown confirmed")
638 ioloop.IOLoop.instance().stop()
642 ioloop.IOLoop.instance().stop()
639 return
643 return
640 else:
644 else:
641 print "No answer for 5s:",
645 print "No answer for 5s:",
642 print "resuming operation..."
646 print "resuming operation..."
643 # no answer, or answer is no:
647 # no answer, or answer is no:
644 # set it back to original SIGINT handler
648 # set it back to original SIGINT handler
645 # use IOLoop.add_callback because signal.signal must be called
649 # use IOLoop.add_callback because signal.signal must be called
646 # from main thread
650 # from main thread
647 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
651 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
648
652
649 def _signal_stop(self, sig, frame):
653 def _signal_stop(self, sig, frame):
650 self.log.critical("received signal %s, stopping", sig)
654 self.log.critical("received signal %s, stopping", sig)
651 ioloop.IOLoop.instance().stop()
655 ioloop.IOLoop.instance().stop()
652
656
653 def _signal_info(self, sig, frame):
657 def _signal_info(self, sig, frame):
654 print self.notebook_info()
658 print self.notebook_info()
655
659
656 def init_components(self):
660 def init_components(self):
657 """Check the components submodule, and warn if it's unclean"""
661 """Check the components submodule, and warn if it's unclean"""
658 status = submodule.check_submodule_status()
662 status = submodule.check_submodule_status()
659 if status == 'missing':
663 if status == 'missing':
660 self.log.warn("components submodule missing, running `git submodule update`")
664 self.log.warn("components submodule missing, running `git submodule update`")
661 submodule.update_submodules(submodule.ipython_parent())
665 submodule.update_submodules(submodule.ipython_parent())
662 elif status == 'unclean':
666 elif status == 'unclean':
663 self.log.warn("components submodule unclean, you may see 404s on static/components")
667 self.log.warn("components submodule unclean, you may see 404s on static/components")
664 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
668 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
665
669
666
670
667 @catch_config_error
671 @catch_config_error
668 def initialize(self, argv=None):
672 def initialize(self, argv=None):
669 self.init_logging()
673 self.init_logging()
670 super(NotebookApp, self).initialize(argv)
674 super(NotebookApp, self).initialize(argv)
671 self.init_configurables()
675 self.init_configurables()
672 self.init_components()
676 self.init_components()
673 self.init_webapp()
677 self.init_webapp()
674 self.init_signal()
678 self.init_signal()
675
679
676 def cleanup_kernels(self):
680 def cleanup_kernels(self):
677 """Shutdown all kernels.
681 """Shutdown all kernels.
678
682
679 The kernels will shutdown themselves when this process no longer exists,
683 The kernels will shutdown themselves when this process no longer exists,
680 but explicit shutdown allows the KernelManagers to cleanup the connection files.
684 but explicit shutdown allows the KernelManagers to cleanup the connection files.
681 """
685 """
682 self.log.info('Shutting down kernels')
686 self.log.info('Shutting down kernels')
683 self.kernel_manager.shutdown_all()
687 self.kernel_manager.shutdown_all()
684
688
685 def notebook_info(self):
689 def notebook_info(self):
686 "Return the current working directory and the server url information"
690 "Return the current working directory and the server url information"
687 mgr_info = self.notebook_manager.info_string() + "\n"
691 mgr_info = self.notebook_manager.info_string() + "\n"
688 return mgr_info +"The IPython Notebook is running at: %s" % self._url
692 return mgr_info +"The IPython Notebook is running at: %s" % self._url
689
693
690 def start(self):
694 def start(self):
691 """ Start the IPython Notebook server app, after initialization
695 """ Start the IPython Notebook server app, after initialization
692
696
693 This method takes no arguments so all configuration and initialization
697 This method takes no arguments so all configuration and initialization
694 must be done prior to calling this method."""
698 must be done prior to calling this method."""
695 ip = self.ip if self.ip else '[all ip addresses on your system]'
699 ip = self.ip if self.ip else '[all ip addresses on your system]'
696 proto = 'https' if self.certfile else 'http'
700 proto = 'https' if self.certfile else 'http'
697 info = self.log.info
701 info = self.log.info
698 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
702 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
699 self.base_project_url)
703 self.base_project_url)
700 for line in self.notebook_info().split("\n"):
704 for line in self.notebook_info().split("\n"):
701 info(line)
705 info(line)
702 info("Use Control-C to stop this server and shut down all kernels.")
706 info("Use Control-C to stop this server and shut down all kernels.")
703
707
704 if self.open_browser or self.file_to_run:
708 if self.open_browser or self.file_to_run:
705 ip = self.ip or LOCALHOST
709 ip = self.ip or LOCALHOST
706 try:
710 try:
707 browser = webbrowser.get(self.browser or None)
711 browser = webbrowser.get(self.browser or None)
708 except webbrowser.Error as e:
712 except webbrowser.Error as e:
709 self.log.warn('No web browser found: %s.' % e)
713 self.log.warn('No web browser found: %s.' % e)
710 browser = None
714 browser = None
711
715
712 if self.file_to_run:
716 if self.file_to_run:
713 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
717 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
714 url = self.notebook_manager.rev_mapping.get(name, '')
718 url = self.notebook_manager.rev_mapping.get(name, '')
715 else:
719 else:
716 url = ''
720 url = ''
717 if browser:
721 if browser:
718 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
722 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
719 self.port, self.base_project_url, url), new=2)
723 self.port, self.base_project_url, url), new=2)
720 threading.Thread(target=b).start()
724 threading.Thread(target=b).start()
721 try:
725 try:
722 ioloop.IOLoop.instance().start()
726 ioloop.IOLoop.instance().start()
723 except KeyboardInterrupt:
727 except KeyboardInterrupt:
724 info("Interrupted...")
728 info("Interrupted...")
725 finally:
729 finally:
726 self.cleanup_kernels()
730 self.cleanup_kernels()
727
731
728
732
729 #-----------------------------------------------------------------------------
733 #-----------------------------------------------------------------------------
730 # Main entry point
734 # Main entry point
731 #-----------------------------------------------------------------------------
735 #-----------------------------------------------------------------------------
732
736
733 def launch_new_instance():
737 def launch_new_instance():
734 app = NotebookApp.instance()
738 app = NotebookApp.instance()
735 app.initialize()
739 app.initialize()
736 app.start()
740 app.start()
737
741
@@ -1,484 +1,488 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Kernel
9 // Kernel
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule Kernel
15 * @submodule Kernel
16 */
16 */
17
17
18 var IPython = (function (IPython) {
18 var IPython = (function (IPython) {
19
19
20 var utils = IPython.utils;
20 var utils = IPython.utils;
21
21
22 // Initialization and connection.
22 // Initialization and connection.
23 /**
23 /**
24 * A Kernel Class to communicate with the Python kernel
24 * A Kernel Class to communicate with the Python kernel
25 * @Class Kernel
25 * @Class Kernel
26 */
26 */
27 var Kernel = function (base_url) {
27 var Kernel = function (base_url) {
28 this.kernel_id = null;
28 this.kernel_id = null;
29 this.shell_channel = null;
29 this.shell_channel = null;
30 this.iopub_channel = null;
30 this.iopub_channel = null;
31 this.stdin_channel = null;
31 this.stdin_channel = null;
32 this.base_url = base_url;
32 this.base_url = base_url;
33 this.running = false;
33 this.running = false;
34 this.username = "username";
34 this.username = "username";
35 this.session_id = utils.uuid();
35 this.session_id = utils.uuid();
36 this._msg_callbacks = {};
36 this._msg_callbacks = {};
37
37
38 if (typeof(WebSocket) !== 'undefined') {
38 if (typeof(WebSocket) !== 'undefined') {
39 this.WebSocket = WebSocket;
39 this.WebSocket = WebSocket;
40 } else if (typeof(MozWebSocket) !== 'undefined') {
40 } else if (typeof(MozWebSocket) !== 'undefined') {
41 this.WebSocket = MozWebSocket;
41 this.WebSocket = MozWebSocket;
42 } else {
42 } else {
43 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
43 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
44 };
44 };
45 };
45 };
46
46
47
47
48 Kernel.prototype._get_msg = function (msg_type, content) {
48 Kernel.prototype._get_msg = function (msg_type, content) {
49 var msg = {
49 var msg = {
50 header : {
50 header : {
51 msg_id : utils.uuid(),
51 msg_id : utils.uuid(),
52 username : this.username,
52 username : this.username,
53 session : this.session_id,
53 session : this.session_id,
54 msg_type : msg_type
54 msg_type : msg_type
55 },
55 },
56 metadata : {},
56 metadata : {},
57 content : content,
57 content : content,
58 parent_header : {}
58 parent_header : {}
59 };
59 };
60 return msg;
60 return msg;
61 };
61 };
62
62
63 /**
63 /**
64 * Start the Python kernel
64 * Start the Python kernel
65 * @method start
65 * @method start
66 */
66 */
67 Kernel.prototype.start = function (notebook_id) {
67 Kernel.prototype.start = function (notebook_id) {
68 var that = this;
68 var that = this;
69 if (!this.running) {
69 if (!this.running) {
70 var qs = $.param({notebook:notebook_id});
70 var qs = $.param({notebook:notebook_id});
71 var url = this.base_url + '?' + qs;
71 var url = this.base_url + '?' + qs;
72 $.post(url,
72 $.post(url,
73 $.proxy(that._kernel_started,that),
73 $.proxy(that._kernel_started,that),
74 'json'
74 'json'
75 );
75 );
76 };
76 };
77 };
77 };
78
78
79 /**
79 /**
80 * Restart the python kernel.
80 * Restart the python kernel.
81 *
81 *
82 * Emit a 'status_restarting.Kernel' event with
82 * Emit a 'status_restarting.Kernel' event with
83 * the current object as parameter
83 * the current object as parameter
84 *
84 *
85 * @method restart
85 * @method restart
86 */
86 */
87 Kernel.prototype.restart = function () {
87 Kernel.prototype.restart = function () {
88 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
88 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
89 var that = this;
89 var that = this;
90 if (this.running) {
90 if (this.running) {
91 this.stop_channels();
91 this.stop_channels();
92 var url = this.kernel_url + "/restart";
92 var url = this.kernel_url + "/restart";
93 $.post(url,
93 $.post(url,
94 $.proxy(that._kernel_started, that),
94 $.proxy(that._kernel_started, that),
95 'json'
95 'json'
96 );
96 );
97 };
97 };
98 };
98 };
99
99
100
100
101 Kernel.prototype._kernel_started = function (json) {
101 Kernel.prototype._kernel_started = function (json) {
102 console.log("Kernel started: ", json.kernel_id);
102 console.log("Kernel started: ", json.kernel_id);
103 this.running = true;
103 this.running = true;
104 this.kernel_id = json.kernel_id;
104 this.kernel_id = json.kernel_id;
105 this.ws_url = json.ws_url;
105 var ws_url = json.ws_url;
106 if (ws_url.match(/wss?:\/\//) == null) {
107 ws_url = "ws" + location.origin.substr(4) + ws_url;
108 };
109 this.ws_url = ws_url;
106 this.kernel_url = this.base_url + "/" + this.kernel_id;
110 this.kernel_url = this.base_url + "/" + this.kernel_id;
107 this.start_channels();
111 this.start_channels();
108 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
112 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
109 };
113 };
110
114
111
115
112 Kernel.prototype._websocket_closed = function(ws_url, early) {
116 Kernel.prototype._websocket_closed = function(ws_url, early) {
113 this.stop_channels();
117 this.stop_channels();
114 $([IPython.events]).trigger('websocket_closed.Kernel',
118 $([IPython.events]).trigger('websocket_closed.Kernel',
115 {ws_url: ws_url, kernel: this, early: early}
119 {ws_url: ws_url, kernel: this, early: early}
116 );
120 );
117 };
121 };
118
122
119 /**
123 /**
120 * Start the `shell`and `iopub` channels.
124 * Start the `shell`and `iopub` channels.
121 * Will stop and restart them if they already exist.
125 * Will stop and restart them if they already exist.
122 *
126 *
123 * @method start_channels
127 * @method start_channels
124 */
128 */
125 Kernel.prototype.start_channels = function () {
129 Kernel.prototype.start_channels = function () {
126 var that = this;
130 var that = this;
127 this.stop_channels();
131 this.stop_channels();
128 var ws_url = this.ws_url + this.kernel_url;
132 var ws_url = this.ws_url + this.kernel_url;
129 console.log("Starting WebSockets:", ws_url);
133 console.log("Starting WebSockets:", ws_url);
130 this.shell_channel = new this.WebSocket(ws_url + "/shell");
134 this.shell_channel = new this.WebSocket(ws_url + "/shell");
131 this.stdin_channel = new this.WebSocket(ws_url + "/stdin");
135 this.stdin_channel = new this.WebSocket(ws_url + "/stdin");
132 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
136 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
133 send_cookie = function(){
137 send_cookie = function(){
134 // send the session id so the Session object Python-side
138 // send the session id so the Session object Python-side
135 // has the same identity
139 // has the same identity
136 this.send(that.session_id + ':' + document.cookie);
140 this.send(that.session_id + ':' + document.cookie);
137 };
141 };
138 var already_called_onclose = false; // only alert once
142 var already_called_onclose = false; // only alert once
139 var ws_closed_early = function(evt){
143 var ws_closed_early = function(evt){
140 if (already_called_onclose){
144 if (already_called_onclose){
141 return;
145 return;
142 }
146 }
143 already_called_onclose = true;
147 already_called_onclose = true;
144 if ( ! evt.wasClean ){
148 if ( ! evt.wasClean ){
145 that._websocket_closed(ws_url, true);
149 that._websocket_closed(ws_url, true);
146 }
150 }
147 };
151 };
148 var ws_closed_late = function(evt){
152 var ws_closed_late = function(evt){
149 if (already_called_onclose){
153 if (already_called_onclose){
150 return;
154 return;
151 }
155 }
152 already_called_onclose = true;
156 already_called_onclose = true;
153 if ( ! evt.wasClean ){
157 if ( ! evt.wasClean ){
154 that._websocket_closed(ws_url, false);
158 that._websocket_closed(ws_url, false);
155 }
159 }
156 };
160 };
157 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
161 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
158 for (var i=0; i < channels.length; i++) {
162 for (var i=0; i < channels.length; i++) {
159 channels[i].onopen = send_cookie;
163 channels[i].onopen = send_cookie;
160 channels[i].onclose = ws_closed_early;
164 channels[i].onclose = ws_closed_early;
161 }
165 }
162 // switch from early-close to late-close message after 1s
166 // switch from early-close to late-close message after 1s
163 setTimeout(function() {
167 setTimeout(function() {
164 for (var i=0; i < channels.length; i++) {
168 for (var i=0; i < channels.length; i++) {
165 if (channels[i] !== null) {
169 if (channels[i] !== null) {
166 channels[i].onclose = ws_closed_late;
170 channels[i].onclose = ws_closed_late;
167 }
171 }
168 }
172 }
169 }, 1000);
173 }, 1000);
170 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
174 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
171 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_reply, this);
175 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_reply, this);
172 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
176 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
173
177
174 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
178 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
175 that.send_input_reply(data);
179 that.send_input_reply(data);
176 });
180 });
177 };
181 };
178
182
179 /**
183 /**
180 * Start the `shell`and `iopub` channels.
184 * Start the `shell`and `iopub` channels.
181 * @method stop_channels
185 * @method stop_channels
182 */
186 */
183 Kernel.prototype.stop_channels = function () {
187 Kernel.prototype.stop_channels = function () {
184 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
188 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
185 for (var i=0; i < channels.length; i++) {
189 for (var i=0; i < channels.length; i++) {
186 if ( channels[i] !== null ) {
190 if ( channels[i] !== null ) {
187 channels[i].onclose = function (evt) {};
191 channels[i].onclose = function (evt) {};
188 channels[i].close();
192 channels[i].close();
189 }
193 }
190 };
194 };
191 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
195 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
192 };
196 };
193
197
194 // Main public methods.
198 // Main public methods.
195
199
196 /**
200 /**
197 * Get info on object asynchronoulsy
201 * Get info on object asynchronoulsy
198 *
202 *
199 * @async
203 * @async
200 * @param objname {string}
204 * @param objname {string}
201 * @param callback {dict}
205 * @param callback {dict}
202 * @method object_info_request
206 * @method object_info_request
203 *
207 *
204 * @example
208 * @example
205 *
209 *
206 * When calling this method pass a callbacks structure of the form:
210 * When calling this method pass a callbacks structure of the form:
207 *
211 *
208 * callbacks = {
212 * callbacks = {
209 * 'object_info_reply': object_info_reply_callback
213 * 'object_info_reply': object_info_reply_callback
210 * }
214 * }
211 *
215 *
212 * The `object_info_reply_callback` will be passed the content object of the
216 * The `object_info_reply_callback` will be passed the content object of the
213 *
217 *
214 * `object_into_reply` message documented in
218 * `object_into_reply` message documented in
215 * [IPython dev documentation](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
219 * [IPython dev documentation](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
216 */
220 */
217 Kernel.prototype.object_info_request = function (objname, callbacks) {
221 Kernel.prototype.object_info_request = function (objname, callbacks) {
218 if(typeof(objname)!=null && objname!=null)
222 if(typeof(objname)!=null && objname!=null)
219 {
223 {
220 var content = {
224 var content = {
221 oname : objname.toString(),
225 oname : objname.toString(),
222 };
226 };
223 var msg = this._get_msg("object_info_request", content);
227 var msg = this._get_msg("object_info_request", content);
224 this.shell_channel.send(JSON.stringify(msg));
228 this.shell_channel.send(JSON.stringify(msg));
225 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
229 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
226 return msg.header.msg_id;
230 return msg.header.msg_id;
227 }
231 }
228 return;
232 return;
229 }
233 }
230
234
231 /**
235 /**
232 * Execute given code into kernel, and pass result to callback.
236 * Execute given code into kernel, and pass result to callback.
233 *
237 *
234 * TODO: document input_request in callbacks
238 * TODO: document input_request in callbacks
235 *
239 *
236 * @async
240 * @async
237 * @method execute
241 * @method execute
238 * @param {string} code
242 * @param {string} code
239 * @param [callbacks] {Object} With the optional following keys
243 * @param [callbacks] {Object} With the optional following keys
240 * @param callbacks.'execute_reply' {function}
244 * @param callbacks.'execute_reply' {function}
241 * @param callbacks.'output' {function}
245 * @param callbacks.'output' {function}
242 * @param callbacks.'clear_output' {function}
246 * @param callbacks.'clear_output' {function}
243 * @param callbacks.'set_next_input' {function}
247 * @param callbacks.'set_next_input' {function}
244 * @param {object} [options]
248 * @param {object} [options]
245 * @param [options.silent=false] {Boolean}
249 * @param [options.silent=false] {Boolean}
246 * @param [options.user_expressions=empty_dict] {Dict}
250 * @param [options.user_expressions=empty_dict] {Dict}
247 * @param [options.user_variables=empty_list] {List od Strings}
251 * @param [options.user_variables=empty_list] {List od Strings}
248 * @param [options.allow_stdin=false] {Boolean} true|false
252 * @param [options.allow_stdin=false] {Boolean} true|false
249 *
253 *
250 * @example
254 * @example
251 *
255 *
252 * The options object should contain the options for the execute call. Its default
256 * The options object should contain the options for the execute call. Its default
253 * values are:
257 * values are:
254 *
258 *
255 * options = {
259 * options = {
256 * silent : true,
260 * silent : true,
257 * user_variables : [],
261 * user_variables : [],
258 * user_expressions : {},
262 * user_expressions : {},
259 * allow_stdin : false
263 * allow_stdin : false
260 * }
264 * }
261 *
265 *
262 * When calling this method pass a callbacks structure of the form:
266 * When calling this method pass a callbacks structure of the form:
263 *
267 *
264 * callbacks = {
268 * callbacks = {
265 * 'execute_reply': execute_reply_callback,
269 * 'execute_reply': execute_reply_callback,
266 * 'output': output_callback,
270 * 'output': output_callback,
267 * 'clear_output': clear_output_callback,
271 * 'clear_output': clear_output_callback,
268 * 'set_next_input': set_next_input_callback
272 * 'set_next_input': set_next_input_callback
269 * }
273 * }
270 *
274 *
271 * The `execute_reply_callback` will be passed the content and metadata
275 * The `execute_reply_callback` will be passed the content and metadata
272 * objects of the `execute_reply` message documented
276 * objects of the `execute_reply` message documented
273 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#execute)
277 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#execute)
274 *
278 *
275 * The `output_callback` will be passed `msg_type` ('stream','display_data','pyout','pyerr')
279 * The `output_callback` will be passed `msg_type` ('stream','display_data','pyout','pyerr')
276 * of the output and the content and metadata objects of the PUB/SUB channel that contains the
280 * of the output and the content and metadata objects of the PUB/SUB channel that contains the
277 * output:
281 * output:
278 *
282 *
279 * http://ipython.org/ipython-doc/dev/development/messaging.html#messages-on-the-pub-sub-socket
283 * http://ipython.org/ipython-doc/dev/development/messaging.html#messages-on-the-pub-sub-socket
280 *
284 *
281 * The `clear_output_callback` will be passed a content object that contains
285 * The `clear_output_callback` will be passed a content object that contains
282 * stdout, stderr and other fields that are booleans, as well as the metadata object.
286 * stdout, stderr and other fields that are booleans, as well as the metadata object.
283 *
287 *
284 * The `set_next_input_callback` will be passed the text that should become the next
288 * The `set_next_input_callback` will be passed the text that should become the next
285 * input cell.
289 * input cell.
286 */
290 */
287 Kernel.prototype.execute = function (code, callbacks, options) {
291 Kernel.prototype.execute = function (code, callbacks, options) {
288
292
289 var content = {
293 var content = {
290 code : code,
294 code : code,
291 silent : true,
295 silent : true,
292 user_variables : [],
296 user_variables : [],
293 user_expressions : {},
297 user_expressions : {},
294 allow_stdin : false
298 allow_stdin : false
295 };
299 };
296 callbacks = callbacks || {};
300 callbacks = callbacks || {};
297 if (callbacks.input_request !== undefined) {
301 if (callbacks.input_request !== undefined) {
298 content.allow_stdin = true;
302 content.allow_stdin = true;
299 }
303 }
300 $.extend(true, content, options)
304 $.extend(true, content, options)
301 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
305 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
302 var msg = this._get_msg("execute_request", content);
306 var msg = this._get_msg("execute_request", content);
303 this.shell_channel.send(JSON.stringify(msg));
307 this.shell_channel.send(JSON.stringify(msg));
304 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
308 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
305 return msg.header.msg_id;
309 return msg.header.msg_id;
306 };
310 };
307
311
308 /**
312 /**
309 * When calling this method pass a callbacks structure of the form:
313 * When calling this method pass a callbacks structure of the form:
310 *
314 *
311 * callbacks = {
315 * callbacks = {
312 * 'complete_reply': complete_reply_callback
316 * 'complete_reply': complete_reply_callback
313 * }
317 * }
314 *
318 *
315 * The `complete_reply_callback` will be passed the content object of the
319 * The `complete_reply_callback` will be passed the content object of the
316 * `complete_reply` message documented
320 * `complete_reply` message documented
317 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
321 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
318 *
322 *
319 * @method complete
323 * @method complete
320 * @param line {integer}
324 * @param line {integer}
321 * @param cursor_pos {integer}
325 * @param cursor_pos {integer}
322 * @param {dict} callbacks
326 * @param {dict} callbacks
323 * @param callbacks.complete_reply {function} `complete_reply_callback`
327 * @param callbacks.complete_reply {function} `complete_reply_callback`
324 *
328 *
325 */
329 */
326 Kernel.prototype.complete = function (line, cursor_pos, callbacks) {
330 Kernel.prototype.complete = function (line, cursor_pos, callbacks) {
327 callbacks = callbacks || {};
331 callbacks = callbacks || {};
328 var content = {
332 var content = {
329 text : '',
333 text : '',
330 line : line,
334 line : line,
331 cursor_pos : cursor_pos
335 cursor_pos : cursor_pos
332 };
336 };
333 var msg = this._get_msg("complete_request", content);
337 var msg = this._get_msg("complete_request", content);
334 this.shell_channel.send(JSON.stringify(msg));
338 this.shell_channel.send(JSON.stringify(msg));
335 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
339 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
336 return msg.header.msg_id;
340 return msg.header.msg_id;
337 };
341 };
338
342
339
343
340 Kernel.prototype.interrupt = function () {
344 Kernel.prototype.interrupt = function () {
341 if (this.running) {
345 if (this.running) {
342 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
346 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
343 $.post(this.kernel_url + "/interrupt");
347 $.post(this.kernel_url + "/interrupt");
344 };
348 };
345 };
349 };
346
350
347
351
348 Kernel.prototype.kill = function () {
352 Kernel.prototype.kill = function () {
349 if (this.running) {
353 if (this.running) {
350 this.running = false;
354 this.running = false;
351 var settings = {
355 var settings = {
352 cache : false,
356 cache : false,
353 type : "DELETE"
357 type : "DELETE"
354 };
358 };
355 $.ajax(this.kernel_url, settings);
359 $.ajax(this.kernel_url, settings);
356 };
360 };
357 };
361 };
358
362
359 Kernel.prototype.send_input_reply = function (input) {
363 Kernel.prototype.send_input_reply = function (input) {
360 var content = {
364 var content = {
361 value : input,
365 value : input,
362 };
366 };
363 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
367 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
364 var msg = this._get_msg("input_reply", content);
368 var msg = this._get_msg("input_reply", content);
365 this.stdin_channel.send(JSON.stringify(msg));
369 this.stdin_channel.send(JSON.stringify(msg));
366 return msg.header.msg_id;
370 return msg.header.msg_id;
367 };
371 };
368
372
369
373
370 // Reply handlers
374 // Reply handlers
371
375
372 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
376 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
373 var callbacks = this._msg_callbacks[msg_id];
377 var callbacks = this._msg_callbacks[msg_id];
374 return callbacks;
378 return callbacks;
375 };
379 };
376
380
377
381
378 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
382 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
379 this._msg_callbacks[msg_id] = callbacks || {};
383 this._msg_callbacks[msg_id] = callbacks || {};
380 }
384 }
381
385
382
386
383 Kernel.prototype._handle_shell_reply = function (e) {
387 Kernel.prototype._handle_shell_reply = function (e) {
384 var reply = $.parseJSON(e.data);
388 var reply = $.parseJSON(e.data);
385 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
389 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
386 var header = reply.header;
390 var header = reply.header;
387 var content = reply.content;
391 var content = reply.content;
388 var metadata = reply.metadata;
392 var metadata = reply.metadata;
389 var msg_type = header.msg_type;
393 var msg_type = header.msg_type;
390 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
394 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
391 if (callbacks !== undefined) {
395 if (callbacks !== undefined) {
392 var cb = callbacks[msg_type];
396 var cb = callbacks[msg_type];
393 if (cb !== undefined) {
397 if (cb !== undefined) {
394 cb(content, metadata);
398 cb(content, metadata);
395 }
399 }
396 };
400 };
397
401
398 if (content.payload !== undefined) {
402 if (content.payload !== undefined) {
399 var payload = content.payload || [];
403 var payload = content.payload || [];
400 this._handle_payload(callbacks, payload);
404 this._handle_payload(callbacks, payload);
401 }
405 }
402 };
406 };
403
407
404
408
405 Kernel.prototype._handle_payload = function (callbacks, payload) {
409 Kernel.prototype._handle_payload = function (callbacks, payload) {
406 var l = payload.length;
410 var l = payload.length;
407 // Payloads are handled by triggering events because we don't want the Kernel
411 // Payloads are handled by triggering events because we don't want the Kernel
408 // to depend on the Notebook or Pager classes.
412 // to depend on the Notebook or Pager classes.
409 for (var i=0; i<l; i++) {
413 for (var i=0; i<l; i++) {
410 if (payload[i].source === 'IPython.kernel.zmq.page.page') {
414 if (payload[i].source === 'IPython.kernel.zmq.page.page') {
411 var data = {'text':payload[i].text}
415 var data = {'text':payload[i].text}
412 $([IPython.events]).trigger('open_with_text.Pager', data);
416 $([IPython.events]).trigger('open_with_text.Pager', data);
413 } else if (payload[i].source === 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell.set_next_input') {
417 } else if (payload[i].source === 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell.set_next_input') {
414 if (callbacks.set_next_input !== undefined) {
418 if (callbacks.set_next_input !== undefined) {
415 callbacks.set_next_input(payload[i].text)
419 callbacks.set_next_input(payload[i].text)
416 }
420 }
417 }
421 }
418 };
422 };
419 };
423 };
420
424
421
425
422 Kernel.prototype._handle_iopub_reply = function (e) {
426 Kernel.prototype._handle_iopub_reply = function (e) {
423 var reply = $.parseJSON(e.data);
427 var reply = $.parseJSON(e.data);
424 var content = reply.content;
428 var content = reply.content;
425 var msg_type = reply.header.msg_type;
429 var msg_type = reply.header.msg_type;
426 var metadata = reply.metadata;
430 var metadata = reply.metadata;
427 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
431 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
428 if (msg_type !== 'status' && callbacks === undefined) {
432 if (msg_type !== 'status' && callbacks === undefined) {
429 // Message not from one of this notebook's cells and there are no
433 // Message not from one of this notebook's cells and there are no
430 // callbacks to handle it.
434 // callbacks to handle it.
431 return;
435 return;
432 }
436 }
433 var output_types = ['stream','display_data','pyout','pyerr'];
437 var output_types = ['stream','display_data','pyout','pyerr'];
434 if (output_types.indexOf(msg_type) >= 0) {
438 if (output_types.indexOf(msg_type) >= 0) {
435 var cb = callbacks['output'];
439 var cb = callbacks['output'];
436 if (cb !== undefined) {
440 if (cb !== undefined) {
437 cb(msg_type, content, metadata);
441 cb(msg_type, content, metadata);
438 }
442 }
439 } else if (msg_type === 'status') {
443 } else if (msg_type === 'status') {
440 if (content.execution_state === 'busy') {
444 if (content.execution_state === 'busy') {
441 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
445 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
442 } else if (content.execution_state === 'idle') {
446 } else if (content.execution_state === 'idle') {
443 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
447 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
444 } else if (content.execution_state === 'restarting') {
448 } else if (content.execution_state === 'restarting') {
445 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
449 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
446 } else if (content.execution_state === 'dead') {
450 } else if (content.execution_state === 'dead') {
447 this.stop_channels();
451 this.stop_channels();
448 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
452 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
449 };
453 };
450 } else if (msg_type === 'clear_output') {
454 } else if (msg_type === 'clear_output') {
451 var cb = callbacks['clear_output'];
455 var cb = callbacks['clear_output'];
452 if (cb !== undefined) {
456 if (cb !== undefined) {
453 cb(content, metadata);
457 cb(content, metadata);
454 }
458 }
455 };
459 };
456 };
460 };
457
461
458
462
459 Kernel.prototype._handle_input_request = function (e) {
463 Kernel.prototype._handle_input_request = function (e) {
460 var request = $.parseJSON(e.data);
464 var request = $.parseJSON(e.data);
461 var header = request.header;
465 var header = request.header;
462 var content = request.content;
466 var content = request.content;
463 var metadata = request.metadata;
467 var metadata = request.metadata;
464 var msg_type = header.msg_type;
468 var msg_type = header.msg_type;
465 if (msg_type !== 'input_request') {
469 if (msg_type !== 'input_request') {
466 console.log("Invalid input request!", request);
470 console.log("Invalid input request!", request);
467 return;
471 return;
468 }
472 }
469 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
473 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
470 if (callbacks !== undefined) {
474 if (callbacks !== undefined) {
471 var cb = callbacks[msg_type];
475 var cb = callbacks[msg_type];
472 if (cb !== undefined) {
476 if (cb !== undefined) {
473 cb(content, metadata);
477 cb(content, metadata);
474 }
478 }
475 };
479 };
476 };
480 };
477
481
478
482
479 IPython.Kernel = Kernel;
483 IPython.Kernel = Kernel;
480
484
481 return IPython;
485 return IPython;
482
486
483 }(IPython));
487 }(IPython));
484
488
General Comments 0
You need to be logged in to leave comments. Login now