# dbopen - open a database and lock exclusively if opened for writing
#
# Copyright (c) 1993, 1994, 1995, 1996, 1997, 1998, 1999 by RIPE NCC
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of the author not be
# used in advertising or publicity pertaining to distribution of the
# software without specific, written prior permission.
#
# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS; IN NO EVENT SHALL
# AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# $Id: dbopen.pl,v 2.13 1999/10/06 16:41:05 marek Exp $
#
# ***NOTE***: flock doesn't work on NFS mounted file systems!!!
#             and thus locking doesn't work properly on NFS file systems
#
#	$RCSfile: dbopen.pl,v $
#	$Revision: 2.13 $
#	$Author: marek $
#	$Date: 1999/10/06 16:41:05 $
#
#
# arguments:
#            *database = pointer to file handle, dbm array and normal array
#            *entry  = pointer to object, determines what file to open
#            $write  = open for write or not
#            $name   = open a file independent of type
#
# uses global hash of hashes to store the info about already opened databases
# hash is: $::opened_databases
# the key is database file name, the vaue is a hash where keys are:
# 'status'     - information about lock mode and process ID which opened the 
#                file encoded in string
# 'handle'     - reference to scalar containing the name of the filehandle 
#                used to access the file
# 'array'      - pointer to array containing a flag indicating whether a file 
#                is opened and the file name
# 'tied_hash'  - pointer to hash tied to the index file
# 'open_count' - scalar value - how many times database is opened.

require "defines.pl";
require "dpr.pl";

# use Hash2Hash;

# return name of db file given text-file name
sub dbfilename { return $_[0].'.db' }

use Fcntl;
use DB_File;

sub dbname {
    local(*entry)=@_;
    
    local($name)=$DBFILE{$entry{"so"}};
	
    $name.=".".&entype(%entry) if ($SPLIT{$entry{"so"}});
    
#    &dpr("name: $name\n");
       
    return $name;
    
}


