]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/KQNotify.pm
kqnotify|fake_inotify: detect Maildir write ops
[public-inbox.git] / lib / PublicInbox / KQNotify.pm
1 # Copyright (C) 2020 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # implements the small subset of Linux::Inotify2 functionality we use
5 # using IO::KQueue on *BSD systems.
6 package PublicInbox::KQNotify;
7 use strict;
8 use IO::KQueue;
9 use PublicInbox::DSKQXS; # wraps IO::KQueue for fork-safe DESTROY
10 use PublicInbox::FakeInotify;
11 use Time::HiRes qw(stat);
12
13 # NOTE_EXTEND detects rename(2), NOTE_WRITE detects link(2)
14 sub MOVED_TO_OR_CREATE () { NOTE_EXTEND|NOTE_WRITE }
15
16 sub new {
17         my ($class) = @_;
18         bless { dskq => PublicInbox::DSKQXS->new, watch => {} }, $class;
19 }
20
21 sub watch {
22         my ($self, $path, $mask, $cb) = @_;
23         my ($fh, $cls, @extra);
24         if (-d $path) {
25                 opendir($fh, $path) or return;
26                 my @st = stat($fh);
27                 @extra = ($path, $st[10]); # 10: ctime
28                 $cls = 'PublicInbox::KQNotify::Watchdir';
29         } else {
30                 open($fh, '<', $path) or return;
31                 $cls = 'PublicInbox::KQNotify::Watch';
32         }
33         my $ident = fileno($fh);
34         $self->{dskq}->{kq}->EV_SET($ident, # ident
35                 EVFILT_VNODE, # filter
36                 EV_ADD | EV_CLEAR, # flags
37                 $mask, # fflags
38                 0, 0); # data, udata
39         if ($mask == NOTE_WRITE || $mask == MOVED_TO_OR_CREATE) {
40                 $self->{watch}->{$ident} = [ $fh, $cb, @extra ];
41         } else {
42                 die "TODO Not implemented: $mask";
43         }
44         bless \$fh, $cls;
45 }
46
47 # emulate Linux::Inotify::fileno
48 sub fileno { ${$_[0]->{dskq}->{kq}} }
49
50 # noop for Linux::Inotify2 compatibility.  Unlike inotify,
51 # kqueue doesn't seem to overflow since it's limited by the number of
52 # open FDs the process has
53 sub on_overflow {}
54
55 # noop for Linux::Inotify2 compatibility, we use `0' timeout for ->kevent
56 sub blocking {}
57
58 # behave like Linux::Inotify2::poll
59 sub poll {
60         my ($self) = @_;
61         my @kevents = $self->{dskq}->{kq}->kevent(0);
62         for my $kev (@kevents) {
63                 my $ident = $kev->[KQ_IDENT];
64                 my $mask = $kev->[KQ_FFLAGS];
65                 my ($dh, $cb, $path, $old_ctime) = @{$self->{watch}->{$ident}};
66                 if (!defined($path) && ($mask & NOTE_WRITE) == NOTE_WRITE) {
67                         eval { $cb->() };
68                 } elsif ($mask & MOVED_TO_OR_CREATE) {
69                         my @new_st = stat($path) or next;
70                         $self->{watch}->{$ident}->[3] = $new_st[10]; # ctime
71                         rewinddir($dh);
72                         PublicInbox::FakeInotify::on_new_files($dh, $cb,
73                                                         $path, $old_ctime);
74                 }
75         }
76 }
77
78 package PublicInbox::KQNotify::Watch;
79 use strict;
80
81 sub cancel { close ${$_[0]} or die "close: $!" }
82
83 package PublicInbox::KQNotify::Watchdir;
84 use strict;
85
86 sub cancel { closedir ${$_[0]} or die "closedir: $!" }
87
88 1;