mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
* perf: answer Steam's missing-file checks on FUSE libraries from the directory cache Before creating and committing each file, the Steam client checks whether it already exists under its exact, lower-cased and folded spellings. On an SD or USB library reached through Android's FUSE layer every one of those misses is a round trip to MediaProvider (Brawlhalla: about 65,000 misses, 92 of the 161 s of preallocation and 60 of the 93 s of commit). The client already caches the listings of its steamapps directories on FUSE and keeps them exact through its own changes, inotify and a short TTL. access() now answers a name that is absent from a live listing of its directory without asking the filesystem. Directory paths match case-insensitively only when the library's filesystem folds case, and names compare case-insensitively, so an existing file is never reported missing. * fix: reuse a listing for a differently cased folder only when the whole path folds case Checking that steamapps alone folds case let a listing of one folder answer for a different folder below it whose name differs only in case. The fill now compares the listed folder with its fully case-swapped path below steamapps, so a listing is reused across case only when the filesystem folds every level. * fix(dircache): match a folded listing only below the same prefix and skip it during a tracked change * fix(linuxfs): a folder renamed or removed above a cached listing drops that listing * fix(linuxfs): watch the folders above a cached listing so a rename or removal by any process drops it * fix(linuxfs): also watch the folders above steamapps, and stop a stuck probe from hanging the tests * fix(linuxfs): a listing under a symlink or an unwatchable folder no longer answers absent lookups * fix(linuxfs): a symlink check that cannot be watched also stops absent answers * fix(linuxfs): a differently cased lookup reuses a listing only for the same folder
952 lines
27 KiB
C
952 lines
27 KiB
C
#define _GNU_SOURCE
|
|
#include <dirent.h>
|
|
#include <dlfcn.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/inotify.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/statfs.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#define DIR_TTL_NS 5000000000LL
|
|
#define MAX_LISTINGS 16
|
|
#define MAX_ENTRIES 100000
|
|
#define MAX_HANDLES 64
|
|
#define MAX_UP 32
|
|
#define FUSE_MAGIC 0x65735546
|
|
#define WATCHED (IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF | IN_ONLYDIR)
|
|
#define ABOVE (IN_DELETE_SELF | IN_MOVE_SELF | IN_ONLYDIR | IN_MASK_ADD)
|
|
#define GONE (IN_DELETE_SELF | IN_MOVE_SELF | IN_IGNORED)
|
|
#define LINKED (IN_DELETE_SELF | IN_MOVE_SELF | IN_DONT_FOLLOW | IN_MASK_ADD)
|
|
|
|
#if __SIZEOF_POINTER__ == 8
|
|
|
|
_Static_assert(sizeof(struct dirent) == sizeof(struct dirent64), "dirent layouts differ");
|
|
|
|
struct entry { ino_t ino; unsigned char type; char *name; };
|
|
|
|
struct listing {
|
|
dev_t dev;
|
|
ino_t ino;
|
|
struct timespec mtime, ctime;
|
|
off_t size;
|
|
nlink_t nlink;
|
|
long long filled;
|
|
long long used;
|
|
int refs;
|
|
int detached;
|
|
int wd;
|
|
int up[MAX_UP], nup;
|
|
int blind;
|
|
size_t n, cap;
|
|
struct entry *e;
|
|
char *recs;
|
|
size_t len;
|
|
char *where;
|
|
int folds;
|
|
};
|
|
|
|
struct handle {
|
|
struct listing *snap;
|
|
struct listing *fill;
|
|
DIR *real;
|
|
size_t pos;
|
|
int fd;
|
|
int wd;
|
|
int up[MAX_UP], nup;
|
|
int blind;
|
|
int dirty;
|
|
char *path;
|
|
struct dirent64 out;
|
|
};
|
|
|
|
struct expect { int wd; uint32_t mask; const char *name; };
|
|
|
|
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_mutex_t oplock = PTHREAD_MUTEX_INITIALIZER;
|
|
static struct listing *table[MAX_LISTINGS];
|
|
static struct handle *handles[MAX_HANDLES];
|
|
static int high_water;
|
|
static int cached_listings;
|
|
static int changing;
|
|
static int notify = -1;
|
|
static int forked;
|
|
static struct { dev_t dev; int fuse; } devs[8];
|
|
static int ndevs;
|
|
|
|
static long long now_ns(void) {
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_MONOTONIC_COARSE, &t);
|
|
return (long long)t.tv_sec * 1000000000LL + t.tv_nsec;
|
|
}
|
|
|
|
static void after_fork(void) {
|
|
pthread_mutex_init(&lock, NULL);
|
|
pthread_mutex_init(&oplock, NULL);
|
|
forked = 1;
|
|
}
|
|
|
|
static int enabled(void) {
|
|
static int cached = -1;
|
|
if (cached < 0) {
|
|
const char *off = getenv("BL_NO_DIR_CACHE");
|
|
cached = strcmp(program_invocation_short_name, "steam") == 0 &&
|
|
(strstr(program_invocation_name, "steamrtarm64") != NULL ||
|
|
strstr(program_invocation_name, "steamrt64") != NULL) && !(off && strcmp(off, "1") == 0);
|
|
if (cached) pthread_atfork(NULL, NULL, after_fork);
|
|
}
|
|
return cached && !forked;
|
|
}
|
|
|
|
static int eligible(const char *path) {
|
|
if (path == NULL || path[0] != '/' || !enabled()) return 0;
|
|
const char *s = strstr(path, "/steamapps");
|
|
return s && (s[10] == '/' || s[10] == 0);
|
|
}
|
|
|
|
static int on_fuse(dev_t dev, const char *path) {
|
|
pthread_mutex_lock(&lock);
|
|
for (int i = 0; i < ndevs; i++) {
|
|
if (devs[i].dev == dev) {
|
|
int fuse = devs[i].fuse;
|
|
pthread_mutex_unlock(&lock);
|
|
return fuse;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
struct statfs fs;
|
|
int saved = errno;
|
|
int fuse = statfs(path, &fs) == 0 && (unsigned long)fs.f_type == FUSE_MAGIC;
|
|
errno = saved;
|
|
pthread_mutex_lock(&lock);
|
|
if (ndevs < 8) {
|
|
devs[ndevs].dev = dev;
|
|
devs[ndevs].fuse = fuse;
|
|
ndevs++;
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
return fuse;
|
|
}
|
|
|
|
static int same(const struct listing *l, const struct stat *st) {
|
|
return l->ino == st->st_ino && l->dev == st->st_dev && l->size == st->st_size && l->nlink == st->st_nlink &&
|
|
l->mtime.tv_sec == st->st_mtim.tv_sec && l->mtime.tv_nsec == st->st_mtim.tv_nsec &&
|
|
l->ctime.tv_sec == st->st_ctim.tv_sec && l->ctime.tv_nsec == st->st_ctim.tv_nsec;
|
|
}
|
|
|
|
static void stamp(struct listing *l, const struct stat *st) {
|
|
l->dev = st->st_dev;
|
|
l->ino = st->st_ino;
|
|
l->size = st->st_size;
|
|
l->nlink = st->st_nlink;
|
|
l->mtime = st->st_mtim;
|
|
l->ctime = st->st_ctim;
|
|
}
|
|
|
|
static void destroy(struct listing *l) {
|
|
for (size_t i = 0; i < l->n; i++) free(l->e[i].name);
|
|
free(l->e);
|
|
free(l->recs);
|
|
free(l->where);
|
|
free(l);
|
|
}
|
|
|
|
static void unref(struct listing *l) {
|
|
if (--l->refs == 0 && l->detached) destroy(l);
|
|
}
|
|
|
|
|
|
static int under(const int *up, int nup, int wd) {
|
|
for (int i = 0; i < nup; i++)
|
|
if (up[i] == wd) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void release(int wd) {
|
|
if (wd < 0 || notify < 0) return;
|
|
for (int i = 0; i < MAX_LISTINGS; i++)
|
|
if (table[i] && (table[i]->wd == wd || under(table[i]->up, table[i]->nup, wd))) return;
|
|
for (int i = 0; i < high_water; i++)
|
|
if (handles[i] && (handles[i]->wd == wd || under(handles[i]->up, handles[i]->nup, wd))) return;
|
|
inotify_rm_watch(notify, wd);
|
|
}
|
|
|
|
static void let_go(int wd, const int *up, int nup) {
|
|
release(wd);
|
|
for (int i = 0; i < nup; i++) release(up[i]);
|
|
}
|
|
|
|
static void detach(struct listing *l) {
|
|
__atomic_sub_fetch(&cached_listings, 1, __ATOMIC_RELEASE);
|
|
l->detached = 1;
|
|
if (l->refs == 0) destroy(l);
|
|
}
|
|
|
|
static void drop(int slot) {
|
|
struct listing *l = table[slot];
|
|
int wd = l->wd, up[MAX_UP], nup = l->nup;
|
|
memcpy(up, l->up, sizeof(up));
|
|
table[slot] = NULL;
|
|
detach(l);
|
|
let_go(wd, up, nup);
|
|
}
|
|
|
|
static void changed(int wd, uint32_t mask) {
|
|
int gone = (mask & GONE) != 0;
|
|
for (int i = 0; i < MAX_LISTINGS; i++)
|
|
if (table[i] && (table[i]->wd == wd || wd < 0 || (gone && under(table[i]->up, table[i]->nup, wd)))) drop(i);
|
|
for (int i = 0; i < high_water; i++)
|
|
if (handles[i] && (handles[i]->wd == wd || wd < 0 || (gone && under(handles[i]->up, handles[i]->nup, wd))))
|
|
__atomic_store_n(&handles[i]->dirty, 1, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
static void forget_below(const char *path) {
|
|
if (path == NULL || path[0] != '/') return;
|
|
size_t n = strlen(path);
|
|
while (n > 1 && path[n - 1] == '/') n--;
|
|
for (int i = 0; i < MAX_LISTINGS; i++) {
|
|
struct listing *l = table[i];
|
|
if (l == NULL || l->where == NULL) continue;
|
|
int prefix = l->folds ? strncasecmp(l->where, path, n) == 0 : strncmp(l->where, path, n) == 0;
|
|
if (prefix && (l->where[n] == '/' || l->where[n] == 0)) drop(i);
|
|
}
|
|
}
|
|
|
|
static void pump(struct expect *x, int nx) {
|
|
char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
|
|
if (notify < 0 || forked) return;
|
|
int saved = errno;
|
|
for (;;) {
|
|
ssize_t n = read(notify, buf, sizeof(buf));
|
|
if (n <= 0) {
|
|
if (n < 0 && errno != EAGAIN && errno != EINTR) {
|
|
notify = -2;
|
|
changed(-1, 0);
|
|
}
|
|
break;
|
|
}
|
|
for (char *p = buf; p < buf + n;) {
|
|
struct inotify_event *ev = (struct inotify_event *)p;
|
|
p += sizeof(*ev) + ev->len;
|
|
if (ev->mask & IN_Q_OVERFLOW) {
|
|
changed(-1, 0);
|
|
continue;
|
|
}
|
|
int k = 0;
|
|
while (k < nx && !(x[k].wd == ev->wd && (ev->mask & x[k].mask) && ev->len && strcmp(ev->name, x[k].name) == 0)) k++;
|
|
if (k < nx) {
|
|
x[k].wd = -1;
|
|
for (int i = 0; i < high_water; i++)
|
|
if (handles[i] && handles[i]->wd == ev->wd) __atomic_store_n(&handles[i]->dirty, 1, __ATOMIC_RELAXED);
|
|
} else {
|
|
changed(ev->wd, ev->mask);
|
|
}
|
|
}
|
|
}
|
|
errno = saved;
|
|
}
|
|
|
|
static int watch(const char *path, uint32_t mask) {
|
|
if (notify == -1) notify = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
|
|
if (notify < 0) {
|
|
notify = -2;
|
|
return -1;
|
|
}
|
|
int saved = errno;
|
|
int wd = inotify_add_watch(notify, path, mask);
|
|
errno = saved;
|
|
return wd;
|
|
}
|
|
|
|
static int find(dev_t dev, ino_t ino) {
|
|
for (int i = 0; i < MAX_LISTINGS; i++)
|
|
if (table[i] && table[i]->dev == dev && table[i]->ino == ino) return i;
|
|
return -1;
|
|
}
|
|
|
|
static int push(struct listing *l, ino_t ino, unsigned char type, const char *name) {
|
|
if (l->n == l->cap) {
|
|
size_t cap = l->cap ? l->cap * 2 : 256;
|
|
struct entry *e = realloc(l->e, cap * sizeof(*e));
|
|
if (e == NULL) return -1;
|
|
l->e = e;
|
|
l->cap = cap;
|
|
}
|
|
char *copy = strdup(name);
|
|
if (copy == NULL) return -1;
|
|
l->e[l->n++] = (struct entry){ino, type, copy};
|
|
return 0;
|
|
}
|
|
|
|
static void install(struct listing *l) {
|
|
l->detached = 0;
|
|
__atomic_add_fetch(&cached_listings, 1, __ATOMIC_RELEASE);
|
|
int slot = find(l->dev, l->ino);
|
|
if (slot >= 0) {
|
|
struct listing *old = table[slot];
|
|
int old_wd = old->wd, up[MAX_UP], nup = old->nup;
|
|
memcpy(up, old->up, sizeof(up));
|
|
table[slot] = l;
|
|
detach(old);
|
|
let_go(old_wd, up, nup);
|
|
return;
|
|
}
|
|
int victim = -1;
|
|
for (int i = 0; i < MAX_LISTINGS; i++) {
|
|
if (table[i] == NULL) { victim = i; break; }
|
|
if (victim < 0 || table[i]->used < table[victim]->used) victim = i;
|
|
}
|
|
if (table[victim]) drop(victim);
|
|
table[victim] = l;
|
|
}
|
|
|
|
static int registered(DIR *d) {
|
|
int n = __atomic_load_n(&high_water, __ATOMIC_ACQUIRE);
|
|
for (int i = 0; i < n; i++)
|
|
if (__atomic_load_n(&handles[i], __ATOMIC_ACQUIRE) == (struct handle *)d) return i;
|
|
return -1;
|
|
}
|
|
|
|
static DIR *wrap(struct handle *h) {
|
|
for (int i = 0; i < MAX_HANDLES; i++) {
|
|
if (handles[i] == NULL) {
|
|
__atomic_store_n(&handles[i], h, __ATOMIC_RELEASE);
|
|
if (i >= high_water) __atomic_store_n(&high_water, i + 1, __ATOMIC_RELEASE);
|
|
return (DIR *)h;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
#define RECLEN(namelen) ((offsetof(struct dirent64, d_name) + (namelen) + 1 + 7) & ~(size_t)7)
|
|
|
|
static int records(struct listing *l) {
|
|
if (l->recs) return 0;
|
|
size_t len = 0;
|
|
for (size_t i = 0; i < l->n; i++) len += RECLEN(strlen(l->e[i].name));
|
|
char *recs = malloc(len ? len : 1);
|
|
if (recs == NULL) return -1;
|
|
size_t off = 0;
|
|
for (size_t i = 0; i < l->n; i++) {
|
|
size_t namelen = strlen(l->e[i].name), reclen = RECLEN(namelen);
|
|
struct dirent64 *r = (struct dirent64 *)(recs + off);
|
|
memset(r, 0, reclen);
|
|
off += reclen;
|
|
r->d_ino = l->e[i].ino;
|
|
r->d_off = (off_t)off;
|
|
r->d_reclen = (unsigned short)reclen;
|
|
r->d_type = l->e[i].type;
|
|
memcpy(r->d_name, l->e[i].name, namelen + 1);
|
|
}
|
|
l->recs = recs;
|
|
l->len = len;
|
|
return 0;
|
|
}
|
|
|
|
|
|
static struct handle *lookup(DIR *d) {
|
|
return registered(d) >= 0 ? (struct handle *)d : NULL;
|
|
}
|
|
|
|
#define REAL(name) static __typeof__(&name) next_##name
|
|
#define NEXT(name) \
|
|
if (next_##name == NULL) next_##name = (__typeof__(&name))dlsym(RTLD_NEXT, #name)
|
|
|
|
REAL(opendir);
|
|
REAL(readdir64);
|
|
REAL(readdir);
|
|
REAL(closedir);
|
|
REAL(dirfd);
|
|
REAL(rewinddir);
|
|
REAL(telldir);
|
|
REAL(seekdir);
|
|
REAL(mkdir);
|
|
REAL(rmdir);
|
|
REAL(unlink);
|
|
REAL(remove);
|
|
REAL(rename);
|
|
REAL(link);
|
|
REAL(symlink);
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
REAL(readdir64_r);
|
|
REAL(readdir_r);
|
|
|
|
__attribute__((constructor)) static void resolve(void) {
|
|
NEXT(opendir);
|
|
NEXT(readdir64);
|
|
NEXT(readdir);
|
|
NEXT(closedir);
|
|
NEXT(dirfd);
|
|
NEXT(rewinddir);
|
|
NEXT(telldir);
|
|
NEXT(seekdir);
|
|
NEXT(mkdir);
|
|
NEXT(rmdir);
|
|
NEXT(unlink);
|
|
NEXT(remove);
|
|
NEXT(rename);
|
|
NEXT(link);
|
|
NEXT(symlink);
|
|
NEXT(readdir64_r);
|
|
NEXT(readdir_r);
|
|
}
|
|
#pragma GCC diagnostic pop
|
|
|
|
static DIR *hit(const struct stat *st, struct handle *h, long long now) {
|
|
pthread_mutex_lock(&lock);
|
|
if (!changing) pump(NULL, 0);
|
|
int slot = find(st->st_dev, st->st_ino);
|
|
if (slot >= 0 && same(table[slot], st) && now - table[slot]->filled < DIR_TTL_NS && records(table[slot]) == 0) {
|
|
h->snap = table[slot];
|
|
h->snap->refs++;
|
|
h->snap->used = now;
|
|
DIR *d = wrap(h);
|
|
if (d) {
|
|
pthread_mutex_unlock(&lock);
|
|
return d;
|
|
}
|
|
unref(h->snap);
|
|
h->snap = NULL;
|
|
} else if (slot >= 0) {
|
|
drop(slot);
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
return NULL;
|
|
}
|
|
|
|
static int above(struct handle *h, const char *path) {
|
|
char up[PATH_MAX];
|
|
size_t n = strlen(path);
|
|
while (n > 1 && path[n - 1] == '/') n--;
|
|
if (n >= sizeof(up)) return 0;
|
|
memcpy(up, path, n);
|
|
up[n] = 0;
|
|
size_t root = (size_t)(strstr(path, "/steamapps") - path) + 10;
|
|
for (size_t i = 1; i <= n; i++) {
|
|
if (up[i] != '/' && up[i] != 0) continue;
|
|
char at = up[i];
|
|
up[i] = 0;
|
|
int wd = i < n ? watch(up, ABOVE) : h->wd;
|
|
int link = watch(up, LINKED);
|
|
up[i] = at;
|
|
if (link < 0) h->blind = 1;
|
|
else if (link != wd) {
|
|
h->blind = 1;
|
|
release(link);
|
|
}
|
|
if (i == n) break;
|
|
if (wd < 0 && i < root) {
|
|
h->blind = 1;
|
|
continue;
|
|
}
|
|
if (wd < 0) return 0;
|
|
if (wd == h->wd || under(h->up, h->nup, wd)) continue;
|
|
if (h->nup == MAX_UP) {
|
|
release(wd);
|
|
return 0;
|
|
}
|
|
h->up[h->nup++] = wd;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int track(struct handle *h, const char *path) {
|
|
pthread_mutex_lock(&lock);
|
|
h->wd = watch(path, WATCHED);
|
|
DIR *d = h->wd >= 0 && above(h, path) ? wrap(h) : NULL;
|
|
if (d == NULL) let_go(h->wd, h->up, h->nup);
|
|
pthread_mutex_unlock(&lock);
|
|
return d != NULL;
|
|
}
|
|
|
|
static void filling(struct handle *h, DIR *real, const struct stat *st, long long now) {
|
|
h->real = real;
|
|
h->fill = calloc(1, sizeof(*h->fill));
|
|
if (h->fill) {
|
|
stamp(h->fill, st);
|
|
h->fill->filled = now;
|
|
h->fill->used = now;
|
|
h->fill->wd = h->wd;
|
|
memcpy(h->fill->up, h->up, sizeof(h->up));
|
|
h->fill->nup = h->nup;
|
|
h->fill->blind = h->blind;
|
|
h->fill->where = strdup(h->path);
|
|
}
|
|
}
|
|
|
|
static void untrack(struct handle *h) {
|
|
pthread_mutex_lock(&lock);
|
|
int i = registered((DIR *)h);
|
|
if (i >= 0) __atomic_store_n(&handles[i], NULL, __ATOMIC_RELEASE);
|
|
let_go(h->wd, h->up, h->nup);
|
|
pthread_mutex_unlock(&lock);
|
|
}
|
|
|
|
DIR *opendir(const char *path) {
|
|
NEXT(opendir);
|
|
struct stat st;
|
|
if (!eligible(path) || stat(path, &st) != 0 || !S_ISDIR(st.st_mode) || !on_fuse(st.st_dev, path))
|
|
return next_opendir(path);
|
|
struct handle *h = calloc(1, sizeof(*h));
|
|
char *copy = strdup(path);
|
|
if (h == NULL || copy == NULL) {
|
|
free(h);
|
|
free(copy);
|
|
return next_opendir(path);
|
|
}
|
|
h->fd = -1;
|
|
h->wd = -1;
|
|
h->path = copy;
|
|
long long now = now_ns();
|
|
DIR *d = hit(&st, h, now);
|
|
if (d) return d;
|
|
if (!track(h, path)) {
|
|
free(h->path);
|
|
free(h);
|
|
return next_opendir(path);
|
|
}
|
|
DIR *real = stat(path, &st) == 0 ? next_opendir(path) : NULL;
|
|
if (real == NULL) {
|
|
untrack(h);
|
|
free(h->path);
|
|
free(h);
|
|
return next_opendir(path);
|
|
}
|
|
filling(h, real, &st, now);
|
|
return (DIR *)h;
|
|
}
|
|
|
|
static void abandon(struct handle *h) {
|
|
if (h->fill) destroy(h->fill);
|
|
h->fill = NULL;
|
|
}
|
|
|
|
static int folds(const char *path, const struct stat *st) {
|
|
char swapped[PATH_MAX];
|
|
struct stat b;
|
|
const char *at = strstr(path, "/steamapps");
|
|
size_t n = strlen(path);
|
|
if (at == NULL || n >= sizeof(swapped)) return 0;
|
|
memcpy(swapped, path, n + 1);
|
|
for (char *c = swapped + (at - path); *c; c++)
|
|
if ((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z')) *c ^= 0x20;
|
|
int saved = errno;
|
|
int same_dir = stat(swapped, &b) == 0 && b.st_dev == st->st_dev && b.st_ino == st->st_ino;
|
|
errno = saved;
|
|
return same_dir;
|
|
}
|
|
|
|
static void finish(struct handle *h) {
|
|
struct listing *l = h->fill;
|
|
struct stat st;
|
|
h->fill = NULL;
|
|
int current = stat(h->path, &st) == 0 && same(l, &st);
|
|
if (current) l->folds = folds(h->path, &st);
|
|
pthread_mutex_lock(&lock);
|
|
if (!changing) pump(NULL, 0);
|
|
if (current && !__atomic_load_n(&h->dirty, __ATOMIC_RELAXED)) install(l);
|
|
else destroy(l);
|
|
pthread_mutex_unlock(&lock);
|
|
}
|
|
|
|
static struct dirent64 *entry(struct handle *h) {
|
|
NEXT(readdir64);
|
|
if (h->real) {
|
|
int saved = errno;
|
|
errno = 0;
|
|
struct dirent64 *r = next_readdir64(h->real);
|
|
int error = errno;
|
|
if (h->fill) {
|
|
if (r) {
|
|
if (h->fill->n >= MAX_ENTRIES || push(h->fill, r->d_ino, r->d_type, r->d_name) != 0) abandon(h);
|
|
} else if (error == 0) {
|
|
finish(h);
|
|
} else {
|
|
abandon(h);
|
|
}
|
|
}
|
|
errno = error ? error : saved;
|
|
return r;
|
|
}
|
|
if (h->pos >= h->snap->len) return NULL;
|
|
const struct dirent64 *r = (const struct dirent64 *)(h->snap->recs + h->pos);
|
|
h->pos += r->d_reclen;
|
|
memcpy(&h->out, r, r->d_reclen);
|
|
return &h->out;
|
|
}
|
|
|
|
struct dirent64 *readdir64(DIR *d) {
|
|
NEXT(readdir64);
|
|
struct handle *h = lookup(d);
|
|
return h ? entry(h) : next_readdir64(d);
|
|
}
|
|
|
|
struct dirent *readdir(DIR *d) {
|
|
NEXT(readdir);
|
|
struct handle *h = lookup(d);
|
|
return h ? (struct dirent *)entry(h) : next_readdir(d);
|
|
}
|
|
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
int readdir64_r(DIR *d, struct dirent64 *out, struct dirent64 **result) {
|
|
NEXT(readdir64_r);
|
|
struct handle *h = lookup(d);
|
|
if (h == NULL) return next_readdir64_r(d, out, result);
|
|
if (h->real) {
|
|
abandon(h);
|
|
return next_readdir64_r(h->real, out, result);
|
|
}
|
|
struct dirent64 *r = entry(h);
|
|
if (r) memcpy(out, r, r->d_reclen);
|
|
*result = r ? out : NULL;
|
|
return 0;
|
|
}
|
|
|
|
int readdir_r(DIR *d, struct dirent *out, struct dirent **result) {
|
|
NEXT(readdir_r);
|
|
if (lookup(d) == NULL) return next_readdir_r(d, out, result);
|
|
return readdir64_r(d, (struct dirent64 *)out, (struct dirent64 **)result);
|
|
}
|
|
|
|
#pragma GCC diagnostic pop
|
|
|
|
int closedir(DIR *d) {
|
|
NEXT(closedir);
|
|
pthread_mutex_lock(&lock);
|
|
int i = registered(d);
|
|
struct handle *h = i >= 0 ? handles[i] : NULL;
|
|
if (h) {
|
|
__atomic_store_n(&handles[i], NULL, __ATOMIC_RELEASE);
|
|
if (h->snap) unref(h->snap);
|
|
let_go(h->wd, h->up, h->nup);
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
if (h == NULL) return next_closedir(d);
|
|
int r = 0;
|
|
if (h->real) r = next_closedir(h->real);
|
|
abandon(h);
|
|
if (h->fd >= 0) close(h->fd);
|
|
free(h->path);
|
|
free(h);
|
|
return r;
|
|
}
|
|
|
|
int dirfd(DIR *d) {
|
|
NEXT(dirfd);
|
|
struct handle *h = lookup(d);
|
|
if (h == NULL) return next_dirfd(d);
|
|
if (h->real) return next_dirfd(h->real);
|
|
if (h->fd < 0) h->fd = open(h->path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
return h->fd;
|
|
}
|
|
|
|
void rewinddir(DIR *d) {
|
|
NEXT(rewinddir);
|
|
struct handle *h = lookup(d);
|
|
if (h == NULL) { next_rewinddir(d); return; }
|
|
if (h->real) {
|
|
abandon(h);
|
|
next_rewinddir(h->real);
|
|
} else {
|
|
h->pos = 0;
|
|
}
|
|
}
|
|
|
|
long telldir(DIR *d) {
|
|
NEXT(telldir);
|
|
struct handle *h = lookup(d);
|
|
if (h == NULL) return next_telldir(d);
|
|
return h->real ? next_telldir(h->real) : (long)h->pos;
|
|
}
|
|
|
|
void seekdir(DIR *d, long pos) {
|
|
NEXT(seekdir);
|
|
struct handle *h = lookup(d);
|
|
if (h == NULL) { next_seekdir(d, pos); return; }
|
|
if (h->real) {
|
|
abandon(h);
|
|
next_seekdir(h->real, pos);
|
|
} else {
|
|
size_t off = 0;
|
|
while (off < (size_t)pos && off < h->snap->len) off += ((const struct dirent64 *)(h->snap->recs + off))->d_reclen;
|
|
if (off == (size_t)pos) h->pos = off;
|
|
}
|
|
}
|
|
|
|
struct side { char dir[PATH_MAX]; const char *name; struct stat before; int known; };
|
|
|
|
static int split(const char *path, struct side *s) {
|
|
size_t n = strlen(path);
|
|
while (n > 1 && path[n - 1] == '/') n--;
|
|
const char *slash = memrchr(path, '/', n);
|
|
if (slash == NULL || (size_t)(slash - path) >= sizeof(s->dir) || slash[1] == 0) return 0;
|
|
size_t d = slash == path ? 1 : (size_t)(slash - path);
|
|
memcpy(s->dir, path, d);
|
|
s->dir[d] = 0;
|
|
s->name = slash + 1;
|
|
return strchr(s->name, '/') == NULL && strcmp(s->name, ".") != 0 && strcmp(s->name, "..") != 0;
|
|
}
|
|
|
|
static int watching(const char *path) {
|
|
return eligible(path) && __atomic_load_n(&cached_listings, __ATOMIC_ACQUIRE) > 0;
|
|
}
|
|
|
|
static void begin(void) {
|
|
pthread_mutex_lock(&lock);
|
|
pump(NULL, 0);
|
|
changing = 1;
|
|
pthread_mutex_unlock(&lock);
|
|
}
|
|
|
|
static void before(struct side *s) {
|
|
s->known = stat(s->dir, &s->before) == 0;
|
|
}
|
|
|
|
enum { ADDED, REMOVED };
|
|
|
|
static int patch(struct side *s, int what, uint32_t event, int ok, int error, const char *target, struct expect *x) {
|
|
struct stat now;
|
|
if (!s->known) return 0;
|
|
int slot = find(s->before.st_dev, s->before.st_ino);
|
|
if (slot < 0) return 0;
|
|
struct listing *l = table[slot];
|
|
if (!same(l, &s->before)) {
|
|
drop(slot);
|
|
return 0;
|
|
}
|
|
size_t exact = l->n, folded = l->n;
|
|
for (size_t i = 0; i < l->n; i++) {
|
|
if (strcmp(l->e[i].name, s->name) == 0) { exact = i; break; }
|
|
if (folded == l->n && strcasecmp(l->e[i].name, s->name) == 0) folded = i;
|
|
}
|
|
if (!ok) {
|
|
if ((what == ADDED && error == EEXIST && exact == l->n && folded == l->n) || (what == REMOVED && error == ENOENT && exact < l->n))
|
|
drop(slot);
|
|
return 0;
|
|
}
|
|
if (stat(s->dir, &now) != 0 || now.st_ino != l->ino) {
|
|
drop(slot);
|
|
return 0;
|
|
}
|
|
if (l->refs > 0) {
|
|
struct listing *copy = calloc(1, sizeof(*copy));
|
|
int failed = copy == NULL;
|
|
for (size_t i = 0; !failed && i < l->n; i++) failed = push(copy, l->e[i].ino, l->e[i].type, l->e[i].name) != 0;
|
|
if (failed) {
|
|
if (copy) destroy(copy);
|
|
drop(slot);
|
|
return 0;
|
|
}
|
|
copy->filled = l->filled;
|
|
copy->used = l->used;
|
|
copy->wd = l->wd;
|
|
memcpy(copy->up, l->up, sizeof(l->up));
|
|
copy->nup = l->nup;
|
|
copy->blind = l->blind;
|
|
copy->where = l->where ? strdup(l->where) : NULL;
|
|
copy->folds = l->folds;
|
|
copy->detached = 0;
|
|
stamp(copy, &s->before);
|
|
table[slot] = copy;
|
|
l->detached = 1;
|
|
l = copy;
|
|
}
|
|
free(l->recs);
|
|
l->recs = NULL;
|
|
l->len = 0;
|
|
int fine = 1, expected = 1;
|
|
struct stat t;
|
|
if (what == ADDED) {
|
|
if (exact < l->n) {
|
|
if (lstat(target, &t) == 0) l->e[exact].ino = t.st_ino;
|
|
expected = event == IN_MOVED_TO;
|
|
} else if (folded < l->n || lstat(target, &t) != 0) {
|
|
fine = 0;
|
|
} else {
|
|
unsigned char type = S_ISDIR(t.st_mode) ? DT_DIR : S_ISLNK(t.st_mode) ? DT_LNK : S_ISREG(t.st_mode) ? DT_REG : DT_UNKNOWN;
|
|
fine = l->n < MAX_ENTRIES && push(l, t.st_ino, type, s->name) == 0;
|
|
}
|
|
} else if (exact < l->n) {
|
|
free(l->e[exact].name);
|
|
memmove(&l->e[exact], &l->e[exact + 1], (l->n - exact - 1) * sizeof(l->e[0]));
|
|
l->n--;
|
|
} else {
|
|
fine = 0;
|
|
}
|
|
if (!fine) {
|
|
drop(slot);
|
|
return 0;
|
|
}
|
|
stamp(l, &now);
|
|
if (!expected) return 0;
|
|
*x = (struct expect){l->wd, event, s->name};
|
|
return 1;
|
|
}
|
|
|
|
static void end(struct expect *x, int nx) {
|
|
pthread_mutex_lock(&lock);
|
|
pump(x, nx);
|
|
changing = 0;
|
|
pthread_mutex_unlock(&lock);
|
|
}
|
|
|
|
int bl_dircache_create(int dirfd, const char *path, int flags, mode_t mode,
|
|
int (*real)(int, const char *, int, mode_t)) __attribute__((visibility("hidden")));
|
|
int bl_dircache_create(int dirfd, const char *path, int flags, mode_t mode,
|
|
int (*real)(int, const char *, int, mode_t)) {
|
|
struct side s;
|
|
if (!(flags & O_CREAT) || (dirfd != AT_FDCWD && (path == NULL || path[0] != '/')) || !watching(path) || !split(path, &s))
|
|
return real(dirfd, path, flags, mode);
|
|
pthread_mutex_lock(&oplock);
|
|
begin();
|
|
before(&s);
|
|
int fd = real(dirfd, path, flags, mode);
|
|
int saved = errno;
|
|
struct expect x;
|
|
pthread_mutex_lock(&lock);
|
|
int nx = patch(&s, ADDED, IN_CREATE, fd >= 0, saved, path, &x);
|
|
pthread_mutex_unlock(&lock);
|
|
end(&x, nx);
|
|
pthread_mutex_unlock(&oplock);
|
|
errno = saved;
|
|
return fd;
|
|
}
|
|
|
|
#define ONE_PATH(name, what, event, call, ...) \
|
|
int name(const char *path, ##__VA_ARGS__) { \
|
|
NEXT(name); \
|
|
struct side s; \
|
|
if (!watching(path) || !split(path, &s)) return call; \
|
|
pthread_mutex_lock(&oplock); \
|
|
begin(); \
|
|
before(&s); \
|
|
int r = call; \
|
|
int saved = errno; \
|
|
struct expect x; \
|
|
pthread_mutex_lock(&lock); \
|
|
int nx = patch(&s, what, event, r == 0, saved, what == ADDED ? path : NULL, &x); \
|
|
if (r == 0 && what == REMOVED) forget_below(path); \
|
|
pthread_mutex_unlock(&lock); \
|
|
end(&x, nx); \
|
|
pthread_mutex_unlock(&oplock); \
|
|
errno = saved; \
|
|
return r; \
|
|
}
|
|
|
|
ONE_PATH(mkdir, ADDED, IN_CREATE, next_mkdir(path, mode), mode_t mode)
|
|
ONE_PATH(rmdir, REMOVED, IN_DELETE, next_rmdir(path))
|
|
ONE_PATH(unlink, REMOVED, IN_DELETE, next_unlink(path))
|
|
ONE_PATH(remove, REMOVED, IN_DELETE, next_remove(path))
|
|
|
|
#define TWO_PATHS(name, gone, event, call) \
|
|
int name(const char *from, const char *to) { \
|
|
NEXT(name); \
|
|
struct side a, b; \
|
|
int track_a = gone && watching(from) && split(from, &a), track_b = watching(to) && split(to, &b); \
|
|
if (!track_a && !track_b) return call(from, to); \
|
|
pthread_mutex_lock(&oplock); \
|
|
begin(); \
|
|
if (track_a) before(&a); \
|
|
if (track_b) before(&b); \
|
|
int r = call(from, to); \
|
|
int saved = errno; \
|
|
struct expect x[2]; \
|
|
int nx = 0; \
|
|
pthread_mutex_lock(&lock); \
|
|
if (track_a) nx += patch(&a, REMOVED, IN_MOVED_FROM, r == 0, saved, NULL, &x[nx]); \
|
|
if (track_b) { \
|
|
if (track_a && strcmp(a.dir, b.dir) == 0) b.known = stat(b.dir, &b.before) == 0; \
|
|
nx += patch(&b, ADDED, event, r == 0, saved, to, &x[nx]); \
|
|
} \
|
|
if (r == 0 && gone) { forget_below(from); forget_below(to); } \
|
|
pthread_mutex_unlock(&lock); \
|
|
end(x, nx); \
|
|
pthread_mutex_unlock(&oplock); \
|
|
errno = saved; \
|
|
return r; \
|
|
}
|
|
|
|
int bl_link_lock(const char *oldpath, const char *newpath, int result) __attribute__((visibility("hidden")));
|
|
static int link_or_lock(const char *from, const char *to) { return bl_link_lock(from, to, next_link(from, to)); }
|
|
|
|
TWO_PATHS(rename, 1, IN_MOVED_TO, next_rename)
|
|
TWO_PATHS(link, 0, IN_CREATE, link_or_lock)
|
|
TWO_PATHS(symlink, 0, IN_CREATE, next_symlink)
|
|
|
|
static int named(const struct listing *l, const char *name) {
|
|
for (size_t i = 0; i < l->n; i++)
|
|
if (strcasecmp(l->e[i].name, name) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int folded_match(const char *where, const char *dir) {
|
|
const char *at = strstr(where, "/steamapps");
|
|
if (at == NULL) return 0;
|
|
size_t n = (size_t)(at - where);
|
|
return strncmp(where, dir, n) == 0 && strcasecmp(where + n, dir + n) == 0;
|
|
}
|
|
|
|
static int same_dir(const struct listing *l, const char *dir) {
|
|
struct stat st;
|
|
int saved = errno;
|
|
int same = stat(dir, &st) == 0 && st.st_dev == l->dev && st.st_ino == l->ino;
|
|
errno = saved;
|
|
return same;
|
|
}
|
|
|
|
static int listed_absent(const char *dir, const char *name, long long now) {
|
|
for (int i = 0; i < MAX_LISTINGS; i++) {
|
|
struct listing *l = table[i];
|
|
if (l == NULL || l->where == NULL || l->blind || now - l->filled >= DIR_TTL_NS) continue;
|
|
if (strcmp(l->where, dir) == 0) return !named(l, name);
|
|
if (l->folds && folded_match(l->where, dir)) return same_dir(l, dir) && !named(l, name);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int bl_dircache_absent(const char *path) __attribute__((visibility("hidden")));
|
|
int bl_dircache_absent(const char *path) {
|
|
struct side s;
|
|
if (!watching(path) || !split(path, &s)) return 0;
|
|
for (const char *c = s.name; *c; c++)
|
|
if ((unsigned char)*c >= 0x80) return 0;
|
|
long long now = now_ns();
|
|
pthread_mutex_lock(&lock);
|
|
int absent = 0;
|
|
if (!changing) {
|
|
pump(NULL, 0);
|
|
absent = listed_absent(s.dir, s.name, now);
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
return absent;
|
|
}
|
|
|
|
#else
|
|
|
|
int bl_dircache_absent(const char *path) __attribute__((visibility("hidden")));
|
|
int bl_dircache_absent(const char *path) {
|
|
(void)path;
|
|
return 0;
|
|
}
|
|
|
|
int bl_dircache_create(int dirfd, const char *path, int flags, mode_t mode,
|
|
int (*real)(int, const char *, int, mode_t)) __attribute__((visibility("hidden")));
|
|
int bl_dircache_create(int dirfd, const char *path, int flags, mode_t mode,
|
|
int (*real)(int, const char *, int, mode_t)) {
|
|
return real(dirfd, path, flags, mode);
|
|
}
|
|
|
|
#endif
|