3 # Copyright (c) 2003 Henk Penning, all rights reserved.
4 # penning@cs.uu.nl, http://www.cs.uu.nl/staff/henkp.html
5 # Version 1.1 was donated to the Apache Software Foundation 2003 Jan 28.
6 # Permission is hereby granted, free of charge, to any person obtaining a
7 # copy of this software and associated documentation files (the "Software"),
8 # to deal in the Software without restriction, including without limitation
9 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 # and/or sell copies of the Software, and to permit persons to whom the
11 # Software is furnished to do so, subject to the following conditions:
13 # The above copyright notice and this permission notice shall be included in
14 # all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19 # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 # DEALINGS IN THE SOFTWARE.
24 # Thanks to Klaus Heinz <heinz@NetBSD.org> for sugestions ao htm_head ;
25 # Peter Pöml for MirrorBrain support ; Jeremy Olexa, Karl Berry, Roland
26 # Pelzer for suggestions regarding rsync support.
33 our $DEF_TIMEOUT = 300 ;
35 our $TIM_PAT = '^(\d+)([smhd])$' ;
36 our %APA_TYPES = () ; for ( qw(backup ftp http) ) { $APA_TYPES { $_ } ++ ; }
37 our %GET_OPTS = () ; for ( qw(all update) ) { $GET_OPTS { $_ } ++ ; }
38 our $HIST_DELTA = 24 * 60 * 60 ;
39 our $APRX_DELTA = 60 ;
40 our $HOME = 'http://www.cs.uu.nl/people/henkp/mirmon/' ;
42 package Base ; #####################################################
46 our ( @ISA, @EXPORT ) ;
48 { @ISA = qw(Exporter) ;
50 qw(aprx_eq aprx_ge aprx_le aprx_gt aprx_lt
51 URL NAM SMA BLD NSS TAB BQ TR TH TD TDr RED GRN H1 H2 H3
52 s4tim pr_interval pr_diff
56 sub Version { "$PRG version $VER" ; }
57 sub version { "$PRG-$VER" ; }
58 sub DEF_TIMEOUT { $DEF_TIMEOUT ; }
59 sub is_get_opt { my $opt = shift ; exists $GET_OPTS { $opt } ; }
64 if ( @_ ) { $self -> { $attr } = shift ; }
65 die "no attr '$attr'" unless exists $self -> { $attr } ;
72 sprintf 'sub %s { my $self = shift ; $self -> getset ( "%s", @_ ) ; }'
78 join "\n", map { Base -> mk_method ( $_ ) ; } @_ ;
81 sub aprx_eq { my ( $t1, $t2 ) = @_ ; abs ( $t1 - $t2 ) < $APRX_DELTA ; }
82 sub aprx_ge { my ( $t1, $t2 ) = @_ ; $t1 > $t2 or aprx_eq $t1, $t2 ; }
83 sub aprx_le { my ( $t1, $t2 ) = @_ ; $t1 < $t2 or aprx_eq $t1, $t2 ; }
84 sub aprx_gt { my ( $t1, $t2 ) = @_ ; $t1 > $t2 and not aprx_eq $t1, $t2 ; }
85 sub aprx_lt { my ( $t1, $t2 ) = @_ ; $t1 < $t2 and not aprx_eq $t1, $t2 ; }
87 sub URL { sprintf '<A HREF="%s">%s</A>', $_[0], $_[1] ; }
88 sub NAM { sprintf '<A NAME="%s">%s</A>', $_[0], $_[1] ; }
89 sub SMA { sprintf "<FONT SIZE=\"-1\">%s</FONT>", $_[0] ; }
90 sub BLD { sprintf "<B>%s</B>", $_[0] ; }
91 sub NSS { sprintf SMA('%s site%s'), $_[0], ( $_[0] == 1 ? '' : 's' ) ; }
92 sub TAB { sprintf "<TABLE BORDER=2 CELLPADDING=3>%s</TABLE>", $_[0] ; }
93 sub BQ { sprintf "<BLOCKQUOTE>\n%s\n</BLOCKQUOTE>\n", $_[0] ; }
94 sub TR { sprintf "<TR>%s</TR>\n", $_[0] ; }
95 sub TH { sprintf "<TH>%s</TH>\n", $_[0] ; }
96 sub TD { sprintf "<TD>%s</TD>\n", $_[0] ; }
97 sub H1 { sprintf "<H1>%s</H1>\n", $_[0] ; }
98 sub H2 { sprintf "<H2>%s</H2>\n", $_[0] ; }
99 sub H3 { sprintf "<H3>%s</H3>\n", $_[0] ; }
100 sub TDr { sprintf "<TD ALIGN=\"RIGHT\">%s</TD>\n", $_[0] ; }
101 sub RED { sprintf "<FONT COLOR=\"RED\">%s</FONT>", $_[0] ; }
102 sub GRN { sprintf '<FONT COLOR="GREEN">%s</FONT>', $_[0] ; }
106 my %tab = ( 's' => 1, 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24 ) ;
107 die "wrong time '$tim'" unless $tim =~ /$TIM_PAT/o ;
108 my $m = $1 ; my $u = $2 ;
109 return $m * $tab { $u } ;
114 my ( $magn, $unit ) ;
115 my $mins = $s / 60 ; my $m = int ( $mins + 0.5 ) ;
116 my $hours = $s / ( 60 * 60 ) ; my $h = int ( $hours + 0.5 ) ;
119 { $magn = $s ; $unit = 'second' ; }
121 { $magn = $m ; $unit = 'minute' ; }
123 { $magn = $h ; $unit = 'hour' ; }
125 { $magn = sprintf "%.1f", $hours / 24 ; $unit = 'day' ; }
127 $unit .= 's' unless $magn == 1 ;
129 return "$magn $unit" ;
138 { $res = BLD 'renewed' ; }
140 { $res = pr_interval $^T - $time ;
141 $res = BLD RED $res if aprx_lt $time, $max ;
147 { my @day = qw(Sun Mon Tue Wed Thu Fri Sat) ;
148 my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) ;
149 my @gmt = gmtime time + 3600 ;
150 sprintf "%s, %02d %s %4d %02d:%02d:%02d GMT"
151 , $day [ $gmt [ 6 ] ]
153 , $mon [ $gmt [ 4 ] ]
167 package Mirmon ; ###################################################
169 BEGIN { use base 'Base' ; Base -> import () ; }
174 { my %opt = ( v => 0 , d => 0 , q => 0 ) ;
176 { my ( $key, $val ) = @_ ;
178 unless ( exists $opt { $key } )
179 { warn "unknown Mirmon option '$key'\n" ; }
181 { $res = $opt { $key } ;
182 $opt { $key } = $val if defined $val ;
188 sub verbose { _opt ( 'v', shift ) ; }
189 sub quiet { _opt ( 'q', shift ) ; }
190 sub debug { _opt ( 'd', shift ) ; }
192 eval Base -> mk_methods ( qw(conf state regions) ) ;
196 my $home = ( getpwuid $< ) [ 7 ] or die "can get homedir '$<' ($!)" ;
197 ( 'mirmon.conf', "$home/.mirmon.conf", '/etc/mirmon.conf' ) ;
203 my $res = bless {}, $self ;
204 $res -> get_config ( $path ) ;
206 $res -> get_regions ;
213 my @LIST = $arg ? ( $arg ) : Mirmon -> config_list ;
214 for my $conf ( @LIST ) { return $conf if -f $conf ; }
215 die sprintf "can't find a config file :\n %s\n" , join "\n ", @LIST ;
221 my $file = $self -> find_config ( $path ) ; # or die
222 $self -> conf ( Mirmon::Conf -> new ( $file ) ) ;
227 my $conf = $self -> conf ;
228 my $state = $conf -> state ;
230 open STATE, $state or die "can't open $state ($!)" ;
231 for my $line ( <STATE> )
233 my $mirror = Mirmon::Mirror -> new ( $self, $line ) ;
234 $res -> { $mirror -> url } = $mirror ;
238 my $mlist = $conf -> mirror_list ;
239 my $style = $conf -> list_style ;
241 open MLIST, $mlist or die "can't open $mlist ($!)" ;
242 for my $line ( <MLIST> )
244 next if $line =~ /^#/ ;
245 next if $line =~ /^\s*$/ ;
246 my ( $reg, $url, $mail ) ;
247 if ( $style eq 'plain' )
248 { ( $reg, $url, $mail ) = split ' ', $line ; }
249 elsif ( $style eq 'apache' )
251 ( $apache_type, $reg, $url, $mail ) = split ' ', $line ;
252 unless ( defined $APA_TYPES { $apache_type } )
253 { print "*** strange type in $url ($apache_type)\n"
254 unless Mirmon::quiet ;
259 if ( $conf -> add_slash and $url !~ m!/$! )
260 { print "*** appended '/' to $url\n" unless Mirmon::quiet ;
264 $in_list { $url } ++ ;
266 unless ( exists $res -> { $url } )
267 { printf "*** added to list %s\n", $url unless Mirmon::quiet ;
268 $res -> { $url } = Mirmon::Mirror -> init ( $self, $url ) ;
270 my $mirror = $res -> { $url } ;
271 $mirror -> region ( $reg ) ;
272 $mirror -> mail ( $mail || '' ) ;
276 for my $url ( sort keys %$res )
277 { # printf "%s\n", $res -> { $url } -> state ;
278 unless ( exists $in_list { $url } )
279 { printf "*** removed from list %s\n", $url unless Mirmon::quiet ;
280 delete $res -> { $url } ;
283 $self -> state ( $res ) ;
288 my $state = $self -> state ;
289 my $file = $self -> conf -> state ;
290 my $TMP = "$file.tmp" ;
291 open TMP, ">$TMP" or die "can't write '$TMP' ($!)" ;
292 for my $url ( sort keys %$state )
293 { printf TMP "%s\n", $state -> { $url } -> state
294 or die "can't print $url to $TMP ($!)" ;
299 { warn "wrote empty state file; keeping previous version" ; }
301 { rename $TMP, $file or die "can't rename '$TMP', '$file' ($!)" ; }
306 my $file = $self -> conf -> countries ;
307 open REGS, $file or die "can't open countries '$file' ($!)" ;
311 my ( $code, $dash, $reg ) = split ' ', $_, 3 ;
312 $self -> { regions } { lc $code } = lc $reg ;
320 my $state = $self -> state ;
321 my $conf = $self -> conf ;
322 my $CMD = $conf -> probe ;
323 my $PAR = $conf -> max_probes ;
327 my $GET = IO::Select -> new () ;
332 for my $url ( sort keys %$state )
333 { my $mirror = $state -> { $url } ;
334 $cnt ++ if $mirror -> last_status eq 'ok' ;
335 if ( $get eq 'all' or $mirror -> last_probe eq 'undef' )
336 { push @QUE, $mirror ; }
337 elsif ( $get eq 'update' )
338 { my $stat = $mirror -> last_status ;
339 my $vrfy = $mirror -> last_ok_probe ;
340 my $lprb = $mirror -> last_probe ;
341 if ( aprx_le $lprb, $^T - s4tim $conf -> min_poll )
342 { if ( $stat ne 'ok' )
343 { push @QUE, $mirror ; $nok ++ ; }
344 elsif ( aprx_le $vrfy, $^T - s4tim $conf -> max_poll )
345 { push @QUE, $mirror ; }
347 { push @NOQ, $mirror ; }
351 { die "unknown opt_get '$get'" ; }
354 if ( Mirmon::verbose )
355 { my $que = scalar @QUE ; my $noq = scalar @NOQ ;
356 printf "ok mirrors %d, queued %d, not queued %d, ok %d, nok %d\n"
357 , $cnt, $que, $noq, $que - $nok, $nok
360 if ( $conf -> randomize )
361 { my $hrs = int ( ( s4tim $conf -> max_poll ) / 60 / 60 + 0.5 ) ;
362 my $avg = int ( $cnt / $hrs + 0.5 ) ;
363 my $prc = ( scalar keys %$state ) / 50 ;
365 my $extras = $flr + ( rand 1 < ( $prc - $flr ) ) ;
368 while ( @QUE < $avg + $nok and @NOQ and $picked < $extras )
369 { my $idx = int rand @NOQ ;
370 push @QUE, $NOQ [ $idx ] ;
371 $NOQ [ $idx ] = $NOQ [ $#NOQ ] ;
376 printf "avg mirrors/hr %d, max extras %d, picked %d ; queued %s\n"
377 , $avg, $extras, $picked, scalar @QUE if Mirmon::verbose ;
382 while ( $GET -> count () < $PAR and @QUE )
383 { my $mirror = shift @QUE ;
384 if ( gethost $mirror -> site )
385 { my $handle = $mirror -> start_probe ;
386 $m4h { $handle } = $mirror ;
387 $GET -> add ( $handle ) ;
391 { $mirror -> update ( 0, 'site_not_found', undef ) ; }
394 my @can_read = $GET -> can_read ( 0 ) ;
396 printf "queue %d, started %d, probes %d, can_read %d\n",
397 scalar @QUE, $started, $GET -> count (), scalar @can_read
400 for my $handle ( @can_read )
401 { # order is important ; wget's hang if/when actions are reversed
402 $GET -> remove ( $handle ) ;
403 $m4h { $handle } -> finish_probe ( $handle ) ;
409 my $stop = time + $conf -> timeout + 10 ;
411 while ( $GET -> count () and time < $stop )
412 { my @can_read = $GET -> can_read ( 0 ) ;
414 printf "wait %2d, probes %d, can_read %d\n",
415 $stop - scalar time, $GET -> count (), scalar @can_read
418 for my $handle ( @can_read )
419 { $GET -> remove ( $handle ) ;
420 $m4h { $handle } -> finish_probe ( $handle ) ;
426 for my $handle ( $GET -> handles () )
427 { $m4h { $handle } -> update ( 0, 'hangs', undef ) ; }
437 ( '<IMG BORDER=1 SRC="%s/bar.gif" ALT="">'
438 , $self -> conf -> icons
442 { sprintf '<IMG BORDER=1 SRC="%s/mm%s%02d.gif" ALT="">'
443 , $self -> conf -> icons, $prf, $cnt ;
447 sub img_sf { my $self = shift ; $self -> img_sf_cnt ( $_[0], 1 ) ; }
452 if ( $hst =~ /-(.*)$/ ) { $hst = $1 ; }
453 return '' unless $hst =~ m/^[sbfzx]+$/ ;
454 if ( length $hst == $HIST and $hst =~ /^(s*b)s*$/ )
455 { return $self -> img_sf_cnt ( 'sb', length $1 ) ; }
456 elsif ( length $hst == $HIST and $hst =~ /^(s*f)s*$/ )
457 { return $self -> img_sf_cnt ( 'sf', length $1 ) ; }
458 elsif ( length $hst == $HIST and $hst =~ /^(s*b)fs*$/ )
459 { return $self -> img_sf_cnt ( 'sbf', length $1 ) ; }
462 my $prf = substr $hst, 0, 1 ;
463 $hst = substr $hst, 1 ;
465 { if ( substr ( $prf, 0, 1 ) eq substr ( $hst, 0, 1 ) )
467 $hst = substr $hst, 1 ;
470 { $res .= $self -> img_sf_cnt ( $prf, $cnt ) ;
471 $prf = substr $hst, 0, 1 ;
472 $hst = substr $hst, 1 ;
476 $res .= $self -> img_sf_cnt ( $prf, $cnt ) if $cnt ;
480 sub gen_histogram_probes
482 my $state = $self -> state ;
490 for my $url ( keys %$state )
491 { my $mirror = $state -> { $url } ;
492 my $lprb = $mirror -> last_probe ;
493 my $stat = $mirror -> last_status ;
494 next if $lprb eq 'undef' ;
495 my $hr = int ( ( $^T - $lprb ) / 3600 + 0.5 ) ;
496 $hr_min = $hr if ! defined $hr_min or $hr < $hr_min ;
497 $hr_max = $hr if ! defined $hr_max or $hr > $hr_max ;
499 { $tab { $hr } ++ ; $s_cnt ++ ; }
501 { $bad { $hr } ++ ; $f_cnt ++ ; }
503 return BQ 'nothing yet' unless scalar keys %tab ;
511 , $s_cnt , GRN ( 'successful' )
512 , $f_cnt , RED ( 'failed' )
517 for my $x ( keys %tab )
518 { my $tot = $tab { $x } + ( $bad { $x } || 0 ) ;
519 $max = $tot if $max < $tot ;
522 return BQ "nothing yet" unless $max ;
524 for my $hr ( $hr_min .. $hr_max )
525 { my $x = $tab { $hr } || 0 ;
526 my $y = $bad { $hr } || 0 ;
527 my $n = int ( $x / $max * $HIST ) ;
528 my $b = int ( $y / $max * $HIST ) ;
534 ( ( $n ? $self -> img_sf_cnt ( 's', $n ) : '' )
535 . ( $b ? $self -> img_sf_cnt ( 'f', $b ) : '' )
536 . ( ( $n + $b ) ? '' : ' ' )
545 my $state = $self -> state ;
547 for my $url ( keys %$state )
548 { my $time = $state -> { $url } -> age ;
549 push @tab, $^T - $time if $time =~ /^\d+$/ ;
553 return undef if $cnt == 0 ;
555 @tab = sort { $a <=> $b } @tab ;
558 for my $age ( @tab ) { $tot += $age ; }
559 my $mean = $tot / $cnt ;
563 { $median = $tab [ 0 ] ; }
565 { my $mid = int ( $#tab / 2 ) ;
566 $median = ( $tab [ $mid ] + $tab [ $mid + 1 ] ) / 2 ;
569 { my $mid = int ( $#tab / 2 ) ;
570 $median = $tab [ $mid ] ;
574 { return $mean, $median, undef ; }
578 { $sum += ( $age - $mean ) ** 2 ; }
579 my $stddev = sqrt ( $sum / ( $cnt - 1 ) ) ;
581 return $mean, $median, $stddev ;
586 my $conf = $self -> conf ;
587 my $min_sync = $conf -> min_sync ;
588 my $max_sync = $conf -> max_sync ;
589 my $min_poll = $conf -> min_poll ;
590 my $max_poll = $conf -> max_poll ;
595 <H4><I>project</I> site -- home</H4>
598 <B><I>project</I> site</B> is an url.
599 The <B>href</B> is the href for the site in the list of mirrors,
600 usually the root of the mirrored file tree.
601 The <B>text</B> is the <I>site</I> of that url.
603 <B>home</B> (represented by the <B>@</B>-symbol) is an url
604 pointing to the document root of the site. This pointer is
605 useful if the <B><I>project</I> site</B> url is invalid,
606 possibly because the mirror site moved the archive.
612 Indicates the type (<B>ftp</B> or <B>http</B>) of
613 the <B><I>project</I> site</B> and <B>home</B> urls.
616 <H4>mirror age, daily stats</H4>
619 The <B>mirror age</B> is based upon the last successful probe.
621 Once a day the status of a mirror site is determined.
622 The status (represented by a colored block) is appended
623 to the <B>right</B> of the status history (<I>right</I>
624 is <I>recent</I>). More precise, the status block is appended
625 if the last status block was appended 24 (or more) hours ago.
626 <P>The status of a mirror depends on its age and a few
627 configuration parameters :
629 <TABLE BORDER=1 CELLPADDING=5>
631 <TH ROWSPAN=3>status</TH>
632 <TH COLSPAN=4>age</TH>
635 <TH COLSPAN=2 BGCOLOR=YELLOW>this project</TH>
636 <TH COLSPAN=2 BGCOLOR=AQUA>in general</TH>
639 <TH BGCOLOR=YELLOW>min</TH>
640 <TH BGCOLOR=YELLOW>max</TH>
641 <TH BGCOLOR=AQUA>min</TH>
642 <TH BGCOLOR=AQUA>max</TH>
645 <TH><FONT COLOR=GREEN>fresh</FONT></TH>
646 <TD BGCOLOR=YELLOW ALIGN=CENTER>0</TD>
647 <TD BGCOLOR=YELLOW ALIGN=CENTER>$min_sync + $max_poll</TD>
648 <TD BGCOLOR=AQUA ALIGN=CENTER>0</TD>
649 <TD BGCOLOR=AQUA ALIGN=CENTER>min_sync + max_poll</TD>
652 <TH><FONT COLOR=BLUE>oldish</FONT></TH>
653 <TD BGCOLOR=YELLOW ALIGN=CENTER>$min_sync + $max_poll</TD>
654 <TD BGCOLOR=YELLOW ALIGN=CENTER>$max_sync + $max_poll</TD>
655 <TD BGCOLOR=AQUA ALIGN=CENTER>min_sync + max_poll</TD>
656 <TD BGCOLOR=AQUA ALIGN=CENTER>max_sync + max_poll</TD>
659 <TH><FONT COLOR="RED">old</FONT></TH>
660 <TD BGCOLOR=YELLOW ALIGN=CENTER>$max_sync + $max_poll</TD>
661 <TD BGCOLOR=YELLOW ALIGN=CENTER>∞</TD>
662 <TD BGCOLOR=AQUA ALIGN=CENTER>max_sync + max_poll</TD>
663 <TD BGCOLOR=AQUA ALIGN=CENTER>∞</TD>
666 <TH><FONT COLOR=BLACK>bad</FONT></TH>
667 <TH COLSPAN=4 BGCOLOR=BLACK>
668 <FONT COLOR=WHITE>the site or mirror tree was never found</FONT></TH>
674 <H4>last probe, probe stats</H4>
677 <B>Last probe</B> indicates when the last successful probe was made.
678 <B>Probe stats</B> gives the probe history (<I>right</I> is <I>recent</I>).
680 <FONT COLOR=GREEN><B>success</B></FONT> or a
681 <FONT COLOR=RED><B>failure</B></FONT>.
687 <B>Last stat</B> gives the status of the last probe.
694 { return '' unless my $ths = shift ;
695 $ths == 1 ? TH '' : "<TH COLSPAN=$ths></TH>\n" ;
701 my $conf = $self -> conf ;
702 my $state = $self -> state ;
704 return '' if $where ne $conf -> put_histo ;
706 my $MAX_H = $conf -> max_age1 ;
708 ( ( 20 * 3600 <= $MAX_H and $MAX_H <= 36 * 3600 )
709 ? int ( $MAX_H / 3600 )
712 my $MAX_O = $conf -> max_age2 ;
713 my $MAX_o = int ( $MAX_O / 3600 + 0.5 ) ;
715 my %W = ( 'old' => 1, 'ded' => 1, 'bad' => 1 ) ;
716 my %Wmx = ( 'old' => 5, 'ded' => 3, 'bad' => 3 ) ;
720 for ( my $x = 0 ; $x < $MAX_h ; $x ++ ) { $tab { $x } = 0 ; }
721 $tab { old } = 0 ; $tab { ded } = 0 ; $tab { bad } = 0 ;
722 for my $url ( keys %$state )
723 { my $time = $state -> { $url } -> age ;
724 if ( $time =~ /^\d+$/ )
725 { my $s = $^T - $time ;
726 my $hr = int ( $s / $MAX_H * ( $MAX_h - 1 ) + 0.5 ) ;
727 if ( $s <= $MAX_H ) { $tab { $hr } ++ ; }
728 elsif ( $s <= $MAX_O ) { $tab { old } ++ ; }
729 else { $tab { ded } ++ ; }
732 { $tab { bad } ++ ; }
735 for ( grep ! exists $Wmx { $_ }, keys %tab )
736 { $max = $tab { $_ } if $tab { $_ } > $max ; }
740 for my $aux ( keys %Wmx )
741 { $bad { $aux } = $tab { $aux } ;
742 if ( $bad { $aux } > $max )
743 { $W { $aux } = $Wmx { $aux } ;
744 my $d = int ( $bad { $aux } / $W { $aux } ) ;
745 for ( my $i = 1 ; $i < $W { $aux } ; $i++ )
746 { $tab { $aux . $i } = $d ;
747 if ( $bad { $aux } % $Wmx { $aux } > $i )
748 { $tab { $aux . $i } ++ ;
752 $tab { $aux } -= ( $W { $aux } - 1 ) * $d ;
753 $max = $tab { $aux } if $max < $tab { $aux } ;
758 # { for my $hr ( keys %tab )
759 # { printf "tab '%s' = '%s'\n", $hr, $tab { $hr } ; }
762 return 'nothing yet' unless $max ;
763 $H = $max if 8 <= $max and $max <= 26 ;
765 { $hst { $_ } = int ( $H * $tab { $_ } / $max + 0.5 ) ; }
766 my @keys = sort { $a <=> $b } grep /^\d+$/, keys %hst ;
768 for my $hr ( @keys ) { $tab_hr += $tab { $hr } ; }
770 , grep ( m/^old/, sort keys %tab )
771 , grep ( m/^ded/, sort keys %tab )
772 , grep ( m/^bad/, sort keys %tab )
774 my $img_bar = sprintf '<IMG SRC="%s/bar.gif" ALT="" BORDER=0>'
776 my %img = ( bar => $img_bar ) ;
777 for my $col ( qw(s b f z) ) { $img { $col } = $self -> img_sf ( $col ) ; }
779 for ( my $h = $H ; $h > 0 ; $h -- )
781 $res .= sprintf "<TH ROWSPAN=3 VALIGN=\"TOP\">↑</TH>\n"
783 $res .= sprintf '<TD ROWSPAN=%d ALIGN="CENTER">%s</TD>' . "\n"
784 , $H-6, NSS ( $max ) if $h == $H - 3 ;
785 $res .= sprintf "<TH ROWSPAN=3 VALIGN=\"BOTTOM\">↓</TH>\n"
790 ( ( $hst { $x } >= $h )
793 : ( $x =~ /^old/ ? 'b' : ( $x =~ /^ded/ ? 'f' : 'z' ) )
795 : ( ( $h == 1 and $hst { $x } == 0 ) ? 'bar' : '' )
798 { $res .= _ths $ths ; $ths = 0 ; $res .= TH $img { $col } ; }
802 $res .= _ths ( $ths ) . "</TR>\n" ;
805 my $HR = '<HR SIZE=2 WIDTH="95%%" NOSHADE>' ;
808 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", 1 ;
809 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $MAX_h ;
810 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W { old } ;
811 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W { ded } ;
812 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W { bad } ;
816 $res .= '<TD ALIGN="CENTER"> <B>age</B> → </TD>' ;
818 $res .= "<TH>|</TH>\n" ;
820 ( '<TD COLSPAN=%d ALIGN="CENTER">'
821 . '← 0 ≤ <B>age</B> ≤ %s →'
823 , $MAX_h - 2, pr_interval ( $MAX_H )
826 $res .= "<TH>|</TH>\n" ;
828 ( '<TD ALIGN="CENTER" COLSPAN=%d>'
829 . ' %sh < %s ≤ %sh '
831 , $W { old }, int($MAX_H/60/60) , BLD ( 'age' ), $MAX_o
834 ( '<TD ALIGN="CENTER" COLSPAN=%d>'
835 . ' <FONT COLOR="RED">old</FONT> '
840 ( '<TD ALIGN="CENTER" COLSPAN=%d>'
841 . ' <FONT COLOR="RED">bad</FONT> '
847 my $FRMT = '<TD ALIGN="CENTER" COLSPAN=%d> %s </TD>' ;
850 $res .= sprintf "$FRMT\n", 1, NSS scalar keys %$state ;
851 $res .= "<TH>|</TH>\n" ;
852 $res .= sprintf "$FRMT\n", $MAX_h - 2, NSS $tab_hr ;
853 $res .= "<TH>|</TH>\n" ;
854 $res .= sprintf "$FRMT\n", $W { old }, NSS $bad { old } ;
855 $res .= sprintf "$FRMT\n", $W { ded }, NSS $bad { ded } ;
856 $res .= sprintf "$FRMT\n", $W { bad }, NSS $bad { bad } ;
859 $res = "<TABLE CELLSPACING=0 CELLPADDING=1 BORDER=0>\n$res\n</TABLE>\n" ;
860 $res = sprintf "<TABLE CELLPADDING=5 BORDER=4>%s</TABLE>\n"
861 , "<TR><TH>\n$res\n</TH></TR>" ;
863 , $self -> img_sf ( 's' ) , $self -> img_sf ( 'b' )
864 , $self -> img_sf ( 'f' ) , $self -> img_sf ( 'z' )
867 { $res .= sprintf "<BR>units %s represent one mirror site.\n"
871 { $res .= sprintf "<BR>each %s unit represents %s mirror sites.\n"
872 , $units, sprintf ( "%.1f", $max / $H ) ;
874 return H2 ( 'age histogram' ) . BQ $res ;
880 my $VERSION = shift ;
881 my $conf = $self -> conf ;
882 my $PPP = $conf -> web_page ;
883 my $state = $self -> state ;
884 my $CCS = $self -> regions ;
885 my $TMP = "$PPP.tmp" ;
889 for my $url ( keys %$state )
890 { my $mirror = $state -> { $url } ;
891 my $reg = $mirror -> region ;
892 push @{ $tab { $reg } }, $mirror ;
895 my $bad = 0 ; my $old = 0 ; my $unr = 0 ;
900 for my $url ( keys %$state )
901 { my $mirror = $state -> { $url } ;
902 my $time = $mirror -> age ;
903 my $stat = $mirror -> last_status ;
904 my $vrfy = $mirror -> last_ok_probe ;
905 if ( $stat eq 'ok' ) { $ok ++ ; } else { $stats { $stat } ++ ; }
906 if ( $time eq 'undef' )
908 elsif ( 'f' eq $conf -> age_code ( $time ) )
910 if ( $vrfy eq 'undef' or aprx_lt $vrfy, $^T - $conf -> max_vrfy )
915 "%d bad -- %d older than %s -- %s unreachable for more than %s"
918 , pr_interval ( $conf -> max_age2 )
920 , pr_interval ( $conf -> max_vrfy )
923 my $PROB = 'last probes : ' ;
924 push @stats, "$ok were ok" if $ok ;
925 for my $stat ( sort keys %stats )
926 { ( my $txt = $stat ) =~ s/_/ /g ;
927 push @stats, sprintf "%s had %s" , $stats { $stat } , RED $txt ;
929 $PROB .= join ', ', @stats ;
931 my ( $mean, $median, $stddev ) = $self -> age_avg ;
932 my $AVGS = "mean mirror age is " ;
933 unless ( defined $mean )
934 { $AVGS = "<I>undefined</I>" ; }
936 { $AVGS .= sprintf "%s", pr_interval $mean ;
937 if ( defined $stddev )
938 { $AVGS .= sprintf ", std_dev %s", pr_interval $stddev ; }
939 $AVGS .= sprintf ", median %s", pr_interval $median ;
942 for my $reg ( sort keys %tab )
943 { $refs .= sprintf " %s \n"
944 , URL "#$reg", "<FONT SIZE=\"+1\">$reg</FONT>"
949 my $NAME = $conf -> project_name ;
950 my $LOGO = $conf -> project_logo
952 ( $conf -> project_url
954 ( '<IMG SRC="%s" ALT="%s" ALIGN="RIGHT" BORDER=0>'
955 , $conf -> project_logo
956 , $conf -> project_name
961 my $HEAD = $conf -> htm_head . "\n" ;
962 my $HTOP = $conf -> htm_top . "\n" ;
963 my $FOOT = $conf -> htm_foot . "\n" ;
964 my $TITL = URL $conf -> project_url, $NAME ;
965 my $EXPD = Base::exp_date ;
966 my $DATE = scalar gmtime $^T ;
967 my $LAST = scalar gmtime ( $get ? $^T : ( stat $conf -> state ) [9] ) ;
969 my $histo_top = $self -> gen_histogram ( 'top' ) ;
970 my $histo_bot = $self -> gen_histogram ( 'bottom' ) ;
972 open PPP, ">$TMP" or die "can't write $TMP ($!)" ;
973 my $prev_select = select PPP ;
975 my $attr1 = "COLSPAN=$COLS BGCOLOR=LIME" ;
976 my $attr2 = 'BGCOLOR=AQUA' ;
977 my $attr3 = "COLSPAN=$COLS BGCOLOR=YELLOW" ;
979 my $num_mirrors = scalar keys %$state ;
980 my $num_regions = scalar keys %tab ;
983 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
986 <TITLE>the status of $NAME mirrors</TITLE>
987 <META HTTP-EQUIV="content-type" CONTENT="text/html; charset=utf-8">
988 <META HTTP-EQUIV=refresh CONTENT=3600>
989 <META HTTP-EQUIV=Expires CONTENT=\"$EXPD\">
992 <BODY BGCOLOR=\"#FFFFFF\">
994 <H2>the status of $TITL mirrors</H2>
995 <TABLE BORDER=0 CELLPADDING=2>
996 <TR><TD>date</TD><TD>:</TD><TD>$DATE (UTC)</TD></TR>
997 <TR><TD>last check</TD>
1005 <BLOCKQUOTE><CENTER>\n$refs\n</CENTER></BLOCKQUOTE>
1008 <TABLE BORDER=2 CELLPADDING=5>
1009 <TR><TH $attr1>$num_mirrors sites in $num_regions regions</TH></TR>
1010 <TR><TH $attr1>$STAT</TH></TR>
1011 <TR><TH $attr1>$PROB</TH></TR>
1012 <TR><TH $attr1>$AVGS</TH></TR>
1014 <TH $attr2>$NAME site -- home</TH>
1015 <TH $attr2>type</TH>
1016 <TH $attr2>mirror age,<BR>daily stats</TH>
1017 <TH $attr2>last probe,<BR>probe stats</TH>
1018 <TH $attr2>last stat</TH>
1024 { ( $CCS -> { $a } || $a ) cmp ( $CCS -> { $b } || $b ) ; }
1027 { my $mirrors = $tab { $reg } ;
1029 my $ccs = exists $CCS -> { $reg } ? $CCS -> { $reg } : $reg ;
1031 ( scalar @{ $mirrors } > 6
1032 ? sprintf "%s - %d sites"
1033 , $ccs, scalar @{ $mirrors }
1036 printf "<TR><TH $attr3>$ccs</TH></TR>\n" ;
1038 for my $mirror ( sort { $a -> cmp ( $b ) } @$mirrors )
1040 printf " <TD ALIGN=RIGHT>%s %s</TD>\n <TD>%s</TD>\n"
1041 , $mirror -> site_url
1042 , $mirror -> home_url
1046 my ( $url, $time, $stat, $vrfy, $hstp, $hsts ) =
1047 $mirror -> as_list ;
1048 my $pr_time = $time =~ /^\d+$/
1049 ? pr_diff $time, $^T - $conf -> max_age2 : ' ' ;
1050 my $pr_last = $vrfy =~ /^\d+$/
1051 ? pr_diff $vrfy, $^T - $conf -> max_vrfy : ' ' ;
1052 my $pr_hstp = $self -> show_hist ( $hstp ) ;
1053 my $pr_hsts = $self -> show_hist ( $hsts ) ;
1055 if ( $stat ne 'ok' ) { $stat =~ s/_/ /g ; $stat = RED $stat ; }
1056 printf " <TD ALIGN=RIGHT>%s<BR>%s</TD>\n" , $pr_time, $pr_hsts ;
1057 printf " <TD ALIGN=RIGHT>%s<BR>%s</TD>\n" , $pr_last, $pr_hstp ;
1058 printf " <TD>%s</TD>\n", $stat ;
1063 my $legend = $self -> legend ;
1064 my $probes = $self -> gen_histogram_probes ;
1065 my $mir_img = sprintf
1066 '<IMG BORDER=2 ALT=mirmon SRC="%s/mirmon.gif">' , $conf -> icons ;
1073 <H3>probe results</H3>
1079 <TH><A HREF=\"$HOME\">$mir_img</A></TH>
1089 select $prev_select ;
1091 if ( print PPP "\n" )
1094 { warn "wrote empty html file; keeping previous version" ; }
1096 { rename $TMP, $PPP or die "can't rename $TMP, $PPP ($!)" ; }
1099 { die "can't print to $TMP ($!)" ; }
1102 package Mirmon::Conf ; #############################################
1104 BEGIN { use base 'Base' ; Base -> import () ; }
1107 ( project_logo => ''
1108 , timeout => $DEF_TIMEOUT
1114 , list_style => 'plain'
1115 , put_histo => 'top'
1124 qw( web_page state countries mirror_list probe
1125 project_name project_url icons
1128 for ( @REQ_KEYS, keys %CNF_defaults ) { $CNF_KEYS { $_ } ++ ; }
1130 my @LIST_STYLE = qw(plain apache) ;
1131 my @PUT_HGRAM = qw(top bottom nowhere) ;
1133 eval Base -> mk_methods ( keys %CNF_KEYS, qw(root site_url) ) ;
1136 { my $self = shift ;
1138 my $res = bless { %CNF_defaults }, $self ;
1139 $res -> root ( $FILE ) ;
1140 $res -> site_url ( {} ) ;
1141 $res -> get_conf () ;
1145 { my $self = shift ;
1146 my $FILE = ( @_ ? shift : $self -> root ) ;
1148 if ( grep $_ eq $FILE, @{ $self -> {_include} } )
1149 { die "already included : '$FILE'" ; }
1151 { push @{ $self -> {_include} }, $FILE ; }
1153 open FILE, $FILE or die "can't open '$FILE' ($!)" ;
1154 my $CONF = join "\n", grep /./, <FILE> ;
1157 $CONF =~ s/\t/ /g ; # replace tabs
1158 $CONF =~ s/^[+ ]+// ; # delete leading space, plus
1159 $CONF =~ s/\n\n\s+/ /g ; # glue continuation lines
1160 $CONF =~ s/\n\n\+\s+//g ; # glue concatenation lines
1161 $CONF =~ s/\n\n\./\n/g ; # glue concatenation lines
1164 print "--$CONF--\n" if Mirmon::debug ;
1165 for ( grep ! /^#/, split /\n\n/, $CONF )
1166 { my ($key,$val) = split ' ', $_, 2 ;
1167 $val = '' unless defined $val ;
1168 print "conf '$FILE' : key '$key', val '$val'\n" if Mirmon::debug ;
1169 if ( exists $CNF_KEYS { $key } )
1170 { $self -> $key ( $val ) ; }
1171 elsif ( $key eq 'site_url' )
1172 { my ( $site, $url ) = split ' ' , $val ;
1173 $url .= '/' if $self -> add_slash and $url !~ m!/$! ;
1174 $self -> site_url -> { $site } = $url ;
1175 # printf "config : for site '%s' use instead\n '%s'\n",
1176 # $site, $url if Mirmon::verbose ;
1178 elsif ( $key eq 'no_add_slash' )
1179 { $self -> add_slash ( 0 ) ; }
1180 elsif ( $key eq 'no_randomize' )
1181 { $self -> randomize ( 0 ) ; }
1182 elsif ( $key eq 'show' )
1183 { $self -> show_conf if Mirmon::verbose ; }
1184 elsif ( $key eq 'exit' )
1185 { die 'exit per config directive' ; }
1186 elsif ( $key eq 'include' )
1187 { $self -> get_conf ( $val ) ; }
1188 elsif ( $key eq 'env' )
1189 { my ( $x, $y ) = split ' ' , $val ;
1191 printf "config : setenv '%s'\n '%s'\n", $x, $y
1192 if Mirmon::verbose ;
1195 { $self -> show_conf ;
1196 die "unknown keyword '$key' (value '$val')\n" ;
1199 my $err = $self -> check ;
1205 { my $self = shift ;
1207 for my $key ( @REQ_KEYS )
1208 { unless ( exists $self -> { $key } )
1209 { $err .= "error: missing config for '$key'\n" ; }
1211 for my $key ( qw(min_poll max_poll max_sync min_sync) )
1212 { my $max = $self -> $key ;
1213 unless ( $max =~ /$TIM_PAT/o )
1214 { $err .= "error: bad timespec for $key ($max)\n" ; }
1216 unless ( grep $self -> { list_style } eq $_, @LIST_STYLE )
1217 { $err .= sprintf "error: unknown 'list_style' '%s'\n",
1218 $self -> list_style ;
1220 unless ( grep $self -> put_histo eq $_, @PUT_HGRAM )
1221 { $err .= sprintf "%s : error: unknown 'put_histo' '%s'\n",
1222 $self -> put_histo ;
1228 { my $self = shift ;
1229 print "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" ;
1230 for my $key ( sort keys %$self )
1231 { next if $key =~ m/^_/ ;
1232 my $val = $self -> { $key } ;
1233 print "show_conf : $key = '$val'\n" ;
1235 for my $key ( sort keys %{ $self -> site_url } )
1236 { printf "show_conf : for site '%s' use instead\n '%s'\n"
1237 , $key, $self -> site_url -> { $key } if Mirmon::verbose ;
1239 printf "show_conf : included '%s'\n"
1240 , join "', '", @{ $self -> {_include} } ;
1241 print "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" ;
1245 { my $self = shift ;
1246 ( s4tim $self -> min_sync ) + ( s4tim $self -> max_poll ) ;
1250 { my $self = shift ;
1251 ( s4tim $self -> max_sync ) + ( s4tim $self -> max_poll ) ;
1255 { my $self = shift ;
1256 ( s4tim $self -> min_poll ) + ( s4tim $self -> max_poll ) ;
1260 { my $self = shift ;
1262 return 'z' unless $time =~ /^\d+$/ ;
1264 ( ( aprx_ge ( $time, $^T - $self -> max_age1 ) )
1266 : ( aprx_ge ( $time, $^T - $self -> max_age2 ) ? 'b' : 'f' )
1270 package Mirmon::Mirror ; ###########################################
1272 BEGIN { use base 'Base' ; Base -> import () ; }
1277 qw(url age last_status last_ok_probe probe_history state_history last_probe) ;
1279 eval Base -> mk_methods ( @FIELDS, qw(mirmon region mail) ) ;
1281 sub state_history_time
1282 { my $self = shift ;
1283 my $res = ( split /-/, $self -> state_history ) [ 0 ] ;
1287 sub state_history_hist
1288 { my $self = shift ;
1289 my $res = ( split /-/, $self -> state_history ) [ 1 ] ;
1294 { my $self = shift ;
1295 my $url = $self -> url ;
1296 my ( $type, $site, $home ) ;
1297 if ( $url =~ m!^(ftp|https?|rsync)://([^/:]+)(:\d+)?/! )
1298 { $type = $1 ; $site = $2 ; $home = $& ; }
1299 return $type, $site, $home ;
1302 sub type { my $self = shift ; ( $self -> _parse ) [ 0 ] ; }
1303 sub site { my $self = shift ; ( $self -> _parse ) [ 1 ] ; }
1304 sub home { my $self = shift ; ( $self -> _parse ) [ 2 ] ; }
1307 { my $self = shift ;
1309 my $age = $self -> age ;
1310 if ( $age eq 'undef' )
1311 { $res = length $self -> state_history_hist
1312 if $self -> last_probe ne 'undef' ;
1315 { $res = ( $^T - $age ) / 24 / 60 / 60 ; }
1320 { my $self = shift ;
1321 my $mirmon = shift ;
1323 my $res = bless { mirmon => $mirmon }, $self ;
1324 @{ $res } { @FIELDS } = ( 'undef' ) x scalar @FIELDS ;
1325 $res -> url ( $url ) ;
1326 $res -> probe_history ( '' ) ;
1327 $res -> state_history ( "$^T-z" ) ;
1328 $res -> mail ( '' ) ;
1333 { my $self = shift ;
1334 my $mirmon = shift ;
1336 my $res = bless { mirmon => $mirmon }, $self ;
1337 @{ $res } { @FIELDS } = split ' ', $line ;
1338 $res -> mail ( '' ) ;
1343 { my $self = shift ;
1347 my $probe_hist = $self -> probe_history ;
1349 { $self -> age ( $time ) ;
1350 $self -> last_ok_probe ( $^T ) ;
1351 $probe_hist .= 's' ;
1354 { $probe_hist .= 'f' ;
1355 $time = $self -> age ;
1358 my $h = $self -> state_history_hist ;
1359 my $t = $self -> state_history_time ;
1361 if ( aprx_ge ( $^T - $t, $HIST_DELTA ) )
1362 { my $n = int ( ( $^T - $t ) / $HIST_DELTA ) ;
1363 $h .= 'x' x ( $n - 1 ) ;
1364 $t = ( $n == 1 ? $t + $HIST_DELTA : $^T ) ;
1368 $h .= $self -> mirmon -> conf -> age_code ( $time ) ;
1369 $h = substr $h, - $HIST ;
1372 $self -> last_status ( $stat ) ;
1373 $self -> probe_history ( substr $probe_hist, - $HIST ) ;
1374 $self -> last_probe ( $^T ) ;
1375 $self -> state_history ( "$t-$h" ) ;
1378 sub as_list { my $self = shift ; @{ $self } { @FIELDS } ; }
1379 sub state { my $self = shift ; join ' ', $self -> as_list ; }
1382 { my $self = shift ;
1383 my $conf = $self -> mirmon -> conf ;
1384 my $probe = $conf -> probe ;
1385 my $timeout = $conf -> timeout ;
1386 $probe =~ s/%TIMEOUT%/$timeout/g ;
1387 my $url = $self -> url ;
1388 my $new = $conf -> site_url -> { $self -> site } ;
1390 { printf "*** site_url : site %s\n -> url %s\n"
1391 , $self -> site, $new if Mirmon::verbose ;
1394 $probe =~ s/%URL%/$url/g ;
1395 my $pipe = new IO::Pipe ;
1396 my $handle = $pipe -> reader ( split ' ', $probe ) ;
1398 { $pipe -> blocking ( 0 ) ; }
1400 { die "start_probe : no pipe for $url" ; }
1401 printf "start %s\n", $url if Mirmon::verbose ;
1402 printf " %s\n", $probe if Mirmon::debug ;
1407 { my $self = shift ;
1408 my $handle = shift ;
1414 $handle -> blocking ( 1 ) ;
1415 if ( $handle -> eof () )
1416 { printf "finish eof %s\n", $self -> url if Mirmon::verbose ; }
1418 { $res = $handle -> getline () ; }
1422 unless ( defined $res )
1423 { $stat = 'no_time' ; }
1424 elsif ( $res =~ /^\s*$/ )
1425 { $stat = 'empty' ; }
1427 { $res = ( split ' ', $res ) [ 0 ] ;
1429 if ( $res !~ /^\d+$/ )
1431 $res = Base::htmlquote $res ;
1432 $res = substr ( $time, 0, 15 ) . '..' if length $res > 15 ;
1436 { $succ = 1 ; $stat = 'ok' ; $time = $res ; }
1439 printf "finish %s\n succ(%s) stat(%s) time(%s)\n"
1443 , ( defined $time ? $time : 'undef' )
1444 if Mirmon::verbose ;
1446 $self -> update ( $succ, $stat, $time ) ;
1449 sub revdom { my $dom = shift ; join '.', reverse split /\./, $dom ; }
1454 ( revdom $a -> site ) cmp ( revdom $b -> site )
1456 ( $a -> type cmp $b -> type )
1463 $hrf =~ /^rsync/ ? $txt : URL $hrf, $txt ;
1466 sub site_url { my $self = shift ; _url $self -> url , $self -> site ; }
1467 sub home_url { my $self = shift ; _url $self -> home, '@' ; }
1473 Mirmon - OO interface for mirmon objects
1479 $m = Mirmon -> new ( [ $path-to-config ] )
1481 $conf = $m -> conf ; # a Mirmon::Conf object
1482 $state = $m -> state ; # the mirmon state
1484 for my $url ( keys %$state )
1485 { $mirror = $state -> { $url } ; # a Mirmon::Mirror object
1486 $mail = $mirror -> mail ; # contact address
1487 $mirror -> age ( time ) ; # set mirror age
1490 Many class and object methods can be used to get or set attributes :
1492 $object -> attribute # get an atttibute
1493 $object -> attribute ( $attr ) # set an atttibute
1495 =head1 Mirmon class methods
1499 =item B<new ( [$path] )>
1501 Create a Mirmon object from a config file found in $path,
1502 or (by default) in the default list of possible config files.
1503 Related objects (config, state) are created and initialised.
1507 Mirmon always reports errors. Normally it only reports
1508 changes (inserts/deletes) found in the mirror_list ;
1509 in I<quiet> mode, it doesn't. In I<verbose> mode, it
1510 reports progress: the startup and finishing of probes.
1512 Mirmon::verbose ( [ $bool ] ) # get/set verbose
1513 Mirmon::quiet ( [ $bool ] ) # get/set quiet
1514 Mirmon::debug ( [ $bool ] ) # get/set debug
1518 =head1 Mirmon object methods
1524 Returns Mirmon's Mirmon::Conf object.
1528 Returns a hashref C<< { url => mirror, ... } >>,
1529 where I<url> is as specified in the mirror list
1530 and I<mirror> is a Mirmon::Mirror object.
1534 Returns a hashref C<< { country_code =E<gt> country_name, ... } >>.
1536 =item B<config_list>
1538 Returns the list of default locations for config files.
1540 =item B<get_dates ( $get )>
1542 Probes all mirrors if $get is C<all> ; or a subset if $get is C<update>.
1546 =head1 Mirmon::Conf object methods
1548 A Mirmon::Conf object represents a mirmon conguration.
1549 It is normaly created by Mirmon::new().
1550 A specified (or default) config file is read and interpreted.
1554 =item attribute methods
1556 For every config file entry, there is an attribute method :
1557 B<web_page>, B<state>, B<countries>, B<mirror_list>, B<probe>,
1558 B<project_name>, B<project_url>, B<icons>, B<project_logo>,
1559 B<timeout>, B<max_probes>, B<min_poll>, B<max_poll>, B<min_sync>,
1560 B<max_sync>, B<list_style>, B<put_histo>, B<randomize>, B<add_slash>.
1564 Returns the file name of (the root of) the configuration file(s).
1568 Returns a hashref C<< { site => url, ... } >>,
1569 as specified in the mirmon config file.
1573 =head1 Mirmon::Mirror object methods
1575 A Mirmon::Mirror object represents the last known state of a mirror.
1576 It is normaly created by Mirmon::new() from the state file,
1577 as specified in the mirmon config file.
1578 Mirmon::Mirror objects can be used to probe mirrors.
1580 =head2 attribute methods
1586 The url as given in the mirror list.
1590 The mirror's timestamp found by the last succesful probe,
1591 or 'undef' if no probe was ever successful.
1593 =item B<last_status>
1595 The status of the last probe, or 'undef' if the mirror was never probed.
1597 =item B<last_ok_probe>
1599 The timestamp of the last succesful probe or 'undef'
1600 if the mirror was never successfully probed.
1602 =item B<probe_history>
1604 The probe history is a list of 's' (for success) and 'f' (for failure)
1605 characters indicating the result of the probe. New results are appended
1606 whenever the mirror is probed.
1608 =item B<state_history>
1610 The state history consists of a timestamp, a '-' char, and a list of
1611 chars indicating a past status: 's' (fresh), 'b' (oldish), 'f' (old),
1612 'z' (bad) or 'x' (skip).
1613 The timestamp indicates when the state history was last updated.
1614 The current status of the mirror is determined by the mirror's age and
1615 a few configuration parameters (min_sync, max_sync, max_poll).
1616 The state history is updated when the mirror is probed.
1617 If the last update of the history was less than 24 hours ago,
1618 the last status is replaced by the current status.
1619 If the last update of the history was more than 24 hours ago,
1620 the current status is appended to the history.
1621 One or more 'skip's are inserted, if the timestamp is two or more days old
1622 (when mirmon hasn't run for more than two days).
1626 The timestamp of the last probe, or 'undef' if the mirror was never probed.
1630 =head2 object methods
1636 Returns the parent Mirmon object.
1638 =item B<state_history_time>
1640 Returns the I<time> part of the state_history attribute.
1642 =item B<state_history_hist>
1644 Returns the I<history> part of the state_history attribute.
1646 =item B<type>, B<site>, B<home>
1648 For an url like I<ftp://www.some.org/path/to/home>,
1649 the B<type> is I<ftp>,
1650 the B<site> is I<www.some.org>,
1651 and B<home> is I<ftp://www.some.org/>.
1653 =item B<age_in_days>
1655 Returns the mirror's age (in fractional days), based on the mirror's
1656 timestamp as found by the last successful probe ; or based on the
1657 length of the state history if no probe was ever successful.
1658 Returns 'undef' if the mirror was never probed.
1662 Returns the mirror's contact address as specified in the mirror list.
1666 Returns the mirror's country code as specified in the mirror list.
1668 =item B<start_probe>
1670 Start a probe for the mirror in non-blocking mode ;
1671 returns the associated (IO::Handle) file handle.
1672 The caller must maintain an association between
1673 the handles and the mirror objects.
1675 =item B<finish_probe ( $handle )>
1677 Sets the (IO::Handle) B<$handle> to blocking IO ;
1678 reads a result from the handle,
1679 and updates the state of the mirror.
1687 <A HREF="mirmon.html">mirmon(1)</A>
1702 <A HREF="http://people.cs.uu.nl/henkp/">Henk P. Penning</A>,
1703 <A HREF="http://www.cs.uu.nl/">Computer Science Department</A>,
1704 <A HREF="http://www.uu.nl/">Utrecht University</A>
1706 mirmon-2.3 - Wed Mar 17 09:29:11 2010 ; henkp
1712 (c) 2003-2010 Henk P. Penning
1713 Computer Science Department, Utrecht University
1714 http://people.cs.uu.nl/henkp/ -- penning@cs.uu.nl
1715 mirmon-2.3 - Wed Mar 17 09:29:11 2010 ; henkp
1721 (c) 2003-2010 Henk P. Penning
1722 Computer Science Department, Utrecht University
1723 http://people.cs.uu.nl/henkp/ -- penning@cs.uu.nl
1724 mirmon-2.3 - Wed Mar 17 09:29:11 2010 ; henkp
1730 package main ; #####################################################
1736 my $VERSION = Base::Version . ' - Wed Mar 17 09:29:11 2010 - henkp' ;
1737 my $DEF_CNF = join ', ', Mirmon -> config_list ;
1738 my $TIMEOUT = Base::DEF_TIMEOUT ;
1740 my $prog = substr $0, rindex ( $0, '/' ) + 1 ;
1741 my $Usage = <<USAGE ;
1742 Usage: $prog [ -v ] [ -q ] [ -t timeout ] [ -get opt ] [ -c conf ]
1743 option v : be verbose
1745 option t : set timeout ; default $TIMEOUT
1746 option get : 'all' : probe all sites
1747 : 'update' : probe a selection of the sites (see doc)
1748 option c : configuration file ; default search :
1750 -------------------------------------------------------------------
1751 Mirmon normally only reports errors and changes in the mirror list.
1753 -------------------------------------------------------------------
1755 sub Usage { die "$_[0]$Usage" ; }
1756 sub Error { die "$prog: $_[0]\n" ; }
1757 sub Warn { warn "$prog: $_[0]\n" ; }
1759 # usage: &GetOptions(ARG,ARG,..) defines $opt_ID as 1 or user spec'ed value
1760 # usage: &GetOptions(\%opt,ARG,ARG,..) defines $opt{ID} as 1 or user value
1761 # ARG = 'ID' | 'ID=SPC' | 'ID:SPC' for no-arg, required-arg or optional-arg
1762 # ID = perl identifier
1763 # SPC = i|f|s for integer, fixedpoint real or string argument
1766 Getopt::Long::config ( 'no_ignore_case' ) ;
1768 Usage '' unless GetOptions ( \%opt, qw(v q t=i get=s c=s version) ) ;
1769 Usage "Arg count\n" unless @ARGV == 0 ;
1771 if ( $opt{version} ) { printf "%s\n", Base::version () ; exit ; }
1773 $opt{v} ||= $opt{d} ;
1775 my $get = $opt{get} ;
1776 if ( $get and ! Base::is_get_opt ( $get ) )
1777 { Error "unknown 'get option' '$get'" ; }
1779 Mirmon::verbose ( $opt{v} ) ;
1780 Mirmon::debug ( $opt{d} ) ;
1781 Mirmon::quiet ( $opt{q} ) ;
1783 my $M = Mirmon -> new ( $opt{c} ) ;
1784 $M -> conf -> timeout ( $opt{t} ) if $opt{t} ;
1785 if ( $get ) { $M -> get_dates ( $get ) ; $M -> put_state ; }
1786 $M -> gen_page ( $get, $VERSION ) ;
1794 mirmon - monitor the state of mirrors
1798 mirmon [ -v ] [ -q ] [ -t timeout ] [ -get opt ] [ -c conf ]
1802 option v : be verbose
1804 option t : set timeout [ default 300 ] ;
1805 option get : 'all' : probe all sites
1806 : 'update' : probe a selection of the sites (see doc)
1807 option c : configuration file ; default list :
1808 ./mirmon.conf $HOME/.mirmon.conf /etc/mirmon.conf
1809 -------------------------------------------------------------------
1810 Mirmon normally only reports errors and changes in the mirror list.
1811 -------------------------------------------------------------------
1815 The program is intended to be run by cron every hour.
1817 42 * * * * perl /path/to/mirmon -get update
1819 It quietly probes a subset of the sites in a given list,
1820 writes the results in the 'state' file and generates a web page
1821 with the results. The subset contains the sites that are new, bad
1822 and/or not probed for a specified time.
1824 When no 'get' option is specified, the program just generates a
1825 new web page from the last known state.
1827 The program checks the mirrors by running a (user specified)
1828 program on a pipe. A (user specified) number of probes is
1829 run in parallel using nonblocking IO. When something can be
1830 read from the pipe, it switches the pipe to blocking IO and
1831 reads one line from the pipe. Then it flushes and closes the
1832 pipe. No attempt is made to kill the probe.
1834 The probe should return something that looks like
1838 that is, a line of text starting with a timestamp. The exit status
1839 of the probe is ignored.
1845 A config file can be specified with the -c option.
1846 If -c is not used, the program looks for a config file in
1850 =item * B<./mirmon.conf>
1852 =item * B<$HOME/.mirmon.conf>
1854 =item * B</etc/mirmon.conf>
1860 A config file looks like this :
1862 +--------------------------------------------------
1863 |# lines that start with '#' are comment
1864 |# blank lines are ignored too
1865 |# tabs are replaced by a space
1867 |# the config entries are 'key' and 'value' pairs
1868 |# a 'key' begins in column 1
1869 |# the 'value' is the rest of the line
1870 |somekey A_val B_val ...
1871 |otherkey X_val Y_val ...
1873 |# indented lines are glued
1874 |# the next three lines mean 'somekey part1 part2 part3'
1879 |# lines starting with a '+' are concatenated
1880 |# the next three lines mean 'somekey part1part2part3'
1885 |# lines starting with a '.' are glued too
1886 |# don't use a '.' on a line by itself
1887 |# 'somekey' gets the value "part1\n part2\n part3"
1891 +--------------------------------------------------
1893 =head1 CONFIG FILE : required entries
1895 =head2 project_name I<name>
1897 Specify a short plaintext name for the project.
1902 =head2 project_url I<url>
1904 Specify an url pointing to the 'home' of the project.
1906 project_url http://www.apache.org/
1908 =head2 mirror_list I<file-name>
1910 Specify the file containing the mirrors to probe.
1912 mirror_list /path/to/mirror-list
1914 If your mirror list is generated by a program, use
1916 mirror_list /path/to/program arg1 ... |
1918 Two formats are supported :
1922 =item * plain : lines like
1924 us http://www.tux.org/ [email] ...
1925 nl http://apache.cs.uu.nl/dist/ [email] ...
1926 nl rsync://archive.cs.uu.nl/apache-dist/ [email] ...
1928 =item * apache : lines like those in the apache mirrors.list
1930 ftp us ftp://ftp.tux.org/pub/net/apache/dist/ user@tux.org ...
1931 http nl http://apache.cs.uu.nl/dist/ user@cs.uu.nl ...
1935 Note that in style 'plain' the third item is reserved for an
1936 optional email address : the site's contact address.
1938 Specify the required format with 'list_style' (see below).
1939 The default style is 'plain'.
1941 =head2 web_page I<file-name>
1943 Specify where the html report page is written.
1945 =head2 icons I<directory-name>
1947 Specify the directory where the icons can be found,
1948 relative to the I<web_page>, or relative to the
1949 DOCUMENTROOT of the web server.
1951 If/when the I<web_page> lives in directory C<.../mirmon/> and
1952 the icons live in directory C<.../mirmon/icons/>,
1957 If/when the icons live in C</path/to/DOCUMENTROOT/icons/mirmon/>, specify
1961 =head2 probe I<program + arguments>
1963 Specify the program+args to probe the mirrors. Example:
1965 probe /usr/bin/wget -q -O - -T %TIMEOUT% -t 1 %URL%TIME
1967 Before the program is started, %TIMEOUT% and %URL% are
1968 substituted with the proper timeout and url values.
1970 Here it is assumed that each hour the root server writes
1971 a timestamp in /path/to/archive/TIME, for instance with
1972 a crontab entry like
1974 42 * * * * perl -e 'printf "%s\n", time' > /path/to/archive/TIME
1976 Mirmon reads one line of output from the probe and interprets
1977 the first word on that line as a timestamp ; for example :
1980 1043625600 Mon Jan 27 00:00:00 2003
1981 1043625600 www.apache.org Mon Jan 27 00:00:00 2003
1983 Mirmon is distributed with a program C<probe> that handles
1984 ftp, http and rsync urls.
1986 =head2 state I<file-name>
1988 Specify where the file containing the state is written.
1990 The program reads this file on startup and writes the
1991 file when mirrors are probed (-get is specified).
1993 =head2 countries I<file-name>
1995 Specify the file containing the country codes;
1996 The file should contain lines like
2001 The mirmon package contains a recent ISO list.
2003 =head1 CONFIG FILE : optional entries
2005 =head2 max_probes I<number>
2007 Optionally specify the number of parallel probes (default 25).
2009 =head2 timeout I<seconds>
2011 Optionally specify the timeout for the probes (default 300).
2013 After the last probe is started, the program waits for
2014 <timeout> + 10 seconds, cleans up and exits.
2016 =head2 project_logo I<logo>
2018 Optionally specify (the SRC of the IMG of) a logo to be placed
2019 top right on the page.
2021 project_logo /icons/apache.gif
2022 project_logo http://www.apache.org/icons/...
2024 =head2 htm_head I<html>
2026 Optionally specify some HTML to be placed before </HEAD>.
2029 <link REL=StyleSheet HREF="/style.css" TYPE="text/css">
2031 =head2 htm_top I<html>
2033 Optionally specify some HTML to be placed near the top of the page.
2035 htm_top testing 1, 2, 3
2037 =head2 htm_foot I<html>
2039 Optionally specify HTML to be placed near the bottom of the page.
2043 <A HREF="..."><IMG SRC="..." BORDER=0></A>
2046 =head2 put_histo top|bottom|nowhere
2048 Optionally specify where the age histogram must be placed.
2049 The default is 'top'.
2051 =head2 min_poll I<time-spec>
2053 For 'min_poll' see next item. A I<time-spec> is a number followed by
2054 a unit 's' (seconds), or 'm' (minutes), or 'h' (hours), or 'd' (days).
2055 For example '3d' (three days) or '36h' (36 hours).
2057 =head2 max_poll I<time-spec>
2059 Optionally specify the maximum probe interval. When the program is
2060 called with option '-get update', all sites are probed which are :
2066 the site appears in the list, but there is no known state
2070 the last probe of the site was unsuccessful
2074 the last probe was more than 'max_poll' ago.
2078 Sites are not probed if the last probe was less than 'min_poll' ago.
2084 the 'reachable' sites are probed twice daily and the 'unreachable'
2085 sites are probed at most six times a day.
2087 The default 'min_poll' is '1h' (1 hour).
2088 The default 'max_poll' is '4h' (4 hours).
2090 =head2 min_sync I<time-spec>
2092 Optionally specify how often the mirrors are required to make an update.
2094 The default 'min_sync' is '1d' (1 day).
2096 =head2 max_sync I<time-spec>
2098 Optionally specify the maximum allowable sync interval.
2100 Sites exceeding the limit will be considered 'old'.
2101 The default 'max_sync' is '2d' (2 days).
2105 To balance the probe load over the hourly mirmon runs,
2106 mirmon may probe a few extra randomly choosen mirrors :
2110 =item * only if the the number of mirrors to probe is below average,
2112 =item * at most 2% of the mirrors
2116 If you don't want this behaviour, use B<no_randomize>.
2120 If the url part of a line in the mirror_list doesn't end
2121 in a slash ('/'), mirmon adds a slash and issues a warning
2122 unless it is in quiet mode.
2124 If you don't want this behaviour, use B<no_add_slash>.
2126 =head2 list_style plain|apache
2128 Optionally specify the format ('plain' or 'apache') of the mirror-list.
2130 See the description of 'mirror_list' above.
2131 The default list_style is 'plain'.
2133 =head2 site_url I<site> I<url>
2135 Optionally specify a substitute url for a site.
2137 When access to a site is restricted (in Australia, for instance),
2138 another (sometimes secret) url can be used to probe the site.
2139 The <site> of an url is the part between '://' and the first '/'.
2141 =head2 env I<key> I<value>
2143 Optionally specify an environment variable.
2145 =head2 include I<file-name>
2147 Optionally specify a file to include.
2149 The specified file is processed 'in situ'. After the specified file is
2150 read and processed, config processing is resumed in the file where the
2151 C<include> was encountered.
2152 The include depth is unlimited. However, it is a fatal error to
2153 include a file twice under the same name.
2157 When the config processor encounters the 'show' command, it
2158 dumps the content of the current config to standout, if option
2159 C<-v> is specified. This is intented for debugging.
2163 When the config processor encounters the 'exit' command, it
2164 terminates the program. This is intented for debugging.
2166 =head1 STATE FILE FORMAT
2168 The state file consists of lines; one line per site.
2169 Each line consists of white space separated fields.
2170 The seven fields are :
2174 =item * field 1 : url
2176 The url as given in the mirror list.
2178 =item * field 2 : age
2180 The mirror's timestamp found by the last succesful probe,
2181 or 'undef' if no probe was ever successful.
2183 =item * field 3 : status last probe
2185 The status of the last probe, or 'undef' if the mirror was never probed.
2187 =item * field 4 : time last succesful probe
2189 The timestamp of the last succesful probe or 'undef'
2190 if the mirror was never successfully probed.
2192 =item * field 5 : probe history
2194 The probe history is a list of 's' (for success) and 'f' (for failure)
2195 characters indicating the result of the probe. New results are appended
2196 whenever the mirror is probed.
2198 =item * field 6 : state history
2200 The state history consists of a timestamp, a '-' char, and a list of
2201 chars indicating a past status: 's' (fresh), 'b' (oldish), 'f' (old),
2202 'z' (bad) or 'x' (skip).
2203 The timestamp indicates when the state history was last updated.
2204 The current status of the mirror is determined by the mirror's age and
2205 a few configuration parameters (min_sync, max_sync, max_poll).
2206 The state history is updated when the mirror is probed.
2207 If the last update of the history was less than 24 hours ago,
2208 the last status is replaced by the current status.
2209 If the last update of the history was more than 24 hours ago,
2210 the current status is appended to the history.
2211 One or more 'skip's is inserted, if the timestamp is two or more days old
2212 (when mirmon hasn't run for more than two days).
2214 =item * field 7 : last probe
2216 The timestamp of the last probe, or 'undef' if the mirror was never probed.
2226 =item * Note: The (empty) state file must exist before mirmon runs.
2228 =item * The mirmon repository is here :
2230 https://subversion.cs.uu.nl/repos/staff.henkp.mirmon/trunk/
2232 =item * The mirmon tarball is here :
2234 http://people.cs.uu.nl/henkp/mirmon/mirmon.tar.gz
2238 =head2 installation suggestions
2240 To install and configure mirmon, take the following steps :
2244 =item * First, make the webdir :
2249 For I<DOCUMENTROOT>, substitute the full pathname
2250 of the document root of your webserver.
2252 =item * Check out the mirmon repository :
2255 svn checkout REPO mirmon
2259 REPO = https://subversion.cs.uu.nl/repos/staff.henkp.mirmon/trunk/
2261 or download the package and unpack it.
2263 =item * Chdir to directory mirmon :
2267 =item * Create the (empty) state file :
2271 =item * Install the icons in the webdir :
2273 mkdir DOCUMENTROOT/mirmon/icons
2274 cp icons/* DOCUMENTROOT/mirmon/icons
2276 =item * Create a mirror list C<mirror_list> ;
2278 Use your favorite editor, or genererate the list from an
2281 nl http://archive.cs.uu.nl/your-project/ contact@cs.uu.nl
2282 uk http://mirrors.this.org/your-project/ mirrors@this.org
2283 us http://mirrors.that.org/your-project/ mirrors@that.org
2285 The email addresses are optional.
2287 =item * Create a mirmon config file C<mirmon.conf> with your favorite editor.
2289 # lines must start in the first column ; no leading white space
2292 mirror_list mirror_list
2294 countries countries.list
2295 web_page DOCUMENTROOT/mirmon/index.html
2297 probe /usr/bin/wget -q -O - -T %TIMEOUT% -t 1 %URL%TIME
2299 This assumes the project's timestamp is in file C<TIME>.
2301 =item * If you have rsync urls, change the probe line to :
2303 probe perl /usr/local/src/mirmon/probe -t %TIMEOUT% %URL%TIME
2305 =item * Run mirmon :
2307 perl mirmon -v -get all
2309 The mirmon report should now be in 'DOCUMENTROOT/mirmon/index.html'
2311 http://www.your.project.org/mirmon/
2313 =item * If/when, at a later date, you want to upgrade mirmon :
2315 cd /usr/local/src/mirmon
2325 <A HREF="mirmon.pm.html">mirmon.pm(3)</A>
2340 <A HREF="http://people.cs.uu.nl/henkp/">Henk P. Penning</A>,
2341 <A HREF="http://www.cs.uu.nl/">Computer Science Department</A>,
2342 <A HREF="http://www.uu.nl/">Utrecht University</A>
2344 mirmon-2.3 - Wed Mar 17 09:29:11 2010 ; henkp
2350 (c) 2003-2010 Henk P. Penning
2351 Computer Science Department, Utrecht University
2352 http://people.cs.uu.nl/henkp/ -- penning@cs.uu.nl
2353 mirmon-2.3 - Wed Mar 17 09:29:11 2010 ; henkp
2359 (c) 2003-2010 Henk P. Penning
2360 Computer Science Department, Utrecht University
2361 http://people.cs.uu.nl/henkp/ -- penning@cs.uu.nl
2362 mirmon-2.3 - Wed Mar 17 09:29:11 2010 ; henkp