diff -r 000000000000 -r 83f4b4db085c bldsystemtools/commonbldutils/AntiVirus.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bldsystemtools/commonbldutils/AntiVirus.pm Tue Feb 02 01:39:43 2010 +0200 @@ -0,0 +1,494 @@ +package AntiVirus; +use strict; + +use File::Copy; +use Win32::Process; + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw{Start Stop Scan WaitTillAntiVirusStarts}; + +# Start: +# Description: attempts to start all supported anti-virus services/processes +# Revised April 2007 to support McAfee only, Sophos and WebRoot having passed into history! +# Arguments: none +# Returns: none +sub Start() +{ + my @iMcAfeeCommands = ( + {AppName => 'net.exe', Params => 'start McAfeeFramework'}, + {AppName => 'net.exe', Params => 'start McShield'}, + {AppName => 'net.exe', Params => 'start McTaskManager'}, + # Must supply full pathname, as these applications are not in "PATH" + {AppName => 'C:\\Program Files\\Network Associates\\Common Framework\\UpdaterUI.exe', Params => '/StartedFromRunKey'}, + {AppName => 'C:\\Program Files\\Network Associates\\Common Framework\\UdaterUI.exe', Params => '/StartedFromRunKey'}, + {AppName => 'C:\\Program Files\\McAfee\\Common Framework\\UpdaterUI.exe', Params => '/StartedFromRunKey'}, + {AppName => 'C:\\Program Files\\McAfee\\Common Framework\\UdaterUI.exe', Params => '/StartedFromRunKey'}, + {AppName => 'C:\\Program Files\\Common Files\\Network Associates\\TalkBack\\tbmon.exe', Params => ''}, + {AppName => 'C:\\Program Files\\Network Associates\\VirusScan\\SHSTAT.EXE', Params => '/STANDALONE'}, + {AppName => 'C:\\Program Files\\McAfee\\VirusScan\\SHSTAT.EXE', Params => '/STANDALONE'}); + + # Execute all "START" commands + for my $iCmd(@iMcAfeeCommands) + { + ExecuteProcess(20,$iCmd); + } + +} + +# WaitTillAntiVirusStarts +# Description: +# Sometimes the Antivirus::Stop() is invoked before the Antivirus services are started in the machine. +# because of which the Antivirus starts when the build is ongoing, and the active Antivirus disrupts/slows down the build +# +# If the Antivirus services fail to start beyond the specified timeout period,reports error and aborts the build. +# +# Arguments : Waiting time in seconds before next attempt is made to check AV service status, max number of attempts +# Returns : returns nothing if AV running, aborts build if AV not running even after the waiting period +sub WaitTillAntiVirusStarts{ + my $waitingTime = shift; + my $retries = shift; + + my @waitList = shift; + + my $defaultWaitingTime = 30; # Waiting time in seconds, before next attempt is made to check AV service status + my $defaultRetries = 5; # Try upto 5 times + + # assign default values to waitingtime,retries if values not specified + unless($waitingTime) + { + $waitingTime = $defaultWaitingTime; + } + unless($retries) + { + $retries = $defaultRetries; + } + + my $attempt = 0; + ## If AV not active, wait and retry + while(1) + { + if(IsAntiVirusActive(@waitList)) + { + return; ## AV is active, all fine, proceed + } + $attempt++; + if($attempt > $retries) + { + last; + } + print "REMARK: One or more Antivirus services not active yet (At ".scalar localtime().") \n"; + print "Waiting $waitingTime secs before rechecking status (Attempt $attempt of $retries) ...\n\n"; + sleep($waitingTime); + } + + ## Antivirus is not active even after waiting $waitingTime secs $retries times, report error and abort the build + + print "ERROR: RealTimeBuild : Antivirus is not active even after waiting for $waitingTime secs x $retries attempts\n"; + print "Antivirus cannot be reliably stopped,abandon build."; +} + + +# IsAntiVirusActive +# Description: +# Runs 'net start' command to get a list of active services,checks if the Antivirus services are in the list +# +# Arguments : None +# Returns : +# returns 0 even if one Antivirus Service is inactive +# returns 1 if All AntiVirus Services active +sub IsAntiVirusActive{ + + my @waitList = shift; + + my @AntiVirusServices = @waitList; + + print "Checking Antivirus services status ...\n"; + my $iCmd={AppName => 'net.exe', Params => 'start'}; + + # Check if net.exe is accessible + unless(-e $iCmd->{'AppName'}) + { + my $iExecutable = FindFileFromPath($iCmd->{'AppName'}); + unless ($iExecutable) + { + print "REMARK: File not found $iCmd->{'AppName'}\n"; + print "REMARK: Unable to check Antivirus status\n"; + return 0; + } + $iCmd->{'AppName'} = $iExecutable; + } + + my $iCommand = "\"$iCmd->{'AppName'}\" $iCmd->{'Params'}" ; + print "Running: $iCommand\n"; + my @iOutput = `$iCommand`; + + return &ParseServiceStatus(\@iOutput,\@AntiVirusServices); +} + + +# ParseServiceStatus +# Description: +# Runs Compares output of 'net start' command to get a list of active services,checks if the Antivirus services are in the list +# +# Arguments : Output of 'net start', Antivirus service list +# Returns : +# returns 0 even if one Antivirus Service is not in the list +# returns 1 if All AntiVirus Services are in the list +sub ParseServiceStatus($$) +{ + my $iOutputRef = shift; + my $iAntiVirusServiceList = shift; + foreach(@$iOutputRef) + { + chomp ; + s/^[\t\s]+//g; # strip whitespace at the beginning of the line + } + + for my $serviceName(@$iAntiVirusServiceList) + { + my @match = grep (/^$serviceName$/i, @$iOutputRef); + unless(@match) + { + print "Antivirus Service \'$serviceName\' inactive\n"; + return 0; + } + } + print "All Antivirus services active.\n"; + return 1; +} + + +# Stop: +# Description: attempts to stop all supported anti-virus services/processes +# Revised April 2007 to support McAfee only, Sophos and WebRoot having passed into history! +# Arguments: none +# Returns: none +sub Stop($$) +{ + # Need to check if the Antivirus services is installed or enabled. + my @iMcAfeeServices = ( + {AppName => 'net.exe', Params => 'stop McAfeeFramework', Services =>'McAfee Framework Service'}, + {AppName => 'net.exe', Params => 'stop McShield', Services =>'Network Associates McShield'}, + {AppName => 'net.exe', Params => 'stop McTaskManager', Services =>'Network Associates Task Manager'}); + + my @iPskillCommands = ( + {AppName => 'pskill.exe', Params => 'UpdaterUI.exe'}, + {AppName => 'pskill.exe', Params => 'tbmon.exe'}, + {AppName => 'pskill.exe', Params => 'shstat.exe'}); + + my @waitList = (); + my @iMcAfeeCommands = (); + + for my $iTestCmd(@iMcAfeeServices) + { + my $iExecutable = $iTestCmd->{'AppName'}; + my $iParams = $iTestCmd->{'Params'}; + my $iServices = $iTestCmd->{'Services'}; + + my @testCommand = `$iExecutable $iParams 2>&1`; + my $iResponse = 0; + + foreach my $iLine (@testCommand) + { + if ($iLine =~ m/does not exist/i) + { + print "REMARK: $iServices not installed, just proceed.\n"; + $iResponse = 1; + } + + if ($iLine =~ m/it is disabled/i) + { + print "REMARK: $iServices not enabled, just proceed.\n"; + $iResponse = 1; + } + + if (($iLine =~ m/service is not started/i ) or ($iLine =~ m/not valid for this service/i )) + { + print "REMARK: $iServices not started, just wait.\n"; + push @waitList, $iServices; + push @iMcAfeeCommands, {AppName =>$iExecutable, Params =>$iParams}; + $iResponse = 1; + } + + if ($iLine =~ m/service was stopped successfully/i) + { + print "REMARK: Stop Success! $iServices was stopped successfully directly by the test command, just proceed.\n"; + $iResponse = 1; + } + + if ($iLine =~ m/service could not be stopped/i) + { + print "REMARK: Stop Success! $iServices could not be stopped at the moment but will be stopped successfully directly by the test command very soon later, just proceed.\n"; + $iResponse = 1; + } + + if ($iLine =~ m/System error 5 has occurred/i) + { + print "WARNING: Access to $iServices is denied, but this won't affect the build, just proceed.\n"; + $iResponse = 1; + } + + } + unless ($iResponse) + { + print "ERROR: Unable to parse the output of $iExecutable for $iParams!\n"; + foreach my $iLine (@testCommand) + { + print $iLine; + } + #push @waitList, $iServices; + #push @iMcAfeeCommands, {AppName =>$iExecutable, Params =>$iParams}; + } + } + + # Wait for the waiting Antivirus services to load before attempting to stop the services + # If Antivirus fails to load after the grace period, the build should be aborted. + my $gWaitingTime = shift; + my $gRetries = shift; + if (@waitList) + { + WaitTillAntiVirusStarts($gWaitingTime, $gRetries, @waitList); + } + + # Execute all "STOP" commands + push @iMcAfeeCommands, @iPskillCommands; + for my $iCmd(@iMcAfeeCommands) + { + ExecuteProcess(20,$iCmd); + } +} + +# Scan: +# Description: performs the virus scan on the specified directores +# Revised April 2007 to support McAfee only, Sophos having passed into history! +# Revised February 2008 to support further McAfee filename changes. +# Arguments: directory in which to place scan output file (logfile); +# array ref to array containing a list of the directories to scan +# Returns: none +sub Scan($$) +{ + my $iOutFilesDir = shift; + my $iDirsToScan = shift; + + # Name(s) of output files. User only supplies directories + my $iOutFileName = 'Anti-virus'; + + # Define McAfee commands, newest version first. McAfee keep changing the names of their files. Thus if an + # executable is not found, we do not treat this as an error, we simply go on to thwe next command definition. + # Note: Must supply full pathname, as these applications are not in "PATH" + my @iMcAfeeCommands =( + # CSScan.exe send most of its output to STDOUT. So define file a second time for us to write this data + {AppName => 'C:\\Program Files\\McAfee\\VirusScan Enterprise\\csscan.exe', + Params => join(" ", @$iDirsToScan) . " /SUB /CLEAN /ALLAPPS /LOG $iOutFilesDir\\$iOutFileName.log", + LogFile => "$iOutFilesDir\\$iOutFileName"}, # Filename without .LOG extension + {AppName => 'C:\\Program Files\\Common Files\\Network Associates\\Engine\\scan.exe', + Params => join(" ", @$iDirsToScan) . " /SUB /NOBREAK /CLEAN /NORENAME /RPTCOR /RPTERR /REPORT=$iOutFilesDir\\$iOutFileName.log", + LogFile => ''} # The older "SCAN.EXE" writes everything to its report file. So no further logging required. + ); + + print "Scanning: @$iDirsToScan\n"; + + # Execute "SCAN" commands in order until one is successful. + for my $iCmd(@iMcAfeeCommands) + { + # First remove/rename any existing output file. This is an unlikely situation in a normal build; + # but it could cause confusion if, for example, a scan was re-run manually. + unless (RemoveExistingFile($iCmd->{'LogFile'})) { next; } + if (RunCommand($iCmd)) { return; } # Success + } + # Arriving at the end of this loop means that no command succeeded! Report failure! + print "WARNING: No Virus Scan accomplished. Possibly no suitable executable found.\n"; +} + +# RunCommand: +# Description: Runs specified command using Perl "backticks" +# passing its output for parsing to the sub ParseOutput(). +# Arguments: the command to execute as a hashref (filename of executable and parameters/args to pass to it) +# Returns: TRUE on success +sub RunCommand($) +{ + my $iCmd = shift; # Hashref - Command spec. + + unless(-e $iCmd->{'AppName'}) + { + print "REMARK: File not found $iCmd->{'AppName'}\n"; + return 0; # FALSE = Failure + } + my $iCommand = "\"$iCmd->{'AppName'}\" $iCmd->{'Params'}" ; + print "Running: $iCommand\n"; + my @iOutput = `$iCommand`; + + # Parse the output flagging any errors + return &ParseOutput(\@iOutput,$iCmd->{'LogFile'}); # Return TRUE or FALSE +} + +# ParseOutput: +# Description: Parse output for warnings and errors. +# The ScanLog-compatible "WARNING: " is prepended to any line containing +# any of these messages or errors, and all lines are printed to STDOUT. +# Arguments: reference to the array of output lines from executed command; Name of logfile (if any) +# Returns: TRUE on Success +# Remarks: note that errors in starting/stopping processes or services do not +# constitute errors in terms of the build process. Failure to start or stop +# the services will not affect the compilation except, at worst, to slow it +# down if stopping the antivirus fails. +sub ParseOutput($) +{ + my $iOutputRef = shift; + my $iLogFile = shift; + + my $fh = \*LOGFILE; + + if($iLogFile) + { + unless (open $fh, ">>$iLogFile.log") + { + print ("WARNING: Failed to open log file: $iLogFile"); + return 0; # FALSE = Failure + } + } + else + { + $fh = \*STDOUT; + } + + for my $line(@$iOutputRef) # For each line of output... + { + $line =~ s/\s+$//; # Remove trailing spaces (McAfee CSSCAN pads each line to 1024 chars!!) + # Does it match an error as returned by PSKill or net stop/start + if($line =~ /The specified service does not exist as an installed service\./i or + $line =~ /The .*? service is not started\./i or + $line =~ /The requested service has already been started./i or + $line =~ /The service name is invalid\./i or + $line =~ /Process does not exist\./i or + $line =~ /Unable to kill process/i or + $line =~ /error/i or + $line =~ /is not recognized as an internal or external command/i) + { + print "WARNING: $line\n"; + } + else # No errors/warnings so just print the line on its own + { + print $fh "$line\n"; + } + } + return 1; # TRUE = Success +} + +# ExecuteProcess: +# Description: Executes specified command using Perl module Win32::Process +# Arguments: the command to execute as a hashref (filename of executable and parameters/args to pass to it) +# Returns: none +sub ExecuteProcess +{ + my $iWaitSecs = shift; + my $iCmd = shift; # Hashref - Command spec. + + my $iExecutable = $iCmd->{'AppName'}; + unless(-e $iExecutable) + { + $iExecutable = FindFileFromPath($iCmd->{'AppName'}); + unless ($iExecutable) + { + print "REMARK: File not found $iCmd->{'AppName'}\n"; + return; + } + } + my $iParams = $iCmd->{'Params'}; + + # my $iFlags = $iDebug? CREATE_NEW_CONSOLE: DETACHED_PROCESS; + # my $iFlags = $iDebug? 0: DETACHED_PROCESS; + my $iFlags = 0; + + # Create a new Perl process (because fork does not work on some versions of Perl on Win32) + # $^X is the path to the Perl binary used to process this script + my $iProcess; + unless (Win32::Process::Create($iProcess, "$iExecutable", "\"$iExecutable\" $iParams", 0, $iFlags, ".")) + { + print "WARNING: Failed to create process for $iExecutable $iParams.\n"; + my $iExitCode = Win32::GetLastError(); + ReportProcessError($iExitCode); + return; + } + + my $iPID = $iProcess->GetProcessID(); + print "\nExecuting: $iExecutable $iParams .....\n"; + my $iRetVal = $iProcess->Wait($iWaitSecs * 1000); # milliseconds. Return value is zero on timeout, else 1. + if ($iRetVal == 0) # Wait timed out + { # No error from child process (so far) + print "Spawned: $iExecutable $iParams - PID=$iPID.\n"; + return 1; # Success + } + else # Child process terminated. Wait usually returns 1. + { # Error in child process?? If so, get exit code + my $iExitCode; + $iProcess->GetExitCode($iExitCode); + unless($iExitCode) + { + print "Executed: $iExecutable $iParams - PID=$iPID.\n"; + return 1; # Success + } + print "REMARK: Failed in execution of $iExecutable $iParams.\n"; + ReportProcessError($iExitCode); + return 0; + } +} + +# ReportProcessError: +# Description: prints error code to STDOUT followed by explanatory text, if avalable +# Arguments: Windows error code +# Returns: none +sub ReportProcessError +{ + my $iExitCode = shift; + + my $iMsg = Win32::FormatMessage($iExitCode); + if (defined $iMsg) + { + printf "ExitCode: 0x%04x = %s\n", $iExitCode, $iMsg; + } + else + { + printf "ExitCode: 0x%04x\n", $iExitCode; + } +} + +# FindFileFromPath: +# Description: Tries to find a file using PATH Environment Variable +# Argument: Filename (Must include extension .EXE, .CMD etc) +# Return: Full pathname of file, if found, otherwise undef +sub FindFileFromPath +{ + my $iFilename = shift; + my $iPathname; + my @iPathDirs = split /;/, $ENV{'PATH'}; + foreach my $iDir (@iPathDirs) + { + while ($iDir =~ s/\\$//){;} # Remove any trailing backslash(es) + $iPathname = "$iDir\\$iFilename"; + if (-e $iPathname) + { + return $iPathname; + } + } + return undef; +} + +# RemoveExistingFile: +# Description: If specified .LOG file exists, rename to .BAK +# Arguments: File to check +# Returns: TRUE on Success +sub RemoveExistingFile +{ + my $iFilename = shift; # Full pathname LESS extension + + unless (-e "$iFilename.log") { return 1; } # Success! + + if (File::Copy::move("$iFilename.log","$iFilename.bak")) { return 1; } # Success! + + print "WARNING: Failed to rename $iFilename.log\nto $iFilename.bak because of:\n$!\n"; + return 0; # FALSE = Failure +} + +1;