/* * Copyright (c) 2000-2001, 2003 Apple Computer, Inc. All Rights Reserved. * * The contents of this file constitute Original Code as defined in and are * subject to the Apple Public Source License Version 1.2 (the 'License'). * You may not use this file except in compliance with the License. Please obtain * a copy of the License at http://www.apple.com/publicsource and read it before * using this file. * * This Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS * OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the * specific language governing rights and limitations under the License. */ #include #include #include #include #include #include #include #include #include #define kAtomicFileMaxBlockSize INT_MAX // // AtomicFile.cpp - Description t.b.d. // AtomicFile::AtomicFile(const std::string &inPath) : mPath(inPath) { pathSplit(inPath, mDir, mFile); } AtomicFile::~AtomicFile() { } // Aquire the write lock and remove the file. void AtomicFile::performDelete() { AtomicLockedFile lock(*this); if (::unlink(mPath.c_str()) != 0) { int error = errno; secdebug("atomicfile", "unlink %s: %s", mPath.c_str(), strerror(error)); if (error == ENOENT) CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST); else UnixError::throwMe(error); } } // Aquire the write lock and rename the file (and bump the version and stuff). void AtomicFile::rename(const std::string &inNewPath) { const char *path = mPath.c_str(); const char *newPath = inNewPath.c_str(); // @@@ lock the destination file too. AtomicLockedFile lock(*this); if (::rename(path, newPath) != 0) { int error = errno; secdebug("atomicfile", "rename(%s, %s): %s", path, newPath, strerror(error)); UnixError::throwMe(error); } } // Lock the file for writing and return a newly created AtomicTempFile. RefPointer AtomicFile::create(mode_t mode) { const char *path = mPath.c_str(); // First make sure the directory to this file exists and is writable mkpath(mDir); RefPointer lock(new AtomicLockedFile(*this)); int fileRef = ropen(path, O_WRONLY|O_CREAT|O_EXCL, mode); if (fileRef == -1) { int error = errno; secdebug("atomicfile", "open %s: %s", path, strerror(error)); // Do the obvious error code translations here. // @@@ Consider moving these up a level. if (error == EACCES) CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); else if (error == EEXIST) CssmError::throwMe(CSSMERR_DL_DATASTORE_ALREADY_EXISTS); else UnixError::throwMe(error); } rclose(fileRef); try { // Now that we have created the lock and the new db file create a tempfile // object. RefPointer temp(new AtomicTempFile(*this, lock, mode)); secdebug("atomicfile", "%p created %s", this, path); return temp; } catch (...) { // Creating the temp file failed so remove the db file we just created too. if (::unlink(path) == -1) { secdebug("atomicfile", "unlink %s: %s", path, strerror(errno)); } throw; } } // Lock the database file for writing and return a newly created AtomicTempFile. RefPointer AtomicFile::write() { RefPointer lock(new AtomicLockedFile(*this)); return new AtomicTempFile(*this, lock); } // Return a bufferedFile containing current version of the file for reading. RefPointer AtomicFile::read() { return new AtomicBufferedFile(mPath); } mode_t AtomicFile::mode() const { const char *path = mPath.c_str(); struct stat st; if (::stat(path, &st) == -1) { int error = errno; secdebug("atomicfile", "stat %s: %s", path, strerror(error)); UnixError::throwMe(error); } return st.st_mode; } // Split full into a dir and file component. void AtomicFile::pathSplit(const std::string &inFull, std::string &outDir, std::string &outFile) { std::string::size_type slash, len = inFull.size(); slash = inFull.rfind('/'); if (slash == std::string::npos) { outDir = ""; outFile = inFull; } else if (slash + 1 == len) { outDir = inFull; outFile = ""; } else { outDir = inFull.substr(0, slash + 1); outFile = inFull.substr(slash + 1, len); } } // // Make sure the directory up to inDir exists inDir *must* end in a slash. // void AtomicFile::mkpath(const std::string &inDir, mode_t mode) { for (std::string::size_type pos = 0; (pos = inDir.find('/', pos + 1)) != std::string::npos;) { std::string path = inDir.substr(0, pos); const char *cpath = path.c_str(); struct stat sb; if (::stat(cpath, &sb)) { if (errno != ENOENT || ::mkdir(cpath, mode)) UnixError::throwMe(errno); } else if (!S_ISDIR(sb.st_mode)) CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); // @@@ Should be is a directory } } int AtomicFile::ropen(const char *const name, int flags, mode_t mode) { int fd, tries_left = 4 /* kNoResRetry */; do { fd = ::open(name, flags, mode); } while (fd < 0 && (errno == EINTR || errno == ENFILE && --tries_left >= 0)); return fd; } int AtomicFile::rclose(int fd) { int result; do { result = ::close(fd); } while(result && errno == EINTR); return result; } // // AtomicBufferedFile - This represents an instance of a file opened for reading. // The file is read into memory and closed after this is done. // The memory is released when this object is destroyed. // AtomicBufferedFile::AtomicBufferedFile(const std::string &inPath) : mPath(inPath), mFileRef(-1), mBuffer(NULL), mLength(0) { } AtomicBufferedFile::~AtomicBufferedFile() { if (mFileRef >= 0) { AtomicFile::rclose(mFileRef); secdebug("atomicfile", "%p closed %s", this, mPath.c_str()); } if (mBuffer) { secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer); free(mBuffer); } } // // Open the file and return the length in bytes. // off_t AtomicBufferedFile::open() { const char *path = mPath.c_str(); if (mFileRef >= 0) { secdebug("atomicfile", "open %s: already open, closing and reopening", path); close(); } mFileRef = AtomicFile::ropen(path, O_RDONLY, 0); if (mFileRef == -1) { int error = errno; secdebug("atomicfile", "open %s: %s", path, strerror(error)); // Do the obvious error code translations here. // @@@ Consider moving these up a level. if (error == ENOENT) CssmError::throwMe(CSSMERR_DL_DATASTORE_DOESNOT_EXIST); else if (error == EACCES) CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); else UnixError::throwMe(error); } mLength = ::lseek(mFileRef, 0, SEEK_END); if (mLength == -1) { int error = errno; secdebug("atomicfile", "lseek(%s, END): %s", path, strerror(error)); AtomicFile::rclose(mFileRef); UnixError::throwMe(error); } secdebug("atomicfile", "%p opened %s: %qd bytes", this, path, mLength); return mLength; } // // Read the file starting at inOffset for inLength bytes into the buffer and return // a pointer to it. On return outLength contain the actual number of bytes read, it // will only ever be less than inLength if EOF was reached, and it will never be more // than inLength. // const uint8 * AtomicBufferedFile::read(off_t inOffset, off_t inLength, off_t &outLength) { if (mFileRef < 0) { secdebug("atomicfile", "read %s: file yet not opened, opening", mPath.c_str()); open(); } off_t bytesLeft = inLength; uint8 *ptr; if (mBuffer) { secdebug("atomicfile", "%p free %s buffer %p", this, mPath.c_str(), mBuffer); free(mBuffer); } mBuffer = ptr = reinterpret_cast(malloc(bytesLeft)); secdebug("atomicfile", "%p allocated %s buffer %p size %qd", this, mPath.c_str(), mBuffer, bytesLeft); off_t pos = inOffset; while (bytesLeft) { size_t toRead = bytesLeft > kAtomicFileMaxBlockSize ? kAtomicFileMaxBlockSize : size_t(bytesLeft); ssize_t bytesRead = ::pread(mFileRef, ptr, toRead, pos); if (bytesRead == -1) { int error = errno; if (error == EINTR) { // We got interrupted by a signal, so try again. secdebug("atomicfile", "pread %s: interrupted, retrying", mPath.c_str()); continue; } secdebug("atomicfile", "pread %s: %s", mPath.c_str(), strerror(error)); free(mBuffer); mBuffer = NULL; UnixError::throwMe(error); } // Read returning 0 means EOF was reached so we're done. if (bytesRead == 0) break; secdebug("atomicfile", "%p read %s: %d bytes to %p", this, mPath.c_str(), bytesRead, ptr); bytesLeft -= bytesRead; ptr += bytesRead; pos += bytesRead; } // Compute length outLength = ptr - mBuffer; return mBuffer; } void AtomicBufferedFile::close() { if (mFileRef < 0) { secdebug("atomicfile", "close %s: already closed", mPath.c_str()); } else { int result = AtomicFile::rclose(mFileRef); mFileRef = -1; if (result == -1) { int error = errno; secdebug("atomicfile", "close %s: %s", mPath.c_str(), strerror(errno)); UnixError::throwMe(error); } secdebug("atomicfile", "%p closed %s", this, mPath.c_str()); } } // // AtomicTempFile - A temporary file to write changes to. // AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer &inLockedFile, mode_t mode) : mFile(inFile), mLockedFile(inLockedFile), mCreating(true) { create(mode); } AtomicTempFile::AtomicTempFile(AtomicFile &inFile, const RefPointer &inLockedFile) : mFile(inFile), mLockedFile(inLockedFile), mCreating(false) { create(mFile.mode()); } AtomicTempFile::~AtomicTempFile() { // rollback if we didn't commit yet. if (mFileRef >= 0) rollback(); } // // Open the file and return the length in bytes. // void AtomicTempFile::create(mode_t mode) { mPath = mFile.dir() + "," + mFile.file(); const char *path = mPath.c_str(); mFileRef = AtomicFile::ropen(path, O_WRONLY|O_CREAT|O_TRUNC, mode); if (mFileRef == -1) { int error = errno; secdebug("atomicfile", "open %s: %s", path, strerror(error)); // Do the obvious error code translations here. // @@@ Consider moving these up a level. if (error == EACCES) CssmError::throwMe(CSSM_ERRCODE_OS_ACCESS_DENIED); else UnixError::throwMe(error); } secdebug("atomicfile", "%p created %s", this, path); } void AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint32 inData) { uint32 aData = htonl(inData); write(inOffsetType, inOffset, reinterpret_cast(&aData), sizeof(aData)); } void AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint32 *inData, uint32 inCount) { #ifdef HOST_LONG_IS_NETWORK_LONG // Optimize this for the case where hl == nl const uint32 *aBuffer = inData; #else auto_array aBuffer(inCount); for (uint32 i = 0; i < inCount; i++) aBuffer.get()[i] = htonl(inData[i]); #endif write(inOffsetType, inOffset, reinterpret_cast(aBuffer.get()), inCount * sizeof(*inData)); } void AtomicTempFile::write(AtomicFile::OffsetType inOffsetType, off_t inOffset, const uint8 *inData, size_t inLength) { off_t pos; if (inOffsetType == AtomicFile::FromEnd) { pos = ::lseek(mFileRef, 0, SEEK_END); if (pos == -1) { int error = errno; secdebug("atomicfile", "lseek(%s, %qd): %s", mPath.c_str(), inOffset, strerror(error)); UnixError::throwMe(error); } } else if (inOffsetType == AtomicFile::FromStart) pos = inOffset; else CssmError::throwMe(CSSM_ERRCODE_INTERNAL_ERROR); off_t bytesLeft = inLength; const uint8 *ptr = inData; while (bytesLeft) { size_t toWrite = bytesLeft > kAtomicFileMaxBlockSize ? kAtomicFileMaxBlockSize : size_t(bytesLeft); ssize_t bytesWritten = ::pwrite(mFileRef, ptr, toWrite, pos); if (bytesWritten == -1) { int error = errno; if (error == EINTR) { // We got interrupted by a signal, so try again. secdebug("atomicfile", "write %s: interrupted, retrying", mPath.c_str()); continue; } secdebug("atomicfile", "write %s: %s", mPath.c_str(), strerror(error)); UnixError::throwMe(error); } // Write returning 0 is bad mmkay. if (bytesWritten == 0) { secdebug("atomicfile", "write %s: 0 bytes written", mPath.c_str()); CssmError::throwMe(CSSMERR_DL_INTERNAL_ERROR); } secdebug("atomicfile", "%p wrote %s %d bytes from %p", this, mPath.c_str(), bytesWritten, ptr); bytesLeft -= bytesWritten; ptr += bytesWritten; pos += bytesWritten; } } void AtomicTempFile::fsync() { if (mFileRef < 0) { secdebug("atomicfile", "fsync %s: already closed", mPath.c_str()); } else { int result; do { result = ::fsync(mFileRef); } while (result && errno == EINTR); if (result == -1) { int error = errno; secdebug("atomicfile", "fsync %s: %s", mPath.c_str(), strerror(errno)); UnixError::throwMe(error); } secdebug("atomicfile", "%p fsynced %s", this, mPath.c_str()); } } void AtomicTempFile::close() { if (mFileRef < 0) { secdebug("atomicfile", "close %s: already closed", mPath.c_str()); } else { int result = AtomicFile::rclose(mFileRef); mFileRef = -1; if (result == -1) { int error = errno; secdebug("atomicfile", "close %s: %s", mPath.c_str(), strerror(errno)); UnixError::throwMe(error); } secdebug("atomicfile", "%p closed %s", this, mPath.c_str()); } } // Commit the current create or write and close the write file. Note that a throw during the commit does an automatic rollback. void AtomicTempFile::commit() { try { fsync(); close(); const char *oldPath = mPath.c_str(); const char *newPath = mFile.path().c_str(); if (::rename(oldPath, newPath) == -1) { int error = errno; secdebug("atomicfile", "rename (%s, %s): %s", oldPath, newPath, strerror(errno)); UnixError::throwMe(error); } // Unlock the lockfile mLockedFile = NULL; secdebug("atomicfile", "%p commited %s", this, oldPath); } catch (...) { rollback(); throw; } } // Rollback the current create or write (happens automatically if commit() isn't called before the destructor is. void AtomicTempFile::rollback() throw() { if (mFileRef >= 0) { AtomicFile::rclose(mFileRef); mFileRef = -1; } // @@@ Log errors if this fails. const char *path = mPath.c_str(); if (::unlink(path) == -1) { secdebug("atomicfile", "unlink %s: %s", path, strerror(errno)); // rollback can't throw } // @@@ Think about this. Depending on how we do locking we might not need this. if (mCreating) { const char *path = mFile.path().c_str(); if (::unlink(path) == -1) { secdebug("atomicfile", "unlink %s: %s", path, strerror(errno)); // rollback can't throw } } } // // An advisory write lock for inFile. // AtomicLockedFile::AtomicLockedFile(AtomicFile &inFile) : mDir(inFile.dir()), mPath(inFile.dir() + "lck~" + inFile.file()) { lock(); } AtomicLockedFile::~AtomicLockedFile() { unlock(); } std::string AtomicLockedFile::unique(mode_t mode) { static const int randomPart = 16; DevRandomGenerator randomGen; std::string::size_type dirSize = mDir.size(); std::string fullname(dirSize + randomPart + 2, '\0'); fullname.replace(0, dirSize, mDir); fullname[dirSize] = '~'; /* UNIQ_PREFIX */ char buf[randomPart]; struct stat filebuf; int result, fd = -1; for (int retries = 0; retries < 10; ++retries) { /* Make a random filename. */ randomGen.random(buf, randomPart); for (int ix = 0; ix < randomPart; ++ix) { char ch = buf[ix] & 0x3f; fullname[ix + dirSize + 1] = ch + ( ch < 26 ? 'A' : ch < 26 + 26 ? 'a' - 26 : ch < 26 + 26 + 10 ? '0' - 26 - 26 : ch == 26 + 26 + 10 ? '-' - 26 - 26 - 10 : '_' - 26 - 26 - 11); } result = lstat(fullname.c_str(), &filebuf); if (result && errno == ENAMETOOLONG) { do fullname.erase(fullname.end() - 1); while((result = lstat(fullname.c_str(), &filebuf)) && errno == ENAMETOOLONG && fullname.size() > dirSize + 8); } /* either it stopped being a problem or we ran out of filename */ if (result && errno == ENOENT) { fd = AtomicFile::ropen(fullname.c_str(), O_WRONLY|O_CREAT|O_EXCL, mode); if (fd >= 0 || errno != EEXIST) break; } } if (fd < 0) { int error = errno; ::syslog(LOG_ERR, "Couldn't create temp file %s: %s", fullname.c_str(), strerror(error)); secdebug("atomicfile", "Couldn't create temp file %s: %s", fullname.c_str(), strerror(error)); UnixError::throwMe(error); } /* @@@ Check for EINTR. */ write(fd, "0", 1); /* pid 0, `works' across networks */ AtomicFile::rclose(fd); return fullname; } /* Return 0 on success and 1 on failure if st is set to the result of stat(old) and -1 on failure if the stat(old) failed. */ int AtomicLockedFile::rlink(const char *const old, const char *const newn, struct stat &sto) { int result = ::link(old,newn); if (result) { int serrno = errno; if (::lstat(old, &sto) == 0) { struct stat stn; if (::lstat(newn, &stn) == 0 && sto.st_dev == stn.st_dev && sto.st_ino == stn.st_ino && sto.st_uid == stn.st_uid && sto.st_gid == stn.st_gid && !S_ISLNK(sto.st_mode)) { /* Link failed but files are the same so the link really went ok. */ return 0; } else result = 1; } errno = serrno; /* Restore errno from link() */ } return result; } /* NFS-resistant rename() * rename with fallback for systems that don't support it * Note that this does not preserve the contents of the file. */ int AtomicLockedFile::myrename(const char *const old, const char *const newn) { struct stat stbuf; int fd = -1; int ret; /* Try a real hardlink */ ret = rlink(old, newn, stbuf); if (ret > 0) { if (stbuf.st_nlink < 2 && (errno == EXDEV || errno == ENOTSUP)) { /* Hard link failed so just create a new file with O_EXCL instead. */ fd = AtomicFile::ropen(newn, O_WRONLY|O_CREAT|O_EXCL, stbuf.st_mode); if (fd >= 0) ret = 0; } } /* We want the errno from the link or the ropen, not that of the unlink. */ int serrno = errno; /* Unlink the temp file. */ ::unlink(old); if (fd > 0) AtomicFile::rclose(fd); errno = serrno; return ret; } int AtomicLockedFile::xcreat(const char *const name, mode_t mode, time_t &tim) { std::string uniqueName = unique(mode); const char *uniquePath = uniqueName.c_str(); struct stat stbuf; /* return the filesystem time to the caller */ stat(uniquePath, &stbuf); tim = stbuf.st_mtime; return myrename(uniquePath, name); } void AtomicLockedFile::lock(mode_t mode) { const char *path = mPath.c_str(); bool triedforce = false; struct stat stbuf; time_t t, locktimeout = 1024; /* DEFlocktimeout, 17 minutes. */ bool doSyslog = false; bool failed = false; int retries = 0; while (!failed) { /* Don't syslog first time through. */ if (doSyslog) ::syslog(LOG_NOTICE, "Locking %s", path); else doSyslog = true; secdebug("atomicfile", "Locking %s", path); /* in order to cater for clock skew: get */ if (!xcreat(path, mode, t)) /* time t from the filesystem */ { /* lock acquired, hurray! */ break; } switch(errno) { case EEXIST: /* check if it's time for a lock override */ if (!lstat(path, &stbuf) && stbuf.st_size <= 16 /* MAX_locksize */ && locktimeout && !lstat(path, &stbuf) && locktimeout < t - stbuf.st_mtime) /* stat() till unlink() should be atomic, but can't guarantee that. */ { if (triedforce) { /* Already tried, force lock override, not trying again */ failed = true; break; } else if (S_ISDIR(stbuf.st_mode) || ::unlink(path)) { triedforce=true; ::syslog(LOG_ERR, "Forced unlock denied on %s", path); secdebug("atomicfile", "Forced unlock denied on %s", path); } else { ::syslog(LOG_ERR, "Forcing lock on %s", path); secdebug("atomicfile", "Forcing lock on %s", path); sleep(16 /* DEFsuspend */); break; } } else triedforce = false; /* legitimate iteration, clear flag */ /* Reset retry counter. */ retries = 0; sleep(8 /* DEFlocksleep */); break; case ENOSPC: /* no space left, treat it as a transient */ #ifdef EDQUOT /* NFS failure */ case EDQUOT: /* maybe it was a short term shortage? */ #endif case ENOENT: case ENOTDIR: case EIO: /*case EACCES:*/ if(++retries < (7 + 1)) /* nfsTRY number of times+1 to ignore spurious NFS errors */ sleep(8 /* DEFlocksleep */); else failed = true; break; #ifdef ENAMETOOLONG case ENAMETOOLONG: /* Filename is too long, shorten and retry */ if (mPath.size() > mDir.size() + 8) { secdebug("atomicfile", "Truncating %s and retrying lock", path); mPath.erase(mPath.end() - 1); path = mPath.c_str(); /* Reset retry counter. */ retries = 0; break; } /* DROPTHROUGH */ #endif default: failed = true; break; } } if (failed) { int error = errno; ::syslog(LOG_ERR, "Lock failure on %s: %s", path, strerror(error)); secdebug("atomicfile", "Lock failure on %s: %s", path, strerror(error)); UnixError::throwMe(error); } } void AtomicLockedFile::unlock() throw() { const char *path = mPath.c_str(); if (::unlink(path) == -1) { secdebug("atomicfile", "unlink %s: %s", path, strerror(errno)); // unlock can't throw } } #undef kAtomicFileMaxBlockSize