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