Diff options
Diffstat (limited to '')
| -rw-r--r-- | source/cache.c | 246 |
1 file changed, 116 insertions, 130 deletions
diff --git a/source/cache.c b/source/cache.c index 9594fd6..0a3166d 100644 --- a/source/cache.c +++ b/source/cache.c @@ -12,7 +12,6 @@ #include "cache.h" #include "cgit.h" #include "html.h" -#include "shared.h" #include <sys/file.h> #ifdef HAVE_LINUX_SENDFILE #include <sys/sendfile.h> @@ -31,21 +30,6 @@ #define FNV_OFFSET 0x811c9dc5 #define FNV_PRIME 0x01000193 -/* - * Cache trouble goes to stderr, which under CGI is the web server's error - * log, so that it cannot land in the middle of the page being written to - * stdout. - */ -__attribute__((format (printf,1,2))) -static void log_error(const char *format, ...) -{ - va_list args; - - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); -} - struct cache_slot { const char *key; size_t keylen; @@ -112,17 +96,12 @@ static int key_fits_slot(const char *key) return strlen(key) + 1 <= CACHE_BUFSIZE; } -static int close_slot(struct cache_slot *slot) +static void close_fd(int *fd) { - int err = 0; - - if (slot->cache_fd > 0) { - if (close(slot->cache_fd)) - err = errno; - else - slot->cache_fd = -1; + if (*fd > 0) { + close(*fd); + *fd = -1; } - return err; } static int print_slot(struct cache_slot *slot) @@ -182,7 +161,7 @@ static int serve_slot(struct cache_slot *slot) err = print_slot(slot); // A client that went away mid-page is not trouble worth a line. if (err && err != EPIPE) - log_error("[cgit] Unable to send slot %s: %s (%d)\n", slot->path, strerror(err), err); + fprintf(stderr, "[cgit] Unable to send slot %s: %s (%d)\n", slot->path, strerror(err), err); return err; } @@ -212,63 +191,95 @@ static int is_modified(struct cache_slot *slot) ); } -static int close_lock(struct cache_slot *slot) -{ - int err = 0; - - if (slot->lock_fd > 0) { - if (close(slot->lock_fd)) - err = errno; - else - slot->lock_fd = -1; - } - return err; -} - /* * The lock file becomes the slot once it is renamed, so it has to open with * the key the same way a slot does. The lock is taken without blocking, * because failing to get it is how a second process learns that this slot is - * already being rebuilt, so it returns an errno instead of waiting. + * already being rebuilt, so it returns an errno instead of waiting. Every + * other failure is logged here, for both callers. */ -static int lock_slot(struct cache_slot *slot) +int cache_lock_file(const char *path, int *fd) { struct stat held, named; + int err; - slot->lock_fd = open(slot->lock_path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); - if (slot->lock_fd == -1) - return errno; - // An flock rather than a POSIX record lock. The descriptor is put on - // stdout for the fill, and a record lock is dropped as soon as any + // Close-on-exec, so that a program a cache fill runs gets the file + // only as its stdout, and holds the lock no longer than it can write + // there. + *fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR); + if (*fd == -1) { + err = errno; + fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", path, strerror(err), err); + return err; + } + // An flock rather than a POSIX record lock. A slot's descriptor is put + // on stdout for the fill, and a record lock is dropped as soon as any // descriptor on the file closes, which an exec filter does to stdout, - // while an flock holds until the last descriptor closes. - if (flock(slot->lock_fd, LOCK_EX | LOCK_NB) < 0) { - int saved_errno = errno; - close(slot->lock_fd); - slot->lock_fd = -1; - return saved_errno; + // while an flock holds until the last descriptor closes. Either way a + // holder that dies takes its lock with it. + if (flock(*fd, LOCK_EX | LOCK_NB) < 0) { + // A lock held elsewhere only means concurrent requests, which + // is not worth a line in the server log. Any other failure, a + // filesystem without lock support say, keeps the cache from + // ever being written and does deserve one. + err = errno; + if (err != EWOULDBLOCK) + fprintf(stderr, "[cgit] Unable to lock %s: %s (%d)\n", path, strerror(err), err); + goto fail; } // The lock landed on whatever inode the path named at open. A holder - // finishing in between renames that inode into place as the live - // slot, and once the path is confirmed to still name this file that - // rename can no longer happen, because it takes the lock held here. + // finishing in between renames that inode into place, and once the + // path is confirmed to still name this file that rename can no longer + // happen, because it takes the lock held here. if ( - fstat(slot->lock_fd, &held) || - stat(slot->lock_path, &named) || - held.st_ino != named.st_ino || + fstat(*fd, &held) || + stat(path, &named) || + held.st_ino != named.st_ino || held.st_dev != named.st_dev ) { - close(slot->lock_fd); - slot->lock_fd = -1; - return EAGAIN; + err = EAGAIN; + goto fail; } // A run that died before its rename leaves the lock file behind, so // start from empty now that nobody else can be writing it. - if (ftruncate(slot->lock_fd, 0) < 0) - return errno; - if (write_in_full(slot->lock_fd, slot->key, slot->keylen + 1) < 0) - return errno; + if (ftruncate(*fd, 0) < 0) { + err = errno; + fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", path, strerror(err), err); + unlink(path); + goto fail; + } return 0; + +fail: + close(*fd); + *fd = -1; + return err; +} + +static int lock_slot(struct cache_slot *slot) +{ + int err = cache_lock_file(slot->lock_path, &slot->lock_fd); + + if (err) + return err; + if (write_in_full(slot->lock_fd, slot->key, slot->keylen + 1) < 0) { + err = errno; + fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", slot->lock_path, strerror(err), err); + unlink(slot->lock_path); + close(slot->lock_fd); + slot->lock_fd = -1; + return err; + } + return 0; +} + +static void restore_stdout(struct cache_slot *slot) +{ + if (slot->saved_stdout >= 0) { + dup2(slot->saved_stdout, STDOUT_FILENO); + close(slot->saved_stdout); + slot->saved_stdout = -1; + } } static int unlock_slot(struct cache_slot *slot, int replace_old_slot) @@ -280,11 +291,7 @@ static int unlock_slot(struct cache_slot *slot, int replace_old_slot) else err = unlink(slot->lock_path); - if (slot->saved_stdout >= 0) { - dup2(slot->saved_stdout, STDOUT_FILENO); - close(slot->saved_stdout); - slot->saved_stdout = -1; - } + restore_stdout(slot); if (err) return errno; @@ -328,11 +335,7 @@ int cache_abandon_fill(void) // caller that could not write it must not try again here. fflush(stdout); - if (slot->saved_stdout >= 0) { - dup2(slot->saved_stdout, STDOUT_FILENO); - close(slot->saved_stdout); - slot->saved_stdout = -1; - } + restore_stdout(slot); replay_lock_file(slot); unlink(slot->lock_path); return 1; @@ -347,7 +350,7 @@ static void publish_slot(struct cache_slot *slot) int err = unlock_slot(slot, 1); if (err) - log_error("[cgit] Unable to publish cache slot %s: %s (%d)\n", slot->path, + fprintf(stderr, "[cgit] Unable to publish cache slot %s: %s (%d)\n", slot->path, strerror(err), err); } @@ -405,13 +408,13 @@ static void refresh_slot(struct cache_slot *slot) // lock_slot, the copy already open is served rather than the newer. if (is_modified(slot) || fill_slot(slot)) { unlock_slot(slot, 0); - close_lock(slot); + close_fd(&slot->lock_fd); } else if (slot->abandoned) { // The abandoned fill answered the visitor itself and removed // the lock file, so only the descriptor is left to clean up. - close_lock(slot); + close_fd(&slot->lock_fd); } else { - close_slot(slot); + close_fd(&slot->cache_fd); if (slot->oversized) { // The expired copy goes too, or every later request // would refill it and drop the result again. @@ -436,11 +439,11 @@ static int process_slot(struct cache_slot *slot) // visitor, and serving the stale copy still open would append // a second page to that answer. if (slot->abandoned) { - close_slot(slot); + close_fd(&slot->cache_fd); return 0; } err = serve_slot(slot); - close_slot(slot); + close_fd(&slot->cache_fd); return err; } @@ -448,23 +451,22 @@ static int process_slot(struct cache_slot *slot) // and two popular pages sharing one slot evict each other on every // alternating visit. if (!err) - log_error("[cgit] Cache slot %s holds a different key, consider a larger cache-size\n", + fprintf(stderr, "[cgit] Cache slot %s holds a different key, consider a larger cache-size\n", slot->path); // If any part of creating a slot fails the page is still rendered // straight to the client and the caller is told the request succeeded, // because it did. - close_slot(slot); - if ((err = lock_slot(slot)) != 0) { - log_error("[cgit] Unable to lock slot %s: %s (%d)\n", slot->lock_path, strerror(err), err); + close_fd(&slot->cache_fd); + if (lock_slot(slot)) { slot->fn(); return 0; } if ((err = fill_slot(slot)) != 0) { - log_error("[cgit] Unable to fill slot %s: %s (%d)\n", slot->lock_path, strerror(err), err); + fprintf(stderr, "[cgit] Unable to fill slot %s: %s (%d)\n", slot->lock_path, strerror(err), err); unlock_slot(slot, 0); - close_lock(slot); + close_fd(&slot->lock_fd); // Rendering again is only right when nothing was delivered, // and an abandoned fill has already sent the error page. if (!slot->abandoned) @@ -473,7 +475,7 @@ static int process_slot(struct cache_slot *slot) } if (slot->abandoned) { - close_lock(slot); + close_fd(&slot->lock_fd); return 0; } @@ -486,26 +488,11 @@ static int process_slot(struct cache_slot *slot) else publish_slot(slot); err = serve_slot(slot); - close_slot(slot); + close_fd(&slot->cache_fd); return err; } /* - * The result lives in a static buffer and is only good until the next call. - */ -static const char *format_time(const char *format, time_t when) -{ - static char buf[64]; - struct tm tm; - - if (!when) - return "-"; - gmtime_r(&when, &tm); - strftime(buf, sizeof(buf) - 1, format, &tm); - return buf; -} - -/* * The accumulator is unsigned long, so on a 64 bit host this is not the * published FNV-1 value. Only slot selection depends on it. */ @@ -514,9 +501,6 @@ unsigned long cache_hash_str(const char *str) unsigned long h = FNV_OFFSET; unsigned char *s = (unsigned char *)str; - if (!s) - return h; - while (*s) { h *= FNV_PRIME; h ^= *s++; @@ -524,6 +508,16 @@ unsigned long cache_hash_str(const char *str) return h; } +/* + * Unlike git's strbuf_complete, an empty path gains the slash too, so an empty + * cache-root keeps its slots under / rather than in the working directory. + */ +static void end_with_slash(struct strbuf *path) +{ + if (!path->len || path->buf[path->len - 1] != '/') + strbuf_addch(path, '/'); +} + int cache_process(int size, const char *path, const char *key, int ttl, size_t max_bytes, cache_fill_fn fn) { @@ -539,21 +533,14 @@ int cache_process(int size, const char *path, const char *key, int ttl, size_t m return 0; } - if (!path) { - log_error("[cgit] Cache path not specified, caching is disabled\n"); - fn(); - return 0; - } - if (!key) - key = ""; if (!key_fits_slot(key)) { - log_error("[cgit] Cache key too long for a slot, caching is disabled for this request\n"); + fprintf(stderr, "[cgit] Cache key too long for a slot, caching is disabled for this request\n"); fn(); return 0; } hash = cache_hash_str(key) % size; strbuf_addstr(&slot_path, path); - strbuf_ensure_end(&slot_path, '/'); + end_with_slash(&slot_path); for (i = 0; i < SLOT_NAME_LEN; i++) { strbuf_addf(&slot_path, "%x", (unsigned char)(hash & 0xf)); hash >>= 4; @@ -577,7 +564,7 @@ int cache_process(int size, const char *path, const char *key, int ttl, size_t m return result; } -int cache_ls(const char *path) +void cache_ls(const char *path) { DIR *dir; struct dirent *ent; @@ -590,42 +577,41 @@ int cache_ls(const char *path) char *nul; int keylen; - if (!path) { - log_error("[cgit] Cache path not specified\n"); - return -1; - } dir = opendir(path); if (!dir) { err = errno; - log_error("[cgit] Unable to open %s: %s (%d)\n", path, strerror(err), err); - return err; + fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", path, strerror(err), err); + return; } strbuf_addstr(&slot_path, path); - strbuf_ensure_end(&slot_path, '/'); + end_with_slash(&slot_path); prefixlen = slot_path.len; while ((ent = readdir(dir))) { + char mtime[64] = "-"; + struct tm tm; + if (strlen(ent->d_name) != SLOT_NAME_LEN) continue; strbuf_setlen(&slot_path, prefixlen); strbuf_addstr(&slot_path, ent->d_name); slot.path = slot_path.buf; if ((err = open_slot(&slot)) != 0) { - log_error("[cgit] Unable to open %s: %s (%d)\n", slot_path.buf, strerror(err), err); - close_slot(&slot); + fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", slot_path.buf, strerror(err), err); + close_fd(&slot.cache_fd); continue; } + if (slot.st.st_mtime && gmtime_r(&slot.st.st_mtime, &tm)) + strftime(mtime, sizeof(mtime), "%Y-%m-%d %H:%M:%S", &tm); // A truncated or corrupt slot may hold no NUL, so the print is // bounded by what was read and cannot run off the end. nul = memchr(slot.buf, 0, slot.buflen); keylen = nul ? (int)(nul - slot.buf) : slot.buflen; htmlf( - "%s %s %10"PRIuMAX" %.*s\n", slot_path.buf, - format_time("%Y-%m-%d %H:%M:%S", slot.st.st_mtime), + "%s %s %10"PRIuMAX" %.*s\n", slot_path.buf, mtime, (uintmax_t)slot.st.st_size, keylen, slot.buf ); - close_slot(&slot); + close_fd(&slot.cache_fd); } closedir(dir); strbuf_release(&slot_path); - return 0; } |
