-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathwatcher.hpp
2286 lines (2046 loc) · 77.9 KB
/
watcher.hpp
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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#ifndef W973564ED9F278A21F3E12037288412FBAF175F889
#define W973564ED9F278A21F3E12037288412FBAF175F889
#include <array>
#include <charconv>
#include <chrono>
#include <filesystem>
#include <functional>
#include <ios>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
namespace wtr {
inline namespace watcher {
/* The `event` object is used to carry information about
filesystem events to the user through the (user-supplied)
callback given to `watch`.
The `event` object will contain the:
- `path_name`: The path to the event.
- `path_type`: One of:
- dir
- file
- hard_link
- sym_link
- watcher
- other
- `effect_type`: One of:
- rename
- modify
- create
- destroy
- owner
- other
- `effect_time`:
The time of the event in nanoseconds since epoch.
The `watcher` type is special.
Events with this type will include messages from
the watcher. You may recieve error messages or
important status updates.
The first event always has a `create` value for the
`effect_type`, a `watcher` value for the `path_type`,
and a status message in the `path_name` field; Either
"s/self/live@{some path}" or "e/self/live@{some path}".
Similarly, the last event always carries `destroy` and
`watcher` in the `effect_type` and `path_type` fields.
The `path_name` field will have the same message as the
first event, except for "die" instead of "live". */
struct event {
private:
/* I like these names. Very human. */
using Nanos = std::chrono::nanoseconds;
using Clock = std::chrono::system_clock;
using TimePoint = std::chrono::time_point<Clock>;
public:
/* Ensure the user's callback can receive
events and will return nothing. */
using callback = std::function<void(event const&)>;
/* Represents "what happened" to a path. */
enum class effect_type {
rename,
modify,
create,
destroy,
owner,
other,
};
/* The essential types of paths. */
enum class path_type {
dir,
file,
hard_link,
sym_link,
watcher,
other,
};
std::filesystem::path const path_name{};
enum effect_type const effect_type {};
enum path_type const path_type {};
long long const effect_time{std::chrono::duration_cast<Nanos>(
TimePoint{Clock::now()}.time_since_epoch())
.count()};
std::unique_ptr<event> const associated{nullptr};
inline event(event const& from) noexcept
: path_name{from.path_name}
, effect_type{from.effect_type}
, path_type{from.path_type}
, effect_time{from.effect_time}
, associated{
from.associated ? std::make_unique<event>(*from.associated)
: nullptr} {};
inline event(
std::filesystem::path const& path_name,
enum effect_type effect_type,
enum path_type path_type) noexcept
: path_name{path_name}
, effect_type{effect_type}
, path_type{path_type} {};
inline event(event const& base, event&& associated) noexcept
: path_name{base.path_name}
, effect_type{base.effect_type}
, path_type{base.path_type}
, associated{std::make_unique<event>(std::forward<event>(associated))} {};
inline ~event() noexcept = default;
/* An equality comparison for all the fields in this object.
Includes the `effect_time`, which might not be wanted,
because the `effect_time` is typically (not always) unique. */
inline friend auto operator==(event const& l, event const& r) noexcept -> bool
{
return l.path_name == r.path_name && l.effect_time == r.effect_time
&& l.path_type == r.path_type && l.effect_type == r.effect_type
&& (l.associated && r.associated ? *l.associated == *r.associated
: ! l.associated && ! r.associated);
}
inline friend auto operator!=(event const& l, event const& r) noexcept -> bool
{
return ! (l == r);
}
};
} /* namespace watcher */
// clang-format off
namespace {
#define wtr_effect_type_to_str_lit(Char, from, Lit) \
switch (from) { \
case ::wtr::event::effect_type::rename : return Lit"rename"; \
case ::wtr::event::effect_type::modify : return Lit"modify"; \
case ::wtr::event::effect_type::create : return Lit"create"; \
case ::wtr::event::effect_type::destroy : return Lit"destroy"; \
case ::wtr::event::effect_type::owner : return Lit"owner"; \
case ::wtr::event::effect_type::other : return Lit"other"; \
default : return Lit"other"; \
}
#define wtr_path_type_to_str_lit(Char, from, Lit) \
switch (from) { \
case ::wtr::event::path_type::dir : return Lit"dir"; \
case ::wtr::event::path_type::file : return Lit"file"; \
case ::wtr::event::path_type::hard_link : return Lit"hard_link"; \
case ::wtr::event::path_type::sym_link : return Lit"sym_link"; \
case ::wtr::event::path_type::watcher : return Lit"watcher"; \
case ::wtr::event::path_type::other : return Lit"other"; \
default : return Lit"other"; \
}
#define wtr_event_to_str_cls_as_json(Char, from, Lit) \
using Cls = std::basic_string<Char>; \
auto&& etm = Lit"\"" + to<Cls>(from.effect_time) + Lit"\""; \
auto&& ety = Lit"\"" + to<Cls>(from.effect_type) + Lit"\""; \
auto&& pnm = Lit"\"" + to<Cls>(from.path_name) + Lit"\""; \
auto&& pty = Lit"\"" + to<Cls>(from.path_type) + Lit"\""; \
return { etm + Lit":{" \
+ Lit"\"effect_type\":" + ety + Lit"," \
+ Lit"\"path_name\":" + pnm + Lit"," \
+ Lit"\"path_type\":" + pty \
+ [&]() -> Cls { \
if (! from.associated) return Cls{}; \
auto asc = from.associated.get(); \
auto ttl = Cls{Lit",\"associated\""}; \
auto ety = Lit"\"" + to<Cls>(asc->effect_type) + Lit"\""; \
auto pnm = Lit"\"" + to<Cls>(asc->path_name) + Lit"\""; \
auto pty = Lit"\"" + to<Cls>(asc->path_type) + Lit"\""; \
return { ttl \
+ Lit":{" \
+ Lit"\"effect_type\":" + ety + Lit"," \
+ Lit"\"path_name\":" + pnm + Lit"," \
+ Lit"\"path_type\":" + pty + Lit"}" \
}; \
}() \
+ Lit"}" \
};
/* For types larger than char and/or char8_t, we can just cast
each element in our `char` buffer to an `unsigned char`, and
then zero-extend the elements to any of `wchar_t`, `char16_t`
and/or `char32_t`.
We can use something like `format_to(chararray, "{}", from)`
when library support is more common.
If we support C++20 later on, we should parameterize `from`
as `std::integral auto from` and add `char8_t` to the list
of allowed narrow character types. */
template<class Char>
inline auto num_to_str(long long from) noexcept -> std::basic_string<Char> {
static_assert(std::is_integral_v<decltype(from)>);
static constexpr bool is_sys_sane_narrow_char_size =
(sizeof(char) == sizeof(signed char))
&& (sizeof(char) == sizeof(unsigned char))
;
static constexpr bool is_narrow_char = (
is_sys_sane_narrow_char_size
&& ( std::is_same_v<Char, char>
|| std::is_same_v<Char, signed char>
|| std::is_same_v<Char, unsigned char>
)
);
static constexpr bool is_wide_char = (
std::is_same_v<Char, wchar_t>
|| std::is_same_v<Char, char16_t>
|| std::is_same_v<Char, char32_t>
);
static_assert(is_narrow_char || is_wide_char);
static constexpr auto buflen = std::numeric_limits<decltype(from)>::digits10 + 1;
auto buf = std::array<char, buflen>{0};
auto [_, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), from);
if (ec != std::errc{}) {
return {};
}
else if constexpr (is_narrow_char) {
return {reinterpret_cast<Char const*>(buf.data()), buflen};
}
else if constexpr (is_wide_char) {
auto xbuf = std::array<Char, buflen>{0};
for (std::size_t i = 0; i < buflen; ++i)
xbuf[i] = static_cast<Char>(static_cast<unsigned char>(buf[i]));
return {xbuf.data(), buflen};
}
};
} /* namespace */
/* We use function templates, as opposed to class templates with an
overloaded () operator, so that we can avoid the extra () or {}
when (needlessly) "constructing" the class and to make extending
the templates once or more for the same type (a-la parameterize the
return type *and* the arguments) possible.
The `template<template<class...> class... Ts> auto to(auto) noexcept`
is just for the user. It allows specialization outside of this file,
and has no effect within this file (it does not guide deduction here).
Some downsides:
`std::hash` takes the class template approach. That's a (well?) known
api. It's not ideal that we miss out on a user's existing familiarity
with that pattern.
We need to "declare" a function template before "defining" it:
`template<class T> auto to(OurTypeOrConcept) -> T` must come before
the `template<> auto to(...) -> T { ... }`. I'm not sure why class
templates can get away without declaring a template like that, or
why we need to do that at all.
Another downside to the function templates is that we need to "declare"
function templates before "defining" them. That's not exactly intuitive
to me, I'm fuzzy on why that is, and users probably shouldn't need to
remember that whenever they specialize this template.
We need to declare a template before defining it:
`template<class T> auto to(OurTypeOrConcept) -> T` must come before
the `template<> auto to(...) -> T { ... }`. I'm not sure why class
templates can get away without declaring a template like that, or
why we need to do that at all.
Beware of linker errors when using a concept as an argument type.
AFAICT This is something that can't be done, or I haven't figured out
how just yet.
The upsides to function templates are:
- We get to a concise api, `to<Type>(from_thing)`
- The user can further specialize the function templates, i.e. We can
have a specialization `to<string>(type_in_file_a)` and, somewhere else,
`to<string>(type_in_file_b)` as well. Class templates cannot do that. */
template<class T, class... Ts> auto to(T) noexcept -> decltype(auto);
template<class T> auto to (long long from) noexcept -> T ;
template<class T> auto to (decltype(::wtr::event::effect_time) from) noexcept -> T ;
template<class T> auto to (enum ::wtr::event::effect_type from) noexcept -> T ;
template<class T> auto to (decltype(::wtr::event::path_name) const& from) noexcept -> T ;
template<class T> auto to (enum ::wtr::event::path_type from) noexcept -> T ;
template<class T> auto to ( ::wtr::event const& from) noexcept -> T ;
template<> inline constexpr auto to<std::basic_string_view<char>> (enum ::wtr::event::effect_type from) noexcept -> std::basic_string_view<char> { wtr_effect_type_to_str_lit( char, from, "" ); }
template<> inline constexpr auto to<std::basic_string_view<char>> (enum ::wtr::event::path_type from) noexcept -> std::basic_string_view<char> { wtr_path_type_to_str_lit( char, from, "" ); }
template<> inline auto to<std::basic_string <char>> (decltype(::wtr::event::path_name) const& from) noexcept -> std::basic_string<char> { return { from.string() }; }
template<> inline auto to<std::basic_string <char>> (enum ::wtr::event::path_type from) noexcept -> std::basic_string<char> { wtr_path_type_to_str_lit( char, from, "" ); }
template<> inline auto to<std::basic_string <char>> (decltype(::wtr::event::effect_time) from) noexcept -> std::basic_string<char> { return num_to_str <char>( from ); }
template<> inline auto to<std::basic_string <char>> (enum ::wtr::event::effect_type from) noexcept -> std::basic_string<char> { wtr_effect_type_to_str_lit( char, from, "" ); }
template<> inline auto to<std::basic_string <char>> ( ::wtr::event const& from) noexcept -> std::basic_string<char> { wtr_event_to_str_cls_as_json(char, from, "" ); }
template<> inline constexpr auto to<std::basic_string_view<wchar_t>>(enum ::wtr::event::effect_type from) noexcept -> std::basic_string_view<wchar_t> { wtr_effect_type_to_str_lit( wchar_t, from, L"" ); }
template<> inline constexpr auto to<std::basic_string_view<wchar_t>>(enum ::wtr::event::path_type from) noexcept -> std::basic_string_view<wchar_t> { wtr_path_type_to_str_lit( wchar_t, from, L"" ); }
template<> inline auto to<std::basic_string <wchar_t>>(enum ::wtr::event::path_type from) noexcept -> std::basic_string<wchar_t> { wtr_path_type_to_str_lit( wchar_t, from, L"" ); }
template<> inline auto to<std::basic_string <wchar_t>>(decltype(::wtr::event::path_name) const& from) noexcept -> std::basic_string<wchar_t> { return { from.wstring() }; }
template<> inline auto to<std::basic_string <wchar_t>>(decltype(::wtr::event::effect_time) from) noexcept -> std::basic_string<wchar_t> { return num_to_str <wchar_t>(from ); }
template<> inline auto to<std::basic_string <wchar_t>>(enum ::wtr::event::effect_type from) noexcept -> std::basic_string<wchar_t> { wtr_effect_type_to_str_lit( wchar_t, from, L"" ); }
template<> inline auto to<std::basic_string <wchar_t>>( ::wtr::event const& from) noexcept -> std::basic_string<wchar_t> { wtr_event_to_str_cls_as_json(wchar_t, from, L"" ); }
/*
template<> inline constexpr auto to<std::basic_string_view<char8_t>>(enum ::wtr::event::effect_type from) noexcept -> std::basic_string_view<char8_t> { wtr_effect_type_to_str_lit( char8_t, from, u8"" ); }
template<> inline constexpr auto to<std::basic_string_view<char8_t>>(enum ::wtr::event::path_type from) noexcept -> std::basic_string_view<char8_t> { wtr_path_type_to_str_lit( char8_t, from, u8"" ); }
template<> inline auto to<std::basic_string <char8_t>>(enum ::wtr::event::path_type from) noexcept -> std::basic_string<char8_t> { wtr_path_type_to_str_lit( char8_t, from, u8"" ); }
template<> inline auto to<std::basic_string <char8_t>>(decltype(::wtr::event::path_name) const& from) noexcept -> std::basic_string<char8_t> { return { from.u8string() }; }
template<> inline auto to<std::basic_string <char8_t>>(decltype(::wtr::event::effect_time) from) noexcept -> std::basic_string<char8_t> { return num_to_str <char8_t>(from ); }
template<> inline auto to<std::basic_string <char8_t>>(enum ::wtr::event::effect_type from) noexcept -> std::basic_string<char8_t> { wtr_effect_type_to_str_lit( char8_t, from, u8"" ); }
template<> inline auto to<std::basic_string <char8_t>>( ::wtr::event const& from) noexcept -> std::basic_string<char8_t> { wtr_event_to_str_cls_as_json(char8_t, from, u8"" ); }
*/
#undef wtr_effect_type_to_str_lit
#undef wtr_path_type_to_str_lit
#undef wtr_event_to_str_cls_as_json
template<class Char, class Traits>
inline auto operator<<(
std::basic_ostream<Char, Traits>& into,
enum ::wtr::event::effect_type from) noexcept
-> std::basic_ostream<Char, Traits>&
{ return into << to<std::basic_string<Char, Traits>>(from); };
template<class Char, class Traits>
inline auto operator<<(
std::basic_ostream<Char, Traits>& into,
enum ::wtr::event::path_type from) noexcept
-> std::basic_ostream<Char, Traits>&
{ return into << to<std::basic_string<Char, Traits>>(from); };
/* Streams out `path_name`, `effect_type` and `path_type`.
Formats the stream as a json object.
Looks like this (without line breaks)
"1678046920675963000":{
"effect_type":"create",
"path_name":"/some_file.txt",
"path_type":"file"
} */
template<class Char, class Traits>
inline auto operator<<(
std::basic_ostream<Char, Traits>& into,
::wtr::event const& from) noexcept
-> std::basic_ostream<Char, Traits>&
{ return into << to<std::basic_string<Char, Traits>>(from); };
// clang-format on
} /* namespace wtr */
#include <atomic>
#ifdef __linux__
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <sys/eventfd.h>
#include <unistd.h>
#elif defined(__APPLE__)
#include <dispatch/dispatch.h>
#endif
namespace detail::wtr::watcher {
/* A semaphore-like construct which can be
used with the "native" async I/O APIs
on (currently) Linux and Darwin.
On Darwin, this is a semaphore which is
schedulable with the dispatch library.
On Linux, this is an eventfd in semaphore
mode. The file descriptor is exposed for
use with poll and friends.
On other platforms, this is an atomic flag
which can be checked in a sleep, wake loop,
ideally with a generous sleep time.
*/
class semabin {
public:
enum state { pending, released, error };
private:
mutable std::atomic<state> is = pending;
public:
#if defined(__linux__)
int const fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE);
inline auto release() noexcept -> state
{
auto write_ev = [this]()
{
if (eventfd_write(this->fd, 1) == 0)
return released;
else
return error;
};
if (this->is == released)
return released;
else
return this->is = write_ev();
}
inline auto state() const noexcept -> state
{
auto read_ev = [this]()
{
uint64_t _ = 0;
if (eventfd_read(this->fd, &_) == 0)
return released;
else if (errno == EAGAIN)
return pending;
else
return error;
};
if (this->is == pending)
return pending;
else
return this->is = read_ev();
}
inline ~semabin() noexcept { close(this->fd); }
#elif defined(__APPLE__)
mutable dispatch_semaphore_t sem = dispatch_semaphore_create(0);
inline auto release() noexcept -> state
{
auto exchange_when = pending;
auto was_exchanged = this->is.compare_exchange_strong(
exchange_when,
released,
std::memory_order_release,
std::memory_order_acquire);
if (was_exchanged) dispatch_semaphore_signal(this->sem);
return released;
}
inline auto state() const noexcept -> state
{
return this->is.load(std::memory_order_acquire);
}
inline ~semabin() noexcept { this->release(); }
#else
inline auto release() noexcept -> enum state { return this->is = released; }
inline auto state() const noexcept -> enum state { return this->is; }
#endif
};
} /* namespace detail::wtr::watcher */
#if defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#include <dispatch/dispatch.h>
#include <filesystem>
#include <mutex>
#include <string>
#include <unordered_set>
namespace detail {
namespace wtr {
namespace watcher {
namespace adapter {
namespace {
// clang-format off
/* If we want less "sleepy" time after a period of time
without receiving filesystem events, we could OR like:
`fsev_flag_listen | kFSEventStreamCreateFlagNoDefer`.
We're talking about saving a maximum latency of `delay_s`
after some period of inactivity, which is not likely to
be noticeable. I'm not sure what Darwin sets the "period
of inactivity" to, and I'm not sure it matters. */
inline constexpr unsigned fsev_listen_for
= kFSEventStreamCreateFlagFileEvents;
inline constexpr auto fsev_listen_since
= kFSEventStreamEventIdSinceNow;
inline constexpr unsigned fsev_flag_path_file
= kFSEventStreamEventFlagItemIsFile;
inline constexpr unsigned fsev_flag_path_dir
= kFSEventStreamEventFlagItemIsDir;
inline constexpr unsigned fsev_flag_path_sym_link
= kFSEventStreamEventFlagItemIsSymlink;
inline constexpr unsigned fsev_flag_path_hard_link
= kFSEventStreamEventFlagItemIsHardlink
| kFSEventStreamEventFlagItemIsLastHardlink;
inline constexpr unsigned fsev_flag_effect_create
= kFSEventStreamEventFlagItemCreated;
inline constexpr unsigned fsev_flag_effect_remove
= kFSEventStreamEventFlagItemRemoved;
inline constexpr unsigned fsev_flag_effect_modify_any
= kFSEventStreamEventFlagItemModified
| kFSEventStreamEventFlagItemChangeOwner
| kFSEventStreamEventFlagItemXattrMod
| kFSEventStreamEventFlagItemFinderInfoMod
| kFSEventStreamEventFlagItemInodeMetaMod;
inline constexpr unsigned fsev_flag_effect_modify
= fsev_flag_effect_modify_any
& ~kFSEventStreamEventFlagItemInodeMetaMod;
inline constexpr unsigned fsev_flag_effect_rename
= kFSEventStreamEventFlagItemRenamed;
inline constexpr unsigned fsev_flag_effect_any
= fsev_flag_effect_create
| fsev_flag_effect_remove
| fsev_flag_effect_modify_any
| fsev_flag_effect_rename;
// clang-format on
struct ContextData {
using fspath = std::filesystem::path;
/* `fs::path` has no hash function, so we use this. */
using pathset = std::unordered_set<std::string>;
::wtr::watcher::event::callback const& callback{};
pathset* seen_created_paths{nullptr};
fspath* last_rename_path{nullptr};
std::mutex* mtx{nullptr};
};
inline auto event_recv_one(ContextData& ctx, char const* path, unsigned flags)
{
using pty = enum ::wtr::watcher::event::path_type;
using ety = enum ::wtr::watcher::event::effect_type;
/* A single path won't have different "types". */
auto pt = flags & fsev_flag_path_file ? pty::file
: flags & fsev_flag_path_dir ? pty::dir
: flags & fsev_flag_path_sym_link ? pty::sym_link
: flags & fsev_flag_path_hard_link ? pty::hard_link
: pty::other;
/* More than one thing can happen to the same path.
(So these `if`s are mostly not exclusive.)
We want to report odd events (even with an empty path)
but we can bail early if we don't recognize the effect
because everything else we do depends on that. */
if (! (flags & fsev_flag_effect_any)) {
ctx.callback({path, ety::other, pt});
return;
}
if (flags & fsev_flag_effect_create) {
auto at = ctx.seen_created_paths->find(path);
if (at == ctx.seen_created_paths->end()) {
ctx.seen_created_paths->emplace(path);
ctx.callback({path, ety::create, pt});
}
}
if (flags & fsev_flag_effect_remove) {
auto at = ctx.seen_created_paths->find(path);
if (at != ctx.seen_created_paths->end()) {
ctx.seen_created_paths->erase(at);
ctx.callback({path, ety::destroy, pt});
}
}
if (flags & fsev_flag_effect_modify) {
ctx.callback({path, ety::modify, pt});
}
if (flags & fsev_flag_effect_rename) {
/* Assumes that the last "renamed-from" path
is "honestly" correlated to the current
"rename-to" path.
For non-destructive rename events, we
usually receive events in this order:
1. A rename event on the "from-path"
2. A rename event on the "to-path"
As long as that pattern holds, we can
store the first path in a set, look it
up, test it against the current path
for inequality, and check that it no
longer exists -- In which case, we can
say that we were renamed from that path
to the current path.
We want to store the last rename-from
path in a set on the heap because the
rename events might not be batched, and
we don't want to trample on some other
watcher with a static.
This pattern breaks down if there are
intervening rename events.
For thoughts on recognizing destructive
rename events, see this directory's
notes (in the `notes.md` file).
*/
auto lr_path = *ctx.last_rename_path;
auto differs = ! lr_path.empty() && lr_path != path;
auto missing = access(lr_path.c_str(), F_OK) == -1;
if (differs && missing)
ctx.callback({
{lr_path, ety::rename, pt},
{ path, ety::rename, pt}
}),
ctx.last_rename_path->clear();
else
*ctx.last_rename_path = path;
}
}
/* Sometimes events are batched together and re-sent
(despite having already been sent).
Example:
[first batch of events from the os]
file 'a' created
-> create event for 'a' is sent
[some tiny delay, 1 ms or so]
[second batch of events from the os]
file 'a' destroyed
-> create event for 'a' is sent
-> destroy event for 'a' is sent
So, we filter out duplicate events when they're sent
in a batch. We do this by storing and pruning the
set of paths which we've seen created. */
inline auto event_recv(
ConstFSEventStreamRef, /* `ConstFS..` is important */
void* maybe_ctx, /* Arguments passed to us */
unsigned long count, /* Event count */
void* maybe_paths, /* Paths with events */
unsigned const* flags, /* Event flags */
FSEventStreamEventId const* /* A unique stream id */
) -> void
{
/* These checks are unfortunate, but they are also necessary.
Once in a blue moon, some of them are missing. */
auto data_ok = maybe_ctx && maybe_paths && flags;
if (! data_ok) return;
auto paths = static_cast<char const**>(maybe_paths);
auto ctx = *static_cast<ContextData*>(maybe_ctx);
if (! ctx.mtx->try_lock()) return;
for (unsigned long i = 0; i < count; i++)
event_recv_one(ctx, paths[i], flags[i]);
ctx.mtx->unlock();
}
static_assert(event_recv == FSEventStreamCallback{event_recv});
inline auto open_event_stream(std::filesystem::path const& path, void* ctx)
-> FSEventStreamRef
{
static auto queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
auto context = FSEventStreamContext{
.version = 0, /* FSEvents.h: "Only valid value is zero." */
.info = ctx, /* The context; Our "argument pointer". */
.retain = nullptr, /* Not needed; We manage the lifetimes. */
.release = nullptr, /* Same reason as `.retain` */
.copyDescription = nullptr, /* Optional string for debugging. */
};
/* path_cfstring and path_array appear to be managed by the
FSEventStream, are always null when we go to close the
stream, and shouldn't be freed before then. Would seem
to me like we'd need to release them (in re. the Create
rule), but we don't appear to. */
void const* path_cfstring =
CFStringCreateWithCString(nullptr, path.c_str(), kCFStringEncodingUTF8);
/* A predefined structure which is (from CFArray.h) --
"appropriate when the values in a CFArray are CFTypes" */
static auto const cf_arr_ty = kCFTypeArrayCallBacks;
CFArrayRef path_array = CFArrayCreate(
nullptr, /* A custom allocator is optional */
&path_cfstring, /* Data: A ptr-ptr of (in our case) strings */
1, /* We're just storing one path here */
&cf_arr_ty /* The type of the data we're storing */
);
/* Request a filesystem event stream for `path` from the
kernel. The event stream will call `event_recv` with
`context` and some details about each filesystem event
the kernel sees for the paths in `path_array`. */
auto stream = FSEventStreamCreate(
nullptr, /* A custom allocator is optional */
&event_recv, /* A callable to invoke on changes */
&context, /* The callable's arguments (context) */
path_array, /* The path(s) we were asked to watch */
fsev_listen_since, /* The time "since when" we watch */
0.016, /* Seconds between scans *after inactivity* */
fsev_listen_for /* Which event types to send up to us */
);
auto data_ok = stream && queue && path_cfstring && path_array;
if (! data_ok) {
if (stream) FSEventStreamRelease(stream);
if (path_cfstring) CFRelease(path_cfstring);
if (path_array) CFRelease(path_array);
return nullptr;
}
FSEventStreamSetDispatchQueue(stream, queue);
FSEventStreamStart(stream);
return stream;
}
inline auto wait(semabin const& sb)
{
auto s = sb.state();
if (s != semabin::pending) return s;
dispatch_semaphore_wait(sb.sem, DISPATCH_TIME_FOREVER);
return semabin::released;
}
/* Bugs, footguns
1.
When we flood the filesystem with events, Darwin may choose,
for reason I don't fully understand, to tell us about events
after we are long gone. Maybe the FSEvent stream (which should
be f'ing closed) sometimes ignores us having asking it to stop.
Maybe some tasks in the dispatch queue are not being cleared
properly. I'm not sure.
Creating events as quickly as possible while a few short-lived
FSEvents streams are open is usually enough to reproduce this.
The recipe to reproduce seems to be a ton of filesystem events
ongoing while some FSEvent streams are open, then closing the
streams either during or shortly after the events stop happening
to the filesystem, but before they have all been reported.
A minimal-ish reproducer is in /etc/wip-fsevents-issue.
Whatever the reason, sometimes, Darwin seems happy call into
our event handler with resource after we have left the memory
space those resources belong to. I consider that a bug somewhere
in FSEvents, Dispatch or maybe something deeper.
At one point, I thought that purging events for the device
would help. Even that fails under sufficiently high load.
The positive side effect may have effectively been a sleep.
Sometimes I even consider adding a deliberate sleep here.
Because time is not a synchronization primitive, that will
also eventually fail. Though, if it's the best we can do,
despite the kernel, maybe it's worth it. I'm not sure.
Before that, I added a bunch of synchronization primitives
to the context and made the lifetime of the context a bit
transactional. We'd check on atomic flags which in the event
reception loop to be sure we were alive there, after which
we'd set an idle flag which the `watch` function would wait
on before cleaning up. All well and good, but then again,
it's not like any of that memory *even exists* when Darwin
calls into it after we've asked it to stop and left.
There's a minimal-ish reproducer in /etc/wip-fsevents-issue.
"Worse is better."
2.
We want to handle any outstanding events before closing,
so we flush the event stream before stopping it.
`FSEventStreamInvalidate()` only needs to be called
if we scheduled via `FSEventStreamScheduleWithRunLoop()`.
That scheduling function is deprecated (as of macOS 13).
Calling `FSEventStreamInvalidate()` fails an assertion
and produces a warning in the console. However, calling
`FSEventStreamRelease()` without first invalidating via
`FSEventStreamInvalidate()` also fails an assertion and
produces a warning. Releasing seems safer than not, so
we'll do that.
*/
inline auto
close_event_stream(FSEventStreamRef stream, ContextData& ctx) -> bool
{
if (! stream) return false;
auto _ = std::scoped_lock{*ctx.mtx};
FSEventStreamFlushSync(stream);
FSEventStreamStop(stream);
FSEventStreamInvalidate(stream);
FSEventStreamRelease(stream);
stream = nullptr;
return true;
}
} /* namespace */
/* Lifetimes
We will ensure that the queue, context and callback
are alive at least until we close the event stream.
They are shared between us and the system. The system
is self-inconsistent but well-meaning. Sometimes, Apple
will use our resources after we've asked it not to. I
consider that a bug somewhere in FSEvents, Dispatch or
maybe something deeper. There's a minimal-ish reproducer
in the `/etc/wip-fsevents-issue` directory. */
inline auto watch(
std::filesystem::path const& path,
::wtr::watcher::event::callback const& cb,
semabin const& living) -> bool
{
auto seen_created_paths = ContextData::pathset{};
auto last_rename_path = ContextData::fspath{};
auto mtx = std::mutex{};
auto ctx = ContextData{cb, &seen_created_paths, &last_rename_path, &mtx};
auto fsevs = open_event_stream(path, &ctx);
auto state_ok = wait(living) == semabin::released;
auto close_ok = close_event_stream(fsevs, ctx);
return state_ok && close_ok;
}
} /* namespace adapter */
} /* namespace watcher */
} /* namespace wtr */
} /* namespace detail */
#endif
#if (defined(__linux__) || __ANDROID_API__) \
&& ! defined(WATER_WATCHER_USE_WARTHOG)
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <utility>
namespace detail::wtr::watcher::adapter {
/* Must maintain that an enumerated value
less than complete is either pending or
a warning (which means "keep running"),
that complete is a successful, terminal
result (means "stop, ok"), and that a
value greater than complete is an error
which we should send along to the user
after we (attempt to) clean up. */
enum class result : unsigned short {
pending = 0,
w,
w_sys_not_watched,
w_sys_phantom,
w_sys_bad_fd,
w_sys_bad_meta,
w_sys_q_overflow,
complete,
e,
e_sys_api_inotify,
e_sys_api_fanotify,
e_sys_api_epoll,
e_sys_api_read,
e_sys_api_eventfd,
e_sys_ret,
e_sys_lim_kernel_version,
e_self_noent,
e_self_ev_recv,
};
struct ep {
/* Number of events allowed to be
given to do_event_recv. The same
value is usually returned by a
call to `epoll_wait`). Any number
between 1 and INT_MAX should be
fine... But lower is fine for us.
We don't lose events if we 'miss'
them, the events are still waiting
in the next call to `epoll_wait`.
*/
static constexpr auto q_ulim = 64;
/* The delay, in milliseconds, while
`epoll_wait` will 'pause' us for
until we are woken up. We check if
we are still alive through the fd
from our semaphore-like eventfd.
*/
static constexpr auto wake_ms = -1;
int fd = -1;
epoll_event interests[q_ulim]{};
};
inline constexpr auto to_str(result r)
{
// clang-format off
switch (r) {
case result::pending: return "pending@";
case result::w: return "w@";
case result::w_sys_not_watched: return "w/sys/not_watched@";
case result::w_sys_phantom: return "w/sys/phantom@";
case result::w_sys_bad_fd: return "w/sys/bad_fd@";
case result::w_sys_bad_meta: return "w/sys/bad_meta@";
case result::w_sys_q_overflow: return "w/sys/q_overflow@";
case result::complete: return "complete@";
case result::e: return "e@";
case result::e_sys_api_inotify: return "e/sys/api/inotify@";
case result::e_sys_api_fanotify: return "e/sys/api/fanotify@";
case result::e_sys_api_epoll: return "e/sys/api/epoll@";
case result::e_sys_api_read: return "e/sys/api/read@";
case result::e_sys_api_eventfd: return "e/sys/api/eventfd@";
case result::e_sys_ret: return "e/sys/ret@";
case result::e_sys_lim_kernel_version: return "e/sys/lim/kernel_version@";
case result::e_self_noent: return "e/self/noent@";
case result::e_self_ev_recv: return "e/self/ev_recv@";
default: return "e/unknown@";
}
// clang-format on
};
inline auto send_msg = [](result r, auto path, auto const& cb)
{
using et = enum ::wtr::watcher::event::effect_type;
using pt = enum ::wtr::watcher::event::path_type;
auto msg = std::string{to_str(r)};
cb({msg + path, et::other, pt::watcher});
};
inline auto make_ep(int ev_fs_fd, int ev_il_fd) -> ep
{
#if __ANDROID_API__
int fd = epoll_create(1);
#else
int fd = epoll_create1(EPOLL_CLOEXEC);
#endif
auto want_ev_fs = epoll_event{.events = EPOLLIN, .data{.fd = ev_fs_fd}};
auto want_ev_il = epoll_event{.events = EPOLLIN, .data{.fd = ev_il_fd}};
bool ctl_ok = fd >= 0
&& epoll_ctl(fd, EPOLL_CTL_ADD, ev_fs_fd, &want_ev_fs) >= 0
&& epoll_ctl(fd, EPOLL_CTL_ADD, ev_il_fd, &want_ev_il) >= 0;
if (! ctl_ok && fd >= 0) close(fd), fd = -1;
return ep{.fd = fd};
}
inline auto is_dir(char const* const path) -> bool
{
struct stat s;
return stat(path, &s) == 0 && S_ISDIR(s.st_mode);
}
/* $ echo time wtr.watcher / -ms 1
| sudo bash -E
...
real 0m25.094s
user 0m4.091s
sys 0m20.856s
$ sudo find / -type d
| wc -l
...
784418
We could parallelize this, but
it's never going to be instant.
It might be worth it to start
watching before we're done this
hot find-and-mark path, despite
not having a full picture.
*/
template<class Fn>
inline auto walkdir_do(char const* const path, Fn const& f) -> void
{
if (DIR* d = opendir(path)) {
f(path);
while (dirent* de = readdir(d)) {
char next[PATH_MAX];
char real[PATH_MAX];
if (de->d_type != DT_DIR) continue;
if (strcmp(de->d_name, ".") == 0) continue;
if (strcmp(de->d_name, "..") == 0) continue;
if (snprintf(next, PATH_MAX, "%s/%s", path, de->d_name) <= 0) continue;
if (! realpath(next, real)) continue;
walkdir_do(real, f);
}
(void)closedir(d);
}
}
} /* namespace detail::wtr::watcher::adapter */
#endif
#if (defined(__linux__) || __ANDROID_API__) \
&& ! defined(WATER_WATCHER_USE_WARTHOG)
#include <linux/version.h>
#if (KERNEL_VERSION(5, 9, 0) <= LINUX_VERSION_CODE) && ! __ANDROID_API__
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>