home *** CD-ROM | disk | FTP | other *** search
- package Apache::ASP::State;
-
- use MLDBM;
- use MLDBM::Sync 0.25;
- use MLDBM::Sync::SDBM_File;
- use SDBM_File;
- use Data::Dumper;
-
- use strict;
- no strict qw(refs);
- use vars qw(%DB %CACHE $DefaultGroupIdLength);
- use Fcntl qw(:flock O_RDWR O_CREAT);
- $DefaultGroupIdLength = 2;
-
- # Database formats supports and their underlying extensions
- %DB = (
- SDBM_File => ['.pag', '.dir'],
- DB_File => [''],
- 'MLDBM::Sync::SDBM_File' => ['.pag', '.dir'],
- GDBM_File => [''],
- 'Tie::TextDir' => [''],
- );
-
- # About locking, we use a separate lock file from the SDBM files
- # generated because locking directly on the SDBM files occasionally
- # results in sdbm store errors. This is less efficient, than locking
- # to the db file directly, but having a separate lock file works for now.
- #
- # If there is no $group given, then the $group will be extracted from
- # the $id as the first 2 letters of that group.
- #
- # If the group and the id are the same length, then what was passed
- # was just a group id, and the object is being created for informational
- # purposes only. So, we don't create a lock file in this case, as this
- # is not a real State object
- #
- sub new {
- my($asp, $id, $group) = @_;
-
- if($id) {
- $id =~ tr///;
- } else {
- $asp->Error("no id: $id passed into new State");
- return;
- }
-
- # default group is first 2 characters of id, simple hashing
- if($group) {
- $group =~ tr///;
- } else {
- $group = substr($id, 0, $DefaultGroupIdLength)
- }
-
- unless($group) {
- $asp->Error("no group defined for id $id");
- return;
- }
-
- my $state_dir = $asp->{state_dir};
- my $group_dir = $state_dir.'/'.$group;
- my $lock_file = $group_dir.'/'.$id.'.lock';
- my $file = $group_dir.'/'.$id;
-
- # we only need SDBM_File for internal, and its faster so use it
- my($state_db, $state_serializer);
- if($id eq 'internal') {
- $state_db = $Apache::ASP::DefaultStateDB;
- $state_serializer = $Apache::ASP::DefaultStateSerializer;
- } elsif($asp->{Internal} && (length($id) > $DefaultGroupIdLength)) {
- # don't get data for dummy group id sessions
- my $internal = $asp->{Internal};
- my $idata = $internal->{$id};
- if(! $idata->{state_db} || ! $idata->{state_serializer}) {
- $state_db = $idata->{state_db} || $asp->{state_db} || $Apache::ASP::DefaultStateDB;
- $state_serializer = $idata->{state_serializer} ||
- $asp->{state_serializer} || $Apache::ASP::DefaultStateSerializer;
-
- # INIT StateDB && StateSerializer if hitting for the first time
- # only if real id like a session id or application
- if(length($id) > $DefaultGroupIdLength) {
- my $diff = 0;
- if(($idata->{state_db} || $Apache::ASP::DefaultStateDB) ne $state_db) {
- $idata->{state_db} = $state_db;
- $diff = 1;
- }
- if(($idata->{state_serializer} || $Apache::ASP::DefaultStateSerializer) ne $state_serializer) {
- $idata->{state_serializer} = $state_serializer;
- $diff = 1;
- }
-
- if($diff) {
- $asp->{dbg} && $asp->Debug("setting internal data for state $id", $idata);
- $internal->{$id} = $idata;
- }
- }
- } else {
- # this state has already been created
- $state_db = $idata->{state_db};
- $state_serializer = $idata->{state_serializer};
- }
- } else {
- # cache layer doesn't need internal
- ($state_db, $state_serializer) = ($asp->{state_db}, $asp->{state_serializer});
- }
-
- my $self =
- bless {
- asp=>$asp,
- dbm => undef,
- 'dir' => $group_dir,
- id => $id,
- file => $file,
- group => $group,
- group_dir => $group_dir,
- reads => 0,
- state_dir => $state_dir,
- writes => 0,
- };
-
- # short circuit before expensive directory tests for group stub
- if ($group eq $id) {
- return $self;
- }
-
- if($asp->config('StateAllWrite')) {
- $asp->{dbg} and $asp->{state_all_write} = 1;
- $self->{dir_perms} = 0777;
- $self->{file_perms} = 0666;
- } elsif($asp->config('StateGroupWrite')) {
- $asp->{dbg} and $asp->{state_group_write} = 1;
- $self->{dir_perms} = 0770;
- $self->{file_perms} = 0660;
- } else {
- $self->{dir_perms} = 0750;
- $self->{file_perms} = 0640;
- }
-
- # push(@{$self->{'ext'}}, @{$DB{$self->{state_db}}});
- # $self->{asp}->Debug("db ext: ".join(",", @{$self->{'ext'}}));
-
- # create state directories
- my @create_dirs;
- unless(-d $state_dir) {
- push(@create_dirs, $state_dir);
- }
- # create group directory
- unless(-d $group_dir) {
- push(@create_dirs, $group_dir);
- }
- if(@create_dirs) {
- $self->UmaskClear;
- for my $create_dir (@create_dirs) {
- # $create_dir =~ tr///; # this doesn't work to untaint with perl 5.6.1, use old method
- $create_dir =~ /^(.*)$/s;
- $create_dir = $1;
- if(mkdir($create_dir, $self->{dir_perms})) {
- $asp->{dbg} && $asp->Debug("creating state dir $create_dir");
- } else {
- my $error = $!;
- -d $create_dir || $self->{asp}->Error("can't create group dir $create_dir: $error");
- }
- }
- $self->UmaskRestore;
- }
-
- # INIT MLDBM::Sync DBM
- {
- local $MLDBM::UseDB = $state_db || 'SDBM_File';
- local $MLDBM::Serializer = $state_serializer || 'Data::Dumper';
- # clear current tied relationship first, if any
- $self->{dbm} = undef;
- local $SIG{__WARN__} = sub {};
-
- my $error;
- $self->{file} =~ /^(.*)$/; # untaint
- $self->{file} = $1;
- local $MLDBM::RemoveTaint = 1;
- $self->{dbm} = &MLDBM::Sync::TIEHASH('MLDBM', $self->{file}, O_RDWR|O_CREAT, $self->{file_perms});
- $asp->{dbg} && $asp->Debug("creating dbm for file $self->{file}, db $MLDBM::UseDB, serializer: $MLDBM::Serializer");
- $error = $! || 'Undefined Error';
-
-
- if(! $self->{dbm}) {
- $self->{asp}->Error(qq{
- Cannot tie to file $self->{file}, $error !!
- Make sure you have the permissions on the directory set correctly, and that your
- version of Data::Dumper is up to date. Also, make sure you have set StateDir to
- to a good directory in the config file. StateDir defaults to Global/.state
- });
- }
- }
-
- $self;
- }
-
- sub Init { shift->{dbm}->CLEAR(); }
- sub Size { shift->{dbm}->SyncSize; }
- sub Delete { shift->{dbm}->CLEAR(); }
- sub WriteLock { shift->{dbm}->Lock; }
- sub ReadLock { shift->{dbm}->ReadLock; }
- sub UnLock { shift->{dbm}->UnLock; }
-
- sub DeleteGroupId {
- my $self = shift;
-
- my $group_dir = $self->{group_dir};
- if(-d $group_dir) {
- $self->{asp}{Internal}->LOCK;
- if(rmdir($group_dir)) {
- $self->{asp}->Debug("deleting group dir $group_dir");
- } else {
- $self->{asp}->Log("cannot delete group dir $group_dir: $!");
- }
- $self->{asp}{Internal}->UNLOCK;
- }
- }
-
- sub GroupId { shift->{group}; }
-
- sub GroupMembers {
- my $self = shift;
- local(*DIR);
- my(%ids, @ids);
-
- unless(opendir(DIR, $self->{group_dir})) {
- $self->{asp}->Log("opening group $self->{group_dir} failed: $!");
- return [];
- }
-
- for(readdir(DIR)) {
- next if /^\.\.?$/;
- $_ =~ /^(.*?)(\.[^\.]+)?$/;
- next unless $1;
- $ids{$1}++;
- }
-
- # need to explicitly close directory, or we get a file
- # handle leak on Solaris
- closedir(DIR);
-
- # since not all sessions have their own dbms now, find session ids in $Internal too
- if(my $internal = $self->{asp}{Internal}) {
- my $cached_keys = {};
- unless($cached_keys = $self->{asp}{internal_cached_keys}) {
- map {
- if(/^([0-9a-f]{2})/) {
- $cached_keys->{$1}{$_}++
- }
- } keys %$internal;
- $self->{asp}{internal_cached_keys} = $cached_keys;
- }
- if(my $group_keys = $cached_keys->{$self->{group}}) {
- %ids = ( %ids, %$group_keys );
- }
- }
-
- @ids = keys %ids;
-
- \@ids;
- }
-
- sub DefaultGroups {
- my $self = shift;
- my(@ids);
- local *STATEDIR;
-
- opendir(STATEDIR, $self->{state_dir})
- || $self->{asp}->Error("can't open state dir $self->{state_dir}");
- my $time = time;
- for(readdir(STATEDIR)) {
- next if /^\./;
- next unless (length($_) eq $DefaultGroupIdLength);
- push(@ids, $_);
- }
- closedir STATEDIR;
-
- \@ids;
- }
-
- sub UmaskClear {
- my $self = shift;
- return if $self->{asp}{win32};
- $self->{umask_restore} = umask(0000);
- }
-
- sub UmaskRestore {
- my $self = shift;
- return if $self->{asp}{win32};
- if(defined $self->{umask_restore}) {
- umask($self->{umask_restore});
- }
- }
-
- sub DESTROY {
- my $self = shift;
- return unless %{$self};
- return if $self->{destroyed}++;
- $self->{dbm} && eval { $self->{dbm}->DESTROY };
- $self->{dbm} = undef;
- }
-
- # don't need to skip DESTROY since we have it defined
- # return if ($AUTOLOAD =~ /DESTROY/);
- sub AUTOLOAD {
- my $self = shift;
- my $AUTOLOAD = $Apache::ASP::State::AUTOLOAD;
- $AUTOLOAD =~ s/^(.*)::(.*?)$/$2/o;
-
- my $value;
- $value = $self->{dbm}->$AUTOLOAD(@_);
-
- $value;
- }
-
- sub TIEHASH {
- my $type = shift;
-
- # dual tie contructor, if we receive a State object to tie
- # then just return it, otherwise construct a new object
- # before tieing
- if((ref $_[0]) =~ /State/) {
- $_[0];
- } else {
- bless &new(@_), $type;
- }
- }
-
- sub FETCH {
- my($self, $index) = @_;
- my $value;
-
- if($index eq '_FILE') {
- $value = $self->{file};
- } elsif($index eq '_SELF') {
- $value = $self;
- } else {
- $value = $self->{dbm}->FETCH($index);
- $self->{reads}++;
- }
-
- $value;
- }
-
- sub STORE {
- my $self = shift;
-
- # don't worry about overhead of Umask* routines, the STORE
- # being called is much heavier
- $self->UmaskClear;
- my $rv = $self->{dbm}->STORE(@_);
- $self->UmaskRestore;
- $self->{writes}++;
-
- $rv;
- }
-
- sub LOCK { my $self = tied(%{$_[0]}); $self->{dbm}->Lock(); }
- sub UNLOCK { my $self = tied(%{$_[0]}); $self->{dbm}->UnLock(); }
-
- 1;
-