#!/usr/bin/perl -w ############################################################################## # $Id: autorpm.pl,v 1.182 2005/04/17 14:34:14 kirk Exp $ ############################################################################## # Created by Kirk Bauer # http://www.autorpm.org # # Please send all questions to the AutoRPM mailing list: # autorpm@autorpm.org # # Please send bug reports and patches to: # autorpm-patches@autorpm.org # autorpm-bugs@autorpm.org # # Most current version can always be found at: # ftp://ftp.autorpm.org/pub/redhat/RPMS ############################################################################### # Little kludge to make perl-libnet RPM for RHL5.X work under RHL6.0 BEGIN { unshift (@INC, "/usr/lib/perl5/site_perl"); } use Getopt::Long; use Net::FTP; use File::Copy; use Term::ReadLine; use Symbol; use integer; use strict; ############################################################################## # Variables ############################################################################## # Default location of PID file my $PIDFile = '/var/run/autorpm.pid'; # Debug mode? my $Debug = 0; # $Interactive tells us if we are interacting with a user my $Interactive = 0; # If set, always print to the screen my $ForcePrint = 0; # If set, prints actions to screen my $ReportCommand = 1; # Default base directory for config files my $BaseDir = '/etc/autorpm.d'; # Default "spool" directory my $TempDir = '/var/spool/autorpm/'; # Default FTP pool definition directory my $PoolDir = Check_Dir ("$BaseDir/pools"); # Default GPG key path (used if no environment var set) my $GPGPath = "$BaseDir/keys"; # This system's hostname my $HostName = `hostname`; chomp ($HostName); # Delay before starting auto command my $StartDelay = 0; # Maximum number of history entries my $MaxHistory = 5000; my $MailProg = '/bin/mail'; if (-x '/usr/bin/mailx') { $MailProg = '/usr/bin/mailx'; } my $Version = '3.3.3'; my $VDate = '04/17/05'; umask(0022); # Default settings here my %Settings = ( 'tips' => 'on', 'color' => 'on', 'ftp_passive_mode' => 'on', 'ftp_hash' => 'off', 'ftp_hash_size' => '8192', 'show_host' => 'full', 'show_rc' => 'on', 'expand_queue_entries' => 'on', 'run_system_cmds' => 'off', 'debug' => 'off', 'rpm_location' => '/bin/rpm', 'rpm_install_opt' => '', 'show_auto_ignore' => 'off', 'interactive_deps' => 'on', ); # Don't wait for carriage return to display STDOUT... $| = 1; # Other global variables my (%AutoIgnore, %LocalFiles); my (%RemoteSizes); my (%Archs, %FTPPools, $FTP_Object); my ($FTPFirewall, $FTPPort, $DoingCache); my ($FTPTimeOut); my $AutoQueueReport = 'PRINT'; my $FirstBlock = 1; my $FTPRetries = 2; my $FTPRetryMinDelay = 5; my $FTPRetryMaxDelay = 600; my $DontReportInteractive = 0; my $VersionCheckMode = 0; my (%InterHash, %AutoHash, %AcceptList, %RejectList, %AcceptRegexList, %RejectRegexList); my ($InterData, $Auto) = (\%InterHash, \%AutoHash); my $Recursing = 0; # Set when we recurse on a remote source my $DeleteOption = 0; my $DeleteOptionArgs = ''; my $ReallyDeleteOption = 0; my $RemoveExclude = 0; my %AutoIgnored; # For backwards compatibility $ENV{'Lang'} = 'en'; # Colors my %Colors; my %AllColors = ( 'Normal' => "\033[0m", 'BrightRed' => "\033[1;31m", 'Red' => "\033[0;31m", 'BrightGreen' => "\033[1;32m", 'Green' => "\033[0;32m", 'Yellow' => "\033[1;33m", 'Brown' => "\033[0;33m", 'BrightBlue' => "\033[1;34m", 'Blue' => "\033[0;34m", 'BrightMagenta' => "\033[1;35m", 'Magenta' => "\033[0;35m", 'BrightCyan' => "\033[1;36m", 'Cyan' => "\033[0;36m", 'BrightWhite' => "\033[1;37m", 'White' => "\033[0;37m", ); ############################################################################## # Small functions ############################################################################## # Sets permissions on file sub TouchFile ($$) { # Filename my $File = $_[0]; # Set to 1 to overwrite my $Write = $_[1]; if (! -f $File) { if ($Write) { # If we are going to write to the file, unlink # whatever might be there in its place (i.e. symlink?) unlink ($File); } # Create file system ("touch $File >/dev/null 2>&1"); } # Set ownership and permissions chmod 0600, $File; chown 0, 0, $File; } sub Usage { print "\nUsage: $0 [--version] [--debug] [--notty] [cmd1 [cmd2 ...]]\n"; print " --notty Makes sure AutoRPM knows it is running from\n"; print " a script (such as a cronjob)\n"; print " --debug Activates very verbose debugging\n"; print " --help Displays this help message.\n"; print " --version Displays the version of AutoRPM.\n"; print " --pidfile X Uses X for the PID file (default: $PIDFile)\n"; print " --tempdir X Uses X for the temporary directory (default: $TempDir)\n"; print "\nIf no commands are specified, AutoRPM will enter interactive mode\n\n"; print "Any of the interactive commands can be specified on the command line,\n"; print " AutoRPM will execute the command(s) and exit\n"; print " ex: $0 \"install updates\" \"remove all\"\n"; print "For a list of commands, run $0 help (not --help)\n"; print "For information about a command, run:\n"; print " $0 \"help command\"\n"; print "\n"; exit (255); } sub Check_Dir { my $ThisDir = $_[0]; # Add / to $ThisDir unless ($ThisDir =~ m=/$=) { $ThisDir = $ThisDir . '/'; } # Create directory if necessary unless (-d $ThisDir) { #mkdir ($ThisDir,0770) or die "ERROR: Can't create directory " . $ThisDir . "\n"; mkdir ($ThisDir,0770); Report ("\nMaking directory: " . $ThisDir . "\n"); } return ($ThisDir); } ############################################################################## # RPM Version comparison code ############################################################################## # rpm_split_name - split an RPM name into the base name and the version # Arguments: # $_[0] RPM file name (with or without directory prefix) # Returns: undef on error # ref to hash on success # $ret->{'Name'}: RPM name # $ret->{'Version'}: RPM version/release string # $ret->{'Arch'}: Architecture sub RPM_Split_Name($) { my %ret; $ret{'Orig'} = $_[0]; chomp($_[0]); return undef unless ($_[0] and ($_[0] =~ /\.rpm$/)); if ($_[0] =~ m#(.*/)?([^/]+)-([^-]+-[^-]+)\.([^.]+|hppa\d\.\d)\.rpm$#) { $ret{'Name'} = $2; $ret{'Version'} = $3; $ret{'Arch'} = $4; return (\%ret); } if (-e $_[0]) { $Debug and Report("DEBUG: RPM_Split_Name: Running RPM on: $_[0]\n"); my $real_name = `$Settings{'rpm_location'} -q --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm\n' -p $_[0] 2>/dev/null`; if ($real_name =~ m#(.*/)?([^/]+)-([^-]+-[^-]+)\.([^.]+|hppa\d\.\d)\.rpm$#) { $ret{'Name'} = $2; $ret{'Version'} = $3; $ret{'Arch'} = $4; return (\%ret); } } # Fail back to doing an FTP request through RPM (slow!) # (make sure it is at least an RPM file) if ($_[0] =~ /^ftp:\/\/.*\.rpm$/) { $Debug and Report("DEBUG: RPM_Split_Name: Running RPM on: $_[0]\n"); my $real_name = `$Settings{'rpm_location'} -q --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm\n' -p $_[0] 2>/dev/null`; if ($real_name =~ m#(.*/)?([^/]+)-([^-]+-[^-]+)\.([^.]+|hppa\d\.\d)\.rpm$#) { $ret{'Name'} = $2; $ret{'Version'} = $3; $ret{'Arch'} = $4; return (\%ret); } } # Nope, give up on this one... Inform ("Bad RPM name: $_[0] - skipping it" . "\n"); return undef; } sub TrimPath ($) { my $Temp = $_[0]; $Temp and $Temp =~ s#(.*/)?([^/]+-[^-]+-[^-]*\.([^.]+|hppa\d\.\d)\.rpm)$#$2#; return ($Temp); } sub BaseName ($) { my $Temp = $_[0]; $Temp and $Temp =~ s#(.*/)?([^/]+-[^-]+-[^-]*)\.([^.]+|hppa\d\.\d)\.rpm$#$2#; return ($Temp); } sub GetVersion ($) { my $Temp = $_[0]; $Temp and $Temp =~ s#(.*/)?[^/]+-([^-]+-[^-]*)\.([^.]+|hppa\d\.\d)\.rpm$#$2#; return ($Temp); } # This version comparison code was sent in by Robert Mitchell and, although # not yet perfect, is better than the original one I had. He took the code # from freshrpms and did some mods to it. Further mods by Simon Liddington # . # # Splits string into minors on . and change from numeric to non-numeric # characters. Minors are compared from the beginning of the string. If the # minors are both numeric then they are numerically compared. If both minors # are non-numeric and a single character they are alphabetically compared, if # they are not a single character they are checked to be the same if the are not # the result is unknown (currently we say the first is newer so that we have # a choice to upgrade). If one minor is numeric and one non-numeric then the # numeric one is newer as it has a longer version string. # We also assume that (for example) .15 is equivalent to 0.15 sub cmp_vers_part($$) { my($va, $vb) = @_; my(@va_dots, @vb_dots); my($a, $b); my($i); if ($vb !~ /^pre/ and $va =~ s/^pre(\d+.*)$/$1/) { if ($va eq $vb) { return -1; } } elsif ($va !~ /^pre/ and $vb =~ s/^pre(\d+.*)$/$1/) { if ($va eq $vb) { return 1; } } @va_dots = split(/\./, $va); @vb_dots = split(/\./, $vb); $a = shift(@va_dots); $b = shift(@vb_dots); # We also assume that (for example) .15 is equivalent to 0.15 if ($a eq '' && $va ne '') { $a = "0"; } if ($b eq '' && $vb ne '') { $b = "0"; } while ((defined($a) && $a ne '') || (defined($b) && $b ne '')) { # compare each minor from left to right if ((not defined($a)) || ($a eq '')) { return -1; } # the longer version is newer if ((not defined($b)) || ($b eq '')) { return 1; } if ($a =~ /^\d+$/ && $b =~ /^\d+$/) { # numeric compare # Frank Steiner, 08/29/2005 # The new algorithm fixes the following problems: # - 1.00 was considered a newer package than 1.0. rpm itself considers # 0 = 00 = 000... # - rpm considers 5.010 newer that 5.09. Autorpm does the reverse. # - numbers larger than 2^31 can't be handled. Such numbers can easily # show up if a version/release is made of a date+time string and a release # number. E.g., the libxml2 package in SLES9 has the release # 200412202113, i.e., sources from 2004-12-20, 21:13. Comparing # two integers that need more than 32bit always returns 0, so no update # occurs. We could use vectors, but with the two fixes above, the # comparison can be done easily with string compare. # # The new algorithm. If we used 64bit integers, 4 and 5 could be replaced # by a integer compare but since 1)-3) were still special cases, it seems # easier to handle 4) and 5) with string comparison, too. Since we don't # use 64bit integers, it is important to avoid any integer operator! Only # use string operator # # 1) First check if one or both strings are equivalent to zero. That can be # 0 or 00 or 000 etc. # The following is much easier if both strings are not equivalent to 0. # 2) If both strings have leading zeroes, remove the common number of 0. # That's because rpm itself considers 5.09 to be an older version than # 5.010. The remaining strings can't be empty or zero due to 1). # 3) After this removal, a string with a leading 0 is always smaller. # That's because rpm considers 4.6 newer than 4.08 (Kirks remark)) # 4) If no string has a leading 0, the shorter one is smaller. # That's because rpm considers release 10 newer than release 9. # 5) If the strings have the same length, do a string compare. # 1): a and/or b are zero (one or more digits "0"). if ($a =~ /^0+$/) { if ($b =~ /^0+$/) { goto NEXT; } # if both are zero, continue with the rest else { return -1; } } if ($b =~ /^0+$/) { return 1; } # 2) a and b are not zero. Remove leading 0 while ($a =~ /^0/ and $b =~ /^0/) { $a = substr($a,1); $b = substr($b,1); } # 3) now a leading 0 indicates the smaller number if ($a =~ /^0/) { return -1; } if ($b =~ /^0/) { return 1; } # 4) shorter string is smaller my $l_a = length($a); my $l_b = length($b); if ($l_a < $l_b) { return -1; } elsif ($l_a > $l_b) { return 1; } # 5) equal length => string compare. elsif ($a ne $b) {return ($a cmp $b);} } elsif ($a =~ /^\D+$/ && $b =~ /^\D+$/) { # string compare if (length($a) == 1 && length($b) == 1) { # only minors with one letter seem to be useful for versioning if ($a ne $b) { return $a cmp $b; } } elsif (($a cmp $b) != 0) { # otherwise we should at least check they are the same and if not say unknown # say newer for now so at least we get choice whether to upgrade or not return -1; } } elsif ( ($a =~ /^\D+$/ && $b =~ /^\d+$/) || ($a =~ /^\d+$/ && $b =~ /^\D+$/) ) { # if we get a number in one and a word in another the one with a number # has a longer version string if ($a =~ /^\d+$/) { return 1; } if ($b =~ /^\d+$/) { return -1; } } else { # minor needs splitting $a =~ /\d+/ || $a =~ /\D+/; # split the $a minor into numbers and non-numbers my @va_bits = ($`, $&, $'); $b =~ /\d+/ || $b =~ /\D+/; # split the $b minor into numbers and non-numbers my @vb_bits = ($`, $&, $'); for ( my $j=2; $j >= 0; $j--) { if ($va_bits[$j] ne '') { unshift(@va_dots,$va_bits[$j]); } if ($vb_bits[$j] ne '') { unshift(@vb_dots,$vb_bits[$j]); } } } NEXT: $a = shift(@va_dots); $b = shift(@vb_dots); } return 0; } # RPM_Compare_Arch - compare RPM architecture strings # Arguments: # $_[0] Result of version comparison # $_[1] arch string of "a" # $_[2] arch string of "b" # $_[3] Which one can lose to a newer version, but worse # arch (1/2, 0 to disable) # Returns: "a" <=> "b" # -1 = a < b, 0 = a==b, 1 = a > b sub RPM_Compare_Arch ($$$$) { my ($ver_res, $Arch1, $Arch2, $Reduce) = @_; if ($Arch1 eq $Arch2) { # If archs are the same, we don't need to do more return ($ver_res); } LoadArchs(); unless ($Archs{$Arch1} or $Archs{$Arch2}) { # Neither arch is known, so use version compare result return ($ver_res); } unless ($Archs{$Arch1}) { # Arch1 is not defined, so say package 2 is better return (-1); } unless ($Archs{$Arch2}) { # Arch2 is not defined, so say package 1 is better return (1); } if ($ver_res == 0) { # Versions were the same... do only an arch compare return($Archs{$Arch1} <=> $Archs{$Arch2}); } elsif ($ver_res == -1) { # Package 2 has newer version than Pkg 1 if ($Archs{$Arch2} > $Archs{$Arch1}) { # This is easy.. pkg2 arch and version are both better/newer return (-1); } else { # Now, pkg2 has a newer version, but pkg1 has a better arch if ($Reduce == 1) { # Package 1 can lose to a newer version, but worse arch return (-1); } else { return (1); } } } else { # Package 1 has newer version than Pkg 2 if ($Archs{$Arch1} > $Archs{$Arch2}) { # This is easy.. pkg1 arch and version are both better/newer return (1); } else { # Now, pkg1 has a newer version, but pkg2 has a better arch if ($Reduce == 2) { # Package 2 can lose to a newer version, but worse arch return (1); } else { return (-1); } } } } # RPM_Compare_Version - compare RPM version strings # Arguments: # $_[0] pkg A ref # $_[1] pkg B ref # $_[2] Which one can lose to a newer version, but worse # arch (1/2, 0 to disable) # Returns: "a" <=> "b" # -1 = a < b, 0 = a==b, 1 = a > b sub RPM_Compare_Version ($$$) { my $Result; my ($Version1,$Release1) = split (/-/,$_[0]->{'Version'}); my ($Version2,$Release2) = split (/-/,$_[1]->{'Version'}); my ($Arch1, $Arch2, $Reduce) = ($_[0]->{'Arch'}, $_[1]->{'Arch'}, $_[2]); if ($VersionCheckMode) { print "Version 1: $Version1\n"; print "Release 1: $Release1\n"; print "Version 2: $Version2\n"; print "Release 2: $Release2\n"; } $Result = cmp_vers_part($Version1,$Version2); if ($Result) { return (RPM_Compare_Arch($Result, $Arch1, $Arch2, $Reduce)); } return (RPM_Compare_Arch(cmp_vers_part($Release1,$Release2), $Arch1, $Arch2, $Reduce)); } ############################################################################## # Process_Local ############################################################################## sub GetInstalledRPMs { my @ret; while (1) { eval { local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required alarm 120; if ($Interactive) { print "Loading installed RPM list... "; } @ret = `$Settings{'rpm_location'} -qa --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm\n'`; die ('ERROR: Error executing ' . $Settings{'rpm_location'} . "\n") unless ($? == 0); alarm 0; }; if ($@) { die unless $@ eq "alarm\n"; # propagate unexpected errors # timed out # NOTE: I have had *numerous* problems with this hanging on RH7.2 (rpm-4.0.3-1.03) # rpm --rebuilddb seems to fix it Report ("\nERROR: rpm -qa is hanging! Running rpm --rebuilddb to fix... "); @ret = `$Settings{'rpm_location'} --rebuilddb`; die ('ERROR: Error executing ' . $Settings{'rpm_location'} . "\n") unless ($? == 0); # --rebuilddb seems to mess the permissions of the files up system("chmod a+r /var/lib/rpm/*"); Report ("Done!\n\n"); } else { # success chomp(@ret); if ($Interactive) { print "Done.\n"; } return(@ret); } } return (); } sub AddPackageToHash ($$) { my ($hash, $pkg) = @_; return unless $pkg; if (($hash->{$pkg->{'Name'}}) and ($#{$hash->{$pkg->{'Name'}}} >= 0)) { # There are other local matches besides this one... for (my $i = 0; $i <= $#{$hash->{$pkg->{'Name'}}}; $i++) { my $this = $hash->{$pkg->{'Name'}}->[$i]; if (RPM_Compare_Version($pkg, $this, 0) >= 0) { # This is newer than or equal to the current entry in the hash... # So, add the new entry to the hash before this entry for (my $j = $#{$hash->{$pkg->{'Name'}}}; $j >= $i; $j--) { $hash->{$pkg->{'Name'}}->[$j+1] = $hash->{$pkg->{'Name'}}->[$j]; } $hash->{$pkg->{'Name'}}->[$i] = $pkg; return; } } } # If we are here, we need to stick it it the end of the list (either it is the oldest, # or it is the only version) push @{$hash->{$pkg->{'Name'}}}, $pkg; } sub AddFileToHash ($$) { my ($hash, $entry) = @_; my $AllArch = $hash->{'SETTINGS'}->{'AllArch'} ? $hash->{'SETTINGS'}->{'AllArch'} : 0; my ($pkg); if ($pkg = RPM_Split_Name($entry)) { AddPackageToHash($hash, $pkg); return $pkg; } else { Inform ("Warning: Tried to process corrupt local RPM: $entry\n"); return undef; } } # For each package in @files, place in either @{$new} or @{$old}. # Pass in undef for old or new to ignore old or new packages, respectively sub SortRPMs ($$@) { my ($hash, $AllArch, @files) = @_; my $ArchSearch; if ($Interactive) { print "Processing and sorting... "; } $hash->{'SETTINGS'}->{'AllArch'} = $AllArch; foreach (@files) { AddFileToHash($hash, $_); } if ($Interactive) { print "Done.\n"; } } # Loads list of compatible architectures sub LoadArchs () { unless (keys %Archs) { my $count = 1000; my $line; foreach $line (`$Settings{'rpm_location'} --showrc 2>&1`) { if ($line =~ s/^compatible archs\s*://) { while ($line =~ s/^\s+([^ ]+)//) { $Archs{$1} = $count--; } } } } } # Process_Local gets a list of local files ready sub Process_Local ($) { my ($Data) = @_; $Data->{'LocalSource'} = ''; # keeps an ID for local source caching unless (keys %AutoIgnore) { Read_Auto_Ignore(); } if ($Data->{'compare_to_installed'}) { print STDERR "Warning: deprecated command Compare_To_Installed()\n (always compares to installed when no Compare_To_Dir() is given.\n"; } if ( ( ( not $Data->{'compare_to_dir'} ) and (not $Data->{'recursive_compare_to_dir'} ) ) ) { # We are comparing to the installed RPMs. $Data->{'LocalSource'} = 'installed'; Inform ("Comparing to locally installed RPMs\n"); unless ($LocalFiles{$Data->{'LocalSource'}}) { SortRPMs(\%{$LocalFiles{$Data->{'LocalSource'}}}, 0, GetInstalledRPMs()); } } if ( ($Data->{'compare_to_dir'} ) or ($Data->{'recursive_compare_to_dir'} ) ) { # We are comparing to one or more local directories $Data->{'LocalSource'} = ''; my $ThisDir; foreach $ThisDir ( @{$Data->{'compare_to_dir'}}, @{$Data->{'recursive_compare_to_dir'}} ) { unless ($ThisDir =~ /\/$/) { $ThisDir .= '/'; } Inform ("\nComparing to directory: " . $ThisDir . "\n"); $Data->{'LocalSource'} .= $ThisDir; } my %LocalSizes; unless ($LocalFiles{$Data->{'LocalSource'}}) { my @TempLocalFiles = (); foreach $ThisDir ( @{$Data->{'compare_to_dir'}}, @{$Data->{'recursive_compare_to_dir'}} ) { # Add / to end of directory unless ($ThisDir =~ m=/$=) { $ThisDir = $ThisDir . '/'; } # Create Directory... unless (-d $ThisDir) { mkdir ($ThisDir,0775) or die "ERROR: Can't create directory $ThisDir: $!\n"; Report("\nMaking directory: " . $ThisDir . "\n"); } opendir (LOCALDIR,$ThisDir) or die "ERROR: Can't open local directory $ThisDir: $!\n"; my $ThisFile; while (defined($ThisFile = readdir(LOCALDIR))) { unless (-d $ThisDir . $ThisFile) { if ($ThisFile =~ m/\.rpm$/) { push @TempLocalFiles, $ThisDir . $ThisFile; $LocalSizes{$ThisDir . $ThisFile} = (-s $ThisDir . $ThisFile); } } } closedir(LOCALDIR); } my $AllArch = ($Data->{'all_arch'} and ($Data->{'all_arch'}->[0] == 1)); SortRPMs(\%{$LocalFiles{$Data->{'LocalSource'}}}, $AllArch, @TempLocalFiles); if (keys %LocalSizes) { # Store sizes... foreach (keys %{$LocalFiles{$Data->{'LocalSource'}}}) { if ((ref $_) eq 'ARRAY') { if ($LocalSizes{$_->{'Orig'}}) { $_->{'Size'} = $LocalSizes{$_->{'Orig'}}; } } } } } } } ############################################################################## # Do_Actions (process the Action Block) ############################################################################## sub AddToLocalCache ($$) { my ($entry, $LocalSource) = @_; if ($LocalFiles{$LocalSource}) { my $pkg = AddFileToHash(\%{$LocalFiles{$LocalSource}}, $entry); $Debug and Report("Added $entry ($pkg->{'Name'}) to local cache $LocalSource\n"); $pkg->{'Checked'} = 1; } } sub DeleteFromLocalCache ($$) { my ($entry, $LocalSource) = @_; if ($LocalFiles{$LocalSource}) { my $pkg = RPM_Split_Name($entry); return 0 unless (ref $pkg); my $array = $LocalFiles{$LocalSource}{$pkg->{'Name'}}; if ($array and ((ref $array) eq 'ARRAY')) { for (my $i = 0; $i <= $#{$array}; $i++) { if ( ($array->[$i]->{'Version'} eq $pkg->{'Version'}) and ($array->[$i]->{'Arch'} eq $pkg->{'Arch'}) ) { $Debug and Report("Deleted $entry from local cache $LocalSource\n"); splice @{$array}, $i, 1; return 1; } } } } return 0; } sub CopyLocalFile ($$) { my ($Data, $LocalFile) = @_; return 0 unless ($LocalFile); if ($Data->{'copy_old_versions'} and ($Data->{'copy_old_versions'}->[0]) ) { my $DestDir = $Data->{'copy_old_versions'}->[0]; if (copy($LocalFile, $DestDir)) { if ($ReportCommand) { Report(" Copied old local version: $LocalFile to $DestDir\n"); } return 1; } else { # Error on copy... return 0; } } else { # Return 1 as there was n oerror return 1; } } # The placement of 'delete_old_version' in the 'updated' # block is deprecated, but it still works for backwards # compatibility (now it goes in the source block) sub DeleteLocalFile ($$) { my ($Data, $LocalFile) = @_; return unless ($LocalFile); unless ($Data->{'updated'}->{'delete_old_version'} and ($Data->{'updated'}->{'delete_old_version'}->[0] == 1) ) { unless ($Data->{'delete_old_versions'} and ($Data->{'delete_old_versions'}->[0] == 1) ) { return 0; } } if (unlink($LocalFile)) { LogAction("store.log", "Removed $LocalFile"); if ($ReportCommand) { Report(' Deleted old local version: ' . $LocalFile . "\n"); } return DeleteFromLocalCache($LocalFile, $Data->{'LocalSource'}); } else { print STDERR "Couldn't remove " . $LocalFile . "!\n"; } return 0; } sub DoStoreCommand ($$$$) { my ($Data, $SourceFile, $dir, $LocalFile) = @_; # Create Directory... unless (-d $dir) { if ($ReportCommand) { mkdir ($dir, 0775) or die "ERROR: Can't create directory $dir: $!\n"; } Report("\nMaking directory: $dir\n\n"); } unless ( $dir =~ m=/$= ) { $dir .= '/'; } if ($ReportCommand) { Report(" Storing $SourceFile\n into $dir... "); } if ($a = VFS_Store($Data, $SourceFile, $dir)) { if ($ReportCommand) { if ($a == 3) { Report("Resumed Transfer Done.\n"); } elsif ($a == 2) { Report("Already there.\n"); } else { Report("Done.\n"); } } $SourceFile = TrimPath($SourceFile); AddToLocalCache($SourceFile, $dir); my $QueueFile = $dir . $SourceFile; if ($LocalFile) { LogAction("store.log", "Stored $QueueFile [upgrade from " . BaseName($LocalFile) . ']'); } else { LogAction("store.log", "Stored $QueueFile"); } # Queue File is now wherever this was just stored... return ($QueueFile); } else { Report("Error.\n"); print STDERR "Couldn't place $SourceFile into $dir!\n"; return('GET_ERROR'); } } sub ExecuteTrigger($$$$) { Inform(" Executing trigger $_[0]...\n"); system("$_[0] '$_[1]' '$_[2]' '$_[3]'"); } sub Do_Actions($$$$$) { my ($Data, $ActionType, $SourceFile, $LocalFile, $AutoIgnoreString) = @_; $Debug and Report("DEBUG: Do_Actions called: type=$ActionType, source=$SourceFile, local=$LocalFile\n"); my ($Msg,$Delete,$QueueFile,@CopyTo,@Triggers,$bn,$ForceInstallChange, $line, $file, $IgnoreOnFailure, $a, @CurrQueueFile); if ( $LocalFile =~ m/^\// ) { $Msg = ' local file '; } else { $Msg = ' installed RPM '; } unless ( keys %{$Data->{$ActionType}} ) { if ( $ActionType eq 'new' ) { Report( 'Source File ' . $SourceFile . " is brand new (no action defined).\n" ); } elsif ( $ActionType eq 'updated' ) { Report( 'Source File ' . $SourceFile . ' is newer than' . $Msg . $LocalFile . " (no action defined).\n"); } return; } $ReportCommand = 1; if ( $Data->{$ActionType}->{'report'} and ( $Data->{$ActionType}->{'report'}->[0] == 0 ) ) { $ReportCommand = 0; } if ($Debug) { $ReportCommand = 1; } $ForceInstallChange = 0; # This next block used for both pgp_require and install if ($Data->{'FTP'}) { $QueueFile = $SourceFile; $QueueFile =~ s=^.*/([^/]+)$=$1=; $QueueFile = $TempDir . $QueueFile; } else { $QueueFile = $SourceFile; } $bn = BaseName($QueueFile); if ($Data->{$ActionType}->{'recursive_store'}) { for my $i ( 0 .. $#{$Data->{$ActionType}->{'recursive_store'}} ) { $QueueFile = DoStoreCommand($Data, $SourceFile, $Data->{$ActionType}->{'recursive_store'}->[$i], $LocalFile); } } if ($Data->{$ActionType}->{'store'}) { for my $i ( 0 .. $#{$Data->{$ActionType}->{'store'}} ) { $QueueFile = DoStoreCommand($Data, $SourceFile, $Data->{$ActionType}->{'store'}->[$i], $LocalFile); } } if ($QueueFile eq 'GET_ERROR') { next; } if ($ReportCommand) { if ($ActionType eq 'new') { Report('-> ' . BaseName($SourceFile) . " is a new RPM and could be installed.\n"); } elsif ($ActionType eq 'same') { Report(' ' . BaseName($SourceFile) . " is the same as the local RPM.\n"); } elsif ($ActionType eq 'old') { Report(' ' . BaseName($SourceFile) . " is older than the local RPM.\n"); } else { Report('-> ' . BaseName($SourceFile) . " is an updated RPM and could be upgraded.\n"); } } @Triggers = (); if ( $Data->{$ActionType}->{'regex_trigger'} ) { foreach my $trigger ( @{$Data->{$ActionType}->{'regex_trigger'}} ) { # Going through each regex_trigger command my $regex = $trigger->[0]; if ($bn =~ m/$regex/) { push @Triggers, $trigger->[1]; } } } if ( $Data->{$ActionType}->{'trigger'} ) { if (my $pkg = RPM_Split_Name($SourceFile)) { foreach my $trigger ( @{$Data->{$ActionType}->{'trigger'}} ) { # Going through each trigger command if (($trigger->[0]) eq ($pkg->{'Name'})) { push @Triggers, $trigger->[1]; } } } } if ( ($ActionType eq 'old') and ($Data->{$ActionType}->{'downgrade'}) and ($Data->{$ActionType}->{'downgrade'}->[0] == 1 ) ) { # Downgrade the package Get_Remote_File($SourceFile, $QueueFile); Report(" ** Downgrading to $bn ** \n"); foreach (`$Settings{'rpm_location'} -U --oldpackage --nodeps $QueueFile 2>&1`) { Report($_); } # Execute any triggers foreach my $trigger (@Triggers) { my $Temp2 = BaseName($LocalFile); ExecuteTrigger($trigger, $bn, 'DOWNGRADED', $Temp2); } } if (($Data->{$ActionType}->{'pgp_require'}) and ($Data->{$ActionType}->{'pgp_require'}->[0] != 0) ) { # Only applies for Auto-Installs or Interactive-Installs... unless ($ENV{'GNUPGHOME'}) { # Set path if necessary $ENV{'GNUPGHOME'} = $GPGPath; } unless ($Data->{$ActionType}->{'pgp_fail_install'} ) { $Data->{$ActionType}->{'pgp_fail_install'}->[0] = 1; } Get_Remote_File($SourceFile, $QueueFile); Inform (' Checking PGP Signature for ' . $bn . '... '); my @Output = `$Settings{'rpm_location'} --checksig $QueueFile 2>&1`; foreach my $i (@Output) { if (($i =~ /pgp/) or ($i =~ /gpg/)) { Inform ("Good Signature.\n"); } else { $Debug and Report("DEBUG: sigcheck returned line: $i"); Report("\n PGP Check Failed for " . $bn . ", switching from Auto to Interactive Install mode.\n"); $ForceInstallChange = 1; last; } } if ( ($ForceInstallChange) and ($Data->{$ActionType}->{'pgp_fail_install'}->[0] == 0) and ($QueueFile)) { Report(' ' . $bn . " won't be installed, deleting file cached from FTP site.\n"); } } if ($ActionType eq 'updated') { if (CopyLocalFile($Data, $LocalFile)) { DeleteLocalFile($Data, $LocalFile); } } if ( ($Data->{$ActionType}->{'install'}) and ($Data->{$ActionType}->{'install'}->[0]) and ($Data->{$ActionType}->{'install'}->[0] != 0) and ( ($ActionType eq 'new') or ($ActionType eq 'updated') ) and ( not ( ($ForceInstallChange) and ($Data->{$ActionType}->{'pgp_fail_install'}->[0] == 0) ))) { if (my $out = RPM_Split_Name($SourceFile)) { $Delete = 0; $IgnoreOnFailure = ''; @CopyTo = (); if ( ( $Data->{$ActionType}->{'delete_after_install'} ) and ($Data->{$ActionType}->{'delete_after_install'}->[0] == 1) ) { $Delete = 2; } if (($Data->{'FTP'}) and ( not ( ($Data->{$ActionType}->{'delete_after_install'}) and ( $Data->{$ActionType}->{'delete_after_install'}->[0] == 0 )) ) ) { $Delete = 1; } if ( ( $Data->{$ActionType}->{'ignore_on_failure'} ) and ($Data->{$ActionType}->{'ignore_on_failure'}->[0] == 1) ) { $IgnoreOnFailure = '*'; } if ( $Data->{$ActionType}->{'copy_after_install'} ) { foreach my $i ( @{$Data->{$ActionType}->{'copy_after_install'}} ) { push @CopyTo, $i; } } if ( $Data->{$ActionType}->{'recursive_copy_after_install'} ) { foreach my $i ( @{$Data->{$ActionType}->{'recursive_copy_after_install'}} ) { push @CopyTo, $i; } } if ( $Data->{$ActionType}->{'copy_on_failure'} ) { foreach my $i ( @{$Data->{$ActionType}->{'copy_on_failure'}} ) { push @CopyTo, "FAIL$i"; } } if ( $Data->{$ActionType}->{'recursive_copy_on_failure'} ) { foreach my $i ( @{$Data->{$ActionType}->{'recursive_copy_on_failure'}} ) { push @CopyTo, "FAIL$i"; } } $out->{'File'} = $QueueFile; $out->{'Delete'} = $Delete; $out->{'Local'} = $LocalFile; @{$out->{'CopyTo'}} = @CopyTo; @{$out->{'Triggers'}} = @Triggers; $out->{'IgnoreOnFailure'} = $IgnoreOnFailure; $out->{'AutoIgnoreString'} = $AutoIgnoreString; $out->{'Size'} = $RemoteSizes{$SourceFile}; if ( $Data->{$ActionType}->{'auto_follow_deps'} ) { $out->{'follow_deps'} = $Data->{$ActionType}->{'auto_follow_deps'}->[0]; } if ($Data->{$ActionType}->{'return_value'}) { return ($out); } elsif (($Data->{$ActionType}->{'install'}->[0] == 1) or ( ($ForceInstallChange) and ($Data->{$ActionType}->{'pgp_fail_install'}->[0] == 1) ) ) { AddPackageToHash($InterData, $out); } else { AddPackageToHash($Auto, $out); } } } } ############################################################################## # Process_Remote and Process_RPM_File ############################################################################## sub Process_Remote { my ($Data) = @_; my (@DirList,$ThisDir,$ThisRegex,$Okay,$SubDir,$Temp,$ThisAction,$MySourceLocation, $ThisFile); $Data->{'RetriesLeft'} = $FTPRetries; VFS_Open($Data); my @RemoteFiles = (); foreach $ThisFile (VFS_Read($Data)) { if ($ThisFile eq 'NODIR') { $ThisFile = ""; # The source is an FTP pool, but the site we are currently connected to # isn't allowing us into the directory for whatever reason... # The VFS_Read has lowered that site's score by quite a bit... but now # we are going to call VFS_Open again to get a new site. return if ($Recursing); $Data->{'RetriesLeft'}--; VFS_Open($Data); } if ( $ThisFile =~ m=/$= ) { # The file is a directory... unless ( ($ThisFile =~ m=\./$=) or ($ThisFile =~ m=\.\./$=) ) { push @DirList, $ThisFile; } } elsif ($ThisFile =~ /\.rpm$/ ) { if ( $Data->{'only_newest'} and (not $Data->{'only_newest'}->[0]) ){ # Process the file now Process_RPM_File ($Data, $ThisFile); } else { # Only process the newest remote file, so save for sorting below push (@RemoteFiles, $ThisFile); } } } if (@RemoteFiles) { # Eliminate old remote files for which there is a newer remote version my %new_remote; my $AllArch = ($Data->{'all_arch'} and ($Data->{'all_arch'}->[0] == 1)); SortRPMs(\%new_remote, $AllArch, @RemoteFiles); foreach my $pkg (values %new_remote) { if (((ref $pkg) eq 'ARRAY') and ($pkg->[0]->{'Orig'})) { # Okay, now we have a "Remote" RPM... lets figure out if we care about it if ($AllArch) { foreach (@{$pkg}) { if ($pkg->[0]->{'Version'} eq $_->{'Version'}) { Process_RPM_File ($Data, $_->{'Orig'}); } } } else { Process_RPM_File ($Data, $pkg->[0]->{'Orig'}); } } } } # Process recursion here... if ( (@DirList) and ( $Data->{'recursive'} ) and ( $Data->{'recursive'}->[0] = 1) ) { unless ($Data->{'SourceLocation'} =~ m=/$=) { $Data->{'SourceLocation'} .= '/'; } foreach $ThisDir (@DirList) { $Okay = 1; $SubDir = $ThisDir; $MySourceLocation = $Data->{'SourceLocation'}; if ($Data->{'FTP'}) { $MySourceLocation =~ s=^ftp://[^/]+==; } $SubDir =~ s=^\Q$MySourceLocation\E==; if ( $Data->{'regex_dir_ignore'} ) { # Apply Regex_Dir_Ignores... foreach $ThisRegex (@{$Data->{'regex_dir_ignore'}}) { if ( $SubDir =~ m/$ThisRegex/ ) { $Okay = 0; } } } if (( $Data->{'regex_dir_accept'} ) and ( $Okay ) ) { # Apply Regex_Dir_Accepts only if dir hasn't been denied already... # By specifying any Regex_Dir_Accepts, every dir must match at least # one of them... $Okay = 0; foreach $ThisRegex (@{$Data->{'regex_dir_accept'}}) { Report("DEBUG: Checking [$SubDir] against Regex_Dir_Accept [$ThisRegex]\n") if $Debug; if ( $SubDir =~ m/$ThisRegex/ ) { $Okay = 1; } } } if ($Okay) { # Looks like we have to recurse into this directory... $Recursing = 1; $Data->{'SourceLocation'} .= $SubDir; if ( $Data->{'recursive_compare_to_dir'} ) { for $Temp ( 0 .. $#{@{$Data->{'recursive_compare_to_dir'}}} ) { unless ($Data->{'recursive_compare_to_dir'}->[$Temp] =~ m=/$=) { $Data->{'recursive_compare_to_dir'}->[$Temp] .= '/'; } $Data->{'recursive_compare_to_dir'}->[$Temp] .= $SubDir; } } foreach $ThisAction ( 'new', 'updated', 'old', 'same' ) { if ( $Data->{$ThisAction}->{'recursive_store'} ) { for $Temp ( 0 .. $#{@{$Data->{$ThisAction}->{'recursive_store'}}} ) { unless ( $Data->{$ThisAction}->{'recursive_store'}->[$Temp] =~ m=/$= ) { $Data->{$ThisAction}->{'recursive_store'}->[$Temp] .= '/'; } $Data->{$ThisAction}->{'recursive_store'}->[$Temp] .= $SubDir; } } if ( $Data->{$ThisAction}->{'recursive_copy_after_install'} ) { for $Temp ( 0 .. $#{@{$Data->{$ThisAction}->{'recursive_copy_after_install'}}} ) { unless ( $Data->{$ThisAction}->{'recursive_copy_after_install'}->[$Temp] =~ m=/$= ) { $Data->{$ThisAction}->{'recursive_copy_after_install'}->[$Temp] .= '/'; } $Data->{$ThisAction}->{'recursive_copy_after_install'}->[$Temp] .= $SubDir; } } if ( $Data->{$ThisAction}->{'recursive_copy_on_failure'} ) { for $Temp ( 0 .. $#{@{$Data->{$ThisAction}->{'recursive_copy_on_failure'}}} ) { unless ( $Data->{$ThisAction}->{'recursive_copy_on_failure'}->[$Temp] =~ m=/$= ) { $Data->{$ThisAction}->{'recursive_copy_on_failure'}->[$Temp] .= '/'; } $Data->{$ThisAction}->{'recursive_copy_on_failure'}->[$Temp] .= $SubDir; } } } Process_Local($Data); Process_Remote($Data); # Go up a directory on everything now... $Data->{'SourceLocation'} =~ s/\Q$SubDir\E$//; if ( $Data->{'recursive_compare_to_dir'} ) { for $Temp ( 0 .. $#{@{$Data->{'recursive_compare_to_dir'}}} ) { $Data->{'recursive_compare_to_dir'}->[$Temp] =~ s/\Q$SubDir\E$//; } } foreach $ThisAction ( 'new', 'updated', 'old', 'same' ) { if ( $Data->{$ThisAction}->{'recursive_store'} ) { for $Temp ( 0 .. $#{@{$Data->{$ThisAction}->{'recursive_store'}}} ) { $Data->{$ThisAction}->{'recursive_store'}->[$Temp] =~ s/\Q$SubDir\E$//; } } if ( $Data->{$ThisAction}->{'recursive_copy_after_install'} ) { for $Temp ( 0 .. $#{@{$Data->{$ThisAction}->{'recursive_copy_after_install'}}} ) { $Data->{$ThisAction}->{'recursive_copy_after_install'}->[$Temp] =~ s/\Q$SubDir\E$//; } } if ( $Data->{$ThisAction}->{'recursive_copy_on_failure'} ) { for $Temp ( 0 .. $#{@{$Data->{$ThisAction}->{'recursive_copy_on_failure'}}} ) { $Data->{$ThisAction}->{'recursive_copy_on_failure'}->[$Temp] =~ s/\Q$SubDir\E$//; } } } } } } VFS_Close($Data); Inform("\n"); } sub Process_RPM_File($$) { my (@LocalMatches, $Temp, $Result, $SourcePackageName, $SourceVersionString, $SourceArch, $LocalPackageName, $LocalVersionString, $LocalArch, $AutoIgnoreString); my ($Data, $ThisFile) = @_; my $Okay = 1; my $bn = BaseName($ThisFile); if ($Data->{'trigger'}) { foreach (@{$Data->{'trigger'}}) { foreach (@{$_}) { print "Argument: $_\n"; } } } if ($Data->{'regex_ignore'}) { # Apply Regex_Ignores... foreach my $ThisRegex (@{$Data->{'regex_ignore'}}) { if ( $bn =~ m/$ThisRegex/ ) { $Okay = 0; } } } if ($Data->{'regex_accept'} and $Okay) { # Apply Regex_Accepts only if file hasn't been denied already... # By specifying any Regex_Accepts, every file must match at least # one of them... $Okay = 0; foreach my $ThisRegex (@{$Data->{'regex_accept'}}) { Report("DEBUG: Checking [$bn] against Regex_Accept [$ThisRegex]\n") if $Debug; if ( $bn =~ m/$ThisRegex/ ) { Report("DEBUG: $bn matched.\n") if $Debug; $Okay = 1; } } } if ($Data->{'ignore_arch'} and $Okay) { # If the file is still okay, but there are some archs to ignore foreach my $ThisArch (@{$Data->{'ignore_arch'}}) { if ( $ThisFile =~ m/$ThisArch\.rpm$/ ) { $Okay = 0; } } } if ($Data->{'accept_arch'} and $Okay) { # Apply Accept_Archs only if file hasn't been denied already... # By specifying any Accept_Archs, every file must match at least # one of them... $Okay = 0; foreach my $ThisArch (@{$Data->{'accept_arch'}}) { if ( $ThisFile =~ m/$ThisArch\.rpm$/ ) { $Okay = 1; } } } # Determine the source name for the auto-ignore file... if ($Data->{'AutoIgnore'}) { if ($Data->{'FTPPool'}) { $AutoIgnoreString = $ThisFile; $AutoIgnoreString =~ s=^.*/([^/]+)$=$1=; $AutoIgnoreString = "$Data->{'FTPPool'}:$AutoIgnoreString"; } elsif ($Data->{'FTP'}) { $AutoIgnoreString = "$Data->{'CurrFTPSite'}:$ThisFile"; } else { $AutoIgnoreString = "$ThisFile"; } } else { $AutoIgnoreString = ''; } return unless $Okay; # Well, the file looks good so far if we are here... my $Orig = $ThisFile; if ($Data->{'FTP'} && $Data->{'CurrFTPSite'}) { if ( ($Data->{'FTPUser'} ne 'anonymous') and ($Data->{'FTPPasswd'} ne ('AutoRPM@' . $HostName) ) ) { $Orig = "ftp://$Data->{'FTPUser'}\:$Data->{'FTPPasswd'}\@"; $Orig .= "$Data->{'CurrFTPSite'}$ThisFile"; } else { $Orig = "ftp://$Data->{'CurrFTPSite'}$ThisFile"; } } my $remote; unless ($remote = RPM_Split_Name($Orig)) { return; } if (Test_Auto_Ignore($AutoIgnoreString)) { $AutoIgnored{$remote->{'Name'}}++; if ($Interactive) { if ($Settings{'show_auto_ignore'} eq 'on') { print "Ignoring: $bn\n"; } } return; } if ($Data->{'accept_list'}) { my $file = $Data->{'accept_list'}->[0]; unless ($AcceptList{$file}) { # List not loaded yet... open (ACCEPT, $file) or die "Could not open Accept_List $file: $!\n"; my $line; while ($line = ) { chomp($line); $AcceptList{$file}{$line}++; } close (ACCEPT); } unless ($AcceptList{$file}{$remote->{'Name'}}) { # This package is not in the accepted package listing, so ignore $Debug and Report("Ignoring $remote->{'Name'} because it is not listed in Accept_List: $file\n"); return; } } if ($Data->{'accept_regex_list'}) { my $file = $Data->{'accept_regex_list'}->[0]; unless ($AcceptRegexList{$file}) { # List not loaded yet... open (ACCEPT, $file) or die "Could not open Accept_Regex_List $file: $!\n"; my $line; while ($line = ) { chomp($line); push @{$AcceptRegexList{$file}}, $line; } close (ACCEPT); } my $matched = ''; foreach (@{$AcceptRegexList{$file}}) { if ($remote->{'Name'} =~ m/$_/) { $matched = $_; last; } } unless ($matched) { $Debug and Report("Ignoring $remote->{'Name'} because it was not matched by any expressions in Accept_Regex_List: $file\n"); return; } } if ($Data->{'reject_list'}) { my $file = $Data->{'reject_list'}->[0]; unless ($RejectList{$file}) { # List not loaded yet... open (REJECT, $file) or die "Could not open Reject_List $file: $!\n"; my $line; while ($line = ) { chomp($line); $RejectList{$file}{$line}++; } close (REJECT); } if ($RejectList{$file}{$remote->{'Name'}}) { # This package is in the rejected package listing, so ignore $Debug and Report("Ignoring $remote->{'Name'} because it is listed in Reject_List: $file\n"); return; } } if ($Data->{'reject_regex_list'}) { my $file = $Data->{'reject_regex_list'}->[0]; unless ($RejectRegexList{$file}) { # List not loaded yet... open (REJECT, $file) or die "Could not open Reject_Regex_List $file: $!\n"; my $line; while ($line = ) { chomp($line); push @{$RejectRegexList{$file}}, $line; } close (REJECT); } foreach (@{$RejectRegexList{$file}}) { if ($remote->{'Name'} =~ m/$_/) { $Debug and Report("Ignoring $remote->{'Name'} because it was matched by an expression ($_) in Reject_Regex_List: $file\n"); return; } } } # Now, we are ready to find out if the file is older, newer, the # same, or brand-new, and take the appropriate action. my $AllArch = ($Data->{'all_arch'} and ($Data->{'all_arch'}->[0] == 1)); my $locals = $LocalFiles{$Data->{'LocalSource'}}; $Debug and Report("DEBUG: Comparing File $ThisFile ($remote->{'Name'})\n"); # Look for old local versions if ($locals and $locals->{$remote->{'Name'}} and ($#{$locals->{$remote->{'Name'}}} > 0)) { # There are one or more old local versions for (my $i = 1; $i <= $#{$locals->{$remote->{'Name'}}}; $i++) { my $old = $locals->{$remote->{'Name'}}->[$i]; if ($AllArch) { my $newer_same_arch = 0; for (my $j = 0; $j < $i; $j++) { if ($remote->{'Arch'} eq $locals->{$remote->{'Name'}}->[$j]->{'Arch'}) { $newer_same_arch = 1; } } next unless ($newer_same_arch); } if (RPM_Compare_Version($remote, $old, 0) == 1) { if ($remote->{'Arch'} eq $old->{'Arch'}) { if (CopyLocalFile($Data, $old->{'Orig'})) { if (DeleteLocalFile($Data, $old->{'Orig'})) { $i--; } } } } } } my $ret; my $local; if ($locals and $locals->{$remote->{'Name'}}) { # Now, look for newest local version if ($AllArch) { foreach (@{$locals->{$remote->{'Name'}}}) { if ($_->{'Arch'} eq $remote->{'Arch'}) { $local = $_; last; } } } else { $local = $locals->{$remote->{'Name'}}->[0]; } } if ($local) { $local->{'Checked'} = 1; $Debug and Report(" DEBUG: Local Match: $local->{'Name'}-$local->{'Version'}\n"); my $Reduce = ( $Data->{'reduce_arch'} and ($Data->{'reduce_arch'}->[0]) ) ? 2 : 0; my $Result = RPM_Compare_Version($remote, $local, $Reduce); if ($Result == -1) { $ret = Do_Actions($Data, 'old', $Orig, $local->{'Orig'}, $AutoIgnoreString); } elsif ($Result == 1) { # 'updated' # We need to make sure we don't try to upgrade a package that is installed simply # for a newer arch... possibly we could remove and re-install in the future to accomplish this? if (($Data->{'LocalSource'} ne 'installed') or ($remote->{'Version'} ne $local->{'Version'})) { $ret = Do_Actions($Data, 'updated', $Orig, $local->{'Orig'}, $AutoIgnoreString); } } else { # 'same' $ret = Do_Actions($Data, 'same', $Orig, $local->{'Orig'}, $AutoIgnoreString); if ($local->{'Size'} and ($local->{'Size'} != $RemoteSizes{$ThisFile})) { if ($local->{'Arch'} eq $remote->{'Arch'}) { # Local file is not the same size... re-download... $ret = Do_Actions($Data, 'updated', $Orig, $local->{'Orig'}, $AutoIgnoreString); } } } } else { # There are no local packages by that name... # Therefore, remote package is 'new' $ret = Do_Actions($Data, 'new', $Orig, '', $AutoIgnoreString); } return $ret; } ############################################################################## # Report commands ############################################################################## # There are now 2 commands, on a per-source-block-basis # Report_To ('email_address') or Report_To ('PRINT') # In $Data->{'report_to'}->[0] # Any unique email address will have its own file that will be mailed # after AutoRPM is done running... # Always_Report (Yes or No) # In $Data->{'always_report'}->[0] # The global variable, $ForcePrint will force the output to the screen. # Reporting has its own data structure, # $Report{'email_address'}, the file... # Internal Variables: # CURRMAILFILE my $SendCurrReport; my $PrintReport = 1; my $TempMailFile; my $NoReport; my %Report; sub Start_Report ($) { my ($Data) = @_; $NoReport = 0; if (($ForcePrint) or (ref($Data->{'report_to'}) ne 'ARRAY') or ($Data->{'report_to'} and ($Data->{'report_to'}->[0] eq 'PRINT')) ) { # Print Report $PrintReport = 1; } elsif ( $Data->{'report_to'} and (not $Data->{'report_to'}->[0]) ) { # Null Report_To, so don't report... $NoReport = 1; } else { # Mail Report $PrintReport = 0; $SendCurrReport = 0; if ( ( $Data->{'always_report'} ) and ( $Data->{'always_report'}->[0] == 1 ) ) { $SendCurrReport = 1; } $TempMailFile = $TempDir . 'mail.report.tmp'; open (CURRMAILFILE,'>' . $TempMailFile) or die "Could not create mail file $TempMailFile: $!\n"; } unless ($NoReport) { my $Date = `date`; chomp($Date); Inform ("\n***********************************************\nAutoRPM $Version on $HostName started $Date\n\n"); } if ($Debug) { Report("\nDEBUG: If you are having problems, send the output of this to \n"); } } # Submit a message to report and note that this report contains an action sub Report($) { my $Msg = $_[0]; unless ($NoReport) { if ($PrintReport) { print $Msg; } elsif ($TempMailFile) { print CURRMAILFILE $Msg; $SendCurrReport = 1; } } } # Submit a message to report but do NOT note that this report contains an action sub Inform { my $Msg = $_[0]; unless ($NoReport) { if ($PrintReport) { print $Msg; } elsif ($TempMailFile) { print CURRMAILFILE $Msg; } } } sub End_Report ($) { my ($Data) = @_; my $Temp = $Data->{'report_to'}->[0]; my $Temp2; my $Date = `date`; unless ($NoReport) { chomp($Date); Inform ("\n*************************************\nFinished $Date\n\n"); unless ($PrintReport) { if (($SendCurrReport) and ($TempMailFile)) { unless ( $Report{$Temp} ) { # This is the file that the message will be placed into... $Report{$Temp} = $TempDir . $Temp; $Report{$Temp} =~ s/\@/_/g; } close (CURRMAILFILE); $Temp2 = $Report{$Temp}; `cat $TempMailFile >> $Temp2`; unlink ($TempMailFile); $TempMailFile = ''; } } } } sub Mail_Reports { my $Temp; foreach my $ThisAddress (keys %Report) { $Temp = $Report{$ThisAddress}; if ( -s $Temp ) { # The file exists and is non-empty, so mail it... `$MailProg -s "AutoRPM on $HostName" $ThisAddress < $Temp`; unlink ($Temp); } } } ############################################################################## # FTP Pool Functions ############################################################################## # This is the maximum value of the "Tries" part of the score... my $MaxTries = 20; # Returns an FTP site name from a FTP URL sub GetSiteName ($) { my ($url) = @_; $url =~ s/^[^:]+:\/\///; $url =~ s/\/.*$//; return ($url); } # This routine uses $Data->{'FTPPool'} to keep track of the name of the current FTP Pool # If a non-null parameter is passed in, then it is assumed that an attempt had # been made to connect to the given FTP and it failed. This will then be # recorded as such in the score file. # Regardless of the parameter, this function will return the site in the # FTP pool with the highest score. sub GetFTPPool ($$) { my ($Data, $BadSite) = @_; my ($ThisSite,$MaxScore, @BestSites, $num, $this, $selected, $Name, $Site); # Load FTP pool if necessary unless ( keys %{$FTPPools{$Data->{'FTPPool'}}} ) { LoadFTPPool($Data, $Data->{'FTPPool'}) or die "FTP Source Pool not defined: $Data->{'FTPPool'}\n"; } if ($BadSite) { $Site = GetSiteName($BadSite); # A site that failed to be connected to was passed in... $FTPPools{$Data->{'FTPPool'}}{$Site}{'tries'}++; } # Find a new site to use... # Step one, determine the highest score... $MaxScore = 0; foreach $ThisSite ( keys %{$FTPPools{$Data->{'FTPPool'}}} ) { if ( (not defined($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'tries'})) or ($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'tries'} <= 0) or (not defined($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'successes'})) or ($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'successes'} < 0) ) { $FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'successes'} = 1; $FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'tries'} = ($MaxTries/2); } $FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'score'} = ($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'successes'}*1000) / $FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'tries'}; if ($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'score'} > $MaxScore) { $MaxScore = $FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'score'}; } } # Step two, pick a random host out of all the hosts with this highest score # (of which there may only be one) foreach $ThisSite ( keys %{$FTPPools{$Data->{'FTPPool'}}} ) { if ($FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'score'} == $MaxScore) { push @BestSites, $FTPPools{$Data->{'FTPPool'}}{$ThisSite}{'url'}; } } # Step three: Pick a random site from the @BestSites list $num = $#BestSites; $this = rand ($num+1); $this = $this / 1; # Process any variable substitutions $selected = $BestSites[$this]; while (($Name) = ($selected =~ /\${([^\}]+)}/)) { if (defined($ENV{$Name})) { $selected =~ s/\${[^\}]+}/$ENV{$Name}/; } else { die "ERROR: Error parsing pool $Data->{'FTPPool'}. No such variable defined: $Name\n"; } } # Return the selected site return ($selected); } # This will load the list of FTP sites and directories associated with # the given FTP pool, and also load their scores. sub LoadFTPPool ($$) { my ($Data, $PoolName) = @_; my $ScoreDir = Check_Dir ("$TempDir/scores"); my ($ThisLine, $Site, $Successes, $Tries, $Name); # Read pool file open (POOLFILE, "$PoolDir/$PoolName") or return (0); while ($ThisLine = ) { chomp($ThisLine); # Ignore comments $ThisLine =~ s/^#.*//; # Get rid of white space $ThisLine =~ s/\s+//; if ($ThisLine) { $ThisLine = StandardizeFTP($ThisLine); $Site = GetSiteName($ThisLine); $FTPPools{$Data->{'FTPPool'}}{$Site}{'tries'} = ($MaxTries/2); $FTPPools{$Data->{'FTPPool'}}{$Site}{'successes'} = 1; $FTPPools{$Data->{'FTPPool'}}{$Site}{'url'} = $ThisLine; } } close (POOLFILE); # Read score file if (-f "$ScoreDir/$PoolName") { open (SCOREFILE, "$ScoreDir/$PoolName") or return (0); while ($ThisLine = ) { chomp ($ThisLine); ($Site, $Successes, $Tries) = split (/ /, $ThisLine); if ( $FTPPools{$Data->{'FTPPool'}}{$Site} ) { $FTPPools{$Data->{'FTPPool'}}{$Site}{'successes'} = $Successes; $FTPPools{$Data->{'FTPPool'}}{$Site}{'tries'} = $Tries; } } close (SCOREFILE); } # Return success return (1); } # This will save the scores for the given FTP pool sub SaveFTPPoolScores ($) { my $PoolName = $_[0]; my $ScoreDir = Check_Dir ("$TempDir/scores"); TouchFile ("$ScoreDir/$PoolName", 1); open (SCOREFILE, ">$ScoreDir/$PoolName"); my $ThisSite; foreach $ThisSite ( keys %{$FTPPools{$PoolName}} ) { if ($FTPPools{$PoolName}{$ThisSite}{'tries'} >= $MaxTries) { # Keep scores in a reasonable range... (i.e. keep tries < $MaxTries) $FTPPools{$PoolName}{$ThisSite}{'tries'} = $FTPPools{$PoolName}{$ThisSite}{'tries'} / 2; $FTPPools{$PoolName}{$ThisSite}{'successes'} = $FTPPools{$PoolName}{$ThisSite}{'successes'} / 2; } print SCOREFILE $ThisSite . ' ' . $FTPPools{$PoolName}{$ThisSite}{'successes'} . ' ' . $FTPPools{$PoolName}{$ThisSite}{'tries'} . "\n"; } close (SCOREFILE); } # This is called to report a successful connection... sub TellSuccess ($$) { my ($Data, $URL) = @_; my $GoodSite = GetSiteName($URL); if ( (not defined($FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'tries'})) or ($FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'tries'} <= 0) or (not defined($FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'successes'})) or ($FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'successes'} < 0) ) { $FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'successes'} = 1; $FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'tries'} = ($MaxTries/2); } $FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'tries'}++; $FTPPools{$Data->{'FTPPool'}}{$GoodSite}{'successes'}++; } # This is called when the directory doesn't even exist on the server. # The server is then ranked very low sub TellNoDir ($$) { my ($Data, $URL) = @_; my $VeryBadSite = GetSiteName($URL); $FTPPools{$Data->{'FTPPool'}}{$VeryBadSite}{'tries'} = $MaxTries-1; $FTPPools{$Data->{'FTPPool'}}{$VeryBadSite}{'successes'} = 0; } ############################################################################## # VFS Functions ############################################################################## sub StandardizeFTP ($) { my $SourceFTP = $_[0]; unless ( $SourceFTP =~ m=^ftp://=) { my ($FTPHost, $FTPDir); if ( ($FTPHost, $FTPDir) = ($SourceFTP =~ m=^([^:]+):(.+)$= ) ) { $SourceFTP = 'ftp://' . $FTPHost . $FTPDir; } } return ($SourceFTP); } # This takes the $Data->{'SourceLocation'} variable and splits it into the various FTP # variables sub ProcessFTPSite ($) { my ($Data) = @_; unless ( ($Data->{'FTPUser'}, $Data->{'FTPPasswd'}, $Data->{'FTPSiteName'}, $Data->{'CurrFTPDir'}) = ($Data->{'SourceLocation'} =~ m=^ftp://([^:]+):([^@]+)@([^/]+)(/.+)$=) ) { ($Data->{'FTPSiteName'}, $Data->{'CurrFTPDir'}) = ($Data->{'SourceLocation'} =~ m=^ftp://([^/]+)(/.+)$=); $Data->{'FTPUser'} = 'anonymous'; $Data->{'FTPPasswd'} = 'AutoRPM@' . $HostName; } unless ($Data->{'CurrFTPDir'} =~ m=/$=) { $Data->{'CurrFTPDir'} = $Data->{'CurrFTPDir'} . '/'; } } my ($LastFTPSite, $LastFTPUser) = ('', ''); sub VFS_Open ($) { my ($Data) = @_; ($Data->{'CurrFTPSite'} = '') unless (defined($Data->{'CurrFTPSite'})); if ($Data->{'FTP'}) { if (($Data->{'FTPPool'}) and (not $Recursing)) { $Data->{'SourceLocation'} = GetFTPPool($Data, ""); Inform ('RPM Source (FTP Pool): ' . $Data->{'FTPPool'} . "\n\n"); } else { $Data->{'SourceLocation'} = StandardizeFTP($Data->{'SourceLocation'}); } my %FTPOptions = (); if ($Settings{'ftp_passive_mode'} eq 'on') { $FTPOptions{'Passive'} = 1; } if ($FTPTimeOut) { $FTPOptions{'Timeout'} = $FTPTimeOut; } if ($FTPFirewall) { $FTPOptions{'Firewall'} = $FTPFirewall; } if ($FTPPort) { $FTPOptions{'Port'} = $FTPPort; } ProcessFTPSite($Data); if ($Data->{'RetriesLeft'} <= 0) { $Data->{'CurrFTPSite'} = ''; } unless ($Data->{'FTPSiteName'} eq $Data->{'CurrFTPSite'}) { if (($LastFTPSite eq $Data->{'FTPSiteName'}) and ($LastFTPUser eq $Data->{'FTPUser'})) { unless ($DoingCache) { Inform ('Remaining Connected to ' . $Data->{'FTPSiteName'} . ' as ' . $Data->{'FTPUser'} . "...\n"); } $Data->{'CurrFTPSite'} = $Data->{'FTPSiteName'}; } else { my $Success = 0; while (($Data->{'RetriesLeft'} > 0) && ($Success == 0)) { $Data->{'RetriesLeft'}--; my $FTPRetryDelay = ((rand ($FTPRetryMaxDelay)) / 1) + $FTPRetryMinDelay; unless ($DoingCache) { Inform ('Connecting to ' . $Data->{'FTPSiteName'} . "...\n"); } if ($FTP_Object = Net::FTP->new($Data->{'FTPSiteName'}, %FTPOptions)) { if ($Interactive and $Settings{'ftp_hash'} eq 'on') { $FTP_Object->hash(\*STDERR, $Settings{'ftp_hash_size'}); } unless ($DoingCache) { Inform ('Logging in as ' . $Data->{'FTPUser'} . "\n"); } unless ($Success = $FTP_Object->login($Data->{'FTPUser'},$Data->{'FTPPasswd'})) { Inform "ERROR: Can't login as " . $Data->{'FTPUser'} . ' on ' . $Data->{'FTPSiteName'} . ' (' . $Data->{'RetriesLeft'} . " Retries Left)\n"; Inform " Sleeping $FTPRetryDelay second(s)...\n"; if ($Data->{'FTPPool'}) { $Data->{'SourceLocation'} = GetFTPPool($Data, $Data->{'SourceLocation'}); ProcessFTPSite($Data); } if ($Interactive) { Inform " [Not sleeping with in interactive mode]...\n"; } else { sleep ($FTPRetryDelay); } } } else { Inform "ERROR: Can't connect to " . $Data->{'FTPSiteName'} . ' (' . $Data->{'RetriesLeft'} . " Retries Left)\n"; Inform " Sleeping $FTPRetryDelay second(s)...\n"; if (($Data->{'FTPPool'}) and (not $Recursing)) { $Data->{'SourceLocation'} = GetFTPPool($Data, $Data->{'SourceLocation'}); ProcessFTPSite($Data); } if ($Interactive) { Inform " [Not sleeping in interactive mode]...\n"; } else { sleep ($FTPRetryDelay); } } } if ($Success == 0) { $Data->{'CurrFTPSite'} = ''; return 0; } else { if (($Data->{'FTPPool'}) and (not $Recursing)) { TellSuccess($Data, $Data->{'SourceLocation'}); } $Data->{'CurrFTPSite'} = $Data->{'FTPSiteName'}; } } } unless ($DoingCache) { $Data->{'FTP_First'} = 1; } } else { # Add / to end of directory unless ($DoingCache) { Inform ('RPM Source: ' . $Data->{'SourceLocation'} . "\n\n"); } unless ($Data->{'SourceLocation'} =~ m=/$=) { $Data->{'SourceLocation'} = $Data->{'SourceLocation'} . '/'; } unless (opendir (SOURCEDIR,$Data->{'SourceLocation'})) { Report("ERROR: Can't open directory " . $Data->{'SourceLocation'} . "\n"); return 0; } } return 1; } sub VFS_Store($$$) { my ($Data, $SourceFile, $DestDir) = @_; my ($RemoteDir,$RemoteFile,$RemoteSize,$LocalSize,$LocalSizeKB); unless ($DestDir =~ m=/$= ) { $DestDir .= '/'; } if ($Data->{'FTP'}) { ($RemoteDir,$RemoteFile) = ( $SourceFile =~ m=(^.*/)([^/]+)$=); $FTP_Object->binary(); $FTP_Object->cwd($RemoteDir); if ($RemoteSizes{$SourceFile}) { $Debug and Report("[size cached]"); $RemoteSize = $RemoteSizes{$SourceFile}; } else { $RemoteSize = $FTP_Object->size($RemoteFile); $RemoteSizes{$SourceFile} = $RemoteSize; } my $RemoteTime = $FTP_Object->mdtm($RemoteFile); my $RemoteSizeKb = $RemoteSize / 1000; if (-s $DestDir . $RemoteFile) { $LocalSize = (-s $DestDir . $RemoteFile); if ($RemoteSize == $LocalSize) { return 2; } else { $Debug and Report("\nAbout to resume transfer: local=$LocalSize, remote=$RemoteSize\n"); # Resume transfer... $LocalSizeKB = $LocalSize / 1000; $RemoteSizeKb -= $LocalSizeKB; if ($ReportCommand) { Inform ("($RemoteSizeKb KB remaining)... "); } if ($FTP_Object->get($RemoteFile, $DestDir . $RemoteFile, $LocalSize)) { $RemoteTime and utime $RemoteTime, $RemoteTime, $DestDir . $RemoteFile; return 3; } } } if ($ReportCommand) { Inform ("($RemoteSizeKb KB)... "); } if ($FTP_Object->get($RemoteFile, $DestDir . $RemoteFile)) { $RemoteTime and utime $RemoteTime, $RemoteTime, $DestDir . $RemoteFile; `chmod a+r $DestDir$RemoteFile`; return 1; } else { Report("ERROR: Failed to transfer $RemoteFile to $DestDir$RemoteFile\n"); return 0; } } else { if (copy($SourceFile, $DestDir)) { my $RemoteTime = (stat($SourceFile))[9]; ($RemoteDir,$RemoteFile) = ( $SourceFile =~ m=(^.*/)([^/]+)$=); utime $RemoteTime, $RemoteTime, $DestDir . $RemoteFile; `chmod a+r $DestDir$RemoteFile`; return 1; } else { Report("ERROR: Failed to copy $RemoteFile to $DestDir$RemoteFile\n"); return 0; } } } sub VFS_Read { my ($Data) = @_; my ($ThisFile,$Temp,$Temp2,$LinkName,$LinkDest,@DirListing,@TempListing,$IsDir,$FileName); my @SourceList = (); # Any directories returned need to end in a /... if ($Data->{'FTP'}) { if ($Data->{'CurrFTPSite'}) { if ($Data->{'FTP_First'}) { $Data->{'FTP_First'} = 0; Inform ('Listing Directory: ' . $Data->{'CurrFTPDir'} . "\n\n"); unless (($FTP_Object->cwd($Data->{'CurrFTPDir'})) and (@DirListing = $FTP_Object->dir())) { Inform(" No such directory ($Data->{'CurrFTPDir'})...\n"); if ($Data->{'FTPPool'}) { TellNoDir($Data, $Data->{'SourceLocation'}); return ('NODIR'); } return (); } foreach $Temp (@DirListing) { $Debug and Report("DEBUG: FTP Directory Listing: $Temp... "); my $FileSize; if ( ($LinkName,$LinkDest) = ($Temp =~ /^l[rwxst-]{9}(?:\s+\S+)+\s+[0123456789]+ ... .. [ ]?..[:]?..\s+(.*) -> (.*)$/ ) ) { if ($LinkName eq $Data->{'CurrFTPDir'}) { # The actual directory we are looking at is a symlink, so look at the directory it points to $Data->{'CurrFTPDir'} =~ s=/[^/]+/*$=/=; $Data->{'CurrFTPDir'} .= "$LinkDest/"; $Data->{'FTP_First'} = 1; $Debug and Report("following dir symlink.\n"); return (VFS_Read($Data)); } # Symbolic link... unless ($LinkDest =~ m=^/= ) { # Relative Link... $LinkDest = $Data->{'CurrFTPDir'} . $LinkDest; } @TempListing = $FTP_Object->dir($LinkDest);# or print STDERR "ERROR: Can't get FTP file listing of " . $LinkDest . "\n"; $IsDir = 0; foreach $Temp2 (@TempListing) { if ( $Temp2 =~ /^d[rwxst-]{9}(?:\s+\S+)+\s+[0123456789]+ ... .. [ ]?..[:]?.. \.$/ ) { # Well, we now know that the sybolic link points to a directory. push @SourceList, $Data->{'CurrFTPDir'} . $LinkName . '/'; $IsDir = 1; $Debug and Report("symlink to dir.\n"); } else { # Get file size ($FileSize) = ($Temp2 =~ /^d[rwxst-]{9}(?:\s+\S+)+\s+([0123456789]+) ... .. [ ]?..[:]?.. \.$/); $Debug and Report("symlink to file.\n"); } } unless ($IsDir) { # Points to a file... push @SourceList, $LinkDest; $RemoteSizes{$LinkDest} = $FileSize; } } elsif ( ($FileName) = ($Temp =~ /^d[rwxst-]{9}(?:\s+\S+)+\s+[0123456789]+ ... .. [ ]?..[:]?..\s+(.*)$/ ) ) { # Directory... push @SourceList, $Data->{'CurrFTPDir'} . $FileName . '/'; $Debug and Report("directory.\n"); } elsif ( ($FileSize, $FileName) = ($Temp =~ /^[rwxst-]{10}(?:\s+\S+)+\s+([0123456789]+) ... .. [ ]?..[:]?..\s+(.*)$/ ) ) { # Regular file... push @SourceList, $Data->{'CurrFTPDir'} . $FileName; $RemoteSizes{$Data->{'CurrFTPDir'} . $FileName} = $FileSize; $Debug and Report("file.\n"); } else { $Debug and Report("UNKNOWN!\n"); } } @SourceList = sort (@SourceList); } if (@SourceList) { return (@SourceList); } else { return (); } } else { return (); } } else { while (defined($ThisFile = readdir(SOURCEDIR))) { $ThisFile = $Data->{'SourceLocation'} . $ThisFile; if ( -d $ThisFile ) { unless ($ThisFile =~ m=/$=) { $ThisFile = $ThisFile . '/'; } } push @SourceList, $ThisFile; } return (@SourceList); } } sub VFS_Close ($) { my ($Data) = @_; if ($Data->{'FTP'}) { if ($Data->{'FTPPool'}) { SaveFTPPoolScores ($Data->{'FTPPool'}); } $LastFTPSite = $Data->{'CurrFTPSite'}; $LastFTPUser = $Data->{'FTPUser'}; $Data->{'CurrFTPDir'} = ''; $Data->{'CurrFTPSite'} = ''; #$FTP_Object->quit; } else { closedir (SOURCEDIR); } } ############################################################################## # Read_Config ############################################################################## sub Config_Trim($) { my $Line = $_[0]; $Line =~ s/^([^\#\"]*)#.*$/$1/; $Line =~ s/^(\".*\")#.*$/$1/; $Line =~ s/^\s+//; $Line =~ s/\s+$//; chomp ($Line); return ($Line); } sub New_Config_Vars { my %Config; $Config{'NeedFuncName'} = 1; $Config{'NeedOParen'} = 0; $Config{'NeedCParen'} = 0; $Config{'NeedSemiColon'} = 0; $Config{'NeedOCurly'} = 0; $Config{'NeedCCurly'} = 0; $Config{'InSourceBlock'} = 0; $Config{'InActionBlock'} = 0; $Config{'ActionName'} = 0; $Config{'ProcessCurr'} = 0; $Config{'Dirty'} = 0; $Config{'LastDirty'} = 1; return \%Config; } sub Read_Config { my ($ThisLine, $FuncName, @ParamList, $NeedParam, $Param, $Name, $Pos); my ($Config, $Data); my $CurrConfigFile = $_[0]; $Data->{'AutoIgnore'} = 1; # Enforce permissions on config file and any backups, etc... # In case there are passwords in this file system ('chmod 0600 ' . $CurrConfigFile . '* 2>/dev/null'); system ('chown root ' . $CurrConfigFile . '* 2>/dev/null'); # Open the config file my $fh = gensym(); open ($fh, $CurrConfigFile) or die "ERROR: Cannot open config file $CurrConfigFile: $!\n"; my $LineNum = 0; $Config = New_Config_Vars(); my $LastLine; while (defined($ThisLine = <$fh>)) { $Debug and Report("Processing line: $ThisLine"); $LineNum++; $ThisLine = Config_Trim($ThisLine); $LastLine = undef; until ($ThisLine eq '') { if (defined($LastLine)) { if ($LastLine eq $ThisLine) { die "ERROR: Error parsing $CurrConfigFile on line $LineNum\n"; } } $LastLine = $ThisLine; # Looks for a new function name... if ($Config->{'NeedFuncName'}) { $FuncName = $ThisLine; $FuncName =~ s/\(.*$//; $FuncName = Config_Trim ($FuncName); if ($FuncName) { if ($FuncName =~ m/ /) { die 'ERROR: Error parsing ' . $CurrConfigFile . "\nSpace not allowed in function name on line " . $LineNum . "\n"; } unless ($FuncName =~ m/[\{\}\(\)\;\#\"]/ ) { @ParamList = (); $FuncName = lc($FuncName); $Config->{'NeedFuncName'} = 0; $Config->{'Dirty'}++; $Config->{'LastDirty'} = $LineNum; $Config->{'NeedOParen'} = 1; unless ($ThisLine =~ s/^.*?\(/\(/ ) { $ThisLine = ''; } $ThisLine = Config_Trim($ThisLine); } } } # Looks for an opening parentesis... if ($ThisLine =~ s/^\(//) { if ($Config->{'NeedOParen'}) { $Config->{'NeedOParen'} = 0; $NeedParam = 1; $ThisLine = Config_Trim($ThisLine); } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', ( not expected, Line ' . $LineNum . "\n"; } } # Looks for an opening curly-brace... if ($ThisLine =~ s/^\{//) { if ($Config->{'NeedOCurly'}) { $Config->{'NeedOCurly'} = 0; $Config->{'NeedCCurly'} = 1; $Config->{'NeedFuncName'} = 1; if ($Config->{'InSourceBlock'}) { if ($Config->{'InActionBlock'}) { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', line ' . $LineNum . "\n"; } else { $Config->{'InActionBlock'} = 1; } } else { $Config->{'InSourceBlock'} = 1; } $ThisLine = Config_Trim($ThisLine); } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', { not expected, Line ' . $LineNum . "\n"; } } # Looks for an closing curly-brace... if ($ThisLine =~ s/^\}// ) { if ( ($Config->{'NeedCCurly'}) and ($Config->{'NeedOParen'} == 0) and ($Config->{'NeedCParen'} == 0) and ($Config->{'NeedSemiColon'} == 0) and ($NeedParam == 0) ) { if ($Config->{'InActionBlock'}) { $Config->{'InActionBlock'} = 0; $Config->{'Dirty'}--; } elsif ($Config->{'InSourceBlock'}) { Start_Report($Data); if ($FirstBlock) { $FirstBlock = 0; if ($StartDelay) { $StartDelay = ((rand ($StartDelay)) / 1); Inform ("\nDelaying $StartDelay seconds...\n"); sleep ($StartDelay); } } Process_Local($Data); Process_Remote($Data); End_Report($Data); $Config = New_Config_Vars(); $Data = {}; } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', line ' . $LineNum . "\n"; } } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', } not expected, Line ' . $LineNum . "\n"; } } # Looks for the actual parameter... if ($NeedParam) { $Param = $ThisLine; $Param =~ s/,[^\"]*\)/\)/; $Param =~ s/^([^\"]*),.*\)/$1\)/; $Param =~ s/^(\".*\"),.*\)/$1\)/; $Param =~ s/\)[^\)]*$//; $Param = Config_Trim ($Param); if ($Param) { # Do variable substitution while (($Name) = ($Param =~ /\${([^\}]+)}/)) { if (defined($ENV{$Name})) { $Param =~ s/\${[^}]+}/$ENV{$Name}/; } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . "\nNo such variable defined: $Name - Line " . $LineNum . "\n"; } } # Process Parameter if ($Param =~ s/^\"// ) { unless ($Param =~ s/\"$// ) { die 'ERROR: Error Parsing ' . $CurrConfigFile . "\nNo Closing quote - Line " . $LineNum . "\n"; } } else { $Param = lc ($Param); if ($Param eq 'yes') { $Param = 1; } elsif ($Param eq 'no') { $Param = 0; } elsif ($Param eq 'interactive') { $Param = 1; } elsif ($Param eq 'auto') { $Param = 2; } elsif ($FuncName eq 'action') { # Don't process action's arguments } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . "\nInvalid Parameter Value ($Param) - Line " . $LineNum . "\n"; } } push @ParamList, $Param; $NeedParam = 0; $Config->{'NeedCParen'} = 1; $ThisLine =~ s/^\"[^\"]*\"([,\)])/$1/; unless ($ThisLine =~ s/^[^,]*([,\)])/$1/) { $ThisLine = ''; } $ThisLine = Config_Trim($ThisLine); } } # If we have a quote at the beginning of the line at this point, something is wrong. if ($ThisLine =~ /^\"/ ) { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', " not expected, Line ' . $LineNum . "\n"; } # Look for closing parenthesis... if ($ThisLine =~ s/^\)//) { if ($Config->{'NeedCParen'}) { $ThisLine = Config_Trim($ThisLine); $Config->{'NeedCParen'} = 0; $Config->{'NeedSemiColon'} = 1; } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', ) not expected, Line ' . $LineNum . "\n"; } } # Look for possible comma... if ($ThisLine =~ s/^,//) { if ($Config->{'NeedCParen'}) { $ThisLine = Config_Trim($ThisLine); $Config->{'NeedCParen'} = 0; $NeedParam = 1; } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', comma not expected, Line ' . $LineNum . "\n"; } } # Look for semi-colon (signals the end of a function call). if ($ThisLine =~ s/^;//) { if ($Config->{'NeedSemiColon'}) { $ThisLine = Config_Trim($ThisLine); $Config->{'NeedSemiColon'} = 0; $Config->{'ProcessCurr'} = 1; } else { die 'ERROR: Error Parsing ' . $CurrConfigFile . ', ; not expected, Line ' . $LineNum . "\n"; } } # Even if we still need a semi-colon, we may be starting a block instead. if ($Config->{'NeedSemiColon'}) { if ($Config->{'InSourceBlock'}) { if ($FuncName eq 'action') { $Config->{'NeedSemiColon'} = 0; $Config->{'NeedOCurly'} = 1; $Config->{'LastDirty'} = $LineNum; $Config->{'ActionName'} = $ParamList[0]; } } else { if ($FuncName eq 'ftp') { $Config->{'NeedSemiColon'} = 0; $Config->{'NeedOCurly'} = 1; $Config->{'LastDirty'} = $LineNum; $Data->{'FTP'} = 1; $Data->{'SourceLocation'} = $ParamList[0]; } elsif ($FuncName eq 'ftppool') { $Config->{'NeedSemiColon'} = 0; $Config->{'NeedOCurly'} = 1; $Config->{'LastDirty'} = $LineNum; $Data->{'FTPPool'} = $ParamList[0]; $Recursing = 0; $Data->{'FTP'} = 1; } elsif ($FuncName eq 'directory') { $Config->{'NeedSemiColon'} = 0; $Config->{'NeedOCurly'} = 1; $Config->{'LastDirty'} = $LineNum; $Data->{'FTP'} = 0; $Data->{'SourceLocation'} = $ParamList[0]; } } } # If we are ready to process the current fuction/parameter pair, let's do it. if ($Config->{'ProcessCurr'}) { if ($Config->{'InSourceBlock'}) { if ($Config->{'InActionBlock'}) { if ($#ParamList <= 0) { push @{$Data->{$Config->{'ActionName'}}->{$FuncName}}, $ParamList[0]; } else { push @{$Data->{$Config->{'ActionName'}}->{$FuncName}}, [ @ParamList ]; } } else { if ($#ParamList <= 0) { push @{$Data->{$FuncName}}, $ParamList[0]; } else { push @{$Data->{$FuncName}}, [ @ParamList ]; } } } else { if ($FuncName eq 'temp_dir') { print STDERR "Warning: deprecated command Temp_Dir() in $CurrConfigFile on Line $LineNum\nUse command-line option --tempdir.\n"; } elsif ($FuncName eq 'remove_packages') { $DeleteOption = $ParamList[0]; $DeleteOptionArgs = $ParamList[1]; } elsif ($FuncName eq 'really_remove_packages') { $ReallyDeleteOption = $ParamList[0]; } elsif ($FuncName eq 'remove_exclude') { $RemoveExclude = $ParamList[0]; } elsif ($FuncName eq 'pool_dir') { $PoolDir = $ParamList[0]; $PoolDir = Check_Dir($PoolDir); } elsif ($FuncName eq 'rpm_location') { print STDERR "Warning: deprecated command RPM_Location() in $CurrConfigFile on Line $LineNum\nTo modify this value, enter interactive mode and run 'set rpm_location /sbin/rpm'.\n"; } elsif ($FuncName eq 'rpm_install_opt') { #print STDERR "Warning: deprecated command RPM_Install_Opt() in $CurrConfigFile on Line $LineNum\nTo modify this value, enter interactive mode and run 'set rpm_install_opt '.\n"; $Settings{'rpm_install_opt'} = $ParamList[0]; } elsif ($FuncName eq 'config_dir') { opendir (CONFIGDIR, $ParamList[0]) or die "Directory $ParamList[0] not found: $!\n"; my $ThisFile; while ($ThisFile = readdir(CONFIGDIR)) { if (($ThisFile !~ /^\./) and (-f "$ParamList[0]/$ThisFile")) { Read_Config("$ParamList[0]/$ThisFile"); } } closedir (CONFIGDIR); } elsif ($FuncName eq 'gpg_path') { $GPGPath = $ParamList[0]; } elsif ($FuncName eq 'config_file') { Read_Config($ParamList[0]); } elsif ($FuncName eq 'ftp_passive') { print STDERR "Warning: deprecated command FTP_Passive() in $CurrConfigFile on Line $LineNum\nTo modify this value, enter interactive mode and run 'set ftp_passive_mode '.\nNOTE: AutoRPM now uses passive mode by default!\n"; } elsif ($FuncName eq 'ftp_timeout') { $FTPTimeOut = $ParamList[0]; } elsif ($FuncName eq 'ftp_retries') { $FTPRetries = $ParamList[0]; } elsif ($FuncName eq 'start_delay') { print STDERR "Warning: deprecated command Start_Delay() in $CurrConfigFile on Line $LineNum\nUse the --delay=XXX option to the 'auto' command instead.\n"; } elsif ($FuncName eq 'ftp_retry_delay') { $FTPRetryMinDelay = $ParamList[0]; $FTPRetryMaxDelay = $ParamList[1]; } elsif ($FuncName eq 'replace_arch') { print STDERR "Warning: deprecated command Replace_Arch() in $CurrConfigFile on Line $LineNum\nAutoRPM will now always choose the best architecture it can for your machine.\n"; } elsif ($FuncName eq 'set_var') { $ENV{$ParamList[0]} = $ParamList[1]; } elsif ($FuncName eq 'eval_var') { $ENV{$ParamList[0]} = `$ParamList[1]`; chomp($ENV{$ParamList[0]}); Report("DEBUG: EvalVar(): $ParamList[0] = $ENV{$ParamList[0]}\n") if $Debug; } elsif ($FuncName eq 'read_var') { if (open (TFILE, $ParamList[1])) { $ENV{$ParamList[0]} = ; close (TFILE); chomp($ENV{$ParamList[0]}); } } elsif ($FuncName eq 'ftp_firewall') { $FTPFirewall = $ParamList[0]; } elsif ($FuncName eq 'ftp_port') { $FTPPort = $ParamList[0]; } elsif ($FuncName eq 'debug') { $Debug = $ParamList[0]; } elsif ($FuncName eq 'hostname') { $HostName = $ParamList[0]; } elsif ($FuncName eq 'umask') { umask(oct($ParamList[0])); } elsif ($FuncName eq 'report_queues_to') { $AutoQueueReport = $ParamList[0]; } else { die 'ERROR: ' . $FuncName . ' not expected in Main File on line ' . $LineNum . "\n"; } } $Config->{'ProcessCurr'} = 0; $Config->{'NeedFuncName'} = 1; $Config->{'Dirty'}--; } $ThisLine = Config_Trim ($ThisLine); } } close ($fh); # If $Config->{'Dirty'} > 0, the file wasn't correct. if ($Config->{'Dirty'}) { my $Msg; if ($NeedParam) { $Msg = 'Parameter Expected for ' . $FuncName; } elsif ($Config->{'NeedOParen'}) { $Msg = '\( Expected'; } elsif ($Config->{'NeedCParen'}) { $Msg = '\) Expected'; } elsif ($Config->{'NeedSemiColon'}) { $Msg = '\; Expected'; } elsif ($Config->{'NeedOCurly'}) { $Msg = '\{ Expected'; } elsif ($Config->{'NeedCCurly'}) { $Msg = '\} Expected'; } else { $Msg = 'Parsing Error'; } die 'ERROR: Error parsing ' . $CurrConfigFile . "\n" . $Msg . ' - Possibly Started on Line ' . $Config->{'LastDirty'} . ".\n"; } } ############################################################################## # Check_Queues ############################################################################## sub CountInter { my $count = 0; foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { $count++; } } if ($count) { Report(" " . $count . " RPM(s) waiting to be installed/updated/removed Interactively\n\n"); } return $count; } sub Check_Queues { my ($Source,$Delete,$Orig); my $Data; # First, start a report section for the install queues. # Of course, if $ForcePrint is on, this won't matter... $Data->{'report_to'}->[0] = $AutoQueueReport; # Only report if something happened $Data->{'always_report'}->[0] = 0; Start_Report($Data); Inform ("\nProcessing Auto-Install Queue:\n"); Do_Auto_Queue(); if (CountInter()) { Report(" To install/upgrade, run 'autorpm' as root...\n"); } End_Report($Data); } sub Get_Remote_File($$) { my ($Remote, $Local) = @_; my ($Data, $ret); my ($FTPFile,$a,$ctmp); if ($Debug) { Report('DEBUG: Get_Remote_File (' . $Remote . ', ' . $Local . ")\n"); } $ret = 1; if ($Remote and ($Remote =~ /^ftp:\/\//)) { $ret = 0; # Only do anything if the original ($Remote) string # is non-null (i.e. came from an FTP site). $Data->{'FTP'} = 1; $DoingCache = 1; $Data->{'SourceLocation'} = $Remote; # Strip the filename off the end of the SourceLocation. $Data->{'SourceLocation'} =~ s=/[^/]+$=/=; if ($Debug) { Report('DEBUG: Inside Get_Remote_File, SourceLocation is now: ' . $Data->{'SourceLocation'} . "\n"); } $Data->{'RetriesLeft'} = $FTPRetries; unless ( ($Data->{'FTPUser'}, $Data->{'FTPPasswd'}, $Data->{'FTPSiteName'}, $FTPFile) = ($Remote =~ m=^ftp://([^:]+):([^@]+)@([^/]+)(/.+)$=) ) { ($Data->{'FTPSiteName'}, $FTPFile) = ($Remote =~ m=^ftp://([^/]+)(/.+)$=); $Data->{'FTPUser'} = 'anonymous'; $Data->{'FTPPasswd'} = 'AutoRPM@' . $HostName; } my $LocalDir = $Local; $LocalDir =~ s=^(.*/)[^/]+$=$1=; $ctmp = BaseName($Remote); my $tried = 0; while (1) { if (-s $Local) { # File exists and is non-zero in size # Now, check to see if it is a complete package my $result = `$Settings{'rpm_location'} --checksig $Local 2>/dev/null`; if (($result =~ /md5/) or ($result =~ /sha1/)) { # File is complete... we are done. $ret = 1; last; } elsif ($tried == 0) { # MD5 check failed... we need to try to resume transfer # (if we haven't already tried) $tried = 1; Inform (" Resuming Transfer of $ctmp... "); } elsif ($tried == 1) { # File is corrupt... remove file and start over $tried = 2; Inform (" $ctmp corrupt, downloading again... "); unlink ($Local); } else { # Can't fix! Report("Error! File still corrupt! Giving Up!\n"); last; } } else { Inform (" Downloading $ctmp... "); } unless ($Data->{'CurrFTPSite'}) { unless (VFS_Open($Data)) { Report("Could not open FTP connection!\n"); last; } } if ($a = VFS_Store($Data, $FTPFile, $LocalDir)) { if ($a == 3) { Inform ("Completed.\n"); } elsif ($a == 2) { Inform ("Already There.\n"); } else { if (-s $Local) { Inform ("Done.\n"); } else { Report("Error (zero-length file)\n"); $tried = 2; unlink ($Local); last; } } } else { Report("Error!\n"); last; } } } $DoingCache = 0; return ($ret); } ############################################################################## # Auto-Ignore Functions ############################################################################## sub Read_Auto_Ignore { if (open (IGNOREFILE,$TempDir . 'auto-ignore')) { # Get Auto-Ignore list... Inform ('Reading Auto-Ignore list... '); my @tmp = ; close (IGNOREFILE); chomp(@tmp); foreach (@tmp) { $AutoIgnore{$_}++; } Inform ("Done.\n\n"); } unless (keys %AutoIgnore) { # Add a placeholder so it isn't reloaded constantly if empty $AutoIgnore{'DUMMY'}++; } } sub Test_Auto_Ignore ($) { my $RPM1 = $_[0]; return 0 unless $RPM1; if ($AutoIgnore{$RPM1}) { if ($Debug) { Report('DEBUG: Auto-Ignoring ' . $RPM1 . "\n"); } return (1); } return (0); } sub Add_Auto_Ignore ($) { my $RPM = $_[0]; return unless $RPM; # Trim off * if it is in the front $RPM =~ s/^\*//; # Add to list in memory $AutoIgnore{$RPM}++; open (IGNOREFILE,'>>' . $TempDir . 'auto-ignore'); print IGNOREFILE $RPM . "\n"; close (IGNOREFILE); } ############################################################################## # New code for 2.0 ############################################################################## sub ReadQueue ($) { my ($InFile) = @_; my (%ret, $line); $Debug and Report("DEBUG: Reading queue file $TempDir$InFile.queue\n"); open (QUEUEFILE, "$TempDir$InFile.queue") or return \%ret; while (defined($line = )) { $Debug and Report(" DEBUG: read line: $line"); chomp ($line); my ($File, $Delete, $Orig, $Local, $CopyTo, $AutoIgnore, $Errors, $Extra, $Triggers) = split (/;/, $line, 9); my $pkg = AddFileToHash(\%ret, $Orig); next unless $pkg; $pkg->{'File'} = $File; $pkg->{'Delete'} = $Delete; $pkg->{'Local'} = $Local; @{$pkg->{'CopyTo'}} = (); @{$pkg->{'Triggers'}} = (); $pkg->{'Errors'} = 0; foreach (split /:/, $CopyTo) { push @{$pkg->{'CopyTo'}}, $_; } foreach (split /:/, $Triggers) { push @{$pkg->{'Triggers'}}, $_; } $pkg->{'IgnoreOnFailure'} = 0; if ($AutoIgnore =~ /^\*/) { $pkg->{'IgnoreOnFailure'} = 1; } $AutoIgnore =~ s/^\*//; $pkg->{'AutoIgnoreString'} = $AutoIgnore; if ($Errors and ($Errors =~ s/^(\d+)://)) { # For backwards compatibility $pkg->{'Size'} = $1; } if ($Extra and ($Extra =~ s/^(\d+)://)) { $pkg->{'Size'} = $1; } if ($Errors) { my ($item, $name, $value); foreach $item (split /\|\|/, $Errors) { $item =~ s/^\s+//; $item =~ s/\s+$//; if ($item) { ($name, $value) = split(/=/, $item, 2); push @{$pkg->{$name}}, $value; $pkg->{'Errors'}++; } } } if ($Extra) { my ($item, $name, $value); foreach $item (split /\|\|/, $Errors) { $item =~ s/^\s+//; $item =~ s/\s+$//; if ($item) { ($name, $value) = split(/=/, $item, 2); $pkg->{$name}= $value; } } } } close (QUEUEFILE); return \%ret; } sub WriteQueue ($$$) { my ($OutFile, $Data, $Append) = @_; my ($File, $Delete, $Orig, $Local, $CopyTo, $Triggers, $AutoIgnore, $Errors, $Extra); my ($name, $pkg); my $type = ($Append ? '>>' : '>'); $Debug and Report("DEBUG: Writing queue file $type$TempDir$OutFile.queue\n"); open (QUEUEFILE, "$type$TempDir$OutFile.queue") or return 0; foreach $name (keys %{$Data}) { if ((ref $Data->{$name}) eq 'ARRAY') { # Only store the newest version my $pkg = $Data->{$name}->[0]; ($File = $pkg->{'File'}) or $File = ''; ($Delete = $pkg->{'Delete'}) or $Delete = ''; ($Orig = $pkg->{'Orig'}) or $Orig = ''; ($Local = $pkg->{'Local'}) or $Local = ''; $AutoIgnore = ($pkg->{'IgnoreOnFailure'} ? '*' : '') . $pkg->{'AutoIgnoreString'}; ($CopyTo = join ':', @{$pkg->{'CopyTo'}}) or $CopyTo = ''; ($Triggers = join ':', @{$pkg->{'Triggers'}}) or $Triggers = ''; $Extra = ''; $Errors = ''; if ($pkg->{'Size'}) { $Extra = $pkg->{'Size'} . ':'; } if ($pkg->{'Errors'}) { my ($type, $error); foreach $type ('Deps', 'DepsOnMe', 'Conflicts', 'OtherErrors') { if ($pkg->{$type}) { foreach $error (@{$pkg->{$type}}) { $Errors .= "$type=$error||"; } } } } foreach $type ('follow_deps') { if ($pkg->{$type}) { $Extra .= "$type=$pkg->{$type}||"; } } print QUEUEFILE join(';', $File, $Delete, $Orig, $Local, $CopyTo, $AutoIgnore, $Errors, $Extra, $Triggers), "\n"; $Debug and Report(" DEBUG: wrote line: " . join(';', $File, $Delete, $Orig, $Local, $CopyTo, $AutoIgnore, $Errors, $Extra, $Triggers) . "\n"); } } close (QUEUEFILE); return 1; } sub DeleteFile ($) { my ($file) = @_; if (unlink ($file)) { Inform (" Deleted $file\n"); } else { Report(" Couldn't Delete $file\n"); } } sub CopyToDir ($$) { my ($file, $dir) = @_; my $name = TrimPath($file); if ((-s "$dir/$name") == (-s "$file")) { return; } if (system("cp $file $dir") == 0) { Inform " Copied to $dir/$name\n"; } else { Inform " Couldn't copy to $dir!\n"; } } sub LogAction ($$) { my ($logfile, $entry) = @_; my $Date = `date`; chomp ($Date); open (LOGFILE, ">>$TempDir$logfile"); print LOGFILE "$Date - $entry\n"; close (LOGFILE); } sub PackageInstalled ($) { my ($pkg) = @_; # Set result for package $pkg->{'Result'} = 'success'; delete $pkg->{'Deps'}; delete $pkg->{'DepsOnMe'}; delete $pkg->{'OtherErrors'}; delete $pkg->{'Conflicts'}; $pkg->{'Errors'} = 0; # Process any file copying foreach my $dir (@{$pkg->{'CopyTo'}}) { unless ($dir =~ /^FAIL/) { CopyToDir($pkg->{'File'}, $dir); } } # Delete file (if necessary) if ($pkg->{'Delete'}) { DeleteFile($pkg->{'File'}); } # Delete from local cache (if necessary) DeleteFromLocalCache($pkg->{'Local'}, 'installed'); # Add to local cache AddToLocalCache(TrimPath($pkg->{'Orig'}), 'installed'); # Log the installation my ($Temp, $Temp2); if ($pkg->{'Local'}) { $Temp = BaseName($pkg->{'File'}); $Temp2 = BaseName($pkg->{'Local'}); LogAction("install.log", "$Temp2 -> $Temp"); } else { $Temp = BaseName($pkg->{'File'}); LogAction("install.log", "Installed $Temp"); } # Execute any triggers foreach my $trigger (@{$pkg->{'Triggers'}}) { if ($pkg->{'Local'}) { ExecuteTrigger($trigger, $Temp, 'UPGRADED', $Temp2); } else { ExecuteTrigger($trigger, $Temp, 'INSTALLED', ''); } } } sub PackageFailed ($) { my ($pkg) = @_; # Delete file and set result if result is invalid if ($pkg->{'Result'} eq 'invalid') { if ($pkg->{'Delete'}) { DeleteFile($pkg->{'File'}); } } # Process any file copying my $dir; foreach $dir (@{$pkg->{'CopyTo'}}) { my $newdir = $dir; if ($newdir =~ s/^FAIL//) { CopyToDir($pkg->{'File'}, $newdir); } } # Execute any triggers foreach my $trigger (@{$pkg->{'Triggers'}}) { my $Temp = BaseName($pkg->{'File'}); ExecuteTrigger($trigger, $Temp, 'FAILED', ''); } } sub ClonePkg ($) { my ($pkg) = @_; my %ret; foreach (keys %{$pkg}) { $ret{$_} = $pkg->{$_}; } return (\%ret); } sub ResetPackage ($) { my $pkg = $_[0]; delete $pkg->{'Deps'}; delete $pkg->{'DepsOnMe'}; delete $pkg->{'OtherErrors'}; delete $pkg->{'Conflicts'}; $pkg->{'Errors'} = 0; $pkg->{'Result'} = 'pending'; } # Handle a list of RPMs with unknown problems # This gets called when there is an unknown error. Since we don't # know what RPM to remove from the list, we need to try to install # each package one at a time and add dependencies as necessary. sub UnknownFailures ($$) { my ($data, $opts) = @_; my ($Fails, @tmp, $pkg, @Warnings); foreach $pkg (keys %{$data}) { if (((ref $data->{$pkg}) eq 'ARRAY') and ($data->{$pkg}->[0]->{'Result'} eq 'pending')) { ($Fails, @tmp) = InstallRPMs($data, $opts, \%{$data->{$pkg}->[0]}); if ($Fails == 0) { # Success push @Warnings, @tmp; next; } $data->{$pkg}->[0]->{'Result'} = 'failed'; push @{$data->{$pkg}->[0]->{'OtherErrors'}}, @tmp; } } return (@Warnings); } # RPM error messages (4.0.2/4.0.3) # Failed deps: #error: failed dependencies: # openssh = 2.9p2-12 is needed by openssh-askpass-2.9p2-12 # libc.so.6 is needed by zlib-1.1.3-24 # libc.so.6(GLIBC_2.1.3) is needed by zlib-1.1.3-24 # scrollkeeper >= 0.1.4 is needed by gnome-media-1.2.3-4 # perl is needed by printconf-0.3.44-1 # /usr/bin/perl is needed by printconf-0.3.44-1 # test is needed by (installed) test2-5.0-1 # (NEW IN RPM 4.1 (RHL8.0)) # Suggested resolutions: # mutt-1.4-4.i386.rpm # Conflict #file /etc/profile.d/less.csh from install of less-358-21 conflicts with file from package less-346-2 #error: Failed dependencies: # test conflicts with test2-5.0-1 # Failed %pre script (does *not* install) #error: execution of %pre scriptlet from kirk-1.0-2 failed, exit status 1 #error: skipping kirk-1.0-2 install, %pre scriptlet failed rc 2 # Already Installed/Newer #package kirk-1.0-2 (which is newer than kirk-1.0-1) is already installed #package kirk-1.0-1 is already installed # No such RPM #error: open of blah.rpm failed: No such file or directory # Corrupt RPM #error: kirk-1.0-14.i386.rpm does not appear to be a RPM package #error: kirk-1.0-13.i386.rpm cannot be installed # RPM DB permission error #error: cannot get exclusive lock on /var/lib/rpm/Packages #error: cannot open Packages index using db3 - Operation not permitted (1) #error: cannot open Packages database in /var/lib/rpm # Read-only FS #error: unpacking of archive failed on file /mnt/tmp/kirk.tmp: cpio: open failed - Read-only file system # Not enough space on FS #installing package kirk-1.0-2 needs 26Kb on the /mnt/blah filesystem # (unlikely) chattr +i on a file (so it can't be replaced) #error: can't rename /tmp/kirk/kirk.tmp to /tmp/kirk/kirk.tmp-RPMDELETE: Operation not permitted #error: unpacking of archive failed on file /tmp/kirk/kirk.tmp: cpio: unlink failed - Operation not permitted # [Warnings] # Config file was modified and new one in package is different than old one #warning: /tmp/kirk.tmp saved as /tmp/kirk.tmp.rpmsave # Config file was modified and new one in package is different than old one (noreplace option in spec file) #warning: /tmp/kirk.tmp created as /tmp/kirk.tmp.rpmnew # Couldn't remove old file (once again, chattr +i) #error: removal of /tmp/kirk2/kirk.tmp2 failed: Operation not permitted # Couldn't remove old file (deleted) #error: removal of /tmp/kirk2/kirk.tmp failed: No such file or directory # Failed %post script (does install) #error: execution of %post scriptlet from kirk-1.0-1 failed, exit status 1 # Trying to install two packages that have a conflict # file xxxx conflicts between attempted installs of yyyy and zzzz # Wrong architecture # package glibc-2.2.4-24 is for a different architecture # Takes a second parameter of 'autorpm-2.5-1' and sees if it is in the data hash sub FindPkg ($$) { my ($data, $pkg) = @_; my ($name, $version); if (($name, $version) = ($pkg =~ /^(.+)-([^-]+-[^-]+)$/)) { if (defined($data->{$name}) and ((ref $data->{$name}) eq 'ARRAY')) { if ($data->{$name}->[0]) { if ($data->{$name}->[0]->{'Version'} =~ /^\Q$version\E/) { return ($data->{$name}->[0]); } } } } return undef; } sub InstallRPMs { my ($data, $opts, @packages) = @_; my ($pkg, $line, @Warnings, @tmp, $ret, $ref); my $list = ''; my $Fails = 0; return (0) unless (@packages); foreach $pkg (@packages) { ResetPackage($pkg); if ($pkg->{'Result'} eq 'pending') { Get_Remote_File($pkg->{'Orig'}, $pkg->{'File'}); $list .= (' ' . $pkg->{'File'}); } } $Debug and Report("DEBUG: package list: $list\n"); my $command; if ($Interactive) { $command = "$Settings{'rpm_location'} $Settings{'rpm_install_opt'} -U -vh"; } else { $command = "$Settings{'rpm_location'} $Settings{'rpm_install_opt'} -U --quiet"; } if ($opts) { if ($opts =~ /f/) { $command .= ' --force'; } elsif ($opts =~ /n/) { $command .= ' --nodeps'; } } my $suggested = 0; $Debug and Report("Executing $command $list\n"); my @output = `$command $list 3>&2 2>&1 1>&3`; if (($? >> 8) != 0) { if (($? >> 8) == 139) { Report(" ERROR: RPM segfaulted!!\n"); } else { $Debug and Report(" WARNING: RPM returned non-zero exit code (" . ($? >> 8) . ")\n"); } } foreach $line (@output) { $Debug and Report(" DEBUG: from RPM: $line"); chomp($line); $line =~ s/^\s+//; $line =~ s/\s+$//; # RPM4.1 now may note that a package mentioned in a depnedency # is installed... at this point, I don't think this provides us # any additional insight.. so I'm going to ignore it, if there. $line =~ s/is needed by \(installed\)/is needed by/; if ($line =~ /^(.+)\s+is needed by (.+)$/) { if ($ref = FindPkg($data, $2)) { # One of the packages we are installing is missing a dep push @{$ref->{'Deps'}}, $1; $ref->{'Result'} = 'failure'; $Fails++; } else { my $other = $2; my $me = $1; $me =~ s/^\s+//; $me =~ s/\s+$//; unless ($me =~ s/^(\S+)\s+.*$/$1/) { # Apparently not 'apache >= xxx', so maybe it is 'webserver' my $tmp = `$Settings{'rpm_location'} -q --whatprovides '$me' 2>/dev/null`; if ($? == 0) { #Success chomp($tmp); $me = $tmp; $me =~ s/^(.+)-[^-]+-[^-]+$/$1/; } } if ($data->{$me}) { push @{$data->{$me}->[0]->{'DepsOnMe'}}, $other; $data->{$me}->[0]->{'Result'} = 'failure'; $Fails++; } else { # Unknown dependency return (-1, @Warnings, "Unknown dep: $line"); } } } elsif ($line =~ /^file (.+) conflicts between attempted installs of (.+) and (.+)$/) { if ($ref = FindPkg($data, $2)) { push @{$ref->{'Conflicts'}}, "$3:$1"; $ref->{'Result'} = 'failure'; } if ($ref = FindPkg($data, $3)) { push @{$ref->{'Conflicts'}}, "$2:$1"; $ref->{'Result'} = 'failure'; } $Fails++; } elsif ($line =~ /^file (.+) from install of (.+) conflicts with file from package (.+)$/) { if ($ref = FindPkg($data, $2)) { push @{$ref->{'Conflicts'}}, "$3:$1"; $ref->{'Result'} = 'failure'; $Fails++; } } elsif ($line =~ /^(.+) conflicts with (.+)$/) { if ($ref = FindPkg($data, $1)) { push @{$ref->{'Conflicts'}}, "$2"; $ref->{'Result'} = 'failure'; $Fails++; } } elsif ($line =~ /^error: execution of .pre scriptlet from (.+) failed.*$/) { if ($ref = FindPkg($data, $1)) { push @{$ref->{'OtherErrors'}}, "\%post scriptlet execution failed"; $ref->{'Result'} = 'failure'; # $Fails++; (note: doesn't stop other packages from being installed) } } elsif ($line =~ /^installing package (.+) needs (.+) on the (.+) filesystem.*$/) { if ($ref = FindPkg($data, $1)) { push @{$ref->{'OtherErrors'}}, "$2 required on the $3 filesystem"; $ref->{'Result'} = 'failure'; $Fails++; } } elsif ($line =~ /^package (.+) \(which is newer than (.+)\) is already installed.*$/) { if ($ref = FindPkg($data, $2)) { $ref->{'Newer'} = $1; $ref->{'Result'} = 'invalid'; Add_Auto_Ignore($ref->{'AutoIgnoreString'}); $Fails++; } } elsif ($line =~ /^package (.+) is already installed.*$/) { if ($ref = FindPkg($data, $1)) { $ref->{'Already'} = 1; $ref->{'Result'} = 'invalid'; Add_Auto_Ignore($ref->{'AutoIgnoreString'}); $Fails++; } } elsif ( ($line =~ /^error: open of (.+) failed.*$/) or ($line =~ /^error: (.+) does not appear to be a RPM package.*$/) or ($line =~ /^error: (.+) cannot be installed.*$/) ) { my $bn = BaseName($1); if ($ref = FindPkg($data, $bn)) { $ref->{'Corrupt'} = $1; $ref->{'Result'} = 'invalid'; } } elsif ($line =~ /^error: unpacking of archive failed on file (.+)$/) { Report("ERROR: RPM could not write file $1\n"); return (-1, @Warnings, "ERROR: RPM could not write file $1\n"); } elsif ($line =~ /^error: execution of .post scriptlet from (.+) failed.*$/) { if ($ref = FindPkg($data, $1)) { push @{$ref->{'Warnings'}}, "\%post scriptlet execution failed"; } } elsif ($line =~ /^warning: (.+) saved as (.+).*$/) { push @Warnings, "$1 saved as $2"; } elsif ($line =~ /^warning: (.+) created as (.+).*$/) { push @Warnings, "$1 created as $2"; } elsif ($line =~ /^error: removal of (.+) failed.*$/) { push @Warnings, "$1 could not be removed"; } elsif ($line =~ /error: cannot get exclusive lock.*/) { die "RPM could not get exclusive lock on RPM database\n"; } elsif ($line =~ /error: cannot open Packages index using db3 - (.+)$/) { die "RPM could not open Packages DB: $1\n"; } elsif ($line =~ /Suggested resolutions/) { $suggested = 1; } elsif ($suggested and ($line =~ /^(.+-[^-]+-[^-]+)\.([^.]+|hppa\d\.\d)\.rpm/)) { # We just want to ignore these as we don't know *which* dep they resolve } elsif ( ($line =~ /error: cannot open Packages database in/) or ($line =~ /error: can.t rename .+ to .+/) or ($line =~ /error: skipping .+ install/) or ($line =~ /error: failed dependencies/) ) { # Ignore (taken care of by other cases) } else { # Unknown/Unmatched error message... push @Warnings, "Unknown Output from RPM: $line"; } } if ($Fails > 0) { # Now, see if we can resolve any dependencies my (@pkgcopies, @depadd, $tmp, $tmppkg, $errors, %Deps); PKGLOOP: foreach $pkg (@packages) { $errors = 0; if ($pkg->{'Deps'}) { foreach (@{$pkg->{'Deps'}}) { $errors++; $tmp = ResolveDeps($pkg, $_, @depadd, @pkgcopies, $pkg); if (ref $tmp) { # Found a dep $Debug and Report("DEBUG: Resolved [Deps]: depadd <- $tmp->{'Name'}\n"); push @depadd, $tmp; push @{$Deps{$tmp->{'Name'}}}, $pkg->{'Name'}; } elsif ($tmp == 0) { # Failed, so skip package next PKGLOOP; } } } if ($pkg->{'DepsOnMe'}) { foreach (@{$pkg->{'DepsOnMe'}}) { $errors++; $tmp = ResolveDeps($pkg, $_, @depadd, @pkgcopies, $pkg); if (ref $tmp) { # Found a dep $Debug and Report("DEBUG: Resolved [DepsOnMe]: depadd <- $tmp->{'Name'}\n"); push @depadd, $tmp; push @{$Deps{$tmp->{'Name'}}}, $pkg->{'Name'}; } elsif ($tmp == 0) { # Failed, so skip package next PKGLOOP; } } } if ($pkg->{'Conflicts'}) { foreach (@{$pkg->{'Conflicts'}}) { $errors++; my ($tmp_conflicts) = $_ =~ m/^([^:]+):.*$/; $tmp = ResolveDeps($pkg, $tmp_conflicts, @depadd, @pkgcopies, $pkg); if (ref $tmp) { # Found a dep $Debug and Report("DEBUG: Resolved [Conflicts]: depadd <- $tmp->{'Name'}\n"); push @depadd, $tmp; push @{$Deps{$tmp->{'Name'}}}, $pkg->{'Name'}; } elsif ($tmp == 0) { # Failed, so skip package next PKGLOOP; } } } if (($errors > 0) or ($pkg->{'Result'} eq 'pending')) { # Now, make a copy of the original package... $tmppkg = ClonePkg($pkg); $tmppkg->{'Result'} = 'pending'; #$tmppkg->{'DepsFollowed'} = 1; $Debug and Report("DEBUG: pkgcopies <- $tmppkg->{'Name'}\n"); push @pkgcopies, $tmppkg; } } if ((@pkgcopies) and (@depadd)) { my @origdepadd = @depadd; foreach (@origdepadd) { $data->{$_->{'Name'}}->[0] = $_; } my ($tFails, @tWarnings, @depaddcopy); while (@depadd) { ($tFails, @tWarnings) = InstallRPMs($data, $opts, @pkgcopies, @depadd); push @Warnings, @tWarnings; if ($tFails == 0) { # Success! $Fails = 0; last; } # Okay, still some number of failures... @depaddcopy = @depadd; @depadd = (); my $removed = 0; my $pkg; foreach $pkg (@depaddcopy) { if ($pkg->{'Result'} eq 'pending') { push @depadd, $pkg; } else { # This package, added to satisfy a dependency, failed # Don't add it, and remove anything in @pkgcopies that needed it $removed++; for (my $i = 0; $i <= $#pkgcopies; $i++) { foreach (@{$Deps{$pkg->{'Name'}}}) { if ($_ eq $pkgcopies[$i]) { splice @pkgcopies, $i, 1; $i--; } } } } } unless ($removed > 0) { # failed, but we couldn't figure out what to remove, so just exit last; } } if ($tFails != 0) { # On failure, remove added packages from data array foreach (@origdepadd) { if ($data->{$_->{'Name'}}) { delete $data->{$_->{'Name'}}; } FollowedDepFailed($_); } } } } return ($Fails, @Warnings); } sub FollowedDepFailed ($) { my ($pkg) = @_; # Don't need to do anything at this time here } sub AskAboutDep ($$) { my ($this, $pkg) = @_; if ($Interactive and ($Settings{'interactive_deps'} eq 'on')) { unless ($this->{'follow_deps'} and ($this->{'follow_deps'} == 1)) { # Ask to follow dependency... unless (GetYN("\nWould you also like to install $pkg?", 'Y')) { return 0; } print "\n"; } } Inform (" Adding $pkg to satisfy dep for $this->{'Name'}\n"); my $ret = \%{$InterData->{$pkg}->[0]}; $ret->{'follow_deps'} = $this->{'follow_deps'}; return ($ret); } sub CheckProvides ($$$@) { my ($this, $dep, $remote_ok, @pending) = @_; my ($file, @provides, $pkg, $possible_match); my $default_return = undef; PROVIDES_LOOP: foreach $pkg (keys %{$InterData}) { $Interactive and print '.'; if ((ref $InterData->{$pkg}) eq 'ARRAY') { my $ref = $InterData->{$pkg}->[0]; $file = ''; if ($ref->{'File'} and (-e $ref->{'File'})) { $file = $ref->{'File'}; } elsif ($ref->{'Orig'} and $remote_ok) { if (($ref->{'Orig'} =~ /^ftp:\/\//) or (-e $ref->{'Orig'})) { $file = $ref->{'Orig'}; } } next unless $file; @provides = `$Settings{'rpm_location'} -q --provides -p $file 2>/dev/null`; chomp(@provides); foreach (@provides) { if (($dep eq $_) or (/^\Q$dep\E/)) { foreach (@pending) { if ($_->{'Name'} eq $pkg) { # Already in dep list... if ($dep eq $_) { # Was an exact match... return -1; } else { $default_return = -1; next PROVIDES_LOOP; } } } if ($dep eq $_) { # Was an exact match... return (AskAboutDep($this, $pkg)); } else { $possible_match = $pkg; next PROVIDES_LOOP; } } } } } if ($possible_match) { return (AskAboutDep($this, $possible_match)); } return $default_return; } # Currently, looks for a newer version of the $dep package in # the interactive queue. # in the queue satisfy the dependency given in $dep sub ResolveDeps ($$@) { my ($this, $dep, @pending) = @_; unless ($Interactive or (($this->{'follow_deps'} and ($this->{'follow_deps'} == 1)))) { return 0; } $dep =~ s/^(\S+).*$/$1/; $dep =~ s/^(.+)-[^-]+-[^-]+$/$1/; foreach my $pkg (@pending) { if ($pkg->{'Name'} eq $dep) { # Already a pending dependency... return -1; } } Inform (" Trying to resolve dependency for $this->{'Name'}: $dep\n"); # Check package names in queue foreach my $pkg (keys %{$InterData}) { if ($pkg eq $dep) { return (AskAboutDep($this, $pkg)); } } # Check "provides" lists for RPMs in queue and that are local files my $ret = CheckProvides($this, $dep, 0, @pending); if ((ref $ret) or ($ret and ($ret == -1))) { return $ret; } # Check "provides" lists for RPMs in queue and that are remote files $ret = CheckProvides($this, $dep, 1, @pending); if ((ref $ret) or ($ret and ($ret == -1))) { return $ret; } return 0; } sub ShowPackageErrors ($) { my ($pkg) = @_; $pkg->{'Errors'} = 0; my $local = GetVersion($pkg->{'Local'}); if ((system ("$Settings{'rpm_location'} -q '$pkg->{'Name'}-$pkg->{'Version'}' >/dev/null 2>/dev/null") == 0) and (not $pkg->{'Already'})) { # Installed successfully my $actioned = ($local ? "(Upgraded from $local)" : 'Installed'); Report(" Package $pkg->{'Name'}-$pkg->{'Version'} $actioned\n"); PackageInstalled($pkg); } else { # Not installed my $action = ($local ? "(Upgrade from $local)" : 'Install'); Report(" Package $pkg->{'Name'}-$pkg->{'Version'} $action FAILED!\n"); PackageFailed($pkg); } if ($pkg->{'Deps'}) { foreach (@{$pkg->{'Deps'}}) { $pkg->{'Errors'}++; Report(" Needs Dependency: $_\n"); } } if ($pkg->{'DepsOnMe'}) { foreach (@{$pkg->{'DepsOnMe'}}) { $pkg->{'Errors'}++; Report(" $_ depends on current version\n"); } } if ($pkg->{'Conflicts'}) { foreach (@{$pkg->{'Conflicts'}}) { $pkg->{'Errors'}++; if (my ($other, $file) = split /:/, $_, 2) { Report(" Conflicts with file $file from package $other\n"); } else { Report(" Conflicts with package $_\n"); } } } if ($pkg->{'OtherErrors'}) { foreach (@{$pkg->{'OtherErrors'}}) { $pkg->{'Errors'}++; Report(" ERROR: $_\n"); } } if ($pkg->{'Newer'}) { Report(" Newer package ($pkg->{'Newer'}) already installed\n"); } if ($pkg->{'Already'}) { Report(" Same version was already installed\n"); } if ($pkg->{'Corrupt'}) { Report(" RPM File is corrupt or unreadable: $pkg->{'Corrupt'}\n"); } if ($pkg->{'Warnings'}) { foreach (@{$pkg->{'Warnings'}}) { Report(" Warning: $_\n"); } } } sub InstallPackages ($$) { my ($data, $opts) = @_; my ($pkg, @packages, $Fails, @tmp, @Warnings, $count, $lastcount); return unless (ref $data and (keys %{$data})); Inform " Installing Packages...\n"; $Fails = 1; $lastcount = 0; foreach $pkg (keys %{$data}) { if ((ref $data->{$pkg}) eq 'ARRAY') { ResetPackage(\%{$data->{$pkg}->[0]}); } } while ($Fails > 0) { $Fails = 0; @packages = (); $count = 0; foreach $pkg (keys %{$data}) { if ((ref $data->{$pkg}) eq 'ARRAY') { if ($data->{$pkg}->[0]{'Result'} eq 'pending') { push @packages, \%{$data->{$pkg}->[0]}; $count++; } } } if ($count == $lastcount) { Report("ERROR: RPM could not determine which packages to remove from install list\n"); push @Warnings, UnknownFailures($data, $opts); last; } $lastcount = $count; ($Fails, @tmp) = InstallRPMs($data, $opts, @packages); if ($Fails == -1) { push @Warnings, UnknownFailures($data, $opts); $Fails = 0; } push @Warnings, @tmp; } if (@Warnings) { Inform " Warnings:\n"; foreach (@Warnings) { Report(" $_\n"); } } foreach $pkg (keys %{$data}) { if ((ref $data->{$pkg}) eq 'ARRAY') { ShowPackageErrors($data->{$pkg}->[0]); } } } sub Do_Auto_Queue { # Process the queue InstallPackages($Auto, undef); # Append failures to interactive queue (or auto-ignore as the case may be) my $pkg; my %inter; foreach $pkg (keys %{$Auto}) { next unless ((ref $Auto->{$pkg}) eq 'ARRAY'); if (($Auto->{$pkg}->[0]->{'Result'} eq 'failure') or ($Auto->{$pkg}->[0]->{'Result'} eq 'pending')) { if ($Auto->{$pkg}->[0]->{'IgnoreOnFailure'}) { Add_Auto_Ignore($Auto->{$pkg}->[0]->{'AutoIgnoreString'}); } else { # Add to interactive queue $InterData->{$pkg}->[0] = $Auto->{$pkg}->[0]; } } else { if ($InterData->{$pkg}) { delete $InterData->{$pkg}; } } } } my $term = undef; my $prompt = 'AutoRPM> '; my $attribs; # Allows some system commands to be run without ! in front my %SysCmds = ( 'rpm' => $Settings{'rpm_location'}, ); my %Aliases = ( '?' => 'help', 'quit' => 'exit', 'ls' => 'list', 'rm' => 'remove', 'upgrade' => 'install', ); my %Commands = ( 'help' => \&HelpCommand, 'abort' => \&AbortCommand, 'save' => \&SaveCommand, 'exit' => \&ExitCommand, 'set' => \&SetCommand, 'install' => \&InstallCommand, 'remove' => \&RemoveCommand, 'add' => \&AddCommand, 'list' => \&ListCommand, 'info' => \&InfoCommand, 'fullinfo' => \&FullInfoCommand, 'cd' => \&CDCommand, 'auto' => \&AutoCommand, ); my @cmdlist = (keys %Commands, keys %Aliases, keys %SysCmds); my %VarChoices = ( 'tips' => [ 'on', 'off'], 'color' => [ 'on', 'off'], 'ftp_passive_mode' => [ 'on', 'off'], 'ftp_hash' => [ 'on', 'off'], 'ftp_hash_size' => [ '_Number Of Seconds'], 'show_host' => [ 'full', 'short', 'none'], 'show_rc' => [ 'on', 'off'], 'expand_queue_entries' => [ 'on', 'off'], 'run_system_cmds' => [ 'on', 'off' ], 'rpm_install_opt' => [ '_CmdLineOptions' ], 'debug' => [ 'on', 'off'], 'rpm_location' => [ '_Filename' ], 'show_auto_ignore' => [ 'on', 'off' ], 'interactive_deps' => [ 'on', 'off' ], ); my %Vars = ( 'tips' => "Whether to show tips on startup", 'color' => "Whether to use color in interactive mode", 'ftp_passive_mode' => "Whether to use passive-mode FTP", 'show_auto_ignore' => "If turned on, files ignored because of ${TempDir}auto-ignore\nwill be displayed when running in interactive mode.", 'interactive_deps' => "If enabled, AutoRPM will always prompt before following\ndependencies in interactive mode.", 'ftp_hash' => "Whether to show hash marks for FTP transfers", 'ftp_hash_size' => "Bytes per ftp hash mark", 'show_host' => "Show hostname in prompt? (full, short, none)", 'show_rc' => "Show return code when executing system commands?", 'expand_queue_entries' => "Look for queue entries in system commands and expand?\n(i.e. '!rpm -qip autorpm' will execute on the autorpm package in the queue)", 'run_system_cmds' => "Allow system commands to be run without a preceeding !", 'rpm_install_opt' => "Flags to be passed to RPM when installing RPMs", 'debug' => "Show verbose debugging information (only for current session)?", 'rpm_location' => 'The location of the RPM binary and any global options', ); my @varlist = (keys %Vars); # Shows mem info in list form sub ShowListInfo ($) { my ($ref) = @_; my $type; if ($ref->{'Local'}) { $type = "[$Colors{Cyan}Update$Colors{Normal}]"; } else { $type = "[$Colors{Normal}New$Colors{Normal} ]"; } my $errors = ''; if ($ref->{'Errors'}) { if ($ref->{'Errors'} == 1) { $errors = "$Colors{BrightRed}($ref->{'Errors'} Error)$Colors{Normal}"; } else { $errors = "$Colors{BrightRed}($ref->{'Errors'} Errors)$Colors{Normal}"; } } print "$type $ref->{'Name'}-$ref->{'Version'} $errors\n"; } sub PadString ($$$) { my ($str, $len, $front) = @_; my $l = length $str; $l = ($len - $l); while ($l > 0) { $l--; if ($front) { $str = " $str"; } else { $str .= ' '; } } return ($str); } # Shows mem info in list form (info on the package already in memory) sub ShowBriefInfo { my ($name, $ref) = @_; print " $Colors{Blue}-$Colors{BrightCyan}-$Colors{BrightWhite}= $name =$Colors{BrightCyan}-$Colors{Blue}-$Colors{Normal}\n"; print "$Colors{Yellow}Type $Colors{Normal}: ", ($ref->{'Local'} ? "$Colors{Cyan}Update$Colors{Normal} (Current Version is $Colors{Cyan}" . GetVersion($ref->{'Local'}) . "$Colors{Normal})" : 'New (no version installed)'), "\n"; print "$Colors{Yellow}Architecture $Colors{Normal}: $ref->{'Arch'}"; if ($ref->{'Size'}) { my $sizeKB = $ref->{'Size'} / 1000; print " $Colors{Yellow}Size$Colors{Normal}: ${sizeKB}KB\n"; } else { print "\n"; } if (-f $ref->{'Orig'}) { print "$Colors{Yellow}Original File $Colors{Normal}: $ref->{'Orig'}\n"; } if (-f $ref->{'File'}) { print "$Colors{Yellow}Local File $Colors{Normal}: $ref->{'File'}\n"; } if ($ref->{'Errors'}) { print "$Colors{BrightRed} \[# Errors\] $Colors{Normal}: $ref->{'Errors'}\n"; my $type; foreach $type ('Deps', 'DepsOnMe', 'Conflicts', 'OtherErrors') { if ($ref->{$type}) { print "$Colors{BrightRed}", PadString($type, 13, 1), " $Colors{Normal}: "; my $first = 1; my $error; foreach $error (@{$ref->{$type}}) { if ($first) { $first = 0; print "$error\n"; } else { print " $error\n"; } } } } } } # Shows full info on package (by running RPM commands) sub ShowFullInfo { ShowBriefInfo (@_); my ($name, $ref) = @_; Get_Remote_File($ref->{'Orig'}, $ref->{'File'}); system("$Settings{'rpm_location'} -qip $ref->{'File'}"); } # This function generates a package structure for a given file sub ProcessFile ($) { my ($file) = @_; if (-f $file and ($file =~ /\.rpm$/)) { my ($cwd, $Data); unless ($file =~ m=^/=) { chomp($cwd = `pwd`); $file = "$cwd/$file"; } $Data->{'FTP'} = 0; $Data->{'updated'}->{'return_value'} = 1; $Data->{'new'}->{'return_value'} = 1; $Data->{'updated'}->{'delete_after_install'}->[0] = 0; $Data->{'new'}->{'delete_after_install'}->[0] = 0; $Data->{'updated'}->{'install'}->[0] = 1; #Interactive mode $Data->{'new'}->{'install'}->[0] = 1; #Interactive mode Process_Local($Data); return (Process_RPM_File($Data, $file)); } return undef; } # This function generates interactive queue entries for a given local or FTP dir sub ProcessDir ($$) { my ($dir, $recursive) = @_; my (%ret, $FTPHost, $FTPDir, $cwd, $Data); if ($dir =~ m=^ftp://=) { $Data->{'FTP'} = 1; } elsif ( ($FTPHost, $FTPDir) = ($dir =~ m=^([^:]+):(\/.+)$= ) ) { $dir = 'ftp://' . $FTPHost . $FTPDir; $Data->{'FTP'} = 1; } elsif (-d $dir) { unless ($dir =~ m=^/=) { chomp($cwd = `pwd`); $dir = "$cwd/$dir/"; } $Data->{'FTP'} = 0; } else { return 0; } $Data->{'recursive'}->[0] = $recursive; $Data->{'SourceLocation'} = $dir; $Data->{'updated'}->{'delete_after_install'}->[0] = 0; $Data->{'new'}->{'delete_after_install'}->[0] = 0; $Data->{'updated'}->{'install'}->[0] = 1; #Interactive mode $Data->{'new'}->{'install'}->[0] = 1; #Interactive mode $Data->{'AutoIgnore'} = 0; Process_Local($Data); Process_Remote($Data); return 1; } sub PkgGlob ($) { my ($glob) = @_; my @ret; # First, quote any dangerous characters $glob = "\Q$glob"; # Now, turn * (now \*) into .* $glob =~ s/\\\*/.*/g; # Now, turn ? (now \?) into . $glob =~ s/\\\?/./g; foreach (keys %{$InterData}) { if (((ref $InterData->{$_}) eq 'ARRAY') and ($_ =~ /^$glob$/)) { push @ret, $InterData->{$_}->[0]; } } return (@ret); } sub FileGlob ($) { my ($glob) = @_; my $tmp; my @ret; foreach (glob ($glob)) { if ($tmp = ProcessFile($_)) { push @ret, $tmp; } } return (@ret); } sub AddToArrayIfUnique ($$) { my ($ref, $item) = @_; foreach (@{$ref}) { return if ($_ eq $item); } push @{$ref}, $item; } sub byname { $a->{'Name'} cmp $b->{'Name'}; } # This function takes in command line arguments and returns an array of # references to package structures sub GetPackages { my ($arg, @ret, @tmp, $ref); foreach $arg (@_) { if ($arg eq 'all') { # All packages in interactive queue foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { push @ret, $InterData->{$_}->[0]; } } } elsif ($arg =~ /^errors*$/) { foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { AddToArrayIfUnique(\@ret, $InterData->{$_}->[0]) if ($InterData->{$_}->[0]->{'Errors'}); } } } elsif ($arg =~ /^!errors*$/) { foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { AddToArrayIfUnique(\@ret, $InterData->{$_}->[0]) unless ($InterData->{$_}->[0]->{'Errors'}); } } } elsif ($arg =~ /^updates*$/) { foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { AddToArrayIfUnique(\@ret, $InterData->{$_}->[0]) if ($InterData->{$_}->[0]->{'Local'}); } } } elsif ($arg =~ /^!updates*$/) { foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { AddToArrayIfUnique(\@ret, $InterData->{$_}->[0]) unless ($InterData->{$_}->[0]->{'Local'}); } } } elsif ($InterData->{$arg} and (ref $InterData->{$arg} eq 'ARRAY') and $InterData->{$arg}->[0]) { # A package in the queue (partial name... i.e. 'autorpm') AddToArrayIfUnique(\@ret, $InterData->{$arg}->[0]); } elsif ($ref = FindPkg($InterData, $arg)) { # A package in the queue (full name... i.e. 'autorpm-2.5-1') AddToArrayIfUnique(\@ret, $ref); } elsif (-f $arg) { # File if ($ref = ProcessFile($arg)) { push @ret, $ref; } } elsif ($arg =~ /^\//) { # Begins with a /, assume it is a file glob push @ret, FileGlob($arg); } else { if (@tmp = PkgGlob($arg)) { # Package glob returned results foreach (@tmp) { AddToArrayIfUnique(\@ret, $_); } } else { # Try file glob if (@tmp = FileGlob($arg)) { # File glob returned results push @ret, @tmp; } else { print STDERR "Not a valid package: $arg (ignoring)\n"; } } } } # Sort return array by name return (sort byname @ret); } sub InstallCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "installs RPM(s) on system"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: install [--nodeps] [--force] \n"; print " Installs the given RPM(s) on the systems\n"; print " (run 'help rpmlist' for details of parameter)\n"; print " NOTE: 'upgrade' and 'install' identical if --force not used\n"; return; } unless (@_) { print "What packages??\n"; return; } my %list; my $pkg; my $opts = ''; while ($_[0] =~ /^--/) { if ($_[0] eq 'force') { $opts .= 'f'; } elsif ($_[0] eq 'nodeps') { $opts .= 'n'; } } foreach $pkg (GetPackages(@_)) { $list{$pkg->{'Name'}}->[0] = $pkg; } InstallPackages(\%list, $opts); # Check results foreach $pkg (keys %list) { if ($list{$pkg}->[0]->{'Result'} eq 'invalid') { if ($InterData->{$pkg}) { $Debug and Report("DEBUG: Removing invalid package from queue: $pkg\n"); delete $InterData->{$pkg}; } } elsif ($list{$pkg}->[0]->{'Result'} eq 'success') { if ($InterData->{$pkg}) { $Debug and Report("DEBUG: Removing package from queue after install: $pkg\n"); delete $InterData->{$pkg}; } } } } sub AddCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "adds RPMs to the interactive queue"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: add --recursive \n"; print " Adds RPMs to the interactive queue\n"; print " --recursive will cause subdirectories to be recursed\n"; print " May specify local or remote directories or individual local files.\n"; print " (run 'help filedirlist' for details of parameter)\n"; return; } my $recursive = 0; while ($_[0]) { if ($_[0] =~ /--recursive/) { $recursive = 1; shift; } else { last; } } unless (@_) { print "What files or directories??\n"; return; } my @tmp; foreach (@_) { unless (ProcessDir($_, $recursive)) { if (-f $_) { my $ref = ProcessFile($_); if ($ref) { AddPackageToHash($InterData, $ref); } } else { # Try file glob if (@tmp = FileGlob($_)) { # File glob returned results foreach (@tmp) { AddPackageToHash($InterData, $_); } } else { print STDERR "Not a valid file or directory: $_ (ignoring)\n"; } } } } } sub RemoveCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "removes RPMs from the interactive queue"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: remove \n"; print " Removes the RPM(s) from the interactive queue\n"; print " The specific version of each package will be ignored in the future.\n"; print " NOTE: Actual file will only be deleted if it was auto-downloaded.\n"; print " (run 'help rpmlist' for details of parameter)\n"; return; } unless (@_) { print "What packages??\n"; return; } my $pkg; foreach (GetPackages(@_)) { $pkg = $_->{'Name'}; if ($InterData->{$pkg} and $InterData->{$pkg}->[0]) { Add_Auto_Ignore($InterData->{$pkg}->[0]->{'AutoIgnoreString'}); if ($InterData->{$pkg}->[0]->{'Delete'} and ($InterData->{$pkg}->[0]->{'Delete'} == 1)) { unlink ($InterData->{$pkg}->[0]->{'File'}); } delete $InterData->{$pkg}; } } } sub FullInfoCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "displays full information about one or more RPMs"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: fullinfo [--sig] [--files] [--deps] [--provides] [--scripts] \n"; print " Displays full information on the given RPM\n"; print " --sig: Also check package signature\n"; print " --files: Also lists files in package\n"; print " --deps: Also lists dependencies of the package\n"; print " --provides: Also lists what the package provides\n"; print " --scripts: Also lists the packages' installation scripts\n"; print " NOTE: May require file to be downloaded!\n"; print " (run 'help rpmlist' for details of parameter)\n"; return; } # Process arguments my %args; while ($_[0]) { if ($_[0] =~ /--(.+)/) { $args{$1} = 1; shift; } else { last; } } unless (@_) { print "What packages??\n"; return; } my $pkg; foreach $pkg (GetPackages(@_)) { ShowFullInfo($pkg->{'Name'}, $pkg); foreach (keys %args) { print " $Colors{Cyan}*$Colors{Yellow} $_ $Colors{Cyan}*$Colors{Normal}\n"; if ($_ eq 'sig') { unless ($ENV{'GNUPGHOME'}) { # Set path if necessary $ENV{'GNUPGHOME'} = $GPGPath; } system("$Settings{'rpm_location'} --checksig $pkg->{'File'} 2>&1"); } elsif ($_ eq 'files') { system("$Settings{'rpm_location'} -qlp $pkg->{'File'} 2>&1"); } elsif ($_ eq 'deps' or $_ eq 'requires') { system("$Settings{'rpm_location'} -q --requires -p $pkg->{'File'} 2>&1"); } elsif ($_ eq 'provides') { system("$Settings{'rpm_location'} -q --provides -p $pkg->{'File'} 2>&1"); } elsif ($_ eq 'scripts') { system("$Settings{'rpm_location'} -q --scripts -p $pkg->{'File'} 2>&1"); } } } } sub InfoCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "displays brief information about one or more RPMs"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: info \n"; print " Displays information on the given RPM\n"; print " NOTE: use command 'fullinfo' for more detail.\n"; print " (run 'help rpmlist' for details of parameter)\n"; return; } unless (@_) { print "What packages??\n"; return; } foreach (GetPackages(@_)) { ShowBriefInfo($_->{'Name'}, $_); print "\n"; } } sub ListCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "lists RPMs in the queue"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: list []\n"; print " Lists all RPMs in the interactive queue\n"; print " (or the ones that are given as arguments)\n"; return; } # List interactive RPMs unless (@_) { push @_, 'all'; } foreach (GetPackages(@_)) { ShowListInfo($_); } } sub CDCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "changes local directory"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: cd \n"; print " changes to the given directory\n"; return; } if ($_[0]) { chdir("$_[0]"); } else { chdir("$ENV{'HOME'}"); } } sub ExitCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "exits interactive mode (saves changes)"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: exit\n"; print " Exits the program and saves changes\n"; return; } WriteData(); # We want to quit out of interactive mode $Interactive = 0; } sub SetCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "Displays or modifies settings"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: set [name [value]]\n"; print " No parameters will display list of settings\n"; print " Provide a name to see the current value and description\n"; print " Provide a name and value to set the setting\n"; return; } unless (@_) { print "Available Settings:\n"; foreach (sort @varlist) { print "$_ "; } print "\n"; } elsif ($#_ eq 0) { print "$_[0]: $Colors{'Yellow'}$Settings{$_[0]}$Colors{'Normal'}\n"; print "$Vars{$_[0]}\nPossible Values: "; foreach (@{$VarChoices{$_[0]}}) { if (s/^_//) { print "$_ "; } else { print "'$_' "; } } print "\n"; } else { my ($var, @val) = @_; $Settings{$var} = "@val"; ProcessSettings(); } } sub AbortCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "aborts interactive mode (does not save changes)"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: abort\n"; print " Aborts the program without saving changes\n"; print " NOTE: any RPMs already installed will remain installed\n"; return; } exit 1; } sub SaveCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "Saves all changes made so far to the queue"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: save\n"; print " Saves all changes made to the interactive queue and settings\n"; return; } WriteQueue('interactive', $InterData, 0) or print STDERR "Could not write to interactive queue!\n"; WriteSettings(); } sub HelpCommand { if ($_[0] and $_[0] eq 'brief_desc') { print "provides help on the various commands"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: help [command]\n"; print " If no command is given, displays list of commands\n"; print " If command is given, displays help on that command\n"; print " \"command\" can also be 'rpmlist' or 'filedirlist'\n"; return; } if (@_) { if ($_[0] eq 'rpmlist') { print " is a flexible specification of a list of RPMs\n"; print "It can contain any number of the following (separated by whitespace):\n"; print " RPM names in queue (i.e. blah-1.0-1)\n"; print " Pattern Matching Expressions (i.e. blah-*)\n"; print " Local filename (full path or file in current directory)\n"; print " 'all' matches all RPMs in queue\n"; print " 'update(s)' matches only updated RPMs (older version already installed)\n"; print " 'error(s)' matches only RPMs that have had installation errors\n"; print "(NOTE: the same package will not end up in the list twice, even if it matches\n"; print " more than one argument)\n"; return; } elsif ($_[0] eq 'filedirlist') { print " is a flexible specification of a list of files and directories\n"; print "It can contain any number of the following (separated by whitespace):\n"; print " Local filename (full path or file in current directory)\n"; print " Local directory (full path or relative to current directory)\n"; print " File Patterns (i.e. blah-*)\n"; print " Remote directory such as:\n"; print " ftp://ftp.autorpm.org/pub/redhat/RPMS/noarch/\n"; print " ftp.autorpm.org:/pub/redhat/RPMS/noarch/\n"; print " When directories are given, all of the files in that directory are processed\n"; print " and entered into the interactive queue (nothing is automatically installed)\n"; return; } else { # Give help on a command if ($Aliases{$_[0]}) { $Commands{$Aliases{$_[0]}}->('full_help'); } elsif ($Commands{$_[0]}) { $Commands{$_[0]}->('full_help'); } else { print "help: no command \"$_[0]\"\n"; } } } else { my ($cmd, $aliases); foreach $cmd (sort @cmdlist) { if ($Commands{$cmd}) { print "$Colors{'Yellow'}$cmd$Colors{'Normal'}: "; $Commands{$cmd}->('brief_desc'); $aliases = ''; foreach (keys %Aliases) { if ($Aliases{$_} eq $cmd) { $aliases .= "$Colors{'BrightBlue'}$_$Colors{'Normal'} "; } } if ($aliases =~ s/ $//) { print " [Aliases: $aliases]"; } print "\n"; } } } } my $_quecomp; my @quelist; my @Specials = ('all', 'updates', 'errors'); sub QueueCompletions { my ($text, $state) = @_; if ($state) { $_quecomp++; } else { $_quecomp = -($#Specials+1); @quelist = (); foreach (keys %{$InterData}) { if ((ref $InterData->{$_}) eq 'ARRAY') { push @quelist, "$_-$InterData->{$_}->[0]->{'Version'}"; } } } if ($_quecomp < 0) { for (; $_quecomp < 0; $_quecomp++) { return $Specials[-($_quecomp+1)] if ($Specials[-($_quecomp+1)] =~ /^\Q$text/); } } for (; $_quecomp <= $#quelist; $_quecomp++) { return $quelist[$_quecomp] if ($quelist[$_quecomp] =~ /^\Q$text/); } return undef; } my $_varcomp; sub VarCompletions { my ($text, $state) = @_; if ($state) { $_varcomp++; } else { $_varcomp = 0; } for (; $_varcomp <= $#varlist; $_varcomp++) { return $varlist[$_varcomp] if ($varlist[$_varcomp] =~ /^\Q$text/); } return undef; } my $_vvcomp; my $_vvname; sub VarValCompletions { my ($text, $state) = @_; if ($state) { $_vvcomp++; } else { $_vvcomp = 0; } unless ($VarChoices{$_vvname}) { return undef; } for (; $_vvcomp < (scalar @{$VarChoices{$_vvname}}); $_vvcomp++) { if ($VarChoices{$_vvname}[$_vvcomp] =~ /^_/) { next; } return $VarChoices{$_vvname}[$_vvcomp] if ($VarChoices{$_vvname}[$_vvcomp] =~ /^\Q$text/); } return undef; } my $_cmdcomp; sub CmdCompletions { my ($text, $state) = @_; if ($state) { $_cmdcomp++; } else { $_cmdcomp = 0; } for (; $_cmdcomp <= $#cmdlist; $_cmdcomp++) { return $cmdlist[$_cmdcomp] if ($cmdlist[$_cmdcomp] =~ /^\Q$text/); } return undef; } sub Completion { my ($text, $line, $start, $end) = @_; if (($line =~ /^\s*!/) and ($text eq $line)) { # TBD: should complete system command... but does not at this point } elsif ($text eq $line) { return ($term->completion_matches($text, \&CmdCompletions)); } elsif ($line =~ /^help\s+/) { return ($term->completion_matches($text, \&CmdCompletions)); } elsif ($line =~ /^set\s+/) { if ($line =~ /^set\s+([^ ]+)\s+/) { if ($line =~ /^set\s+([^ ]+)\s+\S*$/) { # Entering value for a variable # Set name for the completion function $_vvname = $1; return ($term->completion_matches($text, \&VarValCompletions)); } else { return (); } } else { return ($term->completion_matches($text, \&VarCompletions)); } } elsif ($line =~ /^(cd|add)/) { return ($term->completion_matches($text, $attribs->{'filename_completion_function'})); } elsif ($text =~ /^\//) { return ($term->completion_matches($text, $attribs->{'filename_completion_function'})); } elsif (keys %{$InterData}) { return ($term->completion_matches($text, \&QueueCompletions)); } else { return ($term->completion_matches($text, $attribs->{'filename_completion_function'})); } } sub ReadSettings { my ($name, $val, $line); return unless (-s $TempDir . 'settings'); unless (open (SETTINGS, $TempDir . 'settings')) { print STDERR "Cannot open settings file (${TempDir}settings)\n"; return; } while (defined($line = )) { chomp($line); ($name, $val) = ($line =~ /^(.+)\s*=\s*(.*)$/); $name and $Settings{$name} = $val; } close (SETTINGS); } sub ProcessSettings { if ($Settings{'debug'} eq 'on') { $Debug = 1; } if ($Settings{'show_host'} eq 'full') { $prompt = "AutoRPM\@$HostName> "; } elsif ($Settings{'show_host'} eq 'short') { my $short = $HostName; $short =~ s/(^[^.]+)\..*$/$1/; $prompt = "AutoRPM\@$short> "; } else { $prompt = 'AutoRPM> '; } %Colors = %AllColors; # Disable colors if necessary unless ($Settings{'color'} eq 'on') { foreach (%Colors) { $Colors{$_} = ''; } } } sub WriteSettings { unless (open (SETTINGS, '>' . $TempDir . 'settings')) { print STDERR "Cannot write settings file (${TempDir}settings)\n"; return; } foreach (keys %Settings) { unless ($_ eq 'debug') { print SETTINGS "$_=$Settings{$_}\n"; } } close (SETTINGS); } sub ReadData { ReadSettings(); ProcessSettings(); $InterData = ReadQueue('interactive'); } sub ShowTip { open (TIPS, "$BaseDir/support/tips") or return; print " -= AutoRPM Tip ('set tips off' to disable) =-\n\n"; my $count = ; my $pick = int rand $count; my $line; while (($pick > 0) and (defined($line = ))) { if ($line =~ /^==/) { $pick--; } } while (defined($line = )) { last if ($line =~ /^==/); } while (defined($line = )) { last if ($line =~ /^==/); print $line; } close (TIPS); print "\n"; } sub GetYN { my ($text, $default) = @_; if ($text) { print "$text "; if ($default) { if ($default =~ /[yY]/) { print '([Y]/n) '; } else { print '(y/[N]) '; } } else { print '(Y/N) '; } } my $answer = ; chomp($answer); if ($answer =~ /^[yY]/) { return 1; } if (($answer eq '') and ($default =~ /[yY]/)) { return 1; } return 0; } sub ShowFile ($) { print "\n"; open (FILE, "$_[0]") or return 0; print ; close (FILE); print "\n"; return 1; } sub CheckFirstRun { unless (-s $TempDir . '.version') { if (GetYN("This appears to be the first time you have run AutoRPM.\nWould you like to read a brief introduction?", 'N')) { ShowFile("$BaseDir/support/introduction.txt"); } } else { open (LASTVER, $TempDir . '.version') or return; my $lastver = ; close (LASTVER); chomp($lastver); if ($lastver eq $Version) { return; } if (-r "$BaseDir/support/changes-$Version") { if (GetYN("This appears to be the first time you have run version $Version.\nWould you like to read a brief change summary?", 'N')) { ShowFile("$BaseDir/support/changes-$Version"); } } } # create file open (LASTVER, '>' . $TempDir . '.version') or die "ERROR: Can't write to temp dir $TempDir: $!\n"; print LASTVER "$Version\n"; close (LASTVER); } sub InteractiveMode { ReadData(); CheckFirstRun(); $term = new Term::ReadLine 'AutoRPM'; my $type = $term->ReadLine(); unless ($type =~ /Gnu$/) { print "\n* It is highly recommended that you install the Perl module Term::ReadLine::Gnu\n"; print "* This will provide you with history, tab completion, and other nice features.\n\n"; unless (-f $TempDir . '.asked') { if (GetYN("Would you like to try to install that package now (i386)?", 'N')) { if (-f '/etc/fedora-release') { AutoCommand("TermReadlineGnu-fedora.conf"); } elsif (-f '/etc/redhat-release') { AutoCommand("TermReadlineGnu.conf"); } print "\nPlease restart AutoRPM for the improved interface to take effect\n\n"; exit 0; } else { system ("touch $TempDir/.asked >/dev/null 2>&1"); } } } ($Settings{'tips'} eq 'on') and ShowTip(); CountInter(); $attribs = $term->Attribs; $attribs->{'completion_function'} = \&Completion; # Restore and limit history my $features = $term->Features; if ($features->{'getHistory'}) { if (open (HISTORY, $TempDir . 'history')) { $term->StifleHistory($MaxHistory); $term->SetHistory(); close (HISTORY); } } my ($line, $cmd, @args); while ( $Interactive && defined ($line = $term->readline($prompt))) { $line =~ s/^\s+//; $line =~ s/\s+$//; $line and ProcessCommand($line); } print "\n"; WriteData(); } sub WriteData { # Save queues WriteQueue('interactive', $InterData, 0) or print STDERR "Could not create interactive queue!\n"; WriteSettings(); if ($term) { # Save history (if possible) my $features = $term->Features; if ($features->{'getHistory'}) { my $last = ''; if (open (HISTORY, '>' . $TempDir . 'history')) { foreach ($term->GetHistory()) { $_ =~ s/^\s+//; $_ =~ s/\s+$//; unless ($_ =~ /^(exit|quit|abort)/) { if ($_ and $_ ne $last) { $last = $_; print HISTORY "$_\n"; } } } close (HISTORY); } } } } sub ExecSystemCmd (@) { my ($cmd, @args) = @_; if ($Settings{'expand_queue_entries'} eq 'on') { my $ref; for (my $i = 0; $i <= $#args; $i++) { $ref = undef; if ($InterData->{$args[$i]} and (ref $InterData->{$args[$i]} eq 'ARRAY') and $InterData->{$args[$i]}->[0]) { # A package in the queue (partial name... i.e. 'autorpm') $ref = $InterData->{$args[$i]}->[0]; } else { $ref = FindPkg($InterData, $args[$i]); } if ($ref) { if (Get_Remote_File($ref->{'Orig'}, $ref->{'File'})) { $args[$i] = $ref->{'File'}; } } } } system("$cmd @args"); if ($Settings{'show_rc'} eq 'on') { my $rc = $? >> 8; print "[rc=$rc]\n"; } } sub ProcessCommand ($) { my ($cmd, @args) = split /\s+/, $_[0]; if ($Aliases{$cmd}) { $cmd = $Aliases{$cmd}; } if ($cmd =~ s/^\s*!//) { ExecSystemCmd($cmd, @args); } elsif ($SysCmds{$cmd}) { ExecSystemCmd($SysCmds{$cmd}, @args); } elsif ($Commands{$cmd}) { $Commands{$cmd}->(@args); } elsif ($Settings{'run_system_cmds'} eq 'on') { ExecSystemCmd($cmd, @args); } else { print "'$cmd' is not an internal command, and since the\n"; print "'run_system_cmds' setting is not enabled, you must preceed a command\n"; print "with a '!' to run a system command\n"; } } ############################################################################## # Main ############################################################################## sub VersionCheck { LoadArchs(); $Debug = 1; $ForcePrint = 1; print "This mode will test the version comparison code.\n"; print "Just type CTRL-C to quit...\n\n"; print "Which RPM should lose if version is lower but arch is higher (0/1/2)?\n"; my $Reduce = ; chomp($Reduce); while (1) { print 'Type in filename of package one: '; my $RPM1 = ; print 'Type in filename of package two: '; my $RPM2 = ; my $pkg1 = RPM_Split_Name($RPM1); my $pkg2 = RPM_Split_Name($RPM2); unless ($pkg1) { print "ERROR: Invalid RPM: $RPM1\n"; next; } unless ($pkg2) { print "ERROR: Invalid RPM: $RPM2\n"; next; } my ($Name1, $Version1, $Arch1) = ($pkg1->{'Name'}, $pkg1->{'Version'}, $pkg1->{'Arch'}); my ($Name2, $Version2, $Arch2) = ($pkg2->{'Name'}, $pkg2->{'Version'}, $pkg2->{'Arch'}); print "Package #1:\n Name: $Name1\n Version: $Version1\n Arch: $Arch1\n"; print "Package #2:\n Name: $Name2\n Version: $Version2\n Arch: $Arch2\n"; my $Result = RPM_Compare_Version($pkg1, $pkg2, $Reduce); if ($Result < 0) { print "Package two is newer.\n\n"; } if ($Result > 0) { print "Package one is newer.\n\n"; } if ($Result == 0) { print "Package one is the same as package two.\n\n"; } } } sub DoConfig ($) { my ($conf) = @_; unless ($conf =~ /\//) { $conf = "$BaseDir/$conf"; } $Interactive and Inform ("Processing config file: $conf\n"); Read_Config($conf); } sub Remove_Packages () { if ($DeleteOption or $ReallyDeleteOption) { #Inform ("Unmatched local files:\n"); my $delete = 0; if ($DeleteOption and $ReallyDeleteOption) { $delete = 1; } my $tmp; my @todel; my $found = 0; my %Exclude; if ($RemoveExclude) { open (EXCLUDE, $RemoveExclude) or die "Could not open Remove_Exclude $RemoveExclude: $!\n"; my $line; while ($line = ) { chomp($line); $Exclude{$line}++; } close (EXCLUDE); } foreach $tmp (keys %LocalFiles) { foreach (keys %{$LocalFiles{$tmp}}) { if (ref $LocalFiles{$tmp}{$_} eq 'ARRAY') { unless ($LocalFiles{$tmp}{$_}->[0]->{'Checked'}) { if ($Exclude{$LocalFiles{$tmp}{$_}->[0]->{'Name'}}) { next; } if ($AutoIgnored{$LocalFiles{$tmp}{$_}->[0]->{'Name'}}) { next; } if ($delete) { if ($tmp eq 'installed') { Report(" $LocalFiles{$tmp}{$_}->[0]->{'Name'} (Uninstalling...)\n"); push @todel, $_; } else { Report(" $LocalFiles{$tmp}{$_}->[0]->{'Orig'} (Deleting...)\n"); if (unlink($LocalFiles{$tmp}{$_}->[0]->{'Orig'})) { LogAction("store.log", "Removed $LocalFiles{$tmp}{$_}->[0]->{'Orig'}"); } } DeleteFromLocalCache($LocalFiles{$tmp}{$_}->[0]->{'Orig'}, $tmp); } else { $found++; if ($tmp eq 'installed') { Report(" $LocalFiles{$tmp}{$_}->[0]->{'Name'} (Would have uninstalled)\n"); } else { Report(" $LocalFiles{$tmp}{$_}->[0]->{'Orig'} (Would have deleted)\n"); } } } } } } if ($delete) { if (@todel) { foreach (`$Settings{'rpm_location'} -e $DeleteOptionArgs @todel 2>&1`) { Report(" $_"); } foreach (@todel) { if (system ("$Settings{'rpm_location'} -q '$_' >/dev/null 2>/dev/null") == 0) { # Not uninstalled Report(" Uninstall failed: $_\n"); } else { # Uninstalled LogAction("install.log", "Uninstalled $_"); } } } } elsif ($found > 0) { Report("\nThe $found files listed above would have been deleted or uninstalled if you\n"); Report("would have specified both Remove_Packages and Really_Remove_Packages. You rarely\n"); Report("want to use these options. If this is want you want to do, I would recommend\n"); Report("doing serious testing with only Remove_Packages specified (which will not actually\n"); Report("remove anything) before using the Really_Remove_Packages() command!\n"); } } } sub AutoCommand { # Check for help if ($_[0] and $_[0] eq 'brief_desc') { print "automatically processes RPMs as defined in autorpm.conf"; return; } elsif ($_[0] and $_[0] eq 'full_help') { print "Usage: auto [--print] [--delay=] [ ...]\n"; print " This is the automatic mode of AutoRPM, usually run from cron.\n"; print " Will process commands in $BaseDir/autorpm.conf\n"; print " (unless different file specified as argument)\n"; print " (Config files are expected to be in $BaseDir unless path given)\n"; print " --print will force all output to the screen regardless\n"; print " of the settings in the configuration file.\n"; print " --delay=XX will wait up to XX seconds before starting\n"; return; } # Process arguments while ($_[0]) { if ($_[0] =~ /--print/) { $ForcePrint = 1; shift; } elsif ($_[0] =~ /--delay\s*=\s*(\d+)/) { $StartDelay = $1; shift; } else { last; } } if (@_) { foreach (@_) { DoConfig($_); } } else { # Use default DoConfig("$BaseDir/autorpm.conf"); } Remove_Packages(); Check_Queues(); Mail_Reports(); } sub RemovePID () { if (-f $PIDFile) { unlink($PIDFile); } } sub main { # Get arguments my $Help = 0; my $GetVersion = 0; my $NoTTY = 0; my $NewPIDFile = ''; my $NewTempDir = ''; GetOptions ( 'd|debug' => \$Debug, 'notty' => \$NoTTY, 'v|version' => \$GetVersion, 'vcheck' => \$VersionCheckMode, 'pidfile=s' => \$NewPIDFile, 'tempdir=s' => \$NewTempDir, 'h|help' => \$Help ) or Usage(); $Help and Usage(); if ($GetVersion) { print 'AutoRPM Version ' . $Version . ', created ' . $VDate . "\n"; exit 0; } if ($VersionCheckMode) { VersionCheck(); exit 0; } if ($NewTempDir) { $TempDir = Check_Dir($NewTempDir); } if ($NewPIDFile) { $PIDFile = $NewPIDFile; } if (-f $PIDFile) { open (PID, $PIDFile) or die "Could not open PID file $PIDFile: $!\n"; my $pid = ; chomp($pid); $pid =~ s/\s+//g; if ($pid) { system("ps $pid >/dev/null 2>&1") or die "AutoRPM already running: PID=$pid\n"; } close (PID); } open (PID, ">$PIDFile") or die "Could not create PID file $PIDFile: $!\n"; print PID "$$\n"; close (PID); if ((not $NoTTY) and (-t STDIN) and (-t STDOUT)) { # Running manually $ForcePrint = 1; $Interactive = 1; } $TempDir = Check_Dir($TempDir); if (@ARGV) { ReadData(); foreach (@ARGV) { ProcessCommand($_); } WriteData(); } else { if ($Interactive) { InteractiveMode(); } else { # Probably trying to run autorpm the old way... die "ERROR: Must specify command or run interactively!\n"; } } } &main(); &RemovePID(); if (CountInter()) { exit 1; } else { exit 0; }