| /* | |
| * libwebsockets - small server side websockets and web server implementation | |
| * | |
| * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com> | |
| * | |
| * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| * of this software and associated documentation files (the "Software"), to | |
| * deal in the Software without restriction, including without limitation the | |
| * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or | |
| * sell copies of the Software, and to permit persons to whom the Software is | |
| * furnished to do so, subject to the following conditions: | |
| * | |
| * The above copyright notice and this permission notice shall be included in | |
| * all copies or substantial portions of the Software. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
| * IN THE SOFTWARE. | |
| */ | |
| #if !defined(_GNU_SOURCE) | |
| #define _GNU_SOURCE | |
| #endif | |
| #include <pthread.h> | |
| #include <libwebsockets.h> | |
| #include "private-lib-core.h" | |
| #include <string.h> | |
| #include <stdio.h> | |
| #include <unistd.h> | |
| #include <fcntl.h> | |
| #include <dirent.h> | |
| #include <time.h> | |
| #include <errno.h> | |
| #include <stdarg.h> | |
| #include <sys/stat.h> | |
| #include <sys/time.h> | |
| #include <sys/types.h> | |
| #if defined(__APPLE__) | |
| #include <sys/dirent.h> | |
| /* Travis OSX does not have DT_REG... */ | |
| #if !defined(DT_REG) | |
| #define DT_REG 8 | |
| #endif | |
| #endif | |
| struct file_entry { | |
| lws_list_ptr sorted; | |
| lws_list_ptr prev; | |
| char name[64]; | |
| time_t modified; | |
| size_t size; | |
| }; | |
| struct lws_diskcache_scan { | |
| struct file_entry *batch; | |
| const char *cache_dir_base; | |
| lws_list_ptr head; | |
| time_t last_scan_completed; | |
| uint64_t agg_size; | |
| uint64_t cache_size_limit; | |
| uint64_t avg_size; | |
| uint64_t cache_tries; | |
| uint64_t cache_hits; | |
| int cache_subdir; | |
| int batch_in_use; | |
| int agg_file_count; | |
| int secs_waiting; | |
| }; | |
| #define KIB (1024) | |
| #define MIB (KIB * KIB) | |
| #define lp_to_fe(p, _n) lws_list_ptr_container(p, struct file_entry, _n) | |
| static const char *hex = "0123456789abcdef"; | |
| #define BATCH_COUNT 128 | |
| static int | |
| fe_modified_sort(lws_list_ptr a, lws_list_ptr b) | |
| { | |
| struct file_entry *p1 = lp_to_fe(a, sorted), *p2 = lp_to_fe(b, sorted); | |
| return p2->modified - p1->modified; | |
| } | |
| struct lws_diskcache_scan * | |
| lws_diskcache_create(const char *cache_dir_base, uint64_t cache_size_limit) | |
| { | |
| struct lws_diskcache_scan *lds = lws_malloc(sizeof(*lds), "cachescan"); | |
| if (!lds) | |
| return NULL; | |
| memset(lds, 0, sizeof(*lds)); | |
| lds->cache_dir_base = cache_dir_base; | |
| lds->cache_size_limit = cache_size_limit; | |
| return lds; | |
| } | |
| void | |
| lws_diskcache_destroy(struct lws_diskcache_scan **lds) | |
| { | |
| if ((*lds)->batch) | |
| lws_free((*lds)->batch); | |
| lws_free(*lds); | |
| *lds = NULL; | |
| } | |
| int | |
| lws_diskcache_prepare(const char *cache_base_dir, int mode, int uid) | |
| { | |
| char dir[256]; | |
| int n, m; | |
| (void)mkdir(cache_base_dir, mode); | |
| if (chown(cache_base_dir, uid, -1)) | |
| lwsl_err("%s: %s: unable to chown %d\n", __func__, | |
| cache_base_dir, uid); | |
| for (n = 0; n < 16; n++) { | |
| lws_snprintf(dir, sizeof(dir), "%s/%c", cache_base_dir, hex[n]); | |
| (void)mkdir(dir, mode); | |
| if (chown(dir, uid, -1)) | |
| lwsl_err("%s: %s: unable to chown %d\n", __func__, | |
| dir, uid); | |
| for (m = 0; m < 16; m++) { | |
| lws_snprintf(dir, sizeof(dir), "%s/%c/%c", | |
| cache_base_dir, hex[n], hex[m]); | |
| (void)mkdir(dir, mode); | |
| if (chown(dir, uid, -1)) | |
| lwsl_err("%s: %s: unable to chown %d\n", | |
| __func__, dir, uid); | |
| } | |
| } | |
| return 0; | |
| } | |
| /* copies and then truncates the incoming name, and renames the file at the | |
| * untruncated path to have the new truncated name */ | |
| int | |
| lws_diskcache_finalize_name(char *cache) | |
| { | |
| char ren[256], *p; | |
| strncpy(ren, cache, sizeof(ren) - 1); | |
| ren[sizeof(ren) - 1] = '\0'; | |
| p = strchr(cache, '~'); | |
| if (p) { | |
| *p = '\0'; | |
| if (rename(ren, cache)) { | |
| lwsl_err("%s: problem renaming %s to %s\n", __func__, | |
| ren, cache); | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| return 1; | |
| } | |
| int | |
| lws_diskcache_query(struct lws_diskcache_scan *lds, int is_bot, | |
| const char *hash_hex, int *_fd, char *cache, int cache_len, | |
| size_t *extant_cache_len) | |
| { | |
| struct stat s; | |
| int n; | |
| /* caching is disabled? */ | |
| if (!lds->cache_dir_base) | |
| return LWS_DISKCACHE_QUERY_NO_CACHE; | |
| if (!is_bot) | |
| lds->cache_tries++; | |
| n = lws_snprintf(cache, cache_len, "%s/%c/%c/%s", lds->cache_dir_base, | |
| hash_hex[0], hash_hex[1], hash_hex); | |
| lwsl_info("%s: job cache %s\n", __func__, cache); | |
| *_fd = open(cache, O_RDONLY); | |
| if (*_fd >= 0) { | |
| int fd; | |
| if (!is_bot) | |
| lds->cache_hits++; | |
| if (fstat(*_fd, &s)) { | |
| close(*_fd); | |
| return LWS_DISKCACHE_QUERY_NO_CACHE; | |
| } | |
| *extant_cache_len = (size_t)s.st_size; | |
| /* "touch" the hit cache file so it's last for LRU now */ | |
| fd = open(cache, O_RDWR); | |
| if (fd >= 0) | |
| close(fd); | |
| return LWS_DISKCACHE_QUERY_EXISTS; | |
| } | |
| /* bots are too random to pollute the cache with their antics */ | |
| if (is_bot) | |
| return LWS_DISKCACHE_QUERY_NO_CACHE; | |
| /* let's create it first with a unique temp name */ | |
| lws_snprintf(cache + n, cache_len - n, "~%d-%p", (int)getpid(), | |
| extant_cache_len); | |
| *_fd = open(cache, O_RDWR | O_CREAT | O_TRUNC, 0600); | |
| if (*_fd < 0) { | |
| /* well... ok... we will proceed without cache then... */ | |
| lwsl_notice("%s: Problem creating cache %s: errno %d\n", | |
| __func__, cache, errno); | |
| return LWS_DISKCACHE_QUERY_NO_CACHE; | |
| } | |
| return LWS_DISKCACHE_QUERY_CREATING; | |
| } | |
| int | |
| lws_diskcache_secs_to_idle(struct lws_diskcache_scan *lds) | |
| { | |
| return lds->secs_waiting; | |
| } | |
| /* | |
| * The goal is to collect the oldest BATCH_COUNT filepaths and filesizes from | |
| * the dirs under the cache dir. Since we don't need or want a full list of | |
| * files in there in memory at once, we restrict the linked-list size to | |
| * BATCH_COUNT entries, and once it is full, simply ignore any further files | |
| * that are newer than the newest one on that list. Files older than the | |
| * newest guy already on the list evict the newest guy already on the list | |
| * and are sorted into the correct order. In this way no matter the number | |
| * of files to be processed the memory requirement is fixed at BATCH_COUNT | |
| * struct file_entry-s. | |
| * | |
| * The oldest subset of BATCH_COUNT files are sorted into the cd->batch | |
| * allocation in more recent -> least recent order. | |
| * | |
| * We want to track the total size of all files we saw as well, so we know if | |
| * we need to actually do anything yet to restrict how much space it's taking | |
| * up. | |
| * | |
| * And we want to do those things statefully and incrementally instead of one | |
| * big atomic operation, since the user may want a huge cache, so we look in | |
| * one cache dir at a time and track state in the repodir struct. | |
| * | |
| * When we have seen everything, we add the doubly-linked prev pointers and then | |
| * if we are over the limit, start deleting up to BATCH_COUNT files working back | |
| * from the end. | |
| */ | |
| int | |
| lws_diskcache_trim(struct lws_diskcache_scan *lds) | |
| { | |
| size_t cache_size_limit = lds->cache_size_limit; | |
| char dirpath[132], filepath[132 + 32]; | |
| lws_list_ptr lp, op = NULL; | |
| int files_trimmed = 0; | |
| struct file_entry *p; | |
| int fd, n, ret = -1; | |
| size_t trimmed = 0; | |
| struct dirent *de; | |
| struct stat s; | |
| DIR *dir; | |
| if (!lds->cache_subdir) { | |
| if (lds->last_scan_completed + lds->secs_waiting > time(NULL)) | |
| return 0; | |
| lds->batch = lws_malloc(sizeof(struct file_entry) * | |
| BATCH_COUNT, "cache_trim"); | |
| if (!lds->batch) { | |
| lwsl_err("%s: OOM\n", __func__); | |
| return 1; | |
| } | |
| lds->agg_size = 0; | |
| lds->head = NULL; | |
| lds->batch_in_use = 0; | |
| lds->agg_file_count = 0; | |
| } | |
| lws_snprintf(dirpath, sizeof(dirpath), "%s/%c/%c", | |
| lds->cache_dir_base, hex[(lds->cache_subdir >> 4) & 15], | |
| hex[lds->cache_subdir & 15]); | |
| dir = opendir(dirpath); | |
| if (!dir) { | |
| lwsl_err("Unable to walk repo dir '%s'\n", | |
| lds->cache_dir_base); | |
| return -1; | |
| } | |
| do { | |
| de = readdir(dir); | |
| if (!de) | |
| break; | |
| if (de->d_type != DT_REG) | |
| continue; | |
| lds->agg_file_count++; | |
| lws_snprintf(filepath, sizeof(filepath), "%s/%s", dirpath, | |
| de->d_name); | |
| fd = open(filepath, O_RDONLY); | |
| if (fd < 0) { | |
| lwsl_err("%s: cannot open %s\n", __func__, filepath); | |
| continue; | |
| } | |
| n = fstat(fd, &s); | |
| close(fd); | |
| if (n) { | |
| lwsl_notice("%s: cannot stat %s\n", __func__, filepath); | |
| continue; | |
| } | |
| lds->agg_size += s.st_size; | |
| if (lds->batch_in_use == BATCH_COUNT) { | |
| /* | |
| * once we filled up the batch with candidates, we don't | |
| * need to consider any files newer than the newest guy | |
| * on the list... | |
| */ | |
| if (lp_to_fe(lds->head, sorted)->modified < s.st_mtime) | |
| continue; | |
| /* | |
| * ... and if we find an older file later, we know it | |
| * will be replacing the newest guy on the list, so use | |
| * that directly... | |
| */ | |
| p = lds->head; | |
| lds->head = p->sorted; | |
| } else | |
| /* we are still accepting anything to fill the batch */ | |
| p = &lds->batch[lds->batch_in_use++]; | |
| p->sorted = NULL; | |
| strncpy(p->name, de->d_name, sizeof(p->name) - 1); | |
| p->name[sizeof(p->name) - 1] = '\0'; | |
| p->modified = s.st_mtime; | |
| p->size = s.st_size; | |
| lws_list_ptr_insert(&lds->head, &p->sorted, fe_modified_sort); | |
| } while (de); | |
| ret = 0; | |
| lds->cache_subdir++; | |
| if (lds->cache_subdir != 0x100) | |
| goto done; | |
| /* we completed the whole scan... */ | |
| /* if really no guidence, then 256MiB */ | |
| if (!cache_size_limit) | |
| cache_size_limit = 256 * 1024 * 1024; | |
| if (lds->agg_size > cache_size_limit) { | |
| /* apply prev pointers to make the list doubly-linked */ | |
| lp = lds->head; | |
| while (lp) { | |
| p = lp_to_fe(lp, sorted); | |
| p->prev = op; | |
| op = &p->prev; | |
| lp = p->sorted; | |
| } | |
| /* | |
| * reverse the list (start from tail, now traverse using | |
| * .prev)... it's oldest-first now... | |
| */ | |
| lp = op; | |
| while (lp && lds->agg_size > cache_size_limit) { | |
| p = lp_to_fe(lp, prev); | |
| lws_snprintf(filepath, sizeof(filepath), "%s/%c/%c/%s", | |
| lds->cache_dir_base, p->name[0], | |
| p->name[1], p->name); | |
| if (!unlink(filepath)) { | |
| lds->agg_size -= p->size; | |
| trimmed += p->size; | |
| files_trimmed++; | |
| } else | |
| lwsl_notice("%s: Failed to unlink %s\n", | |
| __func__, filepath); | |
| lp = p->prev; | |
| } | |
| if (files_trimmed) | |
| lwsl_notice("%s: %s: trimmed %d files totalling " | |
| "%lldKib, leaving %lldMiB\n", __func__, | |
| lds->cache_dir_base, files_trimmed, | |
| ((unsigned long long)trimmed) / KIB, | |
| ((unsigned long long)lds->agg_size) / MIB); | |
| } | |
| if (lds->agg_size && lds->agg_file_count) | |
| lds->avg_size = lds->agg_size / lds->agg_file_count; | |
| /* | |
| * estimate how long we can go before scanning again... default we need | |
| * to start again immediately | |
| */ | |
| lds->last_scan_completed = time(NULL); | |
| lds->secs_waiting = 1; | |
| if (lds->agg_size < cache_size_limit) { | |
| uint64_t avg = 4096, capacity, projected; | |
| /* let's use 80% of the real average for margin */ | |
| if (lds->agg_size && lds->agg_file_count) | |
| avg = ((lds->agg_size * 8) / lds->agg_file_count) / 10; | |
| /* | |
| * if we collected BATCH_COUNT files of the average size, | |
| * how much can we clean up in 256s? | |
| */ | |
| capacity = avg * BATCH_COUNT; | |
| /* | |
| * if the cache grew by 10%, would we hit the limit even then? | |
| */ | |
| projected = (lds->agg_size * 11) / 10; | |
| if (projected < cache_size_limit) | |
| /* no... */ | |
| lds->secs_waiting = (256 / 2) * ((cache_size_limit - | |
| projected) / capacity); | |
| /* | |
| * large waits imply we may not have enough info yet, so | |
| * check once an hour at least. | |
| */ | |
| if (lds->secs_waiting > 3600) | |
| lds->secs_waiting = 3600; | |
| } else | |
| lds->secs_waiting = 0; | |
| lwsl_info("%s: cache %s: %lldKiB / %lldKiB, next scan %ds\n", __func__, | |
| lds->cache_dir_base, | |
| (unsigned long long)lds->agg_size / KIB, | |
| (unsigned long long)cache_size_limit / KIB, | |
| lds->secs_waiting); | |
| lws_free(lds->batch); | |
| lds->batch = NULL; | |
| lds->cache_subdir = 0; | |
| done: | |
| closedir(dir); | |
| return ret; | |
| } |