blob: 2329edfcc6b9b8c70eef4b9e8bb9840969bbe501 [file] [log] [blame]
// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: lsong@google.com (Libo Song)
#include "net/instaweb/apache/apr_file_system.h"
#include <string>
#include "apr_file_info.h"
#include "apr_file_io.h"
#include "apr_pools.h"
#include "net/instaweb/util/public/basictypes.h"
#include "base/logging.h"
#include "base/scoped_ptr.h"
#include "net/instaweb/util/public/abstract_mutex.h"
#include "net/instaweb/util/public/debug.h"
#include "net/instaweb/util/public/message_handler.h"
#include "net/instaweb/util/public/stdio_file_system.h"
#include "net/instaweb/util/public/thread_system.h"
#include "net/instaweb/util/public/timer.h"
#include "net/instaweb/util/stack_buffer.h"
namespace net_instaweb {
namespace {
apr_status_t locked_apr_dir_read(AbstractMutex* lock, apr_finfo_t* finfo,
apr_int32_t wanted, apr_dir_t* dir) {
ScopedMutex hold_mutex(lock);
return apr_dir_read(finfo, wanted, dir);
}
} // namespace
void AprReportError(MessageHandler* message_handler, const char* filename,
int line, const char* message, int error_code) {
char buf[kStackBufferSize];
apr_strerror(error_code, buf, sizeof(buf));
std::string error_format(message);
error_format.append(" (code=%d %s)");
message_handler->Error(filename, line, error_format.c_str(), error_code, buf);
}
// Helper class to factor out common implementation details between Input and
// Output files, in lieu of multiple inheritance.
class FileHelper {
public:
FileHelper(apr_file_t* file, const char* filename, AbstractMutex* mutex)
: file_(file),
filename_(filename),
mutex_(mutex) {
}
void ReportError(MessageHandler* message_handler, const char* format,
int error_code) {
AprReportError(message_handler, filename_.c_str(), 0, format, error_code);
}
bool Close(MessageHandler* message_handler);
apr_file_t* file() { return file_; }
const std::string& filename() const { return filename_; }
private:
apr_file_t* const file_;
const std::string filename_;
AbstractMutex* mutex_; // owned by the FS object
DISALLOW_COPY_AND_ASSIGN(FileHelper);
};
bool FileHelper::Close(MessageHandler* message_handler) {
// We must hold the lock as apr_file_close accesses the pool_ in order to
// remove a cleanup entry that would close the file on pool destruction.
ScopedMutex hold_mutex(mutex_);
apr_status_t ret = apr_file_close(file_);
if (ret != APR_SUCCESS) {
ReportError(message_handler, "close file", ret);
return false;
} else {
return true;
}
}
class HtmlWriterInputFile : public FileSystem::InputFile {
public:
HtmlWriterInputFile(apr_file_t* file, const char* filename,
AbstractMutex* mutex);
virtual int Read(char* buf, int size, MessageHandler* message_handler);
virtual bool Close(MessageHandler* message_handler) {
return helper_.Close(message_handler);
}
virtual const char* filename() { return helper_.filename().c_str(); }
private:
FileHelper helper_;
DISALLOW_COPY_AND_ASSIGN(HtmlWriterInputFile);
};
class HtmlWriterOutputFile : public FileSystem::OutputFile {
public:
HtmlWriterOutputFile(apr_file_t* file, const char* filename,
AbstractMutex* mutex);
virtual bool Write(const StringPiece& buf,
MessageHandler* message_handler);
virtual bool Flush(MessageHandler* message_handler);
virtual bool Close(MessageHandler* message_handler) {
return helper_.Close(message_handler);
}
virtual bool SetWorldReadable(MessageHandler* message_handler);
virtual const char* filename() { return helper_.filename().c_str(); }
private:
FileHelper helper_;
DISALLOW_COPY_AND_ASSIGN(HtmlWriterOutputFile);
};
HtmlWriterInputFile::HtmlWriterInputFile(apr_file_t* file, const char* filename,
AbstractMutex* mutex)
: helper_(file, filename, mutex) {
}
int HtmlWriterInputFile::Read(char* buf,
int size,
MessageHandler* message_handler) {
apr_size_t bytes = size;
apr_status_t ret = apr_file_read(helper_.file(), buf, &bytes);
if (ret == APR_EOF) {
return 0;
}
if (ret != APR_SUCCESS) {
bytes = 0;
helper_.ReportError(message_handler, "read file", ret);
}
return bytes;
}
HtmlWriterOutputFile::HtmlWriterOutputFile(apr_file_t* file,
const char* filename,
AbstractMutex* mutex)
: helper_(file, filename, mutex) {
}
bool HtmlWriterOutputFile::Write(const StringPiece& buf,
MessageHandler* message_handler) {
bool success = false;
apr_size_t bytes = buf.size();
apr_status_t ret = apr_file_write(helper_.file(), buf.data(), &bytes);
if (ret != APR_SUCCESS) {
helper_.ReportError(message_handler, "write file", ret);
} else if (bytes != buf.size()) {
helper_.ReportError(message_handler, "write file partial", ret);
} else {
success = true;
}
return success;
}
bool HtmlWriterOutputFile::Flush(MessageHandler* message_handler) {
apr_status_t ret = apr_file_flush(helper_.file());
if (ret != APR_SUCCESS) {
helper_.ReportError(message_handler, "flush file", ret);
return false;
}
return true;
}
bool HtmlWriterOutputFile::SetWorldReadable(MessageHandler* message_handler) {
apr_status_t ret = apr_file_perms_set(helper_.filename().c_str(),
APR_FPROT_UREAD | APR_FPROT_UWRITE |
APR_FPROT_GREAD | APR_FPROT_WREAD);
if (ret != APR_SUCCESS) {
helper_.ReportError(message_handler, "set permission", ret);
return false;
}
return true;
}
AprFileSystem::AprFileSystem(apr_pool_t* pool, ThreadSystem* thread_system)
: pool_(NULL),
mutex_(thread_system->NewMutex()) {
apr_pool_create(&pool_, pool);
}
AprFileSystem::~AprFileSystem() {
apr_pool_destroy(pool_);
}
int AprFileSystem::MaxPathLength(const StringPiece& base) const {
// We delegate to StdioFileSystem for simplicity.
return StdioFileSystem().MaxPathLength(base);
}
FileSystem::InputFile* AprFileSystem::OpenInputFile(
const char* filename, MessageHandler* message_handler) {
ScopedMutex hold_mutex(mutex_.get());
apr_file_t* file;
apr_status_t ret = apr_file_open(&file, filename, APR_FOPEN_READ,
APR_OS_DEFAULT, pool_);
if (ret != APR_SUCCESS) {
AprReportError(message_handler, filename, 0, "open input file", ret);
return NULL;
}
return new HtmlWriterInputFile(file, filename, mutex_.get());
}
FileSystem::OutputFile* AprFileSystem::OpenOutputFileHelper(
const char* filename, MessageHandler* message_handler) {
ScopedMutex hold_mutex(mutex_.get());
apr_file_t* file;
apr_status_t ret = apr_file_open(&file, filename,
APR_WRITE | APR_CREATE | APR_TRUNCATE,
APR_OS_DEFAULT, pool_);
if (ret != APR_SUCCESS) {
AprReportError(message_handler, filename, 0, "open output file", ret);
return NULL;
}
return new HtmlWriterOutputFile(file, filename, mutex_.get());
}
FileSystem::OutputFile* AprFileSystem::OpenTempFileHelper(
const StringPiece& prefix_name,
MessageHandler* message_handler) {
ScopedMutex hold_mutex(mutex_.get());
static const char mkstemp_hook[] = "XXXXXX";
scoped_array<char> template_name(
new char[prefix_name.size() + sizeof(mkstemp_hook)]);
memcpy(template_name.get(), prefix_name.data(), prefix_name.size());
memcpy(template_name.get() + prefix_name.size(), mkstemp_hook,
sizeof(mkstemp_hook));
apr_file_t* file;
// A temp file will be generated with the XXXXXX part of template_name being
// replaced.
// Do not use flag APR_DELONCLOSE to delete the temp file on close, it will
// be renamed for later use.
apr_status_t ret = apr_file_mktemp(
&file, template_name.get(),
APR_CREATE | APR_READ | APR_WRITE | APR_EXCL, pool_);
if (ret != APR_SUCCESS) {
AprReportError(message_handler, template_name.get(), 0,
"open temp file", ret);
return NULL;
}
return new HtmlWriterOutputFile(file, template_name.get(), mutex_.get());
}
bool AprFileSystem::RenameFileHelper(
const char* old_filename, const char* new_filename,
MessageHandler* message_handler) {
ScopedMutex hold_mutex(mutex_.get());
apr_status_t ret = apr_file_rename(old_filename, new_filename, pool_);
if (ret != APR_SUCCESS) {
AprReportError(message_handler, new_filename, 0, "renaming temp file", ret);
return false;
}
return true;
}
bool AprFileSystem::RemoveFile(const char* filename,
MessageHandler* message_handler) {
ScopedMutex hold_mutex(mutex_.get());
apr_status_t ret = apr_file_remove(filename, pool_);
if (ret != APR_SUCCESS) {
AprReportError(message_handler, filename, 0, "removing file", ret);
return false;
}
return true;
}
bool AprFileSystem::MakeDir(const char* directory_path,
MessageHandler* handler) {
ScopedMutex hold_mutex(mutex_.get());
apr_status_t ret = apr_dir_make(directory_path, APR_FPROT_OS_DEFAULT, pool_);
if (ret != APR_SUCCESS) {
AprReportError(handler, directory_path, 0, "creating dir", ret);
return false;
}
return true;
}
BoolOrError AprFileSystem::Exists(const char* path, MessageHandler* handler) {
ScopedMutex hold_mutex(mutex_.get());
BoolOrError exists; // Error is the default state.
apr_int32_t wanted = APR_FINFO_TYPE;
apr_finfo_t finfo;
apr_status_t ret = apr_stat(&finfo, path, wanted, pool_);
if (ret != APR_SUCCESS && ret != APR_ENOENT) {
AprReportError(handler, path, 0, "failed to stat", ret);
exists.set_error();
} else {
exists.set(ret == APR_SUCCESS);
}
return exists;
}
BoolOrError AprFileSystem::IsDir(const char* path, MessageHandler* handler) {
ScopedMutex hold_mutex(mutex_.get());
BoolOrError is_dir; // Error is the default state.
apr_int32_t wanted = APR_FINFO_TYPE;
apr_finfo_t finfo;
apr_status_t ret = apr_stat(&finfo, path, wanted, pool_);
if (ret != APR_SUCCESS && ret != APR_ENOENT) {
AprReportError(handler, path, 0, "failed to stat", ret);
is_dir.set_error();
} else {
is_dir.set(ret == APR_SUCCESS && finfo.filetype == APR_DIR);
}
return is_dir;
}
bool AprFileSystem::ListContents(const StringPiece& dir,
StringVector* files,
MessageHandler* handler) {
std::string dirString = dir.as_string();
EnsureEndsInSlash(&dirString);
const char* dirname = dirString.c_str();
apr_dir_t* mydir;
mutex_->Lock();
apr_status_t ret = apr_dir_open(&mydir, dirname, pool_);
mutex_->Unlock();
if (ret != APR_SUCCESS) {
AprReportError(handler, dirname, 0, "failed to opendir", ret);
return false;
} else {
apr_finfo_t finfo;
apr_int32_t wanted = APR_FINFO_NAME;
while (locked_apr_dir_read(mutex_.get(), &finfo, wanted, mydir)
!= APR_ENOENT) {
if ((strcmp(finfo.name, ".") != 0) &&
(strcmp(finfo.name, "..") != 0)) {
files->push_back(dirString + finfo.name);
}
}
}
mutex_->Lock();
ret = apr_dir_close(mydir);
mutex_->Unlock();
if (ret != APR_SUCCESS) {
AprReportError(handler, dirname, 0, "failed to closedir", ret);
return false;
}
return true;
}
bool AprFileSystem::Stat(const StringPiece& path,
apr_finfo_t* file_info, apr_int32_t field_wanted,
MessageHandler* handler) {
ScopedMutex hold_mutex(mutex_.get());
const std::string path_string = path.as_string();
const char* path_str = path_string.c_str();
apr_status_t ret = apr_stat(file_info, path_str, field_wanted, pool_);
if (ret != APR_SUCCESS) {
AprReportError(handler, path_str, 0, "failed to stat", ret);
return false;
} else {
return true;
}
}
// TODO(abliss): there are some situations where this doesn't work
// -- e.g. if the filesystem is mounted noatime.
bool AprFileSystem::Atime(const StringPiece& path,
int64* timestamp_sec, MessageHandler* handler) {
apr_finfo_t file_info;
bool ret = Stat(path, &file_info, APR_FINFO_ATIME, handler);
if (ret) {
// file_info.atime is in microseconds (apr_time_t) despite the
// fact that the underlying Posix stat() call returns the data
// in seconds.
//
// http://apr.apache.org/docs/apr/1.4/
// group__apr__time.html#gadb4bde16055748190eae190c55aa02bb
*timestamp_sec = file_info.atime / Timer::kSecondUs;
}
return ret;
}
bool AprFileSystem::Mtime(const StringPiece& path,
int64* timestamp_sec, MessageHandler* handler) {
apr_finfo_t file_info;
bool ret = Stat(path, &file_info, APR_FINFO_MTIME, handler);
if (ret) {
// file_info.mtime is in microseconds (apr_time_t) despite the
// fact that the underlying Posix stat() call returns the data
// in seconds.
//
// http://apr.apache.org/docs/apr/1.4/
// group__apr__time.html#gadb4bde16055748190eae190c55aa02bb
*timestamp_sec = file_info.mtime / Timer::kSecondUs;
}
return ret;
}
bool AprFileSystem::Size(const StringPiece& path, int64* size,
MessageHandler* handler) {
apr_finfo_t file_info;
bool ret = Stat(path, &file_info, APR_FINFO_SIZE, handler);
if (ret) {
*size = file_info.size;
}
return ret;
}
BoolOrError AprFileSystem::TryLock(const StringPiece& lock_name,
MessageHandler* handler) {
ScopedMutex hold_mutex(mutex_.get());
const std::string lock_string = lock_name.as_string();
const char* lock_str = lock_string.c_str();
// TODO(abliss): mkdir is not atomic on all platforms. We should
// perhaps use an apr_global_mutex_t here.
apr_status_t ret = apr_dir_make(lock_str, APR_FPROT_OS_DEFAULT, pool_);
if (ret == APR_SUCCESS) {
return BoolOrError(true);
} else if (errno == EEXIST) {
return BoolOrError(false);
} else {
AprReportError(handler, lock_str, 0, "creating dir", ret);
return BoolOrError();
}
}
BoolOrError AprFileSystem::TryLockWithTimeout(const StringPiece& lock_name,
int64 timeout_ms,
MessageHandler* handler) {
const std::string lock_string = lock_name.as_string();
const char* lock_str = lock_string.c_str();
BoolOrError result = TryLock(lock_name, handler);
if (result.is_true() || result.is_error()) {
// We got the lock, or the lock is ungettable.
return result;
}
int64 m_time_sec;
if (!Mtime(lock_name, &m_time_sec, handler)) {
// We can't stat the lockfile.
return BoolOrError();
}
int64 elapsed_since_lock_us = apr_time_now() - Timer::kSecondUs * m_time_sec;
int64 timeout_us = Timer::kMsUs * timeout_ms;
if (elapsed_since_lock_us < timeout_us) {
// The lock is held and timeout hasn't elapsed.
return BoolOrError(false);
}
// Lock has timed out. We have two options here:
// 1) Leave the lock in its present state and assume we've taken ownership.
// This is kind to the file system, but causes lots of repeated work at
// timeout, as subsequent threads also see a timed-out lock.
// 2) Force-unlock the lock and re-lock it. This resets the timeout period,
// but is hard on the file system metadata log.
if (!Unlock(lock_name, handler)) {
// We couldn't break the lock. Maybe someone else beat us to it.
// We optimistically forge ahead anyhow (1), since we know we've timed out.
handler->Info(lock_str, 0,
"Breaking lock without reset! now-ctime=%d-%d > %d (sec)\n%s",
static_cast<int>(apr_time_now() / Timer::kSecondUs),
static_cast<int>(m_time_sec),
static_cast<int>(timeout_ms / Timer::kSecondMs),
StackTraceString().c_str());
return BoolOrError(true);
}
handler->Info(lock_str, 0, "Broke lock! now-ctime=%d-%d > %d (sec)\n%s",
static_cast<int>(apr_time_now() / Timer::kSecondUs),
static_cast<int>(m_time_sec),
static_cast<int>(timeout_ms / Timer::kSecondMs),
StackTraceString().c_str());
result = TryLock(lock_name, handler);
if (!result.is_true()) {
// Someone else grabbed the lock after we broke it.
handler->Info(lock_str, 0, "Failed to take lock after breaking it!");
}
return result;
}
bool AprFileSystem::Unlock(const StringPiece& lock_name,
MessageHandler* handler) {
ScopedMutex hold_mutex(mutex_.get());
const std::string lock_string = lock_name.as_string();
const char* lock_str = lock_string.c_str();
apr_status_t ret = apr_dir_remove(lock_str, pool_);
if (ret != APR_SUCCESS && !APR_STATUS_IS_ENOENT(ret)) {
// We discard ENOENT, which indicates the lock was stolen or cleaned.
AprReportError(handler, lock_str, 0, "removing dir", ret);
return false;
}
return true;
}
} // namespace net_instaweb