sub dbopen {
    local(*database, *entry, $write, $name) = @_;

#    &dpr("called with \$database = $database, \n\t\@database = " . 
#	 join("*", @database) . "\n\t\$write = $write, \$name = $name\n" .
#	 "\t\%entry = " . join("*", %entry) . "\n");

    $name = &dbname(*entry) if ($name=~ /^\s*$/);
    my($dbfilename) = dbfilename($name);

#    my($open_status);
#    my($lock_mode);
#    my($lock_pid);

#    &dpr("name: $name\n") if ($name);

#    if ($open_status = $::opened_databases{$name}{'status'}) {
#	$open_status =~ /^(\w+)\:(\w+)$/;
#	$lock_mode = $1;
#	$lock_pid = $2;
#    }
    
    if ($write) {

#	if ($open_status && ($lock_pid == $$)) {
#	    &dpr("database file $name already opened - lock: $lock_mode, " .
#		 "PID: $lock_pid\n");
#	    &syslog("AUDITLOG", 
#		    "database file $name already opened - lock: $lock_mode, " .
#		    "PID: $lock_pid");
#	    if ($db[0] == 1) {
#		# we are trying to open the same handle
#		if ($lock_mode == $LOCK_SH) {
#		    &dpr("Exclusive lock requested - try to acquire\n");
#		    flock($database, $LOCK_UN);
#		    unless (flock($database, $LOCK_EX | $LOCK_NB)) {
#			my $msg = 
#			    "CONTENTION - cannot get exclusive " .
#			    "lock on $name!\n" .
#			    "Waiting for write lock ($!)...";
#			&dpr($msg . "\n");
#			&syslog("AUDITLOG", $msg);
#			flock($database, $LOCK_EX) || die("flock: $!");
#		    }
#		}
#		else {
#		    # $::opened_databases{$name}{'open_count'}++;
#		    # $database = ${$::opened_databases{$name}{'handle'}};
#		    # @database = @{$::opened_databases{$name}{'array'}};
#		    # tie(%database, 'Hash2Hash', 
#		    # $::opened_databases{$name}{'tied_hash'});
#		    # %database = %{$::opened_databases{$name}{'tied_hash'}};
#		}
#		return 1;
#	    }
#	    else {
#		# we are trying to open another handle
#		# just go ahead and open the same file again
#		# what happens if we have read lock on other handle ?!?
#	    }
#	}
		
	# If we want to open the dbfile for writing, make sure that
	# cleandb is not using the database. If it is there will be a
	# race condition between rename and open.

	$name =~ /$SPLITFILENAME/o;
	local($lockfile)=$CLEANLOCK.$2;
	while (-f $lockfile) {
	    sleep 60;
	}
	
	if (!open($database, "+<$name")) {
	    if (! -f $name) {
		# make a database file in case it doesn't exist yet
		# don't do a destructive open since we are not locked yet
		open(NEWDB, ">>".$name) || 
		    &fatalerror("Cannot create new database: ", $name);
		&lock(NEWDB);
		# another process might have build it between the open & lock..
		# so test if the file has still zero length!
		# we could have done this with other flock options
		# but we don't want to use them for compatibility reasons
		if (-z $name) {
		    # printright is required since addkey doesn\'t work
		    # correct when an offset==0;
		    &printrights(NEWDB) if (-z $name);
		    &delormoveindices($name, "", 0);
		    &delormoveindices($name.$CLASSLESSEXT, "", 1);
		}
		close(NEWDB);
		return 0 if (!open($database, "+<$name"));
	    }
	    else {
		return 0;
	    }
	}
	unless (flock($database, $LOCK_EX | $LOCK_NB)) {
	    my $start = time();
	    my $msg = 
		"CONTENTION - cannot get exclusive lock on $name!\n" .
		"Waiting for write lock ($$: $!)...";
	    &dpr($msg . "\n");
	    &syslog("AUDITLOG", $msg);

	    flock($database, $LOCK_EX) || die("flock: $!");

	    my $elapsed = time() - $start;
	    $msg = "Lock acquired - $$ waited for $elapsed seconds";
	    &dpr($msg . "\n");
	    &syslog("AUDITLOG", $msg);
	}
	$DB_HASH->{cachesize} = $DBCACHESIZE;
	tie (%database, DB_File, $dbfilename, O_CREAT|O_RDWR, 0644, $DB_HASH)
	    || &fatalerror("Couldn\'t open dbm file($!): $dbfilename"); 
#	print STDERR "database file $name opened, index tied to $dbfilename\n";
#	$::opened_databases{$name}{'status'}    = "$LOCK_EX:$$";
#        $::opened_databases{$name}{'open_count'}++;
    }
    else { 
        # open readonly
#	if ($open_status && ($lock_pid == $$)) {    
#	    my $msg = 
#		"database file $name already opened - lock: " . 
#		    "$lock_mode, PID: $lock_pid";
#	    &dpr($msg . "\n");
#	    &syslog("AUDITLOG", $msg);
#	    if ($db[0] == 1) {
#		# we are trying to open the same handle
#		return 1;
#	    }
#	    else {
#		# $::opened_databases{$name}{'open_count'}++;
#		# $database = ${$::opened_databases{$name}{'handle'}};
#		# @database = @{$::opened_databases{$name}{'array'}};
#		# tie(%database, 'Hash2Hash',
#		# $::opened_databases{$name}{'tied_hash'});
#		# %database = %{$::opened_databases{$name}{'tied_hash'}};
#		# return 1;
#	    }
#	}

	return 0 if (!open($database, "<$name"));

#	unless (flock($database, $LOCK_SH | $LOCK_NB)) {
#	    &dpr("CONTENTION - can't read during write update on $name!\n" .
#		 "Waiting for read lock ($!)...\n");
#	    flock($database, $LOCK_SH) || die("flock: $!");
#	}

	tie (%database, DB_File, $dbfilename, O_RDONLY) || return 0;

#	$::opened_databases{$name}{'status'} = "$LOCK_SH:$$";
#        $::opened_databases{$name}{'open_count'}++;
    }
    local($oldhandle) = select($database); $|=1; select($oldhandle);
    @database = (1, $name);

    # $::opened_databases{$name}{'handle'}    = \$database;
    # $::opened_databases{$name}{'array'}     = \@database;
    # $::opened_databases{$name}{'tied_hash'} = \%database;

    return 1;
}


# Now, for classless we have a seperate database. Due to a slight design
# error (err'hum) we have to open this seperately. It will keep track
# of things. Thing to achieve here is to *never* have two classless
# indexes open at the same time. This should be done differently but
# we have to get things things running. Usually you would call this
# for a lookup and an insert that requires classless thingies.

# So, once more: ONLY ONE OPEN AT THE SAME TIME!!!!

sub dbclopen {
    local(*entry, $write, $name) = @_;

    &fatalerror("Classless index: $name (write: $write) type: ",
		&entype(%entry), " ", $entry{"so"}, 
		" already opened ($mspnxl[1]) !!!!") if ($mspnxl[0]==1);
    
    $name = &dbname(*entry) if ($name=~ /^\s*$/);
    $name.=$CLASSLESSEXT;

    my($dbfilename) = dbfilename($name);

    if ($write) {
      $DB_HASH->{cachesize} = $DBCACHESIZE;
       tie (%mspnxl, DB_File, $dbfilename, O_CREAT|O_RDWR, 0644, $DB_HASH) ||
	 &fatalerror("Couldn\'t open classless index ($dbfilename) " . 
		     "for writing: $!");
    }
    else {
       tie (%mspnxl, DB_File, $dbfilename, O_RDONLY) || return 0;
    }
    
    @mspnxl=(1, $name);
    
    return 1;
}

