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