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