# modify or create - need to open databases!
# for every object type we need to check for existence of referenced 
# nic handles and maintainers. So we need to have open the database of 
# given object and nic handlesdatabases and maintainer database.
# But for deadlock avoidance reasons 
# we must open databases in particular order - therefore clumsy hack 
# below. We open database in order: any other (alphabetic order), pn, ro, mt
# When updating route object we also check for existence of origin AS so 
# we need to open aut-num objects database additionally.


sub opendatabases {
    local(*entry, $type, *db, *nicdb, *othernicdb, *mtdb, *andb) = @_;
    my($source)     = $entry{'so'};
    my($split)      = $SPLIT{$source};
    local(%pnentry) = ('pn', '1', 'so', $source);
    local(%roentry) = ('ro', '1', 'so', $source);
    local(%mtentry) = ('mt', '1', 'so', $source);
    local(%anentry) = ('an', '1', 'so', $source);

    if (!$split || $type !~ /^rt|pn|ro|mt$/) {
	&dbopen(*db, *entry, 1) or return ($O_COULDNOTOPEN, $type);
	&dbclopen(*entry, 1) if $CLASSLESSDBS{$type};
	if ($split) {
	    if (!&dbopen(*nicdb, *pnentry, 0)) {
		&dbclose(*db);
		&dbclclose() if $CLASSLESSDBS{$type};
		return ($O_COULDNOTOPEN, 'pn');
	    }
	    if (!&dbopen(*othernicdb, *roentry, 0)) {
		&dbclose(*nicdb);
		&dbclose(*db);
		&dbclclose() if $CLASSLESSDBS{$type};
		return ($O_COULDNOTOPEN, 'ro');
	    }
	    if (!&dbopen(*mtdb, *mtentry, 0)) {
		&dbclose(*nicdb);
		&dbclose(*othernicdb);
		&dbclose(*db);
		&dbclclose() if $CLASSLESSDBS{$type};
		return ($O_COULDNOTOPEN, 'mt');
	    }
	}
	return ($O_OK, $type);
    }
    if ($type eq 'rt') {
	&dbopen(*andb, *anentry, 0) or return ($O_COULDNOTOPEN, 'an');
	if (!&dbopen(*db, *entry, 1)) {
	    &dbclose(*andb);
	    return ($O_COULDNOTOPEN, $type);
	}
	else {
	    &dbclopen(*entry, 1);
	}
	if (!&dbopen(*nicdb, *pnentry, 0)) {
	    &dbclose(*andb);
	    &dbclose(*db);
	    &dbclclose();
	    return ($O_COULDNOTOPEN, 'pn');
	}
	if (!&dbopen(*othernicdb, *roentry, 0)) {
	    &dbclose(*andb);
	    &dbclose(*nicdb);
	    &dbclose(*db);
	    &dbclclose();
	    return ($O_COULDNOTOPEN, 'ro');
	}
	if (!&dbopen(*mtdb, *mtentry, 0)) {
	    &dbclose(*andb);
	    &dbclose(*nicdb);
	    &dbclose(*othernicdb);
	    &dbclose(*db);
	    &dbclclose();
	    return ($O_COULDNOTOPEN, 'mt');
	}
    }
    elsif ($type eq 'pn') {
	&dbopen(*db, *entry, 1) or return ($O_COULDNOTOPEN, $type);
	if (!&dbopen(*othernicdb, *roentry, 0)) {
	    &dbclose(*db);
	    return ($O_COULDNOTOPEN, 'ro');
	}
	if (!&dbopen(*mtdb, *mtentry, 0)) {
	    &dbclose(*db);
	    &dbclose(*othernicdb);
	    return ($O_COULDNOTOPEN, 'mt');
	}
    }
    elsif ($type eq 'ro') {
	&dbopen(*othernicdb, *pnentry, 0) or return ($O_COULDNOTOPEN, 'pn');
	if (!&dbopen(*db, *entry, 1)) {
	    &dbclose(*othernicdb);
	    return ($O_COULDNOTOPEN, $type);
	}
	if (!&dbopen(*mtdb, *mtentry, 0)) {
	    &dbclose(*nicdb);
	    &dbclose(*othernicdb);
	    return ($O_COULDNOTOPEN, 'mt');
	}
    }
    elsif ($type eq 'mt') {
	# first open pn as nicdb then ro as othernicdb and then mt as db
	&dbopen(*nicdb, *pnentry, 0) or return ($O_COULDNOTOPEN, 'pn');
	if (!&dbopen(*othernicdb, *roentry, 0)) {
	    &dbclose(*nicdb);
	    return ($O_COULDNOTOPEN, 'ro');
	}
	if (!&dbopen(*db, *entry, 1)) {
	    &dbclose(*nicdb);
	    &dbclose(*othernicdb);
	    return ($O_COULDNOTOPEN, 'mt');
	}
    }
    return ($O_OK, $type);
}

1;
