;;; -*- Mode:LISP; Package:TCP-APPLICATION; Base:10; Readtable:CL -*-

#|

  Copyright LISP Machine, Inc. 1987
   See filename "Copyright.Text" for
  licensing and release information.

|#

(defmacro rwho-vers (packet)
  `(aref ,packet 0))
(defconstant rwho-version-number 1)

(defmacro rwho-type (packet)
  `(aref ,packet 1))
(defconstant rwho-status-type 1)

(defsubst rwho-pad (packet)
  (dpb (aref packet 2) (byte 8 8)
       (aref packet 3)))
(defun set-rwho-pad (packet val)
  (setf (aref packet 2) (ldb (byte 8 8) val))
  (setf (aref packet 3) (ldb (byte 8 0) val))
  val)
(defsetf rwho-pad set-rwho-pad)

(defsubst rwho-send-time (packet)
  (dpb (aref packet 4) (byte 8 24.)
       (dpb (aref packet 5) (byte 8 16.)
	    (dpb (aref packet 6) (byte 8 8)
		 (aref packet 7)))))
(defun set-rwho-send-time (packet val)
  (setf (aref packet 4) (ldb (byte 8 24.) val))
  (setf (aref packet 5) (ldb (byte 8 16.) val))
  (setf (aref packet 6) (ldb (byte 8 8) val))
  (setf (aref packet 7) (ldb (byte 8 0) val))
  val)
(defsetf rwho-send-time set-rwho-send-time)

(defsubst rwho-recv-time (packet)
  (dpb (aref packet 8) (byte 8 24.)
       (dpb (aref packet 9) (byte 8 16.)
	    (dpb (aref packet 10) (byte 8 8)
		 (aref packet 11)))))
(defun set-rwho-recv-time (packet val)
  (setf (aref packet 8) (ldb (byte 8 24.) val))
  (setf (aref packet 9) (ldb (byte 8 16.) val))
  (setf (aref packet 10) (ldb (byte 8 8) val))
  (setf (aref packet 11) (ldb (byte 8 0) val))
  val)
(defsetf rwho-recv-time set-rwho-recv-time)

(defun rwho-hostname (packet)
  (get-asciz-substring packet 12 (+ 12 32) :lower))
(defun set-rwho-hostname (packet val)
  (set-asciz-substring packet 12 (+ 12 32) val :lower))
(defsetf rwho-hostname set-rwho-hostname)

(defun rwho-load-average (packet index)
  (dpb (aref packet (+ 44 (* index 4))) (byte 8 24.)
       (dpb (aref packet (+ 45 (* index 4))) (byte 8 16.)
	    (dpb (aref packet (+ 46 (* index 4))) (byte 8 8)
		 (aref packet (+ 47 (* index 4)))))))
(defun set-rwho-load-average (packet index val)
  (setf (aref packet (+ 44 (* index 4))) (ldb (byte 8 24.) val))
  (setf (aref packet (+ 45 (* index 4))) (ldb (byte 8 16.) val))
  (setf (aref packet (+ 46 (* index 4))) (ldb (byte 8 8) val))
  (setf (aref packet (+ 47 (* index 4))) (ldb (byte 8 0) val))
  val)

(defsubst rwho-boot-time (packet)
  (dpb (aref packet 56) (byte 8 24.)
       (dpb (aref packet 57) (byte 8 16.)
	    (dpb (aref packet 58) (byte 8 8)
		 (aref packet 59)))))
(defun set-rwho-boot-time (packet val)
  (setf (aref packet 56) (ldb (byte 8 24.) val))
  (setf (aref packet 57) (ldb (byte 8 16.) val))
  (setf (aref packet 58) (ldb (byte 8 8) val))
  (setf (aref packet 59) (ldb (byte 8 0) val))
  val)
(defsetf rwho-boot-time set-rwho-boot-time)

(defconstant rwho-utmp-offset 60)
(defconstant rwho-utmp-entry-length 24)

(defun rwho-utmp-ttyname (packet offset)
  (get-asciz-substring packet offset (+ offset 8) :lower))
(defun set-rwho-utmp-ttyname (packet offset val)
  (set-asciz-substring packet offset (+ offset 8) val :lower))

(defun rwho-utmp-userid (packet offset)
  (get-asciz-substring packet (+ offset 8) (+ offset 16) nil))
