##// END OF EJS Templates
Backport PR #6061: make CORS configurable...
MinRK -
Show More
@@ -1,386 +1,428 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 functools
21 21 import json
22 22 import logging
23 23 import os
24 24 import re
25 25 import sys
26 26 import traceback
27 27 try:
28 28 # py3
29 29 from http.client import responses
30 30 except ImportError:
31 31 from httplib import responses
32 32
33 33 from jinja2 import TemplateNotFound
34 34 from tornado import web
35 35
36 36 try:
37 37 from tornado.log import app_log
38 38 except ImportError:
39 39 app_log = logging.getLogger()
40 40
41 41 from IPython.config import Application
42 42 from IPython.utils.path import filefind
43 43 from IPython.utils.py3compat import string_types
44 44 from IPython.html.utils import is_hidden
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # Top-level handlers
48 48 #-----------------------------------------------------------------------------
49 49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
50 50
51 51 class AuthenticatedHandler(web.RequestHandler):
52 52 """A RequestHandler with an authenticated user."""
53 53
54 54 def set_default_headers(self):
55 55 headers = self.settings.get('headers', {})
56 56 for header_name,value in headers.items() :
57 57 try:
58 58 self.set_header(header_name, value)
59 59 except Exception:
60 60 # tornado raise Exception (not a subclass)
61 61 # if method is unsupported (websocket and Access-Control-Allow-Origin
62 62 # for example, so just ignore)
63 63 pass
64 64
65 65 def clear_login_cookie(self):
66 66 self.clear_cookie(self.cookie_name)
67 67
68 68 def get_current_user(self):
69 69 user_id = self.get_secure_cookie(self.cookie_name)
70 70 # For now the user_id should not return empty, but it could eventually
71 71 if user_id == '':
72 72 user_id = 'anonymous'
73 73 if user_id is None:
74 74 # prevent extra Invalid cookie sig warnings:
75 75 self.clear_login_cookie()
76 76 if not self.login_available:
77 77 user_id = 'anonymous'
78 78 return user_id
79 79
80 80 @property
81 81 def cookie_name(self):
82 82 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
83 83 self.request.host
84 84 ))
85 85 return self.settings.get('cookie_name', default_cookie_name)
86 86
87 87 @property
88 88 def password(self):
89 89 """our password"""
90 90 return self.settings.get('password', '')
91 91
92 92 @property
93 93 def logged_in(self):
94 94 """Is a user currently logged in?
95 95
96 96 """
97 97 user = self.get_current_user()
98 98 return (user and not user == 'anonymous')
99 99
100 100 @property
101 101 def login_available(self):
102 102 """May a user proceed to log in?
103 103
104 104 This returns True if login capability is available, irrespective of
105 105 whether the user is already logged in or not.
106 106
107 107 """
108 108 return bool(self.settings.get('password', ''))
109 109
110 110
111 111 class IPythonHandler(AuthenticatedHandler):
112 112 """IPython-specific extensions to authenticated handling
113 113
114 114 Mostly property shortcuts to IPython-specific settings.
115 115 """
116 116
117 117 @property
118 118 def config(self):
119 119 return self.settings.get('config', None)
120 120
121 121 @property
122 122 def log(self):
123 123 """use the IPython log by default, falling back on tornado's logger"""
124 124 if Application.initialized():
125 125 return Application.instance().log
126 126 else:
127 127 return app_log
128 128
129 129 #---------------------------------------------------------------
130 130 # URLs
131 131 #---------------------------------------------------------------
132 132
133 133 @property
134 134 def mathjax_url(self):
135 135 return self.settings.get('mathjax_url', '')
136 136
137 137 @property
138 138 def base_url(self):
139 139 return self.settings.get('base_url', '/')
140 140
141 141 #---------------------------------------------------------------
142 142 # Manager objects
143 143 #---------------------------------------------------------------
144 144
145 145 @property
146 146 def kernel_manager(self):
147 147 return self.settings['kernel_manager']
148 148
149 149 @property
150 150 def notebook_manager(self):
151 151 return self.settings['notebook_manager']
152 152
153 153 @property
154 154 def cluster_manager(self):
155 155 return self.settings['cluster_manager']
156 156
157 157 @property
158 158 def session_manager(self):
159 159 return self.settings['session_manager']
160 160
161 161 @property
162 162 def project_dir(self):
163 163 return self.notebook_manager.notebook_dir
164 164
165 165 #---------------------------------------------------------------
166 # CORS
167 #---------------------------------------------------------------
168
169 @property
170 def allow_origin(self):
171 """Normal Access-Control-Allow-Origin"""
172 return self.settings.get('allow_origin', '')
173
174 @property
175 def allow_origin_pat(self):
176 """Regular expression version of allow_origin"""
177 return self.settings.get('allow_origin_pat', None)
178
179 @property
180 def allow_credentials(self):
181 """Whether to set Access-Control-Allow-Credentials"""
182 return self.settings.get('allow_credentials', False)
183
184 def set_default_headers(self):
185 """Add CORS headers, if defined"""
186 super(IPythonHandler, self).set_default_headers()
187 if self.allow_origin:
188 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
189 elif self.allow_origin_pat:
190 origin = self.get_origin()
191 if origin and self.allow_origin_pat.match(origin):
192 self.set_header("Access-Control-Allow-Origin", origin)
193 if self.allow_credentials:
194 self.set_header("Access-Control-Allow-Credentials", 'true')
195
196 def get_origin(self):
197 # Handle WebSocket Origin naming convention differences
198 # The difference between version 8 and 13 is that in 8 the
199 # client sends a "Sec-Websocket-Origin" header and in 13 it's
200 # simply "Origin".
201 if "Origin" in self.request.headers:
202 origin = self.request.headers.get("Origin")
203 else:
204 origin = self.request.headers.get("Sec-Websocket-Origin", None)
205 return origin
206
207 #---------------------------------------------------------------
166 208 # template rendering
167 209 #---------------------------------------------------------------
168 210
169 211 def get_template(self, name):
170 212 """Return the jinja template object for a given name"""
171 213 return self.settings['jinja2_env'].get_template(name)
172 214
173 215 def render_template(self, name, **ns):
174 216 ns.update(self.template_namespace)
175 217 template = self.get_template(name)
176 218 return template.render(**ns)
177 219
178 220 @property
179 221 def template_namespace(self):
180 222 return dict(
181 223 base_url=self.base_url,
182 224 logged_in=self.logged_in,
183 225 login_available=self.login_available,
184 226 static_url=self.static_url,
185 227 )
186 228
187 229 def get_json_body(self):
188 230 """Return the body of the request as JSON data."""
189 231 if not self.request.body:
190 232 return None
191 233 # Do we need to call body.decode('utf-8') here?
192 234 body = self.request.body.strip().decode(u'utf-8')
193 235 try:
194 236 model = json.loads(body)
195 237 except Exception:
196 238 self.log.debug("Bad JSON: %r", body)
197 239 self.log.error("Couldn't parse JSON", exc_info=True)
198 240 raise web.HTTPError(400, u'Invalid JSON in body of request')
199 241 return model
200 242
201 243 def get_error_html(self, status_code, **kwargs):
202 244 """render custom error pages"""
203 245 exception = kwargs.get('exception')
204 246 message = ''
205 247 status_message = responses.get(status_code, 'Unknown HTTP Error')
206 248 if exception:
207 249 # get the custom message, if defined
208 250 try:
209 251 message = exception.log_message % exception.args
210 252 except Exception:
211 253 pass
212 254
213 255 # construct the custom reason, if defined
214 256 reason = getattr(exception, 'reason', '')
215 257 if reason:
216 258 status_message = reason
217 259
218 260 # build template namespace
219 261 ns = dict(
220 262 status_code=status_code,
221 263 status_message=status_message,
222 264 message=message,
223 265 exception=exception,
224 266 )
225 267
226 268 # render the template
227 269 try:
228 270 html = self.render_template('%s.html' % status_code, **ns)
229 271 except TemplateNotFound:
230 272 self.log.debug("No template for %d", status_code)
231 273 html = self.render_template('error.html', **ns)
232 274 return html
233 275
234 276
235 277 class Template404(IPythonHandler):
236 278 """Render our 404 template"""
237 279 def prepare(self):
238 280 raise web.HTTPError(404)
239 281
240 282
241 283 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
242 284 """static files should only be accessible when logged in"""
243 285
244 286 @web.authenticated
245 287 def get(self, path):
246 288 if os.path.splitext(path)[1] == '.ipynb':
247 289 name = os.path.basename(path)
248 290 self.set_header('Content-Type', 'application/json')
249 291 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
250 292
251 293 return web.StaticFileHandler.get(self, path)
252 294
253 295 def compute_etag(self):
254 296 return None
255 297
256 298 def validate_absolute_path(self, root, absolute_path):
257 299 """Validate and return the absolute path.
258 300
259 301 Requires tornado 3.1
260 302
261 303 Adding to tornado's own handling, forbids the serving of hidden files.
262 304 """
263 305 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
264 306 abs_root = os.path.abspath(root)
265 307 if is_hidden(abs_path, abs_root):
266 308 self.log.info("Refusing to serve hidden file, via 404 Error")
267 309 raise web.HTTPError(404)
268 310 return abs_path
269 311
270 312
271 313 def json_errors(method):
272 314 """Decorate methods with this to return GitHub style JSON errors.
273 315
274 316 This should be used on any JSON API on any handler method that can raise HTTPErrors.
275 317
276 318 This will grab the latest HTTPError exception using sys.exc_info
277 319 and then:
278 320
279 321 1. Set the HTTP status code based on the HTTPError
280 322 2. Create and return a JSON body with a message field describing
281 323 the error in a human readable form.
282 324 """
283 325 @functools.wraps(method)
284 326 def wrapper(self, *args, **kwargs):
285 327 try:
286 328 result = method(self, *args, **kwargs)
287 329 except web.HTTPError as e:
288 330 status = e.status_code
289 331 message = e.log_message
290 332 self.log.warn(message)
291 333 self.set_status(e.status_code)
292 334 self.finish(json.dumps(dict(message=message)))
293 335 except Exception:
294 336 self.log.error("Unhandled error in API request", exc_info=True)
295 337 status = 500
296 338 message = "Unknown server error"
297 339 t, value, tb = sys.exc_info()
298 340 self.set_status(status)
299 341 tb_text = ''.join(traceback.format_exception(t, value, tb))
300 342 reply = dict(message=message, traceback=tb_text)
301 343 self.finish(json.dumps(reply))
302 344 else:
303 345 return result
304 346 return wrapper
305 347
306 348
307 349
308 350 #-----------------------------------------------------------------------------
309 351 # File handler
310 352 #-----------------------------------------------------------------------------
311 353
312 354 # to minimize subclass changes:
313 355 HTTPError = web.HTTPError
314 356
315 357 class FileFindHandler(web.StaticFileHandler):
316 358 """subclass of StaticFileHandler for serving files from a search path"""
317 359
318 360 # cache search results, don't search for files more than once
319 361 _static_paths = {}
320 362
321 363 def initialize(self, path, default_filename=None):
322 364 if isinstance(path, string_types):
323 365 path = [path]
324 366
325 367 self.root = tuple(
326 368 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
327 369 )
328 370 self.default_filename = default_filename
329 371
330 372 def compute_etag(self):
331 373 return None
332 374
333 375 @classmethod
334 376 def get_absolute_path(cls, roots, path):
335 377 """locate a file to serve on our static file search path"""
336 378 with cls._lock:
337 379 if path in cls._static_paths:
338 380 return cls._static_paths[path]
339 381 try:
340 382 abspath = os.path.abspath(filefind(path, roots))
341 383 except IOError:
342 384 # IOError means not found
343 385 return ''
344 386
345 387 cls._static_paths[path] = abspath
346 388 return abspath
347 389
348 390 def validate_absolute_path(self, root, absolute_path):
349 391 """check if the file should be served (raises 404, 403, etc.)"""
350 392 if absolute_path == '':
351 393 raise web.HTTPError(404)
352 394
353 395 for root in self.root:
354 396 if (absolute_path + os.sep).startswith(root):
355 397 break
356 398
357 399 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
358 400
359 401
360 402 class TrailingSlashHandler(web.RequestHandler):
361 403 """Simple redirect handler that strips trailing slashes
362 404
363 405 This should be the first, highest priority handler.
364 406 """
365 407
366 408 SUPPORTED_METHODS = ['GET']
367 409
368 410 def get(self):
369 411 self.redirect(self.request.uri.rstrip('/'))
370 412
371 413 #-----------------------------------------------------------------------------
372 414 # URL pattern fragments for re-use
373 415 #-----------------------------------------------------------------------------
374 416
375 417 path_regex = r"(?P<path>(?:/.*)*)"
376 418 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
377 419 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
378 420
379 421 #-----------------------------------------------------------------------------
380 422 # URL to handler mappings
381 423 #-----------------------------------------------------------------------------
382 424
383 425
384 426 default_handlers = [
385 427 (r".*/", TrailingSlashHandler)
386 428 ]
@@ -1,150 +1,167 b''
1 1 """Tornado handlers for WebSocket <-> ZMQ sockets.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-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 try:
20 20 from urllib.parse import urlparse # Py 3
21 21 except ImportError:
22 22 from urlparse import urlparse # Py 2
23 23
24 24 try:
25 25 from http.cookies import SimpleCookie # Py 3
26 26 except ImportError:
27 27 from Cookie import SimpleCookie # Py 2
28 28 import logging
29
30 import tornado
29 31 from tornado import web
30 32 from tornado import websocket
31 33
32 34 from zmq.utils import jsonapi
33 35
34 36 from IPython.kernel.zmq.session import Session
35 37 from IPython.utils.jsonutil import date_default
36 38 from IPython.utils.py3compat import PY3, cast_unicode
37 39
38 40 from .handlers import IPythonHandler
39 41
40 42 #-----------------------------------------------------------------------------
41 43 # ZMQ handlers
42 44 #-----------------------------------------------------------------------------
43 45
44 46 class ZMQStreamHandler(websocket.WebSocketHandler):
45 47
46 def same_origin(self):
47 """Check to see that origin and host match in the headers."""
48 def check_origin(self, origin):
49 """Check Origin == Host or Access-Control-Allow-Origin.
48 50
49 # The difference between version 8 and 13 is that in 8 the
50 # client sends a "Sec-Websocket-Origin" header and in 13 it's
51 # simply "Origin".
52 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8"):
53 origin_header = self.request.headers.get("Sec-Websocket-Origin")
54 else:
55 origin_header = self.request.headers.get("Origin")
51 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
52 We call it explicitly in `open` on Tornado < 4.
53 """
54 if self.allow_origin == '*':
55 return True
56 56
57 57 host = self.request.headers.get("Host")
58 58
59 59 # If no header is provided, assume we can't verify origin
60 if(origin_header is None or host is None):
60 if(origin is None or host is None):
61 61 return False
62 62
63 parsed_origin = urlparse(origin_header)
64 origin = parsed_origin.netloc
63 host_origin = "{0}://{1}".format(self.request.protocol, host)
65 64
66 # Check to see that origin matches host directly, including ports
67 return origin == host
65 # OK if origin matches host
66 if origin == host_origin:
67 return True
68
69 # Check CORS headers
70 if self.allow_origin:
71 return self.allow_origin == origin
72 elif self.allow_origin_pat:
73 return bool(self.allow_origin_pat.match(origin))
74 else:
75 # No CORS headers deny the request
76 return False
68 77
69 78 def clear_cookie(self, *args, **kwargs):
70 79 """meaningless for websockets"""
71 80 pass
72 81
73 82 def _reserialize_reply(self, msg_list):
74 83 """Reserialize a reply message using JSON.
75 84
76 85 This takes the msg list from the ZMQ socket, unserializes it using
77 86 self.session and then serializes the result using JSON. This method
78 87 should be used by self._on_zmq_reply to build messages that can
79 88 be sent back to the browser.
80 89 """
81 90 idents, msg_list = self.session.feed_identities(msg_list)
82 91 msg = self.session.unserialize(msg_list)
83 92 try:
84 93 msg['header'].pop('date')
85 94 except KeyError:
86 95 pass
87 96 try:
88 97 msg['parent_header'].pop('date')
89 98 except KeyError:
90 99 pass
91 100 msg.pop('buffers')
92 101 return jsonapi.dumps(msg, default=date_default)
93 102
94 103 def _on_zmq_reply(self, msg_list):
95 104 # Sometimes this gets triggered when the on_close method is scheduled in the
96 105 # eventloop but hasn't been called.
97 106 if self.stream.closed(): return
98 107 try:
99 108 msg = self._reserialize_reply(msg_list)
100 109 except Exception:
101 110 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
102 111 else:
103 112 self.write_message(msg)
104 113
105 114 def allow_draft76(self):
106 115 """Allow draft 76, until browsers such as Safari update to RFC 6455.
107 116
108 117 This has been disabled by default in tornado in release 2.2.0, and
109 118 support will be removed in later versions.
110 119 """
111 120 return True
112 121
113 122
114 123 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
124 def set_default_headers(self):
125 """Undo the set_default_headers in IPythonHandler
126
127 which doesn't make sense for websockets
128 """
129 pass
115 130
116 131 def open(self, kernel_id):
117 132 self.kernel_id = cast_unicode(kernel_id, 'ascii')
118 133 # Check to see that origin matches host directly, including ports
119 if not self.same_origin():
120 self.log.warn("Cross Origin WebSocket Attempt.")
121 raise web.HTTPError(404)
134 # Tornado 4 already does CORS checking
135 if tornado.version_info[0] < 4:
136 if not self.check_origin(self.get_origin()):
137 self.log.warn("Cross Origin WebSocket Attempt from %s", self.get_origin())
138 raise web.HTTPError(403)
122 139
123 140 self.session = Session(config=self.config)
124 141 self.save_on_message = self.on_message
125 142 self.on_message = self.on_first_message
126 143
127 144 def _inject_cookie_message(self, msg):
128 145 """Inject the first message, which is the document cookie,
129 146 for authentication."""
130 147 if not PY3 and isinstance(msg, unicode):
131 148 # Cookie constructor doesn't accept unicode strings
132 149 # under Python 2.x for some reason
133 150 msg = msg.encode('utf8', 'replace')
134 151 try:
135 152 identity, msg = msg.split(':', 1)
136 153 self.session.session = cast_unicode(identity, 'ascii')
137 154 except Exception:
138 155 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
139 156
140 157 try:
141 158 self.request._cookies = SimpleCookie(msg)
142 159 except:
143 160 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
144 161
145 162 def on_first_message(self, msg):
146 163 self._inject_cookie_message(msg)
147 164 if self.get_current_user() is None:
148 165 self.log.warn("Couldn't authenticate WebSocket connection")
149 166 raise web.HTTPError(403)
150 167 self.on_message = self.save_on_message
@@ -1,851 +1,882 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 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 import re
27 28 import select
28 29 import signal
29 30 import socket
30 31 import sys
31 32 import threading
32 33 import time
33 34 import webbrowser
34 35
35 36
36 37 # Third party
37 38 # check for pyzmq 2.1.11
38 39 from IPython.utils.zmqrelated import check_for_zmq
39 40 check_for_zmq('2.1.11', 'IPython.html')
40 41
41 42 from jinja2 import Environment, FileSystemLoader
42 43
43 44 # Install the pyzmq ioloop. This has to be done before anything else from
44 45 # tornado is imported.
45 46 from zmq.eventloop import ioloop
46 47 ioloop.install()
47 48
48 49 # check for tornado 3.1.0
49 50 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 51 try:
51 52 import tornado
52 53 except ImportError:
53 54 raise ImportError(msg)
54 55 try:
55 56 version_info = tornado.version_info
56 57 except AttributeError:
57 58 raise ImportError(msg + ", but you have < 1.1.0")
58 59 if version_info < (3,1,0):
59 60 raise ImportError(msg + ", but you have %s" % tornado.version)
60 61
61 62 from tornado import httpserver
62 63 from tornado import web
63 64
64 65 # Our own libraries
65 66 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 67 from .base.handlers import Template404
67 68 from .log import log_request
68 69 from .services.kernels.kernelmanager import MappingKernelManager
69 70 from .services.notebooks.nbmanager import NotebookManager
70 71 from .services.notebooks.filenbmanager import FileNotebookManager
71 72 from .services.clusters.clustermanager import ClusterManager
72 73 from .services.sessions.sessionmanager import SessionManager
73 74
74 75 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 76
76 77 from IPython.config import Config
77 78 from IPython.config.application import catch_config_error, boolean_flag
78 79 from IPython.core.application import BaseIPythonApplication
79 80 from IPython.core.profiledir import ProfileDir
80 81 from IPython.consoleapp import IPythonConsoleApp
81 82 from IPython.kernel import swallow_argv
82 83 from IPython.kernel.zmq.session import default_secure
83 84 from IPython.kernel.zmq.kernelapp import (
84 85 kernel_flags,
85 86 kernel_aliases,
86 87 )
87 88 from IPython.nbformat.sign import NotebookNotary
88 89 from IPython.utils.importstring import import_item
89 90 from IPython.utils import submodule
90 91 from IPython.utils.traitlets import (
91 92 Dict, Unicode, Integer, List, Bool, Bytes,
92 93 DottedObjectName, TraitError,
93 94 )
94 95 from IPython.utils import py3compat
95 96 from IPython.utils.path import filefind, get_ipython_dir
96 97
97 98 from .utils import url_path_join
98 99
99 100 #-----------------------------------------------------------------------------
100 101 # Module globals
101 102 #-----------------------------------------------------------------------------
102 103
103 104 _examples = """
104 105 ipython notebook # start the notebook
105 106 ipython notebook --profile=sympy # use the sympy profile
106 107 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
107 108 """
108 109
109 110 #-----------------------------------------------------------------------------
110 111 # Helper functions
111 112 #-----------------------------------------------------------------------------
112 113
113 114 def random_ports(port, n):
114 115 """Generate a list of n random ports near the given port.
115 116
116 117 The first 5 ports will be sequential, and the remaining n-5 will be
117 118 randomly selected in the range [port-2*n, port+2*n].
118 119 """
119 120 for i in range(min(5, n)):
120 121 yield port + i
121 122 for i in range(n-5):
122 123 yield max(1, port + random.randint(-2*n, 2*n))
123 124
124 125 def load_handlers(name):
125 126 """Load the (URL pattern, handler) tuples for each component."""
126 127 name = 'IPython.html.' + name
127 128 mod = __import__(name, fromlist=['default_handlers'])
128 129 return mod.default_handlers
129 130
130 131 #-----------------------------------------------------------------------------
131 132 # The Tornado web application
132 133 #-----------------------------------------------------------------------------
133 134
134 135 class NotebookWebApplication(web.Application):
135 136
136 137 def __init__(self, ipython_app, kernel_manager, notebook_manager,
137 138 cluster_manager, session_manager, log, base_url,
138 139 settings_overrides, jinja_env_options):
139 140
140 141 settings = self.init_settings(
141 142 ipython_app, kernel_manager, notebook_manager, cluster_manager,
142 143 session_manager, log, base_url, settings_overrides, jinja_env_options)
143 144 handlers = self.init_handlers(settings)
144 145
145 146 super(NotebookWebApplication, self).__init__(handlers, **settings)
146 147
147 148 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
148 149 cluster_manager, session_manager, log, base_url,
149 150 settings_overrides, jinja_env_options=None):
150 151 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
151 152 # base_url will always be unicode, which will in turn
152 153 # make the patterns unicode, and ultimately result in unicode
153 154 # keys in kwargs to handler._execute(**kwargs) in tornado.
154 155 # This enforces that base_url be ascii in that situation.
155 156 #
156 157 # Note that the URLs these patterns check against are escaped,
157 158 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
158 159 base_url = py3compat.unicode_to_str(base_url, 'ascii')
159 160 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
160 161 jenv_opt = jinja_env_options if jinja_env_options else {}
161 162 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
162 163 settings = dict(
163 164 # basics
164 165 log_function=log_request,
165 166 base_url=base_url,
166 167 template_path=template_path,
167 168 static_path=ipython_app.static_file_path,
168 169 static_handler_class = FileFindHandler,
169 170 static_url_prefix = url_path_join(base_url,'/static/'),
170 171
171 172 # authentication
172 173 cookie_secret=ipython_app.cookie_secret,
173 174 login_url=url_path_join(base_url,'/login'),
174 175 password=ipython_app.password,
175 176
176 177 # managers
177 178 kernel_manager=kernel_manager,
178 179 notebook_manager=notebook_manager,
179 180 cluster_manager=cluster_manager,
180 181 session_manager=session_manager,
181 182
182 183 # IPython stuff
183 184 nbextensions_path = ipython_app.nbextensions_path,
184 185 mathjax_url=ipython_app.mathjax_url,
185 186 config=ipython_app.config,
186 187 jinja2_env=env,
187 188 )
188 189
189 190 # allow custom overrides for the tornado web app.
190 191 settings.update(settings_overrides)
191 192 return settings
192 193
193 194 def init_handlers(self, settings):
194 195 # Load the (URL pattern, handler) tuples for each component.
195 196 handlers = []
196 197 handlers.extend(load_handlers('base.handlers'))
197 198 handlers.extend(load_handlers('tree.handlers'))
198 199 handlers.extend(load_handlers('auth.login'))
199 200 handlers.extend(load_handlers('auth.logout'))
200 201 handlers.extend(load_handlers('notebook.handlers'))
201 202 handlers.extend(load_handlers('nbconvert.handlers'))
202 203 handlers.extend(load_handlers('services.kernels.handlers'))
203 204 handlers.extend(load_handlers('services.notebooks.handlers'))
204 205 handlers.extend(load_handlers('services.clusters.handlers'))
205 206 handlers.extend(load_handlers('services.sessions.handlers'))
206 207 handlers.extend(load_handlers('services.nbconvert.handlers'))
207 208 # FIXME: /files/ should be handled by the Contents service when it exists
208 209 nbm = settings['notebook_manager']
209 210 if hasattr(nbm, 'notebook_dir'):
210 211 handlers.extend([
211 212 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
212 213 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
213 214 ])
214 215 # prepend base_url onto the patterns that we match
215 216 new_handlers = []
216 217 for handler in handlers:
217 218 pattern = url_path_join(settings['base_url'], handler[0])
218 219 new_handler = tuple([pattern] + list(handler[1:]))
219 220 new_handlers.append(new_handler)
220 221 # add 404 on the end, which will catch everything that falls through
221 222 new_handlers.append((r'(.*)', Template404))
222 223 return new_handlers
223 224
224 225
225 226 class NbserverListApp(BaseIPythonApplication):
226 227
227 228 description="List currently running notebook servers in this profile."
228 229
229 230 flags = dict(
230 231 json=({'NbserverListApp': {'json': True}},
231 232 "Produce machine-readable JSON output."),
232 233 )
233 234
234 235 json = Bool(False, config=True,
235 236 help="If True, each line of output will be a JSON object with the "
236 237 "details from the server info file.")
237 238
238 239 def start(self):
239 240 if not self.json:
240 241 print("Currently running servers:")
241 242 for serverinfo in list_running_servers(self.profile):
242 243 if self.json:
243 244 print(json.dumps(serverinfo))
244 245 else:
245 246 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
246 247
247 248 #-----------------------------------------------------------------------------
248 249 # Aliases and Flags
249 250 #-----------------------------------------------------------------------------
250 251
251 252 flags = dict(kernel_flags)
252 253 flags['no-browser']=(
253 254 {'NotebookApp' : {'open_browser' : False}},
254 255 "Don't open the notebook in a browser after startup."
255 256 )
256 257 flags['no-mathjax']=(
257 258 {'NotebookApp' : {'enable_mathjax' : False}},
258 259 """Disable MathJax
259 260
260 261 MathJax is the javascript library IPython uses to render math/LaTeX. It is
261 262 very large, so you may want to disable it if you have a slow internet
262 263 connection, or for offline use of the notebook.
263 264
264 265 When disabled, equations etc. will appear as their untransformed TeX source.
265 266 """
266 267 )
267 268
268 269 # Add notebook manager flags
269 270 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
270 271 'Auto-save a .py script everytime the .ipynb notebook is saved',
271 272 'Do not auto-save .py scripts for every notebook'))
272 273
273 274 # the flags that are specific to the frontend
274 275 # these must be scrubbed before being passed to the kernel,
275 276 # or it will raise an error on unrecognized flags
276 277 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
277 278
278 279 aliases = dict(kernel_aliases)
279 280
280 281 aliases.update({
281 282 'ip': 'NotebookApp.ip',
282 283 'port': 'NotebookApp.port',
283 284 'port-retries': 'NotebookApp.port_retries',
284 285 'transport': 'KernelManager.transport',
285 286 'keyfile': 'NotebookApp.keyfile',
286 287 'certfile': 'NotebookApp.certfile',
287 288 'notebook-dir': 'NotebookApp.notebook_dir',
288 289 'browser': 'NotebookApp.browser',
289 290 })
290 291
291 292 # remove ipkernel flags that are singletons, and don't make sense in
292 293 # multi-kernel evironment:
293 294 aliases.pop('f', None)
294 295
295 296 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
296 297 u'notebook-dir', u'profile', u'profile-dir', 'browser']
297 298
298 299 #-----------------------------------------------------------------------------
299 300 # NotebookApp
300 301 #-----------------------------------------------------------------------------
301 302
302 303 class NotebookApp(BaseIPythonApplication):
303 304
304 305 name = 'ipython-notebook'
305 306
306 307 description = """
307 308 The IPython HTML Notebook.
308 309
309 310 This launches a Tornado based HTML Notebook Server that serves up an
310 311 HTML5/Javascript Notebook client.
311 312 """
312 313 examples = _examples
313 314
314 315 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
315 316 FileNotebookManager, NotebookNotary]
316 317 flags = Dict(flags)
317 318 aliases = Dict(aliases)
318 319
319 320 subcommands = dict(
320 321 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
321 322 )
322 323
323 324 kernel_argv = List(Unicode)
324 325
325 326 def _log_level_default(self):
326 327 return logging.INFO
327 328
328 329 def _log_format_default(self):
329 330 """override default log format to include time"""
330 331 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
331 332
332 333 # create requested profiles by default, if they don't exist:
333 334 auto_create = Bool(True)
334 335
335 336 # file to be opened in the notebook server
336 337 file_to_run = Unicode('', config=True)
337 338 def _file_to_run_changed(self, name, old, new):
338 339 path, base = os.path.split(new)
339 340 if path:
340 341 self.file_to_run = base
341 342 self.notebook_dir = path
342 343
343 # Network related information.
344 # Network related information
345
346 allow_origin = Unicode('', config=True,
347 help="""Set the Access-Control-Allow-Origin header
348
349 Use '*' to allow any origin to access your server.
350
351 Takes precedence over allow_origin_pat.
352 """
353 )
354
355 allow_origin_pat = Unicode('', config=True,
356 help="""Use a regular expression for the Access-Control-Allow-Origin header
357
358 Requests from an origin matching the expression will get replies with:
359
360 Access-Control-Allow-Origin: origin
361
362 where `origin` is the origin of the request.
363
364 Ignored if allow_origin is set.
365 """
366 )
367
368 allow_credentials = Bool(False, config=True,
369 help="Set the Access-Control-Allow-Credentials: true header"
370 )
344 371
345 372 ip = Unicode('localhost', config=True,
346 373 help="The IP address the notebook server will listen on."
347 374 )
348 375
349 376 def _ip_changed(self, name, old, new):
350 377 if new == u'*': self.ip = u''
351 378
352 379 port = Integer(8888, config=True,
353 380 help="The port the notebook server will listen on."
354 381 )
355 382 port_retries = Integer(50, config=True,
356 383 help="The number of additional ports to try if the specified port is not available."
357 384 )
358 385
359 386 certfile = Unicode(u'', config=True,
360 387 help="""The full path to an SSL/TLS certificate file."""
361 388 )
362 389
363 390 keyfile = Unicode(u'', config=True,
364 391 help="""The full path to a private key file for usage with SSL/TLS."""
365 392 )
366 393
367 394 cookie_secret = Bytes(b'', config=True,
368 395 help="""The random bytes used to secure cookies.
369 396 By default this is a new random number every time you start the Notebook.
370 397 Set it to a value in a config file to enable logins to persist across server sessions.
371 398
372 399 Note: Cookie secrets should be kept private, do not share config files with
373 400 cookie_secret stored in plaintext (you can read the value from a file).
374 401 """
375 402 )
376 403 def _cookie_secret_default(self):
377 404 return os.urandom(1024)
378 405
379 406 password = Unicode(u'', config=True,
380 407 help="""Hashed password to use for web authentication.
381 408
382 409 To generate, type in a python/IPython shell:
383 410
384 411 from IPython.lib import passwd; passwd()
385 412
386 413 The string should be of the form type:salt:hashed-password.
387 414 """
388 415 )
389 416
390 417 open_browser = Bool(True, config=True,
391 418 help="""Whether to open in a browser after starting.
392 419 The specific browser used is platform dependent and
393 420 determined by the python standard library `webbrowser`
394 421 module, unless it is overridden using the --browser
395 422 (NotebookApp.browser) configuration option.
396 423 """)
397 424
398 425 browser = Unicode(u'', config=True,
399 426 help="""Specify what command to use to invoke a web
400 427 browser when opening the notebook. If not specified, the
401 428 default browser will be determined by the `webbrowser`
402 429 standard library module, which allows setting of the
403 430 BROWSER environment variable to override it.
404 431 """)
405 432
406 433 webapp_settings = Dict(config=True,
407 434 help="Supply overrides for the tornado.web.Application that the "
408 435 "IPython notebook uses.")
409 436
410 437 jinja_environment_options = Dict(config=True,
411 438 help="Supply extra arguments that will be passed to Jinja environment.")
412 439
413 440
414 441 enable_mathjax = Bool(True, config=True,
415 442 help="""Whether to enable MathJax for typesetting math/TeX
416 443
417 444 MathJax is the javascript library IPython uses to render math/LaTeX. It is
418 445 very large, so you may want to disable it if you have a slow internet
419 446 connection, or for offline use of the notebook.
420 447
421 448 When disabled, equations etc. will appear as their untransformed TeX source.
422 449 """
423 450 )
424 451 def _enable_mathjax_changed(self, name, old, new):
425 452 """set mathjax url to empty if mathjax is disabled"""
426 453 if not new:
427 454 self.mathjax_url = u''
428 455
429 456 base_url = Unicode('/', config=True,
430 457 help='''The base URL for the notebook server.
431 458
432 459 Leading and trailing slashes can be omitted,
433 460 and will automatically be added.
434 461 ''')
435 462 def _base_url_changed(self, name, old, new):
436 463 if not new.startswith('/'):
437 464 self.base_url = '/'+new
438 465 elif not new.endswith('/'):
439 466 self.base_url = new+'/'
440 467
441 468 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
442 469 def _base_project_url_changed(self, name, old, new):
443 470 self.log.warn("base_project_url is deprecated, use base_url")
444 471 self.base_url = new
445 472
446 473 extra_static_paths = List(Unicode, config=True,
447 474 help="""Extra paths to search for serving static files.
448 475
449 476 This allows adding javascript/css to be available from the notebook server machine,
450 477 or overriding individual files in the IPython"""
451 478 )
452 479 def _extra_static_paths_default(self):
453 480 return [os.path.join(self.profile_dir.location, 'static')]
454 481
455 482 @property
456 483 def static_file_path(self):
457 484 """return extra paths + the default location"""
458 485 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
459 486
460 487 nbextensions_path = List(Unicode, config=True,
461 488 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
462 489 )
463 490 def _nbextensions_path_default(self):
464 491 return [os.path.join(get_ipython_dir(), 'nbextensions')]
465 492
466 493 mathjax_url = Unicode("", config=True,
467 494 help="""The url for MathJax.js."""
468 495 )
469 496 def _mathjax_url_default(self):
470 497 if not self.enable_mathjax:
471 498 return u''
472 499 static_url_prefix = self.webapp_settings.get("static_url_prefix",
473 500 url_path_join(self.base_url, "static")
474 501 )
475 502
476 503 # try local mathjax, either in nbextensions/mathjax or static/mathjax
477 504 for (url_prefix, search_path) in [
478 505 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
479 506 (static_url_prefix, self.static_file_path),
480 507 ]:
481 508 self.log.debug("searching for local mathjax in %s", search_path)
482 509 try:
483 510 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
484 511 except IOError:
485 512 continue
486 513 else:
487 514 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
488 515 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
489 516 return url
490 517
491 518 # no local mathjax, serve from CDN
492 519 if self.certfile:
493 520 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
494 521 host = u"https://c328740.ssl.cf1.rackcdn.com"
495 522 else:
496 523 host = u"http://cdn.mathjax.org"
497 524
498 525 url = host + u"/mathjax/latest/MathJax.js"
499 526 self.log.info("Using MathJax from CDN: %s", url)
500 527 return url
501 528
502 529 def _mathjax_url_changed(self, name, old, new):
503 530 if new and not self.enable_mathjax:
504 531 # enable_mathjax=False overrides mathjax_url
505 532 self.mathjax_url = u''
506 533 else:
507 534 self.log.info("Using MathJax: %s", new)
508 535
509 536 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
510 537 config=True,
511 538 help='The notebook manager class to use.')
512 539
513 540 trust_xheaders = Bool(False, config=True,
514 541 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
515 542 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
516 543 )
517 544
518 545 info_file = Unicode()
519 546
520 547 def _info_file_default(self):
521 548 info_file = "nbserver-%s.json"%os.getpid()
522 549 return os.path.join(self.profile_dir.security_dir, info_file)
523 550
524 551 notebook_dir = Unicode(py3compat.getcwd(), config=True,
525 552 help="The directory to use for notebooks and kernels."
526 553 )
527 554
528 555 def _notebook_dir_changed(self, name, old, new):
529 556 """Do a bit of validation of the notebook dir."""
530 557 if not os.path.isabs(new):
531 558 # If we receive a non-absolute path, make it absolute.
532 559 self.notebook_dir = os.path.abspath(new)
533 560 return
534 561 if not os.path.isdir(new):
535 562 raise TraitError("No such notebook dir: %r" % new)
536 563
537 564 # setting App.notebook_dir implies setting notebook and kernel dirs as well
538 565 self.config.FileNotebookManager.notebook_dir = new
539 566 self.config.MappingKernelManager.root_dir = new
540 567
541 568
542 569 def parse_command_line(self, argv=None):
543 570 super(NotebookApp, self).parse_command_line(argv)
544 571
545 572 if self.extra_args:
546 573 arg0 = self.extra_args[0]
547 574 f = os.path.abspath(arg0)
548 575 self.argv.remove(arg0)
549 576 if not os.path.exists(f):
550 577 self.log.critical("No such file or directory: %s", f)
551 578 self.exit(1)
552 579
553 580 # Use config here, to ensure that it takes higher priority than
554 581 # anything that comes from the profile.
555 582 c = Config()
556 583 if os.path.isdir(f):
557 584 c.NotebookApp.notebook_dir = f
558 585 elif os.path.isfile(f):
559 586 c.NotebookApp.file_to_run = f
560 587 self.update_config(c)
561 588
562 589 def init_kernel_argv(self):
563 590 """construct the kernel arguments"""
564 591 # Scrub frontend-specific flags
565 592 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
566 593 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
567 594 self.log.warn('\n '.join([
568 595 "Starting all kernels in pylab mode is not recommended,",
569 596 "and will be disabled in a future release.",
570 597 "Please use the %matplotlib magic to enable matplotlib instead.",
571 598 "pylab implies many imports, which can have confusing side effects",
572 599 "and harm the reproducibility of your notebooks.",
573 600 ]))
574 601 # Kernel should inherit default config file from frontend
575 602 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
576 603 # Kernel should get *absolute* path to profile directory
577 604 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
578 605
579 606 def init_configurables(self):
580 607 # force Session default to be secure
581 608 default_secure(self.config)
582 609 self.kernel_manager = MappingKernelManager(
583 610 parent=self, log=self.log, kernel_argv=self.kernel_argv,
584 611 connection_dir = self.profile_dir.security_dir,
585 612 )
586 613 kls = import_item(self.notebook_manager_class)
587 614 self.notebook_manager = kls(parent=self, log=self.log)
588 615 self.session_manager = SessionManager(parent=self, log=self.log)
589 616 self.cluster_manager = ClusterManager(parent=self, log=self.log)
590 617 self.cluster_manager.update_profiles()
591 618
592 619 def init_logging(self):
593 620 # This prevents double log messages because tornado use a root logger that
594 621 # self.log is a child of. The logging module dipatches log messages to a log
595 622 # and all of its ancenstors until propagate is set to False.
596 623 self.log.propagate = False
597 624
598 625 # hook up tornado 3's loggers to our app handlers
599 626 for name in ('access', 'application', 'general'):
600 627 logger = logging.getLogger('tornado.%s' % name)
601 628 logger.parent = self.log
602 629 logger.setLevel(self.log.level)
603
630
604 631 def init_webapp(self):
605 632 """initialize tornado webapp and httpserver"""
633 self.webapp_settings['allow_origin'] = self.allow_origin
634 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
635 self.webapp_settings['allow_credentials'] = self.allow_credentials
636
606 637 self.web_app = NotebookWebApplication(
607 self, self.kernel_manager, self.notebook_manager,
638 self, self.kernel_manager, self.notebook_manager,
608 639 self.cluster_manager, self.session_manager,
609 640 self.log, self.base_url, self.webapp_settings,
610 641 self.jinja_environment_options
611 642 )
612 643 if self.certfile:
613 644 ssl_options = dict(certfile=self.certfile)
614 645 if self.keyfile:
615 646 ssl_options['keyfile'] = self.keyfile
616 647 else:
617 648 ssl_options = None
618 649 self.web_app.password = self.password
619 650 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
620 651 xheaders=self.trust_xheaders)
621 652 if not self.ip:
622 653 warning = "WARNING: The notebook server is listening on all IP addresses"
623 654 if ssl_options is None:
624 655 self.log.critical(warning + " and not using encryption. This "
625 656 "is not recommended.")
626 657 if not self.password:
627 658 self.log.critical(warning + " and not using authentication. "
628 659 "This is highly insecure and not recommended.")
629 660 success = None
630 661 for port in random_ports(self.port, self.port_retries+1):
631 662 try:
632 663 self.http_server.listen(port, self.ip)
633 664 except socket.error as e:
634 665 if e.errno == errno.EADDRINUSE:
635 666 self.log.info('The port %i is already in use, trying another random port.' % port)
636 667 continue
637 668 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
638 669 self.log.warn("Permission to listen on port %i denied" % port)
639 670 continue
640 671 else:
641 672 raise
642 673 else:
643 674 self.port = port
644 675 success = True
645 676 break
646 677 if not success:
647 678 self.log.critical('ERROR: the notebook server could not be started because '
648 679 'no available port could be found.')
649 680 self.exit(1)
650 681
651 682 @property
652 683 def display_url(self):
653 684 ip = self.ip if self.ip else '[all ip addresses on your system]'
654 685 return self._url(ip)
655 686
656 687 @property
657 688 def connection_url(self):
658 689 ip = self.ip if self.ip else 'localhost'
659 690 return self._url(ip)
660 691
661 692 def _url(self, ip):
662 693 proto = 'https' if self.certfile else 'http'
663 694 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
664 695
665 696 def init_signal(self):
666 697 if not sys.platform.startswith('win'):
667 698 signal.signal(signal.SIGINT, self._handle_sigint)
668 699 signal.signal(signal.SIGTERM, self._signal_stop)
669 700 if hasattr(signal, 'SIGUSR1'):
670 701 # Windows doesn't support SIGUSR1
671 702 signal.signal(signal.SIGUSR1, self._signal_info)
672 703 if hasattr(signal, 'SIGINFO'):
673 704 # only on BSD-based systems
674 705 signal.signal(signal.SIGINFO, self._signal_info)
675 706
676 707 def _handle_sigint(self, sig, frame):
677 708 """SIGINT handler spawns confirmation dialog"""
678 709 # register more forceful signal handler for ^C^C case
679 710 signal.signal(signal.SIGINT, self._signal_stop)
680 711 # request confirmation dialog in bg thread, to avoid
681 712 # blocking the App
682 713 thread = threading.Thread(target=self._confirm_exit)
683 714 thread.daemon = True
684 715 thread.start()
685 716
686 717 def _restore_sigint_handler(self):
687 718 """callback for restoring original SIGINT handler"""
688 719 signal.signal(signal.SIGINT, self._handle_sigint)
689 720
690 721 def _confirm_exit(self):
691 722 """confirm shutdown on ^C
692 723
693 724 A second ^C, or answering 'y' within 5s will cause shutdown,
694 725 otherwise original SIGINT handler will be restored.
695 726
696 727 This doesn't work on Windows.
697 728 """
698 729 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
699 730 time.sleep(0.1)
700 731 info = self.log.info
701 732 info('interrupted')
702 733 print(self.notebook_info())
703 734 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
704 735 sys.stdout.flush()
705 736 r,w,x = select.select([sys.stdin], [], [], 5)
706 737 if r:
707 738 line = sys.stdin.readline()
708 739 if line.lower().startswith('y') and 'n' not in line.lower():
709 740 self.log.critical("Shutdown confirmed")
710 741 ioloop.IOLoop.instance().stop()
711 742 return
712 743 else:
713 744 print("No answer for 5s:", end=' ')
714 745 print("resuming operation...")
715 746 # no answer, or answer is no:
716 747 # set it back to original SIGINT handler
717 748 # use IOLoop.add_callback because signal.signal must be called
718 749 # from main thread
719 750 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
720 751
721 752 def _signal_stop(self, sig, frame):
722 753 self.log.critical("received signal %s, stopping", sig)
723 754 ioloop.IOLoop.instance().stop()
724 755
725 756 def _signal_info(self, sig, frame):
726 757 print(self.notebook_info())
727 758
728 759 def init_components(self):
729 760 """Check the components submodule, and warn if it's unclean"""
730 761 status = submodule.check_submodule_status()
731 762 if status == 'missing':
732 763 self.log.warn("components submodule missing, running `git submodule update`")
733 764 submodule.update_submodules(submodule.ipython_parent())
734 765 elif status == 'unclean':
735 766 self.log.warn("components submodule unclean, you may see 404s on static/components")
736 767 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
737 768
738 769 @catch_config_error
739 770 def initialize(self, argv=None):
740 771 super(NotebookApp, self).initialize(argv)
741 772 self.init_logging()
742 773 self.init_kernel_argv()
743 774 self.init_configurables()
744 775 self.init_components()
745 776 self.init_webapp()
746 777 self.init_signal()
747 778
748 779 def cleanup_kernels(self):
749 780 """Shutdown all kernels.
750 781
751 782 The kernels will shutdown themselves when this process no longer exists,
752 783 but explicit shutdown allows the KernelManagers to cleanup the connection files.
753 784 """
754 785 self.log.info('Shutting down kernels')
755 786 self.kernel_manager.shutdown_all()
756 787
757 788 def notebook_info(self):
758 789 "Return the current working directory and the server url information"
759 790 info = self.notebook_manager.info_string() + "\n"
760 791 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
761 792 return info + "The IPython Notebook is running at: %s" % self.display_url
762 793
763 794 def server_info(self):
764 795 """Return a JSONable dict of information about this server."""
765 796 return {'url': self.connection_url,
766 797 'hostname': self.ip if self.ip else 'localhost',
767 798 'port': self.port,
768 799 'secure': bool(self.certfile),
769 800 'base_url': self.base_url,
770 801 'notebook_dir': os.path.abspath(self.notebook_dir),
771 802 }
772 803
773 804 def write_server_info_file(self):
774 805 """Write the result of server_info() to the JSON file info_file."""
775 806 with open(self.info_file, 'w') as f:
776 807 json.dump(self.server_info(), f, indent=2)
777 808
778 809 def remove_server_info_file(self):
779 810 """Remove the nbserver-<pid>.json file created for this server.
780 811
781 812 Ignores the error raised when the file has already been removed.
782 813 """
783 814 try:
784 815 os.unlink(self.info_file)
785 816 except OSError as e:
786 817 if e.errno != errno.ENOENT:
787 818 raise
788 819
789 820 def start(self):
790 821 """ Start the IPython Notebook server app, after initialization
791 822
792 823 This method takes no arguments so all configuration and initialization
793 824 must be done prior to calling this method."""
794 825 if self.subapp is not None:
795 826 return self.subapp.start()
796 827
797 828 info = self.log.info
798 829 for line in self.notebook_info().split("\n"):
799 830 info(line)
800 831 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
801 832
802 833 self.write_server_info_file()
803 834
804 835 if self.open_browser or self.file_to_run:
805 836 try:
806 837 browser = webbrowser.get(self.browser or None)
807 838 except webbrowser.Error as e:
808 839 self.log.warn('No web browser found: %s.' % e)
809 840 browser = None
810 841
811 842 if self.file_to_run:
812 843 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
813 844 if not os.path.exists(fullpath):
814 845 self.log.critical("%s does not exist" % fullpath)
815 846 self.exit(1)
816 847
817 848 uri = url_path_join('notebooks', self.file_to_run)
818 849 else:
819 850 uri = 'tree'
820 851 if browser:
821 852 b = lambda : browser.open(url_path_join(self.connection_url, uri),
822 853 new=2)
823 854 threading.Thread(target=b).start()
824 855 try:
825 856 ioloop.IOLoop.instance().start()
826 857 except KeyboardInterrupt:
827 858 info("Interrupted...")
828 859 finally:
829 860 self.cleanup_kernels()
830 861 self.remove_server_info_file()
831 862
832 863
833 864 def list_running_servers(profile='default'):
834 865 """Iterate over the server info files of running notebook servers.
835 866
836 867 Given a profile name, find nbserver-* files in the security directory of
837 868 that profile, and yield dicts of their information, each one pertaining to
838 869 a currently running notebook server instance.
839 870 """
840 871 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
841 872 for file in os.listdir(pd.security_dir):
842 873 if file.startswith('nbserver-'):
843 874 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
844 875 yield json.load(f)
845 876
846 877 #-----------------------------------------------------------------------------
847 878 # Main entry point
848 879 #-----------------------------------------------------------------------------
849 880
850 881 launch_new_instance = NotebookApp.launch_instance
851 882
General Comments 0
You need to be logged in to leave comments. Login now