/*
* Copyright 1995, 2003 Perforce Software. All rights reserved.
*
* This file is part of Perforce - the FAST SCM System.
*/
/*
* fileio -- FileIO, FileIOBinary, FileIOAppend methods
*
* Hacked to hell for OS implementations.
*/
# define NEED_FLOCK
# define NEED_FSYNC
# define NEED_FCNTL
# define NEED_FILE
# define NEED_STAT
# define NEED_UTIME
# define NEED_ERRNO
# define NEED_READLINK
# include <stdhdrs.h>
# include <error.h>
# include <errornum.h>
# include <debug.h>
# include <tunable.h>
# include <msgsupp.h>
# include <strbuf.h>
# include <datetime.h>
# include <i18napi.h>
# include <charcvt.h>
# include <largefile.h>
# include <lockfile.h>
# include <md5.h>
# include <msgos.h>
# include "filesys.h"
# include "fileio.h"
// We need to know the user's umask so that Chmod() doesn't give
// away permissions beyond the umask.
int global_umask = -1;
# ifndef O_BINARY
# define O_BINARY 0
# endif
const FileIOBinary::OpenMode FileIOBinary::openModes[3] = {
"open for read",
O_RDONLY|O_BINARY,
O_RDONLY|O_BINARY,
0,
"open for write",
O_WRONLY|O_CREAT|O_TRUNC|O_BINARY,
O_WRONLY|O_CREAT|O_APPEND,
1,
"open for read/write",
O_RDWR|O_CREAT|O_BINARY,
O_RDWR|O_CREAT|O_APPEND,
1
} ;
FileIO::FileIO()
{
// Get the umask if we don't know it already.
if( global_umask < 0 )
{
global_umask = umask( 0 );
(void)umask( global_umask );
}
}
# if !defined( OS_VMS ) && !defined( OS_NT )
void
FileIO::Rename( FileSys *target, Error *e )
{
// yeech - must not exist to rename
# if defined(OS_OS2) || defined(OS_AS400)
target->Unlink( 0 );
# endif
# if defined( OS_MACOSX ) || defined( OS_DARWIN )
// On mac, deal with a possible immutable *uchg) flag
struct stat sb;
if( stat( Name(), &sb ) >= 0 && ( sb.st_flags & UF_IMMUTABLE ) )
chflags( Name(), sb.st_flags & ~UF_IMMUTABLE );
if( stat( target->Name(), &sb ) >= 0 && ( sb.st_flags & UF_IMMUTABLE ) )
chflags( target->Name(), sb.st_flags & ~UF_IMMUTABLE );
# endif
// Run on all other UNIX variants, including Mac OS X
if( rename( Name(), target->Name() ) < 0 )
{
e->Sys( "rename", target->Name() );
return;
}
// source file has been deleted, clear the flag
ClearDeleteOnClose();
}
void
FileIO::ChmodTime( int modTime, Error *e )
{
# ifdef HAVE_UTIME
struct utimbuf t;
DateTime now;
now.SetNow();
t.actime = DateTime::Localize( now.Value() );
t.modtime = DateTime::Localize( modTime );
if( utime( Name(), &t ) < 0 )
e->Sys( "utime", Name() );
# endif // HAVE_UTIME
}
# endif // !OS_VMS && !OS_NT
void
FileIO::ChmodTime( Error *e )
{
if( modTime )
ChmodTime( modTime, e );
}
# if !defined( OS_NT )
void
FileIO::Truncate( offL_t offset, Error *e )
{
// Don't bother if non-existent.
if( !( Stat() & FSF_EXISTS ) )
return;
# ifdef HAVE_TRUNCATE
if( truncate( Name(), offset ) >= 0 )
return;
# endif // HAVE_TRUNCATE
e->Sys( "truncate", Name() );
}
void
FileIO::Truncate( Error *e )
{
// Don't bother if non-existent.
if( !( Stat() & FSF_EXISTS ) )
return;
// Try truncate first; if that fails (as it will on secure NCR's),
// then open O_TRUNC.
# ifdef HAVE_TRUNCATE
if( truncate( Name(), 0 ) >= 0 )
return;
# endif // HAVE_TRUNCATE
int fd;
# if !defined ( OS_MACOSX )
if( ( fd = openL( Name(), O_WRONLY|O_TRUNC, PERM_0666 ) ) >= 0 )
# else
if( ( fd = openL( Name(), O_WRONLY|O_TRUNC ) ) >= 0 )
# endif // OS_MACOSX
{
close( fd );
return;
}
e->Sys( "truncate", Name() );
}
# endif // !defined( OS_NT )
/*
* FileIO::Stat() - return flags if file exists
*/
# ifndef OS_NT
int
FileIO::Stat()
{
// Stat & check for missing, special
int flags = 0;
struct statbL sb;
# ifdef HAVE_SYMLINKS
// With symlinks, we first stat the link
// and if it is a link, then stat the actual file.
// A symlink without an underlying file appears
// as FSF_SYMLINK. With an underlying file
// as FSF_SYMLINK|FSF_EXISTS.
if( lstatL( Name(), &sb ) < 0 )
return flags;
if( S_ISLNK( sb.st_mode ) )
flags |= FSF_SYMLINK;
if( S_ISLNK( sb.st_mode ) && statL( Name(), &sb ) < 0 )
return flags;
# else
// No symlinks: just stat the file.
if( statL( Name(), &sb ) < 0 )
return flags;
# endif
flags |= FSF_EXISTS;
if( sb.st_mode & S_IWUSR ) flags |= FSF_WRITEABLE;
if( sb.st_mode & S_IXUSR ) flags |= FSF_EXECUTABLE;
if( S_ISDIR( sb.st_mode ) ) flags |= FSF_DIRECTORY;
if( !S_ISREG( sb.st_mode ) ) flags |= FSF_SPECIAL;
if( !sb.st_size ) flags |= FSF_EMPTY;
# if defined ( OS_DARWIN ) || defined( OS_MACOSX )
// If the immutable bit is set, we can't write this file
// Chmod() will unset the immutable bit if it needs to.
//
if( sb.st_flags & UF_IMMUTABLE ) flags &= ~FSF_WRITEABLE;
# endif
return flags;
}
# endif
int
FileIO::Readlink( StrBuf &linkPath, Error *e )
{
# if defined( HAVE_SYMLINKS ) && !defined( OS_NT )
// call will fail if this is not a symlink
linkPath.Clear();
char buf[SIZE_MEDSTR];
ssize_t namelen;
if( (namelen = readlink( Name(), buf, sizeof(buf)-1 )) < 0 )
{
e->Sys( "readlink", Name() );
return errno;
}
else
{
buf[namelen] = '\0';
linkPath.Set( buf );
}
return 0;
# else
/*
* After searching on the net for a equivalent
* to no avail, we will not "do windows". This
* is used in the Clustered Server which is currently
* linux-only, caveat emptor.
*/
e->Set( MsgSupp::NotImplemented ) << "Windows";
return EINVAL;
# endif
}
/*
* Make a symlink (linux only)
* NB: not atomic!
*/
int
FileIO::Writelink( const StrPtr &linkPath, Error *e )
{
# if defined( HAVE_SYMLINKS ) && !defined( OS_NT )
if( ( Stat() & (FSF_EXISTS|FSF_SYMLINK) ) &&
unlink( Name() ) < 0 )
{
e->Sys( "unlink", Name() );
return errno;
}
// call will fail if target exists
if( symlink( linkPath.Text(), Name() ) < 0 )
{
e->Sys( "symlink", linkPath.Text() );
return errno;
}
return 0;
# else
/*
* After searching on the net for a equivalent
* to no avail, we will not "do windows". This
* is used in the Clustered Server which is currently
* linux-only, caveat emptor.
*/
e->Set( MsgSupp::NotImplemented ) << "Windows";
return EINVAL;
# endif
}
# if !defined( OS_NT )
int
FileIO::GetOwner()
{
int uid = 0;
struct statbL sb;
# ifdef HAVE_SYMLINKS
// With symlinks, we first stat the link
// and if it is a link, then stat the actual file.
// A symlink without an underlying file appears
// as FSF_SYMLINK. With an underlying file
// as FSF_SYMLINK|FSF_EXISTS.
if( lstatL( Name(), &sb ) < 0 )
return uid;
if( S_ISLNK( sb.st_mode ) && statL( Name(), &sb ) < 0 )
return uid;
# else
// No symlinks: just stat the file.
if( statL( Name(), &sb ) < 0 )
return uid;
# endif
return sb.st_uid;
}
# endif
# if !defined( OS_NT )
bool
FileIO::HasOnlyPerm( FilePerm perms )
{
struct statbL sb;
mode_t modeBits = 0;
if( statL( Name(), &sb ) < 0 )
return false;
switch (perms)
{
case FPM_RO:
modeBits = PERM_0222;
break;
case FPM_RW:
modeBits = PERM_0666;
break;
case FPM_ROO:
modeBits = PERM_0400;
break;
case FPM_RXO:
modeBits = PERM_0500;
break;
case FPM_RWO:
modeBits = PERM_0600;
break;
case FPM_RWXO:
modeBits = PERM_0700;
break;
}
/*
* In this case we want an exact match of permissions
* We don't want to "and" to a mask, since we also want
* to verify that the other bits are off.
*/
if( (sb.st_mode & PERMSMASK) == modeBits )
return true;
return false;
}
# endif // not defined OS_NT
# if !defined( OS_NT )
int
FileIO::StatModTime()
{
struct statbL sb;
if( statL( Name(), &sb ) < 0 )
return 0;
return (int)( DateTime::Centralize( sb.st_mtime ) );
}
# endif
/*
* FileIO::Unlink() - remove single file (error optional)
*/
# if !defined( OS_VMS ) && !defined( OS_NT )
void
FileIO::Unlink( Error *e )
{
# if defined(OS_OS2) || defined(OS_DARWIN) || defined(OS_MACOSX)
// yeech - must be writable to remove
// even on Darwin because the uchg flag might be set
// but only for real files, otherwise the chmod
// will propagate to the target file.
Chmod( FPM_RW, 0 );
# endif
if( *Name() && unlink( Name() ) < 0 && e )
e->Sys( "unlink", Name() );
}
void
FileIO::Chmod( FilePerm perms, Error *e )
{
// Don't set perms on symlinks
if( ( GetType() & FST_MASK ) == FST_SYMLINK )
return;
// Permissions for readonly/readwrite, exec vs no exec
int bits = IsExec() ? PERM_0777 : PERM_0666;
switch( perms )
{
case FPM_RO: bits &= ~PERM_0222; break;
case FPM_ROO: bits &= ~PERM_0266; break;
case FPM_RWO: bits = PERM_0600; break; // for key file, set exactly to rwo
case FPM_RXO: bits = PERM_0500; break;
case FPM_RWXO: bits = PERM_0700; break;
}
if( chmod( Name(), bits & ~global_umask ) >= 0 )
return;
# if defined( OS_MACOSX ) || defined( OS_DARWIN )
// On mac, try unlocking the immutable bit and chmoding again.
struct stat sb;
if( stat( Name(), &sb ) >= 0 &&
chflags( Name(), sb.st_flags & ~UF_IMMUTABLE ) >= 0 &&
chmod( Name(), bits & ~global_umask ) >= 0 )
return;
# endif
// Can be called with e==0 to ignore error.
if( e )
e->Sys( "chmod", Name() );
}
# endif /* !OS_VMS && !OS_NT */
/*
* FileIoText Support
* FileIOBinary support
*/
FileIOBinary::~FileIOBinary()
{
Cleanup();
}
# if !defined( OS_NT )
void
FileIOBinary::Open( FileOpenMode mode, Error *e )
{
// Save mode for write, close
this->mode = mode;
// Get bits for (binary) open
int bits = openModes[ mode ].bflags;
// Handle exclusive open (must not already exist)
# ifdef O_EXCL
// Set O_EXCL to ensure we create the file when we open it.
if( GetType() & FST_M_EXCL )
bits |= O_EXCL;
# else
// No O_EXCL: we'll manually check if file already exists.
// Not atomic, but what can one do?
if( ( GetType() & FST_M_EXCL ) && ( Stat() & FSF_EXISTS ) )
{
e->Set( E_FAILED, "file exists" );
// if file is set delete on close unset that because we
// didn't create the file...
ClearDeleteOnClose();
return;
}
# endif
// open stdin/stdout or real file
if( Name()[0] == '-' && !Name()[1] )
{
// we do raw output: flush stdout
// for nice mixing of messages.
if( mode == FOM_WRITE )
fflush( stdout );
fd = openModes[ mode ].standard;
}
else if( ( fd = openL( Name(), bits, PERM_0666 ) ) < 0 )
{
e->Sys( openModes[ mode ].modeName, Name() );
# ifdef O_EXCL
// if we failed to create the file probably due to the
// file already existing (O_EXCL)
// then unset delete on close because we didn't create it...
if( ( bits & (O_EXCL|O_CREAT) ) == (O_EXCL|O_CREAT) )
ClearDeleteOnClose();
# endif
}
}
# endif
void
FileIOBinary::Close( Error *e )
{
if( fd < 2 )
return;
if( ( GetType() & FST_M_SYNC ) )
Fsync( e );
# ifdef OS_LINUX
if( cacheHint && p4tunable.Get( P4TUNE_FILESYS_CACHEHINT ) )
(void) posix_fadvise64( fd, 0, 0, POSIX_FADV_DONTNEED );
# endif
if( close( fd ) < 0 )
e->Sys( "close", Name() );
fd = -1;
if( mode == FOM_WRITE && modTime )
ChmodTime( modTime, e );
if( mode == FOM_WRITE )
Chmod( perms, e );
}
void
FileIOBinary::Fsync( Error *e )
{
# ifdef HAVE_FSYNC
if( fd >= 0 && fsync( fd ) < 0 )
e->Sys( "fsync", Name() );
# endif
}
void
FileIOBinary::Write( const char *buf, int len, Error *e )
{
// Raw, unbuffered write
int l;
if( ( l = write( fd, buf, len ) ) < 0 )
e->Sys( "write", Name() );
else
tellpos += l;
if( checksum && l > 0 )
checksum->Update( StrRef( buf, l ) );
}
int
FileIOBinary::Read( char *buf, int len, Error *e )
{
// Raw, unbuffered read
int l;
if( ( l = read( fd, buf, len ) ) < 0 )
e->Sys( "read", Name() );
else
tellpos += l;
return l;
}
# if !defined( OS_NT )
offL_t
FileIOBinary::GetSize()
{
struct statbL sb;
if( fd >= 0 && fstatL( fd, &sb ) < 0 )
return -1;
if( fd < 0 && statL( Name(), &sb ) < 0 )
return -1;
return sb.st_size;
}
void
FileIOBinary::Seek( offL_t offset, Error *e )
{
if( lseekL( fd, offset, 0 ) == -1 )
e->Sys( "seek", Name() );
tellpos = offset;
}
# endif
offL_t
FileIOBinary::Tell()
{
return tellpos;
}
/*
* FileIOAppend support
*
* On UNIX, FileIOAppend::Write() and Rename() obey a special
* protocol to provide for atomic rotating of files that may
* be open by active processes.
*
* Rename() opens & locks the file, renames it, and then changes
* the permission on the renamed file to read-only.
*
* Write() locks and then fstats the file, to see if it is still
* writeable. If not, it assumes it has been renamed, so it closes
* and reopens the original file (which should always be writeable),
* and then relocks it. It can then write and unlock.
*
* There is the possibility that rapid fire Rename() calls may get
* between Write()'s reopen for write and its lock. In this odd case,
* we retry 10 times.
*/
FileIOAppend::~FileIOAppend()
{
// Need to set mode to read so it doesn't try to chmod the
// file in the destructor
mode = FOM_READ;
}
# if !defined( OS_NT )
void
FileIOAppend::Open( FileOpenMode mode, Error *e )
{
// Save mode for write, close
this->mode = mode;
// open stdin/stdout or real file
if( Name()[0] == '-' && !Name()[1] )
{
fd = openModes[ mode ].standard;
}
else if( ( fd = openL( Name(), openModes[ mode ].aflags, PERM_0666 ) ) < 0 )
{
e->Sys( openModes[ mode ].modeName, Name() );
ClearDeleteOnClose();
}
// Clear send/receive buffers
rcv = snd = 0;
}
offL_t
FileIOAppend::GetSize()
{
offL_t s = 0;
if( !lockFile( fd, LOCKF_SH ) )
{
s = FileIOBinary::GetSize();
lockFile( fd, LOCKF_UN );
}
else
s = FileIOBinary::GetSize();
return s;
}
void
FileIOAppend::Write( const char *buf, int len, Error *e )
{
// Lock and check for writeability.
// If made read-only by Rename(), close and reopen.
// If file keeps becomming read-only, give up after
// trying a while.
int tries = 10;
while( --tries )
{
struct statbL sb;
if( lockFile( fd, LOCKF_EX ) < 0 )
{
e->Sys( "lock", Name() );
return;
}
if( fstatL( fd, &sb ) < 0 )
{
e->Sys( "fstat", Name() );
return;
}
if( sb.st_mode & S_IWUSR )
break;
if( close( fd ) < 0 )
{
e->Sys( "close", Name() );
return;
}
Open( mode, e );
if( e->Test() )
return;
}
if( !tries )
{
e->Set( E_FAILED, "Tired of waiting for %file% to be writeable." )
<< Name();
return;
}
FileIOBinary::Write( buf, len, e );
if( lockFile( fd, LOCKF_UN ) < 0 )
{
e->Sys( "unlock", Name() );
return;
}
}
void
FileIOAppend::Rename( FileSys *target, Error *e )
{
// Open, lock for write, rename, chmod to RO, and close
Open( FOM_WRITE, e );
if( e->Test() )
return;
if( lockFile( fd, LOCKF_EX ) < 0 )
{
e->Sys( "lock", Name() );
return;
}
if( rename( Name(), target->Name() ) < 0 )
{
// Uh oh -- we can't rename. Maybe cross device?
// Do the dumb way: copy/truncate.
mode = FOM_READ;
Close( e );
Copy( target, FPM_RO, e );
if( e->Test() )
return;
Truncate( e );
return;
}
target->Chmod( FPM_RO, e );
// Need to set mode to read so it doesn't try to chmod the
// file in the destructor
mode = FOM_READ;
if( e->Test() )
return;
struct statbL sb;
if( fstatL( fd, &sb ) < 0 )
{
e->Sys( "fstat", Name() );
return;
}
if( sb.st_mode & S_IWUSR )
{
P4INT64 bigINode = sb.st_ino;
P4INT64 bigMode = sb.st_mode;
e->Set( MsgOs::ChmodBetrayal ) <<
Name() << target->Name() <<
StrNum( bigMode ) << StrNum( bigINode );
return;
}
Close( e );
}
# endif
void
FileIOAppend::Close( Error *e )
{
mode = FOM_READ;
FileIOBuffer::Close( e );
}