2 # local-debbugs is part of debbugs, and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2008 by Don Armstrong <don@donarmstrong.com>.
11 use Getopt::Long qw(:config no_ignore_case);
16 local-debbugs - use a local mirror of debbugs
23 --mirror, -M update local mirror
24 --daemon, -D start the daemon
25 --search, -S run a search
27 --debug, -d debugging level (Default 0)
28 --help, -h display this help
29 --man, -m display manual
37 Update the local mirror of debbugs bugs
41 Start up the daemon on the configured local port to serve bugs which
42 have been previously retrieved.
46 Cause the running daemon to show the pkgreport.cgi page corresponding
47 to the search by invoking sensible-browser and an appropriate url.
51 Cause the running daemon to show the bugreport.cgi page corresponding
52 to the bug by invoking sensible-browser and an appropriate url.
56 The port that the daemon is running on (or will be running on.)
58 Defaults to the value of the currently running daemon, the value in
59 the configuration file, or 8080 if nothing is set.
61 =item B<--bugs-to-get>
63 File which contains the set of bugs to get.
64 Defaults to ~/.debbugs/bugs_to_get
68 Hostname for a site which is running a debbugs install.
69 Defaults to bugs.debian.org
73 Hostname for a site which is running an rsyncable mirror of the
74 debbugs install above.
75 Defaults to bugs-mirror.debian.org
83 Display brief useage information.
95 =item Update the local mirror
97 local-debbugs --mirror
99 =item Start up the local-debbugs daemon
101 local-debbugs --daemon
103 =item Search for bugs with severity serious
105 local-debbugs --search severity:serious
116 use File::Temp qw(tempdir);
117 use Params::Validate qw(validate_with :types);
119 use Debbugs::Common qw(checkpid lockpid get_hashname);
120 use Debbugs::Mail qw(get_addresses);
127 my %options = (debug => 0,
133 cgi_bin => '/var/lib/debbugs/www/cgi',
134 css => '/var/lib/debbugs/www/bugs.css',
135 bug_site => 'bugs.debian.org',
136 bug_mirror => 'bugs-mirror.debian.org',
139 my %option_defaults = (port => 8080,
140 debbugs_config => User->Home.'/.debbugs/debbugs_config',
141 mirror_location => User->Home.'/.debbugs/mirror',
142 bugs_to_get => User->Home.'/.debbugs/bugs_to_get',
145 GetOptions(\%options,
146 'daemon|D','show|s','search|select|S','mirror|M', 'stop',
148 'css=s','cgi_bin|cgi-bin|cgi=s',
149 'verbose|v+','quiet|q+',
150 'bug_site|bug-site=s',
151 'bug_mirror|bug-mirror=s',
152 'debug|d+','help|h|?','man|m');
154 pod2usage() if $options{help};
155 pod2usage({verbose=>2}) if $options{man};
157 $DEBUG = $options{debug};
160 if (1 != grep {exists $options{$_}} qw(daemon show search mirror stop)) {
161 push @USAGE_ERRORS,"You must pass one (and only one) of --daemon --show --search or --mirror";
163 $options{verbose} = $options{verbose} - $options{quiet};
165 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
168 # munge in local configuration
170 local_config(\%options);
172 mkpath($options{mirror_location});
174 if ($options{daemon}) {
175 # daemonize, do stuff
176 my $pid = checkpid($options{mirror_location}.'/local-debbugs.pid');
177 if (defined $pid and $pid != 0) {
178 print STDERR "Unable to start daemon; it's already running\n";
181 if (-e $options{mirror_location}.'/local-debbugs.pid' and
183 print STDERR "Unable to determine if daemon is running: $!\n";
186 # ok, now lets daemonize
188 # XXX make sure that all paths have been turned into absolute
190 chdir '/' or die "Can't chdir to /: $!";
191 # allow us not to detach for debugging
192 if ($options{detach}) {
193 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
194 open STDOUT, '>/dev/null'
195 or die "Can't write to /dev/null: $!";
196 defined(my $pid = fork) or die "Can't fork: $!";
198 setsid or die "Can't start a new session: $!";
199 open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
201 lockpid($options{mirror_location}.'/local-debbugs.pid') or
202 die "Unable to deal with the pidfile";
203 # this is the subclass of HTTP::Server::Simple::CGI which handles
204 # the "hard" bits of actually running a tiny webserver for us
206 package local_debbugs::server;
208 use HTTP::Server::Simple;
209 use base qw(HTTP::Server::Simple::CGI);
212 return 'Net::Server::Fork';
217 print "HTTP/1.1 302 Found\r\n";
218 print "Location: $url\r\n";
221 # here we want to call cgi-bin/pkgreport or cgi-bin/bugreport
223 my ($self,$cgi) = @_;
225 my $base_uri = 'http://'.$cgi->virtual_host;
226 if ($cgi->virtual_port ne 80) {
227 $base_uri .= ':'.$cgi->virtual_port;
229 my $path = $cgi->path_info();
230 # RewriteRule ^/[[:space:]]*#?([[:digit:]][[:digit:]][[:digit:]]+)([;&].+)?$ /cgi-bin/bugreport.cgi?bug=$1$2 [L,R,NE]
231 if ($path =~ m{^/?\s*\#?(\d+)((?:[;&].+)?)$}) {
232 redirect($cgi,$base_uri."/cgi-bin/bugreport.cgi?bug=$1$2");
234 # RewriteRule ^/[Ff][Rr][Oo][Mm]:([^/]+\@.+)$ /cgi-bin/pkgreport.cgi?submitter=$1 [L,R,NE]
235 elsif ($path =~ m{^/?\s*from:([^/]+\@.+)$}i) {
236 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?submitter=$1");
238 # RewriteRule ^/([^/]+\@.+)$ /cgi-bin/pkgreport.cgi?maint=$1 [L,R,NE]
239 elsif ($path =~ m{^/?\s*([^/]+\@.+)$}i) {
240 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?maint=$1");
242 # RewriteRule ^/mbox:([[:digit:]][[:digit:]][[:digit:]]+)([;&].+)?$ /cgi-bin/bugreport.cgi?mbox=yes&bug=$1$2 [L,R,NE]
243 elsif ($path =~ m{^/?\s*mbox:\#?(\d+)((?:[;&].+)?)$}i) {
244 redirect($cgi,$base_uri."/cgi-bin/bugreport.cgi?mbox=yes;bug=$1$2");
246 # RewriteRule ^/src:([^/]+)$ /cgi-bin/pkgreport.cgi?src=$1 [L,R,NE]
247 elsif ($path =~ m{^/?src:([^/]+)$}i) {
248 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?src=$1");
250 # RewriteRule ^/severity:([^/]+)$ /cgi-bin/pkgreport.cgi?severity=$1 [L,R,NE]
251 elsif ($path =~ m{^/?severity:([^/]+)$}i) {
252 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?severity=$1");
254 # RewriteRule ^/tag:([^/]+)$ /cgi-bin/pkgreport.cgi?tag=$1 [L,R,NE]
255 elsif ($path =~ m{^/?tag:([^/]+)$}i) {
256 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?tag=$1");
258 # RewriteRule ^/([^/]+)$ /cgi-bin/pkgreport.cgi?pkg=$1 [L,R,NE]
259 elsif ($path =~ m{^/?([^/]+)$}i) {
260 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?pkg=$1");
262 elsif ($path =~ m{^/?cgi(?:-bin)?/((?:(?:bug|pkg)report|version)\.cgi)}) {
263 # dispatch to pkgreport.cgi
264 print "HTTP/1.1 200 OK\n";
265 exec("$options{cgi_bin}/$1") or
266 die "Unable to execute $options{cgi_bin}/$1";
268 elsif ($path =~ m{^/?css/bugs.css}) {
269 my $fh = IO::File->new($options{css},'r') or
270 die "Unable to open $options{css} for reading: $!";
271 print "HTTP/1.1 200 OK\n";
272 print "Content-type: text/css\n";
276 elsif ($path =~ m{^/?$}) {
277 redirect($cgi,$base_uri."/cgi-bin/pkgreport.cgi?package=put%20package%20here");
280 print "HTTP/1.1 404 Not Found\n";
281 print "Content-Type: text/html\n";
283 print "<h1>That which you were seeking, found I have not.</h1>\n";
285 # RewriteRule ^/$ /Bugs/ [L,R,NE]
288 my $debbugs_server = local_debbugs::server->new($options{port}) or
289 die "Unable to create debbugs server";
290 $debbugs_server->run() or
291 die 'Unable to run debbugs server';
293 elsif ($options{stop}) {
295 my $pid = checkpid($options{mirror_location}.'/local-debbugs.pid');
296 if (not defined $pid or $pid == 0) {
297 print STDERR "Unable to open pidfile or daemon not running: $!\n";
300 exit !(kill(15,$pid) == 1);
302 elsif ($options{mirror}) {
303 # run the mirror jobies
304 # figure out which bugs we need
305 my $bugs = select_bugs(\%options);
307 my $tempdir = tempdir();#CLEANUP => 1);
308 my $mirror_log = IO::File->new($options{mirror_location}.'/mirror.log','>') or
309 die "Unable to open $options{mirror_location}/mirror.log for writing: $!";
310 write_bug_list("$tempdir/unarchived_bug_list",$bugs->{unarchived});
311 write_bug_list("$tempdir/archived_bug_list",$bugs->{archived});
313 my @common_rsync_options = ('-avz','--partial');
314 print "Rsyncing bugs\n" if not $options{quiet};
315 run_rsync(log => $mirror_log,
316 ($options{debug}?(debug => \*STDERR):()),
317 options => [@common_rsync_options,
319 '--files-from',"$tempdir/unarchived_bug_list",
320 'rsync://'.$options{bug_mirror}.'/bts-spool-db/',
321 $options{mirror_location}.'/db-h/']
323 print "Rsyncing archived bugs\n" if $options{verbose};
324 run_rsync(log => $mirror_log,
325 ($options{debug}?(debug => \*STDERR):()),
326 options => [@common_rsync_options,
328 '--files-from',"$tempdir/archived_bug_list",
329 'rsync://'.$options{bug_mirror}.'/bts-spool-archive/',
330 $options{mirror_location}.'/archive/',
333 print "Rsyncing indexes\n" if $options{verbose};
334 run_rsync(log => $mirror_log,
335 ($options{debug}?(debug => \*STDERR):()),
336 options => [@common_rsync_options,
339 '--exclude','by-reverse*',
340 'rsync://'.$options{bug_mirror}.'/bts-spool-index/',
341 $options{mirror_location}.'/',
344 print "Rsyncing versions\n" if $options{verbose};
345 run_rsync(log => $mirror_log,
346 ($options{debug}?(debug => \*STDERR):()),
347 options => [@common_rsync_options,
351 'rsync://'.$options{bug_mirror}.'/bts-versions/',
352 $options{mirror_location}.'/versions/',
356 elsif ($options{show}) {
358 # see if the daemon is running
359 my $pid = checkpid($options{mirror_location}.'/local-debbugs.pid');
360 if (not defined $pid or $pid == 0) {
361 print STDERR "Unable to open pidfile or daemon not running: $!\n";
362 print STDERR qq(Mr. T: "I pity da fool who tries to show a bug without a running daemon"\n);
363 print STDERR "Hint: try the --daemon option first\n";
367 my $url = qq(http://localhost:$options{port}/$ARGV[0]);
368 exec('/usr/bin/sensible-browser',$url) or
369 die "Unable to run sensible-browser (try feeding me cheetos?)";
371 elsif ($options{search}) {
372 my $url = qq(http://localhost:$options{port}/cgi-bin/pkgreport.cgi?).
373 join(';',map {if (/:/) {s/:/=/; $_;} else {qq(pkg=$_);}} @ARGV);
374 my $pid = checkpid($options{mirror_location}.'/local-debbugs.pid');
375 if (not defined $pid or $pid == 0) {
376 print STDERR "Unable to open pidfile or daemon not running: $!\n";
377 print STDERR qq(Mr. T: "I pity da fool who tries to search for bugs without a running daemon"\n);
378 print STDERR "Hint: try the --daemon option first\n";
382 exec('/usr/bin/sensible-browser',$url) or
383 die "Unable to run sensible-browser (Maybe chorizo is required?)";
386 # you get here, you were an idiot in checking for @USAGE_ERRORS
388 die "No option that we understand was passed (the first check for this is now buggy, so shoot your maintainer)"
392 # determine the local configuration
396 if (-e '/etc/debbugs/local_debbugs.conf') {
397 Config::Simple->import_from('/etc/debbugs/local_debbugs.conf', $config) or
398 die "Unable to read configuration from /etc/debbugs/local_debbugs.conf: $!";
400 if (-e User->Home.'/.debbugs/local_debbugs.conf') {
401 Config::Simple->import_from(User->Home.'/.debbugs/local_debbugs.conf', $config) or
402 die "Unable to read configuration from ".User->Home.'/.debbugs/local_debbugs.conf: '.$!;
404 for (keys %option_defaults) {
405 if (exists $config->{$_} and not defined $options->{$_}) {
406 $options->{$_} = $config->{$_};
408 if (not defined $options->{$_}) {
409 $options->{$_} = $option_defaults{$_};
415 my ($file,$bug_list) = @_;
416 my $inc_fh = IO::File->new($file,'w') or
417 die "Unable to open $file for writing: $!";
418 foreach my $bug (keys %{$bug_list}) {
419 my $file_loc = get_hashname($bug).'/'.$bug;
420 print {$inc_fh} map {$file_loc.'.'.$_.qq(\n)} qw(log summary report status) or
421 die "Unable to write to $file: $!";
424 die "Unable to close $file: $!";
427 # actually run rsync with the passed options
429 my %param = validate_with(params => \@_,
430 spec => {log => {type => HANDLE,
432 debug => {type => HANDLE,
435 options => {type => ARRAYREF,
439 my ($output,$error) = ('','');
440 my $h = IPC::Run::start(['rsync',@{$param{options}}],
441 \undef,$param{log},$param{log});
443 #print {$param{debug}} $error if defined $param{debug};
446 my $exit = $h->result(0);
447 # this is suboptimal, but we currently don't know whether we've
448 # selected an archive or unarchived bug, so..
449 if (defined $exit and not ($exit == 0 or $exit == 3 or $exit == 23)) {
450 print STDERR "Rsync exited with non-zero status: $exit\n";
456 # select a set of bugs
460 my %valid_keys = (package => 'package',
465 maintainer => 'maint',
466 submitter => 'submitter',
475 distribution => 'dist',
477 archive => 'archive',
478 severity => 'severity',
479 correspondent => 'correspondent',
480 affects => 'affects',
483 my $soap = SOAP::Lite
484 -> uri('Debbugs/SOAP/V1')
485 -> proxy("http://$options{bug_site}/cgi-bin/soap.cgi");
487 my @bug_selections = ();
488 if (not -e $options{bugs_to_get}) {
489 my ($addr) = get_addresses(exists $ENV{DEBEMAIL}?
491 (User->Login . '@' . qx(hostname --fqdn)));
492 # by default include bugs talked to by this user packages
493 # maintained by this user, submitted by this user, and rc
495 push @bug_selections,
496 ("correspondent:$addr archive:0",
497 "maint:$addr archive:0",
498 "submitter:$addr archive:0",
499 "severity:serious severity:grave severity:critical archive:0",
503 my $btg_fh = IO::File->new($options{bugs_to_get},'r') or
504 die "unable to open bugs to get file '$options{bugs_to_get}' for reading: $!";
512 push @bug_selections, $_;
516 # Split archive:both into archive:1 and archive:0
519 if (m/archive:both/) {
522 $y_archive =~ s/archive:both/archive:1/;
523 $n_archive =~ s/archive:both/archive:0/;
524 ($y_archive,$n_archive);
531 for my $selection (@bug_selections) {
532 my $archived_bugs = "unarchived";
533 if ($selection =~ /archive:(\S+)/ and $1) {
534 $archived_bugs = "archived";
536 my @subselects = split /\s+/,$selection;
537 my %search_parameters;
539 for my $subselect (@subselects) {
540 my ($key,$value) = split /:/, $subselect, 2;
542 if (exists $valid_keys{$key}) {
543 push @{$search_parameters{$valid_keys{$key}}},
545 } elsif ($key =~/users?$/) {
546 $users{$value} = 1 if $value;
550 for my $user (keys %users) {
551 my $ut = $soap->get_usertag($user)->result();
552 next unless defined $ut and $ut ne "";
553 for my $tag (keys %{$ut}) {
554 push @{$usertags{$tag}},
558 my $bugs = $soap->get_bugs(%search_parameters,
559 (keys %usertags)?(usertags=>\%usertags):()
561 if (defined $bugs and @{$bugs}) {
562 $bugs{$archived_bugs}{$_} = 1 for @{$bugs};