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