##// END OF EJS Templates
Move environment setting from handler to the notebook application
Cameron Bates -
Show More
@@ -1,914 +1,906
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 Cookie
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28 import time
29 29 import uuid
30 30 import os
31 31
32 32 from tornado import web
33 33 from tornado import websocket
34 34
35 from jinja2 import Environment, FileSystemLoader
36
37 35 from zmq.eventloop import ioloop
38 36 from zmq.utils import jsonapi
39 37
40 38 from IPython.external.decorator import decorator
41 39 from IPython.zmq.session import Session
42 40 from IPython.lib.security import passwd_check
43 41 from IPython.utils.jsonutil import date_default
44 42 from IPython.utils.path import filefind
45 43
46 44 try:
47 45 from docutils.core import publish_string
48 46 except ImportError:
49 47 publish_string = None
50 48
51 49 #-----------------------------------------------------------------------------
52 50 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
53 51 #-----------------------------------------------------------------------------
54 52
55 53 # Google Chrome, as of release 16, changed its websocket protocol number. The
56 54 # parts tornado cares about haven't really changed, so it's OK to continue
57 55 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
58 56 # version as of Oct 30/2011) the version check fails, see the issue report:
59 57
60 58 # https://github.com/facebook/tornado/issues/385
61 59
62 60 # This issue has been fixed in Tornado post 2.1.1:
63 61
64 62 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
65 63
66 64 # Here we manually apply the same patch as above so that users of IPython can
67 65 # continue to work with an officially released Tornado. We make the
68 66 # monkeypatch version check as narrow as possible to limit its effects; once
69 67 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
70 68
71 69 import tornado
72 70
73 71 if tornado.version_info <= (2,1,1):
74 72
75 73 def _execute(self, transforms, *args, **kwargs):
76 74 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
77 75
78 76 self.open_args = args
79 77 self.open_kwargs = kwargs
80 78
81 79 # The difference between version 8 and 13 is that in 8 the
82 80 # client sends a "Sec-Websocket-Origin" header and in 13 it's
83 81 # simply "Origin".
84 82 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
85 83 self.ws_connection = WebSocketProtocol8(self)
86 84 self.ws_connection.accept_connection()
87 85
88 86 elif self.request.headers.get("Sec-WebSocket-Version"):
89 87 self.stream.write(tornado.escape.utf8(
90 88 "HTTP/1.1 426 Upgrade Required\r\n"
91 89 "Sec-WebSocket-Version: 8\r\n\r\n"))
92 90 self.stream.close()
93 91
94 92 else:
95 93 self.ws_connection = WebSocketProtocol76(self)
96 94 self.ws_connection.accept_connection()
97 95
98 96 websocket.WebSocketHandler._execute = _execute
99 97 del _execute
100 98
101 99 #-----------------------------------------------------------------------------
102 100 # Decorator for disabling read-only handlers
103 101 #-----------------------------------------------------------------------------
104 102
105 103 @decorator
106 104 def not_if_readonly(f, self, *args, **kwargs):
107 105 if self.application.read_only:
108 106 raise web.HTTPError(403, "Notebook server is read-only")
109 107 else:
110 108 return f(self, *args, **kwargs)
111 109
112 110 @decorator
113 111 def authenticate_unless_readonly(f, self, *args, **kwargs):
114 112 """authenticate this page *unless* readonly view is active.
115 113
116 114 In read-only mode, the notebook list and print view should
117 115 be accessible without authentication.
118 116 """
119 117
120 118 @web.authenticated
121 119 def auth_f(self, *args, **kwargs):
122 120 return f(self, *args, **kwargs)
123 121
124 122 if self.application.read_only:
125 123 return f(self, *args, **kwargs)
126 124 else:
127 125 return auth_f(self, *args, **kwargs)
128 126
129 127 def urljoin(*pieces):
130 128 """Join componenet of url into a relative url
131 129
132 130 Use to prevent double slash when joining subpath
133 131 """
134 132 striped = [s.strip('/') for s in pieces]
135 133 return '/'.join(s for s in striped if s)
136 134
137 135 #-----------------------------------------------------------------------------
138 136 # Top-level handlers
139 137 #-----------------------------------------------------------------------------
140 138
141 139 class RequestHandler(web.RequestHandler):
142 140 """RequestHandler with default variable setting."""
143 141
144 142 def render(*args, **kwargs):
145 143 kwargs.setdefault('message', '')
146 144 return web.RequestHandler.render(*args, **kwargs)
147 145
148 146 class AuthenticatedHandler(RequestHandler):
149 147 """A RequestHandler with an authenticated user."""
150 148
151 149 def get_current_user(self):
152 150 user_id = self.get_secure_cookie(self.settings['cookie_name'])
153 151 # For now the user_id should not return empty, but it could eventually
154 152 if user_id == '':
155 153 user_id = 'anonymous'
156 154 if user_id is None:
157 155 # prevent extra Invalid cookie sig warnings:
158 156 self.clear_cookie(self.settings['cookie_name'])
159 157 if not self.application.password and not self.application.read_only:
160 158 user_id = 'anonymous'
161 159 return user_id
162 160
163 161 @property
164 162 def logged_in(self):
165 163 """Is a user currently logged in?
166 164
167 165 """
168 166 user = self.get_current_user()
169 167 return (user and not user == 'anonymous')
170 168
171 169 @property
172 170 def login_available(self):
173 171 """May a user proceed to log in?
174 172
175 173 This returns True if login capability is available, irrespective of
176 174 whether the user is already logged in or not.
177 175
178 176 """
179 177 return bool(self.application.password)
180 178
181 179 @property
182 180 def read_only(self):
183 181 """Is the notebook read-only?
184 182
185 183 """
186 184 return self.application.read_only
187 185
188 186 @property
189 187 def ws_url(self):
190 188 """websocket url matching the current request
191 189
192 190 turns http[s]://host[:port] into
193 191 ws[s]://host[:port]
194 192 """
195 193 proto = self.request.protocol.replace('http', 'ws')
196 194 host = self.application.ipython_app.websocket_host # default to config value
197 195 if host == '':
198 196 host = self.request.host # get from request
199 197 return "%s://%s" % (proto, host)
200 198
201 @property
202 def environment(self):
203 """ Jinja 2 template base directory
204 """
205 env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
206 return env
207 199
208 200 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
209 201 """static files should only be accessible when logged in"""
210 202
211 203 @authenticate_unless_readonly
212 204 def get(self, path):
213 205 return web.StaticFileHandler.get(self, path)
214 206
215 207
216 208 class ProjectDashboardHandler(AuthenticatedHandler):
217 209
218 210 @authenticate_unless_readonly
219 211 def get(self):
220 212 nbm = self.application.notebook_manager
221 213 project = nbm.notebook_dir
222 nb = self.environment.get_template('projectdashboard.html')
214 nb = self.application.jinja2_env.get_template('projectdashboard.html')
223 215 self.write( nb.render(project=project,
224 216 base_project_url=self.application.ipython_app.base_project_url,
225 217 base_kernel_url=self.application.ipython_app.base_kernel_url,
226 218 read_only=self.read_only,
227 219 logged_in=self.logged_in,
228 220 login_available=self.login_available))
229 221
230 222
231 223 class LoginHandler(AuthenticatedHandler):
232 224
233 225 def _render(self, message=None):
234 nb = self.environment.get_template('login.html')
226 nb = self.application.jinja2_env.get_template('login.html')
235 227 self.write( nb.render(
236 228 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
237 229 read_only=self.read_only,
238 230 logged_in=self.logged_in,
239 231 login_available=self.login_available,
240 232 base_project_url=self.application.ipython_app.base_project_url,
241 233 message=message
242 234 ))
243 235
244 236 def get(self):
245 237 if self.current_user:
246 238 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
247 239 else:
248 240 self._render()
249 241
250 242 def post(self):
251 243 pwd = self.get_argument('password', default=u'')
252 244 if self.application.password:
253 245 if passwd_check(self.application.password, pwd):
254 246 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
255 247 else:
256 248 self._render(message={'error': 'Invalid password'})
257 249 return
258 250
259 251 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
260 252
261 253
262 254 class LogoutHandler(AuthenticatedHandler):
263 255
264 256 def get(self):
265 257 self.clear_cookie(self.settings['cookie_name'])
266 258 if self.login_available:
267 259 message = {'info': 'Successfully logged out.'}
268 260 else:
269 261 message = {'warning': 'Cannot log out. Notebook authentication '
270 262 'is disabled.'}
271 nb = self.environment.get_template('logout.html')
263 nb = self.application.jinja2_env.get_template('logout.html')
272 264 self.write( nb.render(
273 265 read_only=self.read_only,
274 266 logged_in=self.logged_in,
275 267 login_available=self.login_available,
276 268 base_project_url=self.application.ipython_app.base_project_url,
277 269 message=message))
278 270
279 271
280 272 class NewHandler(AuthenticatedHandler):
281 273
282 274 @web.authenticated
283 275 def get(self):
284 276 nbm = self.application.notebook_manager
285 277 project = nbm.notebook_dir
286 278 notebook_id = nbm.new_notebook()
287 279 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
288 280
289 281 class NamedNotebookHandler(AuthenticatedHandler):
290 282
291 283 @authenticate_unless_readonly
292 284 def get(self, notebook_id):
293 285 nbm = self.application.notebook_manager
294 286 project = nbm.notebook_dir
295 287 if not nbm.notebook_exists(notebook_id):
296 288 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
297 nb = self.environment.get_template('notebook.html')
289 nb = self.application.jinja2_env.get_template('notebook.html')
298 290 self.write( nb.render(project=project,
299 291 notebook_id=notebook_id,
300 292 base_project_url=self.application.ipython_app.base_project_url,
301 293 base_kernel_url=self.application.ipython_app.base_kernel_url,
302 294 kill_kernel=False,
303 295 read_only=self.read_only,
304 296 logged_in=self.logged_in,
305 297 login_available=self.login_available,
306 298 mathjax_url=self.application.ipython_app.mathjax_url,))
307 299
308 300
309 301 class PrintNotebookHandler(AuthenticatedHandler):
310 302
311 303 @authenticate_unless_readonly
312 304 def get(self, notebook_id):
313 305 nbm = self.application.notebook_manager
314 306 project = nbm.notebook_dir
315 307 if not nbm.notebook_exists(notebook_id):
316 308 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
317 nb = self.environment.get_template('printnotebook.html')
309 nb = self.application.jinja2_env.get_template('printnotebook.html')
318 310 self.write( nb.render(
319 311 project=project,
320 312 notebook_id=notebook_id,
321 313 base_project_url=self.application.ipython_app.base_project_url,
322 314 base_kernel_url=self.application.ipython_app.base_kernel_url,
323 315 kill_kernel=False,
324 316 read_only=self.read_only,
325 317 logged_in=self.logged_in,
326 318 login_available=self.login_available,
327 319 mathjax_url=self.application.ipython_app.mathjax_url,
328 320 ))
329 321
330 322 #-----------------------------------------------------------------------------
331 323 # Kernel handlers
332 324 #-----------------------------------------------------------------------------
333 325
334 326
335 327 class MainKernelHandler(AuthenticatedHandler):
336 328
337 329 @web.authenticated
338 330 def get(self):
339 331 km = self.application.kernel_manager
340 332 self.finish(jsonapi.dumps(km.kernel_ids))
341 333
342 334 @web.authenticated
343 335 def post(self):
344 336 km = self.application.kernel_manager
345 337 nbm = self.application.notebook_manager
346 338 notebook_id = self.get_argument('notebook', default=None)
347 339 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
348 340 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
349 341 self.set_header('Location', '/'+kernel_id)
350 342 self.finish(jsonapi.dumps(data))
351 343
352 344
353 345 class KernelHandler(AuthenticatedHandler):
354 346
355 347 SUPPORTED_METHODS = ('DELETE')
356 348
357 349 @web.authenticated
358 350 def delete(self, kernel_id):
359 351 km = self.application.kernel_manager
360 352 km.shutdown_kernel(kernel_id)
361 353 self.set_status(204)
362 354 self.finish()
363 355
364 356
365 357 class KernelActionHandler(AuthenticatedHandler):
366 358
367 359 @web.authenticated
368 360 def post(self, kernel_id, action):
369 361 km = self.application.kernel_manager
370 362 if action == 'interrupt':
371 363 km.interrupt_kernel(kernel_id)
372 364 self.set_status(204)
373 365 if action == 'restart':
374 366 new_kernel_id = km.restart_kernel(kernel_id)
375 367 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
376 368 self.set_header('Location', '/'+new_kernel_id)
377 369 self.write(jsonapi.dumps(data))
378 370 self.finish()
379 371
380 372
381 373 class ZMQStreamHandler(websocket.WebSocketHandler):
382 374
383 375 def _reserialize_reply(self, msg_list):
384 376 """Reserialize a reply message using JSON.
385 377
386 378 This takes the msg list from the ZMQ socket, unserializes it using
387 379 self.session and then serializes the result using JSON. This method
388 380 should be used by self._on_zmq_reply to build messages that can
389 381 be sent back to the browser.
390 382 """
391 383 idents, msg_list = self.session.feed_identities(msg_list)
392 384 msg = self.session.unserialize(msg_list)
393 385 try:
394 386 msg['header'].pop('date')
395 387 except KeyError:
396 388 pass
397 389 try:
398 390 msg['parent_header'].pop('date')
399 391 except KeyError:
400 392 pass
401 393 msg.pop('buffers')
402 394 return jsonapi.dumps(msg, default=date_default)
403 395
404 396 def _on_zmq_reply(self, msg_list):
405 397 try:
406 398 msg = self._reserialize_reply(msg_list)
407 399 except Exception:
408 400 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
409 401 else:
410 402 self.write_message(msg)
411 403
412 404 def allow_draft76(self):
413 405 """Allow draft 76, until browsers such as Safari update to RFC 6455.
414 406
415 407 This has been disabled by default in tornado in release 2.2.0, and
416 408 support will be removed in later versions.
417 409 """
418 410 return True
419 411
420 412
421 413 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
422 414
423 415 def open(self, kernel_id):
424 416 self.kernel_id = kernel_id.decode('ascii')
425 417 try:
426 418 cfg = self.application.ipython_app.config
427 419 except AttributeError:
428 420 # protect from the case where this is run from something other than
429 421 # the notebook app:
430 422 cfg = None
431 423 self.session = Session(config=cfg)
432 424 self.save_on_message = self.on_message
433 425 self.on_message = self.on_first_message
434 426
435 427 def get_current_user(self):
436 428 user_id = self.get_secure_cookie(self.settings['cookie_name'])
437 429 if user_id == '' or (user_id is None and not self.application.password):
438 430 user_id = 'anonymous'
439 431 return user_id
440 432
441 433 def _inject_cookie_message(self, msg):
442 434 """Inject the first message, which is the document cookie,
443 435 for authentication."""
444 436 if isinstance(msg, unicode):
445 437 # Cookie can't constructor doesn't accept unicode strings for some reason
446 438 msg = msg.encode('utf8', 'replace')
447 439 try:
448 440 self.request._cookies = Cookie.SimpleCookie(msg)
449 441 except:
450 442 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
451 443
452 444 def on_first_message(self, msg):
453 445 self._inject_cookie_message(msg)
454 446 if self.get_current_user() is None:
455 447 logging.warn("Couldn't authenticate WebSocket connection")
456 448 raise web.HTTPError(403)
457 449 self.on_message = self.save_on_message
458 450
459 451
460 452 class IOPubHandler(AuthenticatedZMQStreamHandler):
461 453
462 454 def initialize(self, *args, **kwargs):
463 455 self._kernel_alive = True
464 456 self._beating = False
465 457 self.iopub_stream = None
466 458 self.hb_stream = None
467 459
468 460 def on_first_message(self, msg):
469 461 try:
470 462 super(IOPubHandler, self).on_first_message(msg)
471 463 except web.HTTPError:
472 464 self.close()
473 465 return
474 466 km = self.application.kernel_manager
475 467 self.time_to_dead = km.time_to_dead
476 468 self.first_beat = km.first_beat
477 469 kernel_id = self.kernel_id
478 470 try:
479 471 self.iopub_stream = km.create_iopub_stream(kernel_id)
480 472 self.hb_stream = km.create_hb_stream(kernel_id)
481 473 except web.HTTPError:
482 474 # WebSockets don't response to traditional error codes so we
483 475 # close the connection.
484 476 if not self.stream.closed():
485 477 self.stream.close()
486 478 self.close()
487 479 else:
488 480 self.iopub_stream.on_recv(self._on_zmq_reply)
489 481 self.start_hb(self.kernel_died)
490 482
491 483 def on_message(self, msg):
492 484 pass
493 485
494 486 def on_close(self):
495 487 # This method can be called twice, once by self.kernel_died and once
496 488 # from the WebSocket close event. If the WebSocket connection is
497 489 # closed before the ZMQ streams are setup, they could be None.
498 490 self.stop_hb()
499 491 if self.iopub_stream is not None and not self.iopub_stream.closed():
500 492 self.iopub_stream.on_recv(None)
501 493 self.iopub_stream.close()
502 494 if self.hb_stream is not None and not self.hb_stream.closed():
503 495 self.hb_stream.close()
504 496
505 497 def start_hb(self, callback):
506 498 """Start the heartbeating and call the callback if the kernel dies."""
507 499 if not self._beating:
508 500 self._kernel_alive = True
509 501
510 502 def ping_or_dead():
511 503 self.hb_stream.flush()
512 504 if self._kernel_alive:
513 505 self._kernel_alive = False
514 506 self.hb_stream.send(b'ping')
515 507 # flush stream to force immediate socket send
516 508 self.hb_stream.flush()
517 509 else:
518 510 try:
519 511 callback()
520 512 except:
521 513 pass
522 514 finally:
523 515 self.stop_hb()
524 516
525 517 def beat_received(msg):
526 518 self._kernel_alive = True
527 519
528 520 self.hb_stream.on_recv(beat_received)
529 521 loop = ioloop.IOLoop.instance()
530 522 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
531 523 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
532 524 self._beating= True
533 525
534 526 def _really_start_hb(self):
535 527 """callback for delayed heartbeat start
536 528
537 529 Only start the hb loop if we haven't been closed during the wait.
538 530 """
539 531 if self._beating and not self.hb_stream.closed():
540 532 self._hb_periodic_callback.start()
541 533
542 534 def stop_hb(self):
543 535 """Stop the heartbeating and cancel all related callbacks."""
544 536 if self._beating:
545 537 self._beating = False
546 538 self._hb_periodic_callback.stop()
547 539 if not self.hb_stream.closed():
548 540 self.hb_stream.on_recv(None)
549 541
550 542 def kernel_died(self):
551 543 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
552 544 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
553 545 self.write_message(
554 546 {'header': {'msg_type': 'status'},
555 547 'parent_header': {},
556 548 'content': {'execution_state':'dead'}
557 549 }
558 550 )
559 551 self.on_close()
560 552
561 553
562 554 class ShellHandler(AuthenticatedZMQStreamHandler):
563 555
564 556 def initialize(self, *args, **kwargs):
565 557 self.shell_stream = None
566 558
567 559 def on_first_message(self, msg):
568 560 try:
569 561 super(ShellHandler, self).on_first_message(msg)
570 562 except web.HTTPError:
571 563 self.close()
572 564 return
573 565 km = self.application.kernel_manager
574 566 self.max_msg_size = km.max_msg_size
575 567 kernel_id = self.kernel_id
576 568 try:
577 569 self.shell_stream = km.create_shell_stream(kernel_id)
578 570 except web.HTTPError:
579 571 # WebSockets don't response to traditional error codes so we
580 572 # close the connection.
581 573 if not self.stream.closed():
582 574 self.stream.close()
583 575 self.close()
584 576 else:
585 577 self.shell_stream.on_recv(self._on_zmq_reply)
586 578
587 579 def on_message(self, msg):
588 580 if len(msg) < self.max_msg_size:
589 581 msg = jsonapi.loads(msg)
590 582 self.session.send(self.shell_stream, msg)
591 583
592 584 def on_close(self):
593 585 # Make sure the stream exists and is not already closed.
594 586 if self.shell_stream is not None and not self.shell_stream.closed():
595 587 self.shell_stream.close()
596 588
597 589
598 590 #-----------------------------------------------------------------------------
599 591 # Notebook web service handlers
600 592 #-----------------------------------------------------------------------------
601 593
602 594 class NotebookRootHandler(AuthenticatedHandler):
603 595
604 596 @authenticate_unless_readonly
605 597 def get(self):
606 598 nbm = self.application.notebook_manager
607 599 km = self.application.kernel_manager
608 600 files = nbm.list_notebooks()
609 601 for f in files :
610 602 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
611 603 self.finish(jsonapi.dumps(files))
612 604
613 605 @web.authenticated
614 606 def post(self):
615 607 nbm = self.application.notebook_manager
616 608 body = self.request.body.strip()
617 609 format = self.get_argument('format', default='json')
618 610 name = self.get_argument('name', default=None)
619 611 if body:
620 612 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
621 613 else:
622 614 notebook_id = nbm.new_notebook()
623 615 self.set_header('Location', '/'+notebook_id)
624 616 self.finish(jsonapi.dumps(notebook_id))
625 617
626 618
627 619 class NotebookHandler(AuthenticatedHandler):
628 620
629 621 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
630 622
631 623 @authenticate_unless_readonly
632 624 def get(self, notebook_id):
633 625 nbm = self.application.notebook_manager
634 626 format = self.get_argument('format', default='json')
635 627 last_mod, name, data = nbm.get_notebook(notebook_id, format)
636 628
637 629 if format == u'json':
638 630 self.set_header('Content-Type', 'application/json')
639 631 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
640 632 elif format == u'py':
641 633 self.set_header('Content-Type', 'application/x-python')
642 634 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
643 635 self.set_header('Last-Modified', last_mod)
644 636 self.finish(data)
645 637
646 638 @web.authenticated
647 639 def put(self, notebook_id):
648 640 nbm = self.application.notebook_manager
649 641 format = self.get_argument('format', default='json')
650 642 name = self.get_argument('name', default=None)
651 643 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
652 644 self.set_status(204)
653 645 self.finish()
654 646
655 647 @web.authenticated
656 648 def delete(self, notebook_id):
657 649 nbm = self.application.notebook_manager
658 650 nbm.delete_notebook(notebook_id)
659 651 self.set_status(204)
660 652 self.finish()
661 653
662 654
663 655 class NotebookCopyHandler(AuthenticatedHandler):
664 656
665 657 @web.authenticated
666 658 def get(self, notebook_id):
667 659 nbm = self.application.notebook_manager
668 660 project = nbm.notebook_dir
669 661 notebook_id = nbm.copy_notebook(notebook_id)
670 662 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
671 663
672 664
673 665 #-----------------------------------------------------------------------------
674 666 # Cluster handlers
675 667 #-----------------------------------------------------------------------------
676 668
677 669
678 670 class MainClusterHandler(AuthenticatedHandler):
679 671
680 672 @web.authenticated
681 673 def get(self):
682 674 cm = self.application.cluster_manager
683 675 self.finish(jsonapi.dumps(cm.list_profiles()))
684 676
685 677
686 678 class ClusterProfileHandler(AuthenticatedHandler):
687 679
688 680 @web.authenticated
689 681 def get(self, profile):
690 682 cm = self.application.cluster_manager
691 683 self.finish(jsonapi.dumps(cm.profile_info(profile)))
692 684
693 685
694 686 class ClusterActionHandler(AuthenticatedHandler):
695 687
696 688 @web.authenticated
697 689 def post(self, profile, action):
698 690 cm = self.application.cluster_manager
699 691 if action == 'start':
700 692 n = self.get_argument('n',default=None)
701 693 if n is None:
702 694 data = cm.start_cluster(profile)
703 695 else:
704 696 data = cm.start_cluster(profile,int(n))
705 697 if action == 'stop':
706 698 data = cm.stop_cluster(profile)
707 699 self.finish(jsonapi.dumps(data))
708 700
709 701
710 702 #-----------------------------------------------------------------------------
711 703 # RST web service handlers
712 704 #-----------------------------------------------------------------------------
713 705
714 706
715 707 class RSTHandler(AuthenticatedHandler):
716 708
717 709 @web.authenticated
718 710 def post(self):
719 711 if publish_string is None:
720 712 raise web.HTTPError(503, u'docutils not available')
721 713 body = self.request.body.strip()
722 714 source = body
723 715 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
724 716 defaults = {'file_insertion_enabled': 0,
725 717 'raw_enabled': 0,
726 718 '_disable_config': 1,
727 719 'stylesheet_path': 0
728 720 # 'template': template_path
729 721 }
730 722 try:
731 723 html = publish_string(source, writer_name='html',
732 724 settings_overrides=defaults
733 725 )
734 726 except:
735 727 raise web.HTTPError(400, u'Invalid RST')
736 728 print html
737 729 self.set_header('Content-Type', 'text/html')
738 730 self.finish(html)
739 731
740 732 # to minimize subclass changes:
741 733 HTTPError = web.HTTPError
742 734
743 735 class FileFindHandler(web.StaticFileHandler):
744 736 """subclass of StaticFileHandler for serving files from a search path"""
745 737
746 738 _static_paths = {}
747 739 # _lock is needed for tornado < 2.2.0 compat
748 740 _lock = threading.Lock() # protects _static_hashes
749 741
750 742 def initialize(self, path, default_filename=None):
751 743 if isinstance(path, basestring):
752 744 path = [path]
753 745 self.roots = tuple(
754 746 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
755 747 )
756 748 self.default_filename = default_filename
757 749
758 750 @classmethod
759 751 def locate_file(cls, path, roots):
760 752 """locate a file to serve on our static file search path"""
761 753 with cls._lock:
762 754 if path in cls._static_paths:
763 755 return cls._static_paths[path]
764 756 try:
765 757 abspath = os.path.abspath(filefind(path, roots))
766 758 except IOError:
767 759 # empty string should always give exists=False
768 760 return ''
769 761
770 762 # os.path.abspath strips a trailing /
771 763 # it needs to be temporarily added back for requests to root/
772 764 if not (abspath + os.path.sep).startswith(roots):
773 765 raise HTTPError(403, "%s is not in root static directory", path)
774 766
775 767 cls._static_paths[path] = abspath
776 768 return abspath
777 769
778 770 def get(self, path, include_body=True):
779 771 path = self.parse_url_path(path)
780 772
781 773 # begin subclass override
782 774 abspath = self.locate_file(path, self.roots)
783 775 # end subclass override
784 776
785 777 if os.path.isdir(abspath) and self.default_filename is not None:
786 778 # need to look at the request.path here for when path is empty
787 779 # but there is some prefix to the path that was already
788 780 # trimmed by the routing
789 781 if not self.request.path.endswith("/"):
790 782 self.redirect(self.request.path + "/")
791 783 return
792 784 abspath = os.path.join(abspath, self.default_filename)
793 785 if not os.path.exists(abspath):
794 786 raise HTTPError(404)
795 787 if not os.path.isfile(abspath):
796 788 raise HTTPError(403, "%s is not a file", path)
797 789
798 790 stat_result = os.stat(abspath)
799 791 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
800 792
801 793 self.set_header("Last-Modified", modified)
802 794
803 795 mime_type, encoding = mimetypes.guess_type(abspath)
804 796 if mime_type:
805 797 self.set_header("Content-Type", mime_type)
806 798
807 799 cache_time = self.get_cache_time(path, modified, mime_type)
808 800
809 801 if cache_time > 0:
810 802 self.set_header("Expires", datetime.datetime.utcnow() + \
811 803 datetime.timedelta(seconds=cache_time))
812 804 self.set_header("Cache-Control", "max-age=" + str(cache_time))
813 805 else:
814 806 self.set_header("Cache-Control", "public")
815 807
816 808 self.set_extra_headers(path)
817 809
818 810 # Check the If-Modified-Since, and don't send the result if the
819 811 # content has not been modified
820 812 ims_value = self.request.headers.get("If-Modified-Since")
821 813 if ims_value is not None:
822 814 date_tuple = email.utils.parsedate(ims_value)
823 815 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
824 816 if if_since >= modified:
825 817 self.set_status(304)
826 818 return
827 819
828 820 with open(abspath, "rb") as file:
829 821 data = file.read()
830 822 hasher = hashlib.sha1()
831 823 hasher.update(data)
832 824 self.set_header("Etag", '"%s"' % hasher.hexdigest())
833 825 if include_body:
834 826 self.write(data)
835 827 else:
836 828 assert self.request.method == "HEAD"
837 829 self.set_header("Content-Length", len(data))
838 830
839 831 @classmethod
840 832 def get_version(cls, settings, path):
841 833 """Generate the version string to be used in static URLs.
842 834
843 835 This method may be overridden in subclasses (but note that it
844 836 is a class method rather than a static method). The default
845 837 implementation uses a hash of the file's contents.
846 838
847 839 ``settings`` is the `Application.settings` dictionary and ``path``
848 840 is the relative location of the requested asset on the filesystem.
849 841 The returned value should be a string, or ``None`` if no version
850 842 could be determined.
851 843 """
852 844 # begin subclass override:
853 845 static_paths = settings['static_path']
854 846 if isinstance(static_paths, basestring):
855 847 static_paths = [static_paths]
856 848 roots = tuple(
857 849 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
858 850 )
859 851
860 852 try:
861 853 abs_path = filefind(path, roots)
862 854 except IOError:
863 855 logging.error("Could not find static file %r", path)
864 856 return None
865 857
866 858 # end subclass override
867 859
868 860 with cls._lock:
869 861 hashes = cls._static_hashes
870 862 if abs_path not in hashes:
871 863 try:
872 864 f = open(abs_path, "rb")
873 865 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
874 866 f.close()
875 867 except Exception:
876 868 logging.error("Could not open static file %r", path)
877 869 hashes[abs_path] = None
878 870 hsh = hashes.get(abs_path)
879 871 if hsh:
880 872 return hsh[:5]
881 873 return None
882 874
883 875
884 876 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
885 877 # but needed for tornado < 2.2.0 compat
886 878 @classmethod
887 879 def make_static_url(cls, settings, path):
888 880 """Constructs a versioned url for the given path.
889 881
890 882 This method may be overridden in subclasses (but note that it is
891 883 a class method rather than an instance method).
892 884
893 885 ``settings`` is the `Application.settings` dictionary. ``path``
894 886 is the static path being requested. The url returned should be
895 887 relative to the current host.
896 888 """
897 889 static_url_prefix = settings.get('static_url_prefix', '/static/')
898 890 version_hash = cls.get_version(settings, path)
899 891 if version_hash:
900 892 return static_url_prefix + path + "?v=" + version_hash
901 893 return static_url_prefix + path
902 894
903 895 def parse_url_path(self, url_path):
904 896 """Converts a static URL path into a filesystem path.
905 897
906 898 ``url_path`` is the path component of the URL with
907 899 ``static_url_prefix`` removed. The return value should be
908 900 filesystem path relative to ``static_path``.
909 901 """
910 902 if os.path.sep != "/":
911 903 url_path = url_path.replace("/", os.path.sep)
912 904 return url_path
913 905
914 906
@@ -1,622 +1,625
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 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 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import re
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import uuid
32 32 import webbrowser
33 33
34 34 # Third party
35 35 import zmq
36 from jinja2 import Environment, FileSystemLoader
36 37
37 38 # Install the pyzmq ioloop. This has to be done before anything else from
38 39 # tornado is imported.
39 40 from zmq.eventloop import ioloop
40 41 ioloop.install()
41 42
42 43 from tornado import httpserver
43 44 from tornado import web
44 45
45 46 # Our own libraries
46 47 from .kernelmanager import MappingKernelManager
47 48 from .handlers import (LoginHandler, LogoutHandler,
48 49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
51 52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
52 53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
53 54 FileFindHandler,
54 55 )
55 56 from .nbmanager import NotebookManager
56 57 from .filenbmanager import FileNotebookManager
57 58 from .clustermanager import ClusterManager
58 59
59 60 from IPython.config.application import catch_config_error, boolean_flag
60 61 from IPython.core.application import BaseIPythonApplication
61 62 from IPython.core.profiledir import ProfileDir
62 63 from IPython.frontend.consoleapp import IPythonConsoleApp
63 64 from IPython.lib.kernel import swallow_argv
64 65 from IPython.zmq.session import Session, default_secure
65 66 from IPython.zmq.zmqshell import ZMQInteractiveShell
66 67 from IPython.zmq.ipkernel import (
67 68 flags as ipkernel_flags,
68 69 aliases as ipkernel_aliases,
69 70 IPKernelApp
70 71 )
71 72 from IPython.utils.importstring import import_item
72 73 from IPython.utils.traitlets import (
73 74 Dict, Unicode, Integer, List, Enum, Bool,
74 75 DottedObjectName
75 76 )
76 77 from IPython.utils import py3compat
77 78 from IPython.utils.path import filefind
78 79
79 80 #-----------------------------------------------------------------------------
80 81 # Module globals
81 82 #-----------------------------------------------------------------------------
82 83
83 84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
84 85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
85 86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
86 87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
87 88 _cluster_action_regex = r"(?P<action>start|stop)"
88 89
89 90
90 91 LOCALHOST = '127.0.0.1'
91 92
92 93 _examples = """
93 94 ipython notebook # start the notebook
94 95 ipython notebook --profile=sympy # use the sympy profile
95 96 ipython notebook --pylab=inline # pylab in inline plotting mode
96 97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
98 99 """
99 100
100 101 #-----------------------------------------------------------------------------
101 102 # Helper functions
102 103 #-----------------------------------------------------------------------------
103 104
104 105 def url_path_join(a,b):
105 106 if a.endswith('/') and b.startswith('/'):
106 107 return a[:-1]+b
107 108 else:
108 109 return a+b
109 110
110 111 def random_ports(port, n):
111 112 """Generate a list of n random ports near the given port.
112 113
113 114 The first 5 ports will be sequential, and the remaining n-5 will be
114 115 randomly selected in the range [port-2*n, port+2*n].
115 116 """
116 117 for i in range(min(5, n)):
117 118 yield port + i
118 119 for i in range(n-5):
119 120 yield port + random.randint(-2*n, 2*n)
120 121
121 122 #-----------------------------------------------------------------------------
122 123 # The Tornado web application
123 124 #-----------------------------------------------------------------------------
124 125
125 126 class NotebookWebApplication(web.Application):
126 127
127 128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
128 129 cluster_manager, log,
129 130 base_project_url, settings_overrides):
130 131 handlers = [
131 132 (r"/", ProjectDashboardHandler),
132 133 (r"/login", LoginHandler),
133 134 (r"/logout", LogoutHandler),
134 135 (r"/new", NewHandler),
135 136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
136 137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
137 138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
138 139 (r"/kernels", MainKernelHandler),
139 140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
140 141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
141 142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
142 143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
143 144 (r"/notebooks", NotebookRootHandler),
144 145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
145 146 (r"/rstservice/render", RSTHandler),
146 147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
147 148 (r"/clusters", MainClusterHandler),
148 149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
149 150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
150 151 ]
151 152
152 153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
153 154 # base_project_url will always be unicode, which will in turn
154 155 # make the patterns unicode, and ultimately result in unicode
155 156 # keys in kwargs to handler._execute(**kwargs) in tornado.
156 157 # This enforces that base_project_url be ascii in that situation.
157 158 #
158 159 # Note that the URLs these patterns check against are escaped,
159 160 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
160 161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
161 162
162 163 settings = dict(
163 164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
164 165 static_path=ipython_app.static_file_path,
165 166 static_handler_class = FileFindHandler,
166 167 cookie_secret=os.urandom(1024),
167 168 login_url="%s/login"%(base_project_url.rstrip('/')),
168 169 cookie_name='username-%s' % uuid.uuid4(),
169 170 )
170 171
171 172 # allow custom overrides for the tornado web app.
172 173 settings.update(settings_overrides)
173 174
174 175 # prepend base_project_url onto the patterns that we match
175 176 new_handlers = []
176 177 for handler in handlers:
177 178 pattern = url_path_join(base_project_url, handler[0])
178 179 new_handler = tuple([pattern]+list(handler[1:]))
179 180 new_handlers.append( new_handler )
180 181
181 182 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
182 183
183 184 self.kernel_manager = kernel_manager
184 185 self.notebook_manager = notebook_manager
185 186 self.cluster_manager = cluster_manager
186 187 self.ipython_app = ipython_app
187 188 self.read_only = self.ipython_app.read_only
188 189 self.log = log
190 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
191
189 192
190 193
191 194 #-----------------------------------------------------------------------------
192 195 # Aliases and Flags
193 196 #-----------------------------------------------------------------------------
194 197
195 198 flags = dict(ipkernel_flags)
196 199 flags['no-browser']=(
197 200 {'NotebookApp' : {'open_browser' : False}},
198 201 "Don't open the notebook in a browser after startup."
199 202 )
200 203 flags['no-mathjax']=(
201 204 {'NotebookApp' : {'enable_mathjax' : False}},
202 205 """Disable MathJax
203 206
204 207 MathJax is the javascript library IPython uses to render math/LaTeX. It is
205 208 very large, so you may want to disable it if you have a slow internet
206 209 connection, or for offline use of the notebook.
207 210
208 211 When disabled, equations etc. will appear as their untransformed TeX source.
209 212 """
210 213 )
211 214 flags['read-only'] = (
212 215 {'NotebookApp' : {'read_only' : True}},
213 216 """Allow read-only access to notebooks.
214 217
215 218 When using a password to protect the notebook server, this flag
216 219 allows unauthenticated clients to view the notebook list, and
217 220 individual notebooks, but not edit them, start kernels, or run
218 221 code.
219 222
220 223 If no password is set, the server will be entirely read-only.
221 224 """
222 225 )
223 226
224 227 # Add notebook manager flags
225 228 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
226 229 'Auto-save a .py script everytime the .ipynb notebook is saved',
227 230 'Do not auto-save .py scripts for every notebook'))
228 231
229 232 # the flags that are specific to the frontend
230 233 # these must be scrubbed before being passed to the kernel,
231 234 # or it will raise an error on unrecognized flags
232 235 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
233 236
234 237 aliases = dict(ipkernel_aliases)
235 238
236 239 aliases.update({
237 240 'ip': 'NotebookApp.ip',
238 241 'port': 'NotebookApp.port',
239 242 'port-retries': 'NotebookApp.port_retries',
240 243 'keyfile': 'NotebookApp.keyfile',
241 244 'certfile': 'NotebookApp.certfile',
242 245 'notebook-dir': 'NotebookManager.notebook_dir',
243 246 'browser': 'NotebookApp.browser',
244 247 })
245 248
246 249 # remove ipkernel flags that are singletons, and don't make sense in
247 250 # multi-kernel evironment:
248 251 aliases.pop('f', None)
249 252
250 253 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
251 254 u'notebook-dir']
252 255
253 256 #-----------------------------------------------------------------------------
254 257 # NotebookApp
255 258 #-----------------------------------------------------------------------------
256 259
257 260 class NotebookApp(BaseIPythonApplication):
258 261
259 262 name = 'ipython-notebook'
260 263 default_config_file_name='ipython_notebook_config.py'
261 264
262 265 description = """
263 266 The IPython HTML Notebook.
264 267
265 268 This launches a Tornado based HTML Notebook Server that serves up an
266 269 HTML5/Javascript Notebook client.
267 270 """
268 271 examples = _examples
269 272
270 273 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
271 274 FileNotebookManager]
272 275 flags = Dict(flags)
273 276 aliases = Dict(aliases)
274 277
275 278 kernel_argv = List(Unicode)
276 279
277 280 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
278 281 default_value=logging.INFO,
279 282 config=True,
280 283 help="Set the log level by value or name.")
281 284
282 285 # create requested profiles by default, if they don't exist:
283 286 auto_create = Bool(True)
284 287
285 288 # file to be opened in the notebook server
286 289 file_to_run = Unicode('')
287 290
288 291 # Network related information.
289 292
290 293 ip = Unicode(LOCALHOST, config=True,
291 294 help="The IP address the notebook server will listen on."
292 295 )
293 296
294 297 def _ip_changed(self, name, old, new):
295 298 if new == u'*': self.ip = u''
296 299
297 300 port = Integer(8888, config=True,
298 301 help="The port the notebook server will listen on."
299 302 )
300 303 port_retries = Integer(50, config=True,
301 304 help="The number of additional ports to try if the specified port is not available."
302 305 )
303 306
304 307 certfile = Unicode(u'', config=True,
305 308 help="""The full path to an SSL/TLS certificate file."""
306 309 )
307 310
308 311 keyfile = Unicode(u'', config=True,
309 312 help="""The full path to a private key file for usage with SSL/TLS."""
310 313 )
311 314
312 315 password = Unicode(u'', config=True,
313 316 help="""Hashed password to use for web authentication.
314 317
315 318 To generate, type in a python/IPython shell:
316 319
317 320 from IPython.lib import passwd; passwd()
318 321
319 322 The string should be of the form type:salt:hashed-password.
320 323 """
321 324 )
322 325
323 326 open_browser = Bool(True, config=True,
324 327 help="""Whether to open in a browser after starting.
325 328 The specific browser used is platform dependent and
326 329 determined by the python standard library `webbrowser`
327 330 module, unless it is overridden using the --browser
328 331 (NotebookApp.browser) configuration option.
329 332 """)
330 333
331 334 browser = Unicode(u'', config=True,
332 335 help="""Specify what command to use to invoke a web
333 336 browser when opening the notebook. If not specified, the
334 337 default browser will be determined by the `webbrowser`
335 338 standard library module, which allows setting of the
336 339 BROWSER environment variable to override it.
337 340 """)
338 341
339 342 read_only = Bool(False, config=True,
340 343 help="Whether to prevent editing/execution of notebooks."
341 344 )
342 345
343 346 webapp_settings = Dict(config=True,
344 347 help="Supply overrides for the tornado.web.Application that the "
345 348 "IPython notebook uses.")
346 349
347 350 enable_mathjax = Bool(True, config=True,
348 351 help="""Whether to enable MathJax for typesetting math/TeX
349 352
350 353 MathJax is the javascript library IPython uses to render math/LaTeX. It is
351 354 very large, so you may want to disable it if you have a slow internet
352 355 connection, or for offline use of the notebook.
353 356
354 357 When disabled, equations etc. will appear as their untransformed TeX source.
355 358 """
356 359 )
357 360 def _enable_mathjax_changed(self, name, old, new):
358 361 """set mathjax url to empty if mathjax is disabled"""
359 362 if not new:
360 363 self.mathjax_url = u''
361 364
362 365 base_project_url = Unicode('/', config=True,
363 366 help='''The base URL for the notebook server''')
364 367 base_kernel_url = Unicode('/', config=True,
365 368 help='''The base URL for the kernel server''')
366 369 websocket_host = Unicode("", config=True,
367 370 help="""The hostname for the websocket server."""
368 371 )
369 372
370 373 extra_static_paths = List(Unicode, config=True,
371 374 help="""Extra paths to search for serving static files.
372 375
373 376 This allows adding javascript/css to be available from the notebook server machine,
374 377 or overriding individual files in the IPython"""
375 378 )
376 379 def _extra_static_paths_default(self):
377 380 return [os.path.join(self.profile_dir.location, 'static')]
378 381
379 382 @property
380 383 def static_file_path(self):
381 384 """return extra paths + the default location"""
382 385 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
383 386
384 387 mathjax_url = Unicode("", config=True,
385 388 help="""The url for MathJax.js."""
386 389 )
387 390 def _mathjax_url_default(self):
388 391 if not self.enable_mathjax:
389 392 return u''
390 393 static_url_prefix = self.webapp_settings.get("static_url_prefix",
391 394 "/static/")
392 395 try:
393 396 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
394 397 except IOError:
395 398 if self.certfile:
396 399 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
397 400 base = u"https://c328740.ssl.cf1.rackcdn.com"
398 401 else:
399 402 base = u"http://cdn.mathjax.org"
400 403
401 404 url = base + u"/mathjax/latest/MathJax.js"
402 405 self.log.info("Using MathJax from CDN: %s", url)
403 406 return url
404 407 else:
405 408 self.log.info("Using local MathJax from %s" % mathjax)
406 409 return static_url_prefix+u"mathjax/MathJax.js"
407 410
408 411 def _mathjax_url_changed(self, name, old, new):
409 412 if new and not self.enable_mathjax:
410 413 # enable_mathjax=False overrides mathjax_url
411 414 self.mathjax_url = u''
412 415 else:
413 416 self.log.info("Using MathJax: %s", new)
414 417
415 418 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
416 419 config=True,
417 420 help='The notebook manager class to use.')
418 421
419 422 def parse_command_line(self, argv=None):
420 423 super(NotebookApp, self).parse_command_line(argv)
421 424 if argv is None:
422 425 argv = sys.argv[1:]
423 426
424 427 # Scrub frontend-specific flags
425 428 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
426 429 # Kernel should inherit default config file from frontend
427 430 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
428 431
429 432 if self.extra_args:
430 433 f = os.path.abspath(self.extra_args[0])
431 434 if os.path.isdir(f):
432 435 nbdir = f
433 436 else:
434 437 self.file_to_run = f
435 438 nbdir = os.path.dirname(f)
436 439 self.config.NotebookManager.notebook_dir = nbdir
437 440
438 441 def init_configurables(self):
439 442 # force Session default to be secure
440 443 default_secure(self.config)
441 444 self.kernel_manager = MappingKernelManager(
442 445 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
443 446 connection_dir = self.profile_dir.security_dir,
444 447 )
445 448 kls = import_item(self.notebook_manager_class)
446 449 self.notebook_manager = kls(config=self.config, log=self.log)
447 450 self.notebook_manager.log_info()
448 451 self.notebook_manager.load_notebook_names()
449 452 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
450 453 self.cluster_manager.update_profiles()
451 454
452 455 def init_logging(self):
453 456 # This prevents double log messages because tornado use a root logger that
454 457 # self.log is a child of. The logging module dipatches log messages to a log
455 458 # and all of its ancenstors until propagate is set to False.
456 459 self.log.propagate = False
457 460
458 461 def init_webapp(self):
459 462 """initialize tornado webapp and httpserver"""
460 463 self.web_app = NotebookWebApplication(
461 464 self, self.kernel_manager, self.notebook_manager,
462 465 self.cluster_manager, self.log,
463 466 self.base_project_url, self.webapp_settings
464 467 )
465 468 if self.certfile:
466 469 ssl_options = dict(certfile=self.certfile)
467 470 if self.keyfile:
468 471 ssl_options['keyfile'] = self.keyfile
469 472 else:
470 473 ssl_options = None
471 474 self.web_app.password = self.password
472 475 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
473 476 if not self.ip:
474 477 warning = "WARNING: The notebook server is listening on all IP addresses"
475 478 if ssl_options is None:
476 479 self.log.critical(warning + " and not using encryption. This"
477 480 "is not recommended.")
478 481 if not self.password and not self.read_only:
479 482 self.log.critical(warning + "and not using authentication."
480 483 "This is highly insecure and not recommended.")
481 484 success = None
482 485 for port in random_ports(self.port, self.port_retries+1):
483 486 try:
484 487 self.http_server.listen(port, self.ip)
485 488 except socket.error as e:
486 489 if e.errno != errno.EADDRINUSE:
487 490 raise
488 491 self.log.info('The port %i is already in use, trying another random port.' % port)
489 492 else:
490 493 self.port = port
491 494 success = True
492 495 break
493 496 if not success:
494 497 self.log.critical('ERROR: the notebook server could not be started because '
495 498 'no available port could be found.')
496 499 self.exit(1)
497 500
498 501 def init_signal(self):
499 502 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
500 503 # safely extract zmq version info:
501 504 try:
502 505 zmq_v = zmq.pyzmq_version_info()
503 506 except AttributeError:
504 507 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
505 508 if 'dev' in zmq.__version__:
506 509 zmq_v.append(999)
507 510 zmq_v = tuple(zmq_v)
508 511 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
509 512 # This won't work with 2.1.7 and
510 513 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
511 514 # but it will work
512 515 signal.signal(signal.SIGINT, self._handle_sigint)
513 516 signal.signal(signal.SIGTERM, self._signal_stop)
514 517
515 518 def _handle_sigint(self, sig, frame):
516 519 """SIGINT handler spawns confirmation dialog"""
517 520 # register more forceful signal handler for ^C^C case
518 521 signal.signal(signal.SIGINT, self._signal_stop)
519 522 # request confirmation dialog in bg thread, to avoid
520 523 # blocking the App
521 524 thread = threading.Thread(target=self._confirm_exit)
522 525 thread.daemon = True
523 526 thread.start()
524 527
525 528 def _restore_sigint_handler(self):
526 529 """callback for restoring original SIGINT handler"""
527 530 signal.signal(signal.SIGINT, self._handle_sigint)
528 531
529 532 def _confirm_exit(self):
530 533 """confirm shutdown on ^C
531 534
532 535 A second ^C, or answering 'y' within 5s will cause shutdown,
533 536 otherwise original SIGINT handler will be restored.
534 537
535 538 This doesn't work on Windows.
536 539 """
537 540 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
538 541 time.sleep(0.1)
539 542 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
540 543 sys.stdout.flush()
541 544 r,w,x = select.select([sys.stdin], [], [], 5)
542 545 if r:
543 546 line = sys.stdin.readline()
544 547 if line.lower().startswith('y'):
545 548 self.log.critical("Shutdown confirmed")
546 549 ioloop.IOLoop.instance().stop()
547 550 return
548 551 else:
549 552 print "No answer for 5s:",
550 553 print "resuming operation..."
551 554 # no answer, or answer is no:
552 555 # set it back to original SIGINT handler
553 556 # use IOLoop.add_callback because signal.signal must be called
554 557 # from main thread
555 558 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
556 559
557 560 def _signal_stop(self, sig, frame):
558 561 self.log.critical("received signal %s, stopping", sig)
559 562 ioloop.IOLoop.instance().stop()
560 563
561 564 @catch_config_error
562 565 def initialize(self, argv=None):
563 566 self.init_logging()
564 567 super(NotebookApp, self).initialize(argv)
565 568 self.init_configurables()
566 569 self.init_webapp()
567 570 self.init_signal()
568 571
569 572 def cleanup_kernels(self):
570 573 """shutdown all kernels
571 574
572 575 The kernels will shutdown themselves when this process no longer exists,
573 576 but explicit shutdown allows the KernelManagers to cleanup the connection files.
574 577 """
575 578 self.log.info('Shutting down kernels')
576 579 km = self.kernel_manager
577 580 # copy list, since shutdown_kernel deletes keys
578 581 for kid in list(km.kernel_ids):
579 582 km.shutdown_kernel(kid)
580 583
581 584 def start(self):
582 585 ip = self.ip if self.ip else '[all ip addresses on your system]'
583 586 proto = 'https' if self.certfile else 'http'
584 587 info = self.log.info
585 588 info("The IPython Notebook is running at: %s://%s:%i%s" %
586 589 (proto, ip, self.port,self.base_project_url) )
587 590 info("Use Control-C to stop this server and shut down all kernels.")
588 591
589 592 if self.open_browser or self.file_to_run:
590 593 ip = self.ip or '127.0.0.1'
591 594 try:
592 595 browser = webbrowser.get(self.browser or None)
593 596 except webbrowser.Error as e:
594 597 self.log.warn('No web browser found: %s.' % e)
595 598 browser = None
596 599
597 600 if self.file_to_run:
598 601 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
599 602 url = self.notebook_manager.rev_mapping.get(name, '')
600 603 else:
601 604 url = ''
602 605 if browser:
603 606 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
604 607 self.port, self.base_project_url, url), new=2)
605 608 threading.Thread(target=b).start()
606 609 try:
607 610 ioloop.IOLoop.instance().start()
608 611 except KeyboardInterrupt:
609 612 info("Interrupted...")
610 613 finally:
611 614 self.cleanup_kernels()
612 615
613 616
614 617 #-----------------------------------------------------------------------------
615 618 # Main entry point
616 619 #-----------------------------------------------------------------------------
617 620
618 621 def launch_new_instance():
619 622 app = NotebookApp.instance()
620 623 app.initialize()
621 624 app.start()
622 625
General Comments 0
You need to be logged in to leave comments. Login now