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