#lang imperative-syndicate
;; Filesystem change monitor driver

(provide (struct-out file-content)
         spawn-filesystem-driver)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(assertion-struct file-content (name reader-proc content))

;; Internal driver ground-level protocol
(message-struct file-changed (name))
(message-struct file-container-changed (parent-path))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define (spawn-filesystem-driver)
  (spawn #:name 'drivers/filesystem
         (during/spawn (observe (file-content $name $reader-proc _))
           #:name (list 'file-content name reader-proc)
           (track-file name reader-proc))
         (during (observe (inbound (file-changed $name)))
           (monitor-thread name))))

(define (read-file name reader-proc)
  (and (or (file-exists? name) (directory-exists? name))
       (reader-proc name)))

(define (path->parent-path name)
  (let-values (((parent-path _leaf _syntactically-dir?)
                (split-path (path->complete-path name))))
    parent-path))

(define (track-file name reader-proc)
  (field [content (read-file name reader-proc)])
  (assert (file-content name reader-proc (content)))
  (on (message (inbound (file-changed name)))
      (content (read-file name reader-proc)))
  ;; This horrible hack is required to work around limitations in the
  ;; OS's file-change reporting. It seems (?) as if, monitoring both
  ;; "a/b" and "a/", that only the event for "a/" will be fired when
  ;; "a/b" changes. This manifests as follows: if I monitor "a/b" and
  ;; "a/nonexistent", then when "a/b" changes, only "a/nonexistent"'s
  ;; event will fire. Therefore, I've kludged in the
  ;; `file-container-changed` message, which copes with one level of
  ;; directory hierarchy of this problem.
  ;;
  ;; TODO: Consider whether it will actually be required to listen for
  ;; file-container-changed events for ALL recursive parents of the
  ;; path of interest up to the root.
  ;;
  (on (message (inbound (file-container-changed (path->parent-path name))))
      (content (read-file name reader-proc))))

(define (monitor-thread name)
  (define control-ch (make-channel))
  (thread (lambda ()
            (define parent-path (path->parent-path name))
            (let loop ()
              (sync (handle-evt control-ch
                                (lambda (msg)
                                  ;; (log-info "track-file-changes ~v: ~v" name msg)
                                  (match msg
                                    ['quit (void)])))
                    (if (or (file-exists? name) (directory-exists? name)) ;; TODO: TOCTTOU :-(
                        (handle-evt (filesystem-change-evt name)
                                    (lambda (_dummy)
                                      ;; (log-info "track-file-changes ~v: changed" name)
                                      (ground-send! (inbound (file-changed name)))
                                      (loop)))
                        (handle-evt (filesystem-change-evt parent-path)
                                    (lambda (_dummy)
                                      ;; (log-info "track-file-changes ~v: directory changed" name)
                                      (ground-send! (inbound (file-container-changed parent-path)))
                                      (loop))))))
            (signal-background-activity! -1)))
  (signal-background-activity! +1)
  (on-stop (channel-put control-ch 'quit)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(spawn-filesystem-driver)
