Skip to content
Browse files

Work on the server side of the html notebook.

  • Loading branch information...
1 parent d7f2add commit 398f176bf6d9fc4d698d7068a85ba1c6aaef23d0 @ellisonbg ellisonbg committed Mar 23, 2011
View
98 IPython/frontend/html/notebook/kernelmanager.py
@@ -0,0 +1,98 @@
+import signal
+import sys
+import uuid
+
+from IPython.zmq.ipkernel import launch_kernel
+from session import SessionManager
+
+
+class KernelManager(object):
+
+ ip = '127.0.0.1'
+
+ def __init__(self, context):
+ self.context = context
+ self._kernels = {}
+
+ @property
+ def kernel_ids(self):
+ return self._kernels.keys()
+
+ def __len__(self):
+ return len(self.kernel_ids)
+
+ def __contains__(self, kernel_id):
+ if kernel_id in self.kernel_ids:
+ return True
+ else:
+ return False
+
+ def start_kernel(self):
+ kid = str(uuid.uuid4())
+ (process, shell_port, iopub_port, stdin_port, hb_port) = launch_kernel()
+ d = dict(
+ process = process,
+ stdin_port = stdin_port,
+ iopub_port = iopub_port,
+ shell_port = shell_port,
+ hb_port = hb_port,
+ session_manager = SessionManager(self, kid, self.context)
+ )
+ self._kernels[kid] = d
+ return kid
+
+ def kill_kernel(self, kernel_id):
+ kernel_process = self.get_kernel_process(kernel_id)
+ if kernel_process is not None:
+ # Attempt to kill the kernel.
+ try:
+ kernel_process.kill()
+ except OSError, e:
+ # In Windows, we will get an Access Denied error if the process
+ # has already terminated. Ignore it.
+ if not (sys.platform == 'win32' and e.winerror == 5):
+ raise
+ del self._kernels[kernel_id]
+
+ def interrupt_kernel(self, kernel_id):
+ kernel_process = self.get_kernel_process(kernel_id)
+ if kernel_process is not None:
+ if sys.platform == 'win32':
+ from parentpoller import ParentPollerWindows as Poller
+ Poller.send_interrupt(kernel_process.win32_interrupt_event)
+ else:
+ kernel_process.send_signal(signal.SIGINT)
+
+ def signal_kernel(self, kernel_id, signum):
+ """ Sends a signal to the kernel. Note that since only SIGTERM is
+ supported on Windows, this function is only useful on Unix systems.
+ """
+ kernel_process = self.get_kernel_process(kernel_id)
+ if kernel_process is not None:
+ kernel_process.send_signal(signum)
+
+ def get_kernel_process(self, kernel_id):
+ d = self._kernels.get(kernel_id)
+ if d is not None:
+ return d['process']
+ else:
+ raise KeyError("Kernel with id not found: %s" % kernel_id)
+
+ def get_kernel_ports(self, kernel_id):
+ d = self._kernels.get(kernel_id)
+ if d is not None:
+ dcopy = d.copy()
+ dcopy.pop('process')
+ return dcopy
+ else:
+ raise KeyError("Kernel with id not found: %s" % kernel_id)
+
+ def get_session_manager(self, kernel_id):
+ d = self._kernels.get(kernel_id)
+ if d is not None:
+ return d['session_manager']
+ else:
+ raise KeyError("Kernel with id not found: %s" % kernel_id)
+
+
+
View
112 IPython/frontend/html/notebook/notebook.py
@@ -1,38 +1,122 @@
-
+import json
+import logging
import os
+import uuid
+
+import zmq
-import tornado.httpserver
+# Install the pyzmq ioloop. This has to be done before anything else from
+# tornado is imported.
+from zmq.eventloop.zmqstream import ZMQStream
+from zmq.eventloop import ioloop
import tornado.ioloop
-import tornado.options
-import tornado.web
+tornado.ioloop = ioloop
+
+from tornado import httpserver
+from tornado import options
+from tornado import web
+from tornado import websocket
+
+from kernelmanager import KernelManager
-from tornado.options import define, options
+options.define("port", default=8888, help="run on the given port", type=int)
-define("port", default=8888, help="run on the given port", type=int)
+_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
+_session_id_regex = r"(?P<session_id>\w+)"
-class MainHandler(tornado.web.RequestHandler):
+
+class MainHandler(web.RequestHandler):
def get(self):
self.render('notebook.html')
-class NotebookApplication(tornado.web.Application):
+class KernelHandler(web.RequestHandler):
+
+ def get(self):
+ self.write(json.dumps(self.application.kernel_manager.kernel_ids))
+
+ def post(self):
+ kid = self.application.kernel_manager.start_kernel()
+ logging.info("Starting kernel: %s" % kid)
+ self.write(json.dumps(kid))
+
+
+class SessionHandler(web.RequestHandler):
+
+ def post(self, *args, **kwargs):
+ kernel_id = kwargs['kernel_id']
+ session_id = kwargs['session_id']
+ logging.info("Starting session: %s, %s" % (kernel_id,session_id))
+ km = self.application.kernel_manager
+ sm = km.get_session_manager(kernel_id)
+ sm.start_session(session_id)
+ self.finish()
+
+
+class ZMQStreamHandler(websocket.WebSocketHandler):
+
+ stream_name = ''
+
+ def open(self, *args, **kwargs):
+ kernel_id = kwargs['kernel_id']
+ session_id = kwargs['session_id']
+ logging.info("Connection open: %s, %s" % (kernel_id,session_id))
+ sm = self.application.kernel_manager.get_session_manager(kernel_id)
+ method_name = "get_%s_stream" % self.stream_name
+ method = getattr(sm, method_name)
+ self.zmq_stream = method(session_id)
+ self.zmq_stream.on_recv(self._on_zmq_reply)
+ self.session_manager = sm
+ self.session_id = session_id
+
+ def on_message(self, msg):
+ logging.info("Message received: %r" % msg)
+ self.zmq_stream.send(msg)
+
+ def on_close(self):
+ logging.info("Connection closed: %s, %s" % (kernel_id,session_id))
+ self.zmq_stream.close()
+
+ def _on_zmq_reply(self, msg):
+ logging.info("Message reply: %r" % msg)
+ self.write_message(msg)
+
+
+class IOPubStreamHandler(ZMQStreamHandler):
+
+ stream_name = 'iopub'
+
+
+class ShellStreamHandler(ZMQStreamHandler):
+
+ stream_name = 'shell'
+
+
+class NotebookApplication(web.Application):
+
def __init__(self):
handlers = [
- (r"/", MainHandler)
+ (r"/", MainHandler),
+ (r"/kernels", KernelHandler),
+ (r"/kernels/%s/sessions/%s" % (_kernel_id_regex,_session_id_regex), SessionHandler),
+ (r"/kernels/%s/sessions/%s/iopub" % (_kernel_id_regex,_session_id_regex), IOPubStreamHandler),
+ (r"/kernels/%s/sessions/%s/shell" % (_kernel_id_regex,_session_id_regex), ShellStreamHandler),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
)
- tornado.web.Application.__init__(self, handlers, **settings)
+ web.Application.__init__(self, handlers, **settings)
+ self.context = zmq.Context()
+ self.kernel_manager = KernelManager(self.context)
def main():
- tornado.options.parse_command_line()
+ options.parse_command_line()
application = NotebookApplication()
- http_server = tornado.httpserver.HTTPServer(application)
- http_server.listen(options.port)
- tornado.ioloop.IOLoop.instance().start()
+ http_server = httpserver.HTTPServer(application)
+ http_server.listen(options.options.port)
+ ioloop.IOLoop.instance().start()
if __name__ == "__main__":
View
56 IPython/frontend/html/notebook/session.py
@@ -0,0 +1,56 @@
+import logging
+
+import zmq
+from zmq.eventloop.zmqstream import ZMQStream
+
+
+class SessionManager(object):
+
+ def __init__(self, kernel_manager, kernel_id, context):
+ self.context = context
+ self.kernel_manager = kernel_manager
+ self.kernel_id = kernel_id
+ self._sessions = {}
+
+ def __del__(self):
+ self.stop_all()
+
+ def start_session(self, session_id):
+ ports = self.kernel_manager.get_kernel_ports(self.kernel_id)
+ iopub_stream = self.create_connected_stream(ports['iopub_port'], zmq.SUB)
+ shell_stream = self.create_connected_stream(ports['shell_port'], zmq.XREQ)
+ self._sessions[session_id] = dict(
+ iopub_stream = iopub_stream,
+ shell_stream = shell_stream
+ )
+
+ def stop_session(self, session_id):
+ session_dict = self._sessions.get(session_id)
+ if session_dict is not None:
+ for name, stream in session_dict.items():
+ stream.close()
+ del self._sessions[session_id]
+
+ def stop_all(self):
+ for session_id in self._sessions.keys():
+ self.stop_session(session_id)
+
+ def create_connected_stream(self, port, socket_type):
+ sock = self.context.socket(socket_type)
+ addr = "tcp://%s:%i" % (self.kernel_manager.ip, port)
+ logging.info("Connecting to: %s" % addr)
+ sock.connect(addr)
+ return ZMQStream(sock)
+
+ def get_stream(self, session_id, stream_name):
+ session_dict = self._sessions.get(session_id)
+ if session_dict is not None:
+ return session_dict[stream_name]
+ else:
+ raise KeyError("Session with id not found: %s" % session_id)
+
+ def get_iopub_stream(self, session_id):
+ return self.get_stream(session_id, 'iopub_stream')
+
+ def get_shell_stream(self, session_id):
+ return self.get_stream(session_id, 'shell_stream')
View
47 IPython/frontend/html/notebook/static/js/notebook.js
@@ -8,10 +8,11 @@ var IPYTHON = {};
var Notebook = function (selector) {
this.element = $(selector);
+ this.element.scroll();
this.element.data("notebook", this);
this.next_prompt_number = 1;
this.bind_events();
-}
+};
Notebook.prototype.bind_events = function () {
@@ -197,6 +198,7 @@ Notebook.prototype.move_cell_down = function (index) {
Notebook.prototype.sort_cells = function () {
var ncells = this.ncells();
+ var sindex = this.selected_index();
var swapped;
do {
swapped = false
@@ -209,6 +211,7 @@ Notebook.prototype.sort_cells = function () {
};
};
} while (swapped);
+ this.select(sindex);
return this;
};
@@ -464,7 +467,7 @@ TextCell.prototype.select = function () {
TextCell.prototype.edit = function () {
var text_cell = this.element;
var input = text_cell.find("textarea.text_cell_input");
- var output = text_cell.find("div.text_cell_render");
+ var output = text_cell.find("div.text_cell_render");
output.hide();
input.show().trigger('focus');
};
@@ -475,6 +478,10 @@ TextCell.prototype.render = function () {
var input = text_cell.find("textarea.text_cell_input");
var output = text_cell.find("div.text_cell_render");
var text = input.val();
+ if (text === "") {
+ text = this.placeholder;
+ input.val(text);
+ };
output.html(text)
input.html(text);
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
@@ -501,6 +508,42 @@ TextCell.prototype.config_mathjax = function () {
//============================================================================
+var KernelManager = function () {
+ this.kernelid = null;
+ this.baseurl = "/kernels";
+};
+
+
+KernelManager.prototype.create_kernel = function () {
+ var that = this;
+ $.post(this.baseurl, function (data) {
+ that.kernelid = data;
+ }, 'json');
+}
+
+
+KernelManager.prototype.execute = function (code, callback) {
+ var msg = {
+ header : {msg_id : 0, username : "bgranger", session: 0},
+ msg_type : "execute_request",
+ content : {code : code}
+ };
+ var settings = {
+ data : JSON.stringify(msg),
+ processData : false,
+ contentType : "application/json",
+ success : callback,
+ type : "POST"
+ }
+ var url = this.baseurl + "/" + this.kernelid + "/" + ""
+}
+
+
+//============================================================================
+// On document ready
+//============================================================================
+
+
$(document).ready(function () {
MathJax.Hub.Config({
View
80 IPython/frontend/html/notebook/zmqhttp.py
@@ -0,0 +1,80 @@
+import json
+
+from tornado import web
+
+import logging
+
+
+class ZMQHandler(web.RequestHandler):
+
+ def get_stream(self):
+ """Get the ZMQStream for this request."""
+ raise NotImplementedError('Implement get_stream() in a subclass.')
+
+ def _save_method_args(self, *args, **kwargs):
+ """Save the args and kwargs to get/post/put/delete for future use.
+
+ These arguments are not saved in the request or handler objects, but
+ are often needed by methods such as get_stream().
+ """
+ self._method_args = args
+ self._method_kwargs = kwargs
+
+ def _handle_msgs(self, msg):
+ msgs = [msg]
+ stream = self.get_stream()
+ stream.on_recv(lambda m: msgs.append(json.loads(m)))
+ stream.flush()
+ stream.stop_on_recv()
+ logging.info("Reply: %r" % msgs)
+ self.write(json.dumps(msgs))
+ self.finish()
+
+
+class ZMQPubHandler(ZMQHandler):
+
+ SUPPORTED_METHODS = ("POST",)
+
+ def post(self, *args, **kwargs):
+ self._save_method_args(*args, **kwargs)
+ try:
+ msg = json.loads(self.request.body)
+ except:
+ self.send_error(status_code=415)
+ else:
+ logging.info("Request: %r" % msg)
+ self.get_stream().send_json(msg)
+
+
+class ZMQSubHandler(ZMQHandler):
+
+ SUPPORTED_METHODS = ("GET",)
+
+ @web.asynchronous
+ def get(self, *args, **kwargs):
+ self._save_method_args(*args, **kwargs)
+ self.get_stream().on_recv(self._handle_msgs)
+
+
+class ZMQXReqHandler(ZMQHandler):
+
+ SUPPORTED_METHODS = ("POST",)
+
+ @web.asynchronous
+ def post(self, *args, **kwargs):
+ self._save_method_args(*args, **kwargs)
+ logging.info("request: %r" % self.request)
+ try:
+ msg = json.loads(self.request.body)
+ except:
+ self.send_error(status_code=415)
+ else:
+ logging.info("Reply: %r" % msg)
+ stream = self.get_stream()
+ stream.send_json(msg)
+ stream.on_recv(self._handle_msgs)
+
+
+
+
+
View
23 IPython/frontend/html/notebook/zmqws.py
@@ -0,0 +1,23 @@
+"""A simple WebSocket to ZMQ forwarder."""
+
+from tornado import websocket
+
+class ZMQWebSocketBridge(websocket.WebSocketHandler):
+ """A handler to forward between a WebSocket at ZMQ socket."""
+
+ def open(self):
+ self.stream
+
+ @property
+ def stream(self):
+ raise NotImplementedError("stream property must be implemented in a subclass")
+
+ def on_message(self, message):
+ self.stream.send(message)
+
+ def on_zmq_reply(self, reply_list):
+ for part in reply_list:
+ self.write_message(part)
+
+ def on_close(self):
+ print "WebSocket closed"
@fperez
IPython member
fperez added a note Aug 17, 2011

Is a naked print a good idea here? If so, then at least we should do

from __future__ import print_function

at the top and use print() instead, so the 2to3 changes are minimized.

@minrk
IPython member
minrk added a note Aug 17, 2011

Since it's one of the few things 2to3 handles well, making the from __future__ import print_function change manually, really has zero benefits as far as I know, and is primarily a cause for annoyance.

@fperez
IPython member
fperez added a note Aug 17, 2011

I guess I'm just getting used to using print as a function everywhere in anticipation of the transition to 3, so it doesn't bother me much. But I agree that it's not a big deal, and I have no problem if it stays as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

0 comments on commit 398f176

Please sign in to comment.
Something went wrong with that request. Please try again.