File Coverage

blib/lib/MyCPAN/App/BackPAN/Indexer.pm
Criterion Covered Total %
statement 53 224 23.6
branch 1 58 1.7
condition 0 14 0.0
subroutine 17 48 35.4
pod 26 27 96.3
total 97 371 26.1


line stmt bran cond sub pod time code
1             package MyCPAN::App::BackPAN::Indexer;
2              
3 1     1   1292 use strict;
  1         4  
  1         39  
4 1     1   6 no warnings qw(uninitialized redefine);
  1         2  
  1         42  
5              
6 1     1   6 use vars qw($VERSION $Starting_dir $logger);
  1         2  
  1         58  
7              
8 1     1   7 use Carp;
  1         3  
  1         67  
9 1     1   5 use Cwd qw(cwd);
  1         2  
  1         55  
10 1     1   6 use File::Basename;
  1         2  
  1         67  
11 1     1   5 use File::Path qw(mkpath);
  1         3  
  1         51  
12 1     1   5 use File::Spec::Functions qw(catfile file_name_is_absolute rel2abs);
  1         2  
  1         70  
13 1     1   5 use File::Temp qw(tempdir);
  1         3  
  1         88  
14 1     1   2761 use Getopt::Std;
  1         99  
  1         78  
15 1     1   6 use List::Util qw(max);
  1         2  
  1         116  
16 1     1   7 use Log::Log4perl;
  1         4  
  1         9  
