X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=Debbugs%2FCommon.pm;h=ecd46076eafa1a109250f7e19b5e0e7e654f033f;hb=5764f589fd938ce310ba03e4960f7d876d87fa16;hp=91b0259d924d84982dd2219b1ad93595da12d826;hpb=70cf77464dc1d0966e821892d73b2dd6d1babc70;p=debbugs.git diff --git a/Debbugs/Common.pm b/Debbugs/Common.pm index 91b0259..ecd4607 100644 --- a/Debbugs/Common.pm +++ b/Debbugs/Common.pm @@ -1,3 +1,11 @@ +# This module is part of debbugs, and is released +# under the terms of the GPL version 2, or any later +# version at your option. +# See the file README and COPYING for more information. +# +# [Other people have contributed to this file; their copyrights should +# go here too.] +# Copyright 2007 by Don Armstrong . package Debbugs::Common; @@ -32,21 +40,34 @@ BEGIN{ @EXPORT = (); %EXPORT_TAGS = (util => [qw(getbugcomponent getbuglocation getlocationpath get_hashname), qw(appendfile buglog getparsedaddrs getmaintainers), - qw(getmaintainers_reverse) + qw(bug_status), + qw(getmaintainers_reverse), + qw(getpseudodesc), ], + misc => [qw(make_list globify_scalar english_join checkpid), + qw(cleanup_eval_fail), + ], + date => [qw(secs_to_english)], quit => [qw(quit)], - lock => [qw(filelock unfilelock)], + lock => [qw(filelock unfilelock lockpid)], ); @EXPORT_OK = (); - Exporter::export_ok_tags(qw(lock quit util)); + Exporter::export_ok_tags(qw(lock quit date util misc)); $EXPORT_TAGS{all} = [@EXPORT_OK]; } #use Debbugs::Config qw(:globals); + +use Carp; + use Debbugs::Config qw(:config); use IO::File; +use IO::Scalar; use Debbugs::MIME qw(decode_rfc1522); use Mail::Address; +use Cwd qw(cwd); + +use Params::Validate qw(validate_with :types); use Fcntl qw(:flock); @@ -146,6 +167,8 @@ sub get_hashname { Returns the path to the logfile corresponding to the bug. +Returns undef if the bug does not exist. + =cut sub buglog { @@ -153,9 +176,27 @@ sub buglog { my $location = getbuglocation($bugnum, 'log'); return getbugcomponent($bugnum, 'log', $location) if ($location); $location = getbuglocation($bugnum, 'log.gz'); - return getbugcomponent($bugnum, 'log.gz', $location); + return getbugcomponent($bugnum, 'log.gz', $location) if ($location); + return undef; } +=head2 bug_status + + bug_status($bugnum) + + +Returns the path to the summary file corresponding to the bug. + +Returns undef if the bug does not exist. + +=cut + +sub bug_status{ + my ($bugnum) = @_; + my $location = getbuglocation($bugnum, 'summary'); + return getbugcomponent($bugnum, 'summary', $location) if ($location); + return undef; +} =head2 appendfile @@ -166,20 +207,17 @@ Opens a file for appending and writes data to it. =cut sub appendfile { - my $file = shift; - if (!open(AP,">>$file")) { - print $DEBUG_FH "failed open log<\n" if $DEBUG; - print $DEBUG_FH "failed open log err $!<\n" if $DEBUG; - &quit("opening $file (appendfile): $!"); - } - print(AP @_) || &quit("writing $file (appendfile): $!"); - close(AP) || &quit("closing $file (appendfile): $!"); + my ($file,@data) = @_; + my $fh = IO::File->new($file,'a') or + die "Unable top open $file for appending: $!"; + print {$fh} @data or die "Unable to write to $file: $!"; + close $fh or die "Unable to close $file: $!"; } =head2 getparsedaddrs my $address = getparsedaddrs($address); - my @address = getpasredaddrs($address); + my @address = getparsedaddrs($address); Returns the output from Mail::Address->parse, or the cached output if this address has been parsed before. In SCALAR context returns the @@ -188,26 +226,38 @@ first address parsed. =cut -my %_parsedaddrs; +our %_parsedaddrs; sub getparsedaddrs { my $addr = shift; return () unless defined $addr; return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0] if exists $_parsedaddrs{$addr}; - @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr); + { + # don't display the warnings from Mail::Address->parse + local $SIG{__WARN__} = sub { }; + @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr); + } return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]; } -my $_maintainer; -my $_maintainer_rev; +=head2 getmaintainers + + my $maintainer = getmaintainers()->{debbugs} + +Returns a hashref of package => maintainer pairs. + +=cut + +our $_maintainer; +our $_maintainer_rev; sub getmaintainers { return $_maintainer if $_maintainer; my %maintainer; my %maintainer_rev; - for my $file (@config{qw(maintainer_file maintainer_file_override)}) { + for my $file (@config{qw(maintainer_file maintainer_file_override pseduo_maint_file)}) { next unless defined $file; - my $maintfile = new IO::File $file,'r' or - &quitcgi("Unable to open $file: $!"); + my $maintfile = IO::File->new($file,'r') or + die "Unable to open maintainer file $file: $!"; while(<$maintfile>) { next unless m/^(\S+)\s+(\S.*\S)\s*$/; ($a,$b)=($1,$2); @@ -223,12 +273,81 @@ sub getmaintainers { $_maintainer_rev = \%maintainer_rev; return $_maintainer; } + +=head2 getmaintainers_reverse + + my @packages = @{getmaintainers_reverse->{'don@debian.org'}||[]}; + +Returns a hashref of maintainer => [qw(list of packages)] pairs. + +=cut + sub getmaintainers_reverse{ return $_maintainer_rev if $_maintainer_rev; getmaintainers(); return $_maintainer_rev; } +=head2 getpseudodesc + + my $pseudopkgdesc = getpseudodesc(...); + +Returns the entry for a pseudo package from the +$config{pseudo_desc_file}. In cases where pseudo_desc_file is not +defined, returns an empty arrayref. + +This function can be used to see if a particular package is a +pseudopackage or not. + +=cut + +our $_pseudodesc; +sub getpseudodesc { + return $_pseudodesc if $_pseudodesc; + my %pseudodesc; + + if (not defined $config{pseudo_desc_file}) { + $_pseudodesc = {}; + return $_pseudodesc; + } + my $pseudo = IO::File->new($config{pseudo_desc_file},'r') + or die "Unable to open $config{pseudo_desc_file}: $!"; + while(<$pseudo>) { + next unless m/^(\S+)\s+(\S.*\S)\s*$/; + $pseudodesc{lc $1} = $2; + } + close($pseudo); + $_pseudodesc = \%pseudodesc; + return $_pseudodesc; +} + + +=head1 DATE + + my $english = secs_to_english($seconds); + my ($days,$english) = secs_to_english($seconds); + +XXX This should probably be changed to use Date::Calc + +=cut + +sub secs_to_english{ + my ($seconds) = @_; + + my $days = int($seconds / 86400); + my $years = int($days / 365); + $days %= 365; + my $result; + my @age; + push @age, "1 year" if ($years == 1); + push @age, "$years years" if ($years > 1); + push @age, "1 day" if ($days == 1); + push @age, "$days days" if ($days > 1); + $result .= join(" and ", @age); + + return wantarray?(int($seconds/86400),$result):$result; +} + =head1 LOCK @@ -242,13 +361,15 @@ FLOCKs the passed file. Use unfilelock to unlock it. =cut -my @filelocks; -my @cleanups; +our @filelocks; sub filelock { # NB - NOT COMPATIBLE WITH `with-lock' my ($lockfile) = @_; - my ($count,$errors) = @_; + if ($lockfile !~ m{^/}) { + $lockfile = cwd().'/'.$lockfile; + } + my ($count,$errors); $count= 10; $errors= ''; for (;;) { my $fh = eval { @@ -267,11 +388,17 @@ sub filelock { } if (--$count <=0) { $errors =~ s/\n+$//; - &quit("failed to get lock on $lockfile -- $errors"); + die "failed to get lock on $lockfile -- $errors"; } sleep 10; } - push(@cleanups,\&unfilelock); +} + +# clean up all outstanding locks at end time +END { + while (@filelocks) { + unfilelock(); + } } @@ -292,16 +419,71 @@ sub unfilelock { return; } my %fl = %{pop(@filelocks)}; - pop(@cleanups); flock($fl{fh},LOCK_UN) or warn "Unable to unlock lockfile $fl{file}: $!"; close($fl{fh}) or warn "Unable to close lockfile $fl{file}: $!"; unlink($fl{file}) - or warn "Unable to unlink locfile $fl{file}: $!"; + or warn "Unable to unlink lockfile $fl{file}: $!"; } +=head2 lockpid + + lockpid('/path/to/pidfile'); + +Creates a pidfile '/path/to/pidfile' if one doesn't exist or if the +pid in the file does not respond to kill 0. + +Returns 1 on success, false on failure; dies on unusual errors. + +=cut + +sub lockpid { + my ($pidfile) = @_; + if (-e $pidfile) { + my $pid = checkpid($pidfile); + die "Unable to read pidfile $pidfile: $!" if not defined $pid; + return 0 if $pid != 0; + unlink $pidfile or + die "Unable to unlink stale pidfile $pidfile $!"; + } + my $pidfh = IO::File->new($pidfile,'w') or + die "Unable to open $pidfile for writing: $!"; + print {$pidfh} $$ or die "Unable to write to $pidfile $!"; + close $pidfh or die "Unable to close $pidfile $!"; + return 1; +} + +=head2 checkpid + + checkpid('/path/to/pidfile'); + +Checks a pid file and determines if the process listed in the pidfile +is still running. Returns the pid if it is, 0 if it isn't running, and +undef if the pidfile doesn't exist or cannot be read. + +=cut + +sub checkpid{ + my ($pidfile) = @_; + if (-e $pidfile) { + my $pidfh = IO::File->new($pidfile, 'r') or + return undef; + local $/; + my $pid = <$pidfh>; + close $pidfh; + ($pid) = $pid =~ /(\d+)/; + if (defined $pid and kill(0,$pid)) { + return $pid; + } + return 0; + } + else { + return undef; + } +} + =head1 QUIT @@ -311,21 +493,165 @@ These functions are exported with the :quit tag. quit() -Exits the program by calling die after running some cleanups. +Exits the program by calling die. -This should be replaced with an END handler which runs the cleanups -instead. (Or possibly a die handler, if the cleanups are important) +Usage of quit is deprecated; just call die instead. =cut sub quit { - print $DEBUG_FH "quitting >$_[0]<\n" if $DEBUG; - my ($u); - while ($u= $cleanups[$#cleanups]) { &$u; } - die "*** $_[0]\n"; + print {$DEBUG_FH} "quitting >$_[0]<\n" if $DEBUG; + carp "quit() is deprecated; call die directly instead"; +} + + +=head1 MISC + +These functions are exported with the :misc tag + +=head2 make_list + + LIST = make_list(@_); + +Turns a scalar or an arrayref into a list; expands a list of arrayrefs +into a list. + +That is, make_list([qw(a b c)]); returns qw(a b c); make_list([qw(a +b)],[qw(c d)] returns qw(a b c d); + +=cut + +sub make_list { + return map {(ref($_) eq 'ARRAY')?@{$_}:$_} @_; } +=head2 english_join + + print english_join(list => \@list); + print english_join(\@list); + +Joins list properly to make an english phrase. + +=over + +=item normal -- how to separate most values; defaults to ', ' + +=item last -- how to separate the last two values; defaults to ', and ' + +=item only_two -- how to separate only two values; defaults to ' and ' + +=item list -- ARRAYREF values to join; if the first argument is an +ARRAYREF, it's assumed to be the list of values to join + +=back + +In cases where C is empty, returns ''; when there is only one +element, returns that element. + +=cut + +sub english_join { + if (ref $_[0] eq 'ARRAY') { + return english_join(list=>$_[0]); + } + my %param = validate_with(params => \@_, + spec => {normal => {type => SCALAR, + default => ', ', + }, + last => {type => SCALAR, + default => ', and ', + }, + only_two => {type => SCALAR, + default => ' and ', + }, + list => {type => ARRAYREF, + }, + }, + ); + my @list = @{$param{list}}; + if (@list <= 1) { + return @list?$list[0]:''; + } + elsif (@list == 2) { + return join($param{only_two},@list); + } + my $ret = $param{last} . pop(@list); + return join($param{normal},@list) . $ret; +} + + +=head2 globify_scalar + + my $handle = globify_scalar(\$foo); + +if $foo isn't already a glob or a globref, turn it into one using +IO::Scalar. Gives a new handle to /dev/null if $foo isn't defined. + +Will carp if given a scalar which isn't a scalarref or a glob (or +globref), and return /dev/null. May return undef if IO::Scalar or +IO::File fails. (Check $!) + +=cut + +sub globify_scalar { + my ($scalar) = @_; + my $handle; + if (defined $scalar) { + if (defined ref($scalar)) { + if (ref($scalar) eq 'SCALAR' and + not UNIVERSAL::isa($scalar,'GLOB')) { + return IO::Scalar->new($scalar); + } + else { + return $scalar; + } + } + elsif (UNIVERSAL::isa(\$scalar,'GLOB')) { + return $scalar; + } + else { + carp "Given a non-scalar reference, non-glob to globify_scalar; returning /dev/null handle"; + } + } + return IO::File->new('/dev/null','w'); +} + +=head2 cleanup_eval_fail() + + print "Something failed with: ".cleanup_eval_fail($@); + +Does various bits of cleanup on the failure message from an eval (or +any other die message) + +Takes at most two options; the first is the actual failure message +(usually $@ and defaults to $@), the second is the debug level +(defaults to $DEBUG). + +If debug is non-zero, the code at which the failure occured is output. + +=cut + +sub cleanup_eval_fail { + my ($error,$debug) = @_; + if (not defined $error or not @_) { + $error = $@ // 'unknown reason'; + } + if (@_ <= 1) { + $debug = $DEBUG // 0; + } + $debug = 0 if not defined $debug; + + if ($debug > 0) { + return $error; + } + # ditch the "at foo/bar/baz.pm line 5" + $error =~ s/\sat\s\S+\sline\s\d+//; + # ditch trailing multiple periods in case there was a cascade of + # die messages. + $error =~ s/\.+$/\./; + return $error; +} 1;