-
Notifications
You must be signed in to change notification settings - Fork 234
/
fswatch.ml
469 lines (411 loc) · 13.5 KB
/
fswatch.ml
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
(* Unison file synchronizer: src/fswatch.ml *)
(* Copyright 1999-2020, Benjamin C. Pierce
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*)
(*
Protocol description
====================
The file monitoring process receives commands from stdin and
responds to stdout. Commands and responds are single lines composed
of an identifier followed by a single space and a space separated
list of arguments. Arguments are percent-encoded. At the minimum,
spaces and newlines must be escaped. The two processes should accept
any other escaped character.
Unison and the child process starts by indicating the protocol
version they support. At the moment, they should just output the
line 'VERSION 1'.
Debugging is enabled by the 'DEBUG' command.
At any time, the child process can signal an error by sending an
"ERROR msg" message.
When Unison start scanning a part of the replica, it emits command:
'START hash fspath path', thus indicating the archive hash (that
uniquely determines the replica) the replica's fspath and the path
where the scanning process starts. The child process should start
monitoring this location, then acknowledge the command by sending an
'OK' response.
When Unison starts scanning a directory, it emits the command
'DIR path1', where 'path1' is relative to the path given by the
START command (the location of the directory can be obtained by
concatenation of 'fspath', 'path', and 'path1'). The child process
should then start monitoring the directory, before sending an 'OK'
response.
When Unison encounters a followed link, it emits the command
'LINK path1'. The child process is expected to start monitoring
the link target before replying by 'OK'.
Unison signals that it is done scanning the part of the replica
described by the START process by emitting the 'DONE' command. The
child process should not respond to this command.
Unison can ask for a list of paths containing changes in a given
replica by sending the 'CHANGES hash' command. The child process
responds by a sequence of 'RECURSIVE path' responses, followed by a
'DONE' response. These paths should be relative to the replica
'fspath'. The child process will not have to report this changes any
more: it can consider that Unison has taken this information into
account once and for all. Thus, it is expected to thereafter report
only further changes.
Unison can wait for changes in a replica by emitting a 'WAIT hash'
command. It can watch several replicas by sending a serie of these
commands. The child process is expected to respond once, by a
'CHANGE hash1 ... hash2' response that lists the changed replicas
among those included in a 'WAIT' command, when changes are
available. It should cancel pending waits when any other command is
received.
Finally, the command 'RESET hash' tells the child process to stop
watching the given replica. In particular, it can discard any
pending change information for this replica.
*)
let debug = Util.debug "fswatch"
let debugverbose = Trace.debug "fswatch+"
let (>>=) = Lwt.bind
let rec really_write_substring o s pos len =
Lwt_unix.write_substring o s pos len >>= fun l ->
if l = len then
Lwt.return ()
else
really_write_substring o s (pos + l) (len - l)
let split_on_space s =
try
let i = String.index s ' ' in
(String.sub s 0 i,
String.sub s (i + 1) (String.length s - i - 1))
with Not_found ->
(s, "")
let disallowed_char c =
match c with
'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_' | '.' | '~'
| '!' | '*' | '\'' | '(' | ')' | ';' | ':' | '@' | '&'
| '=' | '+' | '$' | ',' | '/' | '?' | '#' | '[' | ']' ->
false
| _ ->
true
let quote s =
let l = String.length s in
let n = ref 0 in
for i = 0 to l - 1 do if disallowed_char s.[i] then incr n done;
if !n = 0 then s else begin
let q = Bytes.create (l + 2 * !n) in
let j = ref 0 in
let hex = "0123456789ABCDEF" in
for i = 0 to l - 1 do
let c = s.[i] in
if disallowed_char s.[i] then begin
Bytes.set q !j '%';
Bytes.set q (!j + 1) hex.[Char.code c lsr 4];
Bytes.set q (!j + 2) hex.[Char.code c land 15];
j := !j + 3
end else begin
Bytes.set q !j c;
incr j
end
done;
Bytes.to_string q
end
let unquote s =
let l = String.length s in
let n = ref 0 in
for i = 0 to l - 1 do if s.[i] = '%' then incr n done;
if !n = 0 then s else begin
let hex_char c =
match c with
'0'..'9' -> Char.code c - Char.code '0'
| 'a'..'f' -> Char.code c - Char.code 'a' + 10
| 'A'..'F' -> Char.code c - Char.code 'A' + 10
| _ -> invalid_arg "unquote"
in
let u = Bytes.create (l - 2 * !n) in
let j = ref 0 in
for i = 0 to l - 2 * !n - 1 do
let c = s.[!j] in
if c = '%' then begin
Bytes.set u i (Char.chr ((hex_char s.[!j + 1]) lsl 4 + hex_char s.[!j + 2]));
j := !j + 3
end else begin
Bytes.set u i c;
incr j
end
done;
Bytes.to_string u
end
module Cond = struct
type t = unit Lwt.t list ref
let make () = ref []
let signal s =
let wl = !s in
s := [];
List.iter (fun w -> Lwt.wakeup w ()) wl
let wait s =
let t = Lwt.wait () in
s := t :: !s;
t
end
(****)
let useWatcher =
Prefs.createBool "watch" false
~category:(`Advanced `General)
"when set, use a file watcher process to detect changes"
"Unison uses a file watcher process, when available, to detect filesystem \
changes; this is used to speed up update detection. Setting this flag to \
false disables the use of this process."
let printf o fmt =
Printf.ksprintf
(fun s ->
debugverbose (fun () -> Util.msg "<< %s" s);
Util.convertUnixErrorsToFatal
"sending command to filesystem watcher"
(fun () -> Lwt_unix.run (really_write_substring o s 0 (String.length s))))
fmt
let read_line i =
let b = Buffer.create 160 in
let buf = Bytes.create 160 in
let start = ref 0 in
let last = ref 0 in
let rec read () =
begin
if !start = !last then begin
Lwt_unix.read i buf 0 160 >>= fun l ->
if l = 0 then
raise (Util.Fatal "Filesystem watcher died unexpectively");
start := 0; last := l;
Lwt.return ()
end else
Lwt.return ()
end >>= fun () ->
try
let i = Bytes.index_from buf !start '\n' in
if i >= !last then raise Not_found;
Buffer.add_subbytes b buf !start (i - !start);
start := i + 1;
let s = Buffer.contents b in
Buffer.clear b;
debugverbose (fun() -> Util.msg ">> %s\n" s);
Lwt.return s
with Not_found ->
Buffer.add_subbytes b buf !start (!last - !start);
start := 0; last := 0;
read ()
in
read
(****)
let path =
try
Str.split (Str.regexp (if Util.osType = `Win32 then ";" else ":"))
(Sys.getenv "PATH")
with Not_found ->
[]
let search_in_path ?(path = path) name =
Filename.concat
(List.find (fun dir ->
let p = Filename.concat dir name in
let found = System.file_exists p in
debug (fun () -> Util.msg "'%s' ...%s\n" p
(match found with true -> "found" | false -> "not found"));
found)
path)
name
let exec_path = [Sys.executable_name]
(*
try
(* Linux *)
[System.fspathFromString (Unix.readlink "/proc/self/exe")]
with Unix.Unix_error _ | Invalid_argument _ ->
let name = (System.argv ()).(0) in
if not (Filename.is_relative name) then
[System.fspathFromString name]
else if Filename.is_implicit name then
try
[search_in_path name]
with Not_found ->
[]
else
[System.fspathConcat (System.getcwd ()) name]
*)
let exec_dir = List.map Filename.dirname exec_path
let watcher =
lazy
(let suffix = if Util.osType = `Win32 then ".exe" else "" in
debug (fun () -> Util.msg "File monitoring helper program...\n");
(try
search_in_path ~path:(exec_dir @ path)
("unison-fsmonitor-" ^ Uutil.myMajorVersion ^ suffix)
with Not_found ->
search_in_path ~path:(exec_dir @ path)
("unison-fsmonitor" ^ suffix)))
type 'a exn_option = Value of 'a | Exn of exn | Nothing
type conn =
{ output : Lwt_unix.file_descr;
has_changes : Cond.t;
has_line : Cond.t;
line_read : Cond.t;
mutable last_line : string exn_option }
let conn = ref None
let rec reader conn read_line =
read_line () >>= fun l ->
Cond.signal conn.has_changes;
if fst (split_on_space l) = "CHANGES" then begin
reader conn read_line
end else begin
conn.last_line <- Value l;
Cond.signal conn.has_line;
Cond.wait conn.line_read >>= fun () ->
reader conn read_line
end
let safeClose fd = try Lwt_unix.close fd with Unix.Unix_error _ -> ()
let currentConnection () =
match !conn with
Some c -> c
| None -> raise (Util.Fatal ("File monitoring helper program not running"))
let closeConnection () =
match !conn with
Some c -> conn := None; safeClose c.output
| None -> ()
let connected () = !conn <> None
let startProcess () =
try
let w = Lazy.force watcher in
let (i1,o1) = Lwt_unix.pipe_out () in
let (i2,o2) = Lwt_unix.pipe_in () in
Lwt_unix.set_close_on_exec i2;
Lwt_unix.set_close_on_exec o1;
Util.convertUnixErrorsToFatal "starting filesystem watcher" (fun () ->
ignore (System.create_process w [|w|] i1 o2 Unix.stderr));
Unix.close i1; Unix.close o2;
let c =
{ output = o1;
has_changes = Cond.make ();
has_line = Cond.make ();
line_read = Cond.make ();
last_line = Nothing }
in
ignore
(Lwt.catch (fun () -> reader c (read_line i2))
(fun e ->
closeConnection (); safeClose i2;
Cond.signal c.has_changes;
c.last_line <- Exn e; Cond.signal c.has_line;
Lwt.return ()));
conn := Some c;
true
with Not_found ->
false
let emitCmd fmt =
let c = currentConnection () in
try
printf c.output fmt
with e ->
closeConnection ();
raise e
let rec readLine () =
let c = currentConnection () in
match c.last_line with
Nothing -> Lwt_unix.run (Cond.wait c.has_line); readLine ()
| Value l -> c.last_line <- Nothing; Cond.signal c.line_read; l
| Exn e -> raise e
let badResponse cmd args expected =
closeConnection ();
if cmd = "ERROR" then
raise (Util.Fatal ("Filesystem watcher error: " ^ (unquote args) ^ "\n\
The watcher can be disabled by setting preference \
'watch' to false"))
else
raise
(Util.Fatal
(Format.sprintf
"Unexpected response '%s %s' from the filesystem watcher \
(expected %s)" cmd args expected))
let readAck () =
let (cmd, args) = split_on_space (readLine ()) in
if cmd <> "OK" then badResponse cmd args "OK"
let readVersion () =
let (cmd, args) = split_on_space (readLine ()) in
if cmd <> "VERSION" then badResponse cmd args "VERSION"
let exchangeVersions () =
let res = startProcess () in
if res then begin
emitCmd "VERSION 1\n";
debug (fun () -> Util.msg "debugging enabled\n"; emitCmd "DEBUG\n");
readVersion ()
end;
res
(****)
type archiveHash = string
let scanning = ref false
let start_path = ref ""
let relpath path =
let s2 = Path.toString path in
let l1 = String.length !start_path in
let l2 = String.length s2 in
if l1 = 0 then begin
s2
end else if l1 = l2 then begin
assert (s2 = !start_path);
""
end else begin
assert
((l2 >= l1 + 1) && String.sub s2 0 l1 = !start_path && s2.[l1] = '/');
String.sub s2 (l1 + 1) (l2 - l1 - 1)
end
let startScanning hash fspath path =
if connected () then begin
emitCmd "START %s %s %s\n"
(quote hash)
(quote (Fspath.toString fspath)) (quote (Path.toString path));
readAck ();
scanning := true;
start_path := Path.toString path
end
let scanDirectory path =
if !scanning then begin
emitCmd "DIR %s\n" (quote (relpath path));
readAck ()
end
let followLink path =
if !scanning then begin
emitCmd "LINK %s\n" (quote (relpath path));
readAck ()
end
let stopScanning () =
if !scanning then begin
scanning := false;
emitCmd "DONE\n"
end
let start hash =
if not (Prefs.read useWatcher) then
false
else if not (connected ()) then
exchangeVersions ()
else begin
emitCmd "RESET %s\n" (quote hash);
true
end
let wait hash =
let c = currentConnection () in
let res = Cond.wait c.has_changes in
emitCmd "WAIT %s\n" (quote hash);
res
(****)
let rec parseChanges l =
let (cmd, args) = split_on_space (readLine ()) in
match cmd with
"CHANGES" ->
parseChanges l
| "RECURSIVE" ->
parseChanges (Path.fromString (unquote args) :: l)
| "DONE" ->
List.rev l
| other ->
badResponse other args "RECURSIVE or DONE"
let getChanges hash =
if connected () then begin
emitCmd "CHANGES %s\n" (quote hash);
parseChanges []
end else
raise (Util.Fatal "No file monitoring helper program found")