17              
18             $VERSION = '1.28_12';
19              
20             =head1 NAME
21              
22             MyCPAN::App::BackPAN::Indexer - The BackPAN indexer application
23              
24             =head1 SYNOPSIS
25              
26             use MyCPAN::Indexer;
27              
28             =head1 DESCRIPTION
29              
30             =cut
31              
32             $|++;
33              
34             __PACKAGE__->activate( @ARGV ) unless caller;
35              
36             BEGIN {
37 1     1   8029 my $cwd = cwd();
38              
39 1         37 my $report_dir = catfile( $cwd, 'indexer_reports' );
40              
41 1         53 my %Defaults = (
42             alarm => 15,
43             # backpan_dir => cwd(),
44             copy_bad_dists => 0,
45             collator_class => 'MyCPAN::Indexer::Collator::Null',
46             dispatcher_class => 'MyCPAN::Indexer::Dispatcher::Parallel',
47             error_report_subdir => catfile( $report_dir, 'errors' ),
48             indexer_class => 'MyCPAN::Indexer',
49             indexer_id => 'Joe Example ',
50             interface_class => 'MyCPAN::Indexer::Interface::Text',
51             log_file_watch_time => 30,
52             # merge_dirs => undef,
53             organize_dists => 0,
54             parallel_jobs => 1,
55             pause_id => 'MYCPAN',
56             pause_full_name => "MyCPAN user ",
57             prefer_bin => 0,
58             queue_class => 'MyCPAN::Indexer::Queue',
59             report_dir => $report_dir,
60             reporter_class => 'MyCPAN::Indexer::Reporter::AsYAML',
61             retry_errors => 1,
62             success_report_subdir => catfile( $report_dir, 'success' ),
63             system_id => 'an unnamed system',
64             worker_class => 'MyCPAN::Indexer::Worker',
65             perl => remember_perl( $^X ),
66             );
67              
68             =over 4
69              
70             =item remember_perl
71              
72             We need to remember the C that started our program. We want to use the
73             same binary to fire off other processes. WE have to do this very early because
74             we are going to discard most of the environment. After we do that, we can't
75             search the PATH to find the C binary.
76              
77             =cut
78              
79             sub remember_perl {
80 1     1 1 2014 require File::Which;
81              
82 1         2038 my $perl = do {
83 1 50       18 if( file_name_is_absolute( $^X ) ) { $^X }
  1 0       47  
    0          
84 0         0 elsif( my $f = File::Which::which( $^X ) ) { $f }
85 0         0 elsif( my $g = rel2abs( $^X ) ) { $g }
86 0         0 else { undef }
87             };
88              
89             =pod
90              
91             # All of this takes place before we have an object. :(
92              
93             my $sub = sub {
94             my $perl = $self->get_config->perl;
95              
96             if( not defined $perl ) {
97             $logger->warn( "I couldn't find a perl! This may cause problems later." );
98             }
99             elsif( -x $perl ) {
100             $logger->debug( "$perl is executable" );
101             }
102             else {
103             $logger->warn( "$perl is not executable. This may cause problems later." );
104             }
105             };
106              
107             $self->push_onto_note( 'pre_logging_items', $sub );
108              
109             =cut
110              
111 1         2012 return $perl;
112             }
113              
114             =item default_keys
115              
116             Return a list of the default keys.
117              
118             =cut
119              
120 0     0 1   sub default_keys { keys %Defaults }
121              
122             =item default( KEY )
123              
124             Return the default value for KEY.
125              
126             =cut
127              
128 0     0 1   sub default { $Defaults{$_[1]} }
129              
130              
131             =item config_class
132              
133             Return the name of the configuration class to use. The default is
134             C. Any configuration class should respond to
135             the same interface.
136              
137             =cut
138              
139 0     0 1   sub config_class { 'ConfigReader::Simple' }
140              
141             =item init_config
142              
143             Load the configuration class, create the new object, and set the defaults.
144              
145             =cut
146              
147             sub init_config {
148 0     0 1   my( $self, $file ) = @_;
149              
150 0           eval "require " . $self->config_class . "; 1";
151              
152 0 0         my $config = $self->config_class->new( defined $file ? $file : () );
153              
154 0           foreach my $key ( $self->default_keys ) {
155 0 0         next if $config->exists( $key );
156 0           $config->set( $key, $self->default( $key ) );
157             }
158              
159 0           $config;
160             }
161             }
162              
163             =item adjust_config
164              
165             After we setup everything, adjust the config for things that we discovered.
166             Set some defaults.
167              
168             =cut
169              
170             sub adjust_config {
171 0     0 1   my( $application ) = @_;
172              
173 0           my $coordinator = $application->get_coordinator;
174 0           my $config = $coordinator->get_config;
175              
176 0           my( $backpan_dir, @merge_dirs ) = @{ $application->{args} };
  0            
177              
178 0 0         $config->set( 'backpan_dir', $backpan_dir ) if defined $backpan_dir;
179 0 0         $config->set( 'merge_dirs', join "\x00", @merge_dirs ) if @merge_dirs;
180              
181             # set the directories to index, either set in:
182             # first argument on the command line
183             # config file
184             # current working directory
185 0 0         unless( $config->get( 'backpan_dir' ) ) {
186 0           $config->set( 'backpan_dir', cwd() );
187             }
188              
189             # in the config file, it's all a single line
190 0 0         if( $config->get( 'merge_dirs' ) ) {
191 0           my @dirs =
192 0   0       grep { length }
193             split /(?
194             $config->get( 'merge_dirs' ) || '';
195              
196 0           $config->set( 'merge_dirs', join "\x00", @dirs );
197             }
198              
199 0 0         if( $config->exists( 'report_dir' ) ) {
200 0           foreach my $subdir ( qw(success error) ) {
201 0           $config->set(
202             "${subdir}_report_subdir",
203             catfile( $config->get( 'report_dir' ), $subdir ),
204             );
205             }
206             }
207              
208             # Adjust for some environment variables
209 0   0       my $log4perl_file =
210             $ENV{'MYCPAN_LOG4PERL_FILE'}
211             ||
212             $coordinator->get_note( 'log4perl_file' )
213             ;
214              
215             # Adjust for some environment variables
216 0 0         $ENV{'PREFER_BIN'} = 1 if $config->get( 'prefer_bin' );
217              
218 0 0         $config->set( 'log4perl_file', $log4perl_file ) if $log4perl_file;
219              
220 0           return 1;
221             }
222              
223             =item new
224              
225             =cut
226              
227             sub new {
228 0     0 1   my( $class, @args ) = @_;
229              
230 0           bless { args => [ @args ] }, $class;
231             }
232              
233             =item get_coordinator
234              
235             =item set_coordinator
236              
237             Convenience methods to deal with the coordinator
238              
239             =cut
240              
241 0     0 1   sub get_coordinator { $_[0]->{coordinator} }
242 0     0 1   sub set_coordinator { $_[0]->{coordinator} = $_[1] }
243              
244             =item process_options
245              
246             Handle the configuration directives from the command line and set default
247             values:
248              
249             -f config_file Default is $script.conf
250             -l log4perl_file Default is $script.log4perl
251             -c Print the config and exit
252              
253             =cut
254              
255             sub process_options {
256 0     0 1   my( $application ) = @_;
257              
258 0           my $run_dir = dirname( $0 );
259 0           ( my $script = basename( $0 ) ) =~ s/\.\w+$//;
260              
261 0           local @ARGV = @{ $application->{args} };
  0            
262 0           getopts( 'cl:f:', \ my %Options );
263              
264             # other things might want to use things from @ARGV, and
265             # we just removed the bits that we wanted.
266 0           $application->{args} = [ @ARGV ]; # XXX: yuck
267              
268 0   0       $Options{f} ||= catfile( $run_dir, "$script.conf" );
269              
270             #$Options{l} ||= catfile( $run_dir, "$script.log4perl" );
271              
272 0           $application->{options} = \%Options;
273             }
274              
275 0     0 0   sub get_option { $_[0]->{options}{$_[1]} }
276              
277             =item setup_coordinator
278              
279             Set up the coordinator object and set its initial values.
280              
281             =cut
282              
283             sub setup_coordinator {
284 0     0 1   my( $application ) = @_;
285              
286 0           require MyCPAN::Indexer::Coordinator;
287 0           my $coordinator = MyCPAN::Indexer::Coordinator->new;
288              
289 0           $coordinator->set_application( $application );
290 0           $application->set_coordinator( $coordinator );
291              
292 0           $coordinator->set_note( 'UUID', $application->get_uuid() );
293 0           $coordinator->set_note( 'tempdirs', [] );
294 0           $coordinator->set_note( 'log4perl_file', $application->get_option( 'l' ) );
295              
296 0           $coordinator;
297             }
298              
299             =item handle_config
300              
301             Load and set the configuration file.
302              
303             You can set the configuration filename with the C<-f> option on the command
304             line.
305              
306             You can print the configuration and exit with the C<-c> option.
307              
308             =cut
309              
310             sub handle_config {
311 0     0 1   my( $application ) = @_;
312              
313             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
314             # Adjust config based on run parameters
315 0           my $config = $application->init_config( $application->get_option('f') );
316 0           $application->get_coordinator->set_config( $config );
317              
318 0           $application->adjust_config;
319              
320             # If this is a dry run, just print the directives and exit
321 0 0         if( $application->get_option( 'c' ) ) {
322 0           my @directives = $config->directives;
323 0           my $longest = max( map { length } @directives );
  0            
324 0           foreach my $directive ( sort @directives ) {
325 0           printf "%${longest}s %-10s\n",
326             $directive,
327             $config->get( $directive );
328             }
329              
330 0           exit;
331             }
332             }
333              
334             =item activate_steps
335              
336             Returns a list of the steps to run in C.
337              
338             =cut
339              
340             sub activate_steps {
341 0     0 1   qw(
342             process_options
343             setup_coordinator
344             setup_environment
345             handle_config
346             setup_logging
347             post_setup_logging_tasks
348             adjust_config
349             disable_the_missiles
350             setup_dirs
351             run_components
352             activate_end
353             );
354             }
355              
356             =item activate
357              
358             Start the process.
359              
360             =cut
361              
362             sub activate {
363 0     0 1   my( $class, @argv ) = @_;
364 1     1   8 use vars qw( %Options $Starting_dir);
  1         2  
  1         1026  
365 0           $Starting_dir = cwd(); # remember this so we can change out of temp dirs in abnormal cleanup
366 0           local %ENV = %ENV;
367              
368 0           my $application = $class->new( @argv );
369              
370 0           foreach my $step ( $application->activate_steps ) {
371 0           $application->$step();
372             }
373              
374 0           $application;
375             }
376              
377             =item run_components
378              
379             Do the work.
380              
381             =cut
382              
383             sub run_components {
384 0     0 1   my( $application ) = @_;
385              
386             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
387             # Load classes and check that they do the right thing
388 0           my @components = $application->components;
389              
390 0           my $coordinator = $application->get_coordinator;
391              
392 0           my $config = $coordinator->get_config;
393              
394 0           foreach my $tuple ( @components ) {
395 0           my( $component_type, $default_class, $method ) = @$tuple;
396              
397 0   0       my $class = $config->get( "${component_type}_class" ) || $default_class;
398              
399 0 0         eval "require $class; 1" or die "$@\n";
400 0 0         die "$component_type [$class] does not implement $method()"
401             unless $class->can( $method );
402              
403 0           $logger->debug( "Calling $class->$method()" );
404              
405 0           my $component = $class->new;
406 0           $component->set_coordinator( $coordinator );
407 0           $component->$method();
408              
409 0           my $set_method = "set_${component_type}";
410 0           $coordinator->$set_method( $component );
411             }
412             }
413              
414             =item activate_end
415              
416             Do stuff before we quit.
417              
418             =cut
419              
420             sub activate_end {
421 0     0 1   my( $application ) = @_;
422              
423 0           $application->cleanup;
424              
425 0           $application->_exit;
426             }
427              
428             =item setup_environment
429              
430             Delete what we don't want and set what we need.
431              
432             We don't want most of the environment, just the minimal to make things not
433             break. We especially want to cleanse PATH. We keep these:
434              
435             DISPLAY
436             USER
437             HOME
438             PWD
439             TERM
440              
441             Some of the things we need are:
442              
443             AUTOMATED_TESTING
444             PERL_MM_USE_DEFAULT
445             PERL_EXTUTILS_AUTOINSTALL
446              
447             =cut
448              
449             sub setup_environment {
450 0           my %pass_through = map { $_, 1 } qw(
  0            
451             DISPLAY USER HOME PWD TERM
452 0     0 1   ), grep { /\A(?:D|MYC)PAN_/ } keys %ENV;
453              
454 0           foreach my $key ( keys %ENV ) {
455 0 0         delete $ENV{$key} unless exists $pass_through{$key}
456             }
457              
458             # Testers conventions
459 0           $ENV{AUTOMATED_TESTING}++;
460              
461             # Makemaker
462 0           $ENV{PERL_MM_USE_DEFAULT}++;
463              
464             # Module::Install
465 0           $ENV{PERL_EXTUTILS_AUTOINSTALL} = '--skipdeps';
466             }
467              
468             =item setup_logging
469              
470             Initialize C.
471              
472             In the configuration, you can set
473              
474             log4perl_file
475             log_file_watch_time
476              
477             You can also use the environment to set the values:
478              
479             MYCPAN_LOG4PERL_FILE
480             MYCPAN_LOGLEVEL (defaults to ERROR)
481              
482             The environment takes precedence.
483              
484             =cut
485              
486             sub setup_logging {
487 0     0 1   my( $self ) = @_;
488              
489 0           my $config = $self->get_coordinator->get_config;
490              
491 0           my $log_config = do {
492 1     1   9 no warnings 'uninitialized';
  1         2  
  1         1544  
493 0 0         if( -e $ENV{MYCPAN_LOG4PERL_FILE} ) {
    0          
494 0           $ENV{MYCPAN_LOG4PERL_FILE};
495             }
496             elsif( -e $config->get( 'log4perl_file' ) ) {
497 0           $config->get( 'log4perl_file' );
498             }
499             };
500              
501 0 0         if( defined $log_config ) {
502 0           Log::Log4perl->init_and_watch(
503             $log_config,
504             $self->get_coordinator->get_config->get( 'log_file_watch_time' )
505             );
506             }
507             else {
508 0           my %hash = (
509             DEBUG => $Log::Log4perl::DEBUG,
510             ERROR => $Log::Log4perl::ERROR,
511             WARN => $Log::Log4perl::WARN,
512             FATAL => $Log::Log4perl::FATAL,
513             );
514              
515 0 0         my $level = defined $ENV{MYCPAN_LOGLEVEL} ?
516             $ENV{MYCPAN_LOGLEVEL} : 'ERROR';
517              
518 0           Log::Log4perl->easy_init( $hash{$level} );
519             }
520              
521 0           $logger = Log::Log4perl->get_logger( 'backpan_indexer' );
522             }
523              
524             =item post_setup_logging_tasks
525              
526             Logging has to happen after we read the config, but there are some
527             things I'd like to check and log, so I must wait to log. Anyone who
528             wants to log something before logging has been set up should push a
529             sub reference onto the C note.
530              
531             =cut
532              
533             sub post_setup_logging_tasks {
534 0     0 1   my $application = shift;
535              
536             # this stuff happened too early to set a pre_logging_items
537 0           $application->_log_perl;
538              
539 0           my $coordinator = $application->get_coordinator;
540              
541 0           my @items = $coordinator->get_note( 'pre_logging_items' );
542              
543 0           foreach my $item ( @items ) {
544 0 0   0     next unless ref $item eq ref sub {};
  0            
545 0           $item->();
546             }
547              
548 0           1;
549             }
550              
551             sub _log_perl {
552 0     0     my( $application ) = @_;
553              
554 0           my $coordinator = $application->get_coordinator;
555 0           my $config = $coordinator->get_config;
556              
557 0           my $perl = $config->perl;
558              
559 0 0         if( not defined $perl ) {
    0          
560 0           $logger->warn( "I couldn't find a perl! This may cause problems later." );
561             }
562             elsif( -x $perl ) {
563 0           $logger->debug( "$perl is executable" );
564             }
565             else {
566 0           $logger->warn( "$perl is not executable. This may cause problems later." );
567             }
568             }
569              
570             =item disable_the_missiles
571              
572             Catch INT signals and set up error handlers to direct things toward Log4perl.
573             Some of this stuff is a bit dangerous, maybe.
574              
575             =cut
576              
577             sub disable_the_missiles {
578 0     0 1   my( $self ) = @_;
579              
580 0           $self->install_int_handler;
581 0           $self->install_warn_handler;
582             }
583              
584             =item install_int_handler
585              
586             Catch INT signals so we can log it, clean up, and exit nicely.
587              
588             =cut
589              
590             sub install_int_handler {
591             #$SIG{__DIE__} = \&Carp::confess;
592              
593             # If we catch an INT we're probably in one of the temporary directories
594             # and have some files open. To clean up the temp dirs, we have to move
595             # above them, so change back to the original directory.
596             $SIG{INT} = sub {
597 0     0     $logger->error("Caught SIGINT in $$" );
598 0           chdir $Starting_dir;
599             exit()
600 0     0 1   };
  0            
601             }
602              
603             =item install_warn_handler
604              
605             Make C go to C.
606              
607             =cut
608              
609             sub install_warn_handler {
610             $SIG{__WARN__} = sub {
611 0     0     $logger->warn( @_ );
612 0     0 1   };
613             }
614              
615             =item components
616              
617             An array of arrays that list the components to load and the method each
618             component needs to implement. You can override the implementing class through
619             the configuration.
620              
621             =cut
622              
623             sub components {
624             (
625 0     0 1   [ qw( reporter MyCPAN::Indexer::Reporter::AsYAML get_reporter ) ],
626             [ qw( queue MyCPAN::Indexer::Queue get_queue ) ],
627             [ qw( dispatcher MyCPAN::Indexer::Dispatcher::Parallel get_dispatcher ) ],
628             [ qw( indexer MyCPAN::Indexer get_indexer ) ],
629             [ qw( worker MyCPAN::Indexer::Worker get_task ) ],
630             [ qw( collator MyCPAN::Indexer::Collator::Null get_collator ) ],
631             [ qw( interface MyCPAN::Indexer::Interface::Curses do_interface ) ],
632             )
633             }
634              
635             =item cleanup
636              
637             Clean up on the way out. We're already done with the run.
638              
639             =cut
640              
641             sub cleanup {
642 0     0 1   my( $self ) = @_;
643              
644 0           require File::Path;
645              
646 0           my @dirs =
647 0           @{ $self->get_coordinator->get_note('tempdirs') },
648             $self->get_coordinator->get_config->temp_dir;
649 0           $logger->debug( "Dirs to remove are @dirs" );
650              
651 0           eval {
652 1     1   8 no warnings;
  1         2  
  1         849  
653 0           File::Path::rmtree [@dirs];
654             };
655              
656 0 0         $logger->error( "Couldn't cleanup: $@" ) if $@;
657             }
658              
659             # I don't remember why I made an explicit exit. Was it to get
660             # out of a Tk app or something?
661             sub _exit {
662 0     0     my( $self ) = @_;
663              
664 0           $logger->info( "Exiting from ", __PACKAGE__ );
665              
666 0           exit 0;
667             }
668              
669             =item setup_dirs
670              
671             Setup the temporary directories, report directories, and so on, etc.
672              
673             =cut
674              
675             sub setup_dirs { # XXX big ugly mess to clean up
676 0     0 1   my( $self ) = @_;
677              
678 0           my $config = $self->get_coordinator->get_config;
679              
680             # Okay, I've gone back and forth on this a couple of times. There is
681             # no default for temp_dir. I create it here so it's only set when I
682             # need it. It either comes from the user or on-demand creation. I then
683             # set it's value in the configuration.
684              
685 0   0       my $temp_dir = $config->temp_dir || tempdir( DIR => cwd(), CLEANUP => 1 );
686 0           $logger->debug( "temp_dir is [$temp_dir] [" . $config->temp_dir . "]" );
687 0           $config->set( 'temp_dir', $temp_dir );
688              
689              
690 0           my $tempdirs = $self->get_coordinator->get_note( 'tempdirs' );
691 0           push @$tempdirs, $temp_dir;
692 0           $self->get_coordinator->set_note( 'tempdirs', $tempdirs );
693              
694 0 0         mkpath( $temp_dir ) unless -d $temp_dir;
695 0 0         $logger->logdie( "temp_dir [$temp_dir] does not exist!" ) unless -d $temp_dir;
696              
697 0           foreach my $key ( qw(report_dir success_report_subdir error_report_subdir) ) {
698 0           my $dir = $config->get( $key );
699              
700 0 0         mkpath( $dir ) unless -d $dir;
701 0 0         $logger->logdie( "$key [$dir] does not exist!" ) unless -d $dir;
702             }
703              
704 0 0         if( $config->retry_errors ) {
705 0           $logger->warn( 'retry_errors no longer deletes error reports, but the worker should skip them if the setting is false' );
706             }
707             }
708              
709             =item get_uuid
710              
711             Generate a unique identifier for this indexer run.
712              
713             =cut
714              
715             sub get_uuid {
716 0     0 1   require Data::UUID;
717 0           my $ug = Data::UUID->new;
718 0           my $uuid = $ug->create;
719 0           $ug->to_string( $uuid );
720             }
721              
722             =back
723              
724             =head1 TO DO
725              
726             =over 4
727              
728             =item Count the lines in the files
729              
730             =item Code stats? Lines of code, lines of pod, lines of comments
731              
732             =back
733              
734             =head1 SOURCE AVAILABILITY
735              
736             This code is in Github:
737              
738             git://github.com/briandfoy/mycpan-indexer.git
739              
740             =head1 AUTHOR
741              
742             brian d foy, C<< >>
743              
744             =head1 COPYRIGHT AND LICENSE
745              
746             Copyright (c) 2008-2013, brian d foy, All Rights Reserved.
747              
748             You may redistribute this under the same terms as Perl itself.
749              
750             =cut
751              
752             1;