#---------------------------------------------------------------------- # Copyright 1999-2003 Mitel Networks Corporation # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. #---------------------------------------------------------------------- package esmith::config; use strict; use vars qw($VERSION); $VERSION = 1.45; use Sys::Syslog qw(:DEFAULT setlogsock); use Fcntl qw(:DEFAULT :flock); use Carp qw(cluck); my $Default_Config = '/home/e-smith/db/configuration'; =pod =head1 NAME esmith::config - Access e-smith config files via hashes =head1 SYNOPSIS use esmith::config; my %config; my $config_obj = tie %config, 'esmith::config', $config_file; # Read in the value of Wibble from the $config_file. print $config{Wibble}; # Write out the value of Wibble to the $config_file. $config{Wibble} = 42; my $filename = $config_obj->filename; =head1 DESCRIPTION The esmith::config package enables Perl programs to read and write entries from the e-smith configuration file using a simple hash interface. The configuration file has a simple ASCII representation, with one "key=value" entry per line. =begin testing use_ok('esmith::config'); chdir '10e-smith-lib'; %Expect = ( foo => 'bar', 'this key' => 'has whitespace', 'that key ' => 'has trailing whitespace', ' another key' => 'has leading whitespace', 'this value' => ' has leading whitespace', 'that value' => 'has trailing whitespace ', 'tricky value' => 'with=equals.', ); =end testing =head2 Tying tie %config, 'esmith::config', $config_file; Reads in the configuration from the given $config_file, returning a tied hash (%config) populated with the keys & values from the $config_file which you can then use like a normal hash. Any writes or deletes are immediately written back to the $config_file. If no $config_file is supplied it falls back to the environment variable ESMITH_CONFIG_DB, and finally defaults to F If the $config_file doesn't exist it will create one for you. =begin testing my %config; tie %config, 'esmith::config', 'dummy.conf'; ok( tied %config, 'tie worked' ); is( $config{foo}, 'bar', ' theres stuff in it' ); ok( !exists $config{FILENAME}, ' it only contains config info'); is( tied(%config)->{FILENAME}, 'dummy.conf', ' and the real object is inside'); tie %config, 'esmith::config', 'I_dont_exist'; ok( tied %config, 'tying a non-existant file' ); is( keys %config, 0, ' and its empty' ); $config{foo} = 42; isnt( -s 'I_dont_exist', 0 ); untie %config; ok( unlink 'I_dont_exist' ); $ENV{ESMITH_CONFIG_DB} = "dummy.conf"; tie %config, 'esmith::config'; ok( tied %config, 'tie to ESMITH_CONFIG_DB worked' ); is_deeply(\%config, \%Expect, " picked up data"); =end testing =head2 Methods You can get at the underlying esmith::config object by using tied(). my $config_obj = tied %config; it has a few useful methods. =over 4 =item filename my $file = $config_obj->filename; Gets the config filename this object is tied to. =begin testing my %config; my $obj = tie %config, 'esmith::config', 'dummy.conf'; is( $obj->filename, 'dummy.conf', 'filename()' ); =end testing =cut sub filename { my($self) = shift; return $self->{FILENAME}; } =back =begin _private =head2 Private methods =over 4 =item _readconf my $config = _readconf($config_file); Returns a hash ref of config key/value pairs read out of the given $config_file. If $config_file doesn't exist an empty hash ref will be returned. _readconf() understands the config file to be formatted as individual lines of simply: key=value any further complexity of parsing the value is handled elsewhere. =end _private =begin testing my $config = esmith::config::_readconf('dummy.conf'); isnt( keys %$config, 0, '_readconf() got something' ); is_deeply( $config, \%Expect, ' read in the right values' ); $config = esmith::config::_readconf('I_dont_exist'); isa_ok( $config, 'HASH', '_readconf from a non-existent file' ); is( keys %$config, 0, ' and its empty' ); =end testing =cut sub _readconf { my ($self, $filename) = @_; my %config = (); unless (open (FH, $filename)) { if (-f $filename) { &log("Config: ERROR: \"$filename\" exists but is not readable"); } return \%config; } my $binmode = $self->_read_binmode; binmode(FH, $binmode) if $binmode; while (my $line = ) { chomp $line; # BLIND UNTAINT! Much code wrongly depends on this and # they should be moved away from it. $line =~ /(.*)/; $line = $1; # ignore comments and blank lines next if $line =~ /^\s*$/ || $line =~ /^\s*#/; my($key, $value) = split /=/, $line, 2; $config{$key} = $value; } close(FH); return \%config; } =begin _private =item _writeconf my $success = _writeconf($config_file, \%config); The given $config_file is overwritten using the entries in %config. Returns whether or not the write succeded. =end _private =begin testing my $scratch = 'scratch.conf'; ok( esmith::config::_writeconf($scratch, \%Expect), '_writeconf() says it worked' ); is_deeply( esmith::config::_readconf($scratch), \%Expect, ' wrote the right things' ); unlink $scratch; =end testing =cut sub _writeconf { my ($self, $filename, $config) = @_; eval { if (-f $filename && ! -r $filename) { die "'$filename' exists but is not readable\n"; } sysopen (FH, "$filename.$$", O_RDWR | O_CREAT, 0660) or die "Cannot open $filename.$$: $!\n"; my $binmode = $self->_write_binmode; binmode(FH, $binmode) if $binmode; die "Error writing to $filename.$$: $!" unless printf FH <{$key}\n" or die "Error writing to $filename.$$: $!"; } close (FH) or die "Error closing $filename.$$: $!"; rename("$filename.$$", $filename) or die "Couldn't rename $filename.$$ to $filename: $!"; my $gid = getgrnam ('admin') || 0; chown (0, $gid, $filename); }; if($@) { chomp $@; &log($@); &log("'$filename' will not be updated"); return; } else { return 1; } } =begin _private =item B<_lock_write> =item B<_lock_read> $self->_lock_write; $self->_lock_read; Sets up read (shared) or write (exclusive) locks on the config file. This is actually locking a semaphore file. Returns if the lock succeeded or failed. =item B<_unlock> $self->_unlock Unlocks the config file. =end _private =cut sub _lock_write { $_[0]->_lock(LOCK_EX) } sub _lock_read { $_[0]->_lock(LOCK_SH) } sub _lock { return if $] eq 5.006; # Locking is broken in perl 5.6.0 my($self, $lock) = @_; my $semaphore = $self->{SEMAPHORE_FILE} = $self->{FILENAME}.'.lock'; eval { open(my $fh, ">>$semaphore") or die "Can't open '$semaphore' semaphore: $!"; $self->{SEMAPHORE} = $fh; flock($fh, $lock) or die "Can't lock '$semaphore' semaphore: $!"; }; if( $@ ) { warn $@; return; } else { return 1; } } sub _unlock { return if $] eq 5.006; # Locking is broken in perl 5.6.0 my($self) = @_; eval { flock($self->{SEMAPHORE}, LOCK_UN) or die "Can't unlock $self->{SEMAPHORE_FILE}: $!"; unlink $self->{SEMAPHORE_FILE}; delete $self->{SEMAPHORE_FILE}; delete $self->{SEMAPHORE}; }; if( $@ ) { warn $@; return; } else { return 1; } } =end _private =back =cut #------------------------------------------------------------ # Constructor for the tied hash. If filename not specified, # defaults to '/home/e-smith/db/configuration'. #------------------------------------------------------------ sub TIEHASH { my $class = shift; my $filename = shift || $ENV{ESMITH_CONFIG_DB} || $Default_Config; if ($filename =~ m:^/home/e-smith/\w+$: ) { cluck "*WARNING* esmith::config($filename) called with old " . "database path. The following package needs to be updated: "; $filename =~ s:e-smith:e-smith/db:; } my $self = { FILENAME => $filename, CONFIG => {}, }; bless $self, $class; $self->{CONFIG} = $self->_readconf($filename); return $self; } #------------------------------------------------------------ # Look up a configuration parameter. #------------------------------------------------------------ sub FETCH { my $self = shift; my $key = shift; # Trim leading and trailing whitespace from the key. $key =~ s/^\s+|\s+$//g; return $self->{CONFIG}{$key}; } #------------------------------------------------------------ # Store a configuration parameter. #------------------------------------------------------------ sub STORE { my $self = shift; my $key = shift; my $value = shift; die "key not defined" unless defined $key; die "value not defined for key $key" unless defined $value; if( $value =~ /\n/ or $key =~ /\n/ ) { &log("$self->{FILENAME}: esmith::config doesn't support newlines in ". "keys or values. Truncating."); $key =~ s/\n.*//s; $value =~ s/\n.*//s; } # Trim leading and trailing whitespace from the key and value. $key =~ s/^\s+|\s+$//g; $value =~ s/^\s+|\s+$//g; # Make sure that the value has a type. Given the format, it should be # sufficient to ensure that it does not begin with a pipe char. if ($value =~ /^\|/) { warn "ERROR: You should not set a config record without a type (key was $key).\n"; } # read in config again, just in case it changed $self->_lock_write; $self->{CONFIG} = $self->_readconf($self->{FILENAME}); if (exists $self->{CONFIG}{$key} and $self->{CONFIG}{$key} eq $value) { $self->_unlock; return undef; } my $msg = "$self->{FILENAME}: OLD $key="; if (exists $self->{CONFIG}{$key}) { $msg .= "$self->{CONFIG}{$key}"; } else { $msg .= "(undefined)"; } &log($msg); $self->{CONFIG} {$key} = $value; &log("$self->{FILENAME}: NEW $key=$self->{CONFIG}{$key}"); $self->_writeconf ($self->{FILENAME}, $self->{CONFIG}); $self->_unlock; return undef; } #------------------------------------------------------------ # Delete a configuration parameter. #------------------------------------------------------------ sub DELETE { my $self = shift; my $key = shift; # Trim leading and trailing whitespace from the key. $key =~ s/^\s+|\s+$//g; # read in config again, just in case it changed $self->_lock_write; $self->{CONFIG} = $self->_readconf($self->{FILENAME}); my $previous = delete $self->{CONFIG} {$key}; $self->_writeconf ($self->{FILENAME}, $self->{CONFIG}); $self->_unlock; &log("$self->{FILENAME}: DELETE $key=$previous"); return $previous; } =begin _private =item CLEAR tie method: Clear the configuration file =end _private =begin testing my $scratch = 'scratch.conf'; tie %config, 'esmith::config', $scratch; ok( tied %config, 'tying a non-existant file' ); is( keys %config, 0, ' and its empty' ); $config{fibble} = 'blah'; isnt( keys %config, 0, ' and its not empty now' ); %config = (); is( keys %config, 0, ' and CLEAR made it empty again' ); unlink $scratch; =end testing =cut sub CLEAR { my $self = shift; $self->{CONFIG} = (); $self->_writeconf ($self->{FILENAME}, $self->{CONFIG}); &log("$self->{FILENAME}: CLEAR"); return undef; } #------------------------------------------------------------ # Check whether a particular key exists in the configuration file. #------------------------------------------------------------ sub EXISTS { my $self = shift; my $key = shift; # Trim leading and trailing whitespace from the key. $key =~ s/^\s+|\s+$//g; return exists $self->{CONFIG} {$key}; } #------------------------------------------------------------ # FIRSTKEY is called whenever we start iterating over the # configuration table. We cache the configuration table at # this point to ensure reasonable results if the # configuration file is changed by another program during # the iteration. #------------------------------------------------------------ sub FIRSTKEY { my $self = shift; my $discard = keys %{$self->{CONFIG}}; # reset each() iterator return each %{$self->{CONFIG}}; } #------------------------------------------------------------ # NEXTKEY is called for all iterations after the first. We # just keep returning results from the cached configuration # table. A null array is returned at the end. If the caller # starts a new iteration, the FIRSTKEY subroutine is called # again, causing the cache to be reloaded. #------------------------------------------------------------ sub NEXTKEY { my $self = shift; return each %{$self->{CONFIG}}; } #------------------------------------------------------------ # Log messages to syslog #------------------------------------------------------------ sub log { # There is a bug in Perl 5.00504 and above. If you are using the unix # domain socket, do NOT use ndelay as part of the second argument # to openlog(). my $msg = shift; $msg =~ s/[^[:ascii:]]/_/g; my $program = $0; setlogsock 'unix'; openlog($program, 'pid', 'local1'); syslog('info', "%s", $msg); closelog(); } =item _read_binmode return undef, indicating that by default binmode() need not be called after file open. =end _private =cut sub _read_binmode { return undef; } sub _write_binmode { return undef; } =head1 BUGS and CAVEATS You can't have newlines in keys or values. While the config values happen to be untainted B as it will change in the future. =head1 AUTHOR SME Server Developers For more information, see http://www.e-smith.org/ =head1 SEE ALSO esmith::db =cut 1;