3 # Copyright (c) 2003-2014 Henk Penning, all rights reserved.
4 # penning@uu.nl, http://www.staff.science.uu.nl/~penni101/
5 # Version 1.1 was donated to the Apache Software Foundation 2003 Jan 28.
7 # Permission is hereby granted, free of charge, to any person obtaining a
8 # copy of this software and associated documentation files (the "Software"),
9 # to deal in the Software without restriction, including without limitation
10 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11 # and/or sell copies of the Software, and to permit persons to whom the
12 # Software is furnished to do so, subject to the following conditions:
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 # DEALINGS IN THE SOFTWARE.
25 # Thanks to Klaus Heinz <heinz@NetBSD.org> for sugestions ao htm_head ;
26 # Peter Pöml for MirrorBrain support ; Jeremy Olexa, Karl Berry, Roland
27 # Pelzer for suggestions regarding rsync support.
34 our $DEF_TIMEOUT = 300 ;
36 our $TIM_PAT = '^(\d+)([smhd])$' ;
37 our %APA_TYPES = () ; $APA_TYPES { $_ } ++ for qw(backup ftp http rsync) ;
38 our %GET_OPTS = () ; $GET_OPTS { $_ } ++ for qw(all update url) ;
39 our $HIST_DELTA = 24 * 60 * 60 ;
40 our $APRX_DELTA = 300 ;
41 our $HOME = 'http://www.staff.science.uu.nl/~penni101/mirmon/' ;
43 package Base ; #####################################################
47 our ( @ISA, @EXPORT ) ;
49 { @ISA = qw(Exporter) ;
51 qw(aprx_eq aprx_ge aprx_le aprx_gt aprx_lt
52 URL NAM SMA BLD NSS TAB BQ TR TH TD TDr RED GRN H1 H2 H3
53 s4tim pr_interval pr_diff
57 sub Version { "$PRG version $VER" ; }
58 sub version { "$PRG-$VER" ; }
59 sub DEF_TIMEOUT { $DEF_TIMEOUT ; }
60 sub is_get_opt { my $opt = shift ; exists $GET_OPTS { $opt } ; }
65 if ( @_ ) { $self -> { $attr } = shift ; }
66 die "no attr '$attr'" unless exists $self -> { $attr } ;
73 sprintf 'sub %s { my $self = shift ; $self -> getset ( "%s", @_ ) ; }'
79 join "\n", map { Base -> mk_method ( $_ ) ; } @_ ;
82 sub aprx_eq { my ( $t1, $t2 ) = @_ ; abs ( $t1 - $t2 ) < $APRX_DELTA ; }
83 sub aprx_ge { my ( $t1, $t2 ) = @_ ; $t1 > $t2 or aprx_eq $t1, $t2 ; }
84 sub aprx_le { my ( $t1, $t2 ) = @_ ; $t1 < $t2 or aprx_eq $t1, $t2 ; }
85 sub aprx_gt { my ( $t1, $t2 ) = @_ ; $t1 > $t2 and not aprx_eq $t1, $t2 ; }
86 sub aprx_lt { my ( $t1, $t2 ) = @_ ; $t1 < $t2 and not aprx_eq $t1, $t2 ; }
88 sub URL { sprintf '<A HREF="%s">%s</A>', $_[0], $_[1] ; }
89 sub NAM { sprintf '<A NAME="%s">%s</A>', $_[0], $_[1] ; }
90 sub SMA { sprintf "<FONT SIZE=\"-1\">%s</FONT>", $_[0] ; }
91 sub BLD { sprintf "<B>%s</B>", $_[0] ; }
92 sub NSS { sprintf SMA('%s site%s'), $_[0], ( $_[0] == 1 ? '' : 's' ) ; }
93 sub TAB { sprintf "<TABLE BORDER=2 CELLPADDING=3>%s</TABLE>", $_[0] ; }
94 sub BQ { sprintf "<BLOCKQUOTE>\n%s\n</BLOCKQUOTE>\n", $_[0] ; }
95 sub TR { sprintf "<TR>%s</TR>\n", $_[0] ; }
96 sub TH { sprintf "<TH>%s</TH>\n", $_[0] ; }
97 sub TD { sprintf "<TD>%s</TD>\n", $_[0] ; }
98 sub H1 { sprintf "<H1>%s</H1>\n", $_[0] ; }
99 sub H2 { sprintf "<H2>%s</H2>\n", $_[0] ; }
100 sub H3 { sprintf "<H3>%s</H3>\n", $_[0] ; }
101 sub TDr { sprintf "<TD ALIGN=\"RIGHT\">%s</TD>\n", $_[0] ; }
102 sub RED { sprintf "<FONT COLOR=\"RED\">%s</FONT>", $_[0] ; }
103 sub GRN { sprintf '<FONT COLOR="GREEN">%s</FONT>', $_[0] ; }
107 my %tab = ( 's' => 1, 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24 ) ;
108 die "wrong time '$tim'" unless $tim =~ /$TIM_PAT/o ;
109 my $m = $1 ; my $u = $2 ;
110 return $m * $tab { $u } ;
115 my ( $magn, $unit ) ;
116 my $mins = $s / 60 ; my $m = int ( $mins + 0.5 ) ;
117 my $hours = $s / ( 60 * 60 ) ; my $h = int ( $hours + 0.5 ) ;
120 { $magn = $s ; $unit = 'second' ; }
122 { $magn = $m ; $unit = 'minute' ; }
124 { $magn = $h ; $unit = 'hour' ; }
126 { $magn = sprintf "%.1f", $hours / 24 ; $unit = 'day' ; }
128 $unit .= 's' unless $magn == 1 ;
130 return "$magn $unit" ;
139 { $res = BLD 'renewed' ; }
141 { $res = pr_interval $^T - $time ;
142 $res = BLD RED $res if aprx_lt $time, $max ;
148 { my @day = qw(Sun Mon Tue Wed Thu Fri Sat) ;
149 my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) ;
150 my @gmt = gmtime time + 3600 ;
151 sprintf "%s, %02d %s %4d %02d:%02d:%02d GMT"
152 , $day [ $gmt [ 6 ] ]
154 , $mon [ $gmt [ 4 ] ]
168 package Mirmon ; ###################################################
170 BEGIN { use base 'Base' ; Base -> import () ; }
175 { my %opt = ( v => 0 , d => 0 , q => 0 ) ;
177 { my ( $key, $val ) = @_ ;
179 unless ( exists $opt { $key } )
180 { warn "unknown Mirmon option '$key'\n" ; }
182 { $res = $opt { $key } ;
183 $opt { $key } = $val if defined $val ;
189 sub verbose { _opt ( 'v', shift ) ; }
190 sub quiet { _opt ( 'q', shift ) ; }
191 sub debug { _opt ( 'd', shift ) ; }
193 eval Base -> mk_methods ( qw(conf state regions) ) ;
197 my $home = ( getpwuid $< ) [ 7 ] or die "can get homedir '$<' ($!)" ;
198 ( 'mirmon.conf', "$home/.mirmon.conf", '/etc/mirmon.conf' ) ;
204 my $res = bless {}, $self ;
205 $res -> get_config ( $path ) ;
207 $res -> get_regions ;
214 my @LIST = $arg ? ( $arg ) : Mirmon -> config_list ;
215 for my $conf ( @LIST ) { return $conf if -r $conf and ! -d $conf ; }
216 die sprintf "can't find a config file :\n %s\n" , join "\n ", @LIST ;
222 my $file = $self -> find_config ( $path ) ; # or die
223 $self -> conf ( Mirmon::Conf -> new ( $file ) ) ;
228 my $conf = $self -> conf ;
229 my $name = $conf -> project_name ;
230 my $state = $conf -> state ;
232 open STATE, $state or die "can't open $state ($!)" ;
233 for my $line ( <STATE> )
235 my $mirror = Mirmon::Mirror -> new ( $self, $line ) ;
236 $res -> { $mirror -> url } = $mirror ;
240 my $mlist = $conf -> mirror_list ;
241 my $style = $conf -> list_style ;
244 open MLIST, $mlist or die "can't open $mlist ($!)" ;
245 for my $line ( <MLIST> )
247 next if $line =~ /^#/ ;
248 next if $line =~ /^\s*$/ ;
249 my ( $reg, $url, $mail ) ;
250 if ( $style eq 'plain' )
251 { ( $reg, $url, $mail ) = split ' ', $line ; }
252 elsif ( $style eq 'apache' )
254 ( $apache_type, $reg, $url, $mail ) = split ' ', $line ;
255 unless ( defined $APA_TYPES { $apache_type } )
256 { print "*** strange type in $url ($apache_type)\n"
257 unless Mirmon::quiet ;
262 if ( $conf -> add_slash and $url !~ m!/$! )
263 { print "*** appended '/' to $url\n" unless Mirmon::quiet ;
267 $in_list { $url } ++ ;
269 unless ( exists $res -> { $url } )
270 { $changes .= sprintf "added %s\n", $url unless Mirmon::quiet ;
271 $res -> { $url } = Mirmon::Mirror -> init ( $self, $url ) ;
273 my $mirror = $res -> { $url } ;
274 $mirror -> region ( $reg ) ;
275 $mirror -> mail ( $mail || '' ) ;
279 for my $url ( sort keys %$res )
280 { # printf "%s\n", $res -> { $url } -> state ;
281 unless ( exists $in_list { $url } )
282 { $changes .= sprintf "removed %s\n", $url unless Mirmon::quiet ;
283 delete $res -> { $url } ;
286 printf "changes in mirror-list for '%s':\n%s", $name, $changes
288 $self -> state ( $res ) ;
293 my $state = $self -> state ;
294 my $file = $self -> conf -> state ;
295 my $TMP = "$file.tmp" ;
296 open TMP, ">$TMP" or die "can't write '$TMP' ($!)" ;
297 for my $url ( sort keys %$state )
298 { printf TMP "%s\n", $state -> { $url } -> state
299 or die "can't print $url to $TMP ($!)" ;
304 { warn "wrote empty state file; keeping previous version" ; }
306 { rename $TMP, $file or die "can't rename '$TMP', '$file' ($!)" ; }
311 my $file = $self -> conf -> countries ;
312 open REGS, $file or die "can't open countries '$file' ($!)" ;
316 my ( $code, $dash, $reg ) = split ' ', $_, 3 ;
317 $self -> { regions } { lc $code } = $reg ;
326 my $xx = $ccs -> { $x } ;
327 my $yy = $ccs -> { $y } ;
328 if ( ! defined $xx and ! defined $yy )
330 elsif ( ! defined $xx )
332 elsif ( ! defined $yy )
342 $i + ( rand 1 < $f ? 1 : 0 ) ;
347 join ', ', map { sprintf "%s %s" , $_, scalar @{ $qs -> { $_ } } ; }
353 die "_rpick : row empty" unless @$row ;
354 my $idx = int rand @$row ;
355 my $res = $row -> [ $idx ] ;
356 $row -> [ $idx ] = $row -> [ $#{$row} ] ;
364 for my $mirr ( @$que )
365 { my $lp = $mirr -> last_probe ;
366 my $hr = int ( ( $^T - $lp ) / 60 / 60 + 0.5 ) ;
367 push @{ $tmp -> [ $hr ] }, $mirr ;
369 [ grep defined $_, @$tmp ] ;
373 { my $bucks = shift ;
375 push @$res, @$_ for @$bucks ;
380 { my $bucks = shift ;
381 die "buck_pick : bucks empty" unless @$bucks ;
382 my $buck = ( sort { @$b <=> @$a } @$bucks ) [ 0 ] ;
389 my $hrs = int ( $poll / 60 / 60 + 0.5 ) ;
391 my $diag1 = _diag_qs $ques ;
393 my $todos = $ques -> { todo } ;
394 my $dones = $ques -> { done } ;
395 my $cnt = @$todos + @$dones ;
396 my $avg = $hrs ? $cnt / $hrs : 0 ;
397 my $iavg = _pr_round $avg ;
399 my $bucks = _buck_split $dones ;
401 while ( @$todos < $iavg and $pick < @$dones )
402 { push @$todos, _buck_pick $bucks ;
406 $ques -> { done } = _buck_join $bucks ;
410 . " avg %.2f -> %d , picked %d ; queued %s\n"
413 , $avg, $iavg, $pick, scalar @$todos
414 , $hrs, _diag_qs ( $ques )
422 my $state = $self -> state ;
423 my $conf = $self -> conf ;
424 my $CMD = $conf -> probe ;
425 my $PAR = $conf -> max_probes ;
428 my $GET = IO::Select -> new () ;
430 for my $col ( qw(new red grn xtr) )
431 { $ques -> { $col } { $_ } = [] for qw(done todo) ; }
432 my $max_poll = s4tim $conf -> max_poll ;
433 my $min_poll = s4tim $conf -> min_poll ;
435 if ( Mirmon::verbose ) { printf "mirrors %d\n", scalar keys %$state ; }
438 { @QUE = sort { $a -> url cmp $b -> url } values %$state ; }
439 elsif ( $get eq 'url' )
440 { @QUE = ( $state -> { $URL } ) ; }
441 elsif ( $get eq 'update' )
442 { my $maxp = $^T - $max_poll ;
443 my $minp = $^T - $min_poll ;
445 if ( Mirmon::verbose )
446 { printf "max_poll %s\n", scalar localtime $maxp ;
447 printf "min_poll %s\n", scalar localtime $minp ;
449 for my $url ( sort keys %$state )
450 { my $mirror = $state -> { $url } ;
451 my $stat = $mirror -> last_status ;
452 my $vrfy = $mirror -> last_ok_probe ;
453 my $lprb = $mirror -> last_probe ;
456 if ( $stat eq 'undef' ) # never probed ; new mirror ; todo
457 { $col = 'new' ; $que = 'todo' ; }
458 elsif ( $conf -> get_xtr ( $mirror -> region ) )
459 { $col = 'xtr' ; $que = 'todo' ; }
461 { my $poll = $stat eq 'ok' ? $maxp : $minp ;
462 $col = $stat eq 'ok' ? 'grn' : 'red' ;
463 $que = ( aprx_le $lprb, $poll ) ? 'todo' : 'done' ;
465 push @{ $ques -> { $col } { $que } }, $mirror ;
468 if ( $conf -> randomize )
469 { my $msg = "randomize green\n" ;
470 $msg .= _randomize $ques -> { grn }, $max_poll ;
471 $msg .= "randomize red\n" ;
472 $msg .= _randomize $ques -> { red }, $min_poll ;
473 print $msg if Mirmon::verbose ;
476 ( @{ $ques -> { new } { todo } }
477 , @{ $ques -> { red } { todo } }
478 , @{ $ques -> { grn } { todo } }
479 , @{ $ques -> { xtr } { todo } }
483 { die "unknown opt_get '$get'" ; }
485 if ( Mirmon::verbose ) { printf "queued %d\n\n", scalar @QUE ; }
489 while ( $GET -> count () < $PAR and @QUE )
490 { my $mirror = shift @QUE ;
491 if ( gethost $mirror -> site )
492 { my $handle = $mirror -> start_probe ;
493 $m4h { $handle } = $mirror ;
494 $GET -> add ( $handle ) ;
498 { $mirror -> update ( 0, 'site_not_found', undef ) ; }
501 my @can_read = $GET -> can_read ( 0 ) ;
503 printf "queue %d, started %d, probes %d, can_read %d\n",
504 scalar @QUE, $started, $GET -> count (), scalar @can_read
507 for my $handle ( @can_read )
508 { # order is important ; wget's hang if/when actions are reversed
509 $GET -> remove ( $handle ) ;
510 $m4h { $handle } -> finish_probe ( $handle ) ;
516 my $stop = time + $conf -> timeout + 10 ;
518 while ( $GET -> count () and time < $stop )
519 { my @can_read = $GET -> can_read ( 0 ) ;
521 printf "wait %2d, probes %d, can_read %d\n",
522 $stop - scalar time, $GET -> count (), scalar @can_read
525 for my $handle ( @can_read )
526 { $GET -> remove ( $handle ) ;
527 $m4h { $handle } -> finish_probe ( $handle ) ;
533 for my $handle ( $GET -> handles () )
534 { $m4h { $handle } -> update ( 0, 'hangs', undef ) ; }
544 ( '<IMG BORDER=1 SRC="%s/bar.gif" ALT="">'
545 , $self -> conf -> icons
549 { sprintf '<IMG BORDER=1 SRC="%s/mm%s%02d.gif" ALT="">'
550 , $self -> conf -> icons, $prf, $cnt ;
554 sub img_sf { my $self = shift ; $self -> img_sf_cnt ( $_[0], 1 ) ; }
559 if ( $hst =~ /-(.*)$/ ) { $hst = $1 ; }
560 return '' unless $hst =~ m/^[sbfzx]+$/ ;
561 if ( length $hst == $HIST and $hst =~ /^(s*b)s*$/ )
562 { return $self -> img_sf_cnt ( 'sb', length $1 ) ; }
563 elsif ( length $hst == $HIST and $hst =~ /^(s*f)s*$/ )
564 { return $self -> img_sf_cnt ( 'sf', length $1 ) ; }
565 elsif ( length $hst == $HIST and $hst =~ /^(s*b)fs*$/ )
566 { return $self -> img_sf_cnt ( 'sbf', length $1 ) ; }
569 my $prf = substr $hst, 0, 1 ;
570 $hst = substr $hst, 1 ;
572 { if ( substr ( $prf, 0, 1 ) eq substr ( $hst, 0, 1 ) )
574 $hst = substr $hst, 1 ;
577 { $res .= $self -> img_sf_cnt ( $prf, $cnt ) ;
578 $prf = substr $hst, 0, 1 ;
579 $hst = substr $hst, 1 ;
583 $res .= $self -> img_sf_cnt ( $prf, $cnt ) if $cnt ;
587 sub gen_histogram_probes
589 my $state = $self -> state ;
597 for my $url ( keys %$state )
598 { my $mirror = $state -> { $url } ;
599 my $lprb = $mirror -> last_probe ;
600 my $stat = $mirror -> last_status ;
601 next if $lprb eq 'undef' ;
602 my $hr = int ( ( $^T - $lprb ) / 3600 + 0.5 ) ;
603 $hr_min = $hr if ! defined $hr_min or $hr < $hr_min ;
604 $hr_max = $hr if ! defined $hr_max or $hr > $hr_max ;
606 { $tab { $hr } ++ ; $s_cnt ++ ; }
608 { $bad { $hr } ++ ; $f_cnt ++ ; }
610 return BQ 'nothing yet' unless scalar keys %tab ;
618 , $s_cnt , GRN ( 'successful' )
619 , $f_cnt , RED ( 'failed' )
624 for my $x ( keys %tab )
625 { my $tot = $tab { $x } + ( $bad { $x } || 0 ) ;
626 $max = $tot if $max < $tot ;
629 return BQ "nothing yet" unless $max ;
631 for my $hr ( $hr_min .. $hr_max )
632 { my $x = $tab { $hr } || 0 ;
633 my $y = $bad { $hr } || 0 ;
634 my $n = int ( $x / $max * $HIST ) ;
635 my $b = int ( $y / $max * $HIST ) ;
641 ( ( $n ? $self -> img_sf_cnt ( 's', $n ) : '' )
642 . ( $b ? $self -> img_sf_cnt ( 'f', $b ) : '' )
643 . ( ( $n + $b ) ? '' : ' ' )
652 my $state = $self -> state ;
654 for my $url ( keys %$state )
655 { my $time = $state -> { $url } -> age ;
656 push @tab, $^T - $time if $time =~ /^\d+$/ ;
660 return undef if $cnt == 0 ;
662 @tab = sort { $a <=> $b } @tab ;
665 for my $age ( @tab ) { $tot += $age ; }
666 my $mean = $tot / $cnt ;
670 { $median = $tab [ 0 ] ; }
672 { my $mid = int ( $#tab / 2 ) ;
673 $median = ( $tab [ $mid ] + $tab [ $mid + 1 ] ) / 2 ;
676 { my $mid = int ( $#tab / 2 ) ;
677 $median = $tab [ $mid ] ;
681 { return $mean, $median, undef ; }
685 { $sum += ( $age - $mean ) ** 2 ; }
686 my $stddev = sqrt ( $sum / ( $cnt - 1 ) ) ;
688 return $mean, $median, $stddev ;
693 my $conf = $self -> conf ;
694 my $min_sync = $conf -> min_sync ;
695 my $max_sync = $conf -> max_sync ;
696 my $min_poll = $conf -> min_poll ;
697 my $max_poll = $conf -> max_poll ;
702 <H4><I>project</I> site -- home</H4>
705 <B><I>project</I> site</B> is an url.
706 The <B>href</B> is the href for the site in the list of mirrors,
707 usually the root of the mirrored file tree.
708 The <B>text</B> is the <I>site</I> of that url.
710 <B>home</B> (represented by the <B>@</B>-symbol) is an url
711 pointing to the document root of the site. This pointer is
712 useful if the <B><I>project</I> site</B> url is invalid,
713 possibly because the mirror site moved the archive.
719 Indicates the type (<B>ftp</B> or <B>http</B>) of
720 the <B><I>project</I> site</B> and <B>home</B> urls.
723 <H4>mirror age, daily stats</H4>
726 The <B>mirror age</B> is based upon the last successful probe.
728 Once a day the status of a mirror site is determined.
729 The status (represented by a colored block) is appended
730 to the <B>right</B> of the status history (<I>right</I>
731 is <I>recent</I>). More precise, the status block is appended
732 if the last status block was appended 24 (or more) hours ago.
733 <P>The status of a mirror depends on its age and a few
734 configuration parameters :
736 <TABLE BORDER=1 CELLPADDING=5>
738 <TH ROWSPAN=3>status</TH>
739 <TH COLSPAN=4>age</TH>
742 <TH COLSPAN=2 BGCOLOR=YELLOW>this project</TH>
743 <TH COLSPAN=2 BGCOLOR=AQUA>in general</TH>
746 <TH BGCOLOR=YELLOW>min</TH>
747 <TH BGCOLOR=YELLOW>max</TH>
748 <TH BGCOLOR=AQUA>min</TH>
749 <TH BGCOLOR=AQUA>max</TH>
752 <TH><FONT COLOR=GREEN>fresh</FONT></TH>
753 <TD BGCOLOR=YELLOW ALIGN=CENTER>0</TD>
754 <TD BGCOLOR=YELLOW ALIGN=CENTER>$min_sync + $max_poll</TD>
755 <TD BGCOLOR=AQUA ALIGN=CENTER>0</TD>
756 <TD BGCOLOR=AQUA ALIGN=CENTER>min_sync + max_poll</TD>
759 <TH><FONT COLOR=BLUE>oldish</FONT></TH>
760 <TD BGCOLOR=YELLOW ALIGN=CENTER>$min_sync + $max_poll</TD>
761 <TD BGCOLOR=YELLOW ALIGN=CENTER>$max_sync + $max_poll</TD>
762 <TD BGCOLOR=AQUA ALIGN=CENTER>min_sync + max_poll</TD>
763 <TD BGCOLOR=AQUA ALIGN=CENTER>max_sync + max_poll</TD>
766 <TH><FONT COLOR="RED">old</FONT></TH>
767 <TD BGCOLOR=YELLOW ALIGN=CENTER>$max_sync + $max_poll</TD>
768 <TD BGCOLOR=YELLOW ALIGN=CENTER>∞</TD>
769 <TD BGCOLOR=AQUA ALIGN=CENTER>max_sync + max_poll</TD>
770 <TD BGCOLOR=AQUA ALIGN=CENTER>∞</TD>
773 <TH><FONT COLOR=BLACK>bad</FONT></TH>
774 <TH COLSPAN=4 BGCOLOR=BLACK>
775 <FONT COLOR=WHITE>the site or mirror tree was never found</FONT></TH>
781 <H4>last probe, probe stats</H4>
784 <B>Last probe</B> indicates when the last successful probe was made.
785 <B>Probe stats</B> gives the probe history (<I>right</I> is <I>recent</I>).
787 <FONT COLOR=GREEN><B>success</B></FONT> or a
788 <FONT COLOR=RED><B>failure</B></FONT>.
794 <B>Last stat</B> gives the status of the last probe.
801 { return '' unless my $ths = shift ;
802 $ths == 1 ? TH '' : "<TH COLSPAN=$ths></TH>\n" ;
808 my $conf = $self -> conf ;
809 my $state = $self -> state ;
811 return '' if $where ne $conf -> put_histo ;
813 my $MAX_H = $conf -> max_age1 ;
815 ( ( 20 * 3600 <= $MAX_H and $MAX_H <= 36 * 3600 )
816 ? int ( $MAX_H / 3600 )
819 my $MAX_O = $conf -> max_age2 ;
820 my $MAX_o = int ( $MAX_O / 3600 + 0.5 ) ;
822 my %W = ( 'old' => 1, 'ded' => 1, 'bad' => 1 ) ;
823 my %Wmx = ( 'old' => 5, 'ded' => 3, 'bad' => 3 ) ;
827 for ( my $x = 0 ; $x < $MAX_h ; $x ++ ) { $tab { $x } = 0 ; }
828 $tab { old } = 0 ; $tab { ded } = 0 ; $tab { bad } = 0 ;
829 for my $url ( keys %$state )
830 { my $time = $state -> { $url } -> age ;
831 if ( $time =~ /^\d+$/ )
832 { my $s = $^T - $time ;
833 my $hr = int ( $s / $MAX_H * ( $MAX_h - 1 ) + 0.5 ) ;
834 if ( $s <= $MAX_H ) { $tab { $hr } ++ ; }
835 elsif ( $s <= $MAX_O ) { $tab { old } ++ ; }
836 else { $tab { ded } ++ ; }
839 { $tab { bad } ++ ; }
842 for ( grep ! exists $Wmx { $_ }, keys %tab )
843 { $max = $tab { $_ } if $tab { $_ } > $max ; }
847 for my $aux ( keys %Wmx )
848 { $bad { $aux } = $tab { $aux } ;
849 if ( $bad { $aux } > $max )
850 { $W { $aux } = $Wmx { $aux } ;
851 my $d = int ( $bad { $aux } / $W { $aux } ) ;
852 for ( my $i = 1 ; $i < $W { $aux } ; $i++ )
853 { $tab { $aux . $i } = $d ;
854 if ( $bad { $aux } % $Wmx { $aux } > $i )
855 { $tab { $aux . $i } ++ ;
859 $tab { $aux } -= ( $W { $aux } - 1 ) * $d ;
860 $max = $tab { $aux } if $max < $tab { $aux } ;
865 # { for my $hr ( keys %tab )
866 # { printf "tab '%s' = '%s'\n", $hr, $tab { $hr } ; }
869 return 'nothing yet' unless $max ;
870 $H = $max if 8 <= $max and $max <= 26 ;
872 { $hst { $_ } = int ( $H * $tab { $_ } / $max + 0.5 ) ; }
873 my @keys = sort { $a <=> $b } grep /^\d+$/, keys %hst ;
875 for my $hr ( @keys ) { $tab_hr += $tab { $hr } ; }
877 , grep ( m/^old/, sort keys %tab )
878 , grep ( m/^ded/, sort keys %tab )
879 , grep ( m/^bad/, sort keys %tab )
881 my $img_bar = sprintf '<IMG SRC="%s/bar.gif" ALT="" BORDER=0>'
883 my %img = ( bar => $img_bar ) ;
884 for my $col ( qw(s b f z) ) { $img { $col } = $self -> img_sf ( $col ) ; }
886 for ( my $h = $H ; $h > 0 ; $h -- )
888 $res .= sprintf "<TH ROWSPAN=3 VALIGN=\"TOP\">↑</TH>\n"
890 $res .= sprintf '<TD ROWSPAN=%d ALIGN="CENTER">%s</TD>' . "\n"
891 , $H-6, NSS ( $max ) if $h == $H - 3 ;
892 $res .= sprintf "<TH ROWSPAN=3 VALIGN=\"BOTTOM\">↓</TH>\n"
897 ( ( $hst { $x } >= $h )
900 : ( $x =~ /^old/ ? 'b' : ( $x =~ /^ded/ ? 'f' : 'z' ) )
902 : ( ( $h == 1 and $hst { $x } == 0 ) ? 'bar' : '' )
905 { $res .= _ths $ths ; $ths = 0 ; $res .= TH $img { $col } ; }
909 $res .= _ths ( $ths ) . "</TR>\n" ;
912 my $HR = '<HR SIZE=2 WIDTH="95%%" NOSHADE>' ;
915 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", 1 ;
916 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $MAX_h ;
917 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W { old } ;
918 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W { ded } ;
919 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W { bad } ;
923 $res .= '<TD ALIGN="CENTER"> <B>age</B> → </TD>' ;
925 $res .= "<TH>|</TH>\n" ;
927 ( '<TD COLSPAN=%d ALIGN="CENTER">'
928 . '← 0 ≤ <B>age</B> ≤ %s →'
930 , $MAX_h - 2, pr_interval ( $MAX_H )
933 $res .= "<TH>|</TH>\n" ;
935 ( '<TD ALIGN="CENTER" COLSPAN=%d>'
936 . ' %sh < %s ≤ %sh '
938 , $W { old }, int($MAX_H/60/60) , BLD ( 'age' ), $MAX_o
941 ( '<TD ALIGN="CENTER" COLSPAN=%d>'
942 . ' <FONT COLOR="RED">old</FONT> '
947 ( '<TD ALIGN="CENTER" COLSPAN=%d>'
948 . ' <FONT COLOR="RED">bad</FONT> '
954 my $FRMT = '<TD ALIGN="CENTER" COLSPAN=%d> %s </TD>' ;
957 $res .= sprintf "$FRMT\n", 1, NSS scalar keys %$state ;
958 $res .= "<TH>|</TH>\n" ;
959 $res .= sprintf "$FRMT\n", $MAX_h - 2, NSS $tab_hr ;
960 $res .= "<TH>|</TH>\n" ;
961 $res .= sprintf "$FRMT\n", $W { old }, NSS $bad { old } ;
962 $res .= sprintf "$FRMT\n", $W { ded }, NSS $bad { ded } ;
963 $res .= sprintf "$FRMT\n", $W { bad }, NSS $bad { bad } ;
966 $res = "<TABLE CELLSPACING=0 CELLPADDING=1 BORDER=0>\n$res\n</TABLE>\n" ;
967 $res = sprintf "<TABLE CELLPADDING=5 BORDER=4>%s</TABLE>\n"
968 , "<TR><TH>\n$res\n</TH></TR>" ;
970 , $self -> img_sf ( 's' ) , $self -> img_sf ( 'b' )
971 , $self -> img_sf ( 'f' ) , $self -> img_sf ( 'z' )
974 { $res .= sprintf "<BR>units %s represent one mirror site.\n"
978 { $res .= sprintf "<BR>each %s unit represents %s mirror sites.\n"
979 , $units, sprintf ( "%.1f", $max / $H ) ;
981 return H2 ( NAM 'age-histogram', 'age histogram' )
988 my $VERSION = shift ;
989 my $conf = $self -> conf ;
990 my $PPP = $conf -> web_page ;
991 my $state = $self -> state ;
992 my $CCS = $self -> regions ;
993 my $TMP = "$PPP.tmp" ;
997 for my $url ( keys %$state )
998 { my $mirror = $state -> { $url } ;
999 my $reg = $mirror -> region ;
1000 push @{ $tab { $reg } }, $mirror ;
1003 my $bad = 0 ; my $old = 0 ; my $unr = 0 ;
1008 for my $url ( keys %$state )
1009 { my $mirror = $state -> { $url } ;
1010 my $time = $mirror -> age ;
1011 my $stat = $mirror -> last_status ;
1012 my $vrfy = $mirror -> last_ok_probe ;
1013 if ( $stat eq 'ok' ) { $ok ++ ; } else { $stats { $stat } ++ ; }
1014 if ( $time eq 'undef' )
1016 elsif ( 'f' eq $conf -> age_code ( $time ) )
1018 if ( $vrfy eq 'undef' or aprx_lt $vrfy, $^T - $conf -> max_vrfy )
1023 "%d bad -- %d older than %s -- %s unreachable for more than %s"
1026 , pr_interval ( $conf -> max_age2 )
1028 , pr_interval ( $conf -> max_vrfy )
1031 my $PROB = 'last probes : ' ;
1032 push @stats, "$ok were ok" if $ok ;
1033 for my $stat ( sort keys %stats )
1034 { ( my $txt = $stat ) =~ s/_/ /g ;
1035 push @stats, sprintf "%s had %s" , $stats { $stat } , RED $txt ;
1037 $PROB .= join ', ', @stats ;
1039 my ( $mean, $median, $stddev ) = $self -> age_avg ;
1040 my $AVGS = "mean mirror age is " ;
1041 unless ( defined $mean )
1042 { $AVGS = "<I>undefined</I>" ; }
1044 { $AVGS .= sprintf "%s", pr_interval $mean ;
1045 if ( defined $stddev )
1046 { $AVGS .= sprintf ", std_dev %s", pr_interval $stddev ; }
1047 $AVGS .= sprintf ", median %s", pr_interval $median ;
1050 for my $reg ( sort keys %tab )
1051 { $refs .= sprintf " %s \n"
1052 , URL "#$reg", "<FONT SIZE=\"+1\">$reg</FONT>"
1057 my $NAME = $conf -> project_name ;
1058 my $LOGO = $conf -> project_logo
1060 ( $conf -> project_url
1062 ( '<IMG SRC="%s" ALT="%s" ALIGN="RIGHT" BORDER=0>'
1063 , $conf -> project_logo
1064 , $conf -> project_name
1069 my $HEAD = $conf -> htm_head . "\n" ;
1070 my $HTOP = $conf -> htm_top . "\n" ;
1071 my $FOOT = $conf -> htm_foot . "\n" ;
1072 my $TITL = URL $conf -> project_url, $NAME ;
1073 my $EXPD = Base::exp_date ;
1074 my $DATE = scalar gmtime $^T ;
1075 my $LAST = scalar gmtime ( $get ? $^T : ( stat $conf -> state ) [9] ) ;
1077 my $histo_top = $self -> gen_histogram ( 'top' ) ;
1078 my $histo_bot = $self -> gen_histogram ( 'bottom' ) ;
1080 open PPP, ">$TMP" or die "can't write $TMP ($!)" ;
1081 my $prev_select = select PPP ;
1083 my $attr1 = "COLSPAN=$COLS BGCOLOR=LIME" ;
1084 my $attr2 = 'BGCOLOR=AQUA' ;
1085 my $attr3 = "COLSPAN=$COLS BGCOLOR=YELLOW" ;
1087 my $num_mirrors = scalar keys %$state ;
1088 my $num_regions = scalar keys %tab ;
1091 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
1094 <TITLE>the status of $NAME mirrors</TITLE>
1095 <META HTTP-EQUIV="content-type" CONTENT="text/html; charset=utf-8">
1096 <META HTTP-EQUIV=refresh CONTENT=3600>
1097 <META HTTP-EQUIV=Expires CONTENT=\"$EXPD\">
1100 <BODY BGCOLOR=\"#FFFFFF\">
1102 <H2>the status of $TITL mirrors</H2>
1103 <TABLE BORDER=0 CELLPADDING=2>
1104 <TR><TD>date</TD><TD>:</TD><TD>$DATE (UTC)</TD></TR>
1105 <TR><TD>last check</TD>
1107 <TD>$LAST (UTC)</TD>
1113 <BLOCKQUOTE><CENTER>\n$refs\n</CENTER></BLOCKQUOTE>
1116 <TABLE BORDER=2 CELLPADDING=5>
1117 <TR><TH $attr1>$num_mirrors sites in $num_regions regions</TH></TR>
1118 <TR><TH $attr1>$STAT</TH></TR>
1119 <TR><TH $attr1>$PROB</TH></TR>
1120 <TR><TH $attr1>$AVGS</TH></TR>
1122 <TH $attr2>$NAME site -- home</TH>
1123 <TH $attr2>type</TH>
1124 <TH $attr2>mirror age,<BR>daily stats</TH>
1125 <TH $attr2>last probe,<BR>probe stats</TH>
1126 <TH $attr2>last stat</TH>
1131 ( sort { _cmp_ccs $CCS, $a, $b } keys %tab )
1132 # { ( $CCS -> { $a } ? lc ( $CCS -> { $a } ) : $a )
1133 # cmp ( $CCS -> { $b } ? lc ( $CCS -> { $b } ) : $b )
1136 { my $mirrors = $tab { $reg } ;
1138 my $ccs = exists $CCS -> { $reg } ? $CCS -> { $reg } : $reg ;
1140 ( scalar @{ $mirrors } > 6
1141 ? sprintf "%s - %d sites"
1142 , $ccs, scalar @{ $mirrors }
1145 printf "<TR><TH $attr3>$ccs</TH></TR>\n" ;
1147 for my $mirror ( sort { $a -> cmp ( $b ) } @$mirrors )
1149 printf " <TD ALIGN=RIGHT>%s %s</TD>\n <TD>%s</TD>\n"
1150 , $mirror -> url_site
1151 , $mirror -> url_home
1155 my ( $url, $time, $stat, $vrfy, $hstp, $hsts ) =
1156 $mirror -> as_list ;
1157 my $pr_time = $time =~ /^\d+$/
1158 ? pr_diff $time, $^T - $conf -> max_age2 : ' ' ;
1159 my $pr_last = $vrfy =~ /^\d+$/
1160 ? pr_diff $vrfy, $^T - $conf -> max_vrfy : ' ' ;
1161 my $pr_hstp = $self -> show_hist ( $hstp ) ;
1162 my $pr_hsts = $self -> show_hist ( $hsts ) ;
1164 if ( $stat ne 'ok' ) { $stat =~ s/_/ /g ; $stat = RED $stat ; }
1165 printf " <TD ALIGN=RIGHT>%s<BR>%s</TD>\n" , $pr_time, $pr_hsts ;
1166 printf " <TD ALIGN=RIGHT>%s<BR>%s</TD>\n" , $pr_last, $pr_hstp ;
1167 printf " <TD>%s</TD>\n", $stat ;
1172 my $legend = $self -> legend ;
1173 my $probes = $self -> gen_histogram_probes ;
1174 my $mir_img = sprintf
1175 '<IMG BORDER=2 ALT=mirmon SRC="%s/mirmon.gif">' , $conf -> icons ;
1182 <H3>probe results</H3>
1188 <TH><A HREF=\"$HOME\">$mir_img</A></TH>
1198 select $prev_select ;
1200 if ( print PPP "\n" )
1203 { warn "wrote empty html file; keeping previous version" ; }
1205 { rename $TMP, $PPP or die "can't rename $TMP, $PPP ($!)" ; }
1208 { die "can't print to $TMP ($!)" ; }
1211 package Mirmon::Conf ; #############################################
1213 BEGIN { use base 'Base' ; Base -> import () ; }
1216 ( project_logo => ''
1217 , timeout => $DEF_TIMEOUT
1223 , list_style => 'plain'
1224 , put_histo => 'top'
1234 qw( web_page state countries mirror_list probe
1235 project_name project_url icons
1238 for ( @REQ_KEYS, keys %CNF_defaults ) { $CNF_KEYS { $_ } ++ ; }
1240 my @LIST_STYLE = qw(plain apache) ;
1241 my @PUT_HGRAM = qw(top bottom nowhere) ;
1243 eval Base -> mk_methods ( keys %CNF_KEYS, qw(root site_url) ) ;
1246 { my $self = shift ;
1248 scalar grep { $_ eq $reg } split ' ', $self -> always_get ;
1252 { my $self = shift ;
1254 my $res = bless { %CNF_defaults }, $self ;
1255 $res -> root ( $FILE ) ;
1256 $res -> site_url ( {} ) ;
1257 $res -> get_conf () ;
1261 { my $self = shift ;
1262 my $FILE = ( @_ ? shift : $self -> root ) ;
1264 if ( grep $_ eq $FILE, @{ $self -> {_include} } )
1265 { die "already included : '$FILE'" ; }
1267 { push @{ $self -> {_include} }, $FILE ; }
1269 open FILE, $FILE or die "can't open '$FILE' ($!)" ;
1270 my $CONF = join "\n", grep /./, <FILE> ;
1273 $CONF =~ s/\t/ /g ; # replace tabs
1274 $CONF =~ s/^[+ ]+// ; # delete leading space, plus
1275 $CONF =~ s/\n\n\s+/ /g ; # glue continuation lines
1276 $CONF =~ s/\n\n\+\s+//g ; # glue concatenation lines
1277 $CONF =~ s/\n\n\./\n/g ; # glue concatenation lines
1280 print "--$CONF--\n" if Mirmon::debug ;
1281 for ( grep ! /^#/, split /\n\n/, $CONF )
1282 { my ($key,$val) = split ' ', $_, 2 ;
1283 $val = '' unless defined $val ;
1284 print "conf '$FILE' : key '$key', val '$val'\n" if Mirmon::debug ;
1285 if ( exists $CNF_KEYS { $key } )
1286 { $self -> $key ( $val ) ; }
1287 elsif ( $key eq 'site_url' )
1288 { my ( $site, $url ) = split ' ' , $val ;
1289 $url .= '/' if $self -> add_slash and $url !~ m!/$! ;
1290 $self -> site_url -> { $site } = $url ;
1291 # printf "config : for site '%s' use instead\n '%s'\n",
1292 # $site, $url if Mirmon::verbose ;
1294 elsif ( $key eq 'no_add_slash' )
1295 { $self -> add_slash ( 0 ) ; }
1296 elsif ( $key eq 'no_randomize' )
1297 { $self -> randomize ( 0 ) ; }
1298 elsif ( $key eq 'show' )
1299 { $self -> show_conf if Mirmon::verbose ; }
1300 elsif ( $key eq 'exit' )
1301 { die 'exit per config directive' ; }
1302 elsif ( $key eq 'include' )
1303 { $self -> get_conf ( $val ) ; }
1304 elsif ( $key eq 'env' )
1305 { my ( $x, $y ) = split ' ' , $val ;
1307 printf "config : setenv '%s'\n '%s'\n", $x, $y
1308 if Mirmon::verbose ;
1311 { $self -> show_conf ;
1312 die "unknown keyword '$key' (value '$val')\n" ;
1315 my $err = $self -> check ;
1321 { my $self = shift ;
1323 for my $key ( @REQ_KEYS )
1324 { unless ( exists $self -> { $key } )
1325 { $err .= "error: missing config for '$key'\n" ; }
1327 for my $key ( qw(min_poll max_poll max_sync min_sync) )
1328 { my $max = $self -> $key ;
1329 unless ( $max =~ /$TIM_PAT/o )
1330 { $err .= "error: bad timespec for $key ($max)\n" ; }
1332 unless ( grep $self -> { list_style } eq $_, @LIST_STYLE )
1333 { $err .= sprintf "error: unknown 'list_style' '%s'\n",
1334 $self -> list_style ;
1336 unless ( grep $self -> put_histo eq $_, @PUT_HGRAM )
1337 { $err .= sprintf "%s : error: unknown 'put_histo' '%s'\n",
1338 $self -> put_histo ;
1344 { my $self = shift ;
1345 print "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" ;
1346 for my $key ( sort keys %$self )
1347 { next if $key =~ m/^_/ ;
1348 my $val = $self -> { $key } ;
1349 print "show_conf : $key = '$val'\n" ;
1351 for my $key ( sort keys %{ $self -> site_url } )
1352 { printf "show_conf : for site '%s' use instead\n '%s'\n"
1353 , $key, $self -> site_url -> { $key } if Mirmon::verbose ;
1355 printf "show_conf : included '%s'\n"
1356 , join "', '", @{ $self -> {_include} } ;
1357 print "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" ;
1361 { my $self = shift ;
1362 ( s4tim $self -> min_sync ) + ( s4tim $self -> max_poll ) ;
1366 { my $self = shift ;
1367 ( s4tim $self -> max_sync ) + ( s4tim $self -> max_poll ) ;
1371 { my $self = shift ;
1372 ( s4tim $self -> min_poll ) + ( s4tim $self -> max_poll ) ;
1376 { my $self = shift ;
1378 return 'z' unless $time =~ /^\d+$/ ;
1380 ( ( aprx_ge ( $time, $^T - $self -> max_age1 ) )
1382 : ( aprx_ge ( $time, $^T - $self -> max_age2 ) ? 'b' : 'f' )
1386 package Mirmon::Mirror ; ###########################################
1388 BEGIN { use base 'Base' ; Base -> import () ; }
1393 qw(url age last_status last_ok_probe probe_history state_history last_probe) ;
1395 eval Base -> mk_methods ( @FIELDS, qw(mirmon region mail) ) ;
1397 sub state_history_time
1398 { my $self = shift ;
1399 my $res = ( split /-/, $self -> state_history ) [ 0 ] ;
1403 sub state_history_hist
1404 { my $self = shift ;
1405 my $res = ( split /-/, $self -> state_history ) [ 1 ] ;
1410 { my $self = shift ;
1411 my $url = $self -> url ;
1412 my ( $type, $site, $home, $path ) ;
1413 if ( $url =~ m!^(ftp|https?|rsync)://([^/:]+)(:\d+)?/! )
1414 { $type = $1 ; $site = $2 ; $home = $& ; $path = $' ; }
1416 { warn "can't parse url ($url)" ; }
1417 return $type, $site, $home, $path ;
1420 sub type { my $self = shift ; ( $self -> _parse ) [ 0 ] ; }
1421 sub site { my $self = shift ; ( $self -> _parse ) [ 1 ] ; }
1422 sub home { my $self = shift ; ( $self -> _parse ) [ 2 ] ; }
1423 sub path { my $self = shift ; ( $self -> _parse ) [ 3 ] ; }
1426 { my $self = shift ;
1428 my $age = $self -> age ;
1429 if ( $age eq 'undef' )
1430 { $res = length $self -> state_history_hist
1431 if $self -> last_probe ne 'undef' ;
1434 { $res = ( $^T - $age ) / 24 / 60 / 60 ; }
1439 { my $self = shift ;
1440 my $mirmon = shift ;
1442 my $res = bless { mirmon => $mirmon }, $self ;
1443 @{ $res } { @FIELDS } = ( 'undef' ) x scalar @FIELDS ;
1444 $res -> url ( $url ) ;
1445 $res -> probe_history ( '' ) ;
1446 $res -> state_history ( "$^T-z" ) ;
1447 $res -> mail ( '' ) ;
1452 { my $self = shift ;
1453 my $mirmon = shift ;
1455 my $res = bless { mirmon => $mirmon }, $self ;
1456 @{ $res } { @FIELDS } = split ' ', $line ;
1457 $res -> mail ( '' ) ;
1462 { my $self = shift ;
1466 my $probe_hist = $self -> probe_history ;
1468 { $self -> age ( $time ) ;
1469 $self -> last_ok_probe ( $^T ) ;
1470 $probe_hist .= 's' ;
1473 { $probe_hist .= 'f' ;
1474 $time = $self -> age ;
1477 my $h = $self -> state_history_hist ;
1478 my $t = $self -> state_history_time ;
1480 if ( aprx_ge ( $^T - $t, $HIST_DELTA ) )
1481 { my $n = int ( ( $^T - $t ) / $HIST_DELTA ) ;
1482 $h .= 'x' x ( $n - 1 ) ;
1483 $t = ( $n == 1 ? $t + $HIST_DELTA : $^T ) ;
1487 $h .= $self -> mirmon -> conf -> age_code ( $time ) ;
1488 $h = substr $h, - $HIST ;
1491 $self -> last_status ( $stat ) ;
1492 $self -> probe_history ( substr $probe_hist, - $HIST ) ;
1493 $self -> last_probe ( $^T ) ;
1494 $self -> state_history ( "$t-$h" ) ;
1497 sub as_list { my $self = shift ; @{ $self } { @FIELDS } ; }
1498 sub state { my $self = shift ; join ' ', $self -> as_list ; }
1501 { my $self = shift ;
1502 my $conf = $self -> mirmon -> conf ;
1503 my $probe = $conf -> probe ;
1504 my $timeout = $conf -> timeout ;
1505 $probe =~ s/%TIMEOUT%/$timeout/g ;
1506 my $url = $self -> url ;
1507 my $new = $conf -> site_url -> { $self -> site } ;
1509 { printf "*** site_url : site %s\n -> url %s\n"
1510 , $self -> site, $new if Mirmon::verbose ;
1513 $probe =~ s/%URL%/$url/g ;
1514 my $pipe = new IO::Pipe ;
1515 my $handle = $pipe -> reader ( split ' ', $probe ) ;
1517 { $pipe -> blocking ( 0 ) ; }
1519 { die "start_probe : no pipe for $url" ; }
1520 printf "start %s\n", $url if Mirmon::verbose ;
1521 printf " %s\n", $probe if Mirmon::debug ;
1526 { my $self = shift ;
1527 my $handle = shift ;
1533 $handle -> blocking ( 1 ) ;
1534 if ( $handle -> eof () )
1535 { printf "finish eof %s\n", $self -> url if Mirmon::verbose ; }
1537 { $res = $handle -> getline () ; }
1541 unless ( defined $res )
1542 { $stat = 'no_time' ; }
1543 elsif ( $res =~ /^\s*$/ )
1544 { $stat = 'empty' ; }
1546 { $res = ( split ' ', $res ) [ 0 ] ;
1548 if ( $res !~ /^\d+$/ )
1550 $res = Base::htmlquote $res ;
1551 $res = substr ( $res, 0, 15 ) . '..' if length $res > 15 ;
1555 { $succ = 1 ; $stat = 'ok' ; $time = $res ; }
1558 printf "finish %s\n succ(%s) stat(%s) time(%s)\n"
1562 , ( defined $time ? $time : 'undef' )
1563 if Mirmon::verbose ;
1565 $self -> update ( $succ, $stat, $time ) ;
1568 sub revdom { my $dom = shift ; join '.', reverse split /\./, $dom ; }
1573 ( revdom $a -> site ) cmp ( revdom $b -> site )
1575 ( $a -> type cmp $b -> type )
1582 $hrf =~ /^rsync/ ? $txt : URL $hrf, $txt ;
1586 { my $self = shift ;
1587 my $type = $self -> type ;
1588 if ( $type eq 'rsync' )
1589 { my $path = $self -> path ;
1590 chop $path if $path =~ m!/$! ;
1591 sprintf '%s::%s', $self -> site , $path ;
1594 { URL $self -> url , $self -> site ; }
1598 { my $self = shift ;
1599 my $type = $self -> type ;
1600 if ( $type eq 'rsync' )
1603 { URL $self -> home, '@' ; }
1610 Mirmon - OO interface for mirmon objects
1616 $m = Mirmon -> new ( [ $path-to-config ] )
1618 $conf = $m -> conf ; # a Mirmon::Conf object
1619 $state = $m -> state ; # the mirmon state
1621 for my $url ( keys %$state )
1622 { $mirror = $state -> { $url } ; # a Mirmon::Mirror object
1623 $mail = $mirror -> mail ; # contact address
1624 $mirror -> age ( time ) ; # set mirror age
1627 Many class and object methods can be used to get or set attributes :
1629 $object -> attribute # get an atttibute
1630 $object -> attribute ( $attr ) # set an atttibute
1632 =head1 Mirmon class methods
1636 =item B<new ( [$path] )>
1638 Create a Mirmon object from a config file found in $path,
1639 or (by default) in the default list of possible config files.
1640 Related objects (config, state) are created and initialised.
1644 Mirmon always reports errors. Normally it only reports
1645 changes (inserts/deletes) found in the mirror_list ;
1646 in I<quiet> mode, it doesn't. In I<verbose> mode, it
1647 reports progress: the startup and finishing of probes.
1649 Mirmon::verbose ( [ $bool ] ) # get/set verbose
1650 Mirmon::quiet ( [ $bool ] ) # get/set quiet
1651 Mirmon::debug ( [ $bool ] ) # get/set debug
1655 =head1 Mirmon object methods
1661 Returns Mirmon's Mirmon::Conf object.
1665 Returns a hashref C<< { url => mirror, ... } >>,
1666 where I<url> is as specified in the mirror list
1667 and I<mirror> is a Mirmon::Mirror object.
1671 Returns a hashref C<< { country_code =E<gt> country_name, ... } >>.
1673 =item B<config_list>
1675 Returns the list of default locations for config files.
1677 =item B<get_dates ( $get [, $URL] )>
1679 Probes all mirrors if $get is C<all> ; or a subset if $get is C<update> ;
1680 or only I<$URL> if $get is C<url>.
1684 =head1 Mirmon::Conf object methods
1686 A Mirmon::Conf object represents a mirmon conguration.
1687 It is normaly created by Mirmon::new().
1688 A specified (or default) config file is read and interpreted.
1692 =item attribute methods
1694 For every config file entry, there is an attribute method :
1695 B<web_page>, B<state>, B<countries>, B<mirror_list>, B<probe>,
1696 B<project_name>, B<project_url>, B<icons>, B<project_logo>,
1697 B<timeout>, B<max_probes>, B<min_poll>, B<max_poll>, B<min_sync>,
1698 B<max_sync>, B<list_style>, B<put_histo>, B<randomize>, B<add_slash>.
1702 Returns the file name of (the root of) the configuration file(s).
1706 Returns a hashref C<< { site => url, ... } >>,
1707 as specified in the mirmon config file.
1711 =head1 Mirmon::Mirror object methods
1713 A Mirmon::Mirror object represents the last known state of a mirror.
1714 It is normaly created by Mirmon::new() from the state file,
1715 as specified in the mirmon config file.
1716 Mirmon::Mirror objects can be used to probe mirrors.
1718 =head2 attribute methods
1724 The url as given in the mirror list.
1728 The mirror's timestamp found by the last successful probe,
1729 or 'undef' if no probe was ever successful.
1731 =item B<last_status>
1733 The status of the last probe, or 'undef' if the mirror was never probed.
1735 =item B<last_ok_probe>
1737 The timestamp of the last successful probe or 'undef'
1738 if the mirror was never successfully probed.
1740 =item B<probe_history>
1742 The probe history is a list of 's' (for success) and 'f' (for failure)
1743 characters indicating the result of the probe. New results are appended
1744 whenever the mirror is probed.
1746 =item B<state_history>
1748 The state history consists of a timestamp, a '-' char, and a list of
1749 chars indicating a past status: 's' (fresh), 'b' (oldish), 'f' (old),
1750 'z' (bad) or 'x' (skip).
1751 The timestamp indicates when the state history was last updated.
1752 The current status of the mirror is determined by the mirror's age and
1753 a few configuration parameters (min_sync, max_sync, max_poll).
1754 The state history is updated when the mirror is probed.
1755 If the last update of the history was less than 24 hours ago,
1756 the last status is replaced by the current status.
1757 If the last update of the history was more than 24 hours ago,
1758 the current status is appended to the history.
1759 One or more 'skip's are inserted, if the timestamp is two or more days old
1760 (when mirmon hasn't run for more than two days).
1764 The timestamp of the last probe, or 'undef' if the mirror was never probed.
1768 =head2 object methods
1774 Returns the parent Mirmon object.
1776 =item B<state_history_time>
1778 Returns the I<time> part of the state_history attribute.
1780 =item B<state_history_hist>
1782 Returns the I<history> part of the state_history attribute.
1784 =item B<type>, B<site>, B<home>
1786 For an url like I<ftp://www.some.org/path/to/home>,
1787 the B<type> is I<ftp>,
1788 the B<site> is I<www.some.org>,
1789 and B<home> is I<ftp://www.some.org/>.
1791 =item B<age_in_days>
1793 Returns the mirror's age (in fractional days), based on the mirror's
1794 timestamp as found by the last successful probe ; or based on the
1795 length of the state history if no probe was ever successful.
1796 Returns 'undef' if the mirror was never probed.
1800 Returns the mirror's contact address as specified in the mirror list.
1804 Returns the mirror's country code as specified in the mirror list.
1806 =item B<start_probe>
1808 Start a probe for the mirror in non-blocking mode ;
1809 returns the associated (IO::Handle) file handle.
1810 The caller must maintain an association between
1811 the handles and the mirror objects.
1813 =item B<finish_probe ( $handle )>
1815 Sets the (IO::Handle) B<$handle> to blocking IO ;
1816 reads a result from the handle,
1817 and updates the state of the mirror.
1826 <a href="mirmon.html">mirmon(1)</a>
1843 <a href="http://www.staff.science.uu.nl/~penni101/">Henk P. Penning</a>,
1844 <a href="http://www.uu.nl/faculty/science/EN/">Faculty of Science</a>,
1845 <a href="http://www.uu.nl/">Utrecht University</a>
1847 mirmon-2.10 - Fri Aug 15 12:26:55 2014 ; henkp ;
1848 <a href="http://validator.w3.org/check?uri=referer">verify html</a>
1855 (c) 2003-2014 Henk P. Penning
1856 Faculty of Science, Utrecht University
1857 http://www.staff.science.uu.nl/~penni101/ -- penning@uu.nl
1858 mirmon-2.10 - Fri Aug 15 12:26:55 2014 ; henkp
1864 (c) 2003-2014 Henk P. Penning
1865 Faculty of Science, Utrecht University
1866 http://www.staff.science.uu.nl/~penni101/ -- penning@uu.nl
1867 mirmon-2.10 - Fri Aug 15 12:26:55 2014 ; henkp
1873 package main ; #####################################################
1879 my $VERSION = Base::Version . ' - Fri Aug 15 12:26:55 2014 - henkp' ;
1880 my $DEF_CNF = join ', ', Mirmon -> config_list ;
1881 my $TIMEOUT = Base::DEF_TIMEOUT ;
1883 my $prog = substr $0, rindex ( $0, '/' ) + 1 ;
1884 my $Usage = <<USAGE ;
1885 Usage: $prog [-v] [-q] [-t timeout] [-c conf] [-get all|update|url <url>]
1886 option v : be verbose
1888 option t : set timeout ; default $TIMEOUT
1889 option get : get all : probe all sites
1890 : get update : probe a selection of the sites (see doc)
1891 : get url <url> : probe some <url> (in the mirror-list).
1892 option c : configuration file ; default search :
1894 -------------------------------------------------------------------
1895 Mirmon normally only reports errors and changes in the mirror list.
1897 -------------------------------------------------------------------
1899 sub Usage { die "$_[0]$Usage" ; }
1900 sub Error { die "$prog: $_[0]\n" ; }
1901 sub Warn { warn "$prog: $_[0]\n" ; }
1903 # usage: &GetOptions(ARG,ARG,..) defines $opt_ID as 1 or user spec'ed value
1904 # usage: &GetOptions(\%opt,ARG,ARG,..) defines $opt{ID} as 1 or user value
1905 # ARG = 'ID' | 'ID=SPC' | 'ID:SPC' for no-arg, required-arg or optional-arg
1906 # ID = perl identifier
1907 # SPC = i|f|s for integer, fixedpoint real or string argument
1910 Getopt::Long::config ( 'no_ignore_case' ) ;
1912 Usage '' unless GetOptions ( \%opt, qw(v q t=i get=s c=s version) ) ;
1913 Usage "Arg count\n" if @ARGV > 1 ;
1914 Usage "Arg count\n" if $opt{get} and $opt{get} eq 'url' and ! @ARGV ;
1916 if ( $opt{version} ) { printf "%s\n", Base::version () ; exit ; }
1918 $opt{v} ||= $opt{d} ;
1922 my $M = Mirmon -> new ( $opt{c} ) ;
1923 $M -> conf -> timeout ( $opt{t} ) if $opt{t} ;
1925 my $get = $opt{get} ;
1927 { Error "url $URL not in list"
1928 if $get eq 'url' and ! $M -> state -> { $URL } ;
1929 Error "unknown 'get option' '$get'" unless Base::is_get_opt ( $get ) ;
1932 Mirmon::verbose ( $opt{v} ) ;
1933 Mirmon::debug ( $opt{d} ) ;
1934 Mirmon::quiet ( $opt{q} ) ;
1936 if ( $get ) { $M -> get_dates ( $get, $URL ) ; $M -> put_state ; }
1937 $M -> gen_page ( $get, $VERSION ) ;
1945 mirmon - monitor the state of mirrors
1949 mirmon [-v] [-q] [-t timeout] [-c conf] [-get all|update|url url]
1957 Be verbose ; B<mirmon> normally only reports
1958 errors and changes in the mirror list.
1964 =item B<-t> I<timeout>
1966 Set the timeout ; the default is I<300>.
1968 =item B<-get> all | update | url <url>
1970 With B<all>, probe all sites.
1971 With B<update>, probe a selection of the sites ; see option C<max_poll> below.
1972 With B<url>, probe only the given I<url>, which must appear in the mirror-list.
1976 Use config file I<name>. The default list is
1978 ./mirmon.conf $HOME/.mirmon.conf /etc/mirmon.conf
1984 The program is intended to be run by cron every hour.
1986 42 * * * * perl /path/to/mirmon -get update
1988 It quietly probes a subset of the sites in a given list,
1989 writes the results in the 'state' file and generates a web page
1990 with the results. The subset contains the sites that are new, bad
1991 and/or not probed for a specified time.
1993 When no 'get' option is specified, the program just generates a
1994 new web page from the last known state.
1996 The program checks the mirrors by running a (user specified)
1997 program on a pipe. A (user specified) number of probes is
1998 run in parallel using nonblocking IO. When something can be
1999 read from the pipe, it switches the pipe to blocking IO and
2000 reads one line from the pipe. Then it flushes and closes the
2001 pipe. No attempt is made to kill the probe.
2003 The probe should return something that looks like
2007 that is, a line of text starting with a timestamp. The exit status
2008 of the probe is ignored.
2014 A config file can be specified with the -c option.
2015 If -c is not used, the program looks for a config file in
2019 =item * B<./mirmon.conf>
2021 =item * B<$HOME/.mirmon.conf>
2023 =item * B</etc/mirmon.conf>
2029 A config file looks like this :
2031 +--------------------------------------------------
2032 |# lines that start with '#' are comment
2033 |# blank lines are ignored too
2034 |# tabs are replaced by a space
2036 |# the config entries are 'key' and 'value' pairs
2037 |# a 'key' begins in column 1
2038 |# the 'value' is the rest of the line
2039 |somekey A_val B_val ...
2040 |otherkey X_val Y_val ...
2042 |# indented lines are glued
2043 |# the next three lines mean 'somekey part1 part2 part3'
2048 |# lines starting with a '+' are concatenated
2049 |# the next three lines mean 'somekey part1part2part3'
2054 |# lines starting with a '.' are glued too
2055 |# don't use a '.' on a line by itself
2056 |# 'somekey' gets the value "part1\n part2\n part3"
2060 +--------------------------------------------------
2062 =head2 required entries
2066 =item project_name I<name>
2068 Specify a short plaintext name for the project.
2073 =item project_url I<url>
2075 Specify an url pointing to the 'home' of the project.
2077 project_url http://www.apache.org/
2079 =item mirror_list I<file-name>
2081 Specify the file containing the mirrors to probe.
2083 mirror_list /path/to/mirror-list
2085 If your mirror list is generated by a program, use
2087 mirror_list /path/to/program arg1 ... |
2089 Two formats are supported :
2093 =item * plain : lines like
2095 us http://www.tux.org/ [email] ...
2096 nl http://apache.cs.uu.nl/dist/ [email] ...
2097 nl rsync://archive.cs.uu.nl/apache-dist/ [email] ...
2099 =item * apache : lines like those in the apache mirrors.list
2101 ftp us ftp://ftp.tux.org/pub/net/apache/dist/ user@tux.org ...
2102 http nl http://apache.cs.uu.nl/dist/ user@cs.uu.nl ...
2106 Note that in style 'plain' the third item is reserved for an
2107 optional email address : the site's contact address.
2109 Specify the required format with option C<list_style> (see below).
2110 The default style is 'plain'.
2112 =item web_page I<file-name>
2114 Specify where the html report page is written.
2116 =item icons I<directory-name>
2118 Specify the directory where the icons can be found,
2119 relative to the I<web_page>, or relative to the
2120 DOCUMENTROOT of the web server.
2122 If/when the I<web_page> lives in directory C<.../mirmon/> and
2123 the icons live in directory C<.../mirmon/icons/>,
2128 If/when the icons live in C</path/to/DOCUMENTROOT/icons/mirmon/>, specify
2132 =item probe I<program + arguments>
2134 Specify the program+args to probe the mirrors. Example:
2136 probe /usr/bin/wget -q -O - -T %TIMEOUT% -t 1 %URL%TIME.txt
2138 Before the program is started, %TIMEOUT% and %URL% are
2139 substituted with the proper timeout and url values.
2141 Here it is assumed that each hour the root server writes
2142 a timestamp in /path/to/archive/TIME.txt, for instance with
2143 a crontab entry like
2145 42 * * * * perl -e 'print time, "\n"' > /path/to/archive/TIME.txt
2147 Mirmon reads one line of output from the probe and interprets
2148 the first word on that line as a timestamp ; for example :
2151 1043625600 Mon Jan 27 00:00:00 2003
2152 1043625600 www.apache.org Mon Jan 27 00:00:00 2003
2154 Mirmon is distributed with a program C<probe> that handles
2155 ftp, http and rsync urls.
2157 =item state I<file-name>
2159 Specify where the file containing the state is written.
2161 The program reads this file on startup and writes the
2162 file when mirrors are probed (-get is specified).
2164 =item countries I<file-name>
2166 Specify the file containing the country codes;
2167 The file should contain lines like
2172 The mirmon package contains a recent ISO list.
2174 I<Fake> domains like I<Backup>, I<Master> are allowed,
2175 and are listed first in the report ; lowercase-first
2176 fake domains (like I<backup>) are listed last.
2180 =head2 optional entries
2184 =item max_probes I<number>
2186 Optionally specify the number of parallel probes (default 25).
2188 =item timeout I<seconds>
2190 Optionally specify the timeout for the probes (default 300).
2192 After the last probe is started, the program waits for
2193 <timeout> + 10 seconds, cleans up and exits.
2195 =item project_logo I<logo>
2197 Optionally specify (the SRC of the IMG of) a logo to be placed
2198 top right on the page.
2200 project_logo /icons/apache.gif
2201 project_logo http://www.apache.org/icons/...
2203 =item htm_head I<html>
2205 Optionally specify some HTML to be placed before </HEAD>.
2208 <link REL=StyleSheet HREF="/style.css" TYPE="text/css">
2210 =item htm_top I<html>
2212 Optionally specify some HTML to be placed near the top of the page.
2214 htm_top testing 1, 2, 3
2216 =item htm_foot I<html>
2218 Optionally specify HTML to be placed near the bottom of the page.
2222 <A HREF="..."><IMG SRC="..." BORDER=0></A>
2225 =item put_histo top|bottom|nowhere
2227 Optionally specify where the age histogram must be placed.
2228 The default is 'top'.
2230 =item min_poll I<time-spec>
2232 For 'min_poll' see next item. A I<time-spec> is a number followed by
2233 a unit 's' (seconds), or 'm' (minutes), or 'h' (hours), or 'd' (days).
2234 For example '3d' (three days) or '36h' (36 hours).
2236 =item max_poll I<time-spec>
2238 Optionally specify the maximum probe interval. When the program is
2239 called with option '-get update', all sites are probed which are :
2245 the site appears in the list, but there is no known state
2249 the last probe of the site was unsuccessful
2253 the last probe was more than 'max_poll' ago.
2257 Sites are not probed if the last probe was less than 'min_poll' ago.
2263 the 'reachable' sites are probed twice daily and the 'unreachable'
2264 sites are probed at most six times a day.
2266 The default 'min_poll' is '1h' (1 hour).
2267 The default 'max_poll' is '4h' (4 hours).
2269 =item min_sync I<time-spec>
2271 Optionally specify how often the mirrors are required to make an update.
2273 The default 'min_sync' is '1d' (1 day).
2275 =item max_sync I<time-spec>
2277 Optionally specify the maximum allowable sync interval.
2279 Sites exceeding the limit will be considered 'old'.
2280 The default 'max_sync' is '2d' (2 days).
2282 =item always_get I<region ...>
2284 Optionally specify a list of regions that must be probed always.
2286 always_get Master Tier1
2288 This is intended for I<fake regions> like I<Master> etc.
2292 Mirmon tries to balance the probe load over the hourly mirmon runs.
2293 If the current run has a below average number of mirrors to probe,
2294 mirmon probes a few extra, randomly chosen mirrors, picked from the
2295 runs that have the highest load.
2297 If you don't want this behaviour, use B<no_randomize>.
2301 If the url part of a line in the mirror_list doesn't end
2302 in a slash ('/'), mirmon adds a slash and issues a warning
2303 unless it is in quiet mode.
2305 If you don't want this behaviour, use B<no_add_slash>.
2307 =item list_style plain|apache
2309 Optionally specify the format ('plain' or 'apache') of the mirror-list.
2311 See the description of 'mirror_list' above.
2312 The default list_style is 'plain'.
2314 =item site_url I<site> I<url>
2316 Optionally specify a substitute url for a site.
2318 When access to a site is restricted (in Australia, for instance),
2319 another (sometimes secret) url can be used to probe the site.
2320 The <site> of an url is the part between '://' and the first '/'.
2322 =item env I<key> I<value>
2324 Optionally specify an environment variable.
2326 =item include I<file-name>
2328 Optionally specify a file to include.
2330 The specified file is processed 'in situ'. After the specified file is
2331 read and processed, config processing is resumed in the file where the
2332 C<include> was encountered.
2333 The include depth is unlimited. However, it is a fatal error to
2334 include a file twice under the same name.
2338 When the config processor encounters the 'show' command, it
2339 dumps the content of the current config to standout, if option
2340 C<-v> is specified. This is intented for debugging.
2344 When the config processor encounters the 'exit' command, it
2345 terminates the program. This is intented for debugging.
2349 =head1 STATE FILE FORMAT
2351 The state file consists of lines; one line per site.
2352 Each line consists of white space separated fields.
2353 The seven fields are :
2357 =item * field 1 : url
2359 The url as given in the mirror list.
2361 =item * field 2 : age
2363 The mirror's timestamp found by the last successful probe,
2364 or 'undef' if no probe was ever successful.
2366 =item * field 3 : status last probe
2368 The status of the last probe, or 'undef' if the mirror was never probed.
2370 =item * field 4 : time last successful probe
2372 The timestamp of the last successful probe or 'undef'
2373 if the mirror was never successfully probed.
2375 =item * field 5 : probe history
2377 The probe history is a list of 's' (for success) and 'f' (for failure)
2378 characters indicating the result of the probe. New results are appended
2379 whenever the mirror is probed.
2381 =item * field 6 : state history
2383 The state history consists of a timestamp, a '-' char, and a list of
2384 chars indicating a past status: 's' (fresh), 'b' (oldish), 'f' (old),
2385 'z' (bad) or 'x' (skip).
2386 The timestamp indicates when the state history was last updated.
2387 The current status of the mirror is determined by the mirror's age and
2388 a few configuration parameters (min_sync, max_sync, max_poll).
2389 The state history is updated when the mirror is probed.
2390 If the last update of the history was less than 24 hours ago,
2391 the last status is replaced by the current status.
2392 If the last update of the history was more than 24 hours ago,
2393 the current status is appended to the history.
2394 One or more 'skip's is inserted, if the timestamp is two or more days old
2395 (when mirmon hasn't run for more than two days).
2397 =item * field 7 : last probe
2399 The timestamp of the last probe, or 'undef' if the mirror was never probed.
2409 =item * Note: The (empty) state file must exist before mirmon runs.
2411 =item * The mirmon repository is here :
2413 https://svn.science.uu.nl/repos/project.mirmon/trunk/
2415 =item * The mirmon tarball is here :
2417 http://www.staff.science.uu.nl/~penni101/mirmon/mirmon.tar.gz
2421 =head2 installation suggestions
2423 To install and configure mirmon, take the following steps :
2427 =item * First, make the webdir :
2432 For I<DOCUMENTROOT>, substitute the full pathname
2433 of the document root of your webserver.
2435 =item * Check out the mirmon repository :
2438 svn checkout REPO mirmon
2442 REPO = https://svn.science.uu.nl/repos/project.mirmon/trunk/
2444 or download the package and unpack it.
2446 =item * Chdir to directory mirmon :
2450 =item * Create the (empty) state file :
2454 =item * Install the icons in the webdir :
2456 mkdir DOCUMENTROOT/mirmon/icons
2457 cp icons/* DOCUMENTROOT/mirmon/icons
2459 =item * Create a mirror list C<mirror_list> ;
2461 Use your favorite editor, or genererate the list from an
2464 nl http://archive.cs.uu.nl/your-project/ contact@cs.uu.nl
2465 uk http://mirrors.this.org/your-project/ mirrors@this.org
2466 us http://mirrors.that.org/your-project/ mirrors@that.org
2468 The email addresses are optional.
2470 =item * Create a mirmon config file C<mirmon.conf> with your favorite editor.
2472 # lines must start in the first column ; no leading white space
2475 mirror_list mirror_list
2477 countries countries.list
2478 web_page DOCUMENTROOT/mirmon/index.html
2480 probe /usr/bin/wget -q -O - -T %TIMEOUT% -t 1 %URL%TIME.txt
2482 This assumes the project's timestamp is in file C<TIME.txt>.
2484 =item * If you have rsync urls, change the probe line to :
2486 probe perl /usr/local/src/mirmon/probe -t %TIMEOUT% %URL%TIME.txt
2488 =item * Run mirmon :
2490 perl mirmon -v -get all
2492 The mirmon report should now be in 'DOCUMENTROOT/mirmon/index.html'
2494 http://www.your.project.org/mirmon/
2496 =item * If/when, at a later date, you want to upgrade mirmon :
2498 cd /usr/local/src/mirmon
2509 <a href="mirmon.pm.html">mirmon.pm(3)</a>
2526 <a href="http://www.staff.science.uu.nl/~penni101/">Henk P. Penning</a>,
2527 <a href="http://www.uu.nl/faculty/science/EN/">Faculty of Science</a>,
2528 <a href="http://www.uu.nl/">Utrecht University</a>
2530 mirmon-2.10 - Fri Aug 15 12:26:55 2014 ; henkp ;
2531 <a href="http://validator.w3.org/check?uri=referer">verify html</a>
2538 (c) 2003-2014 Henk P. Penning
2539 Faculty of Science, Utrecht University
2540 http://www.staff.science.uu.nl/~penni101/ -- penning@uu.nl
2541 mirmon-2.10 - Fri Aug 15 12:26:55 2014 ; henkp
2547 (c) 2003-2014 Henk P. Penning
2548 Faculty of Science, Utrecht University
2549 http://www.staff.science.uu.nl/~penni101/ -- penning@uu.nl
2550 mirmon-2.10 - Fri Aug 15 12:26:55 2014 ; henkp