-
Notifications
You must be signed in to change notification settings - Fork 2
/
robust-watch.rkt
200 lines (170 loc) · 6.84 KB
/
robust-watch.rkt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#lang racket/base
;; This module provides a cross-platform, polling based file watch.
(require
racket/contract
racket/list
racket/set)
(provide
(contract-out
[robust-poll-milliseconds (parameter/c exact-positive-integer?)]
[robust-watch (->* () (path-on-disk? #:batch? any/c) thread?)]))
;; ------------------------------------------------------------------
;; Implementation
(require
racket/hash
"./filesystem.rkt"
"./threads.rkt")
(define-values (report-activity report-status) (report-iface 'robust))
(define robust-poll-milliseconds (make-parameter 250))
(define (get-file-attributes path)
(with-handlers ([exn:fail? (λ _ #f)])
(list (file-or-directory-modify-seconds path)
(file-or-directory-permissions path 'bits)
(cond
[(file-exists? path) (file-size path)] ; does path resolve to a file?
[(directory-exists? path) 0] ; does path resolve to a directory?
[else 0]))))
(define (get-listing-numbers listing)
(for/list ([p listing])
(define attrs (get-file-attributes p))
(if (not attrs)
-1
(apply + attrs))))
(define (get-robust-state path)
(define listing (if (file-exists? path)
(list path)
(recursive-file-list path)))
(make-immutable-hash (map cons
listing
(get-listing-numbers listing))))
(define (mark-changes prev next)
(hash-union prev next
#:combine/key (lambda (k a b)
(if (= a b) 'same 'change))))
(define (mark-status prev next)
(make-immutable-hash
(map
(lambda (pair)
(if (symbol? (cdr pair))
pair
(cons (car pair)
(if (path-on-disk? (car pair)) 'add 'remove))))
(hash->list (mark-changes prev next)))))
(define (get-next-status current-state complete-path)
(define next (get-robust-state complete-path))
(define status-marked-hash (mark-status current-state next))
(values next status-marked-hash))
(define (robust-watch [path (current-directory)] #:batch? [batch? #f])
(define complete-path (path->complete-path (simplify-path path #t)))
(thread
(lambda ()
(let loop ([state (get-robust-state complete-path)])
(define exists? (path-on-disk? complete-path))
(define next
(cond [(not exists?)
(report-activity 'remove complete-path)
#f]
[(equal? #f batch?)
; file exists, we should NOT batch notifications
(define-values (next status-marked-hash)
(get-next-status state complete-path))
(hash-for-each
status-marked-hash
(lambda (affected op)
(unless (equal? op 'same)
(report-activity op affected))))
next]
[else
; file exists, we SHOULD batch notifications
(define-values (next status-marked-hash)
(get-next-status state complete-path))
(define report (filter-not (lambda (arg) (equal? 'same (cdr arg)))
(hash->list status-marked-hash)))
(when (not (null? report))
(define messages
(for/list ([item report])
;item looks like, e.g.: (cons <path:/foo/bar> 'add)
(list 'robust (cdr item) (car item))))
(report-change-literal messages))
next]))
; if we reported a 'remove on the original state then next is #f and we can stop
; watching.
(when next
(sync/enable-break (alarm-evt (+ (current-inexact-milliseconds)
(robust-poll-milliseconds))))
(loop next))))))
(module+ test
(require
rackunit
racket/async-channel
racket/file
(submod "./filesystem.rkt" test-lib)
(submod "./threads.rkt" test-lib))
(define (allow-poll) (sleep (/ (robust-poll-milliseconds) 1000)))
(test-case
"Robust watch over directory, unbatched"
(parameterize ([current-directory (create-temp-directory)]
[robust-poll-milliseconds 50]
[file-activity-channel (make-async-channel)])
(create-file "a")
(create-file "b")
(create-file "c")
(define th (robust-watch))
(allow-poll)
(delete-file "c") (create-file "c")
(delete-file "b")
(allow-poll)
(delete-directory/files (current-directory))
(thread-wait th)
; TODO: Paratition these messages into "may appear" and "must appear"
(define expected-messages
`((robust change ,(build-path (current-directory) "c")) ; must
(robust remove ,(build-path (current-directory) "b")) ; may
(robust remove ,(build-path (current-directory))))) ; must
(let loop ()
(define msg (file-watcher-channel-try-get))
(when msg
(check-true (and (member msg expected-messages) #t))
(loop)))))
(test-case
"Robust watch over directory, batched"
(parameterize ([current-directory (create-temp-directory)]
[robust-poll-milliseconds 50]
[file-activity-channel (make-async-channel)])
(define dir2 (create-temp-directory))
(parameterize ([current-directory dir2])
(make-directory* (build-path "foo" "bar" "baz"))
(current-directory (build-path "foo" "bar" "baz"))
(create-file "a.txt"))
(define th (robust-watch #:batch? #t))
(allow-poll)
(rename-file-or-directory (build-path dir2 "foo")
(build-path (current-directory) "foo"))
(delete-directory/files dir2)
(allow-poll)
(allow-poll)
(define messages (file-watcher-channel-try-get))
(allow-poll)
(define dir (current-directory))
(check-equal? (sort messages path<? #:key last)
`((robust add ,(build-path dir "foo"))
(robust add ,(build-path dir "foo/bar"))
(robust add ,(build-path dir "foo/bar/baz"))
(robust add ,(build-path dir "foo/bar/baz/a.txt"))))
(delete-directory/files (current-directory))
(thread-wait th)))
(test-case
"Robust watch over file"
(parameterize ([current-directory (create-temp-directory)]
[robust-poll-milliseconds 50]
[file-activity-channel (make-async-channel)])
(create-file (build-path "a"))
(define th (robust-watch "a"))
(allow-poll)
(delete-file "a")
(allow-poll)
(thread-wait th)
(delete-directory/files (current-directory))
(check-equal?
(sync (file-activity-channel))
`(robust remove ,(build-path (current-directory) "a"))))))