diff options
Diffstat (limited to 'src/main/exfile.c')
-rw-r--r-- | src/main/exfile.c | 544 |
1 files changed, 544 insertions, 0 deletions
diff --git a/src/main/exfile.c b/src/main/exfile.c new file mode 100644 index 0000000..529524d --- /dev/null +++ b/src/main/exfile.c @@ -0,0 +1,544 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* + * $Id$ + * + * @file exfile.c + * @brief Allow multiple threads to write to the same set of files. + * + * @author Alan DeKok <aland@freeradius.org> + * @copyright 2014 The FreeRADIUS server project + */ +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/exfile.h> + +#include <sys/stat.h> +#include <fcntl.h> + +typedef struct exfile_entry_t { + int fd; //!< File descriptor associated with an entry. + uint32_t hash; //!< Hash for cheap comparison. + time_t last_used; //!< Last time the entry was used. + dev_t st_dev; //!< device inode + ino_t st_ino; //!< inode number + char *filename; //!< Filename. +} exfile_entry_t; + + +struct exfile_t { + uint32_t max_entries; //!< How many file descriptors we keep track of. + uint32_t max_idle; //!< Maximum idle time for a descriptor. + time_t last_cleaned; + +#ifdef HAVE_PTHREAD_H + pthread_mutex_t mutex; +#endif + exfile_entry_t *entries; + bool locking; +}; + + +#ifdef HAVE_PTHREAD_H +#define PTHREAD_MUTEX_LOCK pthread_mutex_lock +#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock + +#else +/* + * This is easier than ifdef's throughout the code. + */ +#define PTHREAD_MUTEX_LOCK(_x) +#define PTHREAD_MUTEX_UNLOCK(_x) +#endif + +#define MAX_TRY_LOCK 4 //!< How many times we attempt to acquire a lock + //!< before giving up. + +static int _exfile_free(exfile_t *ef) +{ + uint32_t i; + + PTHREAD_MUTEX_LOCK(&ef->mutex); + + for (i = 0; i < ef->max_entries; i++) { + if (!ef->entries[i].filename) continue; + + close(ef->entries[i].fd); + } + + PTHREAD_MUTEX_UNLOCK(&ef->mutex); + +#ifdef HAVE_PTHREAD_H + pthread_mutex_destroy(&ef->mutex); +#endif + + return 0; +} + + +/** Initialize a way for multiple threads to log to one or more files. + * + * @param ctx The talloc context + * @param max_entries Max file descriptors to cache, and manage locks for. + * @param max_idle Maximum time a file descriptor can be idle before it's closed. + * @param locking whether or not to lock the files. + * @return the new context, or NULL on error. + */ +exfile_t *exfile_init(TALLOC_CTX *ctx, uint32_t max_entries, uint32_t max_idle, bool locking) +{ + exfile_t *ef; + + ef = talloc_zero(ctx, exfile_t); + if (!ef) return NULL; + + ef->max_entries = max_entries; + ef->max_idle = max_idle; + ef->locking = locking; + + /* + * If we're not locking the files, just return the + * handle. Each call to exfile_open() will just open a + * new file descriptor. + */ + if (!locking) return ef; + + ef->entries = talloc_zero_array(ef, exfile_entry_t, max_entries); + if (!ef->entries) { + talloc_free(ef); + return NULL; + } + +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&ef->mutex, NULL) != 0) { + talloc_free(ef); + return NULL; + } +#endif + + talloc_set_destructor(ef, _exfile_free); + + return ef; +} + + +static void exfile_cleanup_entry(exfile_entry_t *entry) +{ + TALLOC_FREE(entry->filename); + + if (entry->fd >= 0) close(entry->fd); + entry->hash = 0; + entry->fd = -1; +} + + +/* + * Try to open the file. If it doesn't exist, try to + * create it's parent directories. + */ +static int exfile_open_mkdir(exfile_t *ef, char const *filename, mode_t permissions) +{ + int fd; + + /* + * Files in /dev/ are special. We don't try to create + * their parent directories, and we don't try to create + * the files. + */ + if (strncmp(filename, "/dev/", 5) == 0) { + int oflag; + + if (((permissions & 0222) == 0) && (permissions & 0444) != 0) { /* !W + R */ + oflag = O_RDONLY; + + } else if (((permissions & 0222) != 0) && (permissions & 0444) == 0) { /* W + !R */ + oflag = O_WRONLY; + + } else { /* unknown, make it R+W */ + oflag = O_RDWR; + } + + fd = open(filename, oflag, permissions); + if (fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", + filename, strerror(errno)); + return -1; + } + + return fd; + } + + fd = open(filename, O_RDWR | O_CREAT, permissions); + if (fd < 0) { + mode_t dirperm; + char *p, *dir; + + /* + * Maybe the directory doesn't exist. Try to + * create it. + */ + dir = talloc_strdup(ef, filename); + if (!dir) return -1; + p = strrchr(dir, FR_DIR_SEP); + if (!p) { + fr_strerror_printf("No '/' in '%s'", filename); + talloc_free(dir); + return -1; + } + *p = '\0'; + + /* + * Ensure that the 'x' bit is set, so that we can + * read the directory. + */ + dirperm = permissions; + if ((dirperm & 0600) != 0) dirperm |= 0100; + if ((dirperm & 0060) != 0) dirperm |= 0010; + if ((dirperm & 0006) != 0) dirperm |= 0001; + + if (rad_mkdir(dir, dirperm, -1, -1) < 0) { + fr_strerror_printf("Failed to create directory %s: %s", + dir, strerror(errno)); + talloc_free(dir); + return -1; + } + talloc_free(dir); + + fd = open(filename, O_RDWR | O_CREAT, permissions); + if (fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", + filename, strerror(errno)); + return -1; + } + } + + return fd; +} + + +/** Open a new log file, or maybe an existing one. + * + * When multithreaded, the FD is locked via a mutex. This way we're + * sure that no other thread is writing to the file. + * + * @param ef The logfile context returned from exfile_init(). + * @param filename the file to open. + * @param permissions to use. + * @return an FD used to write to the file, or -1 on error. + */ +int exfile_open(exfile_t *ef, char const *filename, mode_t permissions) +{ + int i, found, tries, unused, oldest; + uint32_t hash; + time_t now; + struct stat st; + + if (!ef || !filename) return -1; + + /* + * No locking: just return a new FD. + */ + if (!ef->locking) { + found = exfile_open_mkdir(ef, filename, permissions); + if (found < 0) return -1; + + (void) lseek(found, 0, SEEK_END); + return found; + } + + /* + * It's faster to do hash comparisons of a string than + * full string comparisons. + */ + hash = fr_hash_string(filename); + now = time(NULL); + + PTHREAD_MUTEX_LOCK(&ef->mutex); + + /* + * Clean up idle entries. + */ + if (now > (ef->last_cleaned + 1)) { + ef->last_cleaned = now; + + for (i = 0; i < (int) ef->max_entries; i++) { + if (!ef->entries[i].filename) continue; + + if ((ef->entries[i].last_used + ef->max_idle) >= now) continue; + + /* + * This will block forever if a thread is + * doing something stupid. + */ + exfile_cleanup_entry(&ef->entries[i]); + } + } + + /* + * Find the matching entry, or an unused one. + * + * Also track which entry is the oldest, in case there + * are no unused entries. + */ + found = oldest = unused = -1; + for (i = 0; i < (int) ef->max_entries; i++) { + if (!ef->entries[i].filename) { + if (unused < 0) unused = i; + continue; + } + + if ((oldest < 0) || + (ef->entries[i].last_used < ef->entries[oldest].last_used)) { + oldest = i; + } + + /* + * Hash comparisons are fast. String comparisons are slow. + */ + if (ef->entries[i].hash != hash) continue; + + /* + * But we still need to do string comparisons if + * the hash matches, because 1/2^16 filenames + * will result in a hash collision. And that's + * enough filenames in a long-running server to + * ensure that it happens. + */ + if (strcmp(ef->entries[i].filename, filename) != 0) continue; + + found = i; + break; + } + + /* + * If it wasn't found, create a new entry. + */ + if (found < 0) { + /* + * There are no unused entries. Clean up the + * oldest one. + */ + if (unused < 0) { + exfile_cleanup_entry(&ef->entries[oldest]); + unused = oldest; + } + + /* + * Create a new entry. + */ + i = unused; + + ef->entries[i].hash = hash; + ef->entries[i].filename = talloc_strdup(ef->entries, filename); + ef->entries[i].fd = -1; + + /* + * We've just created the entry. Open the file + * and cache the FD. + */ + reopen: + ef->entries[i].fd = exfile_open_mkdir(ef, filename, permissions); + if (ef->entries[i].fd < 0) { + error: + exfile_cleanup_entry(&ef->entries[i]); + PTHREAD_MUTEX_UNLOCK(&(ef->mutex)); + return -1; + } + + if (fstat(ef->entries[i].fd, &st) < 0) goto error; + + /* + * Remember which device and inode this file is + * for. + */ + ef->entries[i].st_dev = st.st_dev; + ef->entries[i].st_ino = st.st_ino; + + } else { + i = found; + + /* + * Stat the *filename*, not the file we opened. + * If that's not the file we opened, then go back + * and re-open the file. + */ + if (stat(ef->entries[i].filename, &st) == 0) { + if ((st.st_dev != ef->entries[i].st_dev) || + (st.st_ino != ef->entries[i].st_ino)) { + /* + * No longer the same file; reopen. + */ + close(ef->entries[i].fd); + goto reopen; + } + } else { + /* + * Error calling stat, likely the + * file has been moved. Reopen it. + */ + close(ef->entries[i].fd); + goto reopen; + } + } + + /* + * Try to lock it. If we can't lock it, it's because + * some reader has re-named the file to "foo.work" and + * locked it. So, we close the current file, re-open it, + * and try again. + */ + + /* + * Lock from the start of the file. It's the + * only point in the file which is guaranteed to + * exist, and to be consistent across all threads + * and processes. + */ + if (lseek(ef->entries[i].fd, 0, SEEK_SET) < 0) { + fr_strerror_printf("Failed to seek in file %s: %s", filename, strerror(errno)); + goto error; + } + + /* + * Busy-loop trying to lock the file. + */ + for (tries = 0; tries < MAX_TRY_LOCK; tries++) { + if (rad_lockfd_nonblock(ef->entries[i].fd, 0) >= 0) break; + + if (errno != EAGAIN) { + fr_strerror_printf("Failed to lock file %s: %s", filename, strerror(errno)); + goto error; + } + + /* + * Close the file and re-open it. It may + * have been deleted. If it was deleted, + * then the new file should now be unlocked. + */ + close(ef->entries[i].fd); + ef->entries[i].fd = open(filename, O_RDWR | O_CREAT, permissions); + if (ef->entries[i].fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", + filename, strerror(errno)); + goto error; + } + } + + if (tries >= MAX_TRY_LOCK) { + fr_strerror_printf("Failed to lock file %s: too many tries", filename); + goto error; + } + + /* + * See which file it really is. + */ + if (fstat(ef->entries[i].fd, &st) < 0) { + fr_strerror_printf("Failed to stat file %s: %s", filename, strerror(errno)); + goto error; + } + + /* + * Maybe the file was unlinked from the file system, OR + * the file we opened is NOT the one we had cached. If + * so, close the file and re-open it from scratch. + */ + if ((st.st_nlink == 0) || + (st.st_dev != ef->entries[i].st_dev) || + (st.st_ino != ef->entries[i].st_ino)) { + close(ef->entries[i].fd); + goto reopen; + } + + /* + * Sometimes the file permissions are changed externally. + * just be sure to update the permission if necessary. + */ + if ((st.st_mode & ~S_IFMT) != permissions) { + char str_need[10], oct_need[5]; + char str_have[10], oct_have[5]; + + rad_mode_to_oct(oct_need, permissions); + rad_mode_to_str(str_need, permissions); + + rad_mode_to_oct(oct_have, st.st_mode & ~S_IFMT); + rad_mode_to_str(str_have, st.st_mode & ~S_IFMT); + + WARN("File %s permissions are %s (%s) not %s (%s))", filename, + oct_have, str_have, oct_need, str_need); + + if (((st.st_mode | permissions) != st.st_mode) && + (fchmod(ef->entries[i].fd, (st.st_mode & ~S_IFMT) | permissions) < 0)) { + rad_mode_to_oct(oct_need, (st.st_mode & ~S_IFMT) | permissions); + rad_mode_to_str(str_need, (st.st_mode & ~S_IFMT) | permissions); + + WARN("Failed resetting file %s permissions to %s (%s): %s", + filename, oct_need, str_need, fr_syserror(errno)); + } + } + + /* + * If we're appending, seek to the end of the file before + * returning the FD to the caller. + */ + (void) lseek(ef->entries[i].fd, 0, SEEK_END); + + /* + * Return holding the mutex for the entry. + */ + ef->entries[i].last_used = now; + + return ef->entries[i].fd; +} + +/** Close the log file. Really just return it to the pool. + * + * When multithreaded, the FD is locked via a mutex. This way we're + * sure that no other thread is writing to the file. This function + * will unlock the mutex, so that other threads can write to the file. + * + * @param ef The logfile context returned from exfile_init() + * @param fd the FD to close (i.e. return to the pool) + * @return 0 on success, or -1 on error + */ +int exfile_close(exfile_t *ef, int fd) +{ + uint32_t i; + + /* + * No locking: just close the file. + */ + if (!ef->locking) { + close(fd); + return 0; + } + + /* + * Unlock the bytes that we had previously locked. + */ + for (i = 0; i < ef->max_entries; i++) { + if (ef->entries[i].fd == fd) { + (void) lseek(ef->entries[i].fd, 0, SEEK_SET); + (void) rad_unlockfd(ef->entries[i].fd, 0); + + PTHREAD_MUTEX_UNLOCK(&(ef->mutex)); + return 0; + } + } + + PTHREAD_MUTEX_UNLOCK(&(ef->mutex)); + + fr_strerror_printf("Attempt to unlock file which is not tracked"); + return -1; +} |