r8655: add timeouts for functions that did not return as well as a handler-case for...
[irc-logger.git] / logger.lisp
1 ;;;  -*- Mode: Lisp -*-
2 ;;;; $Id: logger.lisp,v 1.11 2003/12/16 21:19:56 krosenberg Exp $
3 ;;;;
4 ;;;; Purpose: A IRC logging bot 
5 ;;;; Author:  Kevin Rosenberg
6
7 (in-package irc-logger)
8
9 (defvar *daemon-monitor-process* nil "Process of background monitor.")
10
11 (defclass channel ()
12   ((name :initarg :name :reader name
13          :documentation "Name of channel.")
14    (streams :initarg :streams :reader streams
15             :documentation "List of output streams.")
16    (output-root :initarg :output-root :reader output-root)
17    (current-output-names :initarg :current-output-names :accessor current-output-names)))
18
19    
20 (defclass logger ()
21   ((connection :initarg :connection :accessor connection
22                :documentation "IRC connection object.")
23    (handler :initform nil :accessor handler
24             :documentation "Background handler process.")
25    (nick :initarg :nick :reader nickname
26          :documentation "Nickname of the bot.")
27    (password :initarg :password :reader password
28              :documentation "Nickname's nickserver password.")
29    (server :initarg :server :reader server
30            :documentation "Connected IRC server.")
31    (channel-names :initarg :channel-names :accessor channel-names
32                   :documentation "List of channel names.")
33    (realname :initarg :realname :reader realname
34            :documentation "Realname for cl-irc")
35    (username :initarg :username :reader username
36            :documentation "Username for cl-irc")
37    (logging-stream :initarg :logging-stream :reader logging-stream 
38                    :documentation "logging-stream for cl-irc.")
39    (channels :initarg :channels :accessor channels
40              :documentation "List of channels.")
41    (user-output :initarg :user-output :reader user-output
42                 :documentation
43                 "Output parameter from user, maybe stream or pathname.")
44    (unichannel :initarg :unichannel :reader unichannel :type boolean
45                :documentation "T if user-output is directory for individual channel output.")
46    (formats :initarg :formats :reader formats
47                   :documentation
48                   "A list of output formats.")
49    (async :initarg :async :reader async
50                   :documentation
51                   "Whether to use async")
52    (last-pong :initform nil :accessor last-pong
53                   :documentation
54                   "utime of last pong message")
55    (private-log :initarg :private-log :reader private-log
56                 :documentation "Pathname of the private log file for the daemon.")
57    (unknown-log :initarg :unknown-log :reader unknown-log
58                 :documentation "Pathname of the log file for unknown messages.")
59    (private-log-stream :initarg :private-log-stream :reader private-log-stream
60                        :documentation "Stream of the private log file for the daemon.")
61    (unknown-log-stream :initarg :unknown-log-stream :reader unknown-log-stream
62                 :documentation "Stream of the log file for unknown messages.")
63    (monitor-events :initform nil :accessor monitor-events
64                    :documentation "List of events for the monitor to process.")
65    (warning-message-utime :initform nil :accessor warning-message-utime
66                   :documentation
67                   "Time of last, potentially active, warning message.")))
68
69 (defvar *loggers* nil "List of active loggers.")
70
71 (defparameter *user-address-scanner*
72   (create-scanner
73    '(:sequence #\!
74      (:register
75       (:greedy-repetition 1 nil :non-whitespace-char-class)))
76    :case-insensitive-mode t))
77
78 (defun find-logger-with-nick (nick)
79   (find nick (the list *loggers*) :test #'string-equal :key #'nickname))
80
81 (defun find-logger-with-connection (conn)
82   (find conn (the list *loggers*) :test #'eq :key #'connection))
83
84 (defun canonicalize-channel-name (name)
85   (string-left-trim '(#\#) name))
86
87 (defun find-channel-with-name (logger name)
88   (find name (the list (channels logger)) :test #'string-equal :key #'name))
89   
90 (defun make-output-name (name year month day)
91     (format nil "~A-~4,'0D.~2,'0D.~2,'0D" (canonicalize-channel-name name)
92             year month day))
93
94 (defmacro with-decoding ((utime &optional zone) &body body)
95   `(multiple-value-bind
96     (second minute hour day-of-month month year day-of-week daylight-p zone)
97     (decode-universal-time ,utime ,@(if zone (list zone)))
98     (declare (ignorable second minute hour day-of-month month year day-of-week daylight-p zone))
99     ,@body))
100
101 (defun format-utime (utime &optional zone)
102   (with-decoding (utime zone)
103     (format nil "~2,'0D:~2,'0D:~2,'0D" hour minute second)))
104
105 (defun format-date-time (utime &key stream)
106   (with-decoding (utime)
107     (format stream "~4,'0D/~2,'0D/~2,'0D ~2,'0D:~2,'0D:~2,'0D" year month day-of-month hour minute second)))
108
109 (defun make-output-name-utime (name utime)
110   (with-decoding (utime 0)
111     (make-output-name name year month day-of-month)))
112
113 (defgeneric write-file-header (format channel-name stream))
114
115 (defmethod write-file-header ((format t) channel-name stream)
116   (declare (ignore channel-name stream))
117   )
118
119 (defgeneric write-file-footer (format channel-name stream))
120
121 (defmethod write-file-footer ((format t) channel-name stream)
122   (declare (ignore channel-name stream))
123   )
124                                
125 (defun %log-file-path (output-root channel-name year month day type)
126   (make-pathname
127    :defaults output-root
128    :directory (append (pathname-directory output-root)
129                       (list
130                        (string-left-trim '(#\#) channel-name)
131                        (format nil "~4,'0D-~2,'0D" year month)))
132    :name (make-output-name channel-name year month day)
133    :type type))
134
135 (defgeneric log-file-path (output-root channel-name year month day format))
136
137 (defmethod log-file-path (output-root channel-name year month day
138                           (format (eql :raw)))
139   (%log-file-path output-root channel-name year month day "raw"))
140
141 (defmethod log-file-path (output-root channel-name year month day (format (eql :sexp)))
142   (%log-file-path output-root channel-name year month day "sexp"))
143
144 (defmethod log-file-path (output-root channel-name year month day (format (eql :text)))
145   (%log-file-path output-root channel-name year month day "txt"))
146
147 (defmethod log-file-path (output-root channel-name year month day (format string))
148   (%log-file-path output-root channel-name year month day format))
149
150
151 (defun log-file-path-utime (output-root channel-name format utime)
152   (with-decoding (utime 0)
153     (log-file-path output-root channel-name year month day-of-month format)))
154
155 (defun get-stream (channel istream)
156   (elt (streams channel) istream))
157
158 (defun (setf get-stream) (value channel istream)
159   (setf (elt (streams channel) istream) value))
160
161 (defun get-format (logger istream)
162   (elt (formats logger) istream))
163
164 (defun get-output-name (channel istream)
165   (elt (current-output-names channel) istream))
166
167 (defun (setf get-output-name) (value channel istream)
168   (setf (elt (current-output-names channel) istream) value))
169
170 (defun ensure-output-stream-for-unichannel (utime logger channel istream)
171   (let ((name (make-output-name-utime (name channel) utime)))
172     (unless (string= name (get-output-name channel istream))
173       (when (get-stream channel istream)
174         (write-file-footer (get-format logger istream)
175                            (name channel)
176                            (get-stream channel istream))
177         (close (get-stream channel istream)))
178       (setf (get-output-name channel istream) name)
179       (let ((path (log-file-path-utime (output-root channel) (name channel)
180                                        (get-format logger istream) utime)))
181         (unless (probe-file path)
182           (ensure-directories-exist path)
183           (setf (get-stream channel istream)
184                 (open path :direction :output :if-exists :error
185                       :if-does-not-exist :create))
186           (write-file-header (get-format logger istream)
187                              (name channel)
188                               (get-stream channel istream))
189           (close (get-stream channel istream)))
190         (setf (get-stream channel istream)
191               (open path :direction :output :if-exists :append
192                     :if-does-not-exist :create))))))
193
194 (defun ensure-output-stream (utime logger channel istream)
195   "Ensures that *output-stream* is correct."
196   (cond
197    ((streamp (user-output logger))
198     (unless (get-stream channel istream)
199       (setf (get-stream channel istream) (user-output logger))))
200    ((pathnamep (user-output logger))
201     (cond
202      ((unichannel logger)
203       (ensure-output-stream-for-unichannel utime logger channel istream))
204      (t
205       (setf (get-stream channel istream)
206         (open (user-output logger) :direction :output :if-exists :append
207               :if-does-not-exist :create)))))))
208
209 (defun user-address (msg)
210   (let ((split (split *user-address-scanner* (raw-message-string msg)
211                       :with-registers-p t)))
212     (if (second split)
213         (second split)
214         "")))
215
216 (defun need-user-address? (type)
217   (case type
218     ((:action :privmsg :names :rpl_topic)
219      nil)
220     (t
221      t)))
222
223 (defgeneric %output-event (format stream utime type channel source text msg 
224                            unichannel))
225
226 (defmethod %output-event ((format t) stream utime type channel source text
227                           msg unichannel)
228   (%output-event :raw stream utime type channel source text msg unichannel))
229
230 (defmethod %output-event ((format (eql :raw)) stream utime type channel source
231                           text msg unichannel)
232   (declare (ignore utime type channel source text text unichannel))
233   (when msg
234     (format stream "~S~%" 
235             (string-right-trim '(#\return) (raw-message-string msg)))))
236
237 (defconstant +posix-epoch+
238   (encode-universal-time 0 0 0 1 1 1970 0))
239
240 (defun posix-time-to-utime (time)
241   (+ time +posix-epoch+))
242
243 (defun last-sexp-field (type msg)
244   (cond
245    ((null msg)
246     nil)
247    ((eq type :kick)
248     (trailing-argument msg))
249    ((eq type :rpl_topicwhotime)
250     (when (stringp (car (last (arguments msg))))
251       (let ((secs (parse-integer (car (last (arguments msg))) :junk-allowed t)))
252         (when secs
253           (posix-time-to-utime secs)))))
254    ((need-user-address? type)
255     (user-address msg))))
256
257 (defmethod %output-event ((format (eql :sexp)) stream utime type channel source text
258                           msg unichannel)
259   (with-standard-io-syntax
260     (let ((cl:*print-case* :downcase))
261       (if unichannel
262           (format stream "(~S ~S ~S ~S ~S)~%" utime type source text (last-sexp-field type msg))
263         (format stream "(~S ~S ~S ~S ~S ~S)~%" utime type source channel text
264                 (last-sexp-field type msg))))))
265
266 (defmethod %output-event ((format (eql :text)) stream utime type channel
267                           source text msg unichannel)
268   (format stream "~A " (format-utime utime 0))
269   (when (and (null unichannel) channel)
270     (format stream "[~A] " channel))
271   
272   (let ((user-address (when (and msg (need-user-address? type)) (user-address msg))))
273     (case type
274       (:privmsg
275        (format stream "<~A> ~A" source text))
276       (:action
277        (format stream "*~A* ~A" source text))
278       (:join
279        (format stream "~A [~A] has joined ~A" source user-address channel))
280       (:part
281        (format stream "-!- ~A [~A] has left ~A" source user-address channel))
282       (:nick
283        (format stream "-!- ~A is now known as ~A" source text))
284       (:kick
285        (format stream "-!- ~A [~A] has been kicked from ~A" source user-address channel))
286       (:quit
287        (format stream "-!- ~A [~A] has quit [~A]" source user-address (if text text "")))
288       (:mode
289        (format stream "-!- ~A has set mode ~A"  source text))
290       (:topic
291        (format stream "-!- ~A changed the topic of ~A to: ~A" source channel text))
292       (:notice
293        (format stream "-~A:~A- ~A" source channel text))
294       (:daemon
295        (format stream "-!!!- ~A" text))
296       (:names
297        (format stream "-!- names: ~A" text))
298       (:rpl_topic
299        (format stream "-!- topic: ~A" text))
300       (t
301        (warn "Unhandled msg type ~A." type))))
302   (write-char #\Newline stream))
303
304 (defun output-event-for-a-stream (msg type channel text logger istream)
305   (ensure-output-stream (received-time msg) logger channel istream)
306   (%output-event  (get-format logger istream) (get-stream channel istream)
307                   (received-time msg) type (name channel) (source msg) text msg
308                   (unichannel logger))
309   (force-output (get-stream channel istream)))
310
311 (defun log-daemon-message (logger fmt &rest args)
312   (let ((text (apply #'format nil fmt args)))
313     (add-private-log-entry logger "~A" text)
314     ;;don't daemon messages to the logs
315     #+ignore
316     (dolist (channel (channels logger))
317       (dotimes (istream (length (formats logger)))
318         (ensure-output-stream time logger channel istream)
319         (%output-event  (get-format logger istream)
320                         (get-stream channel istream)
321                         time :daemon nil nil text nil
322                         (unichannel logger))
323         (force-output (get-stream channel istream))))))
324
325 (defvar *msg*)
326 (defun output-event (msg type channel-name &optional text)
327   (setq *msg* msg)
328   (dolist (logger *loggers*)
329     (case type
330       ((:error :server :kill)
331        (add-private-log-entry logger "~A" (raw-message-string msg)))
332       ((:quit :nick)
333        ;; send to all channels that a nickname is joined
334        (let* ((user (find-user (connection logger)
335                                (case type
336                                  (:nick (source msg))
337                                  (:quit (source msg)))))
338               (channels (when user (cl-irc::channels user))))
339          (dolist (channel (mapcar
340                            #'(lambda (name) (find-channel-with-name logger name)) 
341                            (mapcar #'cl-irc::name channels)))
342            (when channel
343              (dotimes (i (length (formats logger)))
344                (output-event-for-a-stream msg type channel text logger i))))))
345       (t
346        ;; msg contains channel name
347        (let* ((channel (find-channel-with-name logger channel-name)))
348          (when channel
349            (dotimes (i (length (formats logger)))
350              (output-event-for-a-stream msg type channel text logger i))))))))
351
352 (defun get-private-log-stream (logger)
353   (if (and logger (private-log-stream logger))
354       (private-log-stream logger)
355     *standard-output*))
356
357 (defun get-unknown-log-stream (logger)
358   (if (and logger (unknown-log-stream logger))
359       (unknown-log-stream logger)
360     *standard-output*))
361
362 (defun add-log-entry (stream fmt &rest args)
363   (handler-case 
364       (progn
365         (format-date-time (get-universal-time) :stream stream)
366         (write-char #\space stream)
367         (apply #'format stream fmt args)
368         (write-char #\newline stream)
369         (force-output stream))
370     (error (e)
371      (warn "Error ~A when trying to add-log-entry '~A'." e
372            (apply #'format nil fmt args)))))
373
374 (defun add-private-log-entry (logger fmt &rest args)
375   (apply #'add-log-entry (get-private-log-stream logger) fmt args))
376
377 (defun privmsg-hook (msg)
378   (let ((logger (find-logger-with-connection (connection msg)))
379         (channel (first (arguments msg))))
380     (cond
381      ((equal channel (nickname logger))
382       (add-private-log-entry logger "~A" (raw-message-string msg)))
383      (t
384       (output-event msg :privmsg channel (trailing-argument msg))))))
385
386 (defun action-hook (msg)
387   (output-event msg :action (first (arguments msg))
388                 (subseq (trailing-argument msg) 8 
389                         (- (length (trailing-argument msg)) 1))))
390
391 (defun nick-hook (msg)
392   (output-event msg :nick nil (trailing-argument msg)))
393
394 (defun part-hook (msg)
395   (output-event msg :part (first (arguments msg))))
396
397 (defun quit-hook (msg)
398   (output-event msg :quit nil (trailing-argument msg)))
399
400 (defun join-hook (msg)
401   (output-event msg :join (trailing-argument msg)))
402
403 (defun kick-hook (msg)
404   (let ((logger (find-logger-with-connection (connection msg)))
405         (channel (first (arguments msg)))
406         (who-kicked (second (arguments msg))))
407     (output-event msg :kick channel who-kicked)
408     (when (string-equal (nickname logger) who-kicked)
409       (add-private-log-entry
410        "Logging daemon ~A has been kicked from ~A (~A)"
411        (nickname logger) channel (trailing-argument msg))
412       (daemon-sleep 1)
413       (remove-channel-logger logger channel)
414       (daemon-sleep 1)
415       (add-channel-logger logger channel)
416       (add-private-log-entry logger "Rejoined ~A" channel))))
417
418 (defun notice-hook (msg)
419   (let ((logger (find-logger-with-connection (connection msg)))
420         (channel (first (arguments msg))))
421     (cond
422       ((and (string-equal (source msg) "NickServ")
423             (string-equal channel (nickname logger))
424             (string-equal "owned by someone else" (trailing-argument msg)))
425        (if logger
426            (privmsg (connection msg) (source msg) (format nil "IDENTIFY ~A" (password logger)))
427          (add-private-log-entry logger "NickServ asks for identity with connection not found.")))
428       ((equal channel (nickname logger))
429        (add-private-log-entry logger "~A" (raw-message-string msg)))
430       (t
431        (output-event msg :notice channel (trailing-argument msg))))))
432
433 (defun ping-hook (msg)
434   (let ((logger (find-logger-with-connection (connection msg))))
435     (pong (connection msg) (server logger))
436     #+debug (format *standard-output* "Sending pong to ~A~%" (server logger))))
437
438 (defun pong-hook (msg)
439   (let ((logger (find-logger-with-connection (connection msg))))
440     (setf (last-pong logger) (received-time msg))))
441         
442 (defun topic-hook (msg)
443   (output-event msg :topic (first (arguments msg)) (trailing-argument msg)))
444
445 (defun mode-hook (msg)
446   (output-event msg :mode (first (arguments msg)) 
447                 (format nil "~{~A~^ ~}" (cdr (arguments msg)))))
448
449 (defun rpl_namreply-hook (msg)
450   (output-event msg :names (third (arguments msg))
451                 (trailing-argument msg)))
452
453 (defun rpl_endofnames-hook (msg)
454   (declare (ignore msg))
455   ;; nothing to do for this message
456   )
457
458 (defun rpl_topic-hook (msg)
459   (output-event msg :rpl_topic (format nil "~{~A~^ ~}" (arguments msg))
460                 (trailing-argument msg)))
461
462 (defun rpl_topicwhotime-hook (msg)
463   (output-event msg :rpl_topicwhotime
464                 (second (arguments msg))
465                 (third (arguments msg))))
466
467
468 (defun invite-hook (msg)
469   (let ((logger (find-logger-with-connection (connection msg))))
470     (add-private-log-entry logger "~A" (raw-message-string msg))))
471
472
473 (defun make-a-channel (name formats output)
474   (make-instance 'channel
475                  :name name
476                  :streams (make-array (length formats) :initial-element nil)
477                  :output-root (when (and (pathnamep output)
478                                          (null (pathname-name output)))
479                                 output)
480                  :current-output-names (make-array (length formats)
481                                                    :initial-element nil)))
482   
483 (defun make-channels (names formats output)
484   (loop for i from 0 to (1- (length names))
485         collect (make-a-channel (elt names i) formats output)))
486
487 (defun is-unichannel-output (user-output)
488   "Returns T if output is setup for a single channel directory structure."
489   (and (pathnamep user-output) (null (pathname-name user-output))))
490
491 (defun do-connect-and-join (nick server username realname logging-stream channels)
492   (let ((conn (connect :nickname nick :server server
493                        :username username :realname realname
494                        :logging-stream logging-stream)))
495     (mapc #'(lambda (channel) (join conn channel)) channels)
496     (add-hook conn 'irc::irc-privmsg-message 'privmsg-hook)
497     (add-hook conn 'irc::ctcp-action-message 'action-hook)
498     (add-hook conn 'irc::irc-nick-message 'nick-hook)
499     (add-hook conn 'irc::irc-part-message 'part-hook)
500     (add-hook conn 'irc::irc-quit-message 'quit-hook)
501     (add-hook conn 'irc::irc-join-message 'join-hook)
502     (add-hook conn 'irc::irc-kick-message 'kick-hook)
503     (add-hook conn 'irc::irc-mode-message 'mode-hook)
504     (add-hook conn 'irc::irc-topic-message 'topic-hook)
505     (add-hook conn 'irc::irc-notice-message 'notice-hook)
506     (add-hook conn 'irc::irc-error-message 'error-hook)
507     (add-hook conn 'irc::irc-ping-message 'ping-hook)
508     (add-hook conn 'irc::irc-pong-message 'pong-hook)
509     (add-hook conn 'irc::irc-kill-message 'kill-hook)
510     (add-hook conn 'irc::irc-invite-message 'invite-hook)
511     (add-hook conn 'irc::irc-rpl_killdone-message 'warning-hook)
512     (add-hook conn 'irc::irc-rpl_closing-message 'warning-hook)
513     (add-hook conn 'irc::irc-rpl_topic-message 'rpl_topic-hook)
514     (add-hook conn 'irc::irc-rpl_namreply-message 'rpl_namreply-hook)
515     (add-hook conn 'irc::irc-rpl_endofnames-message 'rpl_endofnames-hook)
516     (add-hook conn 'irc::irc-rpl_topicwhotime-message 'rpl_topicwhotime-hook)
517     conn))
518
519 (defmethod cl-irc::irc-message-event :around ((msg cl-irc::irc-message))
520   (let ((result (call-next-method msg)))
521     (typecase msg
522       ((or irc::irc-privmsg-message irc::ctcp-action-message irc::irc-nick-message
523         irc::irc-part-message irc::irc-quit-message irc::irc-join-message
524         irc::irc-kick-message irc::irc-mode-message irc::irc-topic-message
525         irc::irc-notice-message irc::irc-error-message irc::irc-ping-message
526         irc::irc-pong-message irc::irc-kill-message irc::irc-invite-message
527         irc::irc-rpl_killdone-message irc::irc-rpl_closing-message
528         irc::irc-rpl_topic-message irc::irc-rpl_namreply-message
529         irc::irc-rpl_endofnames-message irc::irc-rpl_topicwhotime-message
530         irc::irc-rpl_motd-message irc::irc-rpl_motdstart-message
531         irc::irc-rpl_endofmotd-message)
532        ;; nothing to do
533        )
534       (t
535        (add-log-entry
536         (get-unknown-log-stream (find-logger-with-connection (connection msg)))
537         (raw-message-string msg))))
538     result))
539
540 (defun create-logger (nick server &key channels output password
541                       realname username async
542                       private-log unknown-log
543                       (logging-stream t) (formats '(:text)))
544   "OUTPUT may be a pathname or a stream"
545   ;; check arguments
546   (assert formats)
547   (if (and channels (atom channels))
548       (setq channels (list channels)))
549   (if (atom formats)
550       (setq formats (list formats)))
551   (if (stringp output)
552       (setq output (parse-namestring output)))
553   (let* ((conn (do-connect-and-join nick server username realname logging-stream channels))
554          (logger (make-instance
555                   'logger
556                   :connection conn
557                   :nick nick
558                   :password password
559                   :server server
560                   :channels (make-channels channels formats output)
561                   :channel-names channels
562                   :username username
563                   :realname realname
564                   :async async
565                   :logging-stream logging-stream
566                   :user-output output
567                   :formats formats
568                   :private-log private-log
569                   :unknown-log unknown-log
570                   :private-log-stream (when private-log
571                                         (open private-log :direction :output
572                                               :if-exists :append
573                                               :if-does-not-exist :create))
574                   :unknown-log-stream (when unknown-log
575                                         (open unknown-log :direction :output
576                                               :if-exists :append
577                                               :if-does-not-exist :create))
578                   :unichannel (is-unichannel-output output))))
579     (unless *daemon-monitor-process*
580       (setq *daemon-monitor-process* (cl-irc::start-process 'daemon-monitor "logger-monitor")))
581     logger))
582
583 (defun start-logger (logger async)
584   (if async
585       (setf (handler logger)
586         (start-background-message-handler (connection logger)))
587       (read-message-loop (connection logger))))
588
589 (defun remove-logger (nick)
590   "Quit the active connection with nick and remove from active list."
591   (let ((logger (find-logger-with-nick nick)))
592     (cond
593       ((null logger)
594        (warn
595         "~A No active connection found with nick ~A [remove-logger].~%"
596         (format-date-time (get-universal-time))
597         nick)
598        nil)
599       (t
600        (ignore-errors (irc:quit (connection logger) ""))
601        (stop-background-message-handler (handler logger))
602        (sleep 1)
603        (dolist (channel (channels logger))
604          (let ((c (connection logger)))
605            (when c
606              (ignore-errors (remove-channel c (find-channel c (name channel)))))))
607        (when (private-log-stream logger)
608          (close (private-log-stream logger)))
609        (when (unknown-log-stream logger)
610          (close (unknown-log-stream logger)))
611
612        (setq *loggers*
613              (delete nick *loggers*  :test #'string-equal :key #'nickname))
614        t))))
615
616 (defun add-logger (nick server &key channels output (password "")
617                                     realname username private-log unknown-log
618                                     (logging-stream t) (async t)
619                                     (formats '(:sexp)))
620   (when (find-logger-with-nick nick)
621     (add-private-log-entry (find-logger-with-nick nick)
622                            "Closing previously active connection [add-logger].")
623     (ignore-errors (remove-logger nick)))
624   (let ((logger (create-logger nick server :channels channels :output output
625                                :logging-stream logging-stream :password password
626                                :realname realname :username username
627                                :private-log private-log 
628                                :unknown-log unknown-log 
629                                :formats formats
630                                :async async)))
631     (push logger *loggers*)
632     (start-logger logger async)
633     logger))
634   
635 (defun add-channel-logger (logger channel-name)
636   (cond
637     ((find-channel-with-name logger channel-name)
638      (add-private-log-entry logger "Channel ~A already in logger ~A." channel-name logger)
639      nil)
640     (t
641      (let ((channel (make-a-channel channel-name (formats logger) (user-output logger))))
642        (join (connection logger) channel-name)
643        (push channel (channels logger))
644        (push channel-name (channel-names logger))))))
645
646 (defun remove-channel-logger (logger channel-name)
647   (let ((channel (find-channel-with-name logger channel-name)))
648     (cond
649       (channel
650        (part (connection logger) channel-name)
651        (dotimes (i (length (streams channel)))
652          (when (streamp (get-stream channel i))
653            (close (get-stream channel i))
654            (setf (get-stream channel i) nil)))
655        (setf (channels logger) (delete channel-name (channels logger)
656                                        :test #'string-equal
657                                        :key #'name))
658        (setf (channel-names logger) (delete channel-name (channel-names logger)
659                                             :test #'string-equal))
660        t)
661       (t
662        (add-private-log-entry
663         logger "Channel name ~A not found in logger ~A." channel-name logger)
664        nil))))
665
666 (defun add-hook-logger (logger class hook)
667   (add-hook (connection logger) class hook))
668
669 (defun remove-hook-logger (logger class hook)
670   (remove-hook (connection logger) class hook))
671
672 (defvar *warning-message-utime* nil)
673
674 (defun kill-hook (msg)
675   (let ((target (second (arguments msg)))
676         (logger (find-logger-with-connection (connection msg))))
677     (when (and (stringp target)
678                (string-equal target (nickname logger)))
679       (setf (warning-message-utime logger) (received-time msg)))
680     (add-private-log-entry logger "Killed by ~A" (source msg))))
681
682 (defun error-hook (msg)
683   (let ((text (trailing-argument msg))
684         (logger (find-logger-with-connection (connection msg))))
685     (when (and (stringp text) 
686                (zerop (search (format nil "Closing Link: ~A" (nickname logger)) text)))
687       (setf (warning-message-utime logger) (received-time msg)))
688     (output-event msg :error nil (trailing-argument msg))))
689
690 (defun warning-hook (msg)
691   (let ((logger (find-logger-with-connection (connection msg))))
692     (output-event msg :server 
693                   (format nil "~{~A~^ ~} ~A)"
694                           (arguments msg)
695                           (trailing-argument msg)))
696     (when logger
697       (setf (warning-message-utime logger) (get-universal-time)))))
698   
699 (defun daemon-sleep (seconds)
700   #-allegro (sleep seconds)
701   #+allegro (mp:process-sleep seconds))
702
703 (defun log-disconnection (logger)
704   ;; avoid generating too many reconnect messages in the logs
705   (when (or (null (warning-message-utime logger))
706             (< (- (get-universal-time) (warning-message-utime logger)) 300))
707     (log-daemon-message logger "Disconnected. Attempting reconnection.")))
708
709 (defun log-reconnection (logger)
710   (log-daemon-message logger "Connection restablished."))
711
712 #+ignore
713 (defun is-connected (logger)
714   (%is-connected logger))
715
716 (defparameter *timeout* 60)
717
718 (defun is-connected (logger)
719   #-allegro (%is-connected logger)
720   #+allegro (mp:with-timeout (*timeout* nil)
721               (%is-connected logger)))
722
723 (defun quit-maybe-timeout (connection msg)
724   #-allegro (quit connection msg)
725   #+allegro (mp:with-timeout (*timeout* nil)
726               (quit connection msg)))
727
728 (defun %is-connected (logger)
729   (when (ignore-errors (ping (connection logger) (server logger)))
730     (dotimes (i 20)
731       (when (and (last-pong logger)
732                  (< (- (get-universal-time) (last-pong logger)) 21))
733         (return-from %is-connected t))
734       (daemon-sleep 1))))
735
736
737 (let (*recon-nick* *recon-server* *recon-username* *recon-realname*
738       *recon-user-output* *recon-private-log* *recon-unknown-log*
739       *recon-formats* *recon-async* *recon-logging-stream* *recon-channel-names*)
740   (declare (special *recon-nick* *recon-server* *recon-username* *recon-realname*
741                     *recon-formats* *recon-password* *recon-async* 
742                     *recon-user-output* *recon-private-log* *recon-unknown-log*
743                     *recon-logging-stream* *recon-channel-names*))
744   
745   (defun attempt-reconnection (logger)
746     (when (is-connected logger)
747       (return-from attempt-reconnection nil))
748     
749     (log-disconnection logger)
750     (when (connection logger)
751       (ignore-errors (quit-maybe-timeout (connection logger) "Client terminated by server"))
752       (setf *recon-nick* (nickname logger)
753             *recon-server* (server logger)
754             *recon-username* (username logger)
755             *recon-realname* (realname logger)
756             *recon-password* (password logger)
757             *recon-async* (async logger)
758             *recon-user-output* (user-output logger)
759             *recon-private-log* (private-log logger)
760             *recon-unknown-log* (unknown-log logger)
761             *recon-formats* (formats logger)
762             *recon-logging-stream* (logging-stream logger)
763             *recon-channel-names* (channel-names logger))
764       (ignore-errors (remove-logger logger)))
765     
766     (let ((new-logger
767            (ignore-errors
768             (add-logger *recon-nick* *recon-server*
769                         :channels *recon-channel-names*
770                         :output *recon-user-output*
771                         :password *recon-password*
772                         :realname *recon-realname*
773                         :username *recon-username*
774                         :logging-stream *recon-logging-stream*
775                         :private-log *recon-private-log*
776                         :unknown-log *recon-unknown-log*
777                         :async *recon-async*
778                         :formats *recon-formats*))))
779       (when new-logger
780         (sleep 5)
781         (when (is-connected new-logger)
782           (log-reconnection new-logger)))))
783       
784   )
785
786 (defun daemon-monitor ()
787   "This function runs in the background and monitors the connection of the logger."
788   ;; run forever
789   (do ()
790       ()
791     (block main-loop
792       (dolist (logger *loggers*)
793         (do ((warning-time (warning-message-utime logger) (warning-message-utime logger)))
794             ((or (is-connected logger) (null warning-time)))
795           (cond
796            ((and warning-time (> (- (get-universal-time) warning-time) 180))
797             ;;give up frequent checking because no disconnection despite waiting
798             (setf (warning-message-utime logger) nil))
799            ((not (is-connected logger))
800             (unless warning-time
801               (setf (warning-message-utime logger) (get-universal-time)))
802             (attempt-reconnection logger)
803             ;;after a succesful reconnection, the value of logger will be invalid 
804             (sleep 30)
805             (return-from main-loop))
806            (t
807             (daemon-sleep 30)))))
808       (do ((i 0 (1+ i)))
809           ((or (>= i 20) (some (lambda (logger) (warning-message-utime logger)) *loggers*))) 
810         (daemon-sleep 15)))))
811   
812
813