From 398f176bf6d9fc4d698d7068a85ba1c6aaef23d0 2011-07-21 03:42:31
From: Brian Granger <ellisonbg@gmail.com>
Date: 2011-07-21 03:42:31
Subject: [PATCH] Work on the server side of the html notebook.

---

diff --git a/IPython/frontend/html/notebook/kernelmanager.py b/IPython/frontend/html/notebook/kernelmanager.py
new file mode 100644
index 0000000..9a94f2c
--- /dev/null
+++ b/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)
+
+
+
diff --git a/IPython/frontend/html/notebook/notebook.py b/IPython/frontend/html/notebook/notebook.py
index bf67231..d03545b 100644
--- a/IPython/frontend/html/notebook/notebook.py
+++ b/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__":
diff --git a/IPython/frontend/html/notebook/session.py b/IPython/frontend/html/notebook/session.py
new file mode 100644
index 0000000..bf7df13
--- /dev/null
+++ b/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')
diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js
index 93c2dd9..ffdc09f 100644
--- a/IPython/frontend/html/notebook/static/js/notebook.js
+++ b/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({
diff --git a/IPython/frontend/html/notebook/zmqhttp.py b/IPython/frontend/html/notebook/zmqhttp.py
new file mode 100644
index 0000000..58a4b4f
--- /dev/null
+++ b/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)
+
+
+
+
+    
\ No newline at end of file
diff --git a/IPython/frontend/html/notebook/zmqws.py b/IPython/frontend/html/notebook/zmqws.py
new file mode 100644
index 0000000..e2eab10
--- /dev/null
+++ b/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"