1package CVSMonitor::MetaData; 2 3# Provides an interface to the MetaData stored in the scratch area. 4 5# When refering to modules in this package, a special format can be 6# used. This involves seperating the name of the repository and module 7# with a period. 8# 9# e.g. RepositoryName.ModuleName 10# 11# If only the Module name is provided, we search through all the repositories 12# and if only one module has the name, it will be returned. 13 14require 5.005; 15use strict; 16use UNIVERSAL 'isa'; 17use Fcntl (); 18use Storable (); 19use File::Spec (); 20use Sort::Versions (); 21use Class::Autouse 'File::Flat'; 22 23 24 25 26 27##################################################################### 28# Globals 29 30use vars qw{$errstr}; 31BEGIN { $errstr = '' } 32 33 34 35 36##################################################################### 37# Inheritable constants and path stuff 38 39sub _fileIndex { 'index.dat' } 40sub _directoryRoot { 'CVSMonitor_MetaData_0_6' } 41sub pathAdminLogDir { File::Spec->catdir( $_[0]->root, 'adminlogs' ) } 42sub pathAdminLogFile { 43 my $self = shift; 44 45 # If they have provided a good PID, use that. Otherwise use 46 # the PID for the current process. 47 my $pid = $_[0] =~ /^\d+$/ ? shift : $$; 48 49 File::Spec->catfile( $self->pathAdminLogDir, "$pid.log" ); 50} 51 52 53 54 55 56##################################################################### 57 58# Constructors and basics 59# You should provide a temporary directory for the statistics 60# generator to use. It will need to create a lot of files. 61sub new { 62 my $class = shift; 63 my $base = shift; 64 my $root = $class->_checkWorkArea( $base ) or return undef; 65 my $pragma = shift || 'readonly'; 66 67 # Make sure the CVS client on this platform is OK 68 return undef unless $class->cvsok; 69 70 # Create the object 71 my $self = { 72 base => $base, 73 root => $root, 74 indexFile => File::Spec->catfile( $root, $class->_fileIndex ), 75 _REPOSITORIES => {}, # New modules structure 76 _DIRTY => 1, 77 _NEW => 1, 78 }; 79 bless $self, $class; 80 81 # If the index file exists, make sure we have write access to it 82 my $file = $self->{indexFile}; 83 if ( -e $file ) { 84 return $class->_error( "Directory is in the way of the index" ) if -d $file; 85 if ( -f $file ) { 86 # Can we write to the index file 87 return $class->_error( "No read access to old index file" ) unless -r $file; 88 return $class->_error( "No write access to old index file" ) unless -w $file; 89 } 90 } 91 92 # Save ourselves 93 $self->save( $pragma eq 'lock' ? 'keeplock' : undef ) or return undef; 94 95 $self; 96} 97 98sub load { 99 my $class = shift; 100 my $root = $class->_checkWorkArea( shift ) or return undef; 101 my $pragma = shift || 'readonly'; 102 return undef unless $pragma =~ /^(?:lock|readonly)$/; 103 104 # Does the file exist? 105 my $file = File::Spec->catfile( $root, $class->_fileIndex ) or return undef; 106 return $class->_error( "Index does not exist" ) unless -e $file; 107 return $class->_error( "Cannot read index file" ) unless -r $file; 108 109 # In order to avoid nasty destroy warnings during the quit() phase, we need 110 # To have all modules loaded that are likely to get ->DESTROY calls. That is, 111 # any classes present in the Storable object. 112 Class::Autouse->load( 'CVSMonitor::MetaData::Repository' ); 113 Class::Autouse->load( 'CVSMonitor::MetaData::Module' ); 114 115 # Get a handle to the file 116 open( METADATA, "+<$file" ) or return $class->_error( "Error opening file: $!" ); 117 my $fh = \*METADATA; 118 119 # Exclusive lock the file if we are going to use lock mode 120 if ( $pragma eq 'lock' ) { 121 flock( $fh, Fcntl::LOCK_EX() ) or return undef; 122 } 123 124 # Load the index file and return the CVSMonitor::MetaData object. 125 my $self = Storable::fd_retrieve( $fh ); 126 return undef unless isa( $self, $class ); 127 128 if ( $pragma eq 'lock' ) { 129 # Attach the filehandle to the object if locked. 130 # This allows the object to maintain the exclusive lock. 131 $self->{_FH} = $fh; 132 } else { 133 # Close the filehandle. 134 close $fh; 135 } 136 137 # Reconnect the Repository parent links 138 foreach ( values %{ $self->{_REPOSITORIES} } ) { 139 $_->{_PARENT} = $self; 140 } 141 142 $self; 143} 144 145# Save the index. 146# Serialize some basics and dump to disk 147sub save { 148 my $self = shift; 149 my $lock = shift; 150 unless ( exists $self->{_DIRTY} ) { 151 $self->releaseLock unless $lock; 152 return 1; 153 } 154 155 # Do we have a lock? 156 unless ( exists $self->{_FH} or exists $self->{_NEW} ) { 157 return $self->_error( "No lock on index file" ); 158 } 159 160 # Prepare to do the save 161 delete $self->{_DIRTY}; 162 if ( $self->{_NEW} ) { 163 # There should be no attached filehandle on new files 164 return undef if $self->{_FH}; 165 166 # Create a handle for the new file 167 delete $self->{_NEW}; 168 open( METADATA, "+>$self->{indexFile}" ) 169 or return $self->_error( "Error creating new index file" ); 170 $self->{_FH} = \*METADATA; 171 } 172 173 # Get the freezable structure 174 my $freezable = $self->_freezable or return undef; 175 176 # Truncate and seek 0 on the filehandle to prepare to save 177 seek $self->{_FH}, 0, 0; 178 truncate $self->{_FH}, 0; 179 180 # Freeze to the file. 181 Storable::store_fd( $freezable, $self->{_FH} ) or return undef; 182 183 # Do we want to keep a lock? 184 $self->releaseLock unless $lock; 185 186 # Make the index permissive 187 chmod 0666, $self->{indexFile}; 188 189 1; 190} 191 192# Load if we can, or create a new one 193sub loadOrNew { 194 my $class = shift; 195 my $root = shift; 196 my $pragma = shift || 'readonly'; 197 198 # Check ourselves if the index exists 199 my $base = $class->_checkWorkArea( $root ) or return undef; 200 my $file = File::Spec->catfile( $base, $class->_fileIndex ) or return undef; 201 if ( -e $file ) { 202 # Try to load it 203 return $class->load( $root, $pragma ); 204 } else { 205 # Create a new one 206 return $class->new( $root, $pragma ); 207 } 208} 209 210# Reload from the file, and get lock back. 211# The is an in place reload that works by overwriting the values 212# in the old object with values in a newly loaded object, and taking 213# the newly loaded object's lock. 214# If a module exists in the OLD CVSMonitor::MetaData, but not in the 215# NEW one, the Module will be reblessed as a 216# CVSMonitor::MetaData::Module::Deleted, which will catch any calls, 217# and return an error. This module, however, will NOT be available from 218# the MetaData objects ->getModule type methods. 219# Add a 'nolock' second argument to reload without locking. 220sub reload { 221 my $self = shift; 222 my $lock = shift; 223 my $class = ref $self; 224 $lock = (defined $lock and $lock eq 'nolock') ? undef : 'lock'; 225 my $replacement = $class->load( $self->base, $lock ) or return undef; 226 227 # Work out if any Repositories have been added 228 my %state = (); 229 foreach ( $replacement->getRepositoryNames ) { 230 $state{$_} = $self->getRepository($_) ? 'replace' : 'add'; 231 } 232 233 # Add or Replace the Repositories 234 foreach my $name ( keys %state ) { 235 if ( $state{$name} eq 'add' ) { 236 # Transfer the Repository from the replacement 237 $self->{_REPOSITORIES}->{$name} = $replacement->{_REPOSITORIES}->{$name}; 238 $self->{_REPOSITORIES}->{$name}->{_PARENT} = $self; 239 delete $replacement->{_REPOSITORIES}->{$name}; 240 } else { 241 # Reload the repository 242 $self->{_REPOSITORIES}->{$name}->_reload( $replacement->{_REPOSITORIES}->{$name} ) or return undef; 243 } 244 } 245 246 # Remove and replace all our scalar properties. 247 # One of these will be the file lock 248 foreach ( grep { $_ ne '_REPOSITORIES' } keys %$self ) { 249 delete $self->{$_}; 250 } 251 foreach ( grep { $_ ne '_REPOSITORIES' } keys %$replacement ) { 252 $self->{$_} = $replacement->{$_}; 253 } 254 255 # Since we have a copy of the replacement's index lock, remove their copy. 256 # Also, delete their module hash to remove circular dependencies so the object 257 # will immediately garbage collect. 258 delete $replacement->{_FH}; 259 delete $replacement->{_REPOSITORIES}; 260 261 1; 262} 263 264# Remove ALL module, delete the root directory, and start fresh. 265# ->_reloadDelete all modules will ensure nobody can use them. 266sub reset { 267 my $self = shift; 268 269 # Do we have an index lock? 270 unless ( $self->haveLock ) { 271 return $self->_error( "We do not hold a lock on the index" ); 272 } 273 274 # Get the module list 275 my @Modules = $self->getModules; 276 277 # Don't do this if there are any locked, non-broken modules 278 my $locked = scalar grep { $_->locked and $_->lockHolderExists } @Modules; 279 if ( $locked ) { 280 return $self->_error( "Cannot reset cache while processes hold locked Modules" ); 281 } 282 283 # Remove all the modules 284 foreach my $Module ( @Modules ) { 285 $Module->_reloadDelete; 286 } 287 288 # Remove the root directory 289 my $root = $self->root; 290 my $rv = system( 'rm -rf $root' ); 291 if ( $rv ) { 292 return $self->_error( "Error while trying to delete repository cache" ); 293 } 294 295 # Now create a new MetaData object 296 my $class = ref $self; 297 my $MetaData = $class->new() or return $self->_error( "Failed to recreate the cache" ); 298 299 # Copy the hash elements into outselves 300 delete $self->{$_} foreach keys %$self; 301 $self->{$_} = $MetaData->{$_} foreach keys %$MetaData; 302 303 1; 304} 305 306# Remove files that should not be in the cache directory 307sub clearDebris { 308 my $self = shift; 309 310 # Create an list of files that SHOULD be in the directory 311 my %hash = ( $self->_fileIndex => 1 ); 312 313 # Add the main directories for each module 314 foreach my $Module ( $self->getModules ) { 315 $hash{ $Module->name } = 1; # The directory 316 } 317 318 # Get the list of objects in the directory 319 my $root = $self->root; 320 opendir( DIR, $root ); 321 my @files = readdir DIR; 322 closedir DIR; 323 324 # Filter out ourselves, things in the index, and things we can't delete 325 @files = grep { ! /^\.+$/ and ! $hash{$_} and -w $_ } @files; 326 327 # Remove the remaining things 328 # Remove the filesystem objects that arn't in the index 329 foreach ( @files ) { 330 File::Flat->remove( "$root/$_" ) or return undef; 331 } 332 333 1; 334} 335 336# Removes a MetaData Cache 337sub removeMetaDataCache { 338 my $class = shift; 339 my $base = shift; 340 341 # Get the root directory for the MetaData cache 342 my $root = File::Spec->catfile( $base, $class->_fileIndex ); 343 344 # Call the system to remove it 345 if ( system( "rm -rf $root" ) ) { 346 return $class->_error( "System error while trying to remove MetaData cache ( 'rm -rf $root' )" ); 347 } 348 349 1; 350} 351 352 353 354 355 356##################################################################### 357# Working with MetaData Object 358 359# Get the base directory ( e.g. /tmp ) 360sub base { $_[0]->{base} } 361 362# Get the root directory ( e.g. /tmp/CVSMonitor_MetaData ) 363sub root { $_[0]->{root} } 364 365# Make the MetaData object dirty 366sub dirty { $_[0]->{_DIRTY} = 1 } 367 368# Is there an active task in progress? 369sub active { 370 my $self = shift; 371 372 # We are active if there are any transient, non-broken modules 373 my $transients = scalar grep { ! $_->broken } 374 grep { $_->transient } 375 $self->getModules; 376 377 $transients ? 1 : 0; 378} 379 380 381 382 383##################################################################### 384# Locking 385 386# Release an unneeded lock on the object 387sub releaseLock { 388 my $self = shift; 389 if ( $self->{_FH} ) { 390 # Unlock and close the filehandle 391 flock( $self->{_FH}, Fcntl::LOCK_UN() ); 392 close $self->{_FH}; 393 delete $self->{_FH}; 394 } 395 396 1; 397} 398 399# Do we have a lock? 400sub haveLock { $_[0]->{_FH} ? 1 : 0 } 401 402 403 404 405 406##################################################################### 407# Working with Repositaries 408 409# Get the Repository names. 410# Return in sorted order. 411sub getRepositoryNames { sort keys %{ $_[0]->{_REPOSITORIES} } } 412 413# Get the Repositaries. 414# Return in sorted order. 415sub getRepositories { 416 my $self = shift; 417 map { $self->{_REPOSITORIES}->{$_} } 418 sort keys %{ $self->{_REPOSITORIES} }; 419} 420 421# Get a single Repository by name 422sub getRepository { $_[0]->{_REPOSITORIES}->{$_[1]} } 423 424# Add a new repository 425sub addRepository { 426 my $self = shift; 427 my $name = shift; 428 my $cvsroot = shift; 429 my $options = shift || {}; 430 return $self->_error( 'You did not set a label' ) unless $options->{label}; 431 432 # Check the formats of the arguments 433 unless ( defined $name and length $name ) { 434 return $self->_error( 'No symbolic name was provided' ); 435 } 436 unless ( $self->_checkFormat( 'symbol', $name ) ) { 437 return $self->_error( 'Symbolic name is not in the correct format' ); 438 } 439 unless ( defined $cvsroot and length $cvsroot ) { 440 return $self->_error( 'No CVSROOT was provided' ); 441 } 442 443 # Check for duplicates 444 foreach ( $self->getRepositories ) { 445 # Is the name the same 446 if ( $name eq $_->name ) { 447 return $self->_error( 'A Repository with that name already exists' ); 448 } 449 450 # Is the label the same 451 if ( $options->{label} eq $_->getLabel ) { 452 return $self->_error( "A Repository with that label already exists" ); 453 } 454 455 # Does that cvsroot already exist? 456 if ( $cvsroot eq $_->cvsroot ) { 457 return $self->_error( "A repository with that cvsroot already exists" ); 458 } 459 } 460 461 # Do we hold a lock? 462 unless ( $self->haveLock ) { 463 return $self->_error( "Cannot create module. We do not have a lock on the index file" ); 464 } 465 466 # Create the Repository object 467 my $Repository = CVSMonitor::MetaData::Repository->new( $name, $cvsroot, $options, $self ); 468 return undef unless defined $Repository; 469 470 # Add it 471 $self->{_REPOSITORIES}->{$name} = $Repository; 472 $self->{_DIRTY} = 1; 473 474 1; 475} 476 477# Delete a Repository ( and everything in it ) 478sub deleteRepository { 479 my $self = shift; 480 my $name = shift or return undef; 481 482 # Does the Repository exist? 483 my $Repository = $self->getRepository( $name ) or return undef; 484 485 # Make sure that non of the modules inside the repository are locked 486 foreach my $Module ( $Repository->getModules ) { 487 if ( $Module->locked ) { 488 return $self->_error( "One or more modules in the repository are locked" ); 489 } 490 } 491 492 # Do we hold a metadata lock? 493 unless ( $self->haveLock ) { 494 return $self->_error( "Cannot delete Repository. We do not have a lock on the index file" ); 495 } 496 497 # Delete all the modules from inside the Repository 498 foreach my $Module ( $Repository->getModules ) { 499 $Module->delete or return undef; 500 } 501 502 # Remove the repositories entry 503 delete $self->{_REPOSITORIES}->{$name}->{_PARENT}; 504 delete $self->{_REPOSITORIES}->{$name}; 505 $self->dirty; 506 507 1; 508} 509 510 511 512 513 514##################################################################### 515# Working with modules 516# We provide this layer for convenience. 517# It uses the dotted format for modules 'Repository.Module' 518 519# Get all the ( full ) module names 520sub getModuleNames { 521 my $self = shift; 522 523 # Iterate over the repositories 524 my @names = (); 525 foreach my $Repository ( $self->getRepositories ) { 526 my $repository_name = $Repository->name; 527 push @names, map { 528 $repository_name . '.' . $_->name 529 } $Repository->getModulesNames; 530 } 531 532 @names; 533} 534 535# Get all the modules 536sub getModules { 537 my $self = shift; 538 map { $_->getModules } $self->getRepositories; 539} 540 541# Get a given module by name 542# This goes perhaps a little over the top, but hey, it's easy to write, 543# and I can just remove it later if it seems un-nescesary. 544sub getModule { 545 my $self = shift; 546 my $name = shift or return undef; 547 548 # Do we have a proper full name 549 if ( CVSMonitor::MetaData->_checkFormat( 'module', $name ) ) { 550 my ( $repository_name, $module_name ) = split /\./, $name; 551 my $Repository = $self->getRepository( $repository_name ); 552 unless ( $Repository ) { 553 return $self->_error( "The repository '$repository_name' does not exist" ); 554 } 555 my $Module = $Repository->getModule( $module_name ); 556 return $Module || $self->_error( "Module '$module_name' does not exist in Repository '$repository_name'" ); 557 } 558 559 # Do we have just the module name 560 if ( CVSMonitor::MetaData->_checkFormat( 'symbol', $name ) ) { 561 # Search through the repositories to see if the module exists 562 # in only one repository 563 my $Module = undef; 564 foreach my $Repository ( $self->getRepositories ) { 565 if ( $Repository->getModule( $name ) ) { 566 if ( $Module ) { 567 # More than one 568 return $self->_error( "More than one module with the name '$name' exist" ); 569 } else { 570 $Module = $Repository->getModule( $name ); 571 } 572 } 573 } 574 575 return $Module || $self->_error( "The module '$name' does not exist in any repositories" ); 576 } 577 578 $self->_error( "Invalid format for a module name" ); 579} 580 581 582 583 584##################################################################### 585# Working on all modules 586 587# Update all updatable modules 588sub update { 589 my $self = shift; 590 foreach my $Module ( $self->getModules ) { 591 $Module->update or return undef; 592 } 593 594 1; 595} 596 597# As for Repositories and Modules, work out the update cost 598sub updateCost { 599 my $self = shift; 600 my $total = 0; 601 602 # Iterate over the modules and add them up 603 foreach my $Module ( $self->getModules ) { 604 my $cost = $Module->updateCost; 605 return undef unless defined $cost; 606 $total += $cost; 607 } 608 609 $total; 610} 611 612 613 614 615 616##################################################################### 617# Platform checking 618 619# Is this platform OK with regards to it's CVS client 620sub cvsok { 621 my $self = shift; 622 623 # Is cvs installed 624 unless ( $self->cvslocation ) { 625 return $self->_error( "CVS Client not installed" ); 626 } 627 628 # Get the version 629 my $version = $self->cvsversion; 630 unless ( $version ) { 631 return $self->_error( "Could not determine the CVS version" ); 632 } 633 634 # Make sure the version is high enough 635 if ( Sort::Versions::versioncmp( $version, '1.11.1' ) >= 0 ) { 636 return 1; 637 } else { 638 return $self->_error( "CVS Client is too old. Please install 1.11.1 or greater" ); 639 } 640} 641 642# Find the installed location of CVS. 643# Returns the location of CVS on success. 644# Returns undef if CVS not found. 645sub cvslocation { 646 my $self = shift; 647 my @location = `which cvs`; 648 scalar @location 649 ? chomp($location[0]) 650 : undef; 651} 652 653# Get the version of the CVS client. 654# Returns the version if we can find it. 655# Returns undef if cannot find version. 656sub cvsversion { 657 my $self = shift; 658 my @output = `cvs -v`; 659 chomp(@output); 660 if ( $output[1] =~ /\b(1\.[\d\.p]+)/ ) { 661 return $1; 662 } else { 663 return undef; 664 } 665} 666 667 668 669 670 671##################################################################### 672# Utilities and Error Handling 673 674sub _checkWorkArea { 675 my $class = shift; 676 677 # Get and check the directory we are supposed 678 # to either create in or attach to. 679 my $base = shift; 680 if ( $base ) { 681 unless ( -d $base ) { 682 return $class->_error( "Base directory does not exist" ); 683 } 684 } else { 685 # Set base to the default temp directory 686 $base = File::Spec->tmpdir(); 687 } 688 689 # Does the work area root directory exist? If not, create it 690 my $root = File::Spec->catdir( $base, $class->_directoryRoot ); 691 unless ( -d $root ) { 692 # Everyone needs to be able to write to this directory 693 # because this module could be used by multiple users. 694 unless ( mkdir $root, 0777 ) { 695 return $class->_error( "Unable to create workarea directory '$root'" ); 696 } 697 } 698 699 $root; 700} 701 702# Get a freezable struct for this module 703sub _freezable { 704 my $self = shift; 705 706 # Copy our main hash 707 my $copy = bless { %{ $self } }, ref $self; 708 709 # Remove unwanted things from the copy 710 delete $copy->{_FH}; 711 delete $copy->{_DIRTY}; 712 delete $copy->{_NEW}; 713 714 # Remove the parent references in the repositories 715 my $repositories = { %{ $copy->{_REPOSITORIES} } }; 716 foreach ( keys %$repositories ) { 717 $repositories->{$_} = $repositories->{$_}->_freezable; 718 $repositories->{$_}->{_PARENT} = $copy; 719 } 720 $copy->{_REPOSITORIES} = $repositories; 721 722 $copy; 723} 724 725# Provides string format checking available to all packages 726sub _checkFormat { 727 my $class = shift; 728 my $type = shift or return undef; 729 my $string = shift; 730 731 # Split on type 732 if ( $type eq 'module' ) { 733 # Check for a full referenced module name 734 # This should be [symbol].[symbol] 735 return $string =~ /^\w{1,32}\.\w{1,32}$/i ? 1 : ''; 736 737 } elsif ( $type eq 'symbol' ) { 738 # Checks the format of a symbolic name 739 # ( For a Repository or Module ) 740 # This should be 1 to 32 alphanumeric characters 741 return $string =~ /^\w{1,32}$/i ? 1 : ''; 742 743 } elsif ( $type eq 'type' ) { 744 # Only support pserver 745 return $string eq 'pserver' ? 1 : ''; 746 747 } elsif ( $type eq 'username' ) { 748 # The username restrictions are amazingly flexible. 749 # It MUST be at least one character long, and must NOT 750 # contain the characters : / or whitespace. 751 # At least, that's what I read into the specs in root.c 752 # in the CVS source. 753 return $string =~ /^[^\s\:\/]+$/ ? 1 : ''; 754 755 } elsif ( $type eq 'password' ) { 756 # Password. Ummm... anything that's defined 757 return undef unless defined $string; 758 759 } elsif ( $type eq 'path' ) { 760 # Assume this is correct for now 761 return 1; 762 763 } else { 764 # Unknown format 765 return undef; 766 } 767} 768 769# Generate a date in a particular format 770sub date { 771 return undef unless $_[1]; 772 my @t = gmtime $_[1]; 773 774 # Create an ISO formatted GMT date 775 $t[4] += 1; 776 $t[5] += 1900; 777 $t[0] = '0' . $t[0] if length $t[0] < 2; 778 $t[1] = '0' . $t[1] if length $t[1] < 2; 779 $t[2] = '0' . $t[2] if length $t[2] < 2; 780 $t[3] = '0' . $t[3] if length $t[3] < 2; 781 $t[4] = '0' . $t[4] if length $t[4] < 2; 782 "$t[5]/$t[4]/$t[3] $t[2]:$t[1]:$t[0]"; 783} 784 785sub _error { $errstr = $_[1]; undef } 786sub errstr { $errstr } 787 7881; 789 790__END__ 791 792# Copyright (C) 2002-2004 Adam Kennedy 793# 794# This program is free software; you can redistribute it and/or modify 795# it under the terms of the GNU General Public License as published by 796# the Free Software Foundation; either version 2 of the License, or 797# (at your option) any later version. 798# 799# This program is distributed in the hope that it will be useful, 800# but WITHOUT ANY WARRANTY; without even the implied warranty of 801# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 802# GNU General Public License for more details. 803# 804# You should have received a copy of the GNU General Public License 805# along with this program; if not, write to the Free Software 806# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 807# 808# Should you wish to utilise this software under a different licence, 809# please contact the author. 810