X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FLEI.pm;h=b8159cba29229e1dfecd038f6d33d3874b8227bd;hb=751df49e7db8ba770dff28fb701b31c57ca200e2;hp=784e679d564a9eb8d8bab0d8ec2641248fd9747b;hpb=530287cca30c9812d36e58e77d72742e5c1aa5f6;p=public-inbox.git diff --git a/lib/PublicInbox/LEI.pm b/lib/PublicInbox/LEI.pm index 784e679d..b8159cba 100644 --- a/lib/PublicInbox/LEI.pm +++ b/lib/PublicInbox/LEI.pm @@ -24,6 +24,8 @@ use PublicInbox::DS qw(now dwaitpid); use PublicInbox::Spawn qw(spawn popen_rd); use PublicInbox::Lock; use PublicInbox::Eml; +use PublicInbox::Import; +use PublicInbox::ContentHash qw(git_sha); use Time::HiRes qw(stat); # ctime comparisons for config cache use File::Path qw(mkpath); use File::Spec; @@ -38,7 +40,6 @@ $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 => [ ino watches ] } -our %LIVE_SOCK; # "GLOB(0x....)" => $lei->{sock} # TBD: this is a documentation mechanism to show a subcommand # (may) pass options through to another command: @@ -181,7 +182,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+ all:s), @c_opt ], + qw(jobs|j=s lock=s@ alert=s@ mua=s verbose|v+ + remote-fudge-time=s all:s), @c_opt ], 'lcat' => [ '--stdin|MSGID_OR_URL...', 'display local copy of message(s)', 'stdin|', # /|\z/ must be first for lone dash @@ -199,12 +201,12 @@ our %CMD = ( # sorted in order of importance/use: 'rediff' => [ '--stdin|LOCATION...', 'regenerate a diff with different options', 'stdin|', # /|\z/ must be first for lone dash - qw(git-dir=s@ cwd! verbose|v+ color:s no-color), + qw(git-dir=s@ cwd! verbose|v+ color:s no-color drq:1 dequote-only:1), @diff_opt, @lxs_opt, @net_opt, @c_opt ], 'add-external' => [ 'LOCATION', 'add/set priority of a publicinbox|extindex for extra matches', - qw(boost=i mirror=s inbox-version=i verbose|v+), + qw(boost=i mirror=s inbox-version=i epoch=s verbose|v+), @c_opt, index_opt(), @net_opt ], 'ls-external' => [ '[FILTER]', 'list publicinbox|extindex locations', qw(format|f=s z|0 globoff|g invert-match|v local remote), @c_opt ], @@ -212,7 +214,7 @@ 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 url), @c_opt ], + qw(z|0 ascii l pretty url), @c_opt ], 'forget-external' => [ 'LOCATION...|--prune', 'exclude further results from a publicinbox|extindex', qw(prune), @c_opt ], @@ -256,12 +258,12 @@ our %CMD = ( # sorted in order of importance/use: @c_opt ], 'import' => [ 'LOCATION...|--stdin', 'one-time import/update from URL or filesystem', - qw(stdin| offset=i recursive|r exclude=s include|I=s jobs=s new-only + qw(stdin| offset=i recursive|r exclude=s include|I=s new-only lock=s@ in-format|F=s kw! verbose|v+ incremental! mail-sync!), @net_opt, @c_opt ], 'forget-mail-sync' => [ 'LOCATION...', 'forget sync information for a mail folder', @c_opt ], -'prune-mail-sync' => [ 'LOCATION...|--all', +'refresh-mail-sync' => [ 'LOCATION...|--all', 'prune dangling sync data for a mail folder', 'all:s', @c_opt ], 'export-kw' => [ 'LOCATION...|--all', 'one-time export of keywords of sync sources', @@ -276,7 +278,7 @@ our %CMD = ( # sorted in order of importance/use: 'config' => [ '[...]', sub { 'git-config(1) wrapper for '._config_path($_[0]); }, qw(config-file|system|global|file|f=s), # for conflict detection - qw(c=s@ C=s@), pass_through('git config') ], + 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 ], @@ -418,7 +420,11 @@ my %OPTDESC = ( 'remote' => 'limit operations to those requiring network access', 'remote!' => 'prevent operations requiring network access', -'all:s up' => ['local', 'update all (local) saved searches' ], +# up, refresh-mail-sync, export-kw +'all:s' => ['TYPE|local|remote', 'all remote or local folders' ], + +'remote-fudge-time=s' => [ 'INTERVAL', + 'look for mail INTERVAL older than the last successful query' ], 'mid=s' => 'specify the Message-ID of a message', 'oid=s' => 'specify the git object ID of a message', @@ -513,8 +519,7 @@ sub fail ($$;$) { my ($self, $buf, $exit_code) = @_; $self->{failed}++; err($self, $buf) if defined $buf; - # calls fail_handler - $self->{pkt_op_p}->pkt_do('!') if $self->{pkt_op_p}; + $self->{pkt_op_p}->pkt_do('fail_handler') if $self->{pkt_op_p}; x_it($self, ($exit_code // 1) << 8); undef; } @@ -546,7 +551,7 @@ sub child_error { # passes non-fatal curl exit codes to user sub note_sigpipe { # triggers sigpipe_handler my ($self, $fd) = @_; close(delete($self->{$fd})); # explicit close silences Perl warning - $self->{pkt_op_p}->pkt_do('|') if $self->{pkt_op_p}; + $self->{pkt_op_p}->pkt_do('sigpipe_handler') if $self->{pkt_op_p}; x_it($self, 13); } @@ -573,9 +578,9 @@ sub _lei_atfork_child { close $listener if $listener; undef $listener; $dir_idle->force_close if $dir_idle; + undef $dir_idle; %PATH2CFG = (); $MDIR2CFGPATH = {}; - %LIVE_SOCK = (); eval 'no warnings; undef $PublicInbox::LeiNoteEvent::to_flush'; undef $errors_log; $quit = \&CORE::exit; @@ -609,11 +614,11 @@ sub incr { sub pkt_ops { my ($lei, $ops) = @_; - $ops->{'!'} = [ \&fail_handler, $lei ]; - $ops->{'|'} = [ \&sigpipe_handler, $lei ]; - $ops->{x_it} = [ \&x_it, $lei ]; - $ops->{child_error} = [ \&child_error, $lei ]; - $ops->{incr} = [ \&incr, $lei ]; + $ops->{fail_handler} = [ $lei ]; + $ops->{sigpipe_handler} = [ $lei ]; + $ops->{x_it} = [ $lei ]; + $ops->{child_error} = [ $lei ]; + $ops->{incr} = [ $lei ]; $ops; } @@ -769,6 +774,8 @@ sub lazy_cb ($$$) { sub dispatch { my ($self, $cmd, @argv) = @_; + fchdir($self) or return; + local %ENV = %{$self->{env}}; local $current_lei = $self; # for __WARN__ $self->{2}->autoflush(1); # keep stdout buffered until x_it|DESTROY return _help($self, 'no command given') unless defined($cmd); @@ -865,14 +872,6 @@ sub _config { waitpid(spawn($cmd, \%env, \%rdr), 0); } -sub lei_config { - my ($self, @argv) = @_; - $self->{opt}->{'config-file'} and return fail $self, - "config file switches not supported by `lei config'"; - _config(@_); - x_it($self, $?) if $?; -} - sub lei_daemon_pid { puts shift, $$ } sub lei_daemon_kill { @@ -1104,21 +1103,15 @@ sub accept_dispatch { # Listener {post_accept} callback my ($argc, @argv) = split(/\0/, $buf, -1); undef $buf; my %env = map { split(/=/, $_, 2) } splice(@argv, $argc); - if (chdir($self->{3})) { - local %ENV = %env; - $self->{env} = \%env; - eval { dispatch($self, @argv) }; - send($sock, $@, MSG_EOR) if $@; - } else { - send($sock, "fchdir: $!", MSG_EOR); # implicit close - } + $self->{env} = \%env; + eval { dispatch($self, @argv) }; + send($sock, $@, MSG_EOR) if $@; } sub dclose { my ($self) = @_; delete $self->{-progress}; _drop_wq($self) if $self->{failed}; - close(delete $self->{1}) if $self->{1}; # may reap_compress $self->close if $self->{-event_init_done}; # PublicInbox::DS::close } @@ -1126,23 +1119,28 @@ sub dclose { sub event_step { my ($self) = @_; local %ENV = %{$self->{env}}; - my $sock = $self->{sock}; local $current_lei = $self; eval { - while (my @fds = $recv_cmd->($sock, my $buf, 4096)) { + 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 my $fd (@fds) { - open my $rfh, '+<&=', $fd; + for (@fds) { open my $rfh, '+<&=', $_ } + } + if ($buf eq '') { + _drop_wq($self); # EOF, client disconnected + dclose($self); + } elsif ($buf =~ /\A(STOP|CONT)\z/) { + for my $wq (grep(defined, @$self{@WQ_KEYS})) { + $wq->wq_kill($buf) or $wq->wq_kill_old($buf); } + } else { die "unrecognized client signal: $buf"; } - _drop_wq($self); # EOF, client disconnected - dclose($self); }; if (my $err = $@) { eval { $self->fail($err) }; @@ -1154,6 +1152,7 @@ sub event_step_init { my ($self) = @_; my $sock = $self->{sock} or return; $self->{-event_init_done} //= do { # persist til $ops done + $sock->blocking(0); $self->SUPER::new($sock, EPOLLIN|EPOLLET); $sock; }; @@ -1181,7 +1180,6 @@ sub cfg2lei ($) { open($lei->{1}, '>>&', \*STDOUT) or die "dup 1: $!"; open($lei->{2}, '>>&', \*STDERR) or die "dup 2: $!"; open($lei->{3}, '/') or die "open /: $!"; - chdir($lei->{3}) or die "chdir /': $!"; my ($x, $y); socketpair($x, $y, AF_UNIX, SOCK_SEQPACKET, 0) or die "socketpair: $!"; $lei->{sock} = $x; @@ -1199,12 +1197,11 @@ sub dir_idle_handler ($) { # PublicInbox::DirIdle callback for my $f (keys %{$MDIR2CFGPATH->{$mdir} // {}}) { my $cfg = $PATH2CFG{$f} // next; eval { - local %ENV = %{$cfg->{-env}}; my $lei = cfg2lei($cfg); $lei->dispatch('note-event', "maildir:$mdir", $nc, $bn, $fn); }; - warn "E note-event $f: $@\n" if $@; + warn "E: note-event $f: $@\n" if $@; } } if ($ev->can('cancel') && ($ev->IN_IGNORE || $ev->IN_UNMOUNT)) { @@ -1229,6 +1226,7 @@ sub lazy_start { $errors_log = "$sock_dir/errors.log"; my $addr = pack_sockaddr_un($path); my $lk = bless { lock_path => $errors_log }, 'PublicInbox::Lock'; + umask(077) // die("umask(077): $!"); $lk->lock_acquire; socket($listener, AF_UNIX, SOCK_SEQPACKET, 0) or die "socket: $!"; if ($errno == ECONNREFUSED || $errno == ENOENT) { @@ -1240,7 +1238,6 @@ sub lazy_start { $! = $errno; # allow interpolation to stringify in die die "connect($path): $!"; } - umask(077) // die("umask(077): $!"); bind($listener, $addr) or die "bind($path): $!"; $lk->lock_release; undef $lk; @@ -1347,7 +1344,10 @@ sub lazy_start { open STDERR, '>&STDIN' or die "redirect stderr failed: $!"; open STDOUT, '>&STDIN' or die "redirect stdout failed: $!"; # $daemon pipe to `lei' closed, main loop begins: - PublicInbox::DS->EventLoop; + eval { PublicInbox::DS->EventLoop }; + warn "event loop error: $@\n" if $@; + # exit() may trigger waitpid via various DESTROY, ensure interruptible + PublicInbox::DS::sig_setmask($oldset); dump_and_clear_log(); exit($exit_code // 0); } @@ -1388,7 +1388,7 @@ sub fchdir { sub wq_eof { # EOF callback for main daemon my ($lei) = @_; my $wq1 = delete $lei->{wq1} // return $lei->fail; # already failed - $wq1->wq_wait_old(\&wq_done_wait, $lei); + $wq1->wq_wait_old($wq1->can('_wq_done_wait') // \&wq_done_wait, $lei); } sub watch_state_ok ($) { @@ -1414,6 +1414,7 @@ sub add_maildir_watch ($$) { sub refresh_watches { my ($lei) = @_; + $dir_idle or return; my $cfg = _lei_cfg($lei) or return; my $old = $cfg->{-watches}; my $watches = $cfg->{-watches} //= {}; @@ -1442,20 +1443,16 @@ sub refresh_watches { } # add all known Maildir folders as implicit watches - my $sto = $lei->_lei_store; - my $renames = 0; - if (my $lms = $sto ? $sto->search->lms : undef) { + my $lms = $lei->lms; + if ($lms) { + $lms->lms_write_prepare; for my $d ($lms->folders('maildir:')) { substr($d, 0, length('maildir:')) = ''; - my $cd = canonpath_harder($d); - my $f = "maildir:$cd"; # fixup old bugs while we're iterating: - if ($d ne $cd) { - $sto->ipc_do('lms_rename_folder', - "maildir:$d", $f); - ++$renames; - } + my $cd = canonpath_harder($d); + my $f = "maildir:$cd"; + $lms->rename_folder("maildir:$d", $f) if $d ne $cd; next if $watches->{$f}; # may be set to pause require PublicInbox::LeiWatch; $watches->{$f} = PublicInbox::LeiWatch->new($f); @@ -1463,7 +1460,6 @@ sub refresh_watches { add_maildir_watch($cd, $cfg_f); } } - $lei->sto_done_request if $renames; if ($old) { # cull old non-existent entries for my $url (keys %$old) { next if exists $seen{$url}; @@ -1483,33 +1479,39 @@ sub refresh_watches { } } -sub git_blob_id { - my ($lei, $eml) = @_; - ($lei->{sto} // _lei_store($lei, 1))->git_blob_id($eml); +# TODO: support SHA-256 +sub git_oid { + my $eml = $_[-1]; + $eml->header_set($_) for @PublicInbox::Import::UNWANTED_HEADERS; + git_sha(1, $eml); } -sub lms { # read-only LeiMailSync - my ($lei) = @_; - my $lse = $lei->{lse} // do { - my $sto = $lei->{sto} // _lei_store($lei); - $sto ? $sto->search : undef - }; - $lse ? $lse->lms : undef; +sub lms { + my ($lei, $rw) = @_; + 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; } -sub sto_done_request { # only call this from lei-daemon process (not workers) +sub sto_done_request { my ($lei, $sock) = @_; - if ($sock //= $lei->{sock}) { - $LIVE_SOCK{"$sock"} = $sock; - $lei->{sto}->ipc_do('done', "$sock"); # issue, async wait - } else { # forcibly wait - my $wait = $lei->{sto}->ipc_do('done'); - } + eval { + if ($sock //= $lei->{sock}) { # issue, async wait + $lei->{sto}->wq_io_do('done', [ $sock ]); + } else { # forcibly wait + my $wait = $lei->{sto}->wq_do('done'); + } + }; + $lei->err($@) if $@; } -sub sto_done_complete { # called in lei-daemon when LeiStore->done is complete - my ($sock_str) = @_; - delete $LIVE_SOCK{$sock_str}; # frees {sock} for waiting lei clients +sub cfg_dump ($$) { + my ($lei, $f) = @_; + my $ret = eval { PublicInbox::Config->git_config_dump($f, $lei->{2}) }; + return $ret if !$@; + $lei->err($@); + undef; } 1;