kqueue.lisp - clic - Clic is an command line interactive client for gopher written in Common LISP
 (HTM) git clone git://bitreich.org/clic/ git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/clic/
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Tags
 (DIR) README
 (DIR) LICENSE
       ---
       kqueue.lisp (19988B)
       ---
            1 ;;;-*-Mode: LISP; Package: CCL -*-
            2 ;;
            3 ;; KQUEUE.LISP
            4 ;;
            5 ;; KQUEUE - BSD kernel event notification mechanism support for Common LISP.
            6 ;; Copyright (C) 2007 Terje Norderhaug <terje@in-progress.com>
            7 ;; Released under LGPL - see <http://www.gnu.org>.
            8 ;; Alternative licensing available upon request.
            9 ;; 
           10 ;; DISCLAIMER: The user of this module should understand that executing code is a potentially hazardous 
           11 ;; activity, and that many dangers and obstacles, marked or unmarked, may exist within this code.
           12 ;; As a condition of your use of the module, you assume all risk of personal injury, death, or property
           13 ;; loss, and all other bad things that may happen, even if caused by negligence, ignorance or stupidity.
           14 ;; The author is is no way responsible, and besides, does not have "deep pockets" nor any spare change.
           15 ;;
           16 ;; Version: 0.20 alpha (July 26, 2009) - subject to major revisions, so consider yourself warned.
           17 ;; Tested with Macintosh Common LISP 5.1 and 5.2, but is intended to be platform and system independent in the future.
           18 ;;
           19 ;; Email feedback and improvements to <terje@in-progress.com>.
           20 ;; Updated versions will be available from <http://www.in-progress.com/src/>.
           21 ;;
           22 ;; RELATED IMPLEMENTATIONS
           23 ;; There is another kevent.lisp for other platforms by Risto Laakso (merge?).
           24 ;; Also a Scheme kevent.ss by Jose Antonio Ortega.
           25 ;;
           26 ;; SEE ALSO:
           27 ;; http://people.freebsd.org/~jlemon/papers/kqueue.pdf
           28 ;; http://developer.apple.com/samplecode/FileNotification/index.html
           29 ;; The Man page for kqueue() or kevent().
           30 ;; PyKQueue - Python OO interface to KQueue.
           31 ;; LibEvent - an event notification library in C by Niels Provos.
           32 ;; Liboop - another abstract library in C on top of kevent or other kernel notification.
           33 
           34 #| HISTORY:
           35 
           36 2007-Oct-18 terje version 0.1 released on the Info-MCL mailing list.
           37 2008-Aug-21 terje load-framework-bundle is not needed under MCL 5.2
           38 2008-Aug-21 terje rename get-addr to lookup-function-in-bundle (only for pre MCL 5.2)
           39 2009-Jul-19 terje uses kevent-error condition and strerror.
           40 2009-Jul-24 terje reports errors unless nil-if-not-found in lookup-function-in-bundle. 
           41 2009-Jul-24 terje kevent :variant for C's intptr_t type for 64bit (and osx 10.5) compatibility.
           42 2009-Jul-25 terje 64bit support, dynamically determined for PPC. Kudos to Glen Foy for helping out.
           43 2009-Jul-25 terje make-kevent function.
           44 |#
           45 
           46 #| IMPLEMENTATION NOTES:
           47 
           48 kevents are copied into and from the kernel, so the records don't have to be kept in the app!
           49 kevents does not work in OSX before 10.3.
           50 *kevent-record* has to be explcitly set to :kevent64 to work on 64bit intel macs.
           51 Consider using sysctlbyname() to test for 64bit, 
           52  combining hw.cpu64bit_capable, hw.optional.x86_64 and hw.optional.64bitops
           53 |#
           54 
           55 (in-package :ccl)
           56 
           57 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
           58 
           59 #-ccl-5.2 ; has been added to MCL 5.2
           60 (defmethod load-framework-bundle ((framework-name string) &key (load-executable t))
           61   ;; FRAMWORK CALL FUNCTIONALITY FROM BSD.LISP
           62   ;; (C) 2003 Brendan Burns <bburns@cs.umass.edu>
           63   ;; Released under LGPL.
           64   (with-cfstrs ((framework framework-name))
           65     (let ((err 0)
           66           (baseURL nil)
           67           (bundleURL nil)
           68           (result nil))
           69       (rlet ((folder :fsref))
           70         ;; Find the folder holding the bundle
           71         (setf err (#_FSFindFolder #$kOnAppropriateDisk #$kFrameworksFolderType 
           72                    t folder))
           73         
           74         ;; if everything's cool, make a URL for it
           75         (when (zerop err)
           76           (setf baseURL (#_CFURLCreateFromFSRef (%null-ptr) folder))
           77           (if (%null-ptr-p baseURL) 
           78             (setf err #$coreFoundationUnknownErr)))
           79         
           80         ;; if everything's cool, make a URL for the bundle
           81         (when (zerop err)
           82           (setf bundleURL (#_CFURLCreateCopyAppendingPathComponent (%null-ptr) 
           83                            baseURL framework nil))
           84           (if (%null-ptr-p bundleURL) 
           85             (setf err #$coreFoundationUnknownErr)))
           86         
           87         ;; if everything's cool, load it
           88         (when (zerop err)
           89           (setf result (#_CFBundleCreate (%null-ptr) bundleURL))
           90           (if (%null-ptr-p result)
           91             (setf err #$coreFoundationUnknownErr)))
           92         
           93         ;; if everything's cool, and the user wants it loaded, load it
           94         (when (and load-executable (zerop err))
           95           (if (not (#_CFBundleLoadExecutable result))
           96             (setf err #$coreFoundationUnknownErr)))
           97         
           98         ;; if there's an error, but we've got a pointer, free it and clear result
           99         (when (and (not (zerop err)) (not (%null-ptr-p result)))
          100           (#_CFRelease result)
          101           (setf result nil))
          102         
          103         ;; free the URLs if there non-null
          104         (when (not (%null-ptr-p bundleURL))
          105           (#_CFRelease bundleURL))
          106         (when (not (%null-ptr-p baseURL))
          107           (#_CFRelease baseURL))
          108         
          109         ;; return pointer + error value
          110         (values result err)))))
          111 
          112 #+ignore
          113 (defun get-addr (bundle name)
          114   (let* ((addr (#_CFBundleGetFunctionPointerForName bundle name)))
          115     (rlet ((buf :long))
          116       (setf (%get-ptr buf) addr)
          117       (ash (%get-signed-long buf) -2))))
          118 
          119 #-ccl-5.2
          120 (defun lookup-function-in-bundle (name bundle &optional nil-if-not-found)
          121   (with-cfstrs ((str name))
          122     (let* ((addr (#_CFBundleGetFunctionPointerForName bundle str)))
          123       (if (%null-ptr-p addr)
          124         (unless nil-if-not-found
          125           (error "Couldn't resolve address of foreign function ~s" name))
          126         (rlet ((buf :long)) ;; mcl 5.2 uses %fixnum-from-macptr here
          127           (setf (%get-ptr buf) addr)
          128           (ash (%get-signed-long buf) -2))))))
          129 
          130 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
          131 ;; Convenient way to declare BSD system calls
          132 
          133 #+ignore
          134 (defparameter *system-bundle*
          135   #+ccl-5.2 (get-bundle-for-framework-name "System.framework")
          136   #-ccl-5.2
          137   (let ((bundle (load-framework-bundle "System.framework")))
          138     (terminate-when-unreachable bundle (lambda (b)(#_CFRelease b)))
          139     bundle))
          140 
          141 (defmacro declare-bundle-ff (name name-string &rest arglist &aux (fn (gensym (format nil "ff_~A_" (string name)))))
          142   ;; Is there an existing define-trap like macro for this? or could one be modified for use with bundles?
          143   `(progn
          144      (defloadvar ,fn
          145        (let* ((bundle #+ccl-5.2 (get-bundle-for-framework-name "System.framework")
          146                       #-ccl-5.2
          147                       (let ((bundle (load-framework-bundle "System.framework")))
          148                         (terminate-when-unreachable bundle (lambda (b)(#_CFRelease b)))
          149                         bundle)))
          150          (lookup-function-in-bundle ,name-string bundle)))
          151      ,(let ((args (do ((arglist arglist (cddr arglist))
          152                       (result))
          153                      ((not (cdr arglist)) (nreverse result))
          154                    (push (second arglist) result))))        
          155        `(defun ,name ,args
          156           (ppc-ff-call ,fn ,@arglist)))))
          157 
          158 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
          159 
          160 (declare-bundle-ff %system-kqueue "kqueue" 
          161                    :signed-fullword) ;; returns a file descriptor no!
          162 
          163 (defun system-kqueue ()
          164   (let ((kq (%system-kqueue)))
          165     (if (= kq -1)
          166       (ecase (%system-errno)
          167         (12 (error "The kernel failed to allocate enough memory for the kernel queue")) ; ENOMEM
          168         (24 (error "The per-process descriptor table is full")) ; EMFILE
          169         (23 (error "The system file table is full"))) ; ENFILE 
          170       kq)))
          171 
          172 (declare-bundle-ff %system-kevent "kevent"
          173                   :unsigned-fullword kq
          174                   :address ke
          175                   :unsigned-fullword nke
          176                   :address ko
          177                   :unsigned-fullword nko
          178                   :address timeout
          179                   :signed-fullword)
          180 
          181 (declare-bundle-ff %system-open "open" 
          182                    :address name 
          183                    :unsigned-fullword mode
          184                    :unsigned-fullword arg 
          185                    :signed-fullword)
          186  
          187 (declare-bundle-ff %system-close "close"
          188                    :unsigned-fullword fd 
          189                    :signed-fullword)
          190 
          191 (declare-bundle-ff %system-errno* "__error" 
          192                    :signed-fullword)
          193 
          194 (declare-bundle-ff %system-strerror "strerror" 
          195                    :signed-fullword errno
          196                    :address)
          197 
          198 (defun %system-errno ()
          199   (%get-fixnum (%int-to-ptr (%system-errno*))))
          200 
          201 ; (%system-errno)
          202 
          203 (defconstant $O-EVTONLY #x8000)
          204 ; (defconstant $O-NONBLOCK #x800 "Non blocking mode")
          205 
          206 (defun system-open (posix-namestring)
          207   "Low level open function, as in C, returns an fd number"
          208   (with-cstrs ((name posix-namestring))
          209     (%system-open name $O-EVTONLY 0)))
          210 
          211 (defun system-close (fd)
          212   (%system-close fd))
          213 
          214 (defrecord timespec
          215   (sec :unsigned-long)
          216   (usec :unsigned-long))
          217 
          218 (defVar *kevent-record* nil)
          219 
          220 (def-ccl-pointers determine-64bit-kevents ()
          221   (setf *kevent-record*
          222        (if (ccl::gestalt #$gestaltPowerPCProcessorFeatures
          223                         #+ccl-5.2 #$gestaltPowerPCHas64BitSupport #-ccl-5.2 6)
          224           :kevent32
          225           :kevent64)))
          226 
          227 (defrecord :kevent32
          228   (ident :unsigned-long) ; uintptr_t
          229   (filter :short)
          230   (flags :unsigned-short)
          231   (fflags :unsigned-long)
          232   (data :long)  ; intptr_t
          233   (udata :pointer))
          234 
          235 (defrecord :kevent64
          236   (:variant ; uintptr_t
          237    ((ident64 :uint64))
          238    ((ident :unsigned-long)))
          239   (filter :short)
          240   (flags :unsigned-short)
          241   (fflags :unsigned-long)
          242   (:variant  ; intptr_t
          243    ((data64 :sint64))
          244    ((data :long)))
          245   (:variant ; RMCL :pointer is 32bit
          246    ((udata64 :uint64))
          247    ((udata :pointer))))
          248 
          249 (defun make-kevent (&key (ident 0) (filter 0) (flags 0) (fflags 0) (data 0) (udata *null-ptr*))
          250    (ecase *kevent-record*
          251       (:kevent64   
          252        (make-record kevent64
          253                     :ident ident
          254                     :filter filter 
          255                     :flags flags
          256                     :fflags fflags
          257                     :data data 
          258                     :udata udata))
          259       (:kevent32
          260        (make-record kevent32
          261                     :ident ident
          262                     :filter filter 
          263                     :flags flags
          264                     :fflags fflags
          265                     :data data 
          266                     :udata udata))))
          267 
          268 (defun kevent-rref (ke field)
          269    (ecase *kevent-record*
          270       (:kevent32
          271        (ecase field
          272           (:ident (rref ke :kevent32.ident))
          273           (:filter (rref ke :kevent32.filter))
          274           (:flags (rref ke :kevent32.flags))
          275           (:fflags (rref ke :kevent32.fflags))
          276           (:data (rref ke :kevent32.data))
          277           (:udata (rref ke :kevent32.udata))))
          278       (:kevent64
          279        (ecase field
          280           (:ident (rref ke :kevent64.ident))
          281           (:filter (rref ke :kevent64.filter))
          282           (:flags (rref ke :kevent64.flags))
          283           (:fflags (rref ke :kevent64.fflags))
          284           (:data (rref ke :kevent64.data))
          285           (:udata (rref ke :kevent64.udata))))))
          286 
          287 (defun kevent-filter (ke)
          288    (kevent-rref ke :filter))
          289 
          290 (defun kevent-flags (ke)
          291    (kevent-rref ke :flags))
          292 
          293 (defun kevent-data (ke)
          294    (kevent-rref ke :data))
          295 
          296 
          297 ;; FILTER TYPES:
          298 
          299 (eval-when (:compile-toplevel :load-toplevel :execute) ; added by binghe
          300 
          301 (defconstant $kevent-read-filter -1 "Data available to read")
          302 (defconstant $kevent-write-filter -2 "Writing is possible")
          303 (defconstant $kevent-aio-filter -3 "AIO system call has been made")
          304 (defconstant $kevent-vnode-filter -4 "Event occured on a file descriptor")
          305 (defconstant $kevent-proc-filter -5 "Process performed one or more of the requested events")
          306 (defconstant $kevent-signal-filter -6 "Attempted to deliver a signal to a process")
          307 (defconstant $kevent-timer-filter -7 "Establishes an arbitrary timer")
          308 (defconstant $kevent-netdev-filter -8 "Event occured on a network device")
          309 (defconstant $kevent-filesystem-filter -9)
          310 
          311 ) ; eval-when
          312 
          313 ; FLAGS:
          314 
          315 (defconstant $kevent-add #x01)
          316 (defconstant $kevent-delete #x02)
          317 (defconstant $kevent-enable #x04)
          318 (defconstant $kevent-disable #x08)
          319 (defconstant $kevent-oneshot #x10)
          320 (defconstant $kevent-clear #x20)
          321 (defconstant $kevent-error #x4000)
          322 (defconstant $kevent-eof #x8000 "EV_EOF")
          323 
          324 ;; FFLAGS:
          325 
          326 (defconstant $kevent-file-delete #x01 "The file was unlinked from the file system")
          327 (defconstant $kevent-file-write #x02 "A write occurred on the file")
          328 (defconstant $kevent-file-extend #x04 "The file was extended")
          329 (defconstant $kevent-file-attrib #x08 "The file had its attributes changed")
          330 (defconstant $kevent-file-link #x10 "The link count on the file changed")
          331 (defconstant $kevent-file-rename #x20 "The file was renamed")
          332 (defconstant $kevent-file-revoke #x40 "Access to the file was revoked or the file system was unmounted")
          333 (defconstant $kevent-file-all (logior $kevent-file-delete $kevent-file-write $kevent-file-extend
          334                                       $kevent-file-attrib $kevent-file-link $kevent-file-rename $kevent-file-revoke))
          335 
          336 
          337 (defconstant $kevent-net-linkup #x01 "Link is up")
          338 (defconstant $kevent-net-linkdown #x02 "Link is down")
          339 (defconstant $kevent-net-linkinvalid #x04 "Link state is invalid")
          340 (defconstant $kevent-net-added #x08 "IP adress added")
          341 (defconstant $kevent-net-deleted #x10 "IP adress deleted")
          342 
          343 (define-condition kevent-error (simple-error)
          344   ((errno :initform NIL :initarg :errno)
          345    (ko :initform nil :type (or null kevent) :initarg :ko)
          346    (syserr :initform (%system-errno)))
          347   (:report 
          348    (lambda (c s)
          349      (with-slots (errno ko syserr) c
          350        (format s "kevent system call error ~A [~A]" errno syserr) 
          351        (when errno 
          352           (format s "(~A)" (%get-cstring (%system-strerror errno))))
          353        (when ko
          354           (format s " for ")
          355           (let ((*standard-output* s))
          356             (print-record ko *kevent-record*)))))))
          357 
          358 (defun %kevent (kq &optional ke ko (timeout 0))
          359   (check-type kq integer)
          360   (rlet ((&timeout :timespec :sec timeout :usec 1))
          361     (let ((num (with-timer ;; does not seem to make a difference...  
          362                  (%system-kevent kq (or ke (%null-ptr))(if ke 1 0)(or ko (%null-ptr))(if ko 1 0) &timeout))))
          363       ; "If an error occurs while processing an element of the changelist and there 
          364       ; is enough room in the eventlist, then the event will be placed in the eventlist with 
          365       ; EV_ERROR set in flags and the system error in data."
          366       (when (and ko (plusp (logand $kevent-error (kevent-flags ko))))
          367         (error 'kevent-error 
          368                               :errno (kevent-data ko)
          369                :ko ko))
          370       ; "Otherwise, -1 will be returned, and errno will be set to indicate the error condition."
          371       (when (= num -1)
          372         ;; hack - opentransport provides the constants for the errors documented for the call 
          373         (case (%system-errno)
          374           (0 (error "kevent system call failed with an unspecified error")) ;; should not happen!
          375           (13 (error "The process does not have permission to register a filter")) 
          376           (14 (error "There was an error reading or writing the kevent structure"))  ; EFAULT
          377           (9 (error "The specified descriptor is invalid")) ; EBADF
          378           (4 (error "A signal was delivered before the timeout expired and before any events were placed on the kqueue for return.")) ; EINTR
          379           (22 (error "The specified time limit or filter is invalid")) ; EINVAL
          380           (2 (error "The event could not be found to be modified or deleted")) ; ENOENT
          381           (12 (error "No memory was available to register the event")) ; ENOMEM
          382           (78 (error "The specified process to attach to does not exist"))) ; ESRCH
          383         ;; shouldn't get here... 
          384         (errchk (%system-errno))
          385         (error "error ~A" (%system-errno)))
          386       (unless (zerop num)
          387          (values ko num)))))
          388 
          389 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
          390 ;; CLOS INTERFACE
          391 
          392 (defclass kqueue ()
          393   ((kq :initform (system-kqueue) 
          394        :documentation "file descriptor referencing the kqueue")
          395    (fds :initform NIL)) ;; ## better if kept on top level, perhaps as a hash table...
          396   (:documentation "A kernal event notification channel"))
          397 
          398 (defmethod initialize-instance :after ((q kqueue) &rest rest)
          399   (declare (ignore rest))
          400   (terminate-when-unreachable q 'kqueue-close))
          401 
          402 (defmethod kqueue-close ((q kqueue))
          403   (with-slots (kq fds) q
          404     (when (or kq fds) ;; allow repeated close
          405       (system-close kq)
          406       (setf fds NIL)
          407       (setf kq NIL))))
          408 
          409 (defmethod kqueue-poll ((q kqueue))
          410   "Polls a kqueue for kevents"
          411   ;; may not have to be cleared, but just in case:
          412   (flet ((kqueue-poll2 (ko)
          413            (let ((result (with-slots (kq) q
          414                             (without-interrupts 
          415                              (%kevent kq NIL ko)))))
          416              (when result
          417                 (let ((type  (kevent-filter result)))
          418                   (ecase type
          419                      (0 (values))
          420                      (#.$kevent-read-filter
          421                           (values
          422                            :read
          423                            (kevent-rref result :ident)
          424                            (kevent-rref result :flags)
          425                            (kevent-rref result :fflags)
          426                            (kevent-rref result :data)
          427                            (kevent-rref result :udata)))
          428                       (#.$kevent-write-filter :write)
          429                       (#.$kevent-aio-filter :aio)
          430                       (#.$kevent-vnode-filter
          431                            (values
          432                             :vnode
          433                             (cdr (assoc (kevent-rref result :ident) (slot-value q 'fds)))
          434                             (kevent-rref result :flags)
          435                             (kevent-rref result :fflags)
          436                             (kevent-rref result :data)
          437                             (kevent-rref result :udata)))
          438                       (#.$kevent-filesystem-filter :filesystem)))))))
          439     (ecase *kevent-record*
          440        (:kevent64
          441         (rlet ((ko :kevent64 :ident 0 :filter 0 :flags 0 :fflags 0 :data 0 :udata (%null-ptr)))
          442           (kqueue-poll2 ko)))
          443        (:kevent32
          444         (rlet ((ko :kevent32 :ident 0 :filter 0 :flags 0 :fflags 0 :data 0 :udata (%null-ptr)))
          445           (kqueue-poll2 ko))))))
          446 
          447 (defmethod kqueue-subscribe ((q kqueue) &key ident filter (flags 0) (fflags 0) (data 0) (udata (%null-ptr)))
          448   (let ((ke (make-kevent :ident ident
          449                          :filter filter 
          450                          :flags flags
          451                          :fflags fflags
          452                          :data data 
          453                          :udata udata)))
          454     (with-slots (kq) q
          455        (without-interrupts
          456         (%kevent kq ke)))))
          457 
          458 (defmethod kqueue-vnode-subscribe ((q kqueue) pathname)
          459   "Makes the queue report an event when there is a change to a directory or file" 
          460   (let* ((namestring (posix-namestring (full-pathname pathname)))
          461          (fd (system-open namestring)))
          462     (with-slots (fds) q
          463       (push (cons fd pathname) fds))
          464     (kqueue-subscribe q 
          465                       :ident fd 
          466                       :filter $kevent-vnode-filter 
          467                       :flags (logior $kevent-add $kevent-clear) 
          468                       :fflags $kevent-file-all)
          469     namestring))
          470 
          471 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
          472 
          473 #+test
          474 (defun kevent-d (pathname &optional (*standard-output* (fred)))
          475   "Report changes to a file or directory"
          476   (loop
          477     with kqueue = (make-instance 'kqueue)
          478     with sub = (kqueue-vnode-subscribe kqueue pathname) 
          479     for i from 1 to 60
          480     for result = (multiple-value-list (kqueue-poll kqueue))
          481     unless (equal result '(NIL))
          482     do (progn
          483          (format T "~A~%" result)
          484          (force-output))
          485     ; do (process-allow-schedule)
          486     do (sleep 1)
          487     finally (write-line "Done")
          488     ))
          489 
          490 #|
          491 
          492 ; Report changes to this file in a fred window (save this document to see what happens):
          493 
          494 (process-run-function "kevent-d" #'kevent-d *loading-file-source-file*
          495                       (fred))
          496 
          497 ; Reports files added or removed from the directory of this file:
          498 
          499 (process-run-function "kevent-d" #'kevent-d 
          500                       (make-pathname :directory (pathname-directory *loading-file-source-file*))
          501                       (fred))
          502 |#
          503 
          504 
          505 
          506