room level messages, client disconnection
[mudsync.git] / mudsync / networking.scm
1 (define-module (mudsync networking)
2   #:use-module (8sync systems actors)
3   #:use-module (8sync agenda)
4   #:use-module (ice-9 format)
5   #:use-module (ice-9 match)
6   #:use-module (oop goops)
7
8   #:export (;; Should we be exporting these?
9             %default-server
10             %default-port
11
12             <network-manager>
13             nm-close-everything))
14
15 ;;; Networking
16 ;;; ==========
17
18 (define %default-server #f)
19 (define %default-port 8889)
20
21 (define-class <network-manager> (<actor>)
22   (server-socket #:getter nm-server-socket)
23   ;; mapping of client -> client-id
24   (clients #:getter nm-clients
25            #:init-thunk make-hash-table)
26   ;; send input to this actor
27   (send-input-to #:getter nm-send-input-to
28                  #:init-keyword #:send-input-to)
29   (message-handler
30    #:init-value
31    (make-action-dispatch
32     ((start-listening actor message)
33      (nm-install-socket actor (message-ref message 'server %default-server)
34                         (message-ref message 'port %default-port)))
35     ((send-to-client actor message client data)
36      (nm-send-to-client-id actor client data)))))
37
38 (define-method (nm-close-everything (nm <network-manager>) remove-from-agenda)
39   "Shut it down!"
40   ;; close all clients
41   (hash-for-each
42    (lambda (_ client)
43      (close client)
44      (if remove-from-agenda
45          (8sync-port-remove client)))
46    (nm-clients nm))
47   ;; reset the clients list
48   (set! (nm-clients) (make-hash-table))
49   ;; close the server
50   (close (nm-server-socket nm))
51   (if remove-from-agenda
52       (8sync-port-remove (nm-server-socket nm))))
53
54 ;; Maximum number of backlogged connections when we listen
55 (define %maximum-backlog-conns 128)     ; same as SOMAXCONN on Linux 2.X,
56                                         ; says the intarwebs
57
58 (define (nm-install-socket nm server port)
59   "Install socket on SERVER with PORT"
60   (let ((s (socket PF_INET  ; ipv4
61                    SOCK_STREAM  ; two-way connection-based byte stream
62                    0))
63         (addr (if server
64                   (inet-pton AF_INET server)
65                   INADDR_LOOPBACK)))
66     ;; Totally mimed from the Guile manual.  Not sure if we need this, but:
67     ;; http://www.unixguide.net/network/socketfaq/4.5.shtml
68     (setsockopt s SOL_SOCKET SO_REUSEADDR 1) ; reuse port even if port is busy
69     ;; Connecting to a non-specific address:
70     ;;   (bind s AF_INET INADDR_ANY port)
71     ;; Should this be an option?  Guess I don't know why we'd need it
72     ;; @@: If we wanted to support listening on a particular hostname,
73     ;;   could see 8sync's irc.scm...
74     (bind s AF_INET addr port)
75     ;; Listen to connections
76     (listen s %maximum-backlog-conns)
77
78     ;; Throw a system-error rather than block on an (accept)
79     ;; that has nothing to do
80     (fcntl s F_SETFL
81            (logior O_NONBLOCK
82                    (fcntl s F_GETFL)))
83
84     ;; @@: This is used in Guile's http server under the commit:
85     ;;       * module/web/server/http.scm (http-open): Ignore SIGPIPE. Keeps the
86     ;;         server from dying in some circumstances.
87     ;;   (sigaction SIGPIPE SIG_IGN)
88     ;; Will this break other things that use pipes for us though?
89
90     (slot-set! nm 'server-socket s)
91
92     (format #t "Listening for clients in pid: ~s\n" (getpid))
93     (8sync-port s #:read (lambda (s) (nm-new-client nm s)))
94     ;; TODO: set up periodic close of idle connections?
95     ))
96
97 (define (nm-new-client nm s)
98   "Handle new client coming in to socket S"
99   (let* ((client-connection (accept s))
100          (client-details (cdr client-connection))
101          (client (car client-connection)))
102     (format #t "New client: ~s\n" client-details)
103     (format #t "Client address: ~s\n"
104             (gethostbyaddr
105              (sockaddr:addr client-details)))
106
107     (let ((client-id (big-random-number)))
108       (hash-set! (nm-clients nm) client-id client)
109       ;; @@: Do we need an 8sync-port-wait here?
110       ;;   Is such a thing even possible? :\
111       (8sync-port client #:read (nm-make-client-receive nm client-id))
112       (<- nm (nm-send-input-to nm) 'new-client #:client client-id))))
113
114 (define (nm-make-client-receive nm client-id)
115   "Make a method to receive client data"
116   (let ((buffer '()))
117     (define (reset-buffer)
118       (set! buffer '()))
119     (define (should-read-char client)
120       (and (not (port-closed? client))
121            (char-ready? client)
122            (not (eof-object? (peek-char client)))))
123     (define (receive-handler client)
124       (while (should-read-char client)
125         (set! buffer (cons (read-char client) buffer))
126         (match buffer
127           (;; @@: Do we need the "char?"
128            (#\newline #\return (? char? line-chars) ...)
129            (let ((ready-line (list->string (reverse line-chars))))
130              ;; reset buffer
131              (set! buffer '())
132              ;; run it
133              (nm-handle-line nm client client-id ready-line)))
134           (_ #f)))
135       ;; Shut things down on closed port or EOF object
136       (cond
137        ((port-closed? client)
138         (nm-handle-port-closed nm client client-id))
139        ((and (char-ready? client)
140              (eof-object? (peek-char client)))
141         (nm-handle-port-eof nm client client-id))))
142     receive-handler))
143
144 (define (nm-handle-port-closed nm client client-id)
145   "Handle a closed port"
146   (format #t "DEBUG: handled closed port ~x\n" client-id)
147   (8sync-port-remove client)
148   (hash-remove! (nm-clients nm) client-id)
149   (<- nm (nm-send-input-to nm) 'client-closed #:client client-id))
150
151 (define-method (nm-handle-port-eof nm client client-id)
152   "Handle seeing an EOF on port"
153   (format #t "DEBUG: handled eof-object on port ~x\n" client-id)
154   (close client)
155   (8sync-port-remove client)
156   (hash-remove! (nm-clients nm) client-id)
157   (<- nm (nm-send-input-to nm) 'client-closed #:client client-id))
158
159 (define-method (nm-handle-line nm client client-id line)
160   "Handle an incoming line of input from a client"
161   (<- nm (nm-send-input-to nm) 'client-input
162       #:data line
163       #:client client-id))
164
165 (define-method (nm-send-to-client-id nm client-id data)
166   "Send DATA to TO-CLIENT id"
167   (define client-obj (hash-ref (nm-clients nm) client-id))
168   (if (not client-obj)
169       (throw 'no-such-client
170              "Asked to send data to client but that client doesn't exist"
171              #:client-id client-id
172              #:data data))
173   (display data client-obj))