src/server.lisp @ b7a399c67d24

Keming
author Steve Losh <steve@stevelosh.com>
date Sun, 21 Aug 2016 11:42:23 +0000
parents 743c0a981785
children ba758da80c62
(in-package #:nrepl)

;;;; Config
(defvar *verbose-debug-output* nil)
(defvar *unthreaded* nil)


;;;; Plumbing
(defun handle-base (message)
  (respond message (make-map "status" '("unknown-op"))))

(defun middleware ()
  "Return the stack of middleware.

   In the future we should make this less horrifyingly inefficient, but for
   NREPL development its_fine.

   "
  (list
    #'wrap-session
    #'wrap-session-ls
    #'wrap-session-clone
    #'wrap-session-close
    #'wrap-describe
    #'wrap-load-file
    #'wrap-macroexpand
    #'wrap-eval
    #'wrap-documentation
    #'wrap-arglist
    ))

(defun build-handler (base middleware)
  "Collapse the stack of middleware into a single handler function."
  (if middleware
    (funcall (car middleware)
             (build-handler base (cdr middleware)))
    base))

(defun handle (message)
  "Handle the given NREPL message."
  (when *verbose-debug-output*
    (l "Handling message: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv~%")
    (l "~A~%" message)
    (l "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^~%"))
  (funcall (build-handler #'handle-base (middleware)) message))

(defun handle-message (socket-stream lock)
  "Read and handle a single message from the socket."
  (let ((message (fset:with (read-object socket-stream)
                   "transport" (curry #'write-object socket-stream lock))))
    (handle message)))

(defun handler (socket-stream lock)
  "Read a series of messages from the socket-stream, handling each."
  (p "Client connected...")
  (handler-case (loop (handle-message socket-stream lock))
    (end-of-file () nil))
  (p "Client disconnected..."))


;;;; Server
(defvar *server-thread* nil)

(defmacro run-in-thread (thread-name &rest body)
  "Run `body` in a thread called `name` (usually).  Return the thread.

  If `nrepl::*unthreaded*` is true, the body will be executed immediately in the
  current thread and `nil` will be returned instead.  Useful for debugging.

  "
  `(let ((thunk (lambda () ,@body)))
    (if *unthreaded*
      (progn (funcall thunk) nil)
      (bt:make-thread thunk :name ,thread-name))))


(defun accept-connections (server-socket)
  "Accept connections to the server and spawn threads to handle each."
  (format t "Accepting connections...~%")
  (loop
    (let* ((client-socket (usocket:socket-accept
                            server-socket
                            :element-type '(unsigned-byte 8)))
           (socket-stream (flex:make-flexi-stream
                            (usocket:socket-stream client-socket)
                            :external-format :utf-8))
           (write-lock (bt:make-lock "NREPL client writing lock")))
      (run-in-thread "NREPL Connection Handler"
        (unwind-protect
            (handler socket-stream write-lock)
          (usocket:socket-close client-socket))))))

(defun start-server (&optional (address "127.0.0.1") (port 8675))
  "Fire up a server thread that will listen for connections."
  (format t "Starting server...~%")
  (let ((socket (usocket:socket-listen
                  address port
                  :reuse-address t
                  ;; have to specify element-type here too because usocket+CCL
                  ;; fucks it up if you only specify it in socket-accept
                  :element-type '(unsigned-byte 8))))
    (setf *server-thread*
          (run-in-thread (format nil "NREPL Server (~a/~a)" address port)
            (unwind-protect
                (accept-connections socket)
              (format t "Closing server socket...~%")
              (usocket:socket-close socket))))))

(defun stop-server ()
  "Kill the server thread, if it exists."
  (let ((s (shiftf *server-thread* nil)))
    (when s
      (bt:destroy-thread s))))