Package: release.debian.org Severity: normal User: release.debian.org@packages.debian.org Usertags: unblock X-Debbugs-Cc: inn2@packages.debian.org Control: affects -1 + src:inn2 Please unblock package inn2 This is tagged as a snapshot but is actually 2.7.1 RC1. It contains many documentation fixes, small improvements and fixes to pullnews, and the new ovsqlite-util program which can be used to debug and repair an ovsqlite database. The new package has been used in production for 3 weeks on one of my servers. I am attaching the git diff between debian/2.7.1_20230306-1 and debian/2.7.1_20230322-1, abridged of documentation changes. The full changelog can be consulted at https://salsa.debian.org/md/inn2/-/commits/master . unblock inn2/2.7.1~20230322-1 -- ciao, Marco
diff --git a/.gitignore b/.gitignore index 274716315..9960002af 100644 --- a/.gitignore +++ b/.gitignore @@ -176,6 +176,7 @@ /storage/ovmethods.h /storage/buffindexed/buffindexed_d /storage/ovsqlite/ovsqlite-server +/storage/ovsqlite/ovsqlite-util /storage/ovsqlite/sql-init.c /storage/ovsqlite/sql-init.h /storage/ovsqlite/sql-main.c diff --git a/MANIFEST b/MANIFEST index 35e05aef2..7254d27aa 100644 --- a/MANIFEST +++ b/MANIFEST @@ -210,6 +210,7 @@ doc/man/ovdb_server.8 Manpage for ovdb_server doc/man/ovdb_stat.8 Manpage for ovdb_stat doc/man/overchan.8 Manpage for overchan backend doc/man/ovsqlite-server.8 Manpage for ovsqlite-server +doc/man/ovsqlite-util.8 Manpage for ovsqlite-util doc/man/ovsqlite.5 Manpage for the ovsqlite overview module doc/man/passwd.nntp.5 Manpage for passwd.nntp config file doc/man/perl-nocem.8 Manpage for perl-nocem @@ -331,6 +332,7 @@ doc/pod/ovdb_server.pod Master file for ovdb_server.8 doc/pod/ovdb_stat.pod Master file for ovdb_stat.8 doc/pod/overchan.pod Master file for overchan.8 doc/pod/ovsqlite-server.pod Master file for ovsqlite-server.8 +doc/pod/ovsqlite-util.pod Master file for ovsqlite-util.8 doc/pod/ovsqlite.pod Master file for ovsqlite.5 doc/pod/passwd.nntp.pod Master file for passwd.nntp.5 doc/pod/procbatch.pod Master file for procbatch.8 @@ -774,6 +776,7 @@ storage/ovsqlite/ovmethod.mk Make rules for ovsqlite storage/ovsqlite/ovsqlite-private.c Private code for ovsqlite storage/ovsqlite/ovsqlite-private.h Private header for ovsqlite storage/ovsqlite/ovsqlite-server.c SQLite database exclusive owner +storage/ovsqlite/ovsqlite-util.in Utility program for ovsqlite storage/ovsqlite/ovsqlite.c ovsqlite implementation storage/ovsqlite/ovsqlite.h ovsqlite interface storage/ovsqlite/sql-init.c Generated database setup implementation diff --git a/Makefile.global.in b/Makefile.global.in index 8a185ed39..db42dee2e 100644 --- a/Makefile.global.in +++ b/Makefile.global.in @@ -20,7 +20,7 @@ ## be complying with the NNTP protocol. VERSION = 2.7.1 -VERSION_EXTRA = prerelease +VERSION_EXTRA = rc1 version ## The absolute path to the top of the build directory, used to find the ## libraries built as part of INN. Using relative paths confuses libtool diff --git a/debian/changelog b/debian/changelog index ffbb0e6a6..eff319e64 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +inn2 (2.7.1~20230322-1) unstable; urgency=medium + + * New release candidate 1 of the stable branch. + + -- Marco d'Itri <md@linux.it> Mon, 27 Mar 2023 04:30:21 +0200 + inn2 (2.7.1~20230306-1) unstable; urgency=medium * New upstream snapshot of the stable branch. diff --git a/doc/man/Makefile b/doc/man/Makefile index 906725ebd..30a87587f 100644 --- a/doc/man/Makefile +++ b/doc/man/Makefile @@ -30,9 +30,9 @@ SEC8 = actsync.8 archive.8 batcher.8 buffchan.8 ckpasswd.8 \ innupgrade.8 innwatch.8 innxbatch.8 innxmit.8 mailpost.8 makedbz.8 \ makehistory.8 mod-active.8 news.daily.8 news2mail.8 ninpaths.8 \ nnrpd.8 nntpsend.8 ovdb_init.8 ovdb_monitor.8 ovdb_server.8 \ - ovdb_stat.8 overchan.8 ovsqlite-server.8 perl-nocem.8 procbatch.8 \ - prunehistory.8 radius.8 \ - rc.news.8 scanlogs.8 scanspool.8 send-ihave.8 send-uucp.8 sendinpaths.8 \ + ovdb_stat.8 overchan.8 ovsqlite-server.8 ovsqlite-util.8 perl-nocem.8 \ + procbatch.8 prunehistory.8 radius.8 rc.news.8 \ + scanlogs.8 scanspool.8 send-ihave.8 send-uucp.8 sendinpaths.8 \ tally.control.8 tdx-util.8 tinyleaf.8 writelog.8 all: diff --git a/doc/pod/Makefile b/doc/pod/Makefile index 792ccf568..2fe219533 100644 --- a/doc/pod/Makefile +++ b/doc/pod/Makefile @@ -48,6 +48,7 @@ MAN8 = ../man/actsync.8 ../man/archive.8 ../man/auth_krb5.8 \ ../man/nnrpd.8 ../man/nntpsend.8 \ ../man/ovdb_init.8 ../man/ovdb_monitor.8 ../man/ovdb_server.8 \ ../man/ovdb_stat.8 ../man/overchan.8 ../man/ovsqlite-server.8 \ + ../man/ovsqlite-util.8 \ ../man/procbatch.8 ../man/prunehistory.8 ../man/radius.8 \ ../man/rc.news.8 ../man/scanlogs.8 ../man/scanspool.8 \ ../man/send-ihave.8 ../man/sendinpaths.8 \ @@ -172,6 +173,7 @@ maintclean: distclean ../man/ovdb_stat.8: ovdb_stat.pod ; $(POD2MAN) -s 8 $? > $@ ../man/overchan.8: overchan.pod ; $(POD2MAN) -s 8 $? > $@ ../man/ovsqlite-server.8: ovsqlite-server.pod ; $(POD2MAN) -s 8 $? > $@ +../man/ovsqlite-util.8: ovsqlite-util.pod ; $(POD2MAN) -s 8 $? > $@ ../man/procbatch.8: procbatch.pod ; $(POD2MAN) -s 8 $? > $@ ../man/prunehistory.8: prunehistory.pod ; $(POD2MAN) -s 8 $? > $@ ../man/radius.8: radius.pod ; $(POD2MAN) -s 8 $? > $@ diff --git a/frontends/pullnews.in b/frontends/pullnews.in index 99ebfbf84..b21ce29b4 100644 --- a/frontends/pullnews.in +++ b/frontends/pullnews.in @@ -11,9 +11,10 @@ # Full changelog can be found in the Git commit history of the # INN project. Major changes are: # -# February 2023: +# February-March 2023: # Julien Élie added TLS support for both downstream and upstream -# servers. Also made pullnews robust on socket timeout. +# servers. Also made pullnews robust on socket timeout, and +# added -L (largest article size wanted). # # January 2010: # Geraint A. Edwards added header-only feeding (-B); @@ -131,9 +132,10 @@ END { my $usage = "Usage: $0 [-BhnOqRx] [-a hashfeed] [-b fraction] [-c config] [-C width] [-d level] [-f fraction] [-F fakehop] [-g groups] [-G newsgroups] - [-H headers] [-k checkpt] [-l logfile] [-m header_pats] [-M num] [-N num] - [-p port] [-P hop_limit] [-Q level] [-r file] [-s host[:port][_tlsmode]] - [-S num] [-t retries] [-T seconds] [-w num] [-z num] [-Z num] + [-H headers] [-k checkpt] [-l logfile] [-L size] [-m header_pats] + [-M num] [-N num] [-p port] [-P hop_limit] [-Q level] [-r file] + [-s host[:port][_tlsmode]] [-S num] [-t retries] [-T seconds] + [-w num] [-z num] [-Z num] [upstream_host ...] Options: @@ -187,6 +189,8 @@ Options: -l logfile log progress/stats to logfile (default is stdout). + -L size largest wanted article size in bytes for articles to download. + -m 'Hdr1:regexp1 !Hdr2:regexp2 #Hdr3:regexp3 !#Hdr4:regexp4 ...' feed article only if: the Hdr1 header field body matches regexp1; @@ -258,10 +262,10 @@ sub HELP_MESSAGE { } use vars qw($opt_a $opt_b $opt_B $opt_c $opt_C $opt_d $opt_f $opt_F - $opt_g $opt_G $opt_h $opt_H $opt_k $opt_l $opt_m $opt_M $opt_n + $opt_g $opt_G $opt_h $opt_H $opt_k $opt_l $opt_L $opt_m $opt_M $opt_n $opt_N $opt_O $opt_p $opt_P $opt_q $opt_Q $opt_r $opt_R $opt_s $opt_S $opt_t $opt_T $opt_w $opt_x $opt_z $opt_Z); -getopts("a:b:Bc:C:d:f:F:g:G:hH:k:l:m:M:nN:Op:P:qQ:r:Rs:S:t:T:w:xz:Z:") +getopts("a:b:Bc:C:d:f:F:g:G:hH:k:l:L:m:M:nN:Op:P:qQ:r:Rs:S:t:T:w:xz:Z:") || die $usage; HELP_MESSAGE() if defined $opt_h; @@ -522,7 +526,7 @@ if (not $rnews) { if ($localTLS == 1 && !$localcxn->starttls()) { die " Can't use STARTTLS on $localServer: " . $localcxn->code() . " " - . $localcxn->message() . "\n"; + . join('//', split(/\r?\n/, $localcxn->message())) . "\n"; } if (exists $passwd{$localServer} @@ -530,7 +534,8 @@ if (not $rnews) { { warn sprintf( " failed to authorize: %s %s\n", - $localcxn->code(), $localcxn->message() + $localcxn->code(), + join('//', split(/\r?\n/, $localcxn->message())) ); } } @@ -627,7 +632,8 @@ foreach my $server (@servers) { if ($upstreamTLS == 1 && !$upstream->starttls()) { warn sprintf( "can't use STARTTLS: %s %s\n", - $upstream->code(), $upstream->message() + $upstream->code(), + join('//', split(/\r?\n/, $upstream->message())) ); next; } @@ -635,7 +641,8 @@ foreach my $server (@servers) { if ($username && !$upstream->authinfo($username, $passwd)) { warn sprintf( "failed to authorize: %s %s\n", - $upstream->code(), $upstream->message() + $upstream->code(), + join('//', split(/\r?\n/, $upstream->message())) ); next; } @@ -826,7 +833,8 @@ sub crossFeedGroup { if (!defined($narticles)) { # Group command failed. warn sprintf( "Group command failed for $group: %s %s\n", - $fromServer->code() || 'NO_CODE', $fromServer->message() + $fromServer->code() || 'NO_CODE', + join('//', split(/\r?\n/, $fromServer->message())) ); return 0; } @@ -874,16 +882,46 @@ sub crossFeedGroup { my $i; my @warns; my $skip_article; - for ($i = ($first > $high ? $first : $high + 1); $i <= $last; $i++) { + my $overview; + my $begin = ($first > $high ? $first : $high + 1); + for ($i = $begin; $i <= $last; $i++) { $skip_article = 0; last if defined $maxArts and $count >= $maxArts; last if defined $opt_f and $count >= $toget; $count++; $art_total_count++; sleep $opt_z if defined $opt_z and $count > 1; + + # Do not download articles whose size exceeds the largest wanted size. + # Field 3 contains the Message-ID, field 5 the article size. + if (defined($opt_L)) { + # Retrieve overview data by chunks, so that articles keep being + # downloaded instead of a possible long wait at the start of the + # process of each newsgroup. + if (($count % $progressWidth) == 1) { + # Do not directly use $i + $progressWidth, as the result may + # exceed the maximum article number supported by the server. + my @range + = ($i, + $i + $progressWidth - 1 > $last + ? $last + : $i + $progressWidth - 1); + $overview = $fromServer->xover(\@range); + } + + if (defined($$overview{$i}[5]) and $$overview{$i}[5] > $opt_L) { + print LOG "." unless $quiet; + print LOG "\tDEBUGGING $i\t-- not downloading " + . "article $$overview{$i}[3] " + . "which has $$overview{$i}[5] bytes\n" + if $debug >= 1; + $skip_article = 1; + } + } + # "Optimized mode" -- check if the article is wanted # *before* downloading it. - if (defined $opt_O) { + if (not $skip_article and defined($opt_O)) { # 223 n <a> article retrieved # -- request text separately (after STAT) # 423 no such article number in this group @@ -896,11 +934,12 @@ sub crossFeedGroup { my $new_msgid = $toServer->nntpstat($org_msgid); my $new_code = $toServer->code(); print LOG - "\tDEBUGGING $i\t$org_msgid ($org_code) => $new_code\n" + "\n\tDEBUGGING $i\t$org_msgid ($org_code) => $new_code\n" if $debug >= 3; # Skip the article if it already exists # on the downstream server. if ($new_code == 223) { + print LOG "." unless $quiet; print LOG "\tDEBUGGING $i\t-- not downloading " . "already existing message $org_msgid code=$new_code\n" if $debug >= 1; @@ -928,6 +967,7 @@ sub crossFeedGroup { push @{$article}, "\n" if not $is_control_art; } } + if (not $skip_article and (not $header_only or $is_control_art or $add_bytes_header)) { @@ -1071,13 +1111,13 @@ sub crossFeedGroup { my $cut = join("\n\t", splice(@{$article}, $idx, 1)); $tx_len -= length($cut); $idx_blank_pre_body--; - print LOG "\tDEBUGGING $i\tcut1 $cut" if $debug >= 2; + print LOG "\tDEBUGGING $i\tcut1 $cut\n" if $debug >= 2; while ($article->[$idx] =~ /^[[:space:]](.+)/) { # Folded lines. my $cut = join("\n\t", splice(@{$article}, $idx, 1)); $tx_len -= length($cut); $idx_blank_pre_body--; - print LOG "\tDEBUGGING $i\tcut_ $cut" if $debug >= 2; + print LOG "\tDEBUGGING $i\tcut_ $cut\n" if $debug >= 2; } } @@ -1139,6 +1179,7 @@ sub crossFeedGroup { $pulled->{$server}->{$group}++; if ($skip_due_to_hdrs) { + print LOG "m" unless $quiet; if ($debug >= 2) { print LOG "\tDEBUGGING $i\tskip_art: " . ( @@ -1152,7 +1193,6 @@ sub crossFeedGroup { ) ) . "\n"; } - print LOG "m" unless $quiet; } elsif ($rnews) { printf RNEWS "#! rnews %d\n", $tx_len; map { print RNEWS $_ } @{$article}; @@ -1171,9 +1211,6 @@ sub crossFeedGroup { # 441 posting failed my $code = $toServer->code(); my $msg = $toServer->message(); - print LOG "\tDEBUGGING $i\tPost $code: Msg: <" - . join('//', split(/\r?\n/, $msg)) . ">\n" - if $debug >= 1; $msg =~ s/^340 .*?\n(?=.)//o; if ($msg =~ /^240 /) { print LOG "+" unless $quiet; @@ -1198,6 +1235,10 @@ sub crossFeedGroup { saveConfig(); exit(1); } + print LOG "\tDEBUGGING $i\tPost $code: Msg: <" + . join('//', split(/\r?\n/, $toServer->message())) + . ">\n" + if $debug >= 1; } elsif (not $reader and not $toServer->ihave($msgid, $article)) @@ -1210,9 +1251,6 @@ sub crossFeedGroup { # 437 article rejected -- do not try again my $code = $toServer->code(); my $msg = $toServer->message(); - print LOG "\tDEBUGGING $i\tPost $code: Msg: <" - . join('//', split(/\r?\n/, $msg)) . ">\n" - if $debug >= 1; if ($code == 435) { print LOG "." unless $quiet; $refused{$group}++; @@ -1229,14 +1267,17 @@ sub crossFeedGroup { saveConfig(); exit(1); } + print LOG "\tDEBUGGING $i\tPost $code: Msg: <" + . join('//', split(/\r?\n/, $msg)) . ">\n" + if $debug >= 1; } else { my $code = $toServer->code(); my $msg = $toServer->message(); - print LOG "\tDEBUGGING $i\tPost $code: Msg: <" - . join('//', split(/\r?\n/, $msg)) . ">\n" - if $debug >= 1; print LOG "+" unless $quiet; + print LOG "\tDEBUGGING $i\tPost $code: Msg: <" + . join('//', split(/\r?\n/, $msg)) . ">\n" + if $debug >= 1; $fed{$group}++; $info{server}->{$server}->{fed}++; $info{fed}++; @@ -1244,8 +1285,9 @@ sub crossFeedGroup { } $shash->{$group} = [time, $high = $i]; } elsif ($skip_article) { - # Optimized mode (-O) decided to skip this article... - print LOG "." unless $quiet; + # Optimized mode (-O) or article size check (-L) decided to skip + # this article... + # The "." resulting treatment has already been output. $refused{$group}++; $info{server}->{$server}->{refused}++; $info{refused}++; @@ -1262,20 +1304,23 @@ sub crossFeedGroup { # Net::NNTP is a subclass) when the connection is no longer # active. warn "\nArticle retrieval failed (" - . $fromServer->message() . ")\n\n"; + . join('//', split(/\r?\n/, $fromServer->message())) + . ")\n\n"; return 2; } else { warn "\nUnexpected response from server (" . $fromServer->code() . " " - . $fromServer->message() . ")\n"; + . join('//', split(/\r?\n/, $fromServer->message())) + . ")\n"; saveConfig(); exit(1); } } print LOG "x" unless $quiet; printf LOG ( - "\nDEBUGGING $i %s %s\n", $fromServer->code(), - $fromServer->message() + "\tDEBUGGING $i\t-- article unavailable %s %s\n", + $fromServer->code(), + join('//', split(/\r?\n/, $fromServer->message())) ) if $debug >= 1; } saveConfig() if $checkPoint and ($art_total_count % $checkPoint) == 0; diff --git a/scripts/innreport_inn.pm b/scripts/innreport_inn.pm index a63e17fe8..33d79a373 100644 --- a/scripts/innreport_inn.pm +++ b/scripts/innreport_inn.pm @@ -1670,6 +1670,8 @@ sub collect($$$$$$) { =~ /^python: dynamic authorization access type is not known: /o; # during daily expiration return 1 if $left =~ /^\S+ rejected Expiring process \d+$/o; + # during ovsqlite-util + return 1 if $left =~ /^\S+ rejected ovsqlite-util fixes$/o; # during scanlogs return 1 if $left =~ /^\S+ rejected Flushing log and syslog files$/o; return 1 if $left =~ /^\S+ rejected Snapshot log and syslog files$/o; diff --git a/storage/ovsqlite/ovmethod.config b/storage/ovsqlite/ovmethod.config index c945ba877..57b932ebb 100644 --- a/storage/ovsqlite/ovmethod.config +++ b/storage/ovsqlite/ovmethod.config @@ -2,6 +2,6 @@ name = ovsqlite number = 5 sources = ovsqlite.c ovsqlite-private.c extra-sources = ovsqlite-server.c sql-main.c sql-init.c sqlite-helper.c -programs = ovsqlite-server +programs = ovsqlite-server ovsqlite-util clean = sqlite-helper-gen maintclean = sql-init.c sql-init.h sql-main.c sql-main.h diff --git a/storage/ovsqlite/ovmethod.mk b/storage/ovsqlite/ovmethod.mk index 3e56b3f83..009d3d259 100644 --- a/storage/ovsqlite/ovmethod.mk +++ b/storage/ovsqlite/ovmethod.mk @@ -8,6 +8,9 @@ ovsqlite/ovsqlite-server: $(OVSQLITEOBJECTS) libinnstorage.$(EXTLIB) $(LIBSTORAGE) $(LIBHIST) $(LIBINN) $(STORAGE_LIBS) $(SQLITE3_LIBS) \ $(LIBS) +ovsqlite/ovsqlite-util: ovsqlite/ovsqlite-util.in $(FIXSCRIPT) + $(FIX) ovsqlite/ovsqlite-util.in + ovsqlite/sqlite-helper-gen: ovsqlite/sqlite-helper-gen.in $(FIXSCRIPT) $(FIX) -i ovsqlite/sqlite-helper-gen.in diff --git a/storage/ovsqlite/ovsqlite-util.in b/storage/ovsqlite/ovsqlite-util.in new file mode 100644 index 000000000..626949b82 --- /dev/null +++ b/storage/ovsqlite/ovsqlite-util.in @@ -0,0 +1,501 @@ +#! /usr/bin/perl -w +# fixscript will replace this line with code to load INN::Config + +## Overview manipulation utility for ovsqlite. +## +## Initial version written in March 2023 by Julien ÉLIE. + +use Compress::Zlib; +use Getopt::Std; +use POSIX qw(strftime locale_h); +use strict; + +$0 =~ s!.*/!!; + +# Bail out if the needed DBI Perl module is not installed. +eval { + require DBI; + require DBD::SQLite; + my $err = $DBI::errstr; # Just to silence "used only once" warning. + 1; +} + or die "DBI Perl module with SQLite driver needed" + . " (usually packaged as libdbd-sqlite3-perl, perl-DBD-SQLite," + . " or p5-DBD-SQLite)"; + +# Name of the database file (not configurable for ovsqlite). +my $dbfile = "ovsqlite.db"; + +my $usage = "Usage: + $0 [-AFghioO] [-a article] [-n newsgroup] [-p path] + +Options: + -a article Specify an article number or a range of article numbers on + which to act. + -A Audit the overview database for problems, and report them to + standard error, without trying to fix them. + -F Audit the overview database for problems, fixing them where + possible. To see what would be changed, run $0 + with -A first. + -g Dump overall overview information for the newsgroup specified + with -n. + -h Print this help message. + -i Dump newsgroup-related overview information for all newsgroups + or the newsgroup specified with -n. + -n newsgroup Specify the newsgroup on which to act. + -o Dump overview information for articles in the newsgroup + specified with -n, in the format returned to clients. + -O Dump overview information for articles in the newsgroup + specified with -n, in the format used by overchan. + -p path Read $dbfile database file in path directory instead of + default $INN::Config::pathoverview directory. +"; + +sub HELP_MESSAGE { + print $usage; + exit(0); +} + +my %opt; +getopts("a:AFghioOn:p:", \%opt) || die $usage; + +HELP_MESSAGE() if defined($opt{'h'}); + +my $modes = 0; +$modes++ if defined($opt{'A'}); +$modes++ if defined($opt{'F'}); +$modes++ if defined($opt{'g'}); +$modes++ if defined($opt{'i'}); +$modes++ if defined($opt{'o'}); +$modes++ if defined($opt{'O'}); + +die "Can't use both -A and -F\n\n$usage" + if defined($opt{'A'}) + and defined($opt{'F'}); +die "No action specified\n\n$usage" + if $modes == 0; +die "Only one action allowed at the same time\n\n$usage" + if $modes > 1; +die "A newsgroup must be specified with -n\n\n$usage" + if !defined($opt{'n'}) + and (defined($opt{'g'}) || defined($opt{'o'}) || defined($opt{'O'})); + +my ($low, $high, $compress, $basedict); +my $sql_extraclause_artinfo = ""; +my $sql_extraclause_groupinfo = ""; +my $dbdir = $opt{'p'} || $INN::Config::pathoverview; +my $datasource = "dbi:SQLite:dbname=$dbdir/$dbfile"; +my $pausemsg = "ovsqlite-util fixes"; # Message when pausing INN. + # Known line in innreport. +my $ispaused = 0; + +# To determine the length of compressed overview data. +my @pack_length_bias = ( + 0, + 0x80, + 0x4080, + 0x204080, + 0x10204080, +); + +# Open the connection. The username and password fields are left empty. +# Enabling RaiseError permits not checking every return error codes. +my $dbh = DBI->connect( + $datasource, '', '', + { PrintError => 0, RaiseError => 1, AutoCommit => 0 } +) or die "Can't connect to database: $DBI::errstr"; + +# To process multiple SQL statements in a do() handle. +$dbh->{sqlite_allow_multiple_statements} = 1; + +# Check the specified newsgroup exists, and create appropriate SQL requests. +if (defined($opt{'n'})) { + my $groupid = get_groupid($opt{'n'}); + if ($groupid == 0) { + printf STDERR "Cannot find newsgroup $opt{'n'} in overview\n"; + exit(1); + } else { + $sql_extraclause_artinfo = "where groupid = $groupid"; + $sql_extraclause_groupinfo + = "where cast(groupname as text) = '$opt{'n'}'"; + } +} + +# Parse the specified range of articles. +if (defined($opt{'a'})) { + if ($opt{'a'} =~ /^(\d*)-(\d*)$/) { + $low = $1; + $high = $2; + } elsif ($opt{'a'} =~ /^\d+$/) { + $low = $opt{'a'}; + $high = $low; + } else { + printf STDERR "Cannot parse $opt{'a'} as article numbers\n"; + exit(1); + } + if (defined($low) and length($low) > 0) { + $sql_extraclause_artinfo .= " and artnum >= $low"; + } + if (defined($high) and length($high) > 0) { + $sql_extraclause_artinfo .= " and artnum <= $high"; + } +} + +# Grab information from the misc table. +load_settings(); + +# Pause the server if changes need being done, so that the overview is not +# updated by another process at the same time. +if (defined($opt{'F'})) { + if (system "$INN::Config::newsbin/ctlinnd -s pause '$pausemsg'") { + die "$0: failed to pause INN, aborting\n"; + } + $ispaused = 1; +} + +if (defined($opt{'A'}) or defined($opt{'F'})) { + # Run the checks, and fix them if -F given. + check_groupinfo_consistency(); +} elsif (defined($opt{'g'})) { + dump_overview(); +} elsif (defined($opt{'i'})) { + dump_groupinfo(); +} elsif (defined($opt{'o'})) { + dump_artinfo_clients(); +} elsif (defined($opt{'O'})) { + dump_artinfo_overchan(); +} + +# Close the connection properly. +$dbh->disconnect; + +exit(0); + +END { + # In case we bail out while being paused, make sure that the show goes on! + if ($ispaused) { + if (system "$INN::Config::newsbin/ctlinnd -s go '$pausemsg'") { + die "$0: failed to resume INN with " + . "\"ctlinnd -s go '$pausemsg'\" command " + . "=> please check why and *manually* resume it\n"; + } + } +} + +# Grab compression settings from the database. +sub load_settings { + my $getsetting; + + $getsetting = $dbh->prepare("select value from misc where key = ?1"); + ($compress) = $dbh->selectrow_array($getsetting, undef, "compress"); + if ($compress > 0) { + ($basedict) = $dbh->selectrow_array($getsetting, undef, "basedict"); + defined($basedict) + or die "No basedict value found to decompress overview data\n"; + } +} + +# Return the ID of the newsgroup given as argument, or 0 if not found. +sub get_groupid { + my $groupname = shift; + my ($getgroupid, $groupid); + + $getgroupid = $dbh->prepare( + q{ +select groupid from groupinfo + where cast(groupname as text) = ?1 + and deleted = 0; +} + ); + ($groupid) = $dbh->selectrow_array($getgroupid, undef, $groupname); + + return defined($groupid) ? $groupid : 0; +} + +# Turn enforcement of foreign key constraints on or off, depending on the +# argument given to the function (either 1 for on, or 0 for off). +# The AutoCommit attribute needs being true so as not to start a transaction. +sub pragma_foreign_keys { + my $onoff = shift; + local $dbh->{AutoCommit} = 1; + $dbh->do("pragma foreign_keys = $onoff;"); +} + +# Return an array containing the length of the encoded length of decompressed +# overview data, and the length of actual decompressed overview data, or undef +# if corrupted. +# This function can be called even on uncompressed data. +# The expected argument is the overview data. +sub overview_length { + my $data = shift; + my ($lenlen, $len); + + if ($compress > 0) { + my $first; + + $first = vec($data, 0, 8); + $len = $first; + $lenlen = 1; + while (($first & 0x80) > 0) { + $len = $len << 8 | vec($data, $lenlen, 8); + $lenlen++; + $first <<= 1; + } + if ($lenlen > 5) { + return (undef, undef); + } + $len &= (1 << $lenlen * 7) - 1; + $len += $pack_length_bias[$lenlen - 1]; + } else { + # Uncompressed overview data. + $lenlen = 0; + $len = length($data); + } + return ($lenlen, $len); +} + +# Return decompressed overview data, or undef if a failure occurs. +# This function can be called even on uncompressed data. +# The expected arguments are the newsgroup name, the article number, and the +# associated overview data. +sub decompress_overview { + my ($groupname, $artnum, $data) = @_; + my $result; + + if ($compress > 0) { + my ($lenlen, $len) = overview_length($data); + if (!defined($lenlen)) { + warn "$groupname:$artnum: Corrupt overview data\n"; + return undef; + } + if ($len == 0) { + # No compression. + $result = substr($data, $lenlen); + } else { + my ($inflation, $status); + + ($inflation, $status) + = inflateInit(-Dictionary => "$basedict$groupname:$artnum\r\n"); + if ($status != Z_OK) { + warn + "$groupname:$artnum: inflateInit failed with code $status\n"; + return undef; + } + ($result, $status) = $inflation->inflate(substr($data, $lenlen)); + if ($status != Z_STREAM_END) { + warn "$groupname:$artnum: inflate failed with code $status\n"; + return undef; + } + if (length($result) != $len) { + warn "$groupname:$artnum: Corrupt overview data\n"; + return undef; + } + } + } else { + # Uncompressed overview data. + $result = $data; + } + return $result; +} + +# Perform consistency checks on low water marks, high water marks, and +# article counts in groupinfo. SQL commands were provided by Bo Lindbergh. +sub check_groupinfo_consistency { + my ($statement, $result); + + pragma_foreign_keys(0); + $dbh->do( + q{ +create table temp.repairs ( + groupid integer + primary key, + new_low integer + not null, + low_was_bad integer + not null, + new_high integer + not null, + high_was_bad integer + not null, + new_count integer + not null, + count_was_bad integer + not null, + expired integer + not null, + groupname blob + not null, + flag_alias blob + not null +); + +with new_stats (groupid, new_low, new_high, new_count) as + (select groupid, + coalesce(min(artnum), low), + coalesce(max(artnum), high), + count(artnum) + from groupinfo + natural left join artinfo + where not deleted + group by groupid) +insert into repairs + (groupid, + new_low, low_was_bad, + new_high, high_was_bad, + new_count, count_was_bad, + expired, groupname, flag_alias) + select groupid, + new_low, new_low != low as low_was_bad, + new_high, new_high != high as high_was_bad, + new_count, new_count != "count" as count_was_bad, + expired, groupname, flag_alias + from new_stats + natural join groupinfo + where low_was_bad + or high_was_bad + or count_was_bad; +} + ); + pragma_foreign_keys(1); + + $statement = $dbh->prepare("select count(*) from repairs;"); + ($result) = $dbh->selectrow_array($statement); + + if ($result > 0) { + printf STDERR ( + "%d groupinfo record%s incoherent\n", $result, + ($result > 1) ? "s" : "" + ); + + # Show incoherent records (L, H and C are for Low, High, Count). + $statement = $dbh->prepare( + q{ +select case when low_was_bad then 'L' else '_' end + || case when high_was_bad then 'H' else '_' end + || case when count_was_bad then 'C' else '_' end as bad, groupname + from repairs; +} + ); + $statement->execute(); + + while (my @row = $statement->fetchrow_array()) { + print STDERR " $row[0] for $row[1]\n"; + } + + if (defined($opt{'F'})) { + # Fix groupinfo table. + $result = $dbh->do( + q{ +insert or replace into groupinfo + (groupid, low, high, "count", expired, groupname, flag_alias) + select groupid, new_low, new_high, new_count, + expired, groupname, flag_alias + from repairs; +} + ); + $dbh->commit(); + + printf STDERR ( + "%d groupinfo record%s fixed\n", $result, + ($result > 1) ? "s" : "" + ); + } + + } +} + +# Dump overview information (-g option). +sub dump_overview { + my $statement; + + $statement = $dbh->prepare( + qq{ +select artnum, overview, arrived, expires, quote(token) + from artinfo $sql_extraclause_artinfo; +} + ); + $statement->execute(); + + while (my @row = $statement->fetchrow_array()) { + # quote(token) returns a string in the form "X'token'" without + # surrounding '@' characters. + my $len = (overview_length($row[1]))[1]; + if (!defined($len)) { + warn "$opt{'n'}:$row[0]: Corrupt overview data\n"; + $len = 0; + } + print "$row[0] $len $row[2] $row[3]"; + print " @" . substr($row[4], 2, -1) . "@\n"; + } +} + +# Dump newsgroup-related overview information (-i option). +sub dump_groupinfo { + my $statement; + + $statement = $dbh->prepare( + qq{ +select groupname, high, low, count, flag_alias, expired, deleted + from groupinfo $sql_extraclause_groupinfo; +} + ); + $statement->execute(); + + while (my @row = $statement->fetchrow_array()) { + print join(" ", @row) . "\n"; + } +} + +# Dump overview information in the format returned to clients (-o option). +sub dump_artinfo_clients { + my $statement; + + $statement = $dbh->prepare( + qq{ +select overview, artnum, quote(token), arrived, expires + from artinfo $sql_extraclause_artinfo; +} + ); + $statement->execute(); + + # To generate valid Date header fields. + setlocale(LC_TIME, 'C'); + + while (my @row = $statement->fetchrow_array()) { + my $overdata = decompress_overview($opt{'n'}, $row[1], $row[0]); + # Remove trailing CRLF from overview data. + $overdata =~ s/\r\n//g; + print "$overdata"; + print "\tArticle: $row[1]"; + print "\tToken: @" . substr($row[2], 2, -1) . "@"; + print "\tArrived: " + . strftime('%a, %d %b %Y %H:%M:%S %z (%Z)', localtime($row[3])); + print "\tExpires: " + . strftime('%a, %d %b %Y %H:%M:%S %z (%Z)', localtime($row[4])) + if $row[4] > 0; + print "\n"; + } +} + +# Dump overview information in the format used by overchan (-O option). +sub dump_artinfo_overchan { + my $statement; + + $statement = $dbh->prepare( + qq{ +select quote(token), arrived, expires, overview, artnum + from artinfo $sql_extraclause_artinfo; +} + ); + $statement->execute(); + + while (my @row = $statement->fetchrow_array()) { + print "@" . substr($row[0], 2, -1) . "@"; + my $overdata = decompress_overview($opt{'n'}, $row[4], $row[3]); + # Remove the first field (article number, not expected by overchan) + # and trailing CRLF from overview data. + $overdata =~ s/^\d+\t//; + $overdata =~ s/\r\n//; + print " $row[1] $row[2] $overdata\n"; + } +} diff --git a/support/mkmanifest b/support/mkmanifest index b4694caf0..484090be5 100755 --- a/support/mkmanifest +++ b/support/mkmanifest @@ -266,6 +266,7 @@ site/update storage/buffindexed/buffindexed_d storage/buildconfig storage/ovsqlite/ovsqlite-server +storage/ovsqlite/ovsqlite-util storage/ovsqlite/sqlite-helper-gen storage/tradindexed/tdx-util support/fixconfig
Attachment:
signature.asc
Description: PGP signature