(defun set-rwho-utmp-userid (packet offset val)
  (set-asciz-substring packet (+ offset 8) (+ offset 16) val nil))

(defun rwho-utmp-time-logged-on (packet offset)
  (dpb (aref packet (+ offset 16)) (byte 8 24.)
       (dpb (aref packet (+ offset 17)) (byte 8 16.)
	    (dpb (aref packet (+ offset 18)) (byte 8 8)
		 (aref packet (+ offset 19))))))
(defun set-rwho-utmp-time-logged-in (packet offset val)
  (setf (aref packet (+ offset 16)) (ldb (byte 8 24.) val))
  (setf (aref packet (+ offset 17)) (ldb (byte 8 16.) val))
  (setf (aref packet (+ offset 18)) (ldb (byte 8 8) val))
  (setf (aref packet (+ offset 19)) (ldb (byte 8 0) val))
  val)

(defun rwho-utmp-idle-time (packet offset)
  (dpb (aref packet (+ offset 20)) (byte 8 24.)
       (dpb (aref packet (+ offset 21)) (byte 8 16.)
	    (dpb (aref packet (+ offset 22)) (byte 8 8)
		 (aref packet (+ offset 23))))))
(defun set-rwho-utmp-idle-time (packet offset val)
  (setf (aref packet (+ offset 20)) (ldb (byte 8 24.) val))
  (setf (aref packet (+ offset 21)) (ldb (byte 8 16.) val))
  (setf (aref packet (+ offset 22)) (ldb (byte 8 8) val))
  (setf (aref packet (+ offset 23)) (ldb (byte 8 0) val))
  val)

(defun get-asciz-substring (buffer start end &optional (case :lower))
  (let ((string nil))
    (do ((i start (1+ i)))
	((or (= i end)
	     (zerop (aref buffer i)))
	 (setq string (make-string (- i start)))))
    (dotimes (i (length string))
      (let ((char (aref buffer (+ start i))))
	(setf (char string i) (case case
				(:lower (char-downcase char))
				(:upper (char-upcase char))
				(t char)))))
    string))

(defun set-asciz-substring (buffer start end string &optional (case :lower))
  (do ((dest start (1+ dest))
       (source 0 (1+ source))
       (length (length string)))
      ((= dest end))
    (setf (aref buffer dest) (if (< source length)
				 (let ((char (char string source)))
				   (case case
				     (:lower (char-downcase char))
				     (:upper (char-upcase char))
				     (t char)))
			       0))))

;;;Unix Universal time is calculated from midnight on Jan 1 1970
;;;Lispm Universal time is calculated from midnight on Jan 1 1900
;;;There are 70 years (including 17 leap years) in this interval

(defconstant *rwho-unix-ut-fudge* (* (+ (* 70 365) 17) 24 3600)
  "Fudge factor to convert between Unix and LISPM Universal Times")

(defmacro fudge-ut-to-unix (time)
  `(- ,time *rwho-unix-ut-fudge*))

(defmacro fudge-ut-from-unix (time)
  `(+ ,time *rwho-unix-ut-fudge*))

(defun rwho-print-interval (interval)
  (let ((seconds (mod interval 60))
	(minutes (mod (truncate interval 60) 60))
	(hours (mod (truncate interval 3600) 24))
	(days (truncate interval (* 24 3600))))
    (format nil
	    "~:[~DD ~;~*~]~:[~2,'0D:~;~*~]~2,'0D:~2,'0D"
	    (zerop days)
	    days
	    (zerop hours)
	    hours
	    minutes
	    seconds)))

(defun describe-rwho (packet &optional (length (length packet)))
  (labels ((print-non-zero-universal-time (time)
	     (if (zerop time)
		 "---"
	       (with-output-to-string (*standard-output*)
		 (time:print-universal-time (fudge-ut-from-unix time))))))
    (format t "~&version: ~20t~s" (rwho-vers packet))
    (format t "~&type: ~20t~s" (rwho-type packet))
    (format t "~&pad: ~20t~s" (rwho-pad packet))
    (format t "~&send time: ~20t~s" (print-non-zero-universal-time (rwho-send-time packet)))
    (format t "~&recv time: ~20t~s" (print-non-zero-universal-time (rwho-recv-time packet)))
    (format t "~&hostname: ~20t~s" (rwho-hostname packet))
    (format t "~&load average 0: ~20t~s" (rwho-load-average packet 0))
    (format t "~&load average 1: ~20t~s" (rwho-load-average packet 1))
    (format t "~&load average 2: ~20t~s" (rwho-load-average packet 2))
    (format t "~&boot time: ~20t~s" (print-non-zero-universal-time (rwho-boot-time packet)))
    (do ((offset rwho-utmp-offset (+ offset rwho-utmp-entry-length)))
	((>= offset length))
      (format t "~&User: tty: ~20t~s" (rwho-utmp-ttyname packet offset))
      (format t "~&      userid: ~20t~s" (rwho-utmp-userid packet offset))
      (format t "~&      logged on: ~20t~s" (print-non-zero-universal-time (rwho-utmp-time-logged-on packet offset)))
      (format t "~&      idle: ~20t~s" (with-output-to-string (*standard-output*)
					 (time:print-interval-or-never (rwho-utmp-idle-time packet offset))))
      )))

(defun build-rwho-packet (packet)
  (setf (rwho-vers packet) RWHO-VERSION-NUMBER)
  (setf (rwho-type packet) RWHO-STATUS-TYPE)
  (setf (rwho-pad packet) 0)
  (setf (rwho-send-time packet) (fudge-ut-to-unix (get-universal-time)))
  (setf (rwho-recv-time packet) 0)
  (setf (rwho-hostname packet) (send si:local-host :name))
  (set-rwho-load-average packet 0 0)
  (set-rwho-load-average packet 1 0)
  (set-rwho-load-average packet 2 0)
  (setf (rwho-boot-time packet) (fudge-ut-to-unix time:*ut-at-boot-time*))
  (cond ((zerop (length si:user-id))		;not logged in
	 (setf (fill-pointer packet) rwho-utmp-offset))
	(t
	 (set-rwho-utmp-ttyname packet rwho-utmp-offset "LISPM")
	 (set-rwho-utmp-userid packet rwho-utmp-offset si:user-id)
	 (let ((login-time (fourth (first si:login-history))))
	   (set-rwho-utmp-time-logged-in packet
					 rwho-utmp-offset
					 (if login-time
					     (fudge-ut-to-unix (time:parse-universal-time login-time))
					   (rwho-send-time packet))))
	 (let ((idle (floor (time-difference (zl:time) tv:kbd-last-activity-time) 60)))
	   (set-rwho-utmp-idle-time packet rwho-utmp-offset idle))
	 (setf (fill-pointer packet) (+ rwho-utmp-offset rwho-utmp-entry-length))))
  packet)

(defvar *udp-rwho-server-broadcast-interval* 3600 "60ths of a second between broadcasts")
(defvar *udp-rwho-server-next-broadcast-time* nil)
(defvar *udp-rwho-server-packets* nil "Saved packets received by RWHO Server")
(defvar *udp-rwho-server-enabled* t "Determines whether this machine broadcasts RWHO packets")

(defvar *udp-rwho-server-process* (make-process "UDP RWHO Server"
						:warm-boot-action 'ignore
						:arrest-reasons '(:not-running)))

(defun initialize-udp-rwho-server-process ()
  (setq *udp-rwho-server-packets* nil)
  (send *udp-rwho-server-process* :preset 'udp-rwho-server-process)
  (send *udp-rwho-server-process* :reset)
  (send *udp-rwho-server-process* :run-reason :enable)
  (send *udp-rwho-server-process* :revoke-arrest-reason :not-running)
  t)

(defun broadcast-rwho-packet (stream)
  (unless time:*ut-at-boot-time*
    (process-wait "Universal Time" #'(lambda () time:*ut-at-boot-time*)))
  (when *udp-rwho-server-enabled*
    (send stream :broadcast-packet
	  (build-rwho-packet (udp:get-udp-buffer))
	  (send stream :local-port)))
  (setq *udp-rwho-server-next-broadcast-time*
	(time-increment (zl:time) *udp-rwho-server-broadcast-interval*))
  )

(defun insert-rwho-packet (remote-address buffer)
  (let ((elt (assoc remote-address *udp-rwho-server-packets*)))
    (cond (elt
	   (udp:free-udp-buffer (cdr elt))
	   (setf (cdr elt) buffer))
	  (t
	   (push (cons remote-address buffer) *udp-rwho-server-packets*))))
  (sort *udp-rwho-server-packets* #'string-lessp :key #'(lambda (elt)
							  (rwho-hostname (cdr elt)))))

(defun udp-rwho-server-process ()
  (loop
    (process-wait "UDP Enable" #'(lambda ()
				   (and udp:*udp-stream* (udp:udp-enabled udp:*udp-stream*))))
    (with-open-stream (stream (send (make-instance 'udp:udp-stream)
				    :open "RWHO Server" :local-port (sym ipport-whoserver)))
      (broadcast-rwho-packet stream)
      (loop
	(unless (time-lessp (zl:time) *udp-rwho-server-next-broadcast-time*)
	  (broadcast-rwho-packet stream))
	(when (send stream :listen)
	  (multiple-value-bind (packet length remote-port remote-address)
	      (send stream :read-packet)
	    (declare (ignore length))
	    (declare (ignore remote-port))
	    (unless packet
	      ;;socket closed, UDP shut down, whatever...
	      (return nil))
	    (setf (rwho-recv-time packet) (fudge-ut-to-unix (get-universal-time)))
	    (insert-rwho-packet remote-address packet)))
	(process-wait "RWHO Packet Send/Recv"
		      #'(lambda (s)
			  (or (send s :listen)
			      (not (time-lessp (zl:time) *udp-rwho-server-next-broadcast-time*))))
		      stream)))))

(defun rwho ()
  (when *udp-rwho-server-packets*
    (let ((fudged-time (fudge-ut-to-unix (get-universal-time)))
	  (format-string "~&~A~20T~@9A~30T~@15A~46T~@13A~60T~A"))
      (format t
	      format-string
	      "Host Name"
	      "Terminal"
	      "On Since"
	      "Idle"
	      "User")
      (dolist (x *udp-rwho-server-packets*)
	(let* ((packet (cdr x))
	       (hostname (rwho-hostname packet))
	       (length (length packet)))
	  (cond ((>= (- fudged-time (rwho-recv-time packet)) 180)
		 ;;Packet came 3 minutes ago -- assume host died
		 (format t "~&~16a no response for ~A"
			 hostname
			 (with-output-to-string (*standard-output*)
			   (time:print-interval-or-never (- fudged-time (rwho-recv-time packet))))))
		(t
		 (do ((offset rwho-utmp-offset (+ offset rwho-utmp-entry-length)))
		     ((>= offset length))
		   (format t
			   format-string
			   hostname
			   (rwho-utmp-ttyname packet offset)
			   (with-output-to-string (*standard-output*)
			     (time:print-brief-universal-time
			       (fudge-ut-from-unix (rwho-utmp-time-logged-on packet offset))))
			   (rwho-print-interval (rwho-utmp-idle-time packet offset))
			   (rwho-utmp-userid packet offset))))))))))

(defun ruptime ()
  (when *udp-rwho-server-packets*
    (let ((fudged-time (fudge-ut-to-unix (get-universal-time))))
      (dolist (x *udp-rwho-server-packets*)
	(let* ((packet (cdr x))
	       (hostname (rwho-hostname packet)))
	  (cond ((>= (- fudged-time (rwho-recv-time packet)) 180)
		 ;;Packet came 3 minutes ago -- assume host died
		 (format t "~&~20a no response for ~A"
			 hostname
			 (with-output-to-string (*standard-output*)
			   (time:print-interval-or-never (- fudged-time (rwho-recv-time packet))))))
		(t
		 (let ((lav0 (rwho-load-average packet 0))
		       (lav1 (rwho-load-average packet 1))
		       (lav2 (rwho-load-average packet 2))
		       (users (truncate (- (length packet) rwho-utmp-offset) rwho-utmp-entry-length)))
		   (format t
			   "~&~20a Up ~12a ~2d Users, Load = ~d.~2,'0d ~d.~2,'0d ~d.~2,'0d"
			   hostname
			   (rwho-print-interval (- fudged-time (rwho-boot-time packet)))
			   users
			   (truncate lav0 100)
			   (mod lav0 100)
			   (truncate lav1 100)
			   (mod lav1 100)
			   (truncate lav2 100)
			   (mod lav2 100))))))))))
