1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
6 # [Other people have contributed to this file; their copyrights should
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
10 package Debbugs::Common;
14 Debbugs::Common -- Common routines for all of Debbugs
18 use Debbugs::Common qw(:url :html);
23 This module is a replacement for the general parts of errorlib.pl.
24 subroutines in errorlib.pl will be gradually phased out and replaced
25 with equivalent (or better) functionality here.
33 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
34 use Exporter qw(import);
38 $DEBUG = 0 unless defined $DEBUG;
41 %EXPORT_TAGS = (util => [qw(getbugcomponent getbuglocation getlocationpath get_hashname),
42 qw(appendfile overwritefile buglog getparsedaddrs getmaintainers),
44 qw(getmaintainers_reverse),
46 qw(package_maintainer),
48 qw(open_compressed_file),
50 misc => [qw(make_list globify_scalar english_join checkpid),
51 qw(cleanup_eval_fail),
54 date => [qw(secs_to_english)],
56 lock => [qw(filelock unfilelock lockpid simple_filelock simple_unlockfile)],
59 Exporter::export_ok_tags(keys %EXPORT_TAGS);
60 $EXPORT_TAGS{all} = [@EXPORT_OK];
63 #use Debbugs::Config qw(:globals);
68 use Debbugs::Config qw(:config);
71 use Debbugs::MIME qw(decode_rfc1522);
74 use Storable qw(dclone);
75 use Time::HiRes qw(usleep);
76 use MLDBM qw(DB_File Storable);
77 $MLDBM::DumpMeth='portable';
79 use Params::Validate qw(validate_with :types);
81 use Fcntl qw(:DEFAULT :flock);
82 use Encode qw(is_utf8 decode_utf8);
84 our $DEBUG_FH = \*STDERR if not defined $DEBUG_FH;
88 The following functions are exported by the C<:util> tag
90 =head2 getbugcomponent
92 my $file = getbugcomponent($bug_number,$extension,$location)
94 Returns the path to the bug file in location C<$location>, bug number
95 C<$bugnumber> and extension C<$extension>
100 my ($bugnum, $ext, $location) = @_;
102 if (not defined $location) {
103 $location = getbuglocation($bugnum, $ext);
104 # Default to non-archived bugs only for now; CGI scripts want
105 # archived bugs but most of the backend scripts don't. For now,
106 # anything that is prepared to accept archived bugs should call
107 # getbuglocation() directly first.
108 return undef if defined $location and
109 ($location ne 'db' and $location ne 'db-h');
111 my $dir = getlocationpath($location);
112 return undef if not defined $dir;
113 if (defined $location and $location eq 'db') {
114 return "$dir/$bugnum.$ext";
116 my $hash = get_hashname($bugnum);
117 return "$dir/$hash/$bugnum.$ext";
121 =head2 getbuglocation
123 getbuglocation($bug_number,$extension)
125 Returns the the location in which a particular bug exists; valid
126 locations returned currently are archive, db-h, or db. If the bug does
127 not exist, returns undef.
132 my ($bugnum, $ext) = @_;
133 my $archdir = get_hashname($bugnum);
134 return 'archive' if -r getlocationpath('archive')."/$archdir/$bugnum.$ext";
135 return 'db-h' if -r getlocationpath('db-h')."/$archdir/$bugnum.$ext";
136 return 'db' if -r getlocationpath('db')."/$bugnum.$ext";
141 =head2 getlocationpath
143 getlocationpath($location)
145 Returns the path to a specific location
149 sub getlocationpath {
151 if (defined $location and $location eq 'archive') {
152 return "$config{spool_dir}/archive";
153 } elsif (defined $location and $location eq 'db') {
154 return "$config{spool_dir}/db";
156 return "$config{spool_dir}/db-h";
165 Returns the hash of the bug which is the location within the archive
170 return "" if ( $_[ 0 ] < 0 );
171 return sprintf "%02d", $_[ 0 ] % 100;
178 Returns the path to the logfile corresponding to the bug.
180 Returns undef if the bug does not exist.
186 my $location = getbuglocation($bugnum, 'log');
187 return getbugcomponent($bugnum, 'log', $location) if ($location);
188 $location = getbuglocation($bugnum, 'log.gz');
189 return getbugcomponent($bugnum, 'log.gz', $location) if ($location);
198 Returns the path to the summary file corresponding to the bug.
200 Returns undef if the bug does not exist.
206 my $location = getbuglocation($bugnum, 'summary');
207 return getbugcomponent($bugnum, 'summary', $location) if ($location);
213 appendfile($file,'data','to','append');
215 Opens a file for appending and writes data to it.
220 my ($file,@data) = @_;
221 my $fh = IO::File->new($file,'a') or
222 die "Unable top open $file for appending: $!";
223 print {$fh} @data or die "Unable to write to $file: $!";
224 close $fh or die "Unable to close $file: $!";
229 ovewritefile($file,'data','to','append');
231 Opens file.new, writes data to it, then moves file.new to file.
236 my ($file,@data) = @_;
237 my $fh = IO::File->new("${file}.new",'w') or
238 die "Unable top open ${file}.new for writing: $!";
239 print {$fh} @data or die "Unable to write to ${file}.new: $!";
240 close $fh or die "Unable to close ${file}.new: $!";
241 rename("${file}.new",$file) or
242 die "Unable to rename ${file}.new to $file: $!";
245 =head2 open_compressed_file
247 my $fh = open_compressed_file('foo.gz') or
248 die "Unable to open compressed file: $!";
251 Opens a file; if the file ends in .gz, .xz, or .bz2, the appropriate
252 decompression program is forked and output from it is read.
254 This routine by default opens the file with UTF-8 encoding; if you want some
255 other encoding, specify it with the second option.
258 sub open_compressed_file {
259 my ($file,$encoding) = @_;
260 $encoding //= ':encoding(UTF-8)';
262 my $mode = "<$encoding";
264 if ($file =~ /\.gz$/) {
265 $mode = "-|$encoding";
266 push @opts,'gzip','-dc';
268 if ($file =~ /\.xz$/) {
269 $mode = "-|$encoding";
270 push @opts,'xz','-dc';
272 if ($file =~ /\.bz2$/) {
273 $mode = "-|$encoding";
274 push @opts,'bzip2','-dc';
276 open($fh,$mode,@opts,$file);
282 =head2 getparsedaddrs
284 my $address = getparsedaddrs($address);
285 my @address = getparsedaddrs($address);
287 Returns the output from Mail::Address->parse, or the cached output if
288 this address has been parsed before. In SCALAR context returns the
289 first address parsed.
297 return () unless defined $addr;
298 return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]
299 if exists $_parsedaddrs{$addr};
301 # don't display the warnings from Mail::Address->parse
302 local $SIG{__WARN__} = sub { };
303 @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
305 return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0];
308 =head2 getmaintainers
310 my $maintainer = getmaintainers()->{debbugs}
312 Returns a hashref of package => maintainer pairs.
316 our $_maintainer = undef;
317 our $_maintainer_rev = undef;
319 return $_maintainer if defined $_maintainer;
320 package_maintainer(rehash => 1);
324 =head2 getmaintainers_reverse
326 my @packages = @{getmaintainers_reverse->{'don@debian.org'}||[]};
328 Returns a hashref of maintainer => [qw(list of packages)] pairs.
332 sub getmaintainers_reverse{
333 return $_maintainer_rev if defined $_maintainer_rev;
334 package_maintainer(rehash => 1);
335 return $_maintainer_rev;
338 =head2 package_maintainer
340 my @s = package_maintainer(source => [qw(foo bar baz)],
341 binary => [qw(bleh blah)],
346 =item source -- scalar or arrayref of source package names to return
347 maintainers for, defaults to the empty arrayref.
349 =item binary -- scalar or arrayref of binary package names to return
350 maintainers for; automatically returns source package maintainer if
351 the package name starts with 'src:', defaults to the empty arrayref.
353 =item reverse -- whether to return the source/binary packages a
354 maintainer maintains instead
356 =item rehash -- whether to reread the maintainer and source maintainer
363 our $_source_maintainer = undef;
364 our $_source_maintainer_rev = undef;
365 sub package_maintainer {
366 my %param = validate_with(params => \@_,
367 spec => {source => {type => SCALAR|ARRAYREF,
370 binary => {type => SCALAR|ARRAYREF,
373 maintainer => {type => SCALAR|ARRAYREF,
376 rehash => {type => BOOLEAN,
379 reverse => {type => BOOLEAN,
384 my @binary = make_list($param{binary});
385 my @source = make_list($param{source});
386 my @maintainers = make_list($param{maintainer});
387 if ((@binary or @source) and @maintainers) {
388 croak "It is nonsensical to pass both maintainers and source or binary";
390 if ($param{rehash}) {
391 $_source_maintainer = undef;
392 $_source_maintainer_rev = undef;
393 $_maintainer = undef;
394 $_maintainer_rev = undef;
396 if (not defined $_source_maintainer or
397 not defined $_source_maintainer_rev) {
398 $_source_maintainer = {};
399 $_source_maintainer_rev = {};
400 if (-e $config{spool_dir}.'/source_maintainers.idx' and
401 -e $config{spool_dir}.'/source_maintainers_reverse.idx'
403 tie %{$_source_maintainer},
404 MLDBM => $config{spool_dir}.'/source_maintainers.idx',
406 die "Unable to tie source maintainers: $!";
407 tie %{$_source_maintainer_rev},
408 MLDBM => $config{spool_dir}.'/source_maintainers_reverse.idx',
410 die "Unable to tie source maintainers reverse: $!";
412 for my $fn (@config{('source_maintainer_file',
413 'source_maintainer_file_override',
414 'pseudo_maint_file')}) {
415 next unless defined $fn and length $fn;
417 warn "Missing source maintainer file '$fn'";
420 __add_to_hash($fn,$_source_maintainer,
421 $_source_maintainer_rev);
425 if (not defined $_maintainer or
426 not defined $_maintainer_rev) {
428 $_maintainer_rev = {};
429 if (-e $config{spool_dir}.'/maintainers.idx' and
430 -e $config{spool_dir}.'/maintainers_reverse.idx'
433 MLDBM => $config{spool_dir}.'/binary_maintainers.idx',
435 die "Unable to tie binary maintainers: $!";
436 tie %{$_maintainer_rev},
437 MLDBM => $config{spool_dir}.'/binary_maintainers_reverse.idx',
439 die "Unable to binary maintainers reverse: $!";
441 for my $fn (@config{('maintainer_file',
442 'maintainer_file_override',
443 'pseudo_maint_file')}) {
444 next unless defined $fn and length $fn;
446 warn "Missing maintainer file '$fn'";
449 __add_to_hash($fn,$_maintainer,
455 for my $binary (@binary) {
456 if (not $param{reverse} and $binary =~ /^src:/) {
457 push @source,$binary;
460 push @return,grep {defined $_} make_list($_maintainer->{$binary});
462 for my $source (@source) {
463 $source =~ s/^src://;
464 push @return,grep {defined $_} make_list($_source_maintainer->{$source});
466 for my $maintainer (grep {defined $_} @maintainers) {
467 push @return,grep {defined $_}
468 make_list($_maintainer_rev->{$maintainer});
469 push @return,map {$_ !~ /^src:/?'src:'.$_:$_}
471 make_list($_source_maintainer_rev->{$maintainer});
476 #=head2 __add_to_hash
478 # __add_to_hash($file,$forward_hash,$reverse_hash,'address');
480 # Reads a maintainer/source maintainer/pseudo desc file and adds the
481 # maintainers from it to the forward and reverse hashref; assumes that
482 # the forward is unique; makes no assumptions of the reverse.
487 my ($fn,$forward,$reverse,$type) = @_;
488 if (ref($forward) ne 'HASH') {
489 croak "__add_to_hash must be passed a hashref for the forward";
491 if (defined $reverse and not ref($reverse) eq 'HASH') {
492 croak "if reverse is passed to __add_to_hash, it must be a hashref";
495 my $fh = IO::File->new($fn,'r') or
496 croak "Unable to open $fn for reading: $!";
497 binmode($fh,':encoding(UTF-8)');
500 my @elements = split /\t/;
501 next unless @elements >=2;
502 # we do this because the source maintainer file contains the
503 # archive location, which we don't care about
504 my ($key,$value)=($elements[0],$elements[-1]);
506 $forward->{$key}= $value;
507 if (defined $reverse) {
508 if ($type eq 'address') {
509 for my $m (map {lc($_->address)} (getparsedaddrs($value))) {
510 push @{$reverse->{$m}},$key;
514 push @{$reverse->{$value}}, $key;
523 my $pseudopkgdesc = getpseudodesc(...);
525 Returns the entry for a pseudo package from the
526 $config{pseudo_desc_file}. In cases where pseudo_desc_file is not
527 defined, returns an empty arrayref.
529 This function can be used to see if a particular package is a
530 pseudopackage or not.
534 our $_pseudodesc = undef;
536 return $_pseudodesc if defined $_pseudodesc;
538 __add_to_hash($config{pseudo_desc_file},$_pseudodesc) if
539 defined $config{pseudo_desc_file} and
540 length $config{pseudo_desc_file};
546 sort_versions('1.0-2','1.1-2');
548 Sorts versions using AptPkg::Versions::compare if it is available, or
549 Debbugs::Versions::Dpkg::vercmp if it isn't.
555 use Debbugs::Versions::Dpkg;
556 $vercmp=\&Debbugs::Versions::Dpkg::vercmp;
558 # eventually we'll use AptPkg:::Version or similar, but the current
559 # implementation makes this *super* difficult.
562 # use AptPkg::Version;
563 # $vercmp=\&AptPkg::Version::compare;
568 return sort {$vercmp->($a,$b)} @_;
574 my $english = secs_to_english($seconds);
575 my ($days,$english) = secs_to_english($seconds);
577 XXX This should probably be changed to use Date::Calc
584 my $days = int($seconds / 86400);
585 my $years = int($days / 365);
589 push @age, "1 year" if ($years == 1);
590 push @age, "$years years" if ($years > 1);
591 push @age, "1 day" if ($days == 1);
592 push @age, "$days days" if ($days > 1);
593 $result .= join(" and ", @age);
595 return wantarray?(int($seconds/86400),$result):$result;
601 These functions are exported with the :lock tag
606 filelock($lockfile,$locks);
608 FLOCKs the passed file. Use unfilelock to unlock it.
610 Can be passed an optional $locks hashref, which is used to track which
611 files are locked (and how many times they have been locked) to allow
612 for cooperative locking.
621 # NB - NOT COMPATIBLE WITH `with-lock'
622 my ($lockfile,$locks) = @_;
623 if ($lockfile !~ m{^/}) {
624 $lockfile = cwd().'/'.$lockfile;
626 # This is only here to allow for relocking bugs inside of
627 # Debbugs::Control. Nothing else should be using it.
628 if (defined $locks and exists $locks->{locks}{$lockfile} and
629 $locks->{locks}{$lockfile} >= 1) {
630 if (exists $locks->{relockable} and
631 exists $locks->{relockable}{$lockfile}) {
632 $locks->{locks}{$lockfile}++;
633 # indicate that the bug for this lockfile needs to be reread
634 $locks->{relockable}{$lockfile} = 1;
635 push @{$locks->{lockorder}},$lockfile;
640 confess "Locking already locked file: $lockfile\n".Data::Dumper->Dump([$lockfile,$locks],[qw(lockfile locks)]);
643 my ($fh,$t_lockfile,$errors) =
644 simple_filelock($lockfile,10,1);
646 push @filelocks, {fh => $fh, file => $lockfile};
647 if (defined $locks) {
648 $locks->{locks}{$lockfile}++;
649 push @{$locks->{lockorder}},$lockfile;
653 croak "failed to get lock on $lockfile -- $errors".
654 (defined $locks?Data::Dumper->Dump([$locks],[qw(locks)]):'');
658 =head2 simple_filelock
660 my ($fh,$t_lockfile,$errors) =
661 simple_filelock($lockfile,$count,$wait);
663 Does a flock of lockfile. If C<$count> is zero, does a blocking lock.
664 Otherwise, does a non-blocking lock C<$count> times, waiting C<$wait>
667 In list context, returns the lockfile filehandle, lockfile name, and
668 any errors which occured.
670 When the lockfile filehandle is undef, locking failed.
672 These lockfiles must be unlocked manually at process end.
677 sub simple_filelock {
678 my ($lockfile,$count,$wait) = @_;
679 if (not defined $count) {
685 if (not defined $wait) {
692 my $fh2 = IO::File->new($lockfile,'w')
693 or die "Unable to open $lockfile for writing: $!";
694 # Do a blocking lock if count is zero
695 flock($fh2,LOCK_EX|($count == 0?0:LOCK_NB))
696 or die "Unable to lock $lockfile $!";
705 # use usleep for fractional wait seconds
706 usleep($wait * 1_000_000);
708 last unless (--$count > 0);
711 return wantarray?($fh,$lockfile,$errors):$fh
713 return wantarray?(undef,$lockfile,$errors):undef;
716 # clean up all outstanding locks at end time
723 =head2 simple_unlockfile
725 simple_unlockfile($fh,$lockfile);
730 sub simple_unlockfile {
731 my ($fh,$lockfile) = @_;
733 or warn "Unable to unlock lockfile $lockfile: $!";
735 or warn "Unable to close lockfile $lockfile: $!";
737 or warn "Unable to unlink lockfile $lockfile: $!";
746 Unlocks the file most recently locked.
748 Note that it is not currently possible to unlock a specific file
749 locked with filelock.
755 if (@filelocks == 0) {
756 carp "unfilelock called with no active filelocks!\n";
759 if (defined $locks and ref($locks) ne 'HASH') {
760 croak "hash not passsed to unfilelock";
762 if (defined $locks and exists $locks->{lockorder} and
763 @{$locks->{lockorder}} and
764 exists $locks->{locks}{$locks->{lockorder}[-1]}) {
765 my $lockfile = pop @{$locks->{lockorder}};
766 $locks->{locks}{$lockfile}--;
767 if ($locks->{locks}{$lockfile} > 0) {
770 delete $locks->{locks}{$lockfile};
772 my %fl = %{pop(@filelocks)};
773 simple_unlockfile($fl{fh},$fl{file});
779 lockpid('/path/to/pidfile');
781 Creates a pidfile '/path/to/pidfile' if one doesn't exist or if the
782 pid in the file does not respond to kill 0.
784 Returns 1 on success, false on failure; dies on unusual errors.
791 my $pid = checkpid($pidfile);
792 die "Unable to read pidfile $pidfile: $!" if not defined $pid;
793 return 0 if $pid != 0;
795 die "Unable to unlink stale pidfile $pidfile $!";
797 my $pidfh = IO::File->new($pidfile,O_CREAT|O_EXCL|O_WRONLY) or
798 die "Unable to open $pidfile for writing: $!";
799 print {$pidfh} $$ or die "Unable to write to $pidfile $!";
800 close $pidfh or die "Unable to close $pidfile $!";
806 checkpid('/path/to/pidfile');
808 Checks a pid file and determines if the process listed in the pidfile
809 is still running. Returns the pid if it is, 0 if it isn't running, and
810 undef if the pidfile doesn't exist or cannot be read.
817 my $pidfh = IO::File->new($pidfile, 'r') or
822 ($pid) = $pid =~ /(\d+)/;
823 if (defined $pid and kill(0,$pid)) {
836 These functions are exported with the :quit tag.
842 Exits the program by calling die.
844 Usage of quit is deprecated; just call die instead.
849 print {$DEBUG_FH} "quitting >$_[0]<\n" if $DEBUG;
850 carp "quit() is deprecated; call die directly instead";
856 These functions are exported with the :misc tag
860 LIST = make_list(@_);
862 Turns a scalar or an arrayref into a list; expands a list of arrayrefs
865 That is, make_list([qw(a b c)]); returns qw(a b c); make_list([qw(a
866 b)],[qw(c d)] returns qw(a b c d);
871 return map {(ref($_) eq 'ARRAY')?@{$_}:$_} @_;
877 print english_join(list => \@list);
878 print english_join(\@list);
880 Joins list properly to make an english phrase.
884 =item normal -- how to separate most values; defaults to ', '
886 =item last -- how to separate the last two values; defaults to ', and '
888 =item only_two -- how to separate only two values; defaults to ' and '
890 =item list -- ARRAYREF values to join; if the first argument is an
891 ARRAYREF, it's assumed to be the list of values to join
895 In cases where C<list> is empty, returns ''; when there is only one
896 element, returns that element.
901 if (ref $_[0] eq 'ARRAY') {
902 return english_join(list=>$_[0]);
904 my %param = validate_with(params => \@_,
905 spec => {normal => {type => SCALAR,
908 last => {type => SCALAR,
911 only_two => {type => SCALAR,
914 list => {type => ARRAYREF,
918 my @list = @{$param{list}};
920 return @list?$list[0]:'';
923 return join($param{only_two},@list);
925 my $ret = $param{last} . pop(@list);
926 return join($param{normal},@list) . $ret;
930 =head2 globify_scalar
932 my $handle = globify_scalar(\$foo);
934 if $foo isn't already a glob or a globref, turn it into one using
935 IO::Scalar. Gives a new handle to /dev/null if $foo isn't defined.
937 Will carp if given a scalar which isn't a scalarref or a glob (or
938 globref), and return /dev/null. May return undef if IO::Scalar or
939 IO::File fails. (Check $!)
941 The scalar will fill with octets, not perl's internal encoding, so you
942 must use decode_utf8() after on the scalar, and encode_utf8() on it
943 before. This appears to be a bug in the underlying modules.
950 if (defined $scalar) {
951 if (defined ref($scalar)) {
952 if (ref($scalar) eq 'SCALAR' and
953 not UNIVERSAL::isa($scalar,'GLOB')) {
954 if (is_utf8(${$scalar})) {
955 ${$scalar} = decode_utf8(${$scalar});
956 carp(q(\$scalar must not be in perl's internal encoding));
958 open $handle, '>:scalar:utf8', $scalar;
965 elsif (UNIVERSAL::isa(\$scalar,'GLOB')) {
969 carp "Given a non-scalar reference, non-glob to globify_scalar; returning /dev/null handle";
972 return IO::File->new('/dev/null','>:encoding(UTF-8)');
975 =head2 cleanup_eval_fail()
977 print "Something failed with: ".cleanup_eval_fail($@);
979 Does various bits of cleanup on the failure message from an eval (or
980 any other die message)
982 Takes at most two options; the first is the actual failure message
983 (usually $@ and defaults to $@), the second is the debug level
984 (defaults to $DEBUG).
986 If debug is non-zero, the code at which the failure occured is output.
990 sub cleanup_eval_fail {
991 my ($error,$debug) = @_;
992 if (not defined $error or not @_) {
993 $error = $@ // 'unknown reason';
996 $debug = $DEBUG // 0;
998 $debug = 0 if not defined $debug;
1003 # ditch the "at foo/bar/baz.pm line 5"
1004 $error =~ s/\sat\s\S+\sline\s\d+//;
1005 # ditch croak messages
1006 $error =~ s/^\t+.+\n?//mg;
1007 # ditch trailing multiple periods in case there was a cascade of
1009 $error =~ s/\.+$/\./;
1015 hash_slice(%hash,qw(key1 key2 key3))
1017 For each key, returns matching values and keys of the hash if they exist
1022 # NB: We use prototypes here SPECIFICALLY so that we can be passed a
1023 # hash without uselessly making a reference to first. DO NOT USE
1024 # PROTOTYPES USELESSLY ELSEWHERE.
1025 sub hash_slice(\%@) {
1026 my ($hashref,@keys) = @_;
1027 return map {exists $hashref->{$_}?($_,$hashref->{$_}):()} @keys;