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