#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2021 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License // If the library is compiled as a DLL, this ensures symbols are exported #define NUCLEX_STORAGE_SOURCE 1 #include "LinuxFile.h" #if defined(NUCLEX_STORAGE_LINUX) #include "Nuclex/Storage/FileSystem/Path.h" #include "LinuxFileApi.h" // Wrapper for Linux API with error handling #include "../../Helpers/PosixApi.h" // Linux uses Posix error handling #include "Nuclex/Storage/Errors/FileAccessError.h" #include // for ::read(), ::write(), pread(), pwrite() namespace { // ------------------------------------------------------------------------------------------- // /// Scoped unlocker for a shared lock on a mutex class SharedMutexLockScope { /// Initializes the mutex unlocking scope on the specified mutex public: SharedMutexLockScope(std::shared_mutex &mutex) : mutex(mutex) {} /// Releases the shared lock on the scope's mutex public: ~SharedMutexLockScope() { this->mutex.unlock_shared(); } /// Mutex that will be unlocked when the scope is destroyed private: std::shared_mutex &mutex; }; // ------------------------------------------------------------------------------------------- // /// Scoped unlocker for an exclusive lock on a mutex class ExclusiveMutexLockScope { /// Initializes the mutex unlocking scope on the specified mutex public: ExclusiveMutexLockScope(std::shared_mutex &mutex) : mutex(mutex) {} /// Releases the exclusive lock on the scope's mutex public: ~ExclusiveMutexLockScope() { this->mutex.unlock(); } /// Mutex that will be unlocked when the scope is destroyed private: std::shared_mutex &mutex; }; // ------------------------------------------------------------------------------------------- // /// Value that indicates an invalid position of the file cursor const std::uint64_t InvalidPosition = static_cast(-1); // ------------------------------------------------------------------------------------------- // } // anonymous namespace namespace Nuclex { namespace Storage { namespace FileSystem { namespace Linux { // ------------------------------------------------------------------------------------------- // LinuxFile::LinuxFile( std::shared_ptr backend, const std::string &absolutePath, bool create /* = false */ ) : backend(backend), absolutePath(absolutePath), name(Path::GetFilename(absolutePath)), readerWriterLock(), fileDescriptor(-1), isOpenedWritable(false), fileCursor(InvalidPosition) { // TODO: Implement eager creation of the file is 'create' is true } // ------------------------------------------------------------------------------------------- // LinuxFile::~LinuxFile() { // When the destructor runs, all references to the file have been dropped, // thus there can be no other access. The mutex is not needed. int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); if(fileDescriptor != -1) { ::close(fileDescriptor); } } // ------------------------------------------------------------------------------------------- // std::uint64_t LinuxFile::GetSize() const { int result; // Enter a shared lock on the file descriptor so we can access it // without having to fear a race condition this->readerWriterLock.lock_shared(); { SharedMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); struct ::stat fileStatus; // Try to do a 'stat()' on the file (either by handle or by name) to get information int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); if(fileDescriptor == -1) { result = ::stat(this->absolutePath.c_str(), &fileStatus); } else { result = ::fstat(fileDescriptor, &fileStatus); } // Now check if the call succeeded. If not, save the error number for below. if(likely(result == 0)) { return static_cast(fileStatus.st_size); } else { result = errno; } } // If this place is reached, the stat() call failed and the error number has been // saved in 'result'. Throw a descriptive exception to indicate the problem. std::string errorMessage(u8"Could not obtain file size of '"); errorMessage.append(this->absolutePath); errorMessage.append(u8"'"); Helpers::PosixApi::ThrowExceptionForSystemError(errorMessage, result); } // ------------------------------------------------------------------------------------------- // const std::string &LinuxFile::GetName() const { return this->name; } // ------------------------------------------------------------------------------------------- // void LinuxFile::Rename(const std::string &name) { // Register new name with FileManager // Acquire exclusive lock // Close and rename // Release exclusive lock // Unregister old name with FileManager throw -1; } // ------------------------------------------------------------------------------------------- // std::time_t LinuxFile::GetLastModificationTime() const { int result; // Enter a shared lock on the file descriptor so we can access it // without having to fear a race condition this->readerWriterLock.lock_shared(); { SharedMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); struct ::stat fileStatus; // Try to do a 'stat()' on the file (either by handle or by name) to get information int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); if(fileDescriptor == -1) { result = ::stat(this->absolutePath.c_str(), &fileStatus); } else { result = ::fstat(fileDescriptor, &fileStatus); } // Now check if the call succeeded. If not, save the error number for below. if(likely(result == 0)) { return static_cast(fileStatus.st_mtime); } else { result = errno; } } // If this place is reached, the stat() call failed and the error number has been // saved in 'result'. Throw a descriptive exception to indicate the problem. std::string errorMessage(u8"Could not obtain last modification time of '"); errorMessage.append(this->absolutePath); errorMessage.append(u8"'"); Helpers::PosixApi::ThrowExceptionForSystemError(errorMessage, result); } // ------------------------------------------------------------------------------------------- // void LinuxFile::ReadAt( std::uint64_t location, void *buffer, std::size_t count ) const { this->readerWriterLock.lock_shared(); // Fetch the file descriptor and check if it's already open. int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); while(unlikely(fileDescriptor == -1)) { // File wasn't opened. Upgrade the lock to an exclusive one so we can // attempt to open the file for reading this->readerWriterLock.unlock_shared(); openFileForReading(); this->readerWriterLock.lock_shared(); // Reload the file descriptor. There a small but non-zero chance that // something happened to it between releasing the exclusive lock and // re-acquiring a shared lock, so we re-check and loop. fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); } // At this point, we're guaranteed to have a file descriptor while also // holding a shared lock on it (thus it cannot be swiped from us). int result; { SharedMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); ssize_t readByteCount; // If the read matches the location of the file cursor, we can use // sequential reads (which allow reading stdin, for example). Replace it // with an invalid cursor until the read completes so parallel calls will // use the positional read API. std::uint64_t currentFileCursor = this->fileCursor.exchange(InvalidPosition); if(currentFileCursor == location) { readByteCount = ::read(fileDescriptor, buffer, count); if(likely(result != -1)) { this->fileCursor.store( currentFileCursor + static_cast(readByteCount), std::memory_order_relaxed ); } } else { readByteCount = ::pread( fileDescriptor, buffer, count, static_cast(location) ); } // Success? Then we're done here! if(likely(static_cast(readByteCount) == count)) { return; } // If the read failed, store the error number (or if it succeeded but // provided too few bytes, treat it as an EINTR error - read interrupted) if(readByteCount == -1) { result = errno; } else { result = EINTR; } } // If this spot is reached, an error has prevented the read from completing if(result == EINTR) { std::string message(u8"Read from '"); message.append(this->absolutePath); message.append("' got less than the requested number of bytes"); throw Errors::FileAccessError( std::error_code(EINTR, std::system_category()), message ); } else { std::string message(u8"Read from '"); message.append(this->absolutePath); message.append("' failed"); throw Errors::FileAccessError( std::error_code(result, std::system_category()), message ); } } // ------------------------------------------------------------------------------------------- // void LinuxFile::WriteAt( std::uint64_t location, const void *buffer, std::size_t count ) { this->readerWriterLock.lock_shared(); // Fetch the file descriptor and check if it's already open and writable int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); bool isOpenedWritable = this->isOpenedWritable.load(std::memory_order_relaxed); while(unlikely(fileDescriptor == -1) || (!isOpenedWritable)) { // File wasn't opened or is not writable. Upgrade the lock to an exclusive // one so we can attempt to open the file for writing this->readerWriterLock.unlock_shared(); if(fileDescriptor != -1) { openFileForWriting(); } this->readerWriterLock.lock_shared(); // Reload the file descriptor. There a small but non-zero chance that // something happened to it between releasing the exclusive lock and // re-acquiring a shared lock, so we re-check and loop. fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); isOpenedWritable = this->fileDescriptor.load(std::memory_order_relaxed); } // At this point, we're guaranteed to have a file descriptor while also // holding a shared lock on it (thus it cannot be swiped from us). int result; { SharedMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); ssize_t writtenByteCount; // If the write matches the location of the file cursor, we can use // sequential reads (which allow writing to stdout, for example). Replace // it with an invalid cursor until the write completes so parallel calls // will use the positional write API. std::uint64_t currentFileCursor = this->fileCursor.exchange(InvalidPosition); if(currentFileCursor == location) { writtenByteCount = ::write(fileDescriptor, buffer, count); if(likely(result != -1)) { this->fileCursor.store( currentFileCursor + static_cast(writtenByteCount), std::memory_order_relaxed ); } } else { writtenByteCount = ::pwrite( fileDescriptor, buffer, count, static_cast(location) ); } // Success? Then we're done here! if(likely(static_cast(writtenByteCount) == count)) { return; } // If the write failed, store the error number (or if it succeeded but // provided too few bytes, treat it as an EINTR error - write interrupted) if(writtenByteCount == -1) { result = errno; } else { result = EINTR; } } // If this spot is reached, an error has prevented the write from completing if(result == EINTR) { std::string message(u8"Write to '"); message.append(this->absolutePath); message.append("' stored less than the requested number of bytes"); throw Errors::FileAccessError( std::error_code(EINTR, std::system_category()), message ); } else { std::string message(u8"Write to '"); message.append(this->absolutePath); message.append("' failed"); throw Errors::FileAccessError( std::error_code(result, std::system_category()), message ); } } // ------------------------------------------------------------------------------------------- // void LinuxFile::Flush() { int result; this->readerWriterLock.lock_shared(); { SharedMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); if(!this->isOpenedWritable.load(std::memory_order_relaxed)) { return; } int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); if(fileDescriptor == -1) { return; } int result = ::fsync(fileDescriptor); if(result == 0) { return; } result = errno; } std::string message(u8"Flushing '"); message.append(this->absolutePath); message.append("' failed"); throw Errors::FileAccessError( std::error_code(result, std::system_category()), message ); } // ------------------------------------------------------------------------------------------- // void LinuxFile::openFileForReading() const { this->readerWriterLock.lock(); { ExclusiveMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); // Of course, another thread may have done the same, so re-check // the file descriptor int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); if(likely(fileDescriptor == -1)) { this->fileDescriptor.store( LinuxFileApi::OpenFileForReading(this->absolutePath), std::memory_order_relaxed ); } } } // ------------------------------------------------------------------------------------------- // void LinuxFile::openFileForWriting() const { this->readerWriterLock.lock(); { ExclusiveMutexLockScope fileDescriptorAccessScope(this->readerWriterLock); // Of course, another thread may have done the same, so re-check // the file descriptor int fileDescriptor = this->fileDescriptor.load(std::memory_order_relaxed); if(likely(fileDescriptor == -1)) { this->fileDescriptor.store( LinuxFileApi::OpenFileForWriting(this->absolutePath), std::memory_order_relaxed ); } else { bool isOpenedWritable = this->isOpenedWritable.load(std::memory_order_relaxed); } } } // ------------------------------------------------------------------------------------------- // }}}} // namespace Nuclex::Storage::FileSystem::Linux #endif // defined(NUCLEX_STORAGE_LINUX)