diff --git a/.gitignore b/.gitignore index 49a9d25..7c07843 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ # ignore temporary files *~ + +# ignore logs and configuration files +*.out +*.conf + +# ignore racket compile files +*.dep +*.zo diff --git a/Hermes/Makefile b/Hermes/Makefile index eda5bbb..fdd9a07 100644 --- a/Hermes/Makefile +++ b/Hermes/Makefile @@ -1,3 +1,3 @@ -# Remove idiotic save files +# Remove temporary files clean: - rm -rf *~ + rm -rf *~ *.out *.conf diff --git a/Hermes/TODO b/Hermes/TODO index 8ad5a92..bbc2930 100644 --- a/Hermes/TODO +++ b/Hermes/TODO @@ -1,14 +1,17 @@ FEATURES -1. Create a racket module for commonly used functions -2. Log error messages and channel conservations to proper files on server -4. message parsable? -5. command parsable? -7. maybe fiddle around with irc library -8. separate main running code from definitions -10. authentication for databases -11. user can ask for no of logged in users. Server has to pars -e -12. Hide user's own input in command line -14. bye message prompt for clients -15. Session stickiness for clients +5. parser in the client side should do something similar (/color, /quit) 16. plain tcp -> ssl based +17. fix breaks for improper disconnects from clients +18. Add topics after project completion +** regexes to parse strings for different formats -related to 5 +** align code better for readability + + +GOOD TO HAVE BUT NOT NECESSARY +7. maybe fiddle around with irc library (we leave this for future opl classes) no time got other classes +*14. bye message prompt for clients part of session stickiness +*15. Session stickiness for clients. Log received comms to a local file. +additionally save user details and prompt user to use defaults or create +new ones +10. authentication for databases - to avoid dependencies this is left out +** whispers aren't currently logged - its on purpose diff --git a/Hermes/client.rkt b/Hermes/client.rkt index 3b65cfa..d4ad2a0 100644 --- a/Hermes/client.rkt +++ b/Hermes/client.rkt @@ -1,44 +1,65 @@ #lang racket + +(require "modules/general.rkt") (require math/base) ;; for random number generation ;; TODO clean up string message output and alignment +;; TODO close ports after done +;; i.e. seconds and minutes hours specifically ;; author: Ibrahim Mkusa ;; about: print and read concurrently ;; notes: output may need to be aligned and formatted nicely -(define host "10.0.0.160") ; internal home -(define host2 "67.186.191.81") -(define port-num 4321) +; we will prompt for these in the gui +(define host3 "localhost") +(define port-num 4321) +(define sleep-t 0.1) + +; we won't need this. Just me being overzealous +(define hermes-conf (open-output-file "./hermes_client.conf" #:exists'append)) +(define hermes-conf-s (make-semaphore 1)) + +(define convs-out (open-output-file "./convs_client.out" #:exists 'append)) +(define convs-out-s (make-semaphore 1)) + +(define error-out (open-output-file "./error_client.out" #:exists 'append)) +(define error-out-s (make-semaphore 1)) + ; custodian for client connections (define main-client-cust (make-custodian)) ; make connection to server (define (client port-no) (parameterize ([current-custodian main-client-cust]) ;; connect to server at port 8080 - (define-values (in out) (tcp-connect host2 port-no)) ;; define values - (display in) - (displayln out) + (define-values (in out) (tcp-connect host3 port-no)) ;; define values ;; binds to multiple values akin to unpacking tuples in python + + ; store username to a file for later retrieval along with relevent + ; info used for authentication with server (displayln "What's your name?") (define username (read-line)) + ;send the username to the server (username in out) + (displayln username out) + (flush-output out) + (define a (thread (lambda () - (displayln "Starting receiver thread.") + (displayln-safe "Starting receiver thread." error-out-s error-out) (let loop [] (receive-messages in) - (sleep 1) + (sleep sleep-t) (loop))))) (define t (thread (lambda () - (displayln "Starting sender thread.") + (displayln-safe "Starting sender thread." error-out-s error-out) (let loop [] (send-messages username out) - (sleep 1) + (sleep sleep-t) (loop))))) - (displayln "Now waiting for sender thread.") + (displayln-safe "Now waiting for sender thread." error-out-s error-out) (thread-wait t) ;; returns prompt back to drracket - (displayln "Closing client ports.") + (displayln-safe "Closing client ports." error-out-s error-out) (close-input-port in) (close-output-port out)) (custodian-shutdown-all main-client-cust)) @@ -55,11 +76,14 @@ ":" (number->string (date-second date-today)) " | ")) - ;; intelligent read, quits when user types in "quit" + ;; read, quits when user types in "quit" (define input (read-line)) + ; TODO /quit instead of quit (cond ((string=? input "quit") (displayln (string-append date-print username " signing out. See ya!") out) (flush-output out) + (close-output-port error-out) + (close-output-port convs-out) (exit))) (displayln (string-append date-print username ": " input) out) @@ -68,17 +92,17 @@ ; receives input from server and displays it to stdout (define (receive-messages in) ; retrieve a message from server - (define evt (sync/timeout 60 (read-line-evt in))) + (define evt (sync (read-line-evt in))) (cond [(eof-object? evt) - (displayln "Server connection closed.") + (displayln-safe "Server connection closed." error-out-s error-out) (custodian-shutdown-all main-client-cust) ;(exit) ] [(string? evt) - (displayln evt)] ; could time stamp here or to send message + (displayln-safe evt convs-out-s convs-out)] ; could time stamp here or to send message [else - (displayln (string-append "Nothing received from server for 2 minutes."))])) + (displayln-safe (string-append "Nothing received from server for 2 minutes.") convs-out-s convs-out)])) -(displayln "Starting client.") -(define stop (client 4321)) +(displayln-safe "Starting client." error-out-s error-out) +(define stop-client (client 4321)) diff --git a/Hermes/modules/general.rkt b/Hermes/modules/general.rkt new file mode 100644 index 0000000..b33eb8a --- /dev/null +++ b/Hermes/modules/general.rkt @@ -0,0 +1,24 @@ +#lang racket + +(provide displayln-safe) +;; Several threads may want to print to stdout, so lets make things civil +; constant always available +(define stdout (make-semaphore 1)) + +; prints to stdout with an optional output port +; requires a specified semaphore for the optional output port +(define displayln-safe + (lambda (a-string [a-semaphore stdout] [a-output-port (current-output-port)]) + (cond [(not (and (eq? a-semaphore stdout) (eq? a-output-port (current-output-port)))) + (semaphore-wait a-semaphore) + (semaphore-wait stdout) + (displayln a-string a-output-port) + (flush-output a-output-port) + (displayln a-string) + (semaphore-post stdout) + (semaphore-post a-semaphore)] + [else + (semaphore-wait stdout) + (displayln a-string) + (semaphore-post stdout)]))) + diff --git a/Hermes/server.rkt b/Hermes/server.rkt index 5673eca..df1cf26 100644 --- a/Hermes/server.rkt +++ b/Hermes/server.rkt @@ -1,8 +1,14 @@ #lang racket + +(require "modules/general.rkt") (require math/base) ;; for random number generation -;; globals + (define welcome-message "Welcome to Hermes coms. Type your message below") +(define successful-connection-m "Successfully connected to a client. Sending client a welcome message.") + +(define sleep-t 0.1) + ; track number of connections with closure (define (make-count no-count) (define (increment) @@ -27,8 +33,8 @@ (define (make-connections connections) (define (null-cons?) (null? connections)) - (define (add in out) - (set! connections (append connections (list (list in out)))) + (define (add username in out) + (set! connections (append connections (list (list username in out)))) connections) (define (cons-list) connections) @@ -70,99 +76,161 @@ ; semaphore to control access to c-messages (define messages-s (make-semaphore 1)) ;; control access to messages -;; Several threads may want to print to stdout, so lets make things civil -(define stdout (make-semaphore 1)) - -; Takes a string and a semaphore to print safely to stdout -(define displayln-safe - (lambda (a-string a-semaphore) - (semaphore-wait a-semaphore) - (displayln a-string) - (semaphore-post a-semaphore))) - - +; two files to store error messages, and channel conversations +(define error-out (open-output-file "/home/pcuser/Hermes/Hermes/error_server.txt" #:exists 'append)) +(define convs-out (open-output-file "/home/pcuser/Hermes/Hermes/conversations_server.txt" #:exists 'append)) +(define error-out-s (make-semaphore 1)) +(define convs-out-s (make-semaphore 1)) +; TODO finish logging all error related messages to (define (serve port-no) (define main-cust (make-custodian)) (parameterize ([current-custodian main-cust]) (define listener (tcp-listen port-no 5 #t)) (define (loop) - (accept-and-handle listener) + (receive-clients listener) (loop)) - (displayln "threading the listener") + (displayln-safe "Starting up the listener." error-out-s error-out) (thread loop) + (displayln-safe "Listener successfully started." error-out-s error-out) ;; Create a thread whose job is to simply call broadcast iteratively (thread (lambda () - (displayln-safe "Broadcast thread started!\n" stdout) + (displayln-safe "Broadcast thread started!\n") (let loopb [] - (sleep 0.5) ;; wait 0.5 secs before beginning to broadcast + (sleep sleep-t) ;; wait 0.5 secs before beginning to broadcast (broadcast) (loopb))))) (lambda () - (displayln "\nGoodbye, shutting down all services\n") + (displayln-safe "Goodbye, shutting down all services" error-out-s error-out) + (semaphore-wait error-out-s) + (semaphore-wait convs-out-s) + (close-output-port error-out) + (close-output-port convs-out) + (semaphore-post error-out-s) + (semaphore-post convs-out-s) (custodian-shutdown-all main-cust))) -(define (accept-and-handle listener) +(define (receive-clients listener) (define cust (make-custodian)) (parameterize ([current-custodian cust]) (define-values (in out) (tcp-accept listener)) + + ;TODO retrive user name for client here + ; do some error checking + (define username-evt (sync (read-line-evt in 'linefeed))) + + + ; increment number of connections (semaphore-wait c-count-s) ((c-count 'increment)) (semaphore-post c-count-s) - (displayln-safe (string-append - "Successfully connected to a client. " - "Sending client a welcome message.") - stdout) + (displayln-safe successful-connection-m) (displayln welcome-message out) ;; print to server log and client (define print-no-users (string-append "Number of users in chat: " (number->string ((c-count 'current-count))))) (displayln print-no-users out) - (displayln-safe print-no-users stdout) + (displayln-safe print-no-users convs-out-s convs-out) (flush-output out) (semaphore-wait connections-s) - ((c-connections 'add) in out) + ; TODO add in a username so we have (username input output) + ((c-connections 'add) username-evt in out) (semaphore-post connections-s) ; start a thread to deal with specific client and add descriptor value to the list of threads (define threadcom (thread (lambda () - (handle in out)))) ; comms between server and particular client + (chat_with_client in out)))) ; comms between server and particular client ;; Watcher thread: ;; kills current thread for waiting too long for connection from (thread (lambda () (displayln-safe (string-append "Started a thread to kill hanging " - "connecting threads") stdout) + "connecting threads")) (sleep 1360) (custodian-shutdown-all cust))))) -(define (handle in out) +; whisper selector for the username and message +(define (whisper-info exp) + (cadr exp)) + +(define (whisper-to exp) + (caddr exp)) + +(define (whisper-message exp) + (cadddr exp)) + +(define (chat_with_client in out) ; deals with queueing incoming messages for server to broadcast to all clients (define (something-to-say in) - (define evt-t0 (sync/timeout 60 (read-line-evt in 'linefeed))) + (define evt-t0 (sync (read-line-evt in 'linefeed))) (cond [(eof-object? evt-t0) - ; TODO remove pair of ports associated with client (semaphore-wait connections-s) ((c-connections 'remove-ports) in out) (semaphore-post connections-s) - - (displayln-safe "Connection closed. EOF received" - stdout) + ; TODO some form of identification for this client + (displayln-safe "Connection closed. EOF received" error-out-s error-out) (semaphore-wait c-count-s) ((c-count 'decrement)) (semaphore-post c-count-s) ;(exit) (kill-thread (current-thread))] [(string? evt-t0) - (semaphore-wait messages-s) - ; append the message to list of messages - (display (string-append evt-t0 "\n")) - ((c-messages 'add) evt-t0) - (semaphore-post messages-s)] + ; use regexes to evaluate received input from client + (define whisper (regexp-match #px"(.*)/whisper\\s+(\\w+)\\s+(.*)" evt-t0)) ; is client trying to whisper to someone + (define list-count (regexp-match #px"(.*)/list\\s+count\\s*" evt-t0)) ;; is client asking for number of logged in users + (define list-users (regexp-match #px"(.*)/list\\s+users\\s*" evt-t0)) ;; user names + ; do something whether it was a message, a whisper, request for number of users and so on + (cond [whisper + (semaphore-wait connections-s) + ; get output port for user + (define that-user-ports + (first (filter + (lambda (ports) + (if (string=? (whisper-to whisper) (get-username ports)) + #t + #f)) + ((c-connections 'cons-list))))) + ; try to send that user the whisper + (if (port-closed? (get-output-port that-user-ports)) + (begin + (displayln "User is unavailable" out) + (flush-output out)) + (begin + (displayln (string-append (whisper-info whisper) (whisper-message whisper)) + (get-output-port that-user-ports)) + (flush-output (get-output-port that-user-ports)))) + (semaphore-post connections-s)] + [list-count + ;;should put a semaphore on connections + (semaphore-wait c-count-s) + (semaphore-wait connections-s) + (define no-of-users (string-append "Number of users in chat: " + (number->string ((c-count 'current-count))))) + (displayln no-of-users out) + (flush-output out) + (semaphore-post connections-s) + (semaphore-post c-count-s) + ] + [list-users + (semaphore-wait connections-s) + ; map over connections sending the username to the client + (displayln "Here is a list of users in chat." out) + (map + (lambda (ports) + (displayln (get-username ports) out)) + ((c-connections 'cons-list))) + (flush-output out) + (semaphore-post connections-s)] + [else + ; (displayln-safe evt-t0) debug purposes + (semaphore-wait messages-s) + ; evaluate it . + ((c-messages 'add) evt-t0) + (semaphore-post messages-s)])] [else - (displayln-safe "Timeout waiting. Nothing received from client" stdout)])) + (displayln-safe "Timeout waiting. Nothing received from client")])) ; Executes methods above in another thread (thread (lambda () @@ -173,27 +241,36 @@ ; extracts output port from a list pair of input and output port (define (get-output-port ports) - (cadr ports)) + (caddr ports)) ; extracts input port (define (get-input-port ports) + (cadr ports)) + +; extract username +(define (get-username ports) (car ports)) ; broadcasts received message from clients periodically +; TODO before broadcasting the message make sure the ports is still open +; no EOF if it is remove client from connections (define broadcast (lambda () (semaphore-wait messages-s) (cond [(not (null? ((c-messages 'mes-list)))) (begin (map (lambda (ports) - (displayln (first ((c-messages 'mes-list))) (get-output-port ports)) - (flush-output (get-output-port ports))) + (if (not (port-closed? (get-output-port ports))) + (begin + (displayln (first ((c-messages 'mes-list))) (get-output-port ports)) + (flush-output (get-output-port ports))) + (displayln-safe "Failed to broadcast. Port not open." error-out-s error-out))) ((c-connections 'cons-list))) + (displayln-safe (first ((c-messages 'mes-list))) convs-out-s convs-out) ;; remove top message ((c-messages 'remove-top)) (displayln "Message broadcasted"))]) (semaphore-post messages-s))) -; TODO move to its own file -(define stop (serve 4321)) ;; start server then close with stop -(display "Server process started\n") +(define stop-server (serve 4321)) ;; start server then close with stop +(displayln-safe "Server process started\n" error-out-s error-out)