]> Sergey Matveev's repositories - public-inbox.git/commitdiff
lei: auto-refresh watches in config, cancel missing
authorEric Wong <e@80x24.org>
Wed, 21 Jul 2021 14:07:06 +0000 (14:07 +0000)
committerEric Wong <e@80x24.org>
Thu, 22 Jul 2021 02:29:01 +0000 (02:29 +0000)
This makes behavior less surprising on restarts as we no longer
lose state on restarts, so there's no need to manually run "lei
add-watch" to re-enable watches.  This also allows us to
transparently handle changes if somebody edits the lei config
file directly or via git-config(1).

lib/PublicInbox/DirIdle.pm
lib/PublicInbox/LEI.pm
lib/PublicInbox/LeiNoteEvent.pm
t/lei-watch.t

index 7031e5fd5615f17aa1df4467c6aebdcf36d7c31a..65896f950f51f756998899bd44a8c44a4442c116 100644 (file)
@@ -56,10 +56,13 @@ sub new {
 sub add_watches {
        my ($self, $dirs, $gone) = @_;
        my $fl = $MAIL_IN | ($gone ? $MAIL_GONE : 0);
+       my @ret;
        for my $d (@$dirs) {
-               $self->{inot}->watch($d, $fl);
+               my $w = $self->{inot}->watch($d, $fl) or next;
+               push @ret, $w;
        }
        PublicInbox::FakeInotify::poll_once($self) if !$ino_cls;
+       @ret
 }
 
 sub rm_watches {
index b92d75125df4c9578c5475083a0c79e348b8d3cb..52c551cf998b7561d3b4a9cec57aa957a9502b3f 100644 (file)
@@ -36,7 +36,7 @@ my $GLP_PASS = Getopt::Long::Parser->new;
 $GLP_PASS->configure(qw(gnu_getopt no_ignore_case auto_abbrev pass_through));
 
 our %PATH2CFG; # persistent for socket daemon
-our $MDIR2CFGPATH; # /path/to/maildir => { /path/to/config => undef }
+our $MDIR2CFGPATH; # /path/to/maildir => { /path/to/config => [ ino watches ] }
 
 # TBD: this is a documentation mechanism to show a subcommand
 # (may) pass options through to another command:
@@ -820,6 +820,8 @@ sub _lei_cfg ($;$) {
                }
        }
        $self->{cfg} = $PATH2CFG{$f} = $cfg;
+       refresh_watches($self);
+       $cfg;
 }
 
 sub _lei_store ($;$) {
@@ -1353,36 +1355,62 @@ sub watch_state_ok ($) {
        $state =~ /\Apause|(?:import|index|tag)-(?:ro|rw)\z/;
 }
 
+sub cancel_maildir_watch ($$) {
+       my ($d, $cfg_f) = @_;
+       my $w = delete $MDIR2CFGPATH->{$d}->{$cfg_f};
+       scalar(keys %{$MDIR2CFGPATH->{$d}}) or
+               delete $MDIR2CFGPATH->{$d};
+       for my $x (@{$w // []}) { $x->cancel }
+}
+
 sub refresh_watches {
        my ($lei) = @_;
        my $cfg = _lei_cfg($lei) or return;
-       $cfg->{-env} //= { %{$lei->{env}}, PWD => '/' }; # for cfg2lei
+       my $old = $cfg->{-watches};
        my $watches = $cfg->{-watches} //= {};
-       require PublicInbox::LeiWatch;
+       my %seen;
+       my $cfg_f = $cfg->{'-f'};
        for my $w (grep(/\Awatch\..+\.state\z/, keys %$cfg)) {
                my $url = substr($w, length('watch.'), -length('.state'));
-               my $lw = $watches->{$w} //= PublicInbox::LeiWatch->new($url);
+               require PublicInbox::LeiWatch;
+               my $lw = $watches->{$url} //= PublicInbox::LeiWatch->new($url);
+               $seen{$url} = undef;
                my $state = $cfg->get_1("watch.$url", 'state');
                if (!watch_state_ok($state)) {
                        $lei->err("watch.$url.state=$state not supported");
                        next;
                }
-               my $f = $cfg->{'-f'};
                if ($url =~ /\Amaildir:(.+)/i) {
                        my $d = File::Spec->canonpath($1);
                        if ($state eq 'pause') {
-                               delete $MDIR2CFGPATH->{$d}->{$f};
-                               scalar(keys %{$MDIR2CFGPATH->{$d}}) or
-                                       delete $MDIR2CFGPATH->{$d};
-                       } elsif (!exists($MDIR2CFGPATH->{$d}->{$f})) {
-                               $dir_idle->add_watches(["$d/cur", "$d/new"], 1);
-                               $MDIR2CFGPATH->{$d}->{$f} = undef;
+                               cancel_maildir_watch($d, $cfg_f);
+                       } elsif (!exists($MDIR2CFGPATH->{$d}->{$cfg_f})) {
+                               my @w = $dir_idle->add_watches(
+                                               ["$d/cur", "$d/new"], 1);
+                               push @{$MDIR2CFGPATH->{$d}->{$cfg_f}}, @w if @w;
                        }
                } else { # TODO: imap/nntp/jmap
                        $lei->child_error(1,
                                "E: watch $url not supported, yet");
                }
        }
+       if ($old) { # cull old non-existent entries
+               for my $url (keys %$old) {
+                       next if exists $seen{$url};
+                       delete $old->{$url};
+                       if ($url =~ /\Amaildir:(.+)/i) {
+                               my $d = File::Spec->canonpath($1);
+                               cancel_maildir_watch($d, $cfg_f);
+                       } else { # TODO: imap/nntp/jmap
+                               $lei->child_error(1, "E: watch $url TODO");
+                       }
+               }
+       }
+       if (scalar keys %$watches) {
+               $cfg->{-env} //= { %{$lei->{env}}, PWD => '/' }; # for cfg2lei
+       } else {
+               delete $cfg->{-watches};
+       }
 }
 
 1;
index bf15cd26feeb039528157892bb4571255aa5e9ee..d6511cf6cd1bb750d9bfe33d874c243510dae68b 100644 (file)
@@ -14,7 +14,7 @@ sub flush_lei ($) {
        my ($lei) = @_;
        if (my $lne = delete $lei->{cfg}->{-lei_note_event}) {
                $lne->wq_close(1, undef, $lei); # runs _lei_wq_eof;
-       } else { # lms_clear_src calls only:
+       } elsif ($lei->{sto}) { # lms_clear_src calls only:
                my $wait = $lei->{sto}->ipc_do('done');
        }
 }
index 3a2f9e64516fda179f82274e904ece03b8ff045e..492f6c1d7790e8e58d6ce6bd40905e609dbfb852 100644 (file)
@@ -13,9 +13,27 @@ $have_fast_inotify or
 my ($ro_home, $cfg_path) = setup_public_inboxes;
 test_lei(sub {
        my $md = "$ENV{HOME}/md";
+       my $cfg_f = "$ENV{HOME}/.config/lei/config";
        my $md2 = $md.'2';
        lei_ok 'ls-watch';
        is($lei_out, '', 'nothing in ls-watch, yet');
+
+       my ($ino_fdinfo, $ino_contents);
+       SKIP: {
+               $have_fast_inotify && $^O eq 'linux' or
+                       skip 'Linux/inotify-only internals check', 1;
+               lei_ok 'daemon-pid'; chomp(my $pid = $lei_out);
+               skip 'missing /proc/$PID/fd', 1 if !-d "/proc/$pid/fd";
+               my @ino = grep {
+                       readlink($_) =~ /\binotify\b/
+               } glob("/proc/$pid/fd/*");
+               is(scalar(@ino), 1, 'only one inotify FD');
+               my $ino_fd = (split('/', $ino[0]))[-1];
+               $ino_fdinfo = "/proc/$pid/fdinfo/$ino_fd";
+               open my $fh, '<', $ino_fdinfo or xbail "open $ino_fdinfo: $!";
+               $ino_contents = [ <$fh> ];
+       }
+
        if (0) { # TODO
                my $url = 'imaps://example.com/foo.bar.0';
                lei_ok([qw(add-watch --state=pause), $url], undef, {});
@@ -44,6 +62,27 @@ test_lei(sub {
        my $e2 = eml_load($f2[0]);
        my $e1 = eml_load("$f[0]S");
        is_deeply($e2, $e1, 'results match');
+
+       SKIP: {
+               $ino_fdinfo or skip 'Linux/inotify-only watch check', 1;
+               open my $fh, '<', $ino_fdinfo or xbail "open $ino_fdinfo: $!";
+               my $cmp = [ <$fh> ];
+               ok(scalar(@$cmp) > scalar(@$ino_contents),
+                       'inotify has Maildir watches');
+       }
+
+       is(xsys(qw(git config -f), $cfg_f,
+                       '--remove-section', "watch.maildir:$md"),
+               0, 'unset config state');
+       lei_ok 'ls-watch', \'refresh watches';
+       is($lei_out, '', 'no watches left');
+
+       SKIP: {
+               $ino_fdinfo or skip 'Linux/inotify-only removal removal', 1;
+               open my $fh, '<', $ino_fdinfo or xbail "open $ino_fdinfo: $!";
+               my $cmp = [ <$fh> ];
+               is_deeply($cmp, $ino_contents, 'inotify Maildir watches gone');
+       };
 });
 
 done_testing;