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