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