src/server.lisp @ 15eabbd388ea default tip

Add a restart for ports already in use
author Steve Losh <steve@stevelosh.com>
date Tue, 14 Mar 2017 17:46:14 +0000
parents ca726222b6f1
children (none)
(in-package :nrepl)

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


;;;; Plumbing
(defun handle-base (message)
  "Base handler function.

  Just returns a fallback response if no earlier middleware handled the 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*
    (log-message "~%; Handling message: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv~%")
    (log-message "~S~%" message)
    (log-message "; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^~%"))
  (funcall (build-handler #'handle-base (middleware)) message))

(defun handle-message (socket-stream lock)
  "Read and handle a single message from `socket-stream`."
  (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 `socket-stream`, handling each."
  (log-message "Client connected...")
  (handler-case (loop (handle-message socket-stream lock))
    (end-of-file () nil))
  (log-message "Client disconnected..."))


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

(define-condition port-error (error)
  ((port :initarg :port))
  (:report (lambda (err stream)
             (format stream "Port ~D is already in use"
                     (slot-value err 'port)))))

(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 socket-listen (address port)
  (handler-case
      (values (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))
              port)
    (usocket:address-in-use-error ()
      (restart-case (error 'port-error :port port)
        (use-different-port (new-port)
          :report (lambda (stream)
                    (format stream "Select a port other than ~D" port))
          :interactive (lambda ()
                         (format *query-io* "~&Type a form to be evaluated:~%")
                         (finish-output *query-io*)
                         (list (read *query-io*)))
          (socket-listen address new-port))))))


(defun accept-connections (server-socket)
  "Accept connections to the server and spawn threads to handle each."
  (log-message "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 (&key (address "127.0.0.1") (port 8675))
  "Fire up a server thread that will listen for connections."
  (log-message "Starting server...~%")
  (multiple-value-bind (socket port) (socket-listen address port)
    (setf *server-thread*
          (run-in-thread (format nil "NREPL Server (~a/~a)" address port)
            (unwind-protect
                (accept-connections socket)
              (log-message "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
      (log-message "Stopping server...~%")
      (bt:destroy-thread s))))