ACC SHELL
# -*- Mode: cperl; coding: utf-8; cperl-indent-level: 4 -*-
# vim: ts=4 sts=4 sw=4:
package CPAN::Mirrors;
use strict;
use vars qw($VERSION $urllist $silent);
$VERSION = "1.77";
use Carp;
use FileHandle;
use Fcntl ":flock";
sub new {
my ($class, $file) = @_;
my $self = bless {
mirrors => [],
geography => {},
}, $class;
my $handle = FileHandle->new;
$handle->open($file)
or croak "Couldn't open $file: $!";
flock $handle, LOCK_SH;
$self->_parse($file,$handle);
flock $handle, LOCK_UN;
$handle->close;
# populate continents & countries
return $self
}
sub continents {
my ($self) = @_;
return keys %{$self->{geography}};
}
sub countries {
my ($self, @continents) = @_;
@continents = $self->continents unless @continents;
my @countries;
for my $c (@continents) {
push @countries, keys %{ $self->{geography}{$c} };
}
return @countries;
}
sub mirrors {
my ($self, @countries) = @_;
return @{$self->{mirrors}} unless @countries;
my %wanted = map { $_ => 1 } @countries;
my @found;
for my $m (@{$self->{mirrors}}) {
push @found, $m if exists $wanted{$m->country};
}
return @found;
}
sub best_mirrors {
my ($self, %args) = @_;
my $how_many = $args{how_many} || 1;
my $callback = $args{callback};
my $verbose = $args{verbose};
my $conts = $args{continents} || [];
$conts = [$conts] unless ref $conts;
my $seen = {};
if ( ! @$conts ) {
print "Searching for the best continent ...\n" if $verbose;
my @best = $self->_find_best_continent($seen, $verbose, $callback);
# how many continents to find enough mirrors? We should scan
# more than we need -- arbitrarily, we'll say x2
my $count = 0;
for my $c ( @best ) {
push @$conts, $c;
$count += $self->mirrors( $self->countries($c) );
last if $count >= 2 * $how_many;
}
}
print "Scanning " . join(", ", @$conts) . " ...\n" if $verbose;
my @timings;
for my $m ($self->mirrors($self->countries(@$conts))) {
next unless $m->ftp;
my $hostname = $m->hostname;
if ( $seen->{$hostname} ) {
push @timings, $seen->{$hostname}
if defined $seen->{$hostname}[1];
}
else {
my $ping = $m->ping;
next unless defined $ping;
push @timings, [$m, $ping];
$callback->($m,$ping) if $callback;
}
}
return unless @timings;
$how_many = @timings if $how_many > @timings;
my @best =
map { $_->[0] }
sort { $a->[1] <=> $b->[1] } @timings;
return wantarray ? @best[0 .. $how_many-1] : $best[0];
}
sub _find_best_continent {
my ($self, $seen, $verbose, $callback) = @_;
my %median;
CONT: for my $c ( $self->continents ) {
my @mirrors = $self->mirrors( $self->countries($c) );
next CONT unless @mirrors;
my $sample = 9;
my $n = (@mirrors < $sample) ? @mirrors : $sample;
my @tests;
RANDOM: while ( @mirrors && @tests < $n ) {
my $m = splice( @mirrors, int(rand(@mirrors)), 1 );
my $ping = $m->ping;
$callback->($m,$ping) if $callback;
# record undef so we don't try again
$seen->{$m->hostname} = [$m, $ping];
next RANDOM unless defined $ping;
push @tests, $ping;
}
next CONT unless @tests;
@tests = sort { $a <=> $b } @tests;
if ( @tests == 1 ) {
$median{$c} = $tests[0];
}
elsif ( @tests % 2 ) {
$median{$c} = $tests[ int(@tests / 2) ];
}
else {
my $mid_high = int(@tests/2);
$median{$c} = ($tests[$mid_high-1] + $tests[$mid_high])/2;
}
}
my @best_cont = sort { $median{$a} <=> $median{$b} } keys %median ;
if ( $verbose ) {
print "Median result by continent:\n";
for my $c ( @best_cont ) {
printf( " %d ms %s\n", int($median{$c}*1000+.5), $c );
}
}
return wantarray ? @best_cont : $best_cont[0];
}
# Adapted from Parse::CPAN::MirroredBy by Adam Kennedy
sub _parse {
my ($self, $file, $handle) = @_;
my $output = $self->{mirrors};
my $geo = $self->{geography};
local $/ = "\012";
my $line = 0;
my $mirror = undef;
while ( 1 ) {
# Next line
my $string = <$handle>;
last if ! defined $string;
$line = $line + 1;
# Remove the useless lines
chomp( $string );
next if $string =~ /^\s*$/;
next if $string =~ /^\s*#/;
# Hostname or property?
if ( $string =~ /^\s/ ) {
# Property
unless ( $string =~ /^\s+(\w+)\s+=\s+\"(.*)\"$/ ) {
croak("Invalid property on line $line");
}
my ($prop, $value) = ($1,$2);
$mirror ||= {};
if ( $prop eq 'dst_location' ) {
my (@location,$continent,$country);
@location = (split /\s*,\s*/, $value)
and ($continent, $country) = @location[-1,-2];
$continent =~ s/\s\(.*//;
$continent =~ s/\W+$//; # if Jarkko doesn't know latitude/longitude
$geo->{$continent}{$country} = 1 if $continent && $country;
$mirror->{continent} = $continent || "unknown";
$mirror->{country} = $country || "unknown";
}
elsif ( $prop eq 'dst_http' ) {
$mirror->{http} = $value;
}
elsif ( $prop eq 'dst_ftp' ) {
$mirror->{ftp} = $value;
}
elsif ( $prop eq 'dst_rsync' ) {
$mirror->{rsync} = $value;
}
else {
$prop =~ s/^dst_//;
$mirror->{$prop} = $value;
}
} else {
# Hostname
unless ( $string =~ /^([\w\.-]+)\:\s*$/ ) {
croak("Invalid host name on line $line");
}
my $current = $mirror;
$mirror = { hostname => "$1" };
if ( $current ) {
push @$output, CPAN::Mirrored::By->new($current);
}
}
}
if ( $mirror ) {
push @$output, CPAN::Mirrored::By->new($mirror);
}
return;
}
#--------------------------------------------------------------------------#
package CPAN::Mirrored::By;
use strict;
use Net::Ping ();
sub new {
my($self,$arg) = @_;
$arg ||= {};
bless $arg, $self;
}
sub hostname { shift->{hostname} }
sub continent { shift->{continent} }
sub country { shift->{country} }
sub http { shift->{http} || '' }
sub ftp { shift->{ftp} || '' }
sub rsync { shift->{rsync} || '' }
sub url {
my $self = shift;
return $self->{ftp} || $self->{http};
}
sub ping {
my $self = shift;
my $ping = Net::Ping->new("tcp",1);
my ($proto) = $self->url =~ m{^([^:]+)};
my $port = $proto eq 'http' ? 80 : 21;
return unless $port;
$ping->port_number($port);
$ping->hires(1);
my ($alive,$rtt) = $ping->ping($self->hostname);
return $alive ? $rtt : undef;
}
1;
ACC SHELL 2018