]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Common.pm
* Rip out the @cleanups; use an END{} instead
[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)],
47                      date   => [qw(secs_to_english)],
48                      quit   => [qw(quit)],
49                      lock   => [qw(filelock unfilelock @cleanups 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 Fcntl qw(:flock);
68
69 our $DEBUG_FH = \*STDERR if not defined $DEBUG_FH;
70
71 =head1 UTILITIES
72
73 The following functions are exported by the C<:util> tag
74
75 =head2 getbugcomponent
76
77      my $file = getbugcomponent($bug_number,$extension,$location)
78
79 Returns the path to the bug file in location C<$location>, bug number
80 C<$bugnumber> and extension C<$extension>
81
82 =cut
83
84 sub getbugcomponent {
85     my ($bugnum, $ext, $location) = @_;
86
87     if (not defined $location) {
88         $location = getbuglocation($bugnum, $ext);
89         # Default to non-archived bugs only for now; CGI scripts want
90         # archived bugs but most of the backend scripts don't. For now,
91         # anything that is prepared to accept archived bugs should call
92         # getbuglocation() directly first.
93         return undef if defined $location and
94                         ($location ne 'db' and $location ne 'db-h');
95     }
96     my $dir = getlocationpath($location);
97     return undef if not defined $dir;
98     if (defined $location and $location eq 'db') {
99         return "$dir/$bugnum.$ext";
100     } else {
101         my $hash = get_hashname($bugnum);
102         return "$dir/$hash/$bugnum.$ext";
103     }
104 }
105
106 =head2 getbuglocation
107
108      getbuglocation($bug_number,$extension)
109
110 Returns the the location in which a particular bug exists; valid
111 locations returned currently are archive, db-h, or db. If the bug does
112 not exist, returns undef.
113
114 =cut
115
116 sub getbuglocation {
117     my ($bugnum, $ext) = @_;
118     my $archdir = get_hashname($bugnum);
119     return 'archive' if -r getlocationpath('archive')."/$archdir/$bugnum.$ext";
120     return 'db-h' if -r getlocationpath('db-h')."/$archdir/$bugnum.$ext";
121     return 'db' if -r getlocationpath('db')."/$bugnum.$ext";
122     return undef;
123 }
124
125
126 =head2 getlocationpath
127
128      getlocationpath($location)
129
130 Returns the path to a specific location
131
132 =cut
133
134 sub getlocationpath {
135      my ($location) = @_;
136      if (defined $location and $location eq 'archive') {
137           return "$config{spool_dir}/archive";
138      } elsif (defined $location and $location eq 'db') {
139           return "$config{spool_dir}/db";
140      } else {
141           return "$config{spool_dir}/db-h";
142      }
143 }
144
145
146 =head2 get_hashname
147
148      get_hashname
149
150 Returns the hash of the bug which is the location within the archive
151
152 =cut
153
154 sub get_hashname {
155     return "" if ( $_[ 0 ] < 0 );
156     return sprintf "%02d", $_[ 0 ] % 100;
157 }
158
159 =head2 buglog
160
161      buglog($bugnum);
162
163 Returns the path to the logfile corresponding to the bug.
164
165 =cut
166
167 sub buglog {
168     my $bugnum = shift;
169     my $location = getbuglocation($bugnum, 'log');
170     return getbugcomponent($bugnum, 'log', $location) if ($location);
171     $location = getbuglocation($bugnum, 'log.gz');
172     return getbugcomponent($bugnum, 'log.gz', $location);
173 }
174
175
176 =head2 appendfile
177
178      appendfile($file,'data','to','append');
179
180 Opens a file for appending and writes data to it.
181
182 =cut
183
184 sub appendfile {
185         my ($file,@data) = @_;
186         my $fh = IO::File->new($file,'a') or
187              die "Unable top open $file for appending: $!";
188         print {$fh} @data or die "Unable to write to $file: $!";
189         close $fh or die "Unable to close $file: $!";
190 }
191
192 =head2 getparsedaddrs
193
194      my $address = getparsedaddrs($address);
195      my @address = getparsedaddrs($address);
196
197 Returns the output from Mail::Address->parse, or the cached output if
198 this address has been parsed before. In SCALAR context returns the
199 first address parsed.
200
201 =cut
202
203
204 our %_parsedaddrs;
205 sub getparsedaddrs {
206     my $addr = shift;
207     return () unless defined $addr;
208     return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]
209          if exists $_parsedaddrs{$addr};
210     {
211          # don't display the warnings from Mail::Address->parse
212          local $SIG{__WARN__} = sub { };
213          @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
214     }
215     return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0];
216 }
217
218 =head2 getmaintainers
219
220      my $maintainer = getmaintainers()->{debbugs}
221
222 Returns a hashref of package => maintainer pairs.
223
224 =cut
225
226 our $_maintainer;
227 our $_maintainer_rev;
228 sub getmaintainers {
229     return $_maintainer if $_maintainer;
230     my %maintainer;
231     my %maintainer_rev;
232     for my $file (@config{qw(maintainer_file maintainer_file_override pseduo_maint_file)}) {
233          next unless defined $file;
234          my $maintfile = IO::File->new($file,'r') or
235               die "Unable to open maintainer file $file: $!");
236          while(<$maintfile>) {
237               next unless m/^(\S+)\s+(\S.*\S)\s*$/;
238               ($a,$b)=($1,$2);
239               $a =~ y/A-Z/a-z/;
240               $maintainer{$a}= $b;
241               for my $maint (map {lc($_->address)} getparsedaddrs($b)) {
242                    push @{$maintainer_rev{$maint}},$a;
243               }
244          }
245          close($maintfile);
246     }
247     $_maintainer = \%maintainer;
248     $_maintainer_rev = \%maintainer_rev;
249     return $_maintainer;
250 }
251
252 =head2 getmaintainers_reverse
253
254      my @packages = @{getmaintainers_reverse->{'don@debian.org'}||[]};
255
256 Returns a hashref of maintainer => [qw(list of packages)] pairs.
257
258 =cut
259
260 sub getmaintainers_reverse{
261      return $_maintainer_rev if $_maintainer_rev;
262      getmaintainers();
263      return $_maintainer_rev;
264 }
265
266 =head2 getpseudodesc
267
268      my $pseudopkgdesc = getpseudodesc(...);
269
270 Returns the entry for a pseudo package from the
271 $config{pseudo_desc_file}. In cases where pseudo_desc_file is not
272 defined, returns an empty arrayref.
273
274 This function can be used to see if a particular package is a
275 pseudopackage or not.
276
277 =cut
278
279 our $_pseudodesc;
280 sub getpseudodesc {
281     return $_pseudodesc if $_pseudodesc;
282     my %pseudodesc;
283
284     if (not defined $config{pseudo_desc_file}) {
285          $_pseudodesc = {};
286          return $_pseudodesc;
287     }
288     my $pseudo = IO::File->new($config{pseudo_desc_file},'r')
289          or die "Unable to open $config{pseudo_desc_file}: $!";
290     while(<$pseudo>) {
291         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
292         $pseudodesc{lc $1} = $2;
293     }
294     close($pseudo);
295     $_pseudodesc = \%pseudodesc;
296     return $_pseudodesc;
297 }
298
299
300 =head1 DATE
301
302     my $english = secs_to_english($seconds);
303     my ($days,$english) = secs_to_english($seconds);
304
305 XXX This should probably be changed to use Date::Calc
306
307 =cut
308
309 sub secs_to_english{
310      my ($seconds) = @_;
311
312      my $days = int($seconds / 86400);
313      my $years = int($days / 365);
314      $days %= 365;
315      my $result;
316      my @age;
317      push @age, "1 year" if ($years == 1);
318      push @age, "$years years" if ($years > 1);
319      push @age, "1 day" if ($days == 1);
320      push @age, "$days days" if ($days > 1);
321      $result .= join(" and ", @age);
322
323      return wantarray?(int($seconds/86400),$result):$result;
324 }
325
326
327 =head1 LOCK
328
329 These functions are exported with the :lock tag
330
331 =head2 filelock
332
333      filelock
334
335 FLOCKs the passed file. Use unfilelock to unlock it.
336
337 =cut
338
339 our @filelocks;
340
341 sub filelock {
342     # NB - NOT COMPATIBLE WITH `with-lock'
343     my ($lockfile) = @_;
344     if ($lockfile !~ m{^/}) {
345          $lockfile = cwd().'/'.$lockfile;
346     }
347     my ($count,$errors);
348     $count= 10; $errors= '';
349     for (;;) {
350         my $fh = eval {
351              my $fh2 = IO::File->new($lockfile,'w')
352                   or die "Unable to open $lockfile for writing: $!";
353              flock($fh2,LOCK_EX|LOCK_NB)
354                   or die "Unable to lock $lockfile $!";
355              return $fh2;
356         };
357         if ($@) {
358              $errors .= $@;
359         }
360         if ($fh) {
361              push @filelocks, {fh => $fh, file => $lockfile};
362              last;
363         }
364         if (--$count <=0) {
365             $errors =~ s/\n+$//;
366             die "failed to get lock on $lockfile -- $errors";
367         }
368         sleep 10;
369     }
370 }
371
372 # clean up all outstanding locks at end time
373 END {
374      while (@filelocks) {
375           unfilelock();
376      }
377 }
378
379
380 =head2 unfilelock
381
382      unfilelock()
383
384 Unlocks the file most recently locked.
385
386 Note that it is not currently possible to unlock a specific file
387 locked with filelock.
388
389 =cut
390
391 sub unfilelock {
392     if (@filelocks == 0) {
393         warn "unfilelock called with no active filelocks!\n";
394         return;
395     }
396     my %fl = %{pop(@filelocks)};
397     flock($fl{fh},LOCK_UN)
398          or warn "Unable to unlock lockfile $fl{file}: $!";
399     close($fl{fh})
400          or warn "Unable to close lockfile $fl{file}: $!";
401     unlink($fl{file})
402          or warn "Unable to unlink lockfile $fl{file}: $!";
403 }
404
405
406 =head2 lockpid
407
408       lockpid('/path/to/pidfile');
409
410 Creates a pidfile '/path/to/pidfile' if one doesn't exist or if the
411 pid in the file does not respond to kill 0.
412
413 Returns 1 on success, false on failure; dies on unusual errors.
414
415 =cut
416
417 sub lockpid {
418      my ($pidfile) = @_;
419      if (-e $pidfile) {
420           my $pidfh = IO::File->new($pidfile, 'r') or
421                die "Unable to open pidfile $pidfile: $!";
422           local $/;
423           my $pid = <$pidfh>;
424           ($pid) = $pid =~ /(\d+)/;
425           if (defined $pid and kill(0,$pid)) {
426                return 0;
427           }
428           close $pidfh;
429           unlink $pidfile or
430                die "Unable to unlink stale pidfile $pidfile $!";
431      }
432      my $pidfh = IO::File->new($pidfile,'w') or
433           die "Unable to open $pidfile for writing: $!";
434      print {$pidfh} $$ or die "Unable to write to $pidfile $!";
435      close $pidfh or die "Unable to close $pidfile $!";
436      return 1;
437 }
438
439
440 =head1 QUIT
441
442 These functions are exported with the :quit tag.
443
444 =head2 quit
445
446      quit()
447
448 Exits the program by calling die.
449
450 Usage of quit is deprecated; just call die instead.
451
452 =cut
453
454 sub quit {
455      print {$DEBUG_FH} "quitting >$_[0]<\n" if $DEBUG;
456      carp "quit() is deprecated; call die directly instead";
457 }
458
459
460 =head1 MISC
461
462 These functions are exported with the :misc tag
463
464 =head2 make_list
465
466      LIST = make_list(@_);
467
468 Turns a scalar or an arrayref into a list; expands a list of arrayrefs
469 into a list.
470
471 That is, make_list([qw(a b c)]); returns qw(a b c); make_list([qw(a
472 b)],[qw(c d)] returns qw(a b c d);
473
474 =cut
475
476 sub make_list {
477      return map {(ref($_) eq 'ARRAY')?@{$_}:$_} @_;
478 }
479
480
481 =head2 globify_scalar
482
483      my $handle = globify_scalar(\$foo);
484
485 if $foo isn't already a glob or a globref, turn it into one using
486 IO::Scalar. Gives a new handle to /dev/null if $foo isn't defined.
487
488 Will carp if given a scalar which isn't a scalarref or a glob (or
489 globref), and return /dev/null. May return undef if IO::Scalar or
490 IO::File fails. (Check $!)
491
492 =cut
493
494 sub globify_scalar {
495      my ($scalar) = @_;
496      my $handle;
497      if (defined $scalar) {
498           if (defined ref($scalar)) {
499                if (ref($scalar) eq 'SCALAR' and
500                    not UNIVERSAL::isa($scalar,'GLOB')) {
501                     return IO::Scalar->new($scalar);
502                }
503                else {
504                     return $scalar;
505                }
506           }
507           elsif (UNIVERSAL::isa(\$scalar,'GLOB')) {
508                return $scalar;
509           }
510           else {
511                carp "Given a non-scalar reference, non-glob to globify_scalar; returning /dev/null handle";
512           }
513      }
514      return IO::File->new('/dev/null','w');
515 }
516
517
518 1;
519
520 __END__