]> git.donarmstrong.com Git - debbugs.git/blob - Debbugs/Common.pm
* Split out a new checkpid function
[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 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         # if there is no location, the bug doesn't exist
96         return undef if not defined $location;
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 =cut
168
169 sub buglog {
170     my $bugnum = shift;
171     my $location = getbuglocation($bugnum, 'log');
172     return getbugcomponent($bugnum, 'log', $location) if ($location);
173     $location = getbuglocation($bugnum, 'log.gz');
174     return getbugcomponent($bugnum, 'log.gz', $location);
175 }
176
177
178 =head2 appendfile
179
180      appendfile($file,'data','to','append');
181
182 Opens a file for appending and writes data to it.
183
184 =cut
185
186 sub appendfile {
187         my ($file,@data) = @_;
188         my $fh = IO::File->new($file,'a') or
189              die "Unable top open $file for appending: $!";
190         print {$fh} @data or die "Unable to write to $file: $!";
191         close $fh or die "Unable to close $file: $!";
192 }
193
194 =head2 getparsedaddrs
195
196      my $address = getparsedaddrs($address);
197      my @address = getparsedaddrs($address);
198
199 Returns the output from Mail::Address->parse, or the cached output if
200 this address has been parsed before. In SCALAR context returns the
201 first address parsed.
202
203 =cut
204
205
206 our %_parsedaddrs;
207 sub getparsedaddrs {
208     my $addr = shift;
209     return () unless defined $addr;
210     return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]
211          if exists $_parsedaddrs{$addr};
212     {
213          # don't display the warnings from Mail::Address->parse
214          local $SIG{__WARN__} = sub { };
215          @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
216     }
217     return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0];
218 }
219
220 =head2 getmaintainers
221
222      my $maintainer = getmaintainers()->{debbugs}
223
224 Returns a hashref of package => maintainer pairs.
225
226 =cut
227
228 our $_maintainer;
229 our $_maintainer_rev;
230 sub getmaintainers {
231     return $_maintainer if $_maintainer;
232     my %maintainer;
233     my %maintainer_rev;
234     for my $file (@config{qw(maintainer_file maintainer_file_override pseduo_maint_file)}) {
235          next unless defined $file;
236          my $maintfile = IO::File->new($file,'r') or
237               die "Unable to open maintainer file $file: $!";
238          while(<$maintfile>) {
239               next unless m/^(\S+)\s+(\S.*\S)\s*$/;
240               ($a,$b)=($1,$2);
241               $a =~ y/A-Z/a-z/;
242               $maintainer{$a}= $b;
243               for my $maint (map {lc($_->address)} getparsedaddrs($b)) {
244                    push @{$maintainer_rev{$maint}},$a;
245               }
246          }
247          close($maintfile);
248     }
249     $_maintainer = \%maintainer;
250     $_maintainer_rev = \%maintainer_rev;
251     return $_maintainer;
252 }
253
254 =head2 getmaintainers_reverse
255
256      my @packages = @{getmaintainers_reverse->{'don@debian.org'}||[]};
257
258 Returns a hashref of maintainer => [qw(list of packages)] pairs.
259
260 =cut
261
262 sub getmaintainers_reverse{
263      return $_maintainer_rev if $_maintainer_rev;
264      getmaintainers();
265      return $_maintainer_rev;
266 }
267
268 =head2 getpseudodesc
269
270      my $pseudopkgdesc = getpseudodesc(...);
271
272 Returns the entry for a pseudo package from the
273 $config{pseudo_desc_file}. In cases where pseudo_desc_file is not
274 defined, returns an empty arrayref.
275
276 This function can be used to see if a particular package is a
277 pseudopackage or not.
278
279 =cut
280
281 our $_pseudodesc;
282 sub getpseudodesc {
283     return $_pseudodesc if $_pseudodesc;
284     my %pseudodesc;
285
286     if (not defined $config{pseudo_desc_file}) {
287          $_pseudodesc = {};
288          return $_pseudodesc;
289     }
290     my $pseudo = IO::File->new($config{pseudo_desc_file},'r')
291          or die "Unable to open $config{pseudo_desc_file}: $!";
292     while(<$pseudo>) {
293         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
294         $pseudodesc{lc $1} = $2;
295     }
296     close($pseudo);
297     $_pseudodesc = \%pseudodesc;
298     return $_pseudodesc;
299 }
300
301
302 =head1 DATE
303
304     my $english = secs_to_english($seconds);
305     my ($days,$english) = secs_to_english($seconds);
306
307 XXX This should probably be changed to use Date::Calc
308
309 =cut
310
311 sub secs_to_english{
312      my ($seconds) = @_;
313
314      my $days = int($seconds / 86400);
315      my $years = int($days / 365);
316      $days %= 365;
317      my $result;
318      my @age;
319      push @age, "1 year" if ($years == 1);
320      push @age, "$years years" if ($years > 1);
321      push @age, "1 day" if ($days == 1);
322      push @age, "$days days" if ($days > 1);
323      $result .= join(" and ", @age);
324
325      return wantarray?(int($seconds/86400),$result):$result;
326 }
327
328
329 =head1 LOCK
330
331 These functions are exported with the :lock tag
332
333 =head2 filelock
334
335      filelock
336
337 FLOCKs the passed file. Use unfilelock to unlock it.
338
339 =cut
340
341 our @filelocks;
342
343 sub filelock {
344     # NB - NOT COMPATIBLE WITH `with-lock'
345     my ($lockfile) = @_;
346     if ($lockfile !~ m{^/}) {
347          $lockfile = cwd().'/'.$lockfile;
348     }
349     my ($count,$errors);
350     $count= 10; $errors= '';
351     for (;;) {
352         my $fh = eval {
353              my $fh2 = IO::File->new($lockfile,'w')
354                   or die "Unable to open $lockfile for writing: $!";
355              flock($fh2,LOCK_EX|LOCK_NB)
356                   or die "Unable to lock $lockfile $!";
357              return $fh2;
358         };
359         if ($@) {
360              $errors .= $@;
361         }
362         if ($fh) {
363              push @filelocks, {fh => $fh, file => $lockfile};
364              last;
365         }
366         if (--$count <=0) {
367             $errors =~ s/\n+$//;
368             die "failed to get lock on $lockfile -- $errors";
369         }
370         sleep 10;
371     }
372 }
373
374 # clean up all outstanding locks at end time
375 END {
376      while (@filelocks) {
377           unfilelock();
378      }
379 }
380
381
382 =head2 unfilelock
383
384      unfilelock()
385
386 Unlocks the file most recently locked.
387
388 Note that it is not currently possible to unlock a specific file
389 locked with filelock.
390
391 =cut
392
393 sub unfilelock {
394     if (@filelocks == 0) {
395         warn "unfilelock called with no active filelocks!\n";
396         return;
397     }
398     my %fl = %{pop(@filelocks)};
399     flock($fl{fh},LOCK_UN)
400          or warn "Unable to unlock lockfile $fl{file}: $!";
401     close($fl{fh})
402          or warn "Unable to close lockfile $fl{file}: $!";
403     unlink($fl{file})
404          or warn "Unable to unlink lockfile $fl{file}: $!";
405 }
406
407
408 =head2 lockpid
409
410       lockpid('/path/to/pidfile');
411
412 Creates a pidfile '/path/to/pidfile' if one doesn't exist or if the
413 pid in the file does not respond to kill 0.
414
415 Returns 1 on success, false on failure; dies on unusual errors.
416
417 =cut
418
419 sub lockpid {
420      my ($pidfile) = @_;
421      if (-e $pidfile) {
422           my $pid = checkpid($pidfile);
423           die "Unable to read pidfile $pidfile: $!" if not defined $pid;
424           return 0 if $pid != 0;
425           unlink $pidfile or
426                die "Unable to unlink stale pidfile $pidfile $!";
427      }
428      my $pidfh = IO::File->new($pidfile,'w') or
429           die "Unable to open $pidfile for writing: $!";
430      print {$pidfh} $$ or die "Unable to write to $pidfile $!";
431      close $pidfh or die "Unable to close $pidfile $!";
432      return 1;
433 }
434
435 =head2 checkpid
436
437      checkpid('/path/to/pidfile');
438
439 Checks a pid file and determines if the process listed in the pidfile
440 is still running. Returns the pid if it is, 0 if it isn't running, and
441 undef if the pidfile doesn't exist or cannot be read.
442
443 =cut
444
445 sub checkpid{
446      my ($pidfile) = @_;
447      if (-e $pidfile) {
448           my $pidfh = IO::File->new($pidfile, 'r') or
449                return undef;
450           local $/;
451           my $pid = <$pidfh>;
452           close $pidfh;
453           ($pid) = $pid =~ /(\d+)/;
454           if (defined $pid and kill(0,$pid)) {
455                return $pid;
456           }
457           return 0;
458      }
459      else {
460           return undef;
461      }
462 }
463
464
465 =head1 QUIT
466
467 These functions are exported with the :quit tag.
468
469 =head2 quit
470
471      quit()
472
473 Exits the program by calling die.
474
475 Usage of quit is deprecated; just call die instead.
476
477 =cut
478
479 sub quit {
480      print {$DEBUG_FH} "quitting >$_[0]<\n" if $DEBUG;
481      carp "quit() is deprecated; call die directly instead";
482 }
483
484
485 =head1 MISC
486
487 These functions are exported with the :misc tag
488
489 =head2 make_list
490
491      LIST = make_list(@_);
492
493 Turns a scalar or an arrayref into a list; expands a list of arrayrefs
494 into a list.
495
496 That is, make_list([qw(a b c)]); returns qw(a b c); make_list([qw(a
497 b)],[qw(c d)] returns qw(a b c d);
498
499 =cut
500
501 sub make_list {
502      return map {(ref($_) eq 'ARRAY')?@{$_}:$_} @_;
503 }
504
505
506 =head2 english_join
507
508      print english_join(', ',' and ',@list);
509
510 Joins list properly to make an english phrase.
511
512
513
514 =cut
515
516 sub english_join {
517      my ($normal,$last,@list) = @_;
518      if (@list <= 1) {
519           return @list?$list[0]:'';
520      }
521      my $ret = $last . pop(@list);
522      $ret = join($normal,@list) . $ret;
523      return $ret;
524 }
525
526
527 =head2 globify_scalar
528
529      my $handle = globify_scalar(\$foo);
530
531 if $foo isn't already a glob or a globref, turn it into one using
532 IO::Scalar. Gives a new handle to /dev/null if $foo isn't defined.
533
534 Will carp if given a scalar which isn't a scalarref or a glob (or
535 globref), and return /dev/null. May return undef if IO::Scalar or
536 IO::File fails. (Check $!)
537
538 =cut
539
540 sub globify_scalar {
541      my ($scalar) = @_;
542      my $handle;
543      if (defined $scalar) {
544           if (defined ref($scalar)) {
545                if (ref($scalar) eq 'SCALAR' and
546                    not UNIVERSAL::isa($scalar,'GLOB')) {
547                     return IO::Scalar->new($scalar);
548                }
549                else {
550                     return $scalar;
551                }
552           }
553           elsif (UNIVERSAL::isa(\$scalar,'GLOB')) {
554                return $scalar;
555           }
556           else {
557                carp "Given a non-scalar reference, non-glob to globify_scalar; returning /dev/null handle";
558           }
559      }
560      return IO::File->new('/dev/null','w');
561 }
562
563
564 1;
565
566 __END__