(in-package :empire) (defparameter *version* "0.0.1") ;; ;; Connection ;; (defvar *default-empire-server* "localhost") (defvar *default-empire-server-port* 6665) (defclass connection () ((user :initarg :user :accessor user) (password :initarg :password :accessor password :initform nil) (server-name :initarg :server-name :accessor server-name :initform "Unknown server") (server-port :initarg :server-port :accessor server-port :initform *default-empire-server-port*) (socket :initarg :socket :accessor socket :documentation "Slot to store socket (for internal use only).") (stream :initarg :stream :accessor network-stream :documentation "Stream used to talk to the empire server.") (connection-handler :accessor connection-handler :initform nil :documentation "Handler function for incoming messages") (mode :accessor connection-mode :initform nil :documentation "TODO(gefla)") (session :accessor session :initarg :session :documentation "web session to which this connection belongs") (send-queue :accessor send-queue :initform (locked-queue:create) :documentation "lines outstanding to be sent at next prompt"))) (defun make-connection (&key (user nil) (password nil) (server-name *default-empire-server*) (server-port *default-empire-server-port*) (socket nil) (network-stream nil) session) (make-instance 'connection :user user :password password :server-name server-name :server-port server-port :socket socket :stream network-stream :session session)) ;;; interface (defun connect (&key (user nil) (password nil) (server-name *default-empire-server*) (server-port *default-empire-server-port*) session) "Connect to server and return a connection object." (let* ((socket (usocket:socket-connect server-name server-port)) (stream (usocket:socket-stream socket)) (connection (make-connection :user user :password password :socket socket :network-stream stream :server-name server-name :server-port server-port :session session)) (handler (spawn-with-name (format nil "empire-handler-~a" user) #'handle-connection connection))) (setf (connection-handler connection) handler) (empire-log:info "~a: connect" connection) connection)) (defgeneric quit (connection)) (defgeneric handle-connection (connection)) (defgeneric read-message (connection)) (defgeneric send-message (connection message &key next-mode)) (defgeneric connected-p (connection)) (defgeneric reconnect (conncetion)) (defmethod connected-p ((c connection)) (sb-thread:thread-alive-p (connection-handler c))) (defmethod reconnect ((c connection)) (with-slots (server-name server-port user password socket stream connection-handler) c (let* ((sock (usocket:socket-connect server-name server-port)) (s (usocket:socket-stream sock))) (setf socket sock stream s connection-handler (spawn-with-name (format nil "empire-handler-~a" user) #'handle-connection c)))) (empire-log:info "~a: reconnect" c)) (defparameter +C_CMDOK+ "0") (defparameter +C_DATA+ "1") (defparameter +C_INIT+ "2") (defparameter +C_EXIT+ "3") (defparameter +C_FLUSH+ "4") (defparameter +C_NOECHO+ "5") (defparameter +C_PROMPT+ "6") (defparameter +C_ABORT+ "7") (defparameter +C_REDIR+ "8") (defparameter +C_PIPE+ "9") (defparameter +C_CMDERR+ "a") (defparameter +C_BADCMD+ "b") (defparameter +C_EXECUTE+ "c") (defparameter +C_FLASH+ "d") (defparameter +C_INFORM+ "e") (defclass base-mode () ((connection :initarg :connection :accessor connection))) (defclass init-mode (base-mode) ((phase :initform :initial :documentation "Initialization phase"))) (defclass play-mode (base-mode) nil) (defun make-mode (connection mode) (make-instance mode :connection connection)) (defgeneric set-new-mode (base-mode new-mode)) (defmethod set-new-mode ((m base-mode) new-mode) (let* ((c (connection m)) (mode (make-mode c new-mode))) (empire-log:info "~a: set-new-mode ~a -> ~a" c (connection-mode c) new-mode) (setf (connection-mode c) mode))) (defgeneric handle-cmdok (base-mode message)) (defgeneric handle-data (base-mode message)) (defgeneric handle-init (base-mode message)) (defgeneric handle-exit (base-mode message)) (defgeneric handle-prompt (base-mode message)) (defgeneric handle-flush (base-mode message)) (defgeneric handle-simple-message (base-mode message)) (defgeneric handle-ignore (base-mode message)) (defmethod handle-init ((m init-mode) message) (declare (ignorable message)) (with-slots (phase connection) m (ecase phase (:initial (handle-cmdok m message)) (:play-sent (set-new-mode m 'play-mode))))) (defmethod handle-cmdok ((m init-mode) message) (declare (ignorable message)) (with-slots (phase connection) m (flet ((init-phase (mode send-args next-phase) (with-slots (phase connection) mode (send-message-one connection (apply #'format nil send-args)) (setf phase next-phase)))) (with-slots (user password) (connection m) (ecase phase (:initial (init-phase m `("client eow ~a" ,*version*) :client-sent)) (:client-sent (init-phase m `("coun ~a" ,user) :coun-sent)) (:coun-sent (init-phase m `("pass ~a" ,password) :pass-sent)) (:pass-sent (init-phase m '("play") :play-sent))))))) (defmethod handle-exit ((m init-mode) message) (declare (ignorable message)) (with-slots (phase connection) m (ecase phase (:play-sent (send-message-one connection "kill") (setf phase :kill-sent)) (:kill-sent (send-message-one connection "play") (setf phase :play-sent))))) (defmethod handle-data ((m init-mode) message) (declare (ignorable message)) t) (defmethod handle-exit ((m play-mode) message) (declare (ignorable message)) (quit (connection m))) (defmethod handle-data ((m play-mode) message) (let* ((c (connection m))) (empire-web:data (session c) message))) (defmethod handle-simple-message ((m play-mode) message) (let* ((c (connection m))) (empire-web:data (session c) message))) (defmethod handle-flush ((m play-mode) message) (let* ((c (connection m))) (empire-web:prompt (session c) message) (send-next-line c))) (defmethod send-next-line ((c connection)) (let ((next-event (locked-queue:dequeue (send-queue c)))) (etypecase next-event (string (send-message-one c next-event)) (cons (let ((message (car next-event)) (mode (cdr next-event))) (send-message-one c message) (set-new-mode (connection-mode c) mode)))))) (defun read-no-eval (stream) (let ((*read-eval* nil)) (read stream))) (defmethod handle-prompt ((m play-mode) message) (let* ((c (connection m))) (with-input-from-string (s message) (let* ((minutes (read-no-eval s)) (btus (read-no-eval s))) (empire-web:prompt (session c) (format nil "[~a,~a]: " minutes btus)))) (send-next-line c))) (defmethod handle-ignore ((m init-mode) message) (declare (ignorable message)) t) (defparameter *line-type-dispatch* (list `(,+C_DATA+ . handle-data) `(,+C_INIT+ . handle-init) `(,+C_EXIT+ . handle-exit) `(,+C_FLUSH+ . handle-flush) `(,+C_BADCMD+ . handle-simple-message) `(,+C_FLASH+ . handle-simple-message) `(,+C_CMDOK+ . handle-cmdok) `(,+C_PROMPT+ . handle-prompt))) (defun parse-server-line (line) (let* ((first-space-index (loop for i from 0 below (length line) if (eq (char line i) #\Space) do (return i))) (message-type (subseq line 0 first-space-index)) (message (if first-space-index (subseq line (+ first-space-index 1)) ""))) (values message message-type))) (defmethod handle-connection ((c connection)) (setf (connection-mode c) (make-mode c 'init-mode)) (loop while (read-message c))) (defun lookup-handler (message-type) (cdr (assoc message-type *line-type-dispatch* :test 'string=))) (define-condition no-handler (error) ((mode :initarg :mode) (type :initarg :type)) (:report (lambda (condition stream) (with-slots (mode type) condition (format stream "No handler for ~a in mode ~a." type mode))))) (defmethod read-message ((c connection)) (handler-case (let* ((s (network-stream c)) (line (read-line s))) (empire-log:info "~a: < ~a" c line) (multiple-value-bind (message type) (parse-server-line line) (let ((handler (lookup-handler type)) (mode (connection-mode c))) (if handler (funcall handler mode message) (error 'no-handler :mode mode :type type)))) line) (sb-int:closed-stream-error () nil) (sb-int:simple-stream-error () nil) (end-of-file () nil))) (defun raw-send-message (s message) (write-sequence message s) (terpri s) (force-output s)) (defgeneric send-message-one (connection string)) (defmethod send-message-one ((c connection) message) (empire-log:info "~a: > ~a" c message) (let ((s (network-stream c))) (raw-send-message s message))) (defmethod send-message ((c connection) message &key next-mode) (if (not (connected-p c)) (reconnect c)) (if next-mode (locked-queue:enqueue (send-queue c) (cons message next-mode)) (locked-queue:enqueue (send-queue c) message))) (defparameter +special-command-char+ #\;) (defun special-command-p (line) (and (> (length line) 0) (char= +special-command-char+ (aref line 0)))) (defclass xdump-mode (play-mode) ((parser :initform (xdump:make-parser)) (phase :initform :meta-meta) (dump-queue :initform nil) (dump-index :initform 0))) (defmethod handle-data ((m xdump-mode) message) (with-slots (connection parser phase dump-queue dump-index) m (if (xdump:parse-line parser message) ;;XXX consider something like a 'pop-mode function (ccase phase (:meta-meta (setf phase :meta-table parser (xdump:make-parser)) (send-message connection "xdump meta table")) (:meta-table (setf phase :table-table parser (xdump:make-parser)) (send-message connection "xdump table *")) (:table-table (setf phase :tables-meta dump-queue (xdump::table-entries (xdump::get-table "table")) dump-index 0 parser (xdump:make-parser)) (send-message connection (format nil "xdump meta ~a" (xdump-data::table-name (aref dump-queue 0))))) (:tables-meta (setf phase :tables-content parser (xdump:make-parser)) (send-message connection (format nil "xdump ~a *" (xdump-data::table-name (aref dump-queue dump-index))))) (:tables-content (if (< dump-index (fill-pointer dump-queue)) (progn (setf phase :tables-meta parser (xdump:make-parser)) (incf dump-index) (loop while (and (< dump-index (fill-pointer dump-queue)) (let ((dump-queue-entry (aref dump-queue dump-index))) (or (null dump-queue-entry) (string= "meta" (xdump-data::table-name dump-queue-entry))))) do (incf dump-index)) (if (and (<= dump-index (fill-pointer dump-queue)) (not (null (aref dump-queue dump-index)))) (send-message connection (format nil "xdump meta ~a" (xdump-data::table-name (aref dump-queue dump-index)))))) (set-new-mode (connection-mode connection) 'play-mode))))))) (defmethod special-xup ((c connection)) (send-message c "xdump meta meta" :next-mode 'xdump-mode)) (defmethod special-command ((c connection) line) (cond ((string= line "xup") (special-xup c)) (t (empire-web:data (session c) "Unknown special command"))) line) (defmethod command ((c connection) line) (cond ((special-command-p line) (special-command c (subseq line 1))) (t (send-message c line)))) (defmethod quit ((c connection)) (usocket:socket-close (socket c)))