X-Git-Url: http://www.git.stargrave.org/?p=public-inbox.git;a=blobdiff_plain;f=lib%2FPublicInbox%2FLEI.pm;h=93b4ea03b67d58a9193b99e3970e94e221b2aff9;hp=635cd0c5508aa3050640b3c2b001b5e8faf992e9;hb=7e4bd71efcf3bf61216ec1a7577e33be5f95b8a9;hpb=eb0e4dd09d540eb29c71fe1cc92161a13a0b608a diff --git a/lib/PublicInbox/LEI.pm b/lib/PublicInbox/LEI.pm index 635cd0c5..93b4ea03 100644 --- a/lib/PublicInbox/LEI.pm +++ b/lib/PublicInbox/LEI.pm @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2021 all contributors +# Copyright (C) all contributors # License: AGPL-3.0+ # Backend for `lei' (local email interface). Unlike the C10K-oriented @@ -12,14 +12,14 @@ use parent qw(PublicInbox::DS PublicInbox::LeiExternal PublicInbox::LeiQuery); use Getopt::Long (); use Socket qw(AF_UNIX SOCK_SEQPACKET MSG_EOR pack_sockaddr_un); -use Errno qw(EPIPE EAGAIN EINTR ECONNREFUSED ENOENT ECONNRESET); +use Errno qw(EPIPE EAGAIN ECONNREFUSED ENOENT ECONNRESET); use Cwd qw(getcwd); use POSIX qw(strftime); use IO::Handle (); use Fcntl qw(SEEK_SET); use PublicInbox::Config; -use PublicInbox::Syscall qw(EPOLLIN EPOLLET); -use PublicInbox::DS qw(now dwaitpid); +use PublicInbox::Syscall qw(EPOLLIN); +use PublicInbox::DS qw(dwaitpid); use PublicInbox::Spawn qw(spawn popen_rd); use PublicInbox::Lock; use PublicInbox::Eml; @@ -77,19 +77,16 @@ sub rel2abs { return $p; } my $pwd = $self->{env}->{PWD}; - my $cwd; if (defined $pwd) { - my $xcwd = $self->{3} // - ($cwd = getcwd() // die "getcwd(PWD=$pwd): $!"); if (my @st_pwd = stat($pwd)) { - my @st_cwd = stat($xcwd) or die "stat($xcwd): $!"; + my @st_cwd = stat($self->{3}) or die "stat({3}): $!"; "@st_pwd[1,0]" eq "@st_cwd[1,0]" or $self->{env}->{PWD} = $pwd = undef; } else { # PWD was invalid $self->{env}->{PWD} = $pwd = undef; } } - $pwd //= $self->{env}->{PWD} = $cwd // getcwd() // die "getcwd: $!"; + $pwd //= $self->{env}->{PWD} = getcwd() // die "getcwd: $!"; File::Spec->rel2abs($p, $pwd); } @@ -133,9 +130,10 @@ sub url_folder_cache { sub ale { my ($self) = @_; - $self->{ale} //= do { + $self->{ale} // do { require PublicInbox::LeiALE; - $self->_lei_cfg(1)->{ale} //= PublicInbox::LeiALE->new($self); + my $cfg = $self->_lei_cfg(1); + $self->{ale} = $cfg->{ale} //= PublicInbox::LeiALE->new($self); }; } @@ -148,7 +146,7 @@ sub index_opt { my @c_opt = qw(c=s@ C=s@ quiet|q); my @net_opt = (qw(no-torsocks torsocks=s), PublicInbox::LeiQuery::curl_opt()); -my @lxs_opt = qw(remote! local! external! include|I=s@ exclude=s@ only=s@ +my @lxs_opt = qw(remote! local! external! include|I=s@ exclude=s@ only|O=s@ import-remote!); # we don't support -C as an alias for --find-copies since it's already @@ -181,8 +179,8 @@ our %CMD = ( # sorted in order of importance/use: shared color! mail-sync!), @c_opt, opt_dash('limit|n=i', '[0-9]+') ], 'up' => [ 'OUTPUT...|--all', 'update saved search', - qw(jobs|j=s lock=s@ alert=s@ mua=s verbose|v+ - remote-fudge-time=s all:s), @c_opt ], + qw(jobs|j=s lock=s@ alert=s@ mua=s verbose|v+ exclude=s@ + remote-fudge-time=s all:s remote! local! external!), @net_opt, @c_opt ], 'lcat' => [ '--stdin|MSGID_OR_URL...', 'display local copy of message(s)', 'stdin|', # /|\z/ must be first for lone dash @@ -205,7 +203,7 @@ our %CMD = ( # sorted in order of importance/use: 'mail-diff' => [ '--stdin|LOCATION...', 'diff the contents of emails', 'stdin|', # /|\z/ must be first for lone dash - qw(verbose|v+ color:s no-color raw-header), + qw(verbose|v+ in-format|F=s color:s no-color raw-header), @diff_opt, @net_opt, @c_opt ], 'add-external' => [ 'LOCATION', @@ -218,15 +216,15 @@ our %CMD = ( # sorted in order of importance/use: 'ls-mail-sync' => [ '[FILTER]', 'list mail sync folders', qw(z|0 globoff|g invert-match|v local remote), @c_opt ], 'ls-mail-source' => [ 'URL', 'list IMAP or NNTP mail source folders', - qw(z|0 ascii l pretty url), @c_opt ], + qw(z|0 ascii l pretty url), @net_opt, @c_opt ], 'forget-external' => [ 'LOCATION...|--prune', 'exclude further results from a publicinbox|extindex', qw(prune), @c_opt ], 'ls-search' => [ '[PREFIX]', 'list saved search queries', qw(format|f=s pretty l ascii z|0), @c_opt ], -'forget-search' => [ 'OUTPUT', 'forget a saved search', - qw(verbose|v+), @c_opt ], +'forget-search' => [ 'OUTPUT...|--prune', 'forget a saved search', + qw(verbose|v+ prune:s), @c_opt ], 'edit-search' => [ 'OUTPUT', "edit saved search via `git config --edit'", @c_opt ], 'rm' => [ '--stdin|LOCATION...', @@ -268,7 +266,8 @@ our %CMD = ( # sorted in order of importance/use: 'forget-mail-sync' => [ 'LOCATION...', 'forget sync information for a mail folder', @c_opt ], 'refresh-mail-sync' => [ 'LOCATION...|--all', - 'prune dangling sync data for a mail folder', 'all:s', @c_opt ], + 'prune dangling sync data for a mail folder', 'all:s', + @net_opt, @c_opt ], 'export-kw' => [ 'LOCATION...|--all', 'one-time export of keywords of sync sources', qw(all:s mode=s), @net_opt, @c_opt ], @@ -276,15 +275,15 @@ our %CMD = ( # sorted in order of importance/use: 'one-time conversion from URL or filesystem to another format', qw(stdin| in-format|F=s out-format|f=s output|mfolder|o=s lock=s@ kw!), @net_opt, @c_opt ], -'p2q' => [ 'FILE|COMMIT_OID|--stdin', +'p2q' => [ 'LOCATION_OR_COMMIT...|--stdin', "use a patch to generate a query for `lei q --stdin'", - qw(stdin| want|w=s@ uri debug), @c_opt ], + qw(stdin| in-format|F=s want|w=s@ uri debug), @net_opt, @c_opt ], 'config' => [ '[...]', sub { 'git-config(1) wrapper for '._config_path($_[0]); }, qw(config-file|system|global|file|f=s), # for conflict detection qw(edit|e c=s@ C=s@), pass_through('git config') ], 'inspect' => [ 'ITEMS...|--stdin', 'inspect lei/store and/or local external', - qw(stdin| pretty ascii dir=s), @c_opt ], + qw(stdin| pretty ascii dir|d=s), @c_opt ], 'init' => [ '[DIRNAME]', sub { "initialize storage, default: ".store_path($_[0]); @@ -337,7 +336,8 @@ my %OPTDESC = ( 'path-a|a=s' => 'pre-image pathname associated with OID', 'path-b|b=s' => 'post-image pathname associated with OID', 'git-dir=s@' => 'additional git repository to scan', -'dir=s inspect' => 'specify a inboxdir, extindex topdir or Xapian shard', +'dir|d=s inspect' => + 'specify a inboxdir, extindex topdir or Xapian shard', 'proxy=s' => [ 'PROTO://HOST[:PORT]', # shared with curl(1) "proxy for (e.g. `socks5h://0:9050')" ], 'torsocks=s' => ['VAL|auto|no|yes', @@ -355,6 +355,7 @@ my %OPTDESC = ( 'want|w=s@' => [ 'PREFIX|dfpost|dfn', # common ones in help... 'search prefixes to extract (default: dfpost7)' ], +'uri p2q' => [ 'URI escape output' ], 'alert=s@' => ['CMD,:WINCH,:bell,', 'run command(s) or perform ops when done writing to output ' . @@ -395,7 +396,7 @@ my %OPTDESC = ( 'exclude specified external(s) from search' ], 'include|I=s@ q' => [ 'LOCATION', 'include specified external(s) in search' ], -'only=s@ q' => [ 'LOCATION', +'only|O=s@ q' => [ 'LOCATION', 'only use specified external(s) for search' ], 'jobs=s q' => [ '[SEARCH_JOBS][,WRITER_JOBS]', 'control number of search and writer jobs' ], @@ -410,6 +411,9 @@ my %OPTDESC = ( 'url ls-mail-source' => 'show full URL of newsgroup or IMAP folder', 'format|f=s ls-external' => $ls_format, +'prune:s forget-search' => + ['TYPE|local|remote', 'prune all, remote or local folders' ], + 'limit|n=i@' => ['NUM', 'limit on number of matches (default: 10000)' ], 'offset=i' => ['OFF', 'search result offset (default: 0)'], @@ -454,16 +458,12 @@ my %CONFIG_KEYS = ( 'leistore.dir' => 'top-level storage location', ); -my @WQ_KEYS = qw(lxs l2m ikw pmd wq1 lne); # internal workers +my @WQ_KEYS = qw(lxs l2m ikw pmd wq1 lne v2w); # internal workers sub _drop_wq { my ($self) = @_; for my $wq (grep(defined, delete(@$self{@WQ_KEYS}))) { - if ($wq->wq_kill) { - $wq->wq_close(0, undef, $self); - } elsif ($wq->wq_kill_old) { - $wq->wq_wait_old(undef, $self); - } + $wq->wq_kill('-TERM'); $wq->DESTROY; } } @@ -515,17 +515,11 @@ sub sigpipe_handler { # handles SIGPIPE from @WQ_KEYS workers fail_handler($_[0], 13, delete $_[0]->{1}); } -# PublicInbox::OnDestroy callback for SIGINT to take out the entire pgid -sub sigint_reap { - my ($pgid) = @_; - dwaitpid($pgid) if kill('-INT', $pgid); -} - sub fail ($$;$) { - my ($self, $buf, $exit_code) = @_; + my ($self, $msg, $exit_code) = @_; local $current_lei = $self; $self->{failed}++; - warn($buf, "\n") if defined $buf; + warn(substr($msg, -1, 1) eq "\n" ? $msg : "$msg\n") if defined $msg; $self->{pkt_op_p}->pkt_do('fail_handler') if $self->{pkt_op_p}; x_it($self, ($exit_code // 1) << 8); undef; @@ -546,7 +540,7 @@ sub child_error { # passes non-fatal curl exit codes to user my ($self, $child_error, $msg) = @_; # child_error is $? local $current_lei = $self; $child_error ||= 1 << 8; - warn($msg, "\n") if defined $msg; + warn(substr($msg, -1, 1) eq "\n" ? $msg : "$msg\n") if defined $msg; if ($self->{pkt_op_p}) { # to top lei-daemon $self->{pkt_op_p}->pkt_do('child_error', $child_error); } elsif ($self->{sock}) { # to lei(1) client @@ -567,7 +561,8 @@ sub _lei_atfork_child { my ($self, $persist) = @_; # we need to explicitly close things which are on stack if ($persist) { - chdir '/' or die "chdir(/): $!"; + open $self->{3}, '<', '/' or die "open(/) $!"; + fchdir($self); close($_) for (grep(defined, delete @$self{qw(0 1 2 sock)})); if (my $cfg = $self->{cfg}) { delete @$cfg{qw(-lei_store -watches -lei_note_event)}; @@ -575,8 +570,10 @@ sub _lei_atfork_child { } else { # worker, Net::NNTP (Net::Cmd) uses STDERR directly open STDERR, '+>&='.fileno($self->{2}) or warn "open $!"; STDERR->autoflush(1); + POSIX::setpgid(0, $$) // die "setpgid(0, $$): $!"; } - close($_) for (grep(defined, delete @$self{qw(3 old_1 au_done)})); + close($_) for (grep(defined, delete @$self{qw(old_1 au_done)})); + delete $self->{-socks}; if (my $op_c = delete $self->{pkt_op_c}) { close(delete $op_c->{sock}); } @@ -598,6 +595,7 @@ sub _lei_atfork_child { $cb->(@_) unless PublicInbox::Eml::warn_ignore(@_) }; } + $SIG{TERM} = sub { exit(128 + 15) }; $current_lei = $persist ? undef : $self; # for SIG{__WARN__} } @@ -636,7 +634,10 @@ sub pkt_ops { sub workers_start { my ($lei, $wq, $jobs, $ops, $flds) = @_; - $ops = pkt_ops($lei, { ($ops ? %$ops : ()) }); + $ops //= {}; + ($wq->can('net_merge_all_done') && $lei->{auth}) and + $lei->{auth}->op_merge($ops, $wq, $lei); + pkt_ops($lei, $ops); $ops->{''} //= [ $wq->can('_lei_wq_eof') || \&wq_eof, $lei ]; my $end = $lei->pkt_op_pair; my $ident = $wq->{-wq_ident} // "lei-$lei->{cmd} worker"; @@ -646,18 +647,30 @@ sub workers_start { my $op_c = delete $lei->{pkt_op_c}; @$end = (); $lei->event_step_init; + $wq->wq_wait_async($wq->can('_wq_done_wait') // \&wq_done_wait, $lei); ($op_c, $ops); } # call this when we're ready to wait on events and yield to other clients sub wait_wq_events { my ($lei, $op_c, $ops) = @_; + my $wq1 = $lei->{wq1}; + ($wq1 && $wq1->can('net_merge_all_done') && !$lei->{auth}) and + $wq1->net_merge_all_done; for my $wq (grep(defined, @$lei{qw(ikw pmd)})) { # auxiliary WQs - $wq->wq_close(1); + $wq->wq_close; } + $wq1->{lei_sock} = $lei->{sock} if $wq1; $op_c->{ops} = $ops; } +sub wq1_start { + my ($lei, $wq, $jobs) = @_; + my ($op_c, $ops) = workers_start($lei, $wq, $jobs // 1); + $lei->{wq1} = $wq; + wait_wq_events($lei, $op_c, $ops); # net_merge_all_done if !{auth} +} + sub _help { require PublicInbox::LeiHelp; PublicInbox::LeiHelp::call($_[0], $_[1], \%CMD, \%OPTDESC); @@ -786,7 +799,7 @@ sub lazy_cb ($$$) { sub dispatch { my ($self, $cmd, @argv) = @_; - fchdir($self) or return; + fchdir($self); local %ENV = %{$self->{env}}; local $current_lei = $self; # for __WARN__ $self->{2}->autoflush(1); # keep stdout buffered until x_it|DESTROY @@ -806,7 +819,8 @@ sub dispatch { next if $d eq ''; # same as git(1) chdir $d or return fail($self, "cd $d: $!"); } - open $self->{3}, '.' or return fail($self, "open . $!"); + open $self->{3}, '<', '.' or + return fail($self, "open . $!"); } $cb->($self, @argv); } elsif (grep(/\A-/, $cmd, @argv)) { # --help or -h only @@ -855,9 +869,7 @@ sub _lei_cfg ($;$) { } if (scalar(keys %PATH2CFG) > 5) { # FIXME: use inotify/EVFILT_VNODE to detect unlinked configs - for my $k (keys %PATH2CFG) { - delete($PATH2CFG{$k}) unless -f $k - } + delete(@PATH2CFG{grep(!-f, keys %PATH2CFG)}); } $self->{cfg} = $PATH2CFG{$f} = $cfg; refresh_watches($self); @@ -1117,7 +1129,7 @@ sub accept_dispatch { # Listener {post_accept} callback my %env = map { split(/=/, $_, 2) } splice(@argv, $argc); $self->{env} = \%env; eval { dispatch($self, @argv) }; - send($sock, $@, MSG_EOR) if $@; + $self->fail($@) if $@; } sub dclose { @@ -1134,26 +1146,29 @@ sub event_step { local %ENV = %{$self->{env}}; local $current_lei = $self; eval { - my $buf; - while (my @fds = $recv_cmd->($self->{sock}, $buf, 4096)) { - if (scalar(@fds) == 1 && !defined($fds[0])) { - return if $! == EAGAIN; - next if $! == EINTR; - last if $! == ECONNRESET; - die "recvmsg: $!"; - } - for (@fds) { open my $rfh, '+<&=', $_ } + my @fds = $recv_cmd->($self->{sock} // return, my $buf, 4096); + if (scalar(@fds) == 1 && !defined($fds[0])) { + return if $! == EAGAIN; + die "recvmsg: $!" if $! != ECONNRESET; + $buf = ''; + @fds = (); # for open loop below: } + for (@fds) { open my $rfh, '+<&=', $_ } if ($buf eq '') { _drop_wq($self); # EOF, client disconnected dclose($self); - } elsif ($buf =~ /\A(STOP|CONT)\z/) { + $buf = 'TERM'; + } + if ($buf =~ /\A(?:STOP|CONT|TERM)\z/) { + my $sig = "-$buf"; for my $wq (grep(defined, @$self{@WQ_KEYS})) { - $wq->wq_kill($buf) or $wq->wq_kill_old($buf); + $wq->wq_kill($sig); } } else { die "unrecognized client signal: $buf"; } + my $s = $self->{-socks} // []; # lei up --all + @$s = grep { send($_, $buf, MSG_EOR) } @$s; }; if (my $err = $@) { eval { $self->fail($err) }; @@ -1164,10 +1179,10 @@ sub event_step { sub event_step_init { my ($self) = @_; my $sock = $self->{sock} or return; - $self->{-event_init_done} //= do { # persist til $ops done + $self->{-event_init_done} // do { # persist til $ops done $sock->blocking(0); - $self->SUPER::new($sock, EPOLLIN|EPOLLET); - $sock; + $self->SUPER::new($sock, EPOLLIN); + $self->{-event_init_done} = $sock; }; } @@ -1192,7 +1207,7 @@ sub cfg2lei ($) { open($lei->{0}, '<&', \*STDIN) or die "dup 0: $!"; open($lei->{1}, '>>&', \*STDOUT) or die "dup 1: $!"; open($lei->{2}, '>>&', \*STDERR) or die "dup 2: $!"; - open($lei->{3}, '/') or die "open /: $!"; + open($lei->{3}, '<', '/') or die "open /: $!"; my ($x, $y); socketpair($x, $y, AF_UNIX, SOCK_SEQPACKET, 0) or die "socketpair: $!"; $lei->{sock} = $x; @@ -1206,7 +1221,7 @@ sub dir_idle_handler ($) { # PublicInbox::DirIdle callback my $fn = $ev->fullname; if ($fn =~ m!\A(.+)/(new|cur)/([^/]+)\z!) { # Maildir file my ($mdir, $nc, $bn) = ($1, $2, $3); - $nc = '' if $ev->IN_DELETE; + $nc = '' if $ev->IN_DELETE || $ev->IN_MOVED_FROM; for my $f (keys %{$MDIR2CFGPATH->{$mdir} // {}}) { my $cfg = $PATH2CFG{$f} // next; eval { @@ -1263,6 +1278,9 @@ sub lazy_start { require PublicInbox::CmdIPC4; $send_cmd = PublicInbox::CmdIPC4->can('send_cmd4'); PublicInbox::CmdIPC4->can('recv_cmd4'); + } // do { + $send_cmd = PublicInbox::Syscall->can('send_cmd4'); + PublicInbox::Syscall->can('recv_cmd4'); }; } $recv_cmd or die <<""; @@ -1305,10 +1323,11 @@ sub lazy_start { USR2 => \&noop, }; require PublicInbox::DirIdle; - local $dir_idle = PublicInbox::DirIdle->new([$sock_dir], sub { + local $dir_idle = PublicInbox::DirIdle->new(sub { # just rely on wakeup to hit PostLoopCallback set below dir_idle_handler($_[0]) if $_[0]->fullname ne $path; - }, 1); + }); + $dir_idle->add_watches([$sock_dir]); PublicInbox::DS->SetPostLoopCallback(sub { my ($dmap, undef) = @_; if (@st = defined($path) ? stat($path) : ()) { @@ -1322,11 +1341,10 @@ sub lazy_start { $quit->(); } return 1 if defined($path); - my $now = now(); my $n = 0; for my $s (values %$dmap) { $s->can('busy') or next; - if ($s->busy($now)) { + if ($s->busy) { ++$n; } else { $s->close; @@ -1384,14 +1402,15 @@ sub wq_done_wait { # dwaitpid callback sub fchdir { my ($lei) = @_; my $dh = $lei->{3} // die 'BUG: lei->{3} (CWD) gone'; - chdir($dh) || $lei->fail("fchdir: $!"); + chdir($dh) || die "fchdir: $!"; } sub wq_eof { # EOF callback for main daemon - my ($lei) = @_; + my ($lei, $wq_fld) = @_; local $current_lei = $lei; - my $wq1 = delete $lei->{wq1} // return $lei->fail; # already failed - $wq1->wq_wait_old($wq1->can('_wq_done_wait') // \&wq_done_wait, $lei); + my $wq = delete $lei->{$wq_fld // 'wq1'}; + $lei->sto_done_request($wq); + $wq // $lei->fail; # already failed } sub watch_state_ok ($) { @@ -1490,16 +1509,18 @@ sub git_oid { } sub lms { - my ($lei, $rw) = @_; + my ($lei, $creat) = @_; my $sto = $lei->{sto} // _lei_store($lei) // return; require PublicInbox::LeiMailSync; my $f = "$sto->{priv_eidx}->{topdir}/mail_sync.sqlite3"; - (-f $f || $rw) ? PublicInbox::LeiMailSync->new($f) : undef; + (-f $f || $creat) ? PublicInbox::LeiMailSync->new($f) : undef; } sub sto_done_request { - my ($lei, $sock) = @_; + my ($lei, $wq) = @_; + return unless $lei->{sto}; local $current_lei = $lei; + my $sock = $wq ? $wq->{lei_sock} : undef; eval { if ($sock //= $lei->{sock}) { # issue, async wait $lei->{sto}->wq_io_do('done', [ $sock ]);