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