##// END OF EJS Templates
remove base_kernel_url
MinRK -
Show More
@@ -1,387 +1,382 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 clear_login_cookie(self):
55 55 self.clear_cookie(self.cookie_name)
56 56
57 57 def get_current_user(self):
58 58 user_id = self.get_secure_cookie(self.cookie_name)
59 59 # For now the user_id should not return empty, but it could eventually
60 60 if user_id == '':
61 61 user_id = 'anonymous'
62 62 if user_id is None:
63 63 # prevent extra Invalid cookie sig warnings:
64 64 self.clear_login_cookie()
65 65 if not self.login_available:
66 66 user_id = 'anonymous'
67 67 return user_id
68 68
69 69 @property
70 70 def cookie_name(self):
71 71 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
72 72 self.request.host
73 73 ))
74 74 return self.settings.get('cookie_name', default_cookie_name)
75 75
76 76 @property
77 77 def password(self):
78 78 """our password"""
79 79 return self.settings.get('password', '')
80 80
81 81 @property
82 82 def logged_in(self):
83 83 """Is a user currently logged in?
84 84
85 85 """
86 86 user = self.get_current_user()
87 87 return (user and not user == 'anonymous')
88 88
89 89 @property
90 90 def login_available(self):
91 91 """May a user proceed to log in?
92 92
93 93 This returns True if login capability is available, irrespective of
94 94 whether the user is already logged in or not.
95 95
96 96 """
97 97 return bool(self.settings.get('password', ''))
98 98
99 99
100 100 class IPythonHandler(AuthenticatedHandler):
101 101 """IPython-specific extensions to authenticated handling
102 102
103 103 Mostly property shortcuts to IPython-specific settings.
104 104 """
105 105
106 106 @property
107 107 def config(self):
108 108 return self.settings.get('config', None)
109 109
110 110 @property
111 111 def log(self):
112 112 """use the IPython log by default, falling back on tornado's logger"""
113 113 if Application.initialized():
114 114 return Application.instance().log
115 115 else:
116 116 return app_log
117 117
118 118 #---------------------------------------------------------------
119 119 # URLs
120 120 #---------------------------------------------------------------
121 121
122 122 @property
123 123 def ws_url(self):
124 124 """websocket url matching the current request
125 125
126 126 By default, this is just `''`, indicating that it should match
127 127 the same host, protocol, port, etc.
128 128 """
129 129 return self.settings.get('websocket_url', '')
130 130
131 131 @property
132 132 def mathjax_url(self):
133 133 return self.settings.get('mathjax_url', '')
134 134
135 135 @property
136 136 def base_url(self):
137 137 return self.settings.get('base_url', '/')
138 138
139 @property
140 def base_kernel_url(self):
141 return self.settings.get('base_kernel_url', '/')
142
143 139 #---------------------------------------------------------------
144 140 # Manager objects
145 141 #---------------------------------------------------------------
146 142
147 143 @property
148 144 def kernel_manager(self):
149 145 return self.settings['kernel_manager']
150 146
151 147 @property
152 148 def notebook_manager(self):
153 149 return self.settings['notebook_manager']
154 150
155 151 @property
156 152 def cluster_manager(self):
157 153 return self.settings['cluster_manager']
158 154
159 155 @property
160 156 def session_manager(self):
161 157 return self.settings['session_manager']
162 158
163 159 @property
164 160 def project_dir(self):
165 161 return self.notebook_manager.notebook_dir
166 162
167 163 #---------------------------------------------------------------
168 164 # template rendering
169 165 #---------------------------------------------------------------
170 166
171 167 def get_template(self, name):
172 168 """Return the jinja template object for a given name"""
173 169 return self.settings['jinja2_env'].get_template(name)
174 170
175 171 def render_template(self, name, **ns):
176 172 ns.update(self.template_namespace)
177 173 template = self.get_template(name)
178 174 return template.render(**ns)
179 175
180 176 @property
181 177 def template_namespace(self):
182 178 return dict(
183 179 base_url=self.base_url,
184 base_kernel_url=self.base_kernel_url,
185 180 logged_in=self.logged_in,
186 181 login_available=self.login_available,
187 182 static_url=self.static_url,
188 183 )
189 184
190 185 def get_json_body(self):
191 186 """Return the body of the request as JSON data."""
192 187 if not self.request.body:
193 188 return None
194 189 # Do we need to call body.decode('utf-8') here?
195 190 body = self.request.body.strip().decode(u'utf-8')
196 191 try:
197 192 model = json.loads(body)
198 193 except Exception:
199 194 self.log.debug("Bad JSON: %r", body)
200 195 self.log.error("Couldn't parse JSON", exc_info=True)
201 196 raise web.HTTPError(400, u'Invalid JSON in body of request')
202 197 return model
203 198
204 199 def get_error_html(self, status_code, **kwargs):
205 200 """render custom error pages"""
206 201 exception = kwargs.get('exception')
207 202 message = ''
208 203 status_message = responses.get(status_code, 'Unknown HTTP Error')
209 204 if exception:
210 205 # get the custom message, if defined
211 206 try:
212 207 message = exception.log_message % exception.args
213 208 except Exception:
214 209 pass
215 210
216 211 # construct the custom reason, if defined
217 212 reason = getattr(exception, 'reason', '')
218 213 if reason:
219 214 status_message = reason
220 215
221 216 # build template namespace
222 217 ns = dict(
223 218 status_code=status_code,
224 219 status_message=status_message,
225 220 message=message,
226 221 exception=exception,
227 222 )
228 223
229 224 # render the template
230 225 try:
231 226 html = self.render_template('%s.html' % status_code, **ns)
232 227 except TemplateNotFound:
233 228 self.log.debug("No template for %d", status_code)
234 229 html = self.render_template('error.html', **ns)
235 230 return html
236 231
237 232
238 233 class Template404(IPythonHandler):
239 234 """Render our 404 template"""
240 235 def prepare(self):
241 236 raise web.HTTPError(404)
242 237
243 238
244 239 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
245 240 """static files should only be accessible when logged in"""
246 241
247 242 @web.authenticated
248 243 def get(self, path):
249 244 if os.path.splitext(path)[1] == '.ipynb':
250 245 name = os.path.basename(path)
251 246 self.set_header('Content-Type', 'application/json')
252 247 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
253 248
254 249 return web.StaticFileHandler.get(self, path)
255 250
256 251 def compute_etag(self):
257 252 return None
258 253
259 254 def validate_absolute_path(self, root, absolute_path):
260 255 """Validate and return the absolute path.
261 256
262 257 Requires tornado 3.1
263 258
264 259 Adding to tornado's own handling, forbids the serving of hidden files.
265 260 """
266 261 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
267 262 abs_root = os.path.abspath(root)
268 263 if is_hidden(abs_path, abs_root):
269 264 raise web.HTTPError(404)
270 265 return abs_path
271 266
272 267
273 268 def json_errors(method):
274 269 """Decorate methods with this to return GitHub style JSON errors.
275 270
276 271 This should be used on any JSON API on any handler method that can raise HTTPErrors.
277 272
278 273 This will grab the latest HTTPError exception using sys.exc_info
279 274 and then:
280 275
281 276 1. Set the HTTP status code based on the HTTPError
282 277 2. Create and return a JSON body with a message field describing
283 278 the error in a human readable form.
284 279 """
285 280 @functools.wraps(method)
286 281 def wrapper(self, *args, **kwargs):
287 282 try:
288 283 result = method(self, *args, **kwargs)
289 284 except web.HTTPError as e:
290 285 status = e.status_code
291 286 message = e.log_message
292 287 self.set_status(e.status_code)
293 288 self.finish(json.dumps(dict(message=message)))
294 289 except Exception:
295 290 self.log.error("Unhandled error in API request", exc_info=True)
296 291 status = 500
297 292 message = "Unknown server error"
298 293 t, value, tb = sys.exc_info()
299 294 self.set_status(status)
300 295 tb_text = ''.join(traceback.format_exception(t, value, tb))
301 296 reply = dict(message=message, traceback=tb_text)
302 297 self.finish(json.dumps(reply))
303 298 else:
304 299 return result
305 300 return wrapper
306 301
307 302
308 303
309 304 #-----------------------------------------------------------------------------
310 305 # File handler
311 306 #-----------------------------------------------------------------------------
312 307
313 308 # to minimize subclass changes:
314 309 HTTPError = web.HTTPError
315 310
316 311 class FileFindHandler(web.StaticFileHandler):
317 312 """subclass of StaticFileHandler for serving files from a search path"""
318 313
319 314 # cache search results, don't search for files more than once
320 315 _static_paths = {}
321 316
322 317 def initialize(self, path, default_filename=None):
323 318 if isinstance(path, string_types):
324 319 path = [path]
325 320
326 321 self.root = tuple(
327 322 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
328 323 )
329 324 self.default_filename = default_filename
330 325
331 326 def compute_etag(self):
332 327 return None
333 328
334 329 @classmethod
335 330 def get_absolute_path(cls, roots, path):
336 331 """locate a file to serve on our static file search path"""
337 332 with cls._lock:
338 333 if path in cls._static_paths:
339 334 return cls._static_paths[path]
340 335 try:
341 336 abspath = os.path.abspath(filefind(path, roots))
342 337 except IOError:
343 338 # IOError means not found
344 339 return ''
345 340
346 341 cls._static_paths[path] = abspath
347 342 return abspath
348 343
349 344 def validate_absolute_path(self, root, absolute_path):
350 345 """check if the file should be served (raises 404, 403, etc.)"""
351 346 if absolute_path == '':
352 347 raise web.HTTPError(404)
353 348
354 349 for root in self.root:
355 350 if (absolute_path + os.sep).startswith(root):
356 351 break
357 352
358 353 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
359 354
360 355
361 356 class TrailingSlashHandler(web.RequestHandler):
362 357 """Simple redirect handler that strips trailing slashes
363 358
364 359 This should be the first, highest priority handler.
365 360 """
366 361
367 362 SUPPORTED_METHODS = ['GET']
368 363
369 364 def get(self):
370 365 self.redirect(self.request.uri.rstrip('/'))
371 366
372 367 #-----------------------------------------------------------------------------
373 368 # URL pattern fragments for re-use
374 369 #-----------------------------------------------------------------------------
375 370
376 371 path_regex = r"(?P<path>(?:/.*)*)"
377 372 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
378 373 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
379 374
380 375 #-----------------------------------------------------------------------------
381 376 # URL to handler mappings
382 377 #-----------------------------------------------------------------------------
383 378
384 379
385 380 default_handlers = [
386 381 (r".*/", TrailingSlashHandler)
387 382 ]
@@ -1,842 +1,829 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 27 import select
28 28 import signal
29 29 import socket
30 30 import sys
31 31 import threading
32 32 import time
33 33 import webbrowser
34 34
35 35
36 36 # Third party
37 37 # check for pyzmq 2.1.11
38 38 from IPython.utils.zmqrelated import check_for_zmq
39 39 check_for_zmq('2.1.11', 'IPython.html')
40 40
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 3.1.0
49 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (3,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 66 from .base.handlers import Template404
67 67 from .log import log_request
68 68 from .services.kernels.kernelmanager import MappingKernelManager
69 69 from .services.notebooks.nbmanager import NotebookManager
70 70 from .services.notebooks.filenbmanager import FileNotebookManager
71 71 from .services.clusters.clustermanager import ClusterManager
72 72 from .services.sessions.sessionmanager import SessionManager
73 73
74 74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 75
76 76 from IPython.config.application import catch_config_error, boolean_flag
77 77 from IPython.core.application import BaseIPythonApplication
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.consoleapp import IPythonConsoleApp
80 80 from IPython.kernel import swallow_argv
81 81 from IPython.kernel.zmq.session import default_secure
82 82 from IPython.kernel.zmq.kernelapp import (
83 83 kernel_flags,
84 84 kernel_aliases,
85 85 )
86 86 from IPython.utils.importstring import import_item
87 87 from IPython.utils.localinterfaces import localhost
88 88 from IPython.utils import submodule
89 89 from IPython.utils.traitlets import (
90 90 Dict, Unicode, Integer, List, Bool, Bytes,
91 91 DottedObjectName
92 92 )
93 93 from IPython.utils import py3compat
94 94 from IPython.utils.path import filefind, get_ipython_dir
95 95
96 96 from .utils import url_path_join
97 97
98 98 #-----------------------------------------------------------------------------
99 99 # Module globals
100 100 #-----------------------------------------------------------------------------
101 101
102 102 _examples = """
103 103 ipython notebook # start the notebook
104 104 ipython notebook --profile=sympy # use the sympy profile
105 105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 106 """
107 107
108 108 #-----------------------------------------------------------------------------
109 109 # Helper functions
110 110 #-----------------------------------------------------------------------------
111 111
112 112 def random_ports(port, n):
113 113 """Generate a list of n random ports near the given port.
114 114
115 115 The first 5 ports will be sequential, and the remaining n-5 will be
116 116 randomly selected in the range [port-2*n, port+2*n].
117 117 """
118 118 for i in range(min(5, n)):
119 119 yield port + i
120 120 for i in range(n-5):
121 121 yield max(1, port + random.randint(-2*n, 2*n))
122 122
123 123 def load_handlers(name):
124 124 """Load the (URL pattern, handler) tuples for each component."""
125 125 name = 'IPython.html.' + name
126 126 mod = __import__(name, fromlist=['default_handlers'])
127 127 return mod.default_handlers
128 128
129 129 #-----------------------------------------------------------------------------
130 130 # The Tornado web application
131 131 #-----------------------------------------------------------------------------
132 132
133 133 class NotebookWebApplication(web.Application):
134 134
135 135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 136 cluster_manager, session_manager, log, base_url,
137 137 settings_overrides):
138 138
139 139 settings = self.init_settings(
140 140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 141 session_manager, log, base_url, settings_overrides)
142 142 handlers = self.init_handlers(settings)
143 143
144 144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 145
146 146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 147 cluster_manager, session_manager, log, base_url,
148 148 settings_overrides):
149 149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 150 # base_url will always be unicode, which will in turn
151 151 # make the patterns unicode, and ultimately result in unicode
152 152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 153 # This enforces that base_url be ascii in that situation.
154 154 #
155 155 # Note that the URLs these patterns check against are escaped,
156 156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 159 settings = dict(
160 160 # basics
161 161 log_function=log_request,
162 162 base_url=base_url,
163 base_kernel_url=ipython_app.base_kernel_url,
164 163 template_path=template_path,
165 164 static_path=ipython_app.static_file_path,
166 165 static_handler_class = FileFindHandler,
167 166 static_url_prefix = url_path_join(base_url,'/static/'),
168 167
169 168 # authentication
170 169 cookie_secret=ipython_app.cookie_secret,
171 170 login_url=url_path_join(base_url,'/login'),
172 171 password=ipython_app.password,
173 172
174 173 # managers
175 174 kernel_manager=kernel_manager,
176 175 notebook_manager=notebook_manager,
177 176 cluster_manager=cluster_manager,
178 177 session_manager=session_manager,
179 178
180 179 # IPython stuff
181 180 nbextensions_path = ipython_app.nbextensions_path,
182 181 mathjax_url=ipython_app.mathjax_url,
183 182 config=ipython_app.config,
184 183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
185 184 )
186 185
187 186 # allow custom overrides for the tornado web app.
188 187 settings.update(settings_overrides)
189 188 return settings
190 189
191 190 def init_handlers(self, settings):
192 191 # Load the (URL pattern, handler) tuples for each component.
193 192 handlers = []
194 193 handlers.extend(load_handlers('base.handlers'))
195 194 handlers.extend(load_handlers('tree.handlers'))
196 195 handlers.extend(load_handlers('auth.login'))
197 196 handlers.extend(load_handlers('auth.logout'))
198 197 handlers.extend(load_handlers('notebook.handlers'))
199 198 handlers.extend(load_handlers('nbconvert.handlers'))
200 199 handlers.extend(load_handlers('services.kernels.handlers'))
201 200 handlers.extend(load_handlers('services.notebooks.handlers'))
202 201 handlers.extend(load_handlers('services.clusters.handlers'))
203 202 handlers.extend(load_handlers('services.sessions.handlers'))
204 203 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 204 handlers.extend([
206 205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
207 206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 207 ])
209 208 # prepend base_url onto the patterns that we match
210 209 new_handlers = []
211 210 for handler in handlers:
212 211 pattern = url_path_join(settings['base_url'], handler[0])
213 212 new_handler = tuple([pattern] + list(handler[1:]))
214 213 new_handlers.append(new_handler)
215 214 # add 404 on the end, which will catch everything that falls through
216 215 new_handlers.append((r'(.*)', Template404))
217 216 return new_handlers
218 217
219 218
220 219 class NbserverListApp(BaseIPythonApplication):
221 220
222 221 description="List currently running notebook servers in this profile."
223 222
224 223 flags = dict(
225 224 json=({'NbserverListApp': {'json': True}},
226 225 "Produce machine-readable JSON output."),
227 226 )
228 227
229 228 json = Bool(False, config=True,
230 229 help="If True, each line of output will be a JSON object with the "
231 230 "details from the server info file.")
232 231
233 232 def start(self):
234 233 if not self.json:
235 234 print("Currently running servers:")
236 235 for serverinfo in list_running_servers(self.profile):
237 236 if self.json:
238 237 print(json.dumps(serverinfo))
239 238 else:
240 239 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
241 240
242 241 #-----------------------------------------------------------------------------
243 242 # Aliases and Flags
244 243 #-----------------------------------------------------------------------------
245 244
246 245 flags = dict(kernel_flags)
247 246 flags['no-browser']=(
248 247 {'NotebookApp' : {'open_browser' : False}},
249 248 "Don't open the notebook in a browser after startup."
250 249 )
251 250 flags['no-mathjax']=(
252 251 {'NotebookApp' : {'enable_mathjax' : False}},
253 252 """Disable MathJax
254 253
255 254 MathJax is the javascript library IPython uses to render math/LaTeX. It is
256 255 very large, so you may want to disable it if you have a slow internet
257 256 connection, or for offline use of the notebook.
258 257
259 258 When disabled, equations etc. will appear as their untransformed TeX source.
260 259 """
261 260 )
262 261
263 262 # Add notebook manager flags
264 263 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
265 264 'Auto-save a .py script everytime the .ipynb notebook is saved',
266 265 'Do not auto-save .py scripts for every notebook'))
267 266
268 267 # the flags that are specific to the frontend
269 268 # these must be scrubbed before being passed to the kernel,
270 269 # or it will raise an error on unrecognized flags
271 270 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
272 271
273 272 aliases = dict(kernel_aliases)
274 273
275 274 aliases.update({
276 275 'ip': 'NotebookApp.ip',
277 276 'port': 'NotebookApp.port',
278 277 'port-retries': 'NotebookApp.port_retries',
279 278 'transport': 'KernelManager.transport',
280 279 'keyfile': 'NotebookApp.keyfile',
281 280 'certfile': 'NotebookApp.certfile',
282 281 'notebook-dir': 'NotebookManager.notebook_dir',
283 282 'browser': 'NotebookApp.browser',
284 283 })
285 284
286 285 # remove ipkernel flags that are singletons, and don't make sense in
287 286 # multi-kernel evironment:
288 287 aliases.pop('f', None)
289 288
290 289 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
291 290 u'notebook-dir', u'profile', u'profile-dir']
292 291
293 292 #-----------------------------------------------------------------------------
294 293 # NotebookApp
295 294 #-----------------------------------------------------------------------------
296 295
297 296 class NotebookApp(BaseIPythonApplication):
298 297
299 298 name = 'ipython-notebook'
300 299
301 300 description = """
302 301 The IPython HTML Notebook.
303 302
304 303 This launches a Tornado based HTML Notebook Server that serves up an
305 304 HTML5/Javascript Notebook client.
306 305 """
307 306 examples = _examples
308 307
309 308 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
310 309 FileNotebookManager]
311 310 flags = Dict(flags)
312 311 aliases = Dict(aliases)
313 312
314 313 subcommands = dict(
315 314 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
316 315 )
317 316
318 317 kernel_argv = List(Unicode)
319 318
320 319 def _log_level_default(self):
321 320 return logging.INFO
322 321
323 322 def _log_format_default(self):
324 323 """override default log format to include time"""
325 324 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
326 325
327 326 # create requested profiles by default, if they don't exist:
328 327 auto_create = Bool(True)
329 328
330 329 # file to be opened in the notebook server
331 330 file_to_run = Unicode('')
332 331
333 332 # Network related information.
334 333
335 334 ip = Unicode(config=True,
336 335 help="The IP address the notebook server will listen on."
337 336 )
338 337 def _ip_default(self):
339 338 return localhost()
340 339
341 340 def _ip_changed(self, name, old, new):
342 341 if new == u'*': self.ip = u''
343 342
344 343 port = Integer(8888, config=True,
345 344 help="The port the notebook server will listen on."
346 345 )
347 346 port_retries = Integer(50, config=True,
348 347 help="The number of additional ports to try if the specified port is not available."
349 348 )
350 349
351 350 certfile = Unicode(u'', config=True,
352 351 help="""The full path to an SSL/TLS certificate file."""
353 352 )
354 353
355 354 keyfile = Unicode(u'', config=True,
356 355 help="""The full path to a private key file for usage with SSL/TLS."""
357 356 )
358 357
359 358 cookie_secret = Bytes(b'', config=True,
360 359 help="""The random bytes used to secure cookies.
361 360 By default this is a new random number every time you start the Notebook.
362 361 Set it to a value in a config file to enable logins to persist across server sessions.
363 362
364 363 Note: Cookie secrets should be kept private, do not share config files with
365 364 cookie_secret stored in plaintext (you can read the value from a file).
366 365 """
367 366 )
368 367 def _cookie_secret_default(self):
369 368 return os.urandom(1024)
370 369
371 370 password = Unicode(u'', config=True,
372 371 help="""Hashed password to use for web authentication.
373 372
374 373 To generate, type in a python/IPython shell:
375 374
376 375 from IPython.lib import passwd; passwd()
377 376
378 377 The string should be of the form type:salt:hashed-password.
379 378 """
380 379 )
381 380
382 381 open_browser = Bool(True, config=True,
383 382 help="""Whether to open in a browser after starting.
384 383 The specific browser used is platform dependent and
385 384 determined by the python standard library `webbrowser`
386 385 module, unless it is overridden using the --browser
387 386 (NotebookApp.browser) configuration option.
388 387 """)
389 388
390 389 browser = Unicode(u'', config=True,
391 390 help="""Specify what command to use to invoke a web
392 391 browser when opening the notebook. If not specified, the
393 392 default browser will be determined by the `webbrowser`
394 393 standard library module, which allows setting of the
395 394 BROWSER environment variable to override it.
396 395 """)
397 396
398 397 webapp_settings = Dict(config=True,
399 398 help="Supply overrides for the tornado.web.Application that the "
400 399 "IPython notebook uses.")
401 400
402 401 enable_mathjax = Bool(True, config=True,
403 402 help="""Whether to enable MathJax for typesetting math/TeX
404 403
405 404 MathJax is the javascript library IPython uses to render math/LaTeX. It is
406 405 very large, so you may want to disable it if you have a slow internet
407 406 connection, or for offline use of the notebook.
408 407
409 408 When disabled, equations etc. will appear as their untransformed TeX source.
410 409 """
411 410 )
412 411 def _enable_mathjax_changed(self, name, old, new):
413 412 """set mathjax url to empty if mathjax is disabled"""
414 413 if not new:
415 414 self.mathjax_url = u''
416 415
417 416 base_url = Unicode('/', config=True,
418 417 help='''The base URL for the notebook server.
419 418
420 419 Leading and trailing slashes can be omitted,
421 420 and will automatically be added.
422 421 ''')
423 422 def _base_url_changed(self, name, old, new):
424 423 if not new.startswith('/'):
425 424 self.base_url = '/'+new
426 425 elif not new.endswith('/'):
427 426 self.base_url = new+'/'
428 427
429 428 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
430 429 def _base_project_url_changed(self, name, old, new):
431 430 self.log.warn("base_project_url is deprecated, use base_url")
432 431 self.base_url = new
433 432
434 base_kernel_url = Unicode('/', config=True,
435 help='''The base URL for the kernel server
436
437 Leading and trailing slashes can be omitted,
438 and will automatically be added.
439 ''')
440 def _base_kernel_url_changed(self, name, old, new):
441 if not new.startswith('/'):
442 self.base_kernel_url = '/'+new
443 elif not new.endswith('/'):
444 self.base_kernel_url = new+'/'
445
446 433 websocket_url = Unicode("", config=True,
447 434 help="""The base URL for the websocket server,
448 435 if it differs from the HTTP server (hint: it almost certainly doesn't).
449 436
450 437 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
451 438 """
452 439 )
453 440
454 441 extra_static_paths = List(Unicode, config=True,
455 442 help="""Extra paths to search for serving static files.
456 443
457 444 This allows adding javascript/css to be available from the notebook server machine,
458 445 or overriding individual files in the IPython"""
459 446 )
460 447 def _extra_static_paths_default(self):
461 448 return [os.path.join(self.profile_dir.location, 'static')]
462 449
463 450 @property
464 451 def static_file_path(self):
465 452 """return extra paths + the default location"""
466 453 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
467 454
468 455 nbextensions_path = List(Unicode, config=True,
469 456 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
470 457 )
471 458 def _nbextensions_path_default(self):
472 459 return [os.path.join(get_ipython_dir(), 'nbextensions')]
473 460
474 461 mathjax_url = Unicode("", config=True,
475 462 help="""The url for MathJax.js."""
476 463 )
477 464 def _mathjax_url_default(self):
478 465 if not self.enable_mathjax:
479 466 return u''
480 467 static_url_prefix = self.webapp_settings.get("static_url_prefix",
481 468 url_path_join(self.base_url, "static")
482 469 )
483 470
484 471 # try local mathjax, either in nbextensions/mathjax or static/mathjax
485 472 for (url_prefix, search_path) in [
486 473 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
487 474 (static_url_prefix, self.static_file_path),
488 475 ]:
489 476 self.log.debug("searching for local mathjax in %s", search_path)
490 477 try:
491 478 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
492 479 except IOError:
493 480 continue
494 481 else:
495 482 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
496 483 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
497 484 return url
498 485
499 486 # no local mathjax, serve from CDN
500 487 if self.certfile:
501 488 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
502 489 host = u"https://c328740.ssl.cf1.rackcdn.com"
503 490 else:
504 491 host = u"http://cdn.mathjax.org"
505 492
506 493 url = host + u"/mathjax/latest/MathJax.js"
507 494 self.log.info("Using MathJax from CDN: %s", url)
508 495 return url
509 496
510 497 def _mathjax_url_changed(self, name, old, new):
511 498 if new and not self.enable_mathjax:
512 499 # enable_mathjax=False overrides mathjax_url
513 500 self.mathjax_url = u''
514 501 else:
515 502 self.log.info("Using MathJax: %s", new)
516 503
517 504 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
518 505 config=True,
519 506 help='The notebook manager class to use.')
520 507
521 508 trust_xheaders = Bool(False, config=True,
522 509 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
523 510 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
524 511 )
525 512
526 513 info_file = Unicode()
527 514
528 515 def _info_file_default(self):
529 516 info_file = "nbserver-%s.json"%os.getpid()
530 517 return os.path.join(self.profile_dir.security_dir, info_file)
531 518
532 519 def parse_command_line(self, argv=None):
533 520 super(NotebookApp, self).parse_command_line(argv)
534 521
535 522 if self.extra_args:
536 523 arg0 = self.extra_args[0]
537 524 f = os.path.abspath(arg0)
538 525 self.argv.remove(arg0)
539 526 if not os.path.exists(f):
540 527 self.log.critical("No such file or directory: %s", f)
541 528 self.exit(1)
542 529 if os.path.isdir(f):
543 530 self.config.FileNotebookManager.notebook_dir = f
544 531 elif os.path.isfile(f):
545 532 self.file_to_run = f
546 533
547 534 def init_kernel_argv(self):
548 535 """construct the kernel arguments"""
549 536 # Scrub frontend-specific flags
550 537 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
551 538 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
552 539 self.log.warn('\n '.join([
553 540 "Starting all kernels in pylab mode is not recommended,",
554 541 "and will be disabled in a future release.",
555 542 "Please use the %matplotlib magic to enable matplotlib instead.",
556 543 "pylab implies many imports, which can have confusing side effects",
557 544 "and harm the reproducibility of your notebooks.",
558 545 ]))
559 546 # Kernel should inherit default config file from frontend
560 547 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
561 548 # Kernel should get *absolute* path to profile directory
562 549 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
563 550
564 551 def init_configurables(self):
565 552 # force Session default to be secure
566 553 default_secure(self.config)
567 554 self.kernel_manager = MappingKernelManager(
568 555 parent=self, log=self.log, kernel_argv=self.kernel_argv,
569 556 connection_dir = self.profile_dir.security_dir,
570 557 )
571 558 kls = import_item(self.notebook_manager_class)
572 559 self.notebook_manager = kls(parent=self, log=self.log)
573 560 self.session_manager = SessionManager(parent=self, log=self.log)
574 561 self.cluster_manager = ClusterManager(parent=self, log=self.log)
575 562 self.cluster_manager.update_profiles()
576 563
577 564 def init_logging(self):
578 565 # This prevents double log messages because tornado use a root logger that
579 566 # self.log is a child of. The logging module dipatches log messages to a log
580 567 # and all of its ancenstors until propagate is set to False.
581 568 self.log.propagate = False
582 569
583 570 # hook up tornado 3's loggers to our app handlers
584 571 for name in ('access', 'application', 'general'):
585 572 logger = logging.getLogger('tornado.%s' % name)
586 573 logger.parent = self.log
587 574 logger.setLevel(self.log.level)
588 575
589 576 def init_webapp(self):
590 577 """initialize tornado webapp and httpserver"""
591 578 self.web_app = NotebookWebApplication(
592 579 self, self.kernel_manager, self.notebook_manager,
593 580 self.cluster_manager, self.session_manager,
594 581 self.log, self.base_url, self.webapp_settings
595 582 )
596 583 if self.certfile:
597 584 ssl_options = dict(certfile=self.certfile)
598 585 if self.keyfile:
599 586 ssl_options['keyfile'] = self.keyfile
600 587 else:
601 588 ssl_options = None
602 589 self.web_app.password = self.password
603 590 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
604 591 xheaders=self.trust_xheaders)
605 592 if not self.ip:
606 593 warning = "WARNING: The notebook server is listening on all IP addresses"
607 594 if ssl_options is None:
608 595 self.log.critical(warning + " and not using encryption. This "
609 596 "is not recommended.")
610 597 if not self.password:
611 598 self.log.critical(warning + " and not using authentication. "
612 599 "This is highly insecure and not recommended.")
613 600 success = None
614 601 for port in random_ports(self.port, self.port_retries+1):
615 602 try:
616 603 self.http_server.listen(port, self.ip)
617 604 except socket.error as e:
618 605 if e.errno == errno.EADDRINUSE:
619 606 self.log.info('The port %i is already in use, trying another random port.' % port)
620 607 continue
621 608 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
622 609 self.log.warn("Permission to listen on port %i denied" % port)
623 610 continue
624 611 else:
625 612 raise
626 613 else:
627 614 self.port = port
628 615 success = True
629 616 break
630 617 if not success:
631 618 self.log.critical('ERROR: the notebook server could not be started because '
632 619 'no available port could be found.')
633 620 self.exit(1)
634 621
635 622 @property
636 623 def display_url(self):
637 624 ip = self.ip if self.ip else '[all ip addresses on your system]'
638 625 return self._url(ip)
639 626
640 627 @property
641 628 def connection_url(self):
642 629 ip = self.ip if self.ip else localhost()
643 630 return self._url(ip)
644 631
645 632 def _url(self, ip):
646 633 proto = 'https' if self.certfile else 'http'
647 634 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
648 635
649 636 def init_signal(self):
650 637 if not sys.platform.startswith('win'):
651 638 signal.signal(signal.SIGINT, self._handle_sigint)
652 639 signal.signal(signal.SIGTERM, self._signal_stop)
653 640 if hasattr(signal, 'SIGUSR1'):
654 641 # Windows doesn't support SIGUSR1
655 642 signal.signal(signal.SIGUSR1, self._signal_info)
656 643 if hasattr(signal, 'SIGINFO'):
657 644 # only on BSD-based systems
658 645 signal.signal(signal.SIGINFO, self._signal_info)
659 646
660 647 def _handle_sigint(self, sig, frame):
661 648 """SIGINT handler spawns confirmation dialog"""
662 649 # register more forceful signal handler for ^C^C case
663 650 signal.signal(signal.SIGINT, self._signal_stop)
664 651 # request confirmation dialog in bg thread, to avoid
665 652 # blocking the App
666 653 thread = threading.Thread(target=self._confirm_exit)
667 654 thread.daemon = True
668 655 thread.start()
669 656
670 657 def _restore_sigint_handler(self):
671 658 """callback for restoring original SIGINT handler"""
672 659 signal.signal(signal.SIGINT, self._handle_sigint)
673 660
674 661 def _confirm_exit(self):
675 662 """confirm shutdown on ^C
676 663
677 664 A second ^C, or answering 'y' within 5s will cause shutdown,
678 665 otherwise original SIGINT handler will be restored.
679 666
680 667 This doesn't work on Windows.
681 668 """
682 669 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
683 670 time.sleep(0.1)
684 671 info = self.log.info
685 672 info('interrupted')
686 673 print(self.notebook_info())
687 674 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
688 675 sys.stdout.flush()
689 676 r,w,x = select.select([sys.stdin], [], [], 5)
690 677 if r:
691 678 line = sys.stdin.readline()
692 679 if line.lower().startswith('y'):
693 680 self.log.critical("Shutdown confirmed")
694 681 ioloop.IOLoop.instance().stop()
695 682 return
696 683 else:
697 684 print("No answer for 5s:", end=' ')
698 685 print("resuming operation...")
699 686 # no answer, or answer is no:
700 687 # set it back to original SIGINT handler
701 688 # use IOLoop.add_callback because signal.signal must be called
702 689 # from main thread
703 690 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
704 691
705 692 def _signal_stop(self, sig, frame):
706 693 self.log.critical("received signal %s, stopping", sig)
707 694 ioloop.IOLoop.instance().stop()
708 695
709 696 def _signal_info(self, sig, frame):
710 697 print(self.notebook_info())
711 698
712 699 def init_components(self):
713 700 """Check the components submodule, and warn if it's unclean"""
714 701 status = submodule.check_submodule_status()
715 702 if status == 'missing':
716 703 self.log.warn("components submodule missing, running `git submodule update`")
717 704 submodule.update_submodules(submodule.ipython_parent())
718 705 elif status == 'unclean':
719 706 self.log.warn("components submodule unclean, you may see 404s on static/components")
720 707 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
721 708
722 709 @catch_config_error
723 710 def initialize(self, argv=None):
724 711 super(NotebookApp, self).initialize(argv)
725 712 self.init_logging()
726 713 self.init_kernel_argv()
727 714 self.init_configurables()
728 715 self.init_components()
729 716 self.init_webapp()
730 717 self.init_signal()
731 718
732 719 def cleanup_kernels(self):
733 720 """Shutdown all kernels.
734 721
735 722 The kernels will shutdown themselves when this process no longer exists,
736 723 but explicit shutdown allows the KernelManagers to cleanup the connection files.
737 724 """
738 725 self.log.info('Shutting down kernels')
739 726 self.kernel_manager.shutdown_all()
740 727
741 728 def notebook_info(self):
742 729 "Return the current working directory and the server url information"
743 730 info = self.notebook_manager.info_string() + "\n"
744 731 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
745 732 return info + "The IPython Notebook is running at: %s" % self.display_url
746 733
747 734 def server_info(self):
748 735 """Return a JSONable dict of information about this server."""
749 736 return {'url': self.connection_url,
750 737 'hostname': self.ip if self.ip else 'localhost',
751 738 'port': self.port,
752 739 'secure': bool(self.certfile),
753 740 'base_url': self.base_url,
754 741 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
755 742 }
756 743
757 744 def write_server_info_file(self):
758 745 """Write the result of server_info() to the JSON file info_file."""
759 746 with open(self.info_file, 'w') as f:
760 747 json.dump(self.server_info(), f, indent=2)
761 748
762 749 def remove_server_info_file(self):
763 750 """Remove the nbserver-<pid>.json file created for this server.
764 751
765 752 Ignores the error raised when the file has already been removed.
766 753 """
767 754 try:
768 755 os.unlink(self.info_file)
769 756 except OSError as e:
770 757 if e.errno != errno.ENOENT:
771 758 raise
772 759
773 760 def start(self):
774 761 """ Start the IPython Notebook server app, after initialization
775 762
776 763 This method takes no arguments so all configuration and initialization
777 764 must be done prior to calling this method."""
778 765 if self.subapp is not None:
779 766 return self.subapp.start()
780 767
781 768 info = self.log.info
782 769 for line in self.notebook_info().split("\n"):
783 770 info(line)
784 771 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
785 772
786 773 self.write_server_info_file()
787 774
788 775 if self.open_browser or self.file_to_run:
789 776 try:
790 777 browser = webbrowser.get(self.browser or None)
791 778 except webbrowser.Error as e:
792 779 self.log.warn('No web browser found: %s.' % e)
793 780 browser = None
794 781
795 782 f = self.file_to_run
796 783 if f:
797 784 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
798 785 if f.startswith(nbdir):
799 786 f = f[len(nbdir):]
800 787 else:
801 788 self.log.warn(
802 789 "Probably won't be able to open notebook %s "
803 790 "because it is not in notebook_dir %s",
804 791 f, nbdir,
805 792 )
806 793
807 794 if os.path.isfile(self.file_to_run):
808 795 url = url_path_join('notebooks', f)
809 796 else:
810 797 url = url_path_join('tree', f)
811 798 if browser:
812 799 b = lambda : browser.open("%s%s" % (self.connection_url, url),
813 800 new=2)
814 801 threading.Thread(target=b).start()
815 802 try:
816 803 ioloop.IOLoop.instance().start()
817 804 except KeyboardInterrupt:
818 805 info("Interrupted...")
819 806 finally:
820 807 self.cleanup_kernels()
821 808 self.remove_server_info_file()
822 809
823 810
824 811 def list_running_servers(profile='default'):
825 812 """Iterate over the server info files of running notebook servers.
826 813
827 814 Given a profile name, find nbserver-* files in the security directory of
828 815 that profile, and yield dicts of their information, each one pertaining to
829 816 a currently running notebook server instance.
830 817 """
831 818 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
832 819 for file in os.listdir(pd.security_dir):
833 820 if file.startswith('nbserver-'):
834 821 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
835 822 yield json.load(f)
836 823
837 824 #-----------------------------------------------------------------------------
838 825 # Main entry point
839 826 #-----------------------------------------------------------------------------
840 827
841 828 launch_new_instance = NotebookApp.launch_instance
842 829
@@ -1,195 +1,195 b''
1 1 """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) 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 import logging
20 20 from tornado import web
21 21
22 22 from zmq.utils import jsonapi
23 23
24 24 from IPython.utils.jsonutil import date_default
25 25 from IPython.html.utils import url_path_join, url_escape
26 26
27 27 from ...base.handlers import IPythonHandler, json_errors
28 28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Kernel handlers
32 32 #-----------------------------------------------------------------------------
33 33
34 34
35 35 class MainKernelHandler(IPythonHandler):
36 36
37 37 @web.authenticated
38 38 @json_errors
39 39 def get(self):
40 40 km = self.kernel_manager
41 41 self.finish(jsonapi.dumps(km.list_kernels(self.ws_url)))
42 42
43 43 @web.authenticated
44 44 @json_errors
45 45 def post(self):
46 46 km = self.kernel_manager
47 47 kernel_id = km.start_kernel()
48 48 model = km.kernel_model(kernel_id, self.ws_url)
49 location = url_path_join(self.base_kernel_url, 'api', 'kernels', kernel_id)
49 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
50 50 self.set_header('Location', url_escape(location))
51 51 self.set_status(201)
52 52 self.finish(jsonapi.dumps(model))
53 53
54 54
55 55 class KernelHandler(IPythonHandler):
56 56
57 57 SUPPORTED_METHODS = ('DELETE', 'GET')
58 58
59 59 @web.authenticated
60 60 @json_errors
61 61 def get(self, kernel_id):
62 62 km = self.kernel_manager
63 63 km._check_kernel_id(kernel_id)
64 64 model = km.kernel_model(kernel_id, self.ws_url)
65 65 self.finish(jsonapi.dumps(model))
66 66
67 67 @web.authenticated
68 68 @json_errors
69 69 def delete(self, kernel_id):
70 70 km = self.kernel_manager
71 71 km.shutdown_kernel(kernel_id)
72 72 self.set_status(204)
73 73 self.finish()
74 74
75 75
76 76 class KernelActionHandler(IPythonHandler):
77 77
78 78 @web.authenticated
79 79 @json_errors
80 80 def post(self, kernel_id, action):
81 81 km = self.kernel_manager
82 82 if action == 'interrupt':
83 83 km.interrupt_kernel(kernel_id)
84 84 self.set_status(204)
85 85 if action == 'restart':
86 86 km.restart_kernel(kernel_id)
87 87 model = km.kernel_model(kernel_id, self.ws_url)
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
89 89 self.write(jsonapi.dumps(model))
90 90 self.finish()
91 91
92 92
93 93 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
94 94
95 95 def create_stream(self):
96 96 km = self.kernel_manager
97 97 meth = getattr(km, 'connect_%s' % self.channel)
98 98 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
99 99
100 100 def initialize(self, *args, **kwargs):
101 101 self.zmq_stream = None
102 102
103 103 def on_first_message(self, msg):
104 104 try:
105 105 super(ZMQChannelHandler, self).on_first_message(msg)
106 106 except web.HTTPError:
107 107 self.close()
108 108 return
109 109 try:
110 110 self.create_stream()
111 111 except web.HTTPError:
112 112 # WebSockets don't response to traditional error codes so we
113 113 # close the connection.
114 114 if not self.stream.closed():
115 115 self.stream.close()
116 116 self.close()
117 117 else:
118 118 self.zmq_stream.on_recv(self._on_zmq_reply)
119 119
120 120 def on_message(self, msg):
121 121 msg = jsonapi.loads(msg)
122 122 self.session.send(self.zmq_stream, msg)
123 123
124 124 def on_close(self):
125 125 # This method can be called twice, once by self.kernel_died and once
126 126 # from the WebSocket close event. If the WebSocket connection is
127 127 # closed before the ZMQ streams are setup, they could be None.
128 128 if self.zmq_stream is not None and not self.zmq_stream.closed():
129 129 self.zmq_stream.on_recv(None)
130 130 self.zmq_stream.close()
131 131
132 132
133 133 class IOPubHandler(ZMQChannelHandler):
134 134 channel = 'iopub'
135 135
136 136 def create_stream(self):
137 137 super(IOPubHandler, self).create_stream()
138 138 km = self.kernel_manager
139 139 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
140 140 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
141 141
142 142 def on_close(self):
143 143 km = self.kernel_manager
144 144 if self.kernel_id in km:
145 145 km.remove_restart_callback(
146 146 self.kernel_id, self.on_kernel_restarted,
147 147 )
148 148 km.remove_restart_callback(
149 149 self.kernel_id, self.on_restart_failed, 'dead',
150 150 )
151 151 super(IOPubHandler, self).on_close()
152 152
153 153 def _send_status_message(self, status):
154 154 msg = self.session.msg("status",
155 155 {'execution_state': status}
156 156 )
157 157 self.write_message(jsonapi.dumps(msg, default=date_default))
158 158
159 159 def on_kernel_restarted(self):
160 160 logging.warn("kernel %s restarted", self.kernel_id)
161 161 self._send_status_message('restarting')
162 162
163 163 def on_restart_failed(self):
164 164 logging.error("kernel %s restarted failed!", self.kernel_id)
165 165 self._send_status_message('dead')
166 166
167 167 def on_message(self, msg):
168 168 """IOPub messages make no sense"""
169 169 pass
170 170
171 171
172 172 class ShellHandler(ZMQChannelHandler):
173 173 channel = 'shell'
174 174
175 175
176 176 class StdinHandler(ZMQChannelHandler):
177 177 channel = 'stdin'
178 178
179 179
180 180 #-----------------------------------------------------------------------------
181 181 # URL to handler mappings
182 182 #-----------------------------------------------------------------------------
183 183
184 184
185 185 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
186 186 _kernel_action_regex = r"(?P<action>restart|interrupt)"
187 187
188 188 default_handlers = [
189 189 (r"/api/kernels", MainKernelHandler),
190 190 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
191 191 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
192 192 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
193 193 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
194 194 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
195 195 ]
@@ -1,127 +1,127 b''
1 1 """Tornado handlers for the sessions web service.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
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 import json
20 20
21 21 from tornado import web
22 22
23 23 from ...base.handlers import IPythonHandler, json_errors
24 24 from IPython.utils.jsonutil import date_default
25 25 from IPython.html.utils import url_path_join, url_escape
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Session web service handlers
29 29 #-----------------------------------------------------------------------------
30 30
31 31
32 32 class SessionRootHandler(IPythonHandler):
33 33
34 34 @web.authenticated
35 35 @json_errors
36 36 def get(self):
37 37 # Return a list of running sessions
38 38 sm = self.session_manager
39 39 sessions = sm.list_sessions()
40 40 self.finish(json.dumps(sessions, default=date_default))
41 41
42 42 @web.authenticated
43 43 @json_errors
44 44 def post(self):
45 45 # Creates a new session
46 46 #(unless a session already exists for the named nb)
47 47 sm = self.session_manager
48 48 nbm = self.notebook_manager
49 49 km = self.kernel_manager
50 50 model = self.get_json_body()
51 51 if model is None:
52 52 raise web.HTTPError(400, "No JSON data provided")
53 53 try:
54 54 name = model['notebook']['name']
55 55 except KeyError:
56 56 raise web.HTTPError(400, "Missing field in JSON data: name")
57 57 try:
58 58 path = model['notebook']['path']
59 59 except KeyError:
60 60 raise web.HTTPError(400, "Missing field in JSON data: path")
61 61 # Check to see if session exists
62 62 if sm.session_exists(name=name, path=path):
63 63 model = sm.get_session(name=name, path=path)
64 64 else:
65 65 kernel_id = km.start_kernel(cwd=nbm.get_os_path(path))
66 66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
67 location = url_path_join(self.base_kernel_url, 'api', 'sessions', model['id'])
67 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
68 68 self.set_header('Location', url_escape(location))
69 69 self.set_status(201)
70 70 self.finish(json.dumps(model, default=date_default))
71 71
72 72 class SessionHandler(IPythonHandler):
73 73
74 74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
75 75
76 76 @web.authenticated
77 77 @json_errors
78 78 def get(self, session_id):
79 79 # Returns the JSON model for a single session
80 80 sm = self.session_manager
81 81 model = sm.get_session(session_id=session_id)
82 82 self.finish(json.dumps(model, default=date_default))
83 83
84 84 @web.authenticated
85 85 @json_errors
86 86 def patch(self, session_id):
87 87 # Currently, this handler is strictly for renaming notebooks
88 88 sm = self.session_manager
89 89 model = self.get_json_body()
90 90 if model is None:
91 91 raise web.HTTPError(400, "No JSON data provided")
92 92 changes = {}
93 93 if 'notebook' in model:
94 94 notebook = model['notebook']
95 95 if 'name' in notebook:
96 96 changes['name'] = notebook['name']
97 97 if 'path' in notebook:
98 98 changes['path'] = notebook['path']
99 99
100 100 sm.update_session(session_id, **changes)
101 101 model = sm.get_session(session_id=session_id)
102 102 self.finish(json.dumps(model, default=date_default))
103 103
104 104 @web.authenticated
105 105 @json_errors
106 106 def delete(self, session_id):
107 107 # Deletes the session with given session_id
108 108 sm = self.session_manager
109 109 km = self.kernel_manager
110 110 session = sm.get_session(session_id=session_id)
111 111 sm.delete_session(session_id)
112 112 km.shutdown_kernel(session['kernel']['id'])
113 113 self.set_status(204)
114 114 self.finish()
115 115
116 116
117 117 #-----------------------------------------------------------------------------
118 118 # URL to handler mappings
119 119 #-----------------------------------------------------------------------------
120 120
121 121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122 122
123 123 default_handlers = [
124 124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 125 (r"/api/sessions", SessionRootHandler)
126 126 ]
127 127
@@ -1,122 +1,121 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 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 // On document ready
10 10 //============================================================================
11 11
12 12 // for the time beeing, we have to pass marked as a parameter here,
13 13 // as injecting require.js make marked not to put itself in the globals,
14 14 // which make both this file fail at setting marked configuration, and textcell.js
15 15 // which search marked into global.
16 16 require(['components/marked/lib/marked',
17 17 'notebook/js/widgets/init'],
18 18
19 19 function (marked) {
20 20 "use strict";
21 21
22 22 window.marked = marked;
23 23
24 24 // monkey patch CM to be able to syntax highlight cell magics
25 25 // bug reported upstream,
26 26 // see https://github.com/marijnh/CodeMirror2/issues/670
27 27 if(CodeMirror.getMode(1,'text/plain').indent === undefined ){
28 28 console.log('patching CM for undefined indent');
29 29 CodeMirror.modes.null = function() {
30 30 return {token: function(stream) {stream.skipToEnd();},indent : function(){return 0;}};
31 31 };
32 32 }
33 33
34 34 CodeMirror.patchedGetMode = function(config, mode){
35 35 var cmmode = CodeMirror.getMode(config, mode);
36 36 if(cmmode.indent === null) {
37 37 console.log('patch mode "' , mode, '" on the fly');
38 38 cmmode.indent = function(){return 0;};
39 39 }
40 40 return cmmode;
41 41 };
42 42 // end monkey patching CodeMirror
43 43
44 44 IPython.mathjaxutils.init();
45 45
46 46 $('#ipython-main-app').addClass('border-box-sizing');
47 47 $('div#notebook_panel').addClass('border-box-sizing');
48 48
49 49 var opts = {
50 50 base_url : IPython.utils.get_body_data("baseUrl"),
51 base_kernel_url : IPython.utils.get_body_data("baseKernelUrl"),
52 51 notebook_path : IPython.utils.get_body_data("notebookPath"),
53 52 notebook_name : IPython.utils.get_body_data('notebookName')
54 53 };
55 54
56 55 IPython.page = new IPython.Page();
57 56 IPython.layout_manager = new IPython.LayoutManager();
58 57 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
59 58 IPython.quick_help = new IPython.QuickHelp();
60 59 IPython.login_widget = new IPython.LoginWidget('span#login_widget', opts);
61 60 IPython.notebook = new IPython.Notebook('div#notebook', opts);
62 61 IPython.keyboard_manager = new IPython.KeyboardManager();
63 62 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
64 63 IPython.menubar = new IPython.MenuBar('#menubar', opts);
65 64 IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container');
66 65 IPython.tooltip = new IPython.Tooltip();
67 66 IPython.notification_area = new IPython.NotificationArea('#notification_area');
68 67 IPython.notification_area.init_notification_widgets();
69 68
70 69 IPython.layout_manager.do_resize();
71 70
72 71 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
73 72 '<span id="test2" style="font-weight: bold;">x</span>'+
74 73 '<span id="test3" style="font-style: italic;">x</span></pre></div>');
75 74 var nh = $('#test1').innerHeight();
76 75 var bh = $('#test2').innerHeight();
77 76 var ih = $('#test3').innerHeight();
78 77 if(nh != bh || nh != ih) {
79 78 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
80 79 }
81 80 $('#fonttest').remove();
82 81
83 82 IPython.page.show();
84 83
85 84 IPython.layout_manager.do_resize();
86 85 var first_load = function () {
87 86 IPython.layout_manager.do_resize();
88 87 var hash = document.location.hash;
89 88 if (hash) {
90 89 document.location.hash = '';
91 90 document.location.hash = hash;
92 91 }
93 92 IPython.notebook.set_autosave_interval(IPython.notebook.minimum_autosave_interval);
94 93 // only do this once
95 94 $([IPython.events]).off('notebook_loaded.Notebook', first_load);
96 95 };
97 96
98 97 $([IPython.events]).on('notebook_loaded.Notebook', first_load);
99 98 $([IPython.events]).trigger('app_initialized.NotebookApp');
100 99 IPython.notebook.load_notebook(opts.notebook_name, opts.notebook_path);
101 100
102 101 if (marked) {
103 102 marked.setOptions({
104 103 gfm : true,
105 104 tables: true,
106 105 langPrefix: "language-",
107 106 highlight: function(code, lang) {
108 107 if (!lang) {
109 108 // no language, no highlight
110 109 return code;
111 110 }
112 111 var highlighted;
113 112 try {
114 113 highlighted = hljs.highlight(lang, code, false);
115 114 } catch(err) {
116 115 highlighted = hljs.highlightAuto(code);
117 116 }
118 117 return highlighted.value;
119 118 }
120 119 });
121 120 }
122 121 });
@@ -1,119 +1,118 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 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 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16
17 17 var Session = function(notebook, options){
18 18 this.kernel = null;
19 19 this.id = null;
20 20 this.notebook = notebook;
21 21 this.name = notebook.notebook_name;
22 22 this.path = notebook.notebook_path;
23 23 this.base_url = notebook.base_url;
24 this.base_kernel_url = options.base_kernel_url || utils.get_body_data("baseKernelUrl");
25 24 };
26 25
27 26 Session.prototype.start = function(callback) {
28 27 var that = this;
29 28 var model = {
30 29 notebook : {
31 30 name : this.name,
32 31 path : this.path
33 32 }
34 33 };
35 34 var settings = {
36 35 processData : false,
37 36 cache : false,
38 37 type : "POST",
39 38 data: JSON.stringify(model),
40 39 dataType : "json",
41 40 success : function (data, status, xhr) {
42 41 that._handle_start_success(data);
43 42 if (callback) {
44 43 callback(data, status, xhr);
45 44 }
46 45 },
47 46 };
48 47 var url = utils.url_join_encode(this.base_url, 'api/sessions');
49 48 $.ajax(url, settings);
50 49 };
51 50
52 51 Session.prototype.rename_notebook = function (name, path) {
53 52 this.name = name;
54 53 this.path = path;
55 54 var model = {
56 55 notebook : {
57 56 name : this.name,
58 57 path : this.path
59 58 }
60 59 };
61 60 var settings = {
62 61 processData : false,
63 62 cache : false,
64 63 type : "PATCH",
65 64 data: JSON.stringify(model),
66 65 dataType : "json",
67 66 };
68 67 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
69 68 $.ajax(url, settings);
70 69 };
71 70
72 71 Session.prototype.delete = function() {
73 72 var settings = {
74 73 processData : false,
75 74 cache : false,
76 75 type : "DELETE",
77 76 dataType : "json",
78 77 };
79 78 this.kernel.running = false;
80 79 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
81 80 $.ajax(url, settings);
82 81 };
83 82
84 83 // Kernel related things
85 84 /**
86 85 * Create the Kernel object associated with this Session.
87 86 *
88 87 * @method _handle_start_success
89 88 */
90 89 Session.prototype._handle_start_success = function (data, status, xhr) {
91 90 this.id = data.id;
92 var kernel_service_url = utils.url_path_join(this.base_kernel_url, "api/kernels");
91 var kernel_service_url = utils.url_path_join(this.base_url, "api/kernels");
93 92 this.kernel = new IPython.Kernel(kernel_service_url);
94 93 this.kernel._kernel_started(data.kernel);
95 94 };
96 95
97 96 /**
98 97 * Prompt the user to restart the IPython kernel.
99 98 *
100 99 * @method restart_kernel
101 100 */
102 101 Session.prototype.restart_kernel = function () {
103 102 this.kernel.restart();
104 103 };
105 104
106 105 Session.prototype.interrupt_kernel = function() {
107 106 this.kernel.interrupt();
108 107 };
109 108
110 109
111 110 Session.prototype.kill_kernel = function() {
112 111 this.kernel.kill();
113 112 };
114 113
115 114 IPython.Session = Session;
116 115
117 116 return IPython;
118 117
119 118 }(IPython));
@@ -1,352 +1,351 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block stylesheet %}
4 4
5 5 {% if mathjax_url %}
6 6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 7 {% endif %}
8 8 <script type="text/javascript">
9 9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 10 // where it will be undefined, and should prompt a dialog later.
11 11 window.mathjax_url = "{{mathjax_url}}";
12 12 </script>
13 13
14 14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15 15
16 16 {{super()}}
17 17
18 18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19 19
20 20 {% endblock %}
21 21
22 22 {% block params %}
23 23
24 24 data-project="{{project}}"
25 25 data-base-url="{{base_url}}"
26 data-base-kernel-url="{{base_kernel_url}}"
27 26 data-notebook-name="{{notebook_name}}"
28 27 data-notebook-path="{{notebook_path}}"
29 28 class="notebook_app"
30 29
31 30 {% endblock %}
32 31
33 32
34 33 {% block header %}
35 34
36 35 <span id="save_widget" class="nav pull-left">
37 36 <span id="notebook_name"></span>
38 37 <span id="checkpoint_status"></span>
39 38 <span id="autosave_status"></span>
40 39 </span>
41 40
42 41 {% endblock %}
43 42
44 43
45 44 {% block site %}
46 45
47 46 <div id="menubar-container" class="container">
48 47 <div id="menubar">
49 48 <div class="navbar">
50 49 <div class="navbar-inner">
51 50 <div class="container">
52 51 <ul id="menus" class="nav">
53 52 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
54 53 <ul id="file_menu" class="dropdown-menu">
55 54 <li id="new_notebook"
56 55 title="Make a new notebook (Opens a new window)">
57 56 <a href="#">New</a></li>
58 57 <li id="open_notebook"
59 58 title="Opens a new window with the Dashboard view">
60 59 <a href="#">Open...</a></li>
61 60 <!-- <hr/> -->
62 61 <li class="divider"></li>
63 62 <li id="copy_notebook"
64 63 title="Open a copy of this notebook's contents and start a new kernel">
65 64 <a href="#">Make a Copy...</a></li>
66 65 <li id="rename_notebook"><a href="#">Rename...</a></li>
67 66 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
68 67 <!-- <hr/> -->
69 68 <li class="divider"></li>
70 69 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
71 70 <ul class="dropdown-menu">
72 71 <li><a href="#"></a></li>
73 72 <li><a href="#"></a></li>
74 73 <li><a href="#"></a></li>
75 74 <li><a href="#"></a></li>
76 75 <li><a href="#"></a></li>
77 76 </ul>
78 77 </li>
79 78 <li class="divider"></li>
80 79 <li id="print_preview"><a href="#">Print Preview</a></li>
81 80 <li class="dropdown-submenu"><a href="#">Download as</a>
82 81 <ul class="dropdown-menu">
83 82 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
84 83 <li id="download_py"><a href="#">Python (.py)</a></li>
85 84 <li id="download_html"><a href="#">HTML (.html)</a></li>
86 85 <li id="download_rst"><a href="#">reST (.rst)</a></li>
87 86 </ul>
88 87 </li>
89 88 <li class="divider"></li>
90 89
91 90 <li id="kill_and_exit"
92 91 title="Shutdown this notebook's kernel, and close this window">
93 92 <a href="#" >Close and halt</a></li>
94 93 </ul>
95 94 </li>
96 95 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
97 96 <ul id="edit_menu" class="dropdown-menu">
98 97 <li id="cut_cell"><a href="#">Cut Cell</a></li>
99 98 <li id="copy_cell"><a href="#">Copy Cell</a></li>
100 99 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
101 100 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
102 101 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
103 102 <li id="delete_cell"><a href="#">Delete Cell</a></li>
104 103 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
105 104 <li class="divider"></li>
106 105 <li id="split_cell"><a href="#">Split Cell</a></li>
107 106 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
108 107 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
109 108 <li class="divider"></li>
110 109 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
111 110 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
112 111 <li class="divider"></li>
113 112 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
114 113 </ul>
115 114 </li>
116 115 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
117 116 <ul id="view_menu" class="dropdown-menu">
118 117 <li id="toggle_header"
119 118 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
120 119 <a href="#">Toggle Header</a></li>
121 120 <li id="toggle_toolbar"
122 121 title="Show/Hide the action icons (below menu bar)">
123 122 <a href="#">Toggle Toolbar</a></li>
124 123 </ul>
125 124 </li>
126 125 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
127 126 <ul id="insert_menu" class="dropdown-menu">
128 127 <li id="insert_cell_above"
129 128 title="Insert an empty Code cell above the currently active cell">
130 129 <a href="#">Insert Cell Above</a></li>
131 130 <li id="insert_cell_below"
132 131 title="Insert an empty Code cell below the currently active cell">
133 132 <a href="#">Insert Cell Below</a></li>
134 133 </ul>
135 134 </li>
136 135 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
137 136 <ul id="cell_menu" class="dropdown-menu">
138 137 <li id="run_cell" title="Run this cell, and move cursor to the next one">
139 138 <a href="#">Run</a></li>
140 139 <li id="run_cell_select_below" title="Run this cell, select below">
141 140 <a href="#">Run and Select Below</a></li>
142 141 <li id="run_cell_insert_below" title="Run this cell, insert below">
143 142 <a href="#">Run and Insert Below</a></li>
144 143 <li id="run_all_cells" title="Run all cells in the notebook">
145 144 <a href="#">Run All</a></li>
146 145 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
147 146 <a href="#">Run All Above</a></li>
148 147 <li id="run_all_cells_below" title="Run this cell and all cells below it">
149 148 <a href="#">Run All Below</a></li>
150 149 <li class="divider"></li>
151 150 <li id="change_cell_type" class="dropdown-submenu"
152 151 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
153 152 <a href="#">Cell Type</a>
154 153 <ul class="dropdown-menu">
155 154 <li id="to_code"
156 155 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
157 156 <a href="#">Code</a></li>
158 157 <li id="to_markdown"
159 158 title="Contents will be rendered as HTML and serve as explanatory text">
160 159 <a href="#">Markdown</a></li>
161 160 <li id="to_raw"
162 161 title="Contents will pass through nbconvert unmodified">
163 162 <a href="#">Raw NBConvert</a></li>
164 163 <li id="to_heading1"><a href="#">Heading 1</a></li>
165 164 <li id="to_heading2"><a href="#">Heading 2</a></li>
166 165 <li id="to_heading3"><a href="#">Heading 3</a></li>
167 166 <li id="to_heading4"><a href="#">Heading 4</a></li>
168 167 <li id="to_heading5"><a href="#">Heading 5</a></li>
169 168 <li id="to_heading6"><a href="#">Heading 6</a></li>
170 169 </ul>
171 170 </li>
172 171 <li class="divider"></li>
173 172 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
174 173 <ul class="dropdown-menu">
175 174 <li id="toggle_current_output"
176 175 title="Hide/Show the output of the current cell">
177 176 <a href="#">Toggle</a>
178 177 </li>
179 178 <li id="toggle_current_output_scroll"
180 179 title="Scroll the output of the current cell">
181 180 <a href="#">Toggle Scrolling</a>
182 181 </li>
183 182 <li id="clear_current_output"
184 183 title="Clear the output of the current cell">
185 184 <a href="#">Clear</a>
186 185 </li>
187 186 </ul>
188 187 </li>
189 188 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
190 189 <ul class="dropdown-menu">
191 190 <li id="toggle_all_output"
192 191 title="Hide/Show the output of all cells">
193 192 <a href="#">Toggle</a>
194 193 </li>
195 194 <li id="toggle_all_output_scroll"
196 195 title="Scroll the output of all cells">
197 196 <a href="#">Toggle Scrolling</a>
198 197 </li>
199 198 <li id="clear_all_output"
200 199 title="Clear the output of all cells">
201 200 <a href="#">Clear</a>
202 201 </li>
203 202 </ul>
204 203 </li>
205 204 </ul>
206 205 </li>
207 206 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
208 207 <ul id="kernel_menu" class="dropdown-menu">
209 208 <li id="int_kernel"
210 209 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
211 210 <a href="#">Interrupt</a></li>
212 211 <li id="restart_kernel"
213 212 title="Restart the Kernel">
214 213 <a href="#">Restart</a></li>
215 214 </ul>
216 215 </li>
217 216 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
218 217 <ul id="help_menu" class="dropdown-menu">
219 218 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
220 219 <li class="divider"></li>
221 220 {% set
222 221 sections = (
223 222 (
224 223 ("http://ipython.org/documentation.html","IPython Help",True),
225 224 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True),
226 225 ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True),
227 226 ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True),
228 227 ),(
229 228 ("http://docs.python.org","Python",True),
230 229 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
231 230 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
232 231 ("http://matplotlib.org/contents.html","Matplotlib",True),
233 232 ("http://docs.sympy.org/dev/index.html","SymPy",True),
234 233 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
235 234 )
236 235 )
237 236 %}
238 237
239 238 {% for helplinks in sections %}
240 239 {% for link in helplinks %}
241 240 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
242 241 {{'<i class="icon-external-link menu-icon pull-right"></i>' if link[2]}}
243 242 {{link[1]}}
244 243 </a></li>
245 244 {% endfor %}
246 245 {% if not loop.last %}
247 246 <li class="divider"></li>
248 247 {% endif %}
249 248 {% endfor %}
250 249 </li>
251 250 </ul>
252 251 </li>
253 252 </ul>
254 253 <div id="kernel_indicator" class="indicator_area pull-right">
255 254 <i id="kernel_indicator_icon"></i>
256 255 </div>
257 256 <div id="modal_indicator" class="indicator_area pull-right">
258 257 <i id="modal_indicator_icon"></i>
259 258 </div>
260 259 <div id="notification_area"></div>
261 260 </div>
262 261 </div>
263 262 </div>
264 263 </div>
265 264 <div id="maintoolbar" class="navbar">
266 265 <div class="toolbar-inner navbar-inner navbar-nobg">
267 266 <div id="maintoolbar-container" class="container"></div>
268 267 </div>
269 268 </div>
270 269 </div>
271 270
272 271 <div id="ipython-main-app">
273 272
274 273 <div id="notebook_panel">
275 274 <div id="notebook"></div>
276 275 <div id="pager_splitter"></div>
277 276 <div id="pager">
278 277 <div id='pager_button_area'>
279 278 </div>
280 279 <div id="pager-container" class="container"></div>
281 280 </div>
282 281 </div>
283 282
284 283 </div>
285 284 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
286 285
287 286
288 287 {% endblock %}
289 288
290 289
291 290 {% block script %}
292 291
293 292 {{super()}}
294 293
295 294 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
296 295 <script type="text/javascript">
297 296 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js") }}";
298 297 </script>
299 298 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
300 299 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
301 300 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
302 301 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
303 302 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
304 303 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
305 304 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
306 305 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
307 306 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
308 307 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
309 308 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
310 309 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
311 310 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
312 311 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
313 312
314 313 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
315 314
316 315 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
317 316
318 317 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
319 318 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
320 319 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
321 320 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
322 321 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
323 322 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
324 323 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
325 324 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
326 325 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
327 326 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
328 327 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
329 328 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
330 329 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
331 330 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
332 331 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
333 332 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
334 333 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
335 334 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
336 335 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
337 336 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
338 337 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
339 338 <script src="{{ static_url("notebook/js/keyboardmanager.js") }}" type="text/javascript" charset="utf-8"></script>
340 339 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
341 340 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
342 341 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
343 342 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
344 343 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
345 344
346 345 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
347 346
348 347 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
349 348 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
350 349 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
351 350
352 351 {% endblock %}
@@ -1,99 +1,98 b''
1 1 {% extends "page.html" %}
2 2
3 3 {% block title %}{{page_title}}{% endblock %}
4 4
5 5
6 6 {% block stylesheet %}
7 7 {{super()}}
8 8 <link rel="stylesheet" href="{{ static_url("tree/css/override.css") }}" type="text/css" />
9 9 {% endblock %}
10 10
11 11 {% block params %}
12 12
13 13 data-project="{{project}}"
14 14 data-base-url="{{base_url}}"
15 15 data-notebook-path="{{notebook_path}}"
16 data-base-kernel-url="{{base_kernel_url}}"
17 16
18 17 {% endblock %}
19 18
20 19
21 20 {% block site %}
22 21
23 22 <div id="ipython-main-app" class="container">
24 23
25 24 <div id="tab_content" class="tabbable">
26 25 <ul id="tabs" class="nav nav-tabs">
27 26 <li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
28 27 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
29 28 </ul>
30 29
31 30 <div class="tab-content">
32 31 <div id="notebooks" class="tab-pane active">
33 32 <div id="notebook_toolbar" class="row-fluid">
34 33 <div class="span8">
35 34 <form id='alternate_upload' class='alternate_upload' >
36 35 <span id="drag_info" style="position:absolute" >
37 36 To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
38 37 </span>
39 38 <input type="file" name="datafile" class="fileinput" multiple='multiple'>
40 39 </form>
41 40 </div>
42 41 <div class="span4 clearfix">
43 42 <span id="notebook_buttons" class="pull-right">
44 43 <button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
45 44 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small"><i class="icon-refresh"></i></button>
46 45 </span>
47 46 </div>
48 47 </div>
49 48
50 49 <div id="notebook_list">
51 50 <div id="notebook_list_header" class="row-fluid list_header">
52 51 <div id="project_name">
53 52 <ul class="breadcrumb">
54 53 <li><a href="{{breadcrumbs[0][0]}}"><i class="icon-home"></i></a><span>/</span></li>
55 54 {% for crumb in breadcrumbs[1:] %}
56 55 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a> <span>/</span></li>
57 56 {% endfor %}
58 57 </ul>
59 58 </div>
60 59 </div>
61 60 </div>
62 61 </div>
63 62
64 63 <div id="clusters" class="tab-pane">
65 64
66 65 <div id="cluster_toolbar" class="row-fluid">
67 66 <div class="span8">
68 67 <span id="cluster_list_info">IPython parallel computing clusters</span>
69 68 </div>
70 69 <div class="span4" class="clearfix">
71 70 <span id="cluster_buttons" class="pull-right">
72 71 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small"><i class="icon-refresh"></i></button>
73 72 </span>
74 73 </div>
75 74 </div>
76 75
77 76 <div id="cluster_list">
78 77 <div id="cluster_list_header" class="row-fluid list_header">
79 78 <div class="profile_col span4">profile</div>
80 79 <div class="status_col span3">status</div>
81 80 <div class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</div>
82 81 <div class="action_col span2">action</div>
83 82 </div>
84 83 </div>
85 84 </div>
86 85 </div>
87 86
88 87 </div>
89 88
90 89 {% endblock %}
91 90
92 91 {% block script %}
93 92 {{super()}}
94 93 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
95 94 <script src="{{static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
96 95 <script src="{{static_url("tree/js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
97 96 <script src="{{static_url("tree/js/clusterlist.js") }}" type="text/javascript" charset="utf-8"></script>
98 97 <script src="{{static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
99 98 {% endblock %}
@@ -1,163 +1,162 b''
1 1 .. _working_remotely:
2 2
3 3 Running a notebook server
4 4 =========================
5 5
6 6
7 7 The :ref:`IPython notebook <htmlnotebook>` web-application is based on a
8 8 server-client structure. This server uses a :ref:`two-process kernel
9 9 architecture <ipythonzmq>` based on ZeroMQ_, as well as Tornado_ for serving
10 10 HTTP requests. By default, a notebook server runs on http://127.0.0.1:8888/
11 11 and is accessible only from `localhost`. This document describes how you can
12 12 :ref:`secure a notebook server <notebook_security>` and how to :ref:`run it on
13 13 a public interface <notebook_public_server>`.
14 14
15 15 .. _ZeroMQ: http://zeromq.org
16 16
17 17 .. _Tornado: http://www.tornadoweb.org
18 18
19 19
20 20 .. _notebook_security:
21 21
22 22 Notebook security
23 23 -----------------
24 24
25 25 You can protect your notebook server with a simple single password by
26 26 setting the :attr:`NotebookApp.password` configurable. You can prepare a
27 27 hashed password using the function :func:`IPython.lib.security.passwd`:
28 28
29 29 .. sourcecode:: ipython
30 30
31 31 In [1]: from IPython.lib import passwd
32 32 In [2]: passwd()
33 33 Enter password:
34 34 Verify password:
35 35 Out[2]: 'sha1:67c9e60bb8b6:9ffede0825894254b2e042ea597d771089e11aed'
36 36
37 37 .. note::
38 38
39 39 :func:`~IPython.lib.security.passwd` can also take the password as a string
40 40 argument. **Do not** pass it as an argument inside an IPython session, as it
41 41 will be saved in your input history.
42 42
43 43 You can then add this to your :file:`ipython_notebook_config.py`, e.g.::
44 44
45 45 # Password to use for web authentication
46 46 c = get_config()
47 47 c.NotebookApp.password =
48 48 u'sha1:67c9e60bb8b6:9ffede0825894254b2e042ea597d771089e11aed'
49 49
50 50 When using a password, it is a good idea to also use SSL, so that your
51 51 password is not sent unencrypted by your browser. You can start the notebook
52 52 to communicate via a secure protocol mode using a self-signed certificate with
53 53 the command::
54 54
55 55 $ ipython notebook --certfile=mycert.pem
56 56
57 57 .. note::
58 58
59 59 A self-signed certificate can be generated with ``openssl``. For example,
60 60 the following command will create a certificate valid for 365 days with
61 61 both the key and certificate data written to the same file::
62 62
63 63 $ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem
64 64
65 65 Your browser will warn you of a dangerous certificate because it is
66 66 self-signed. If you want to have a fully compliant certificate that will not
67 67 raise warnings, it is possible (but rather involved) to obtain one,
68 68 as explained in detail in `this tutorial`__.
69 69
70 70 .. __: http://arstechnica.com/security/news/2009/12/how-to-get-set-with-a-secure-sertificate-for-free.ars
71 71
72 72 Keep in mind that when you enable SSL support, you will need to access the
73 73 notebook server over ``https://``, not over plain ``http://``. The startup
74 74 message from the server prints this, but it is easy to overlook and think the
75 75 server is for some reason non-responsive.
76 76
77 77
78 78 .. _notebook_public_server:
79 79
80 80 Running a public notebook server
81 81 --------------------------------
82 82
83 83 If you want to access your notebook server remotely via a web browser,
84 84 you can do the following.
85 85
86 86 Start by creating a certificate file and a hashed password, as explained
87 87 above. Then create a custom profile for the notebook, with the following
88 88 command line, type::
89 89
90 90 $ ipython profile create nbserver
91 91
92 92 In the profile directory just created, edit the file
93 93 ``ipython_notebook_config.py``. By default, the file has all fields
94 94 commented; the minimum set you need to uncomment and edit is the following::
95 95
96 96 c = get_config()
97 97
98 98 # Kernel config
99 99 c.IPKernelApp.pylab = 'inline' # if you want plotting support always
100 100
101 101 # Notebook config
102 102 c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/mycert.pem'
103 103 c.NotebookApp.ip = '*'
104 104 c.NotebookApp.open_browser = False
105 105 c.NotebookApp.password = u'sha1:bcd259ccf...[your hashed password here]'
106 106 # It is a good idea to put it on a known, fixed port
107 107 c.NotebookApp.port = 9999
108 108
109 109 You can then start the notebook and access it later by pointing your browser
110 110 to ``https://your.host.com:9999`` with ``ipython notebook
111 111 --profile=nbserver``.
112 112
113 113 Running with a different URL prefix
114 114 -----------------------------------
115 115
116 116 The notebook dashboard (the landing page with an overview
117 117 of the notebooks in your working directory) typically lives at the URL
118 118 ``http://localhost:8888/``. If you prefer that it lives, together with the
119 119 rest of the notebook, under a sub-directory,
120 120 e.g. ``http://localhost:8888/ipython/``, you can do so with
121 121 configuration options like the following (see above for instructions about
122 122 modifying ``ipython_notebook_config.py``)::
123 123
124 c.NotebookApp.base_project_url = '/ipython/'
125 c.NotebookApp.base_kernel_url = '/ipython/'
124 c.NotebookApp.base_url = '/ipython/'
126 125 c.NotebookApp.webapp_settings = {'static_url_prefix':'/ipython/static/'}
127 126
128 127 Using a different notebook store
129 128 --------------------------------
130 129
131 130 By default, the notebook server stores the notebook documents that it saves as
132 131 files in the working directory of the notebook server, also known as the
133 132 ``notebook_dir``. This logic is implemented in the
134 133 :class:`FileNotebookManager` class. However, the server can be configured to
135 134 use a different notebook manager class, which can
136 135 store the notebooks in a different format.
137 136
138 137 Currently, we ship a :class:`AzureNotebookManager` class that stores notebooks
139 138 in Azure blob storage. This can be used by adding the following lines to your
140 139 ``ipython_notebook_config.py`` file::
141 140
142 141 c.NotebookApp.notebook_manager_class =
143 142 'IPython.html.services.notebooks.azurenbmanager.AzureNotebookManager'
144 143 c.AzureNotebookManager.account_name = u'paste_your_account_name_here'
145 144 c.AzureNotebookManager.account_key = u'paste_your_account_key_here'
146 145 c.AzureNotebookManager.container = u'notebooks'
147 146
148 147 In addition to providing your Azure Blob Storage account name and key, you
149 148 will have to provide a container name; you can use multiple containers to
150 149 organize your notebooks.
151 150
152 151
153 152 Known issues
154 153 ------------
155 154
156 155 When behind a proxy, especially if your system or browser is set to autodetect
157 156 the proxy, the notebook web application might fail to connect to the server's
158 157 websockets, and present you with a warning at startup. In this case, you need
159 158 to configure your system not to use the proxy for the server's address.
160 159
161 160 For example, in Firefox, go to the Preferences panel, Advanced section,
162 161 Network tab, click 'Settings...', and add the address of the notebook server
163 162 to the 'No proxy for' field.
General Comments 0
You need to be logged in to leave comments. Login now