diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index 26b76e150..e2e87d0f4 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -14,9 +14,12 @@ #define ETAG_FMT "\"%zx-%zx\"" -FileCache::FileCache(size_t capacity) : hv::LRUCache(capacity) { - stat_interval = 10; // s - expired_time = 60; // s +FileCache::FileCache(size_t capacity) + : hv::LRUCache(capacity) { + stat_interval = 10; // s + expired_time = 60; // s + max_header_length = FILE_CACHE_DEFAULT_HEADER_LENGTH; + max_file_size = FILE_CACHE_DEFAULT_MAX_FILE_SIZE; } file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { @@ -26,6 +29,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { #endif bool modified = false; if (fc) { + std::lock_guard lock(fc->mutex); time_t now = time(NULL); if (now - fc->stat_time > stat_interval) { fc->stat_time = now; @@ -52,85 +56,112 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { flags |= O_BINARY; #endif int fd = -1; + bool is_dir = false; #ifdef OS_WIN - if(wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); - if(_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { + if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); + if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { param->error = ERR_OPEN_FILE; return NULL; } - if(S_ISREG(st.st_mode)) { + if (S_ISREG(st.st_mode)) { fd = _wopen(wfilepath.c_str(), flags); - }else if (S_ISDIR(st.st_mode)) { - // NOTE: open(dir) return -1 on windows - fd = 0; + } else if (S_ISDIR(st.st_mode)) { + is_dir = true; } #else - if(stat(filepath, &st) != 0) { + if (::stat(filepath, &st) != 0) { param->error = ERR_OPEN_FILE; return NULL; } fd = open(filepath, flags); #endif - if (fd < 0) { + if (fd < 0 && !is_dir) { param->error = ERR_OPEN_FILE; return NULL; } - defer(if (fd > 0) { close(fd); }) + defer(if (fd >= 0) { close(fd); }) if (fc == NULL) { if (S_ISREG(st.st_mode) || (S_ISDIR(st.st_mode) && - filepath[strlen(filepath)-1] == '/')) { + filepath[strlen(filepath) - 1] == '/')) { fc = std::make_shared(); fc->filepath = filepath; fc->st = st; + fc->header_reserve = max_header_length; time(&fc->open_time); fc->stat_time = fc->open_time; fc->stat_cnt = 1; - put(filepath, fc); - } - else { + // NOTE: do NOT put() into cache yet — defer until fully initialized + } else { param->error = ERR_MISMATCH; return NULL; } } - if (S_ISREG(fc->st.st_mode)) { - param->filesize = fc->st.st_size; - // FILE - if (param->need_read) { - if (fc->st.st_size > param->max_read) { - param->error = ERR_OVER_LIMIT; - return NULL; + // Hold fc->mutex for initialization, but release before put() + // to avoid lock-order inversion with RemoveExpiredFileCache(). + // Lock order: LRUCache mutex → fc->mutex (never reverse). + { + std::lock_guard lock(fc->mutex); + if (S_ISREG(st.st_mode)) { + param->filesize = st.st_size; + // FILE + if (param->need_read) { + if (st.st_size > param->max_read) { + param->error = ERR_OVER_LIMIT; + // Leave existing cache entry's state untouched + return NULL; + } } - fc->resize_buf(fc->st.st_size); - int nread = read(fd, fc->filebuf.base, fc->filebuf.len); - if (nread != fc->filebuf.len) { - hloge("Failed to read file: %s", filepath); - param->error = ERR_READ_FILE; - return NULL; + // Validation passed — commit new stat into cached entry + fc->st = st; + if (param->need_read) { + fc->resize_buf(fc->st.st_size, max_header_length); + // Loop to handle partial reads (EINTR, etc.) + char* dst = fc->filebuf.base; + size_t remaining = fc->filebuf.len; + while (remaining > 0) { + int nread = read(fd, dst, remaining); + if (nread < 0) { + if (errno == EINTR) continue; + hloge("Failed to read file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + if (nread == 0) { + hloge("Unexpected EOF reading file: %s", filepath); + param->error = ERR_READ_FILE; + return NULL; + } + dst += nread; + remaining -= nread; + } } - } - const char* suffix = strrchr(filepath, '.'); - if (suffix) { - http_content_type content_type = http_content_type_enum_by_suffix(suffix+1); - if (content_type == TEXT_HTML) { - fc->content_type = "text/html; charset=utf-8"; - } else if (content_type == TEXT_PLAIN) { - fc->content_type = "text/plain; charset=utf-8"; - } else { - fc->content_type = http_content_type_str_by_suffix(suffix+1); + const char* suffix = strrchr(filepath, '.'); + if (suffix) { + http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); + if (content_type == TEXT_HTML) { + fc->content_type = "text/html; charset=utf-8"; + } else if (content_type == TEXT_PLAIN) { + fc->content_type = "text/plain; charset=utf-8"; + } else { + fc->content_type = http_content_type_str_by_suffix(suffix + 1); + } } + } else if (S_ISDIR(st.st_mode)) { + // DIR + fc->st = st; + std::string page; + make_index_of_page(filepath, page, param->path); + fc->resize_buf(page.size(), max_header_length); + memcpy(fc->filebuf.base, page.c_str(), page.size()); + fc->content_type = "text/html; charset=utf-8"; } - } - else if (S_ISDIR(fc->st.st_mode)) { - // DIR - std::string page; - make_index_of_page(filepath, page, param->path); - fc->resize_buf(page.size()); - memcpy(fc->filebuf.base, page.c_str(), page.size()); - fc->content_type = "text/html; charset=utf-8"; - } - gmtime_fmt(fc->st.st_mtime, fc->last_modified); - snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + gmtime_fmt(fc->st.st_mtime, fc->last_modified); + snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, + (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + } // release fc->mutex before put() to maintain lock ordering + // Cache the fully initialized entry (acquires LRUCache mutex only) + put(filepath, fc); } return fc; } @@ -154,6 +185,12 @@ file_cache_ptr FileCache::Get(const char* filepath) { void FileCache::RemoveExpiredFileCache() { time_t now = time(NULL); remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) { + // Use try_to_lock to avoid lock-order inversion with Open(). + // If the entry is busy, skip it — it will be checked next cycle. + std::unique_lock lock(fc->mutex, std::try_to_lock); + if (!lock.owns_lock()) { + return false; + } return (now - fc->stat_time > expired_time); }); } diff --git a/http/server/FileCache.h b/http/server/FileCache.h index 363c41d88..d06cf422e 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -1,90 +1,158 @@ #ifndef HV_FILE_CACHE_H_ #define HV_FILE_CACHE_H_ +/* + * FileCache — Enhanced File Cache for libhv HTTP server + * + * Features: + * 1. Configurable max_header_length (default 4096, tunable per-instance) + * 2. prepend_header() returns bool to report success/failure + * 3. Exposes header/buffer metrics via accessors + * 4. Fixes stat() name collision in is_modified() + * 5. max_cache_num / max_file_size configurable at runtime + * 6. Reserved header space can be tuned per-instance + * 7. Source-level API compatible; struct layout differs from original (no ABI/layout compatibility) + */ + #include -#include #include #include +#include "hexport.h" #include "hbuf.h" #include "hstring.h" #include "LRUCache.h" -#define HTTP_HEADER_MAX_LENGTH 1024 // 1K -#define FILE_CACHE_MAX_NUM 100 -#define FILE_CACHE_MAX_SIZE (1 << 22) // 4M +// Default values — may be overridden at runtime via FileCache setters +#define FILE_CACHE_DEFAULT_HEADER_LENGTH 4096 // 4K +#define FILE_CACHE_DEFAULT_MAX_NUM 100 +#define FILE_CACHE_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M typedef struct file_cache_s { + mutable std::mutex mutex; // protects all mutable state below std::string filepath; struct stat st; time_t open_time; time_t stat_time; uint32_t stat_cnt; - HBuf buf; // http_header + file_content - hbuf_t filebuf; - hbuf_t httpbuf; + HBuf buf; // header_reserve + file_content + hbuf_t filebuf; // points into buf: file content region + hbuf_t httpbuf; // points into buf: header + file content after prepend char last_modified[64]; char etag[64]; std::string content_type; + // --- new: expose header metrics --- + int header_reserve; // reserved bytes before file content + int header_used; // actual bytes used by prepend_header + file_cache_s() { stat_cnt = 0; + header_reserve = FILE_CACHE_DEFAULT_HEADER_LENGTH; + header_used = 0; + memset(last_modified, 0, sizeof(last_modified)); + memset(etag, 0, sizeof(etag)); } + // NOTE: caller must hold mutex. + // On Windows, Open() uses _wstat() directly instead of calling this. bool is_modified() { time_t mtime = st.st_mtime; - stat(filepath.c_str(), &st); + ::stat(filepath.c_str(), &st); return mtime != st.st_mtime; } + // NOTE: caller must hold mutex bool is_complete() { - if(S_ISDIR(st.st_mode)) return filebuf.len > 0; - return filebuf.len == st.st_size; + if (S_ISDIR(st.st_mode)) return filebuf.len > 0; + return filebuf.len == (size_t)st.st_size; } - void resize_buf(int filesize) { - buf.resize(HTTP_HEADER_MAX_LENGTH + filesize); - filebuf.base = buf.base + HTTP_HEADER_MAX_LENGTH; + // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers + void resize_buf(size_t filesize, int reserved) { + if (reserved < 0) reserved = 0; + header_reserve = reserved; + buf.resize((size_t)reserved + filesize); + filebuf.base = buf.base + reserved; filebuf.len = filesize; + // Invalidate httpbuf since buffer may have been reallocated + httpbuf.base = NULL; + httpbuf.len = 0; + header_used = 0; } - void prepend_header(const char* header, int len) { - if (len > HTTP_HEADER_MAX_LENGTH) return; + void resize_buf(size_t filesize) { + resize_buf(filesize, header_reserve); + } + + // Thread-safe: prepend header into reserved space. + // Returns true on success, false if header exceeds reserved space. + // On failure, httpbuf falls back to filebuf (body only, no header). + bool prepend_header(const char* header, int len) { + std::lock_guard lock(mutex); + if (len <= 0 || len > header_reserve) { + // Safe fallback: point httpbuf at filebuf so callers always get valid data + httpbuf = filebuf; + header_used = 0; + return false; + } httpbuf.base = filebuf.base - len; - httpbuf.len = len + filebuf.len; + httpbuf.len = (size_t)len + filebuf.len; memcpy(httpbuf.base, header, len); + header_used = len; + return true; } + + // --- thread-safe accessors --- + int get_header_reserve() const { std::lock_guard lock(mutex); return header_reserve; } + int get_header_used() const { std::lock_guard lock(mutex); return header_used; } + int get_header_remaining() const { std::lock_guard lock(mutex); return header_reserve - header_used; } + bool header_fits(int len) const { std::lock_guard lock(mutex); return len > 0 && len <= header_reserve; } } file_cache_t; -typedef std::shared_ptr file_cache_ptr; +typedef std::shared_ptr file_cache_ptr; -class FileCache : public hv::LRUCache { +class HV_EXPORT FileCache : public hv::LRUCache { public: - int stat_interval; - int expired_time; + // --- configurable parameters (were hardcoded macros before) --- + int stat_interval; // seconds between stat() checks + int expired_time; // seconds before cache entry expires + int max_header_length; // reserved header bytes per entry + int max_file_size; // max cached file size (larger = large-file path) - FileCache(size_t capacity = FILE_CACHE_MAX_NUM); + explicit FileCache(size_t capacity = FILE_CACHE_DEFAULT_MAX_NUM); struct OpenParam { - bool need_read; - int max_read; - const char* path; - size_t filesize; - int error; + bool need_read; + int max_read; // per-request override for max file size + const char* path; // URL path (for directory listing) + size_t filesize; // [out] actual file size + int error; // [out] error code if Open returns NULL OpenParam() { need_read = true; - max_read = FILE_CACHE_MAX_SIZE; + max_read = FILE_CACHE_DEFAULT_MAX_FILE_SIZE; path = "/"; filesize = 0; error = 0; } }; + file_cache_ptr Open(const char* filepath, OpenParam* param); bool Exists(const char* filepath) const; bool Close(const char* filepath); void RemoveExpiredFileCache(); + // --- new: getters --- + int GetMaxHeaderLength() const { return max_header_length; } + int GetMaxFileSize() const { return max_file_size; } + int GetStatInterval() const { return stat_interval; } + int GetExpiredTime() const { return expired_time; } + + // --- new: setters --- + void SetMaxHeaderLength(int len) { max_header_length = len < 0 ? 0 : len; } + void SetMaxFileSize(int size) { max_file_size = size < 1 ? 1 : size; } + protected: file_cache_ptr Get(const char* filepath); }; diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index 005967328..b5800dd7b 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -801,11 +801,13 @@ int HttpHandler::GetSendData(char** data, size_t* len) { // FileCache // NOTE: no copy filebuf, more efficient header = pResp->Dump(true, false); - fc->prepend_header(header.c_str(), header.size()); - *data = fc->httpbuf.base; - *len = fc->httpbuf.len; - state = SEND_DONE; - return *len; + if (fc->prepend_header(header.c_str(), header.size())) { + *data = fc->httpbuf.base; + *len = fc->httpbuf.len; + state = SEND_DONE; + return *len; + } + // Header too large for reserved space — fall through to normal path } // API service content_length = pResp->ContentLength(); @@ -842,8 +844,8 @@ int HttpHandler::GetSendData(char** data, size_t* len) { } case SEND_DONE: { - // NOTE: remove file cache if > FILE_CACHE_MAX_SIZE - if (fc && fc->filebuf.len > FILE_CACHE_MAX_SIZE) { + // NOTE: remove file cache if > max_file_size + if (fc && fc->filebuf.len > files->GetMaxFileSize()) { files->Close(fc->filepath.c_str()); } fc = NULL; diff --git a/http/server/HttpServer.cpp b/http/server/HttpServer.cpp index aa296fe8b..6471de503 100644 --- a/http/server/HttpServer.cpp +++ b/http/server/HttpServer.cpp @@ -136,6 +136,7 @@ static void loop_thread(void* userdata) { FileCache* filecache = &privdata->filecache; filecache->stat_interval = service->file_cache_stat_interval; filecache->expired_time = service->file_cache_expired_time; + filecache->SetMaxFileSize(service->max_file_cache_size); if (filecache->expired_time > 0) { // NOTE: add timer to remove expired file cache htimer_t* timer = htimer_add(hloop, [](htimer_t* timer) {