]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Common.pm
english_join needs to actually return and use params
[debbugs.git] / Debbugs / Common.pm
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.
5 #
6 # [Other people have contributed to this file; their copyrights should
7 # go here too.]
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
9
10 package Debbugs::Common;
11
12 =head1 NAME
13
14 Debbugs::Common -- Common routines for all of Debbugs
15
16 =head1 SYNOPSIS
17
18 use Debbugs::Common qw(:url :html);
19
20
21 =head1 DESCRIPTION
22
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.
26
27 =head1 FUNCTIONS
28
29 =cut
30
31 use warnings;
32 use strict;
33 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
34 use base qw(Exporter);
35
36 BEGIN{
37      $VERSION = 1.00;
38      $DEBUG = 0 unless defined $DEBUG;
39
40      @EXPORT = ();
41      %EXPORT_TAGS = (util   => [qw(getbugcomponent getbuglocation getlocationpath get_hashname),
42                                 qw(appendfile buglog getparsedaddrs getmaintainers),
43                                 qw(getmaintainers_reverse),
44                                 qw(getpseudodesc),
45                                ],
46                      misc   => [qw(make_list globify_scalar english_join checkpid)],
47                      date   => [qw(secs_to_english)],
48                      quit   => [qw(quit)],
49                      lock   => [qw(filelock unfilelock lockpid)],
50                     );
51      @EXPORT_OK = ();
52      Exporter::export_ok_tags(qw(lock quit date util misc));
53      $EXPORT_TAGS{all} = [@EXPORT_OK];
54 }
55
56 #use Debbugs::Config qw(:globals);
57
58 use Carp;
59
60 use Debbugs::Config qw(:config);
61 use IO::File;
62 use IO::Scalar;
63 use Debbugs::MIME qw(decode_rfc1522);
64 use Mail::Address;
65 use Cwd qw(cwd);
66
67 use Params::Validate qw(validate_with :types);
68
69 use Fcntl qw(:flock);
70
71 our $DEBUG_FH = \*STDERR if not defined $DEBUG_FH;
72
73 =head1 UTILITIES
74
75 The following functions are exported by the C<:util> tag
76
77 =head2 getbugcomponent
78
79      my $file = getbugcomponent($bug_number,$extension,$location)
80
81 Returns the path to the bug file in location C<$location>, bug number
82 C<$bugnumber> and extension C<$extension>
83
84 =cut
85
86 sub getbugcomponent {
87     my ($bugnum, $ext, $location) = @_;
88
89     if (not defined $location) {
90         $location = getbuglocation($bugnum, $ext);
91         # Default to non-archived bugs only for now; CGI scripts want
92         # archived bugs but most of the backend scripts don't. For now,
93         # anything that is prepared to accept archived bugs should call
94         # getbuglocation() directly first.
95         return undef if defined $location and
96                         ($location ne 'db' and $location ne 'db-h');
97     }
98     my $dir = getlocationpath($location);
99     return undef if not defined $dir;
100     if (defined $location and $location eq 'db') {
101         return "$dir/$bugnum.$ext";
102     } else {
103         my $hash = get_hashname($bugnum);
104         return "$dir/$hash/$bugnum.$ext";
105     }
106 }
107
108 =head2 getbuglocation
109
110      getbuglocation($bug_number,$extension)
111
112 Returns the the location in which a particular bug exists; valid
113 locations returned currently are archive, db-h, or db. If the bug does
114 not exist, returns undef.
115
116 =cut
117
118 sub getbuglocation {
119     my ($bugnum, $ext) = @_;
120     my $archdir = get_hashname($bugnum);
121     return 'archive' if -r getlocationpath('archive')."/$archdir/$bugnum.$ext";
122     return 'db-h' if -r getlocationpath('db-h')."/$archdir/$bugnum.$ext";
123     return 'db' if -r getlocationpath('db')."/$bugnum.$ext";
124     return undef;
125 }
126
127
128 =head2 getlocationpath
129
130      getlocationpath($location)
131
132 Returns the path to a specific location
133
134 =cut
135
136 sub getlocationpath {
137      my ($location) = @_;
138      if (defined $location and $location eq 'archive') {
139           return "$config{spool_dir}/archive";
140      } elsif (defined $location and $location eq 'db') {
141           return "$config{spool_dir}/db";
142      } else {
143           return "$config{spool_dir}/db-h";
144      }
145 }
146
147
148 =head2 get_hashname
149
150      get_hashname
151
152 Returns the hash of the bug which is the location within the archive
153
154 =cut
155
156 sub get_hashname {
157     return "" if ( $_[ 0 ] < 0 );
158     return sprintf "%02d", $_[ 0 ] % 100;
159 }
160
161 =head2 buglog
162
163      buglog($bugnum);
164
165 Returns the path to the logfile corresponding to the bug.
166
167 Returns undef if the bug does not exist.
168
169 =cut
170
171 sub buglog {
172     my $bugnum = shift;
173     my $location = getbuglocation($bugnum, 'log');
174     return getbugcomponent($bugnum, 'log', $location) if ($location);
175     $location = getbuglocation($bugnum, 'log.gz');
176     return getbugcomponent($bugnum, 'log.gz', $location) if ($location);
177     return undef;
178 }
179
180
181 =head2 appendfile
182
183      appendfile($file,'data','to','append');
184
185 Opens a file for appending and writes data to it.
186
187 =cut
188
189 sub appendfile {
190         my ($file,@data) = @_;
191         my $fh = IO::File->new($file,'a') or
192              die "Unable top open $file for appending: $!";
193         print {$fh} @data or die "Unable to write to $file: $!";
194         close $fh or die "Unable to close $file: $!";
195 }
196
197 =head2 getparsedaddrs
198
199      my $address = getparsedaddrs($address);
200      my @address = getparsedaddrs($address);
201
202 Returns the output from Mail::Address->parse, or the cached output if
203 this address has been parsed before. In SCALAR context returns the
204 first address parsed.
205
206 =cut
207
208
209 our %_parsedaddrs;
210 sub getparsedaddrs {
211     my $addr = shift;
212     return () unless defined $addr;
213     return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]
214          if exists $_parsedaddrs{$addr};
215     {
216          # don't display the warnings from Mail::Address->parse
217          local $SIG{__WARN__} = sub { };
218          @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
219     }
220     return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0];
221 }
222
223 =head2 getmaintainers
224
225      my $maintainer = getmaintainers()->{debbugs}
226
227 Returns a hashref of package => maintainer pairs.
228
229 =cut
230
231 our $_maintainer;
232 our $_maintainer_rev;
233 sub getmaintainers {
234     return $_maintainer if $_maintainer;
235     my %maintainer;
236     my %maintainer_rev;
237     for my $file (@config{qw(maintainer_file maintainer_file_override pseduo_maint_file)}) {
238          next unless defined $file;
239          my $maintfile = IO::File->new($file,'r') or
240               die "Unable to open maintainer file $file: $!";
241          while(<$maintfile>) {
242               next unless m/^(\S+)\s+(\S.*\S)\s*$/;
243               ($a,$b)=($1,$2);
244               $a =~ y/A-Z/a-z/;
245               $maintainer{$a}= $b;
246               for my $maint (map {lc($_->address)} getparsedaddrs($b)) {
247                    push @{$maintainer_rev{$maint}},$a;
248               }
249          }
250          close($maintfile);
251     }
252     $_maintainer = \%maintainer;
253     $_maintainer_rev = \%maintainer_rev;
254     return $_maintainer;
255 }
256
257 =head2 getmaintainers_reverse
258
259      my @packages = @{getmaintainers_reverse->{'don@debian.org'}||[]};
260
261 Returns a hashref of maintainer => [qw(list of packages)] pairs.
262
263 =cut
264
265 sub getmaintainers_reverse{
266      return $_maintainer_rev if $_maintainer_rev;
267      getmaintainers();
268      return $_maintainer_rev;
269 }
270
271 =head2 getpseudodesc
272
273      my $pseudopkgdesc = getpseudodesc(...);
274
275 Returns the entry for a pseudo package from the
276 $config{pseudo_desc_file}. In cases where pseudo_desc_file is not
277 defined, returns an empty arrayref.
278
279 This function can be used to see if a particular package is a
280 pseudopackage or not.
281
282 =cut
283
284 our $_pseudodesc;
285 sub getpseudodesc {
286     return $_pseudodesc if $_pseudodesc;
287     my %pseudodesc;
288
289     if (not defined $config{pseudo_desc_file}) {
290          $_pseudodesc = {};
291          return $_pseudodesc;
292     }
293     my $pseudo = IO::File->new($config{pseudo_desc_file},'r')
294          or die "Unable to open $config{pseudo_desc_file}: $!";
295     while(<$pseudo>) {
296         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
297         $pseudodesc{lc $1} = $2;
298     }
299     close($pseudo);
300     $_pseudodesc = \%pseudodesc;
301     return $_pseudodesc;
302 }
303
304
305 =head1 DATE
306
307     my $english = secs_to_english($seconds);
308     my ($days,$english) = secs_to_english($seconds);
309
310 XXX This should probably be changed to use Date::Calc
311
312 =cut
313
314 sub secs_to_english{
315      my ($seconds) = @_;
316
317      my $days = int($seconds / 86400);
318      my $years = int($days / 365);
319      $days %= 365;
320      my $result;
321      my @age;
322      push @age, "1 year" if ($years == 1);
323      push @age, "$years years" if ($years > 1);
324      push @age, "1 day" if ($days == 1);
325      push @age, "$days days" if ($days > 1);
326      $result .= join(" and ", @age);
327
328      return wantarray?(int($seconds/86400),$result):$result;
329 }
330
331
332 =head1 LOCK
333
334 These functions are exported with the :lock tag
335
336 =head2 filelock
337
338      filelock
339
340 FLOCKs the passed file. Use unfilelock to unlock it.
341
342 =cut
343
344 our @filelocks;
345
346 sub filelock {
347     # NB - NOT COMPATIBLE WITH `with-lock'
348     my ($lockfile) = @_;
349     if ($lockfile !~ m{^/}) {
350          $lockfile = cwd().'/'.$lockfile;
351     }
352     my ($count,$errors);
353     $count= 10; $errors= '';
354     for (;;) {
355         my $fh = eval {
356              my $fh2 = IO::File->new($lockfile,'w')
357                   or die "Unable to open $lockfile for writing: $!";
358              flock($fh2,LOCK_EX|LOCK_NB)
359                   or die "Unable to lock $lockfile $!";
360              return $fh2;
361         };
362         if ($@) {
363              $errors .= $@;
364         }
365         if ($fh) {
366              push @filelocks, {fh => $fh, file => $lockfile};
367              last;
368         }
369         if (--$count <=0) {
370             $errors =~ s/\n+$//;
371             die "failed to get lock on $lockfile -- $errors";
372         }
373         sleep 10;
374     }
375 }
376
377 # clean up all outstanding locks at end time
378 END {
379      while (@filelocks) {
380           unfilelock();
381      }
382 }
383
384
385 =head2 unfilelock
386
387      unfilelock()
388
389 Unlocks the file most recently locked.
390
391 Note that it is not currently possible to unlock a specific file
392 locked with filelock.
393
394 =cut
395
396 sub unfilelock {
397     if (@filelocks == 0) {
398         warn "unfilelock called with no active filelocks!\n";
399         return;
400     }
401     my %fl = %{pop(@filelocks)};
402     flock($fl{fh},LOCK_UN)
403          or warn "Unable to unlock lockfile $fl{file}: $!";
404     close($fl{fh})
405          or warn "Unable to close lockfile $fl{file}: $!";
406     unlink($fl{file})
407          or warn "Unable to unlink lockfile $fl{file}: $!";
408 }
409
410
411 =head2 lockpid
412
413       lockpid('/path/to/pidfile');
414
415 Creates a pidfile '/path/to/pidfile' if one doesn't exist or if the
416 pid in the file does not respond to kill 0.
417
418 Returns 1 on success, false on failure; dies on unusual errors.
419
420 =cut
421
422 sub lockpid {
423      my ($pidfile) = @_;
424      if (-e $pidfile) {
425           my $pid = checkpid($pidfile);
426           die "Unable to read pidfile $pidfile: $!" if not defined $pid;
427           return 0 if $pid != 0;
428           unlink $pidfile or
429                die "Unable to unlink stale pidfile $pidfile $!";
430      }
431      my $pidfh = IO::File->new($pidfile,'w') or
432           die "Unable to open $pidfile for writing: $!";
433      print {$pidfh} $$ or die "Unable to write to $pidfile $!";
434      close $pidfh or die "Unable to close $pidfile $!";
435      return 1;
436 }
437
438 =head2 checkpid
439
440      checkpid('/path/to/pidfile');
441
442 Checks a pid file and determines if the process listed in the pidfile
443 is still running. Returns the pid if it is, 0 if it isn't running, and
444 undef if the pidfile doesn't exist or cannot be read.
445
446 =cut
447
448 sub checkpid{
449      my ($pidfile) = @_;
450      if (-e $pidfile) {
451           my $pidfh = IO::File->new($pidfile, 'r') or
452                return undef;
453           local $/;
454           my $pid = <$pidfh>;
455           close $pidfh;
456           ($pid) = $pid =~ /(\d+)/;
457           if (defined $pid and kill(0,$pid)) {
458                return $pid;
459           }
460           return 0;
461      }
462      else {
463           return undef;
464      }
465 }
466
467
468 =head1 QUIT
469
470 These functions are exported with the :quit tag.
471
472 =head2 quit
473
474      quit()
475
476 Exits the program by calling die.
477
478 Usage of quit is deprecated; just call die instead.
479
480 =cut
481
482 sub quit {
483      print {$DEBUG_FH} "quitting >$_[0]<\n" if $DEBUG;
484      carp "quit() is deprecated; call die directly instead";
485 }
486
487
488 =head1 MISC
489
490 These functions are exported with the :misc tag
491
492 =head2 make_list
493
494      LIST = make_list(@_);
495
496 Turns a scalar or an arrayref into a list; expands a list of arrayrefs
497 into a list.
498
499 That is, make_list([qw(a b c)]); returns qw(a b c); make_list([qw(a
500 b)],[qw(c d)] returns qw(a b c d);
501
502 =cut
503
504 sub make_list {
505      return map {(ref($_) eq 'ARRAY')?@{$_}:$_} @_;
506 }
507
508
509 =head2 english_join
510
511      print english_join(list => \@list);
512      print english_join(\@list);
513
514 Joins list properly to make an english phrase.
515
516 =over
517
518 =item normal -- how to separate most values; defaults to ', '
519
520 =item last -- how to separate the last two values; defaults to ', and '
521
522 =item only_two -- how to separate only two values; defaults to ' and '
523
524 =item list -- ARRAYREF values to join; if the first argument is an
525 ARRAYREF, it's assumed to be the list of values to join
526
527 =back
528
529 In cases where C<list> is empty, returns ''; when there is only one
530 element, returns that element.
531
532 =cut
533
534 sub english_join {
535     if (ref $_[0] eq 'ARRAY') {
536         return english_join(list=>$_[0]);
537     }
538     my %param = validate_with(params => \@_,
539                               spec  => {normal => {type => SCALAR,
540                                                    default => ', ',
541                                                   },
542                                         last   => {type => SCALAR,
543                                                    default => ', and ',
544                                                   },
545                                         only_two => {type => SCALAR,
546                                                      default => ' and ',
547                                                     },
548                                         list     => {type => ARRAYREF,
549                                                     },
550                                        },
551                              );
552     my @list = @{$param{list}};
553     if (@list <= 1) {
554         return @list?$list[0]:'';
555     }
556     elsif (@list == 2) {
557         return join($param{only_two},@list);
558     }
559     my $ret = $param{last} . pop(@list);
560     return join($param{normal},@list) . $ret;
561 }
562
563
564 =head2 globify_scalar
565
566      my $handle = globify_scalar(\$foo);
567
568 if $foo isn't already a glob or a globref, turn it into one using
569 IO::Scalar. Gives a new handle to /dev/null if $foo isn't defined.
570
571 Will carp if given a scalar which isn't a scalarref or a glob (or
572 globref), and return /dev/null. May return undef if IO::Scalar or
573 IO::File fails. (Check $!)
574
575 =cut
576
577 sub globify_scalar {
578      my ($scalar) = @_;
579      my $handle;
580      if (defined $scalar) {
581           if (defined ref($scalar)) {
582                if (ref($scalar) eq 'SCALAR' and
583                    not UNIVERSAL::isa($scalar,'GLOB')) {
584                     return IO::Scalar->new($scalar);
585                }
586                else {
587                     return $scalar;
588                }
589           }
590           elsif (UNIVERSAL::isa(\$scalar,'GLOB')) {
591                return $scalar;
592           }
593           else {
594                carp "Given a non-scalar reference, non-glob to globify_scalar; returning /dev/null handle";
595           }
596      }
597      return IO::File->new('/dev/null','w');
598 }
599
600
601 1;
602
603 __END__