imapsync/W/learn/proximapsync
Nick Bebout 9a927be251 1.882
2018-05-07 09:04:23 -05:00

1356 lines
45 KiB
Perl
Executable file

#!/usr/bin/perl -T
use strict ;
use warnings ;
use Getopt::Long ( ) ;
use LWP::UserAgent ( ) ;
use English qw( -no_match_vars ) ;
use Readonly ;
use Data::Dumper ;
use Test::More ;
use Digest::HMAC_SHA1 qw( hmac_sha1_hex ) ;
use Sys::Hostname ;
use Sys::MemInfo ;
use IPC::Open3 ( ) ;
Readonly my $RCS = q{$Id: proximapsync,v 1.9 2018/05/06 12:16:00 gilles Exp gilles $} ;
Readonly our $VERSION = version_from_rcs( $RCS ) ;
Readonly my $EX_OK => 0 ; #/* successful termination */
Readonly my $EX_USAGE => 64 ; #/* command line usage error */
Readonly my $KIBI => 1024 ;
Readonly my $EXIT_TESTS_FAILED => 254 ; # Like Test::More API
Readonly my $nb_attempts_max => 5 ;
main( @ARGV ) ;
sub main {
my @options = @ARG ;
my $sync = init( @options ) ;
#
cgibuildheader( $sync ) ;
myprint( output( $sync ) ) ;
output_reset_with( $sync ) ;
# Header is printed so now we can use myprint
presentation( $sync ) ;
# Unit tests if asked, then exit.
testsexit( $sync ) ;
# The real stuff
loop_until_done_or_abort( $sync ) ;
myprint( debugmemory( $sync, ' after' ) ) ;
return ;
}
sub loop_until_done_or_abort{
my $mysync = shift ;
$mysync->{ nb_attempts_max } = $nb_attempts_max ;
$mysync->{ nb_attempts } = 0 ;
while ( can_and_have_to_sync( $mysync ) ) {
# ideas:
# * Warn if http instead of https
my $url = choose_remote( $mysync ) ;
myprint( "\nGonna delegate the imap sync to $url\n" ) ;
my $response = proximapsync( $mysync, $url ) ;
if ( ! $response->is_success ) {
myprint( "Failure with $url\n" ) ;
my $removed = remove_remote( $mysync ) ;
myprint( "Removed $removed from list of proxies\n" ) ;
#$mysync->{ sync_done } = 1 ;
}else{
myprint( "Success with $url\n" ) ;
$mysync->{ sync_done } = 1 ;
}
}
myprint( "No more things to do\n" ) ;
return ;
}
sub tests_can_and_have_to_sync {
note( 'Entering tests_can_and_have_to_sync()' ) ;
is( undef, can_and_have_to_sync( ), 'can_and_have_to_sync: no args => undef' ) ;
my $mysync ;
is( undef, can_and_have_to_sync( $mysync ), 'can_and_have_to_sync: undef => undef' ) ;
$mysync = { } ;
is( undef, can_and_have_to_sync( $mysync ), 'can_and_have_to_sync: undef => undef' ) ;
$mysync->{ nb_attempts_max } = 1 ;
$mysync->{ nb_attempts } = 1 ;
$mysync->{ remote } = [ ] ;
is( 0, can_and_have_to_sync( $mysync ), 'can_and_have_to_sync: nb_attempts_max reached => 0' ) ;
note( 'Leaving tests_can_and_have_to_sync()' ) ;
return ;
}
sub can_and_have_to_sync {
my $mysync = shift ;
if ( ! defined( $mysync ) ) { return ; }
if ( ! all_defined(
$mysync->{ nb_attempts },
$mysync->{ nb_attempts_max },
$mysync->{ remote },
)
) {
myprint( "One or more parameters are missing: nb_attempts nb_attempts_max remote\n" ) ;
return ;
}
if ( $mysync->{ nb_attempts } >= $mysync->{ nb_attempts_max } ) {
# stop
myprint( "Max attempts " . $mysync->{ nb_attempts } . " reached!\n" ) ;
return 0 ;
}
my $nb_proxies = scalar( @{ $mysync->{ remote } } ) ;
if ( $nb_proxies <= 0 ) {
myprint( "No more remote to delegate the sync to!\n" ) ;
return ;
}
# It will be more elaborate later
if ( $mysync->{ sync_done } ) {
# stop
return 0 ;
}else{
# try again
return 1 ;
}
}
# This filter_param sub is here just during the development process
# in order to find and modify it along the way
sub filter_param {
my $mysync = shift ;
# copy the $mysync elements needed for the remote
my $remote = {} ;
for my $key ( qw(
host1 user1 password1
host2 user2 password2
abort
testslive tests
simulong
)
) {
if ( exists( $mysync->{ $key } )
and defined( $mysync->{ $key } ) ) {
$remote->{ $key } = $mysync->{ $key } ;
}
}
return $remote ;
}
sub presentation {
my $mysync = shift ;
my( $infos ) = join( q{},
"Here is proximapsync ", software_version( $mysync ),
" on host ", hostname(),
", a $OSNAME system with ",
ram_memory_info( ),
"\n",
) ;
myprint( $infos ) ;
myprint( debugmemory( $mysync, ' before' ) ) ;
myprint( "PID is $PROCESS_ID\n" ) ;
myprint( "cookie_jar file: " . $mysync->{ cookie_jar } . "\n" ) ;
#my $uri = new URI ;
#$uri->query_form( [ qw( remote https://lamiral.info/cgi-bin/imapsync ) ] ) ;
#myprint( "uri: $uri\n" ) ;
return ;
}
sub init {
my @options = @ARG ;
my $mysync = { } ;
$mysync->{ timestart } = time ; # Is a float because of use Time::HiRres
$mysync->{ rcs } = $RCS ;
$mysync->{ memory_consumption_at_start } = memory_consumption( ) || 0 ;
my @loadavg = loadavg( ) ;
$mysync->{ cpu_number } = cpu_number( ) ;
$mysync->{ loaddelay } = load_and_delay( $mysync->{ cpu_number }, @loadavg ) ;
$mysync->{ loadavg } = join( q{ }, $loadavg[ 0 ] )
. " on $mysync->{cpu_number} cores and "
. ram_memory_info( ) ;
# Just create a CGI object if under cgi context only. Needed for get_options() call
cgibegin( $mysync ) ;
my $options_good = get_options( $mysync, @options ) ;
if ( ! defined $options_good ) { exit $EX_USAGE ; }
$mysync->{ debugmemory } = 1 ;
set_cookie_jar( $mysync ) ;
#set_default_remote( $mysync ) ;
#myprint( Data::Dumper->Dump( [ $mysync ] ) ) ;
return $mysync ;
}
sub set_default_remote {
my $mysync = shift ;
$mysync->{ remote } = ( $mysync->{ remote } )
? $mysync->{ remote }
: [ 'http://lamiral.info/cgi-bin/imapsync',
'http://lamiral.info/cgi-bin/imapsyncQQ',
'http://lamiral.info/cgi-bin/imapsyncXX',
'http://lamiral.info/cgi-bin/imapsyncKK',
] ;
return ;
}
sub set_cookie_jar {
my $mysync = shift ;
if ( -d -w "$ENV{HOME}/" ) {
$mysync->{ cookie_jar } = "$ENV{HOME}/.imapsync_cookies.txt" ;
return ;
}
if ( -d -w "/var/tmp/euid_$EFFECTIVE_USER_ID" ) {
$mysync->{ cookie_jar } = "/var/tmp/euid_$EFFECTIVE_USER_ID/imapsync_cookies.txt" ;
return ;
}
if ( -d -w '/var/tmp' ) {
mkdir "/var/tmp/euid_$EFFECTIVE_USER_ID" || return ;
$mysync->{ cookie_jar } = "/var/tmp/euid_$EFFECTIVE_USER_ID/imapsync_cookies.txt" ;
}
return ;
}
sub proximapsync {
my $mysync = shift ;
my $url = shift || return ;
my $response = post_to_url( $mysync, $url ) ;
$mysync->{ nb_attempts } += 1 ;
return $response ;
}
{
# This block is because of the lexical variable
# $status_printed that has to be initialized
# each time, even in mod_perl perl-registry context
# (the sub-function issue)
my $status_printed ;
sub post_to_url {
my $mysync = shift ;
my $url = shift ;
my $sync_remote = filter_param( $mysync ) ;
my $ua = LWP::UserAgent->new ;
push @{ $ua->requests_redirectable }, 'POST' ;
my $agent = 'proximapsync/' . software_version( $mysync ) ;
$ua->agent("$agent ") ; # append the default to the end
$ua->protocols_allowed( [ 'http', 'https'] ) ;
$ua->timeout( 10 ) ; # secondes
$ua->max_redirect( 7 ) ;
$ua->env_proxy; # Load proxy settings from *_proxy environment variables.
$ua->cookie_jar( { file => $mysync->{ cookie_jar }, autosave => 1, } ) ;
#myprint( Data::Dumper->Dump( [ $sync_remote ] ) ) ;
$status_printed = 0 ;
my $response = $ua->post( $url, ':content_cb' => \&post_callback, $sync_remote ) ;
myprint( "Remote request on $url is now finished using " . $response->redirects . " redirects by following this reverse history:\n" ) ;
myprint( response_results( $response ) ) ;
return $response ;
}
sub post_callback {
my ( $chunk, $response, $protocol ) = @_ ;
if ( ! $status_printed ) {
myprint( $response->status_line, " in post_callback\n" ) ;
$status_printed = 1 ;
}
#Do whatever you like with $chunk
myprint( $chunk ) ;
return ;
}
}
sub response_results {
my $response = shift ;
my $string = q{} ;
while ( $response ) {
$string .= $response->base . ' => ' . $response->status_line . "\n" ;
$response = $response->previous ;
}
return "$string" ;
}
sub cgibuildheader {
my $mysync = shift ;
if ( ! under_cgi_context( $mysync ) ) { return ; }
my $proximapsync_runs = $mysync->{cgi}->cookie( 'proximapsync_runs' ) || 0 ;
my $cookie = $mysync->{cgi}->cookie(
-name => 'proximapsync_runs',
-value => 1 + $proximapsync_runs,
-expires => '+20y',
-path => '/cgi-bin/proximapsync',
) ;
my $httpheader ;
if( $mysync->{ loaddelay } ) {
$httpheader = $mysync->{cgi}->header(
-type => 'text/plain',
-status => '503 Service Unavailable' . ". Be back in $mysync->{ loaddelay } min. Load on " . hostname() . " is $mysync->{ loadavg }",
) ;
}else{
$httpheader = $mysync->{cgi}->header(
-type => 'text/plain',
-status => '200 OK to proximapsync. ' . "Load on " . hostname() . " is $mysync->{ loadavg }",
-cookie => $cookie,
) ;
}
output_start( $mysync, $httpheader ) ;
return ;
}
sub tests_remote_from_credentials {
note( 'Entering tests_remote_from_credentials()' ) ;
is( undef, remote_from_credentials( ) , 'remote_from_credentials: no args => undef' ) ;
my $mysync = {} ;
is( undef, remote_from_credentials( $mysync ) , 'remote_from_credentials: undef => undef' ) ;
$mysync->{ remote } = [ 'http://def/xxx' ] ;
is( 'http://def/xxx', remote_from_credentials( $mysync ) , 'remote_from_credentials: http://def/xxx => http://def/xxx' ) ;
$mysync->{ remote } = [ 'http://abc/xxx' ] ;
is( 'http://abc/xxx', remote_from_credentials( $mysync ) , 'remote_from_credentials: http://abc/xxx => http://abc/xxx' ) ;
$mysync->{ remote } = [ 'http://abc/xxx', 'http://def/xxx' ] ;
is( 'http://def/xxx', remote_from_credentials( $mysync ) , 'remote_from_credentials: http://abc/xxx http://def/xxx => http://def/xxx' ) ;
note( 'Leaving tests_remote_from_credentials()' ) ;
return ;
}
sub remote_from_credentials {
my $mysync = shift ;
my $index = remote_index_from_credentials( $mysync ) ;
my $remote = $mysync->{ remote }->[ $index ] || undef ;
return( $remote ) ;
}
sub tests_remote_index_from_credentials {
note( 'Entering tests_remote_index_from_credentials()' ) ;
is( 0, remote_index_from_credentials( ) , 'remote_index_from_credentials: no args => 0' ) ;
my $mysync = {} ;
is( 0, remote_index_from_credentials( $mysync ) , 'remote_index_from_credentials: undef => 0' ) ;
$mysync->{ remote } = [ 'http://def/xxx' ] ;
is( 0, remote_index_from_credentials( $mysync ) , 'remote_index_from_credentials: http://def/xxx => http://def/xxx' ) ;
$mysync->{ remote } = [ 'http://abc/xxx' ] ;
is( 0, remote_index_from_credentials( $mysync ) , 'remote_index_from_credentials: http://abc/xxx => http://abc/xxx' ) ;
$mysync->{ remote } = [ 'http://abc/xxx', 'http://def/xxx' ] ;
is( 1, remote_index_from_credentials( $mysync ) , 'remote_index_from_credentials: http://abc/xxx http://def/xxx => http://def/xxx' ) ;
note( 'Leaving tests_remote_index_from_credentials()' ) ;
return ;
}
sub remote_index_from_credentials {
my $mysync = shift ;
my $num_proxies ;
if ( 'ARRAY' eq ref( $mysync->{ remote } ) ) {
$num_proxies = scalar( @{ $mysync->{ remote } } ) ;
}else{
$num_proxies = 0 ;
}
my $string = join( q{},
$mysync->{ host1 } || q{},
$mysync->{ user1 } || q{},
$mysync->{ password1 } || q{},
$mysync->{ host2 } || q{},
$mysync->{ user2 } || q{},
$mysync->{ password2 } || q{},
) ;
my $index = index_from_string_mod( $string, $num_proxies ) ;
#my $remote = $mysync->{ remote }->[ $index ] || undef ;
return( $index ) ;
}
sub tests_remote_remove_index {
note( 'Entering tests_remote_remove_index()' ) ;
my $mysync = {} ;
is_deeply( [ ], [ remote_remove_index( $mysync, 0) ], 'remote_remove_index: [ ] 0 => [ ]' ) ;
is_deeply( [ ], $mysync->{ remote }, 'remote_remove_index: undef 0 => remote = [ ]' ) ;
$mysync->{ remote } = [ ] ;
is_deeply( [ ], [ remote_remove_index( $mysync, 0) ], 'remote_remove_index: [ ] 0 => [ ]' ) ;
is_deeply( [ ], $mysync->{ remote }, 'remote_remove_index: [ ] 0 => remote = [ ]' ) ;
$mysync->{ remote } = [ 'ABC' ] ;
is_deeply( [ 'ABC' ], [ remote_remove_index( $mysync, 0) ], 'remote_remove_index: [ ABC ] 0 => [ ABC ]' ) ;
is_deeply( [ ], $mysync->{ remote }, 'remote_remove_index: [ ABC ] 0 => remote = [ ]' ) ;
$mysync->{ remote } = [ 'ABC' ] ;
is_deeply( [ ], [ remote_remove_index( $mysync, 1) ], 'remote_remove_index: [ ABC ] 1 => [ ]' ) ;
is_deeply( [ 'ABC' ], $mysync->{ remote }, 'remote_remove_index: [ ABC ] 1 => remote = [ ABC ]' ) ;
$mysync->{ remote } = [ 'ABC', 'DEF' ] ;
is_deeply( [ 'ABC' ], [ remote_remove_index( $mysync, 0) ], 'remote_remove_index: [ ABC DEF ] 0 => [ ABC ]' ) ;
is_deeply( [ 'DEF' ], $mysync->{ remote }, 'remote_remove_index: [ ABC DEF ] 0 => remote = [ DEF ]' ) ;
$mysync->{ remote } = [ 'ABC', 'DEF' ] ;
is_deeply( [ 'DEF' ], [ remote_remove_index( $mysync, 1) ], 'remote_remove_index: [ ABC DEF ] 0 => [ DEF ]' ) ;
is_deeply( [ 'ABC' ], $mysync->{ remote }, 'remote_remove_index: [ ABC DEF ] 0 => remote = [ ABC ]' ) ;
note( 'Leaving tests_remote_remove_index()' ) ;
return ;
}
sub remote_remove_index {
my $mysync = shift ;
my $index = shift ;
splice @{ $mysync->{ remote } }, $index, 1 ;
return ;
}
sub tests_index_from_string_mod {
note( 'Entering tests_index_from_string_mod()' ) ;
is( 0, index_from_string_mod( ), 'index_from_string_mod: no args => 0' ) ;
is( 0, index_from_string_mod( 'ABC' ), 'index_from_string_mod: ABC but no modulo (=> modulo 1) => 0' ) ;
is( 0, index_from_string_mod( 'DEF' ), 'index_from_string_mod: DEF but no modulo (=> modulo 1) => 0' ) ;
is( 0, index_from_string_mod( 'ABC', 1 ), 'index_from_string_mod: ABC modulo 1 => 0' ) ;
is( 0, index_from_string_mod( 'DEF', 1 ), 'index_from_string_mod: DEF modulo 1 => 0' ) ;
is( 0, index_from_string_mod( 'ABC', 2 ), 'index_from_string_mod: ABC modulo 2 => 0' ) ;
is( 1, index_from_string_mod( 'DEF', 2 ), 'index_from_string_mod: DEF modulo 2 => 1' ) ;
is( 2, index_from_string_mod( 'ABC', 3 ), 'index_from_string_mod: ABC modulo 3 => 2' ) ;
is( 0, index_from_string_mod( 'ABC', 4 ), 'index_from_string_mod: ABC modulo 4 => 0' ) ;
is( 2, index_from_string_mod( 'ABC', 5 ), 'index_from_string_mod: ABC modulo 5 => 2' ) ;
is( 2, index_from_string_mod( 'ABC', 6 ), 'index_from_string_mod: ABC modulo 6 => 2' ) ;
is( 3, index_from_string_mod( 'ABC', 7 ), 'index_from_string_mod: ABC modulo 7 => 3' ) ;
my $str = q{} ;
foreach my $int ( 1 .. 31 ) { $str .= '_' . index_from_string_mod( 'ABC', $int ) ; }
is( '_0_0_2_0_2_2_3_4_2_2_4_8_1_10_2_12_9_2_1_12_17_4_16_20_7_14_2_24_13_2_21',
$str, 'index_from_string_mod: ABC modulo 1 .. 31 => ' ) ;
$str = q{} ;
foreach my $int ( 1 .. 31 ) { $str .= '_' . index_from_string_mod( 'DEF', $int ) ; }
is( '_0_1_0_1_0_3_5_1_0_5_1_9_9_5_0_1_5_9_17_5_12_1_3_9_15_9_18_5_13_15_7',
$str, 'index_from_string_mod: DEF modulo 1 .. 31 => ' ) ;
$str = q{} ;
foreach my $int ( 1 .. 31 ) { $str .= '' . index_from_string_mod( $str, 8 ) ; }
is( '3141012455013656157247430216425', $str, 'index_from_string_mod: 1 .. 31 modulo 8 => 3141012455013656157247430216425' ) ;
note( 'Leaving tests_index_from_string_mod()' ) ;
return ;
}
sub index_from_string_mod {
my $string = shift || q{} ;
my $modulo = shift || 1 ;
# from the $string, do a hash in hexa, keep the first 8 hexa "chars",
# convert this to a decimal in the range 0 2^32-1,
# and return the rest of division by $modulo
my $index = hex( substr( hmac_sha1_hex( $string ), 0, 8 ) ) % $modulo ;
#myprint( "[$index] from [$string] modulo [$modulo]\n" ) ;
return( $index ) ;
}
sub tests_choose_remote {
note( 'Entering tests_choose_remote()' ) ;
is( undef, choose_remote( ), 'choose_remote: no args => undef' ) ;
my $mysync ;
is( undef, choose_remote( $mysync ), 'choose_remote: undef => undef' ) ;
$mysync->{ remote } = [] ;
is( undef, choose_remote( $mysync ), 'choose_remote: [] => undef' ) ;
$mysync->{ remote } = [ 'A' ] ;
is( 'A', choose_remote( $mysync ), 'choose_remote: [ "A" ] => "A"' ) ;
$mysync->{ remote } = [qw(A B C)] ;
is( 'B', choose_remote( $mysync ), 'choose_remote: [ A B C ] => B' ) ;
$mysync->{ remote } = [qw(C A B)] ;
is( 'A', choose_remote( $mysync ), 'choose_remote: [ C A B ] => A' ) ;
$mysync->{ remote } = [qw(B C A)] ;
is( 'C', choose_remote( $mysync ), 'choose_remote: [ B C A ] => C' ) ;
note( 'Leaving tests_choose_remote()' ) ;
return ;
}
sub choose_remote {
# This intermediate function is there for allowing
# different way to choose a remote
my $mysync = shift ;
# anyone at random
# A preferable way could be to get a random index
# and inform of it (for an abort later)
# my $elected_index = randindex( @{ $mysync->{ remote } } ) ;
#
#my $elected = randelem( @{ $mysync->{ remote } } ) ;
# same credentials => same remote (if all proxies are presented in same number and order)
# The aim is to be able to send an abort request later to the same remote
my $elected = remote_from_credentials( $mysync ) ;
return( $elected ) ;
}
sub tests_remove_remote {
note( 'Entering tests_remove_remote()' ) ;
is( undef, remove_remote( ), 'remove_remote: no args => undef' ) ;
my $mysync ;
is( undef, remove_remote( $mysync ), 'remove_remote: undef => undef' ) ;
$mysync->{ remote } = [] ;
is( undef, remove_remote( $mysync ), 'remove_remote: [] => undef' ) ;
is_deeply( [ ], $mysync->{ remote }, 'remove_remote: [] => remote = []' ) ;
$mysync->{ remote } = [ 'A' ] ;
is( 'A', remove_remote( $mysync ), 'remove_remote: [ A ] => A' ) ;
is_deeply( [ ], $mysync->{ remote }, 'remove_remote: [ A ] => remote = [ ]' ) ;
$mysync->{ remote } = [qw(A B C)] ;
is( 'B', remove_remote( $mysync ), 'remove_remote: [ A B C ] => B' ) ;
is_deeply( [ qw(A C) ], $mysync->{ remote }, 'remove_remote: [ A B C ] => remote = [ A C ]' ) ;
$mysync->{ remote } = [qw(C A B)] ;
is( 'A', remove_remote( $mysync ), 'remove_remote: [ C A B ] => A' ) ;
is_deeply( [ qw(C B) ], $mysync->{ remote }, 'remove_remote: [ C A B ] => remote = [ C B ]' ) ;
$mysync->{ remote } = [qw(B C A)] ;
is( 'C', remove_remote( $mysync ), 'remove_remote: [ B C A ] => C' ) ;
is_deeply( [ qw(B A) ], $mysync->{ remote }, 'remove_remote: [B C A ] => remote = [ B A ]' ) ;
note( 'Leaving tests_remove_remote()' ) ;
return ;
}
sub remove_remote {
my $mysync = shift ;
my $index = remote_index_from_credentials( $mysync ) ;
my ( $removed ) = remote_remove_index( $mysync, $index ) ;
return $removed ;
}
sub tests_randelem {
note( 'Entering tests_randelem()' ) ;
is( undef, randelem( ), 'randelem: no args => undef' ) ;
is( 1, randelem( 1 ), 'randelem: 1 => 1' ) ;
is( 0, randelem( 0 ), 'randelem: 0 => 0' ) ;
is( '', randelem( '' ), 'randelem: "" => ""' ) ;
is( 'A', randelem( 'A' ), 'randelem: "A" => "A"' ) ;
my $AorB = randelem( 'A', 'B' ) ;
ok( (('A' eq $AorB) or ('B' eq $AorB)), 'randelem: "A" "B" => "A" or "B"' ) ;
note( 'Leaving tests_randelem()' ) ;
return ;
}
sub randelem {
return( $ARG[ rand( @ARG ) ] ) ;
}
# from imapsync
sub myprint { return print @ARG ; }
sub myprintf { return printf @ARG ; }
sub mysprintf {
my( $format, @list ) = @ARG ;
return sprintf $format, @list ;
}
sub firstline {
# extract the first line of a file (without \n)
my( $file ) = @_ ;
my $line = q{} ;
if ( ! -e $file ) {
myprint( "Cannot open file $file since it does not exist\n" ) ;
return ;
}
open my $FILE, '<', $file or do {
myprint( "Error opening file $file : $OS_ERROR\n" ) ;
return ;
} ;
$line = <$FILE> || q{} ;
close $FILE ;
chomp $line ;
return $line ;
}
sub memory_consumption {
# memory consumed by imapsync until now in bytes
return( ( memory_consumption_of_pids( ) )[0] );
}
sub debugmemory {
my $mysync = shift ;
if ( ! $mysync->{debugmemory} ) { return q{} ; }
my $precision = shift ;
return( mysprintf( "Memory consumption$precision: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ) ;
}
sub memory_consumption_of_pids {
my @pid = @_;
@pid = ( @pid ) ? @pid : ( $PROCESS_ID ) ;
my @val ;
if ( 'MSWin32' eq $OSNAME ) {
# MSWin32 later...
# @val = memory_consumption_of_pids_win32( @pid ) ;
}else{
# Unix
local $ENV{PATH} = '/bin';
# Use IPC::Open3 from perlcrit -3
my @ps = backtick( "ps -o vsz -p @pid" ) ;
shift @ps; # First line is column name "VSZ"
chomp @ps;
# convert to octets
@val = map { $_ * $KIBI } @ps ;
}
return( @val ) ;
}
sub backtick {
my $command = shift ;
if ( ! $command ) { return ; }
my ( $writer, $reader, $err ) ;
my @output ;
my $pid ;
my $eval = eval {
$pid = IPC::Open3::open3( $writer, $reader, $err, $command ) ;
} ;
if ( $EVAL_ERROR ) {
myprint( $EVAL_ERROR ) ;
return ;
}
if ( ! $eval ) { return ; }
if ( ! $pid ) { return ; }
waitpid( $pid, 0 ) ;
@output = <$reader>; # Output here
#
#my @errors = <$err>; #Errors here, instead of the console
if ( not @output ) { return ; }
#myprint( @output ) ;
if ( $output[0] =~ /\Qopen3: exec of $command failed\E/mxs ) { return ; }
if ( wantarray ) {
return( @output ) ;
} else {
return( join( q{}, @output) ) ;
}
}
sub cgibegin {
if ( ! under_cgi_context( ) ) { return ; }
my $mysync = shift ;
require CGI ;
CGI->import( qw( -no_debug ) ) ;
require CGI::Carp ;
CGI::Carp->import( qw( fatalsToBrowser ) ) ;
$mysync->{cgi} = CGI->new( ) ;
return ;
}
sub myGetOptions {
# Started as a copy of Luke Ross Getopt::Long::CGI
# https://metacpan.org/release/Getopt-Long-CGI
# So this sub function is under the same license as Getopt-Long-CGI Luke Ross wants it,
# which was Perl 5.6 or later licenses at the date of the copy.
my $mysync = shift @ARG ;
my $arguments_ref = shift @ARG ;
my %options = @ARG ;
if ( not under_cgi_context( $mysync ) ) {
# Not CGI - pass upstream for normal command line handling
return Getopt::Long::GetOptionsFromArray( $arguments_ref, %options ) ;
}
my $mycgi = $mysync->{cgi} ;
# We must be in CGI context now
if ( !defined( $mycgi ) ) { return ; }
my $badthings = 0 ;
foreach my $key ( sort keys %options ) {
my $val = $options{$key} ;
if ( $key !~ m/^([\w\d\|]+)([=:][isf])?([\+!\@\%])?$/mxs ) {
$badthings++ ;
next ; # Unknown item
}
my $name = [ split '|', $1, 1 ]->[0] ;
if ( ( $3 || q{} ) eq '+' ) {
${$val} = $mycgi->param( $name ) ; # "Incremental" integer
}
elsif ( $2 ) {
my @values = $mycgi->multi_param( $name ) ;
my $type = $2 ;
#myprint( "type[$type]values[@values]\$3[", $3 || q{}, "]val[$val]ref(val)[", ref($val), "]\n" ) ;
if ( ( $3 || q{} ) eq '%' or ref( $val ) eq 'HASH' ) {
my %values = map { split /=/mxs, $_ } @values ;
if ( $type =~ m/i$/mxs ) {
foreach my $k ( keys %values ) {
$values{$k} = int $values{$k} ;
}
}
elsif ( $type =~ m/f$/mxs ) {
foreach my $k ( keys %values ) {
$values{$k} = 0 + $values{$k};
}
}
if ( 'REF' eq ref $val ) {
%{ ${$val} } = %values ;
}
else {
%{$val} = %values ;
}
}
else {
if ( $type =~ m/i$/mxs ) {
@values = map { q{} ne $_ ? int $_ : undef } @values ;
}
elsif ( $type =~ m/f$/mxs ) {
@values = map { 0 + $_ } @values ;
}
if ( ( $3 || q{} ) eq '@' ) {
@{ ${$val} } = @values ;
}
elsif ( ref( $val ) eq 'ARRAY' ) {
@{$val} = @values ;
}
else {
${$val} = $values[0] ;
}
}
}
else {
# Checkbox
# Considers only --name
# Should consider also --no-name and --noname
${$val} = $mycgi->param( $name ) ? 1 : undef ;
}
}
if ( $badthings ) {
return ;
}
else {
return ( 1 ) ;
}
}
sub tests_get_options_cgi {
note( 'Entering tests_get_options_cgi()' ) ;
# Temporary, have to think harder about testing CGI context in command line --tests
# API:
# * input arguments: two ways, command line or CGI
# * the program arguments
# * QUERY_STRING env variable
# * return
# * QUERY_STRING length
# CGI context
local $ENV{SERVER_SOFTWARE} = 'Votre serviteur' ;
# Real full test
# = 'host1=test1.lamiral.info&user1=test1&password1=secret1&host2=test2.lamiral.info&user2=test2&password2=secret2&debugenv=on'
my $mysync ;
is( undef, get_options_cgi( $mysync ), 'get_options_cgi: no CGI module => undef' ) ;
require CGI ;
CGI->import( qw( -no_debug ) ) ;
is( undef, get_options_cgi( $mysync ), 'get_options_cgi: no CGI param => undef' ) ;
# Testing boolean
$mysync->{cgi} = CGI->new( 'version=on&debugenv=on' ) ;
local $ENV{'QUERY_STRING'} = 'version=on&debugenv=on' ;
is( 22, get_options_cgi( $mysync ), 'get_options_cgi: QUERY_STRING => 22' ) ;
is( 1, $mysync->{ version }, 'get_options_cgi: --version => 1' ) ;
# debugenv is not allowed in cgi context
is( undef, $mysync->{debugenv}, 'get_options_cgi: $mysync->{debugenv} => undef' ) ;
# QUERY_STRING in this test is only for return value of get_options_cgi
# Have to think harder, GET/POST context, is this return value a good thing?
local $ENV{'QUERY_STRING'} = 'host1=test1.lamiral.info&user1=test1' ;
$mysync->{cgi} = CGI->new( 'host1=test1.lamiral.info&user1=test1' ) ;
is( 36, get_options_cgi( $mysync, ), 'get_options_cgi: QUERY_STRING => 36' ) ;
is( 'test1', $mysync->{user1}, 'get_options_cgi: $mysync->{user1} => test1' ) ;
#local $ENV{'QUERY_STRING'} = undef ;
# Testing s@ as ref
$mysync->{cgi} = CGI->new( 'folder=fd1' ) ;
get_options_cgi( $mysync ) ;
is_deeply( [ 'fd1' ], $mysync->{folder}, 'get_options_cgi: $mysync->{folder} => fd1' ) ;
$mysync->{cgi} = CGI->new( 'folder=fd1&folder=fd2' ) ;
get_options_cgi( $mysync ) ;
is_deeply( [ 'fd1', 'fd2' ], $mysync->{folder}, 'get_options_cgi: $mysync->{folder} => fd1, fd2' ) ;
# Testing boolean ! with --noxxx, does not work
$mysync->{cgi} = CGI->new( 'nodry=on' ) ;
get_options_cgi( $mysync ) ;
is( undef, $mysync->{dry}, 'get_options_cgi: --nodry => $mysync->{dry} => undef' ) ;
$mysync->{cgi} = CGI->new( 'host1=example.com' ) ;
get_options_cgi( $mysync ) ;
is( 'example.com', $mysync->{host1}, 'get_options_cgi: --host1=example.com => $mysync->{host1} => example.com' ) ;
$mysync->{cgi} = CGI->new( 'simulong=' ) ;
get_options_cgi( $mysync ) ;
is( undef, $mysync->{simulong}, 'get_options_cgi: --simulong= => $mysync->{simulong} => undef' ) ;
$mysync->{cgi} = CGI->new( 'simulong' ) ;
get_options_cgi( $mysync ) ;
is( undef, $mysync->{simulong}, 'get_options_cgi: --simulong => $mysync->{simulong} => undef' ) ;
$mysync->{cgi} = CGI->new( 'simulong=4' ) ;
get_options_cgi( $mysync ) ;
is( 4, $mysync->{simulong}, 'get_options_cgi: --simulong=4 => $mysync->{simulong} => 4' ) ;
note( 'Leaving tests_get_options_cgi()' ) ;
return ;
}
sub get_options_cgi {
# In CGI context arguments are not in @ARGV but in QUERY_STRING variable (with GET).
my $mysync = shift @ARG ;
my $mycgi ;
if ( under_cgi_context( ) ) {
$mycgi = $mysync->{cgi} ;
}
my @arguments = @ARG ;
# final 0 is used to print usage when no option is given
my $numopt = length $ENV{'QUERY_STRING'} || 1 ;
my $opt_ret = myGetOptions(
$mysync,
\@arguments,
'abort!' => \$mysync->{abort},
'host1=s' => \$mysync->{host1},
'host2=s' => \$mysync->{host2},
'user1=s' => \$mysync->{user1},
'user2=s' => \$mysync->{user2},
'password1=s' => \$mysync->{password1},
'password2=s' => \$mysync->{password2},
'dry!' => \$mysync->{dry},
'version' => \$mysync->{version},
'ssl1!' => \$mysync->{ssl1},
'ssl2!' => \$mysync->{ssl2},
'tls1!' => \$mysync->{tls1},
'tls2!' => \$mysync->{tls2},
'justlogin!' => \$mysync->{justlogin},
'justconnect!' => \$mysync->{justconnect},
'addheader!' => \$mysync->{addheader},
'automap!' => \$mysync->{automap},
'justautomap!' => \$mysync->{justautomap},
'gmail1' => \$mysync->{gmail1},
'gmail2' => \$mysync->{gmail2},
'office1' => \$mysync->{office1},
'office2' => \$mysync->{office2},
'exchange1' => \$mysync->{exchange1},
'exchange2' => \$mysync->{exchange2},
'domino1' => \$mysync->{domino1},
'domino2' => \$mysync->{domino2},
'f1f2=s@' => \$mysync->{f1f2},
'folder=s@' => \$mysync->{folder},
'testslive!' => \$mysync->{testslive},
'testslive6!' => \$mysync->{testslive6},
'releasecheck!' => \$mysync->{releasecheck},
'simulong=i' => \$mysync->{simulong},
'remote=s@' => \$mysync->{remote},
'debugmemory!' => \$mysync->{debugmemory},
'debug!' => \$mysync->{debug},
'tests' => \$mysync->{tests},
'testsunit=s@' => \$mysync->{testsunit},
) ;
$mysync->{debug} and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ;
my $numopt_after = scalar @arguments ;
if ( $numopt_after ) {
myprint( "Extra arguments found: @arguments\n", "It usually means a quoting issue in the command line\n" ) ;
return ;
}
if ( ! $opt_ret ) {
return ;
}
return $numopt ;
}
sub tests_get_options {
note( 'Entering tests_get_options()' ) ;
# API:
# * input arguments: two ways, command line or CGI
# * the program arguments
# * QUERY_STRING env variable
# * return
# * undef if bad things happened like
# * options not known
# * number of arguments or QUERY_STRING length
my $mysync3 = { } ;
is( undef, get_options( $mysync3, qw( --noexist ) ), 'get_options: --noexist => undef' ) ;
is( undef, $mysync3->{ noexist }, 'get_options: --noexist => undef' ) ;
is( undef, get_options( $mysync3, qw( --lalala --noexist --version ) ), 'get_options: --lalala --noexist --version => undef' ) ;
is( 1, $mysync3->{ version }, 'get_options: --version => 1' ) ;
is( undef, $mysync3->{ noexist }, 'get_options: --noexist => undef' ) ;
$mysync3 = { } ;
get_options( $mysync3, qw( --host1 HOST_01) ) ;
is( 'HOST_01', $mysync3->{ host1 }, 'get_options: --host1 HOST_01 => HOST_01' ) ;
is( undef, $mysync3->{ version }, 'get_options: --version => 1' ) ;
get_options( $mysync3, "--version" ) ;
is( 1, $mysync3->{ version }, 'get_options: --version => 1' ) ;
is( undef, get_options( $mysync3, qw( --help --noexist --version ) ), 'get_options: --help --noexist --version => undef' ) ;
is( undef, get_options( $mysync3, qw( extra ) ), 'get_options: extra => undef' ) ;
is( undef, get_options( $mysync3, qw( --help extra1 --version extra2 ) ), 'get_options: --help extra1 --version extra2 => undef' ) ;
#myprint( Data::Dumper->Dump( [ $mysync3 ] ) ) ;
note( 'Leaving tests_get_options()' ) ;
return ;
}
sub get_options {
my $mysync = shift @ARG ;
my @arguments = @ARG ;
my $ret = get_options_cgi( $mysync, @arguments ) ;
foreach my $key ( sort keys %{ $mysync } ) {
if ( ! defined $mysync->{$key} ) {
delete $mysync->{$key} ;
next ;
}
if ( 'ARRAY' eq ref( $mysync->{$key} )
and 0 == scalar( @{ $mysync->{$key} } ) ) {
delete $mysync->{$key} ;
}
}
return $ret ;
}
sub output {
my $mysync = shift @ARG ;
if ( not $mysync ) { return ; }
my @output = @ARG ;
$mysync->{ output } .= join( q{}, @output ) ;
return $mysync->{ output } ;
}
sub output_start {
my $mysync = shift @ARG ;
if ( not $mysync ) { return ; }
my @output = @ARG ;
$mysync->{ output } = join( q{}, @output ) . ( $mysync->{ output } || q{} ) ;
return $mysync->{ output } ;
}
sub output_reset_with {
my $mysync = shift @ARG ;
if ( not $mysync ) { return ; }
my @output = @ARG ;
$mysync->{ output } = join( q{}, @output ) ;
return $mysync->{ output } ;
}
sub under_cgi_context {
# Under cgi context
if ( $ENV{SERVER_SOFTWARE} ) {
return 1 ;
}
# Not in cgi context
return ;
}
sub loadavg {
if ( 'linux' eq $OSNAME ) {
return ( loadavg_linux( @ARG ) ) ;
}
return( 'unknown' ) ;
}
sub loadavg_linux {
my $line = shift ;
if ( ! $line ) {
$line = firstline( '/proc/loadavg' ) or return ;
}
my ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) = split /\s/mxs, $line ;
if ( all_defined( $avg_1_min, $avg_5_min, $avg_15_min ) ) {
#myprint( "System load: $avg_1_min $avg_5_min $avg_15_min $current_runs\n" ) ;
return ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) ;
}
return ;
}
sub all_defined {
if ( not @ARG ) {
return 0 ;
}
foreach my $elem ( @ARG ) {
if ( not defined $elem ) {
return 0 ;
}
}
return 1 ;
}
sub load_and_delay {
# Basically return 0 if load is not heavy, ie <= 1 per processor
if ( 4 > scalar @ARG ) { return ; }
my ( $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) = @ARG ;
if ( 0 == $cpu_num ) { return ; }
# Let divide by number of cores
( $avg_1_min, $avg_5_min, $avg_15_min ) = map { $_ / $cpu_num } ( $avg_1_min, $avg_5_min, $avg_15_min ) ;
# One of avg ok => ok, for now it is a OR
if ( $avg_1_min <= 1 ) { return 0 ; }
if ( $avg_5_min <= 1 ) { return 1 ; } # Retry in 1 minute
if ( $avg_15_min <= 1 ) { return 5 ; } # Retry in 5 minutes
return 15 ; # Retry in 15 minutes
}
sub ram_memory_info {
# In GigaBytes so division by 1024 * 1024 * 1024
#
return(
sprintf( "%.1f/%.1f free GiB of RAM",
Sys::MemInfo::get("freemem") / ( $KIBI ** 3 ),
Sys::MemInfo::get("totalmem") / ( $KIBI ** 3 ),
)
) ;
}
sub cpu_number {
my $cpu_number_forced = shift ;
# Well, here 1 is better than 0 or undef
my $cpu_number = 1 ; # Default value, erased if better found
my @cpuinfo ;
if ( $ENV{"NUMBER_OF_PROCESSORS"} ) {
# might be under a Windows system
$cpu_number = $ENV{"NUMBER_OF_PROCESSORS"} ;
#$debug and myprint( "Number of processors found by env var NUMBER_OF_PROCESSORS: $cpu_number\n" ) ;
}elsif ( 'darwin' eq $OSNAME ) {
$cpu_number = backtick( "sysctl -n hw.ncpu" ) ;
chomp( $cpu_number ) ;
#$debug and myprint( "Number of processors found by cmd 'sysctl -n hw.ncpu': $cpu_number\n" ) ;
}elsif ( ! -e '/proc/cpuinfo' ) {
#$debug and myprint( "Number of processors not found so I might assume there is only 1\n" ) ;
$cpu_number = 1 ;
}elsif( @cpuinfo = file_to_array( '/proc/cpuinfo' ) ) {
$cpu_number = grep { /^processor/mxs } @cpuinfo ;
#$debug and myprint( "Number of processors found via /proc/cpuinfo: $cpu_number\n" ) ;
}
if ( defined $cpu_number_forced ) {
$cpu_number = $cpu_number_forced ;
}
return( integer_or_1( $cpu_number ) ) ;
}
sub integer_or_1 {
my $number = shift ;
if ( is_an_integer( $number ) ) {
return $number ;
}
# else
return 1 ;
}
sub is_an_integer {
my $number = shift ;
if ( ! defined $number ) { return ; }
return( $number =~ m{^\d+$}xo ) ;
}
sub file_to_array {
my( $file ) = shift ;
my @string ;
open my $FILE, '<', $file or do {
myprint( "Error reading file $file : $OS_ERROR" ) ;
return ;
} ;
@string = <$FILE> ;
close $FILE ;
return( @string ) ;
}
sub software_version {
my $mysync = shift ;
my $rcs = $mysync->{rcs} ;
my $version ;
$version = version_from_rcs( $rcs ) ;
return( $version ) ;
}
sub version_from_rcs {
my $rcs = shift ;
if ( ! $rcs ) { return ; }
my $version = 'UNKNOWN' ;
if ( $rcs =~ m{,v\s+(\d+\.\d+)}mxso ) {
$version = $1
}
return( $version ) ;
}
sub testsexit {
my $mysync = shift ;
if ( ! ( $mysync->{ tests } or $mysync->{ testsunit } ) ) {
return ;
}
my $test_builder = Test::More->builder ;
tests( $mysync ) ;
testunitsession( $mysync ) ;
my @summary = $test_builder->summary() ;
my @details = $test_builder->details() ;
my $nb_tests_run = scalar( @summary ) ;
my $nb_tests_expected = $test_builder->expected_tests() ;
my $nb_tests_failed = count_0s( @summary ) ;
my $tests_failed = report_failures( @details ) ;
if ( $nb_tests_failed or ( $nb_tests_run != $nb_tests_expected ) ) {
#$test_builder->reset( ) ;
myprint( "Summary of tests: failed $nb_tests_failed tests, run $nb_tests_run tests, expected to run $nb_tests_expected tests.\n",
"List of failed tests:\n", $tests_failed ) ;
exit $EXIT_TESTS_FAILED ;
}
exit ;
}
sub testunitsession {
my $mysync = shift ;
if ( ! $mysync ) { return ; }
if ( ! $mysync->{ testsunit } ) { return ; }
my @functions = @{ $mysync->{ testsunit } } ;
if ( ! @functions ) { return ; }
SKIP: {
if ( ! @functions ) { skip( 'No test in normal run' ) ; }
testsunit( @functions ) ;
done_testing( ) ;
}
return ;
}
sub count_0s {
my @array = @ARG ;
if ( ! @array ) { return 0 ; }
my $nb_zeros = 0 ;
map { $_ == 0 and $nb_zeros += 1 } @array ;
return $nb_zeros ;
}
sub report_failures {
my @details = @ARG ;
if ( ! @details ) { return ; }
my $counter = 1 ;
my $report = q{} ;
foreach my $details ( @details ) {
if ( ! $details->{ 'ok' } ) {
my $name = $details->{ 'name' } || 'NONAME' ;
$report .= "nb $counter - $name\n" ;
}
$counter += 1 ;
}
return $report ;
}
sub testsunit {
my @functions = @ARG ;
if ( ! @functions ) { #
myprint( "testsunit warning: no argument given\n" ) ;
return ;
}
foreach my $function ( @functions ) {
if ( ! $function ) {
myprint( "testsunit warning: argument is empty\n" ) ;
next ;
}
if ( ! exists &$function ) {
myprint( "testsunit warning: function $function does not exist\n" ) ;
next ;
}
if ( ! defined &$function ) {
myprint( "testsunit warning: function $function is not defined\n" ) ;
next ;
}
my $function_ref = \&{ $function } ;
&$function_ref() ;
}
return ;
}
sub tests_template {
note( 'Entering tests_template()' ) ;
is( undef, undef, 'template: undef is undef' ) ;
like( 'aBCd', qr/BC/, 'template: aBCd is like BC' ) ;
unlike( 'aCBd', qr/BC/, 'template: aCBd is not like BC' ) ;
is_deeply( {}, {}, 'template: a hash is a hash' ) ;
is_deeply( [], [], 'template: an array is an array' ) ;
note( 'Leaving tests_template()' ) ;
return ;
}
sub tests {
my $mysync = shift ;
if ( ! $mysync->{ tests } ) { return ; }
SKIP: {
if ( ! $mysync->{ tests } ) {
skip( 'No test in normal run' )
}
note( 'Entering tests()' ) ;
tests_get_options( ) ;
tests_get_options_cgi( ) ;
tests_randelem( ) ;
tests_index_from_string_mod( ) ;
tests_remote_from_credentials( ) ;
tests_choose_remote( ) ;
tests_remote_index_from_credentials( ) ;
tests_remote_remove_index( ) ;
tests_remove_remote( ) ;
tests_can_and_have_to_sync( ) ;
done_testing( 91 ) ;
note( 'Leaving tests()' ) ;
}
return ;
}