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 # $Id: mirmon,v 1.37 2006/12/04 15:16:11 henkp Exp henkp $
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
27 my $VER = '$Id: mirmon,v 1.37 2006/12/04 15:16:11 henkp Exp henkp $';
32 my $DEF_CNF = "/etc/$PRG.conf";
33 my %CNF = qw( timeout 300
43 my @REQ_KEYS = qw( web_page state countries mirror_list probe
44 project_name project_url icons
47 qw( project_logo min_poll min_sync max_sync list_style htm_top htm_foot
51 for ( @REQ_KEYS, @OPT_KEYS, keys %CNF ) { $CNF_KEYS{$_}++; }
52 my $TIM_PAT = '^(\d+)([smhd])$';
53 my @LIST_STYLE = qw(plain apache);
54 my @GET_OPTS = qw(all update);
55 my @PUT_HGRAM = qw(top bottom nowhere);
58 for (qw(backup ftp http)) { $APA_TYPES{$_}++; }
59 my $prog = substr( $0, rindex( $0, '/' ) + 1 );
61 Usage: $prog [ -v ] [ -q ] [ -t timeout ] [ -get opt ] [ -c conf ]
64 option t : set timeout [ default $CNF{timeout} ] ;
65 option get : 'all' : probe all sites
66 : 'update' : probe a selection of the sites (see doc)
67 option c : configuration file [ default $DEF_CNF ]
68 -------------------------------------------------------------------
69 Documentation : the program contains 'pod' style documentation.
70 Extract the doc with 'pod2text $prog' or 'pod2html $prog OUT', etc.
71 -------------------------------------------------------------------
73 sub Usage { die "$_[0]$Usage"; }
74 sub Error { die "$prog: $_[0]\n"; }
75 sub Warn { warn "$prog: $_[0]\n"; }
76 # usage: &GetOptions(ARG,ARG,..) defines $opt_ID as 1 or user spec'ed value
77 # usage: &GetOptions(\%opt,ARG,ARG,..) defines $opt{ID} as 1 or user value
78 # ARG = 'ID' | 'ID=SPC' | 'ID:SPC' for no-arg, required-arg or optional-arg
79 # ID = perl identifier
80 # SPC = i|f|s for integer, fixedpoint real or string argument
82 Getopt::Long::config('no_ignore_case');
83 # Usage() unless GetOptions() ;
85 Usage() unless GetOptions( \%opt, 'v', 'q', 't=i', 'get=s', 'c=s' );
86 Usage("Arg count\n") unless @ARGV >= 0;
88 my $GET = IO::Select->new();
95 # <META HTTP-EQUIV=Expires CONTENT="Tue, 04 Dec 1993 21:29:02 GMT">
97 my @day = qw(Sun Mon Tue Wed Thu Fri Sat);
98 my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
99 my @gmt = gmtime time + 3600;
100 sprintf "%s, %02d %s %4d %02d:%02d:%02d GMT", $day[ $gmt[6] ], $gmt[3],
101 $mon[ $gmt[4] ], $gmt[5] + 1900, @gmt[ 2, 1, 0 ];
104 return $opt{c} if $opt{c};
105 my $HOME = ( getpwuid $< )[7] or Error "can get homedir '$<' ($!)";
106 my @LIST = ( "$PRG.conf", "$HOME/.$PRG.conf", $DEF_CNF );
107 for my $conf (@LIST) { return $conf if -f $conf; }
108 Error sprintf "can't find a config file :\n %s", join "\n ", @LIST;
111 print "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n";
112 for my $key ( sort keys %CNF ) {
113 next if $key =~ m/^_/;
114 print "show_conf : $key = '$CNF{$key}'\n";
116 for my $key ( sort keys %HREF ) {
117 printf "show_conf : for site '%s' use instead\n '%s'\n", $key,
121 printf "show_conf : included '%s'\n", join "', '", @{ $CNF{_include} };
122 print "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n";
127 if ( grep $_ eq $FILE, @{ $CNF{_include} } ) {
128 Error "already included : '$FILE'";
130 else { push @{ $CNF{_include} }, $FILE; }
131 open FILE, $FILE or Error "can't open '$FILE' ($!)";
132 my $CONF = join "\n", grep /./, <FILE>;
134 $CONF =~ s/\t/ /g; # replace tabs
135 $CONF =~ s/^[+ ]+//; # delete leading space, plus
136 $CONF =~ s/\n\n\s+/ /g; # glue continuation lines
137 $CONF =~ s/\n\n\+\s+//g; # glue concatenation lines
138 $CONF =~ s/\n\n\./\n/g; # glue concatenation lines
140 print "--$CONF--\n" if $opt{d};
141 for ( grep !/^#/, split /\n\n/, $CONF ) {
142 my ( $key, $val ) = split ' ', $_, 2;
143 $val = '' unless defined $val;
144 print "conf '$FILE' : key '$key', val '$val'\n" if $opt{d};
145 if ( exists $CNF_KEYS{$key} ) { $CNF{$key} = $val; }
146 elsif ( $key eq 'site_url' ) {
147 my ( $site, $url ) = split ' ', $val;
148 $url .= '/' unless $url =~ m!/$!;
149 $HREF{ lc $site } = $url;
150 printf "config : for site '%s' use instead\n '%s'\n", $site, $url
153 elsif ( $key eq 'env' ) {
154 my ( $x, $y ) = split ' ', $val;
156 printf "config : setenv '%s'\n '%s'\n", $x, $y if $opt{v};
158 elsif ( $key eq 'no_randomize' ) { $CNF{randomize} = 0; }
159 elsif ( $key eq 'include' ) { get_conf $val ; }
160 elsif ( $key eq 'show' ) { show_conf unless $opt{q}; }
161 elsif ( $key eq 'exit' ) { Error 'exit per config directive'; }
162 elsif ( $key eq 'max_age' ) { $CNF{max_sync} = $val; }
165 Error "unknown keyword '$key' (value '$val')";
172 $CNF{timeout} = $opt{t} if $opt{t};
173 for my $key (@REQ_KEYS) {
174 unless ( exists $CNF{$key} ) {
175 $err .= "$prog error: missing config for '$key'\n";
178 for my $key (qw(min_poll max_poll max_sync min_sync)) {
179 my $max = $CNF{$key};
180 unless ( $max =~ /$TIM_PAT/o ) {
181 $err .= "$prog error: $key ($max) doesn't match /$TIM_PAT/\n";
184 unless ( grep $CNF{list_style} eq $_, @LIST_STYLE ) {
185 $err .= sprintf "%s : error: unknown 'list_style' '%s'\n", $prog,
188 unless ( grep $CNF{put_histo} eq $_, @PUT_HGRAM ) {
189 $err .= sprintf "%s : error: unknown 'put_histo' '%s'\n", $prog,
192 if ( $opt{get} and not grep $opt{get} eq $_, @GET_OPTS ) {
193 $err .= sprintf "%s : error: unknown 'get option' '%s'\n", $prog,
197 $opt{q} = 0 if $opt{v};
201 my %tab = ( 's' => 1, 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24 );
202 Error "wrong time '$tim'" unless $tim =~ /$TIM_PAT/o;
205 return $m * $tab{$u};
207 sub aprx_eq { my ( $t1, $t2 ) = @_; abs( $t1 - $t2 ) < 60; }
208 sub aprx_ge { my ( $t1, $t2 ) = @_; $t1 > $t2 or aprx_eq $t1, $t2; }
209 sub aprx_le { my ( $t1, $t2 ) = @_; $t1 < $t2 or aprx_eq $t1, $t2; }
210 sub aprx_gt { my ( $t1, $t2 ) = @_; $t1 > $t2 and not aprx_eq $t1, $t2; }
211 sub aprx_lt { my ( $t1, $t2 ) = @_; $t1 < $t2 and not aprx_eq $t1, $t2; }
216 my $m = int( $mins + 0.5 );
217 my $hours = $s / ( 60 * 60 );
218 my $h = int( $hours + 0.5 );
219 if ( $s < 50 ) { $magn = $s; $unit = 'second'; }
220 elsif ( $m < 50 ) { $magn = $m; $unit = 'minute'; }
221 elsif ( $h < 36 ) { $magn = $h; $unit = 'hour'; }
222 else { $magn = sprintf "%.1f", $hours / 24; $unit = 'day'; }
223 $unit .= 's' unless $magn == 1;
224 return "$magn $unit";
227 ( tim_to_s $CNF {min_sync} ) + ( tim_to_s $CNF {max_poll} );
230 ( tim_to_s $CNF {max_sync} ) + ( tim_to_s $CNF {max_poll} );
233 ( tim_to_s $CNF {min_poll} ) + ( tim_to_s $CNF {max_poll} );
237 return 'z' unless $time =~ /^\d+$/;
239 ( aprx_ge( $time, $^T - max_age1 ) )
241 : ( aprx_ge( $time, $^T - max_age2 ) ? 'b' : 'f' )
247 printf "*** %-10s %s\n", $stat, $url unless $opt{q};
248 my ( $time, $vrfy, $hstp, $hsts );
249 if ( exists $OLD{$url} ) {
250 $time = $OLD{$url}[0];
251 $vrfy = $OLD{$url}[2];
252 $hstp = substr $OLD{$url}[3], 1 - $HIST;
253 $hsts = $OLD{$url}[4];
261 $RES{$url} = [ $time, $stat, $vrfy, $hstp . 'f', $hsts, $^T ];
269 ? substr( $OLD{$url}[3], 1 - $HIST )
272 my $hsts = ( exists $OLD{$url} ? $OLD{$url}[4] : '' );
273 printf "result %d %s\n", $time, $url if $opt{v};
274 $RES{$url} = [ $time, $stat, $^T, $hstp . 's', $hsts, $^T ];
278 open STT, $STT or Error "can't open '$STT' ($!)";
281 my ( $url, $time, $stat, $vrfy, $hstp, $hsts, $lprb ) = split ' ';
283 $hstp = '' unless defined $hstp;
284 $hsts = '' unless defined $hsts;
285 $hsts = '' if $hsts eq 'undef';
286 $lprb = 'undef' unless defined $lprb;
287 $OLD{$url} = [ $time, $stat, $vrfy, $hstp, $hsts, $lprb ];
294 printf "check_hist: last '$time' hsts '$hsts'\n" if $opt{d};
297 if ( $hsts eq '' ) { $stmp = 0; $hist = ''; }
298 else { ( $stmp, $hist ) = split '-', $hsts; }
299 if ( aprx_le $stmp, $^T - tim_to_s '1d' ) {
300 $res = sprintf "%s-%s%s", $^T, substr( $hist, 1 - $HIST ),
307 my $TMP = "$STT.tmp";
308 open TMP, ">$TMP" or Error "can't write '$TMP' ($!)";
309 for my $url ( sort keys %RES ) {
310 $RES{$url}[4] = check_hist $RES {$url}[0], $RES{$url}[4];
311 my @OUT = @{ $RES{$url} };
313 printf TMP "%s %s\n", $url, join ' ', @OUT
314 or Error "can't print to $TMP ($!)";
317 if ( -z $TMP ) { Warn "wrote empty state file; keeping previous version"; }
318 else { rename $TMP, $STT or Error "can't rename '$TMP', '$STT' ($!)"; }
322 open CCS, $CCS or Error "can't open '$CCS' ($!)";
326 my ( $code, $dash, $reg ) = split ' ', $_, 3;
327 $CCS{ lc $code } = lc $reg;
333 my ( $type, $site, $home );
334 if ( $url =~ m!^(ftp|http)://([^/:]+)(:\d+)?/! ) {
339 return $type, $site, $home;
341 sub type { my ( $t, $s, $h ) = type_site $_[0]; $t; }
342 sub site { my ( $t, $s, $h ) = type_site $_[0]; $s; }
343 sub home { my ( $t, $s, $h ) = type_site $_[0]; $h; }
347 open LST, $LST or Error "can't open '$LST' ($!)";
352 if ( $CNF{list_style} eq 'plain' ) {
353 ( $reg, $url ) = split ' ';
354 unless ( $url =~ m!/$! ) {
355 print "*** mirmon appended '/' to $url\n" unless $opt{q};
359 elsif ( $CNF{list_style} eq 'apache' ) {
361 ( $apache_type, $reg, $url ) = split ' ';
362 unless ( defined $APA_TYPES{$apache_type} ) {
363 print "*** strange type : $apache_type\n" unless $opt{q};
366 unless ( $url =~ m!/$! ) {
367 print "*** missing '/' in $url\n" unless $opt{q};
371 my $site = site $url ;
372 my $type = type $url ;
373 unless ( defined $site ) {
374 print "*** strange url : '$url'\n" unless $opt{q};
377 $LST{$url} = [ $type, $site, $reg ];
380 sub url { sprintf '<A HREF="%s">%s</A>', $_[0], $_[1]; }
381 sub nam { sprintf '<A NAME="%s">%s</A>', $_[0], $_[1]; }
382 sub SMA { sprintf "<FONT SIZE=\"-1\">%s</FONT>", $_[0]; }
383 sub BLD { sprintf "<B>%s</B>", $_[0]; }
384 sub NSS { sprintf SMA('%s site%s'), $_[0], ( $_[0] == 1 ? '' : 's' ); }
385 sub TAB { sprintf "<TABLE BORDER=2 CELLPADDING=3>%s</TABLE>", $_[0]; }
386 sub TR { sprintf "<TR>%s</TR>\n", $_[0]; }
387 sub TH { sprintf "<TH>%s</TH>\n", $_[0]; }
388 sub TD { sprintf "<TD>%s</TD>\n", $_[0]; }
389 sub TDr { sprintf "<TD ALIGN=\"RIGHT\">%s</TD>\n", $_[0]; }
390 sub RED { sprintf "<FONT COLOR=\"RED\">%s</FONT>", $_[0]; }
391 sub GRN { sprintf '<FONT COLOR="GREEN">%s</FONT>', $_[0]; }
403 if ( $time == $^T ) { $res = BLD 'renewed'; }
405 $res = pr_interval $^T - $time;
406 $res = BLD RED $res if aprx_lt $time, $max;
411 sprintf '<IMG BORDER=1 SRC="%s/mm%s%02d.gif" ALT="">', $CNF{icons}, $_[0],
414 sub img_sf { img_sf_cnt $_[0], 1; }
417 return '' unless $hst =~ m/^[sbfz]+$/;
418 if ( length $hst == $HIST and $hst =~ /^(s*b)s*$/ ) {
419 return img_sf_cnt 'sb', length $1;
421 elsif ( length $hst == $HIST and $hst =~ /^(s*f)s*$/ ) {
422 return img_sf_cnt 'sf', length $1;
424 elsif ( length $hst == $HIST and $hst =~ /^(s*b)fs*$/ ) {
425 return img_sf_cnt 'sbf', length $1;
429 my $prf = substr $hst, 0, 1;
430 $hst = substr $hst, 1;
431 while ( $hst ne '' ) {
432 if ( substr( $prf, 0, 1 ) eq substr( $hst, 0, 1 ) ) {
434 $hst = substr $hst, 1;
437 $res .= img_sf_cnt $prf, $cnt;
438 $prf = substr $hst, 0, 1;
439 $hst = substr $hst, 1;
443 $res .= img_sf_cnt $prf, $cnt if $cnt;
449 return '' if $hsts eq '';
450 my ( $t, $h ) = split '-', $hsts;
451 if ( aprx_lt $t, $^T ) { $h .= age_code $time ; }
452 return show_hist substr $h, -$HIST;
454 sub gen_histogram_probes {
455 my ( $time, $stat, $vrfy, $hstp, $hsts, $lprb );
463 return '' unless scalar keys %RES;
464 for my $url ( keys %RES ) {
465 ( $time, $stat, $vrfy, $hstp, $hsts, $lprb ) = @{ $RES{$url} };
466 my $hr = int( ( $^T - $lprb ) / 3600 + 0.5 );
467 $hr_min = $hr if !defined $hr_min or $hr < $hr_min;
468 $hr_max = $hr if !defined $hr_max or $hr > $hr_max;
469 if ( $stat eq 'ok' ) { $tab{$hr}++; $s_cnt++; }
470 else { $bad{$hr}++; $f_cnt++; }
476 . TH sprintf( '%s %s, %s %s',
477 $s_cnt, GRN('successful'), $f_cnt, RED('failed') )
480 for my $x ( keys %tab ) {
481 my $tot = $tab{$x} + ( $bad{$x} || 0 );
482 $max = $tot if $max < $tot;
484 return "<BLOCKQUOTE>\nnothing yet\n</BLOCKQUOTE>\n" unless $max;
485 for my $hr ( $hr_min .. $hr_max ) {
486 my $x = $tab{$hr} || 0;
487 my $y = $bad{$hr} || 0;
488 my $n = int( $x / $max * $HIST );
489 my $b = int( $y / $max * $HIST );
495 ( $n ? img_sf_cnt( 's', $n ) : '' )
496 . ( $b ? img_sf_cnt( 'f', $b ) : '' )
497 . ( ( $n + $b ) ? '' : ' ' )
501 return "<BLOCKQUOTE>\n" . TAB($res) . "</BLOCKQUOTE>\n";
504 my $MAX_H = max_age1;
506 ( 20 * 3600 <= $MAX_H and $MAX_H <= 36 * 3600 )
507 ? int( $MAX_H / 3600 )
510 my $MAX_O = max_age2;
511 my $MAX_o = int( $MAX_O / 3600 + 0.5 );
513 my %W = ( 'old' => 1, 'ded' => 1, 'bad' => 1 );
514 my %Wmx = ( 'old' => 5, 'ded' => 3, 'bad' => 3 );
518 for ( my $x = 0 ; $x < $MAX_h ; $x++ ) { $tab{$x} = 0; }
522 for my $url ( keys %RES ) {
523 my $time = $RES{$url}[0];
524 if ( $time =~ /^\d+$/ ) {
526 my $hr = int( $s / $MAX_H * ( $MAX_h - 1 ) + 0.5 );
527 if ( $s <= $MAX_H ) { $tab{$hr}++; }
528 elsif ( $s <= $MAX_O ) { $tab{old}++; }
529 else { $tab{ded}++; }
531 else { $tab{bad}++; }
534 for ( grep !exists $Wmx{$_}, keys %tab ) {
535 $max = $tab{$_} if $tab{$_} > $max;
538 for my $aux ( keys %Wmx ) {
539 $bad{$aux} = $tab{$aux};
540 if ( $bad{$aux} > $max ) {
541 $W{$aux} = $Wmx{$aux};
542 my $d = int( $bad{$aux} / $W{$aux} );
543 for ( my $i = 1 ; $i < $W{$aux} ; $i++ ) {
544 $tab{ $aux . $i } = $d;
545 if ( $bad{$aux} % $Wmx{$aux} > $i ) {
550 $tab{$aux} -= ( $W{$aux} - 1 ) * $d;
551 $max = $tab{$aux} if $max < $tab{$aux};
555 # { for my $hr ( keys %tab )
556 # { printf "tab '%s' = '%s'\n", $hr, $tab { $hr } ; }
558 return 'nothing yet' unless $max;
559 $H = $max if 8 <= $max and $max <= 26;
560 for ( keys %tab ) { $hst{$_} = int( $H * $tab{$_} / $max + 0.5 ); }
561 my @keys = sort { $a <=> $b } grep /^\d+$/, keys %hst;
563 for my $hr (@keys) { $tab_hr += $tab{$hr}; }
564 push @keys, grep ( m/^old/, sort keys %tab ),
565 grep ( m/^ded/, sort keys %tab ), grep ( m/^bad/, sort keys %tab );
566 for ( my $h = $H ; $h > 0 ; $h-- ) {
568 $res .= sprintf "<TH ROWSPAN=3 VALIGN=\"TOP\">↑</TH>\n"
570 $res .= sprintf '<TD ROWSPAN=%d ALIGN="CENTER">%s</TD>' . "\n", $H - 6,
573 $res .= sprintf "<TH ROWSPAN=3 VALIGN=\"BOTTOM\">↓</TH>\n"
576 $res .= sprintf "<TH>%s</TH>\n",
582 : ( $x =~ /^old/ ? 'b' : ( $x =~ /^ded/ ? 'f' : 'z' ) )
585 ( $h == 1 and $hst{$x} == 0 )
586 ? sprintf( '<IMG SRC="%s/bar.gif" ALT="" BORDER=0>',
594 my $HR = '<HR SIZE=2 WIDTH="95%%" NOSHADE>';
596 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", 1;
597 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $MAX_h;
598 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W{old};
599 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W{ded};
600 $res .= sprintf "<TD COLSPAN=%d>$HR</TD>\n", $W{bad};
603 $res .= '<TD ALIGN="CENTER"> <B>age</B> → </TD>';
604 $res .= "<TH>|</TH>\n";
606 sprintf( '<TD COLSPAN=%d ALIGN="CENTER">'
607 . '← 0 ≤ <B>age</B> ≤ %s →'
609 $MAX_h - 2, pr_interval($MAX_H) );
610 $res .= "<TH>|</TH>\n";
612 '<TD ALIGN="CENTER" COLSPAN=%d>'
613 . ' %sh < %s ≤ %sh '
615 $W{old}, int( $MAX_H / 60 / 60 ),
619 '<TD ALIGN="CENTER" COLSPAN=%d>'
620 . ' <FONT COLOR="RED">old</FONT> '
625 '<TD ALIGN="CENTER" COLSPAN=%d>'
626 . ' <FONT COLOR="RED">bad</FONT> '
631 my $FRMT = '<TD ALIGN="CENTER" COLSPAN=%d> %s </TD>';
633 $res .= sprintf "$FRMT\n", 1, NSS scalar keys %RES;
634 $res .= "<TH>|</TH>\n";
635 $res .= sprintf "$FRMT\n", $MAX_h - 2, NSS $tab_hr ;
636 $res .= "<TH>|</TH>\n";
637 $res .= sprintf "$FRMT\n", $W{old}, NSS $bad {old};
638 $res .= sprintf "$FRMT\n", $W{ded}, NSS $bad {ded};
639 $res .= sprintf "$FRMT\n", $W{bad}, NSS $bad {bad};
641 $res = "<TABLE CELLSPACING=0 CELLPADDING=1 BORDER=0>\n$res\n</TABLE>\n";
642 $res = sprintf "<TABLE CELLPADDING=5 BORDER=4>%s</TABLE>\n",
643 "<TR><TH>\n$res\n</TH></TR>";
645 $res .= sprintf "<BR>units %s %s %s %s represent one mirror site.\n",
646 img_sf('s'), img_sf('f'), img_sf('b'), img_sf('z');
650 sprintf "<BR>each %s %s %s %s unit represents %s mirror sites.\n",
651 img_sf('s'), img_sf('f'), img_sf('b'), img_sf('z'),
652 sprintf( "%.1f", $max / $H );
656 sub revdom { my $dom = shift; join '.', reverse split /\./, $dom; }
658 my $a_type = $a->[0];
659 my $b_type = $b->[0];
660 my $a_site = $a->[2];
661 my $b_site = $b->[2];
662 ( revdom $a_site ) cmp( revdom $b_site )
663 or $a_type cmp $b_type;
665 sub by_CCS { ( $CCS{$a} || $a ) cmp( $CCS{$b} || $b ); }
669 my $TMP = "$PPP.tmp";
672 for my $url ( keys %LST ) {
673 my ( $type, $site, $reg ) = @{ $LST{$url} };
674 push @{ $tab{$reg} }, [ $type, $url, $site ];
682 for my $url ( keys %RES ) {
683 my ( $time, $stat, $vrfy ) = @{ $RES{$url} };
684 if ( $stat eq 'ok' ) { $ok++; }
685 else { $stats{$stat}++; }
686 if ( $time eq 'undef' ) { $bad++; }
687 elsif ( 'f' eq age_code $time ) { $old++; }
688 if ( $vrfy eq 'undef' or aprx_lt $vrfy, $^T - max_vrfy ) { $unr++; }
691 sprintf "%d bad -- %d older than %s -- %s unreachable for more than %s",
692 $bad, $old, pr_interval(max_age2), $unr, pr_interval(max_vrfy);
693 my $PROB = 'last probes : ';
694 push @stats, "$ok were ok" if $ok;
695 for my $stat ( sort keys %stats ) {
696 push @stats, sprintf "%s had %s", $stats{$stat}, RED $stat ;
698 $PROB .= join ', ', @stats;
699 for my $reg ( sort keys %tab ) {
700 $refs .= sprintf " %s \n", url "#$reg",
701 "<FONT SIZE=\"+1\">$reg</FONT>";
709 '<IMG SRC="%s" ALT="%s" ALIGN="RIGHT" BORDER=0>',
710 $CNF{project_logo}, $CNF{project_name}
714 my $HTOP = $CNF{htm_top} ? $CNF{htm_top} . "\n" : '';
715 my $FOOT = $CNF{htm_foot} ? $CNF{htm_foot} . "\n" : '';
716 my $HEAD = $CNF{htm_head} ? $CNF{htm_head} . "\n" : '';
717 my $TITL = url $CNF{project_url}, $CNF{project_name};
719 open PPP, ">$TMP" or Error "can't write $TMP ($!)";
720 print PPP '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01'
721 . ' Transitional//EN"' . '>';
722 print PPP "<HTML>\n";
723 print PPP "<HEAD>\n";
724 print PPP "<TITLE>the status of $CNF{project_name} mirrors</TITLE>\n";
725 printf PPP "%s\n", '<meta HTTP-EQUIV="content-type" '
726 . 'CONTENT="text/html; charset=ISO-8859-1">';
727 print PPP "<META HTTP-EQUIV=\"refresh\" CONTENT=\"3600\">\n";
728 print PPP "<META HTTP-EQUIV=\"Expires\" CONTENT=\"$EXPD\">\n";
729 print PPP $HEAD if $HEAD;
730 print PPP "</HEAD>\n";
731 print PPP "<BODY BGCOLOR=\"#FFFFFF\">\n";
733 print PPP "<H2>the status of $TITL mirrors</H2>\n";
734 print PPP "<TABLE BORDER=0 CELLPADDING=2>\n";
735 printf PPP "<TR><TD>date</TD><TD>:</TD><TD>%s (GMT)</TD></TR>\n",
737 printf PPP "<TR><TD>last check</TD><TD>:</TD><TD>%s (GMT)</TD></TR>\n",
738 scalar gmtime( $opt{get} ? $^T : ( stat $CNF{state} )[9] );
739 print PPP "</TABLE>\n";
740 printf PPP "<P>%s</P>\n", $HTOP if $HTOP;
741 if ( $CNF{put_histo} eq 'top' ) {
742 print PPP "<H2>age histogram</H2>\n";
743 print PPP "<BLOCKQUOTE>\n";
744 print PPP gen_histogram;
745 print PPP "</BLOCKQUOTE>\n";
747 print PPP "<H2>regions</H2>\n";
748 print PPP "<BLOCKQUOTE>\n";
749 print PPP "<CENTER>\n";
750 printf PPP "%s\n", $refs;
751 print PPP "</CENTER>\n";
752 print PPP "</BLOCKQUOTE>\n";
753 print PPP "<H2>report</H2>\n";
754 my $attr1 = "COLSPAN=$COLS BGCOLOR=\"LIME\"";
755 my $attr2 = 'BGCOLOR="AQUA"';
756 print PPP "<BLOCKQUOTE>\n";
757 print PPP "<TABLE BORDER=2 CELLPADDING=5>\n";
758 printf PPP "<TR><TH $attr1>%d sites in %d regions</TH></TR>\n",
759 scalar keys %LST, scalar keys %tab;
760 printf PPP "<TR><TH $attr1>%s</TH></TR>\n", $STAT;
761 printf PPP "<TR><TH $attr1>%s</TH></TR>\n", $PROB;
763 printf PPP " <TH $attr2>%s site -- home</TH>\n", $CNF{project_name};
764 printf PPP " <TH $attr2>%s</TH>\n", 'type';
765 printf PPP " <TH $attr2>%s</TH>\n", 'mirror age,<BR>daily stats';
766 printf PPP " <TH $attr2>%s</TH>\n", 'last probe,<BR>probe stats';
767 printf PPP " <TH $attr2>%s</TH>\n", 'last stat';
769 for my $reg ( sort by_CCS keys %tab ) {
770 my $itms = $tab{$reg};
771 my $ccs = exists $CCS{$reg} ? $CCS{$reg} : $reg;
775 ? sprintf "%s - %d sites",
780 my $attr3 = "COLSPAN=$COLS BGCOLOR=\"YELLOW\"";
781 printf PPP "<TR><TH $attr3>$ccs</TH></TR>\n";
782 for my $itm ( sort by_type_site @{$itms} ) {
783 my ( $type, $url, $site ) = @{$itm};
784 my ( $time, $stat, $hstp, $hsts, $vrfy );
785 my ( $pr_time, $pr_last, $pr_hstp, $pr_hsts );
787 printf PPP " <TD ALIGN=\"RIGHT\">%s %s</TD>\n"
788 . " <TD>%s</TD>\n", url( $url, $site ), url( home($url), '@' ),
790 if ( exists $RES{$url} ) {
791 ( $time, $stat, $vrfy, $hstp, $hsts ) = @{ $RES{$url} };
792 $pr_time = $time =~ /^\d+$/
793 ? diff $time, $^T - max_age2
795 $pr_last = $vrfy =~ /^\d+$/
796 ? diff $vrfy, $^T - max_vrfy
798 $pr_hstp = show_hist $hstp ;
799 $pr_hsts = show_hist_age $hsts, $time;
802 ( $pr_time, $pr_last, $pr_hstp, $pr_hsts, $stat ) =
803 ( ' ', ' ', '', '', ' ' );
805 $stat = RED $stat if $stat ne 'ok';
806 printf PPP " <TD ALIGN=\"RIGHT\">%s<BR>%s</TD>\n", $pr_time,
808 printf PPP " <TD ALIGN=\"RIGHT\">%s<BR>%s</TD>\n", $pr_last,
810 printf PPP " <TD>%s</TD>\n", $stat;
814 print PPP "</TABLE>\n";
815 print PPP "</BLOCKQUOTE>\n";
816 if ( $CNF{put_histo} eq 'bottom' ) {
817 print PPP "<H2>age histogram</H2>\n";
818 print PPP "<BLOCKQUOTE>\n";
819 print PPP gen_histogram;
820 print PPP "</BLOCKQUOTE>\n";
823 print PPP "<H3>probe results</H3>\n";
824 print PPP gen_histogram_probes;
825 print PPP "<H3>software</H3>\n";
826 print PPP "<BLOCKQUOTE><TABLE><TR>\n";
827 my $MIR_IMG = sprintf '<IMG BORDER=2 ALT="mirmon" SRC="%s/mirmon.gif">',
829 print PPP sprintf "<TH><A HREF=\"%s\">%s</A></TH>\n",
830 'http://www.cs.uu.nl/people/henkp/mirmon/', $MIR_IMG;
831 print PPP "<TD>$VER</TD>\n";
832 print PPP "</TR></TABLE></BLOCKQUOTE>\n";
834 print PPP "</BODY>\n";
836 if ( print PPP "\n" ) {
839 Warn "wrote empty html file; keeping previous version";
841 else { rename $TMP, $PPP or Error "can't rename $TMP, $PPP ($!)"; }
843 else { Error "can't print to $TMP ($!)"; }
848 <H4><I>project</I> site -- home</H4>
850 <B><I>project</I> site</B> is an url.
851 The <B>href</B> is the href for the site in the list of mirrors,
852 usually the root of the mirrored file tree.
853 The <B>text</B> is the <I>site</I> of that url.
855 <B>home</B> (represented by the <B>@</B>-symbol) is an url
856 pointing to the document root of the site. This pointer is
857 useful if the <B><I>project</I> site</B> url is invalid,
858 possibly because the mirror site moved the archive.
862 Indicates the type (<B>ftp</B> or <B>http</B>) of
863 the <B><I>project</I> site</B> and <B>home</B> urls.
865 <H4>mirror age, daily stats</H4>
867 The <B>mirror age</B> is based upon the last successful probe.
869 Once a day the status of a mirror site is determined.
870 The status (represented by a colored block) is appended
871 to the <B>right</B> of the status history (<I>right</I>
872 is <I>recent</I>). More precise, the status block is appended
873 if the last status block was appended 24 (or more) hours ago.
874 <P>The status of a mirror depends on its age and a few
875 configuration parameters :
877 <TABLE BORDER=1 CELLPADDING=5>
879 <TH ROWSPAN=3>status</TH>
880 <TH COLSPAN=4>age</TH>
883 <TH COLSPAN=2 BGCOLOR="YELLOW">this project</TH>
884 <TH COLSPAN=2 BGCOLOR="AQUA">in general</TH>
887 <TH BGCOLOR="YELLOW">min</TH>
888 <TH BGCOLOR="YELLOW">max</TH>
889 <TH BGCOLOR="AQUA">min</TH>
890 <TH BGCOLOR="AQUA">max</TH>
893 <TH><FONT COLOR="GREEN">fresh</FONT></TH>
894 <TD BGCOLOR="YELLOW" ALIGN="CENTER">0</TD>
895 <TD BGCOLOR="YELLOW" ALIGN="CENTER">
896 @{[$CNF{min_sync}]} + @{[$CNF{max_poll}]}</TD>
897 <TD BGCOLOR="AQUA" ALIGN="CENTER">0</TD>
898 <TD BGCOLOR="AQUA" ALIGN="CENTER">min_sync + max_poll</TD>
901 <TH><FONT COLOR="BLUE">oldish</FONT></TH>
902 <TD BGCOLOR="YELLOW" ALIGN="CENTER">
903 @{[$CNF{min_sync}]} + @{[$CNF{max_poll}]}</TD>
904 <TD BGCOLOR="YELLOW" ALIGN="CENTER">
905 @{[$CNF{max_sync}]} + @{[$CNF{max_poll}]}</TD>
906 <TD BGCOLOR="AQUA" ALIGN="CENTER">min_sync + max_poll</TD>
907 <TD BGCOLOR="AQUA" ALIGN="CENTER">max_sync + max_poll</TD>
910 <TH><FONT COLOR="RED">old</FONT></TH>
911 <TD BGCOLOR="YELLOW" ALIGN="CENTER">
912 @{[$CNF{max_sync}]} + @{[$CNF{max_poll}]}</TD>
913 <TD BGCOLOR="YELLOW" ALIGN="CENTER">∞</TD>
914 <TD BGCOLOR="AQUA" ALIGN="CENTER">max_sync + max_poll</TD>
915 <TD BGCOLOR="AQUA" ALIGN="CENTER">∞</TD>
918 <TH><FONT COLOR="BLACK">bad</FONT></TH>
919 <TH COLSPAN=4 BGCOLOR="BLACK">
920 <FONT COLOR="WHITE">the site or mirror tree was never found</FONT></TH>
925 <H4>last probe, probe stats</H4>
927 <B>Last probe</B> indicates when the last successful probe was made.
928 <B>Probe stats</B> gives the probe history (<I>right</I> is <I>recent</I>).
930 <FONT COLOR="GREEN"><B>success</B></FONT> or a
931 <FONT COLOR="RED"><B>failure</B></FONT>.
935 <B>Last stat</B> gives the status of the last probe.
942 my $TIMEOUT = $CNF{timeout};
943 my $src = $HREF{ lc site $url } || $url;
944 $CMD =~ s/%TIMEOUT%/$TIMEOUT/g;
945 $CMD =~ s/%URL%/$src/g;
946 printf "*** SUBSTITUTE site %s\n+ url %s\n+ %s\n", site($url),
947 $HREF{ lc site $url }, $CMD
948 if $HREF{ lc site $url } and $opt{v};
949 my $WGT = new IO::Pipe;
950 my $res = $WGT->reader( split ' ', $CMD );
956 else { err $url, 'no pipe'; }
960 my $url = $URL{$WGT};
963 unless ( $WGT->eof() ) { $time = $WGT->getline(); }
967 return err $url, 'no time' unless defined $time;
968 return err $url, "empty" if $time =~ /^\s*$/;
969 $time = ( split ' ', $time )[0];
970 if ( $time !~ /^\d+$/ ) {
971 $time = htmlquote $time ;
972 $time = substr( $time, 0, 15 ) . '..' if length $time > 15;
975 else { res $url, $time, 'ok'; }
980 my $PAR = $CNF{max_probes};
981 my $cnt_LST = scalar keys %LST;
982 for my $url ( sort keys %LST ) {
983 if ( $opt{get} eq 'all' or !exists $OLD{$url} ) { push @QUE, $url; }
984 elsif ( $opt{get} eq 'update' ) {
985 my $stat = $OLD{$url}[1];
986 my $vrfy = $OLD{$url}[2];
987 my $lprb = $OLD{$url}[5];
992 $^T - tim_to_s $CNF {min_poll}
995 $stat ne 'ok' or aprx_le $vrfy,
996 $^T - tim_to_s $CNF {max_poll}
1002 elsif ( $CNF{randomize} and 0 == int rand $cnt_LST ) {
1005 else { $RES{$url} = $OLD{$url}; }
1007 else { Error "unknown opt_get '$opt{get}'"; }
1010 while ( $GET->count() < $PAR and @QUE ) {
1011 my $url = shift @QUE;
1012 if ( gethost site $url ) { start_date $url, $CMD; }
1013 else { err $url, 'site not found'; }
1015 my @can_read = $GET->can_read(0);
1016 printf "que %d, get %d, can %d\n", scalar @QUE, $GET->count(),
1019 for my $can_read (@can_read) { get_date $can_read ; }
1022 my $stop = time + $CNF{timeout} + 10;
1023 while ( $GET->count() and time < $stop ) {
1025 my @can_read = $GET->can_read(0);
1026 printf "wait %2d, get %d, can %d\n", $stop - scalar time, $GET->count(),
1029 for my $can_read (@can_read) { get_date $can_read ; }
1031 for my $WGT ( $GET->handles() ) {
1032 my $url = $URL{$WGT};
1037 get_ccs $CNF {countries};
1038 get_state $CNF {state};
1039 get_list $CNF {mirror_list};
1041 get_dates $CNF {probe};
1042 put_state $CNF {state};
1044 else { %RES = %OLD }
1045 gen_page $CNF {web_page};
1049 mirmon - monitor the state of mirrors
1051 mirmon [ -v ] [ -q ] [ -t timeout ] [ -get opt ] [ -c conf ]
1053 option v : be verbose
1055 option t : set timeout [ default 300 ] ;
1056 option get : 'all' : probe all sites
1057 : 'update' : probe a selection of the sites (see doc)
1058 option c : configuration file ; default list :
1059 ./mirmon.conf $HOME/.mirmon.conf /etc/mirmon.conf
1060 -------------------------------------------------------------------
1061 Documentation : the program contains 'pod' style documentation.
1062 Extract the doc with 'pod2text mirmon' or 'pod2html mirmon OUT', etc.
1063 -------------------------------------------------------------------
1065 The program is intended to be run by cron every hour.
1066 42 * * * * perl /path/to/mirmon -q -get update
1067 It quietly probes a subset of the sites in a given list,
1068 writes the results in the 'state' file and generates a web page
1069 with the results. The subset contains the sites that are new, bad
1070 and/or not probed for a specified time.
1071 When no 'get' option is specified, the program just generates a
1072 new web page from the last known state.
1073 The program checks the mirrors by running a (user specified)
1074 program on a pipe. A (user specified) number of probes is
1075 run in parallel using nonblocking IO. When something can be
1076 read from the pipe, it switches the pipe to blocking IO and
1077 reads one line from the pipe. Then it flushes and closes the
1078 pipe. No attempt is made to kill the probe.
1079 The probe should return something that looks like "1043625600\n",
1080 that is, a timestamp followed by a newline. The exit status of
1081 the probe is ignored.
1084 A config file can be specified with the -c option.
1085 If -c is not used, the program looks for a config file in
1087 -- $HOME/.mirmon.conf
1090 A config file looks like this :
1091 +--------------------------------------------------
1092 |# lines that start with '#' are comment
1093 |# blank lines are ignored too
1094 |# tabs are replaced by a space
1096 |# the config entries are 'key' and 'value' pairs
1097 |# a 'key' begins in column 1
1098 |# the 'value' is the rest of the line
1099 |somekey A_val B_val ...
1100 |otherkey X_val Y_val ...
1102 |# indented lines are glued
1103 |# the next three lines mean 'somekey part1 part2 part3'
1108 |# lines starting with a '+' are concatenated
1109 |# the next three lines mean 'somekey part1part2part3'
1114 |# lines starting with a '.' are glued too
1115 |# don't use a '.' on a line by itself
1116 |# 'somekey' gets the value "part1\n part2\n part3"
1120 +--------------------------------------------------
1121 =head1 CONFIG FILE : required entries
1122 =head2 project_name <name>
1123 Specify a short plaintext name for the project.
1126 =head2 project_url <url>
1127 Specify an url pointing to the 'home' of the project.
1128 project_url http://www.apache.org/
1129 =head2 mirror_list <file name>
1130 Specify the file containing the mirrors to probe.
1131 Two formats are supported :
1132 -- plain : lines like
1133 us http://www.tux.org/
1134 nl http://apache.cs.uu.nl/dist/
1135 -- apache : lines like those in the apache mirrors.list
1136 ftp us ftp://ftp.tux.org/pub/net/apache/dist/ user@tux.org
1137 http nl http://apache.cs.uu.nl/dist/ user@cs.uu.nl
1138 Specify the required format with 'list_style' (see below).
1139 The default style is 'plain'.
1140 If the url part of a line doesn't end in a slash ('/'), mirmon
1141 adds a slash and issues a warning unless it is in quiet mode.
1142 =head2 web_page <file name>
1143 Specify where the html report page is written.
1144 =head2 icons <directory name>
1145 Specify the directory where the icons can be found.
1146 =head2 probe <program + arguments>
1147 Specify the program+args to probe the mirrors. Example:
1148 probe /sw/bin/wget -q -O - -T %TIMEOUT% -t 1 %URL%TIME
1149 Before the program is started, %TIMEOUT% and %URL% are
1150 substituted with the proper timeout and url values.
1151 Here it is assumed that each hour the root server writes
1152 a timestamp in /path/to/archive/TIME, for instance with
1153 a crontab entry like
1154 42 * * * * perl -e 'printf "%s\n", time' > /path/to/archive/TIME
1155 Mirmon reads one line of output from the probe and interprets
1156 the first word on that line as a timestamp ; for example :
1158 1043625600 Mon Jan 27 00:00:00 2003
1159 1043625600 www.apache.org Mon Jan 27 00:00:00 2003
1160 =head2 state <file name>
1161 Specify where the file containing the state is written.
1162 The program reads this file on startup and writes the
1163 file when mirrors are probed (-get is specified).
1164 =head2 countries <file name>
1165 Specify the file containing the country codes;
1166 The file should contain lines like
1169 The mirmon package contains a recent ISO list.
1170 =head1 CONFIG FILE : optional entries
1171 =head2 max_probes <number>
1172 Optionally specify the number of parallel probes (default 25).
1173 =head2 timeout <seconds>
1174 Optionally specify the timeout for the probes (default 300).
1175 After the last probe is started, the program waits for
1176 <timeout> + 10 seconds, cleans up and exits.
1177 =head2 project_logo <logo>
1178 Optionally specify (the SRC of the IMG of) a logo to be placed
1179 top right on the page.
1180 project_logo /icons/apache.gif
1181 project_logo http://www.apache.org/icons/...
1182 =head2 htm_head <html>
1183 Optionally specify some HTML to be placed before </HEAD>.
1185 <link REL=StyleSheet HREF="/style.css" TYPE="text/css">
1186 =head2 htm_top <html>
1187 Optionally specify some HTML to be placed near the top of the page.
1188 The supplied text is placed between <P> and </P>.
1189 htm_top testing 1, 2, 3
1190 =head2 htm_foot <html>
1191 Optionally specify HTML to be placed near the bottom of the page.
1194 <A HREF="..."><IMG SRC="..." BORDER=0></A>
1196 =head2 put_histo top|bottom|nowhere
1197 Optionally specify where the age histogram must be placed.
1198 The default is 'top'.
1199 =head2 min_poll <time spec>
1200 For 'min_poll' see next item. A <time spec> is a number followed by
1201 a unit 's' (seconds), or 'm' (minutes), or 'h' (hours), or 'd' (days).
1202 For example '3d' (three days) or '36h' (36 hours).
1203 =head2 max_poll <time spec>
1204 Optionally specify the maximum probe interval. When the program is
1205 called with option '-get update', all sites are probed which are :
1206 -- new : the site appears in the list, but there is no known state
1207 -- bad : the last probe of the site was unsuccessful
1208 -- old : the last probe was more than 'max_poll' ago.
1209 Sites are not probed if the last probe was less than 'min_poll' ago.
1213 the 'reachable' sites are probed twice daily and the 'unreachable'
1214 sites are probed at most six times a day.
1215 The default 'min_poll' is '1h' (1 hour).
1216 The default 'max_poll' is '4h' (4 hours).
1217 =head2 min_sync <time spec>
1218 Optionally specify how often the mirrors are required to
1219 make an update. The default 'min_sync' is '1d' (1 day).
1220 =head2 max_sync <time spec>
1221 Optionally specify the maximum allowable sync interval.
1222 Sites exceeding the limit will be considered 'old'.
1223 The default 'max_sync' is '2d' (2 days).
1225 With a low probablility, mirmon probes mirrors that would
1226 otherwise not be probed. In the long run, this balances
1227 the number of mirror probes over the hourly mirmon runs.
1228 Specifically, if there are N mirrors in the list and some
1229 mirmon run would probe K sites, on average (N-K)/N extra
1230 sites will be probed.
1231 If you don't want this behaviour, use 'no_randomize'.
1232 =head2 list_style plain|apache
1233 Optionally specify the format ('plain' or 'apache') of the
1234 mirror-list. See the description of 'mirror_list' above.
1235 The default list_style is 'plain'.
1236 =head2 site_url <site> <url>
1237 Optionally specify a substitute url for a site. When access to
1238 a site is restricted (in Australia, for instance), another
1239 (sometimes secret) url can be used to probe the site. The <site>
1240 of an url is the part between '://' and the first '/'.
1241 =head2 env <key> <value>
1242 Optionally specify an environment variable.
1243 =head2 include <file name>
1244 Optionally specify a file to include. The specified file is processed
1245 'in situ'. After the specified file is read and processed, config
1246 processing is resumed in the file where the 'include' was encountered.
1247 The 'include' depth is unlimited. However, it is a fatal error to
1248 include a file twice under the same name.
1250 When the config processor encounters the 'show' command, it
1251 dumps the content of the current config to standout, if option
1252 -v is specified. This is intented for debugging.
1254 When the config processor encounters the 'exit' command, it
1255 terminates the program. This is intented for debugging.
1256 =head1 STATE FILE FORMAT
1257 The state file consists of lines; one line per site.
1258 Each line consists of white space separated fields.
1259 The seven fields are :
1260 =head2 field 1 : url
1261 The url as given in the mirror list.
1262 =head2 field 2 : age
1263 The age of the site, or 'undef' if no probe was ever successful.
1264 =head2 field 3 : status last probe
1265 The status of the last probe.
1266 =head2 field 4 : time last succesful probe
1267 The timestamp of the last succesful probe or 'undef'
1268 if the site was never successfully probed.
1269 =head2 field 5 : probe history
1270 The probe history is a list of 's' (for success) and 'f' (for failure)
1271 characters indicating the result of the probe. New results are appended
1272 whenever the site is probed.
1273 =head2 field 6 : state history
1274 The state history consists of a timestamp, a '-' char, and a list of
1275 chars indicating a past status: 's' (fresh), 'b' (oldish), 'f' (old)
1276 or 'z' (bad). The timestamp indicates when the state history was last
1277 updated. The state history is updated when the state file is updated
1278 and the last update of the history state was 24 (or more) hours ago.
1279 The status is determined by the site's age and a few configuration
1280 parameters. The details are explained in the legend of the report page.
1281 =head2 field 7 : last probe
1282 The timestamp of the last probe.
1286 The '#!' path for perl is probably wrong.
1292 <A HREF="http://www.cs.uu.nl/staff/henkp.html">Henk P. Penning</A>,
1293 <A HREF="http://www.cs.uu.nl/">Computer Science Department</A>,
1294 <A HREF="http://www.uu.nl/">Utrecht University</A>
1296 $Id: mirmon,v 1.37 2006/12/04 15:16:11 henkp Exp henkp $
1300 (c) 2003 Henk P. Penning, Computer Science Department, Utrecht University
1301 http://www.cs.uu.nl/staff/henkp.html -- penning@cs.uu.nl