711 lines
18 KiB
C
Raw Normal View History

2025-12-28 03:14:31 +01:00
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
2025-12-28 03:14:31 +01:00
* Utility functions implementation
*/
#include "util.h"
#include "dwn.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <pwd.h>
#include <pthread.h>
#include <stdatomic.h>
#include <fcntl.h>
/* ========== Async Logging Configuration ========== */
#define LOG_RING_SIZE 256 /* Number of log entries in ring buffer */
#define LOG_MSG_MAX_LEN 512 /* Max length of a single log message */
#define LOG_MAX_FILE_SIZE (5 * 1024 * 1024) /* 5 MB max log file size */
#define LOG_FLUSH_INTERVAL_MS 100 /* Flush to disk every 100ms */
/* Log entry in ring buffer */
typedef struct {
char message[LOG_MSG_MAX_LEN];
LogLevel level;
_Atomic int ready; /* 0 = empty, 1 = ready to write */
} LogEntry;
/* Static state for async logging */
static LogEntry log_ring[LOG_RING_SIZE];
static _Atomic size_t log_write_idx = 0; /* Next slot to write to */
static _Atomic size_t log_read_idx = 0; /* Next slot to read from */
static _Atomic int log_running = 0; /* Log thread running flag */
static pthread_t log_thread;
static int log_fd = -1; /* File descriptor for log file */
static char *log_path = NULL; /* Path to log file */
static LogLevel min_level = LOG_INFO;
static _Atomic size_t log_file_size = 0; /* Current log file size */
static const char *level_names[] = {
"DEBUG",
"INFO",
"WARN",
"ERROR"
};
static const char *level_colors[] = {
"\033[36m", /* Cyan for DEBUG */
"\033[32m", /* Green for INFO */
"\033[33m", /* Yellow for WARN */
"\033[31m" /* Red for ERROR */
};
/* Forward declarations for internal log functions */
static void log_rotate_if_needed(void);
static void *log_writer_thread(void *arg);
/* ========== Async Logging Implementation ========== */
/* Rotate log file if it exceeds max size */
static void log_rotate_if_needed(void)
{
if (log_fd < 0 || log_path == NULL) {
return;
}
size_t current_size = atomic_load(&log_file_size);
if (current_size < LOG_MAX_FILE_SIZE) {
return;
}
/* Close current file */
close(log_fd);
log_fd = -1;
/* Create backup filename */
size_t path_len = strlen(log_path);
char *backup_path = malloc(path_len + 8);
if (backup_path != NULL) {
snprintf(backup_path, path_len + 8, "%s.old", log_path);
/* Remove old backup if exists, rename current to backup */
unlink(backup_path);
rename(log_path, backup_path);
free(backup_path);
}
/* Reopen fresh log file */
log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
atomic_store(&log_file_size, 0);
}
/* Background thread that writes log entries to file */
static void *log_writer_thread(void *arg)
{
(void)arg;
while (atomic_load(&log_running)) {
size_t read_idx = atomic_load(&log_read_idx);
size_t write_idx = atomic_load(&log_write_idx);
/* Process all available entries */
while (read_idx != write_idx) {
size_t idx = read_idx % LOG_RING_SIZE;
LogEntry *entry = &log_ring[idx];
/* Wait for entry to be ready (spin briefly) */
int attempts = 0;
while (!atomic_load(&entry->ready) && attempts < 100) {
attempts++;
/* Brief yield */
struct timespec ts = {0, 1000}; /* 1 microsecond */
nanosleep(&ts, NULL);
}
if (atomic_load(&entry->ready)) {
/* Write to file if open */
if (log_fd >= 0) {
log_rotate_if_needed();
if (log_fd >= 0) {
ssize_t written = write(log_fd, entry->message, strlen(entry->message));
if (written > 0) {
atomic_fetch_add(&log_file_size, (size_t)written);
}
}
}
/* Mark entry as consumed */
atomic_store(&entry->ready, 0);
}
read_idx++;
atomic_store(&log_read_idx, read_idx);
}
/* Sleep before next check */
struct timespec ts = {0, LOG_FLUSH_INTERVAL_MS * 1000000L};
nanosleep(&ts, NULL);
}
return NULL;
}
void log_init(const char *path)
{
/* Initialize ring buffer */
memset(log_ring, 0, sizeof(log_ring));
atomic_store(&log_write_idx, 0);
atomic_store(&log_read_idx, 0);
if (path != NULL) {
char *expanded = expand_path(path);
if (expanded != NULL) {
/* Ensure directory exists */
char *dir = strdup(expanded);
if (dir != NULL) {
char *last_slash = strrchr(dir, '/');
if (last_slash != NULL) {
*last_slash = '\0';
/* Create directory recursively using mkdir */
struct stat st;
if (stat(dir, &st) != 0) {
/* Directory doesn't exist, try to create it */
char cmd[512];
snprintf(cmd, sizeof(cmd), "mkdir -p '%s' 2>/dev/null", dir);
int ret = system(cmd);
(void)ret; /* Ignore result - file open will fail if dir creation fails */
}
}
free(dir);
}
/* Open log file with O_APPEND for atomic writes */
log_fd = open(expanded, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
if (log_fd < 0) {
fprintf(stderr, "Warning: Could not open log file: %s\n", expanded);
} else {
/* Get current file size */
struct stat st;
if (fstat(log_fd, &st) == 0) {
atomic_store(&log_file_size, (size_t)st.st_size);
}
}
log_path = expanded; /* Keep for rotation */
}
}
/* Start background writer thread */
atomic_store(&log_running, 1);
if (pthread_create(&log_thread, NULL, log_writer_thread, NULL) != 0) {
fprintf(stderr, "Warning: Could not create log writer thread\n");
atomic_store(&log_running, 0);
}
}
void log_close(void)
{
/* Signal thread to stop */
if (!atomic_load(&log_running)) {
goto cleanup;
}
atomic_store(&log_running, 0);
/* Wait for thread to finish - give it time to flush */
pthread_join(log_thread, NULL);
cleanup:
/* Close file */
if (log_fd >= 0) {
close(log_fd);
log_fd = -1;
}
/* Free path */
if (log_path != NULL) {
free(log_path);
log_path = NULL;
}
}
void log_set_level(LogLevel level)
{
min_level = level;
}
void log_flush(void)
{
/* Force flush all pending log entries synchronously */
if (!atomic_load(&log_running) || log_fd < 0) {
return;
}
size_t read_idx = atomic_load(&log_read_idx);
size_t write_idx = atomic_load(&log_write_idx);
while (read_idx != write_idx) {
size_t idx = read_idx % LOG_RING_SIZE;
LogEntry *entry = &log_ring[idx];
if (atomic_load(&entry->ready)) {
ssize_t written = write(log_fd, entry->message, strlen(entry->message));
if (written > 0) {
atomic_fetch_add(&log_file_size, (size_t)written);
}
atomic_store(&entry->ready, 0);
}
read_idx++;
atomic_store(&log_read_idx, read_idx);
}
/* Sync to disk */
fsync(log_fd);
}
void log_msg(LogLevel level, const char *fmt, ...)
{
if (level < min_level) {
return;
}
/* Get timestamp */
time_t now = time(NULL);
struct tm tm_info;
localtime_r(&now, &tm_info); /* Thread-safe version */
char time_buf[32];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_info);
/* Format the log message */
char msg_buf[LOG_MSG_MAX_LEN - 64]; /* Leave room for prefix */
va_list args;
va_start(args, fmt);
vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
va_end(args);
/* Print to stderr (always, for immediate visibility) */
fprintf(stderr, "%s[%s] %s: %s%s\n",
level_colors[level], time_buf, level_names[level], "\033[0m", msg_buf);
/* Add to ring buffer for async file write (non-blocking) */
if (atomic_load(&log_running)) {
size_t write_idx = atomic_fetch_add(&log_write_idx, 1);
size_t idx = write_idx % LOG_RING_SIZE;
LogEntry *entry = &log_ring[idx];
/* Check if slot is available (not overwriting unread entry) */
/* If buffer is full, we drop the message rather than block */
if (!atomic_load(&entry->ready)) {
entry->level = level;
snprintf(entry->message, sizeof(entry->message),
"[%s] %s: %s\n", time_buf, level_names[level], msg_buf);
atomic_store(&entry->ready, 1);
}
/* If entry->ready is true, buffer is full - message is dropped (non-blocking) */
}
}
/* ========== Memory allocation ========== */
void *dwn_malloc(size_t size)
{
void *ptr = malloc(size);
if (ptr == NULL && size > 0) {
LOG_ERROR("Memory allocation failed for %zu bytes", size);
exit(EXIT_FAILURE);
}
return ptr;
}
void *dwn_calloc(size_t nmemb, size_t size)
{
void *ptr = calloc(nmemb, size);
if (ptr == NULL && nmemb > 0 && size > 0) {
LOG_ERROR("Memory allocation failed for %zu elements", nmemb);
exit(EXIT_FAILURE);
}
return ptr;
}
void *dwn_realloc(void *ptr, size_t size)
{
void *new_ptr = realloc(ptr, size);
if (new_ptr == NULL && size > 0) {
LOG_ERROR("Memory reallocation failed for %zu bytes", size);
exit(EXIT_FAILURE);
}
return new_ptr;
}
char *dwn_strdup(const char *s)
{
if (s == NULL) {
return NULL;
}
char *dup = strdup(s);
if (dup == NULL) {
LOG_ERROR("String duplication failed");
exit(EXIT_FAILURE);
}
return dup;
}
void dwn_free(void *ptr)
{
free(ptr);
}
void secure_wipe(void *ptr, size_t size)
{
if (ptr == NULL || size == 0) {
return;
}
#if defined(__GLIBC__) && (__GLIBC__ >= 2) && (__GLIBC_MINOR__ >= 25)
/* Use explicit_bzero if available (glibc 2.25+) */
explicit_bzero(ptr, size);
#else
/* Fallback: Use volatile to prevent compiler optimization */
volatile unsigned char *p = (volatile unsigned char *)ptr;
while (size--) {
*p++ = 0;
}
#endif
}
/* ========== String utilities ========== */
char *str_trim(char *str)
{
if (str == NULL) {
return NULL;
}
/* Trim leading whitespace */
while (isspace((unsigned char)*str)) {
str++;
}
if (*str == '\0') {
return str;
}
/* Trim trailing whitespace */
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) {
end--;
}
end[1] = '\0';
return str;
}
bool str_starts_with(const char *str, const char *prefix)
{
if (str == NULL || prefix == NULL) {
return false;
}
return strncmp(str, prefix, strlen(prefix)) == 0;
}
bool str_ends_with(const char *str, const char *suffix)
{
if (str == NULL || suffix == NULL) {
return false;
}
size_t str_len = strlen(str);
size_t suffix_len = strlen(suffix);
if (suffix_len > str_len) {
return false;
}
return strcmp(str + str_len - suffix_len, suffix) == 0;
}
int str_split(char *str, char delim, char **parts, int max_parts)
{
if (str == NULL || parts == NULL || max_parts <= 0) {
return 0;
}
int count = 0;
char *start = str;
while (*str && count < max_parts) {
if (*str == delim) {
*str = '\0';
parts[count++] = start;
start = str + 1;
}
str++;
}
if (count < max_parts && *start) {
parts[count++] = start;
}
return count;
}
char *shell_escape(const char *str)
{
if (str == NULL) {
return dwn_strdup("");
}
/* Count single quotes to determine buffer size */
size_t quotes = 0;
for (const char *p = str; *p; p++) {
if (*p == '\'') quotes++;
}
/* Each single quote becomes '\'' (4 chars), plus 2 for surrounding quotes */
size_t len = strlen(str) + quotes * 3 + 3;
char *escaped = dwn_malloc(len);
char *dst = escaped;
*dst++ = '\'';
for (const char *src = str; *src; src++) {
if (*src == '\'') {
/* Close quote, add escaped quote, reopen quote: '\'' */
*dst++ = '\'';
*dst++ = '\\';
*dst++ = '\'';
*dst++ = '\'';
} else {
*dst++ = *src;
}
}
*dst++ = '\'';
*dst = '\0';
return escaped;
}
/* ========== File utilities ========== */
bool file_exists(const char *path)
{
if (path == NULL) {
return false;
}
struct stat st;
return stat(path, &st) == 0;
}
char *file_read_all(const char *path)
{
FILE *f = fopen(path, "r");
if (f == NULL) {
return NULL;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *content = dwn_malloc(size + 1);
size_t read = fread(content, 1, size, f);
content[read] = '\0';
fclose(f);
return content;
}
bool file_write_all(const char *path, const char *content)
{
FILE *f = fopen(path, "w");
if (f == NULL) {
return false;
}
size_t len = strlen(content);
size_t written = fwrite(content, 1, len, f);
fclose(f);
return written == len;
}
char *expand_path(const char *path)
{
if (path == NULL) {
return NULL;
}
if (path[0] == '~') {
const char *home = getenv("HOME");
if (home == NULL) {
struct passwd *pw = getpwuid(getuid());
if (pw != NULL) {
home = pw->pw_dir;
}
}
if (home != NULL) {
size_t len = strlen(home) + strlen(path);
char *expanded = dwn_malloc(len);
snprintf(expanded, len, "%s%s", home, path + 1);
return expanded;
}
}
return dwn_strdup(path);
}
/* ========== Color utilities ========== */
unsigned long parse_color(const char *color_str)
{
if (color_str == NULL || dwn == NULL || dwn->display == NULL) {
return 0;
}
XColor color, exact;
if (XAllocNamedColor(dwn->display, dwn->colormap, color_str, &color, &exact)) {
return color.pixel;
}
/* Try parsing as hex */
if (color_str[0] == '#') {
unsigned int r, g, b;
if (sscanf(color_str + 1, "%02x%02x%02x", &r, &g, &b) == 3) {
color.red = r << 8;
color.green = g << 8;
color.blue = b << 8;
color.flags = DoRed | DoGreen | DoBlue;
if (XAllocColor(dwn->display, dwn->colormap, &color)) {
return color.pixel;
}
}
}
LOG_WARN("Could not parse color: %s", color_str);
return 0;
}
void color_to_rgb(unsigned long color, int *r, int *g, int *b)
{
XColor xc;
xc.pixel = color;
XQueryColor(dwn->display, dwn->colormap, &xc);
*r = xc.red >> 8;
*g = xc.green >> 8;
*b = xc.blue >> 8;
}
/* ========== Time utilities ========== */
long get_time_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
void sleep_ms(int ms)
{
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000L;
nanosleep(&ts, NULL);
}
/* ========== Process utilities ========== */
int spawn(const char *cmd)
{
if (cmd == NULL) {
return -1;
}
LOG_DEBUG("Spawning: %s", cmd);
pid_t pid = fork();
if (pid == 0) {
/* Child process */
setsid();
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(EXIT_FAILURE);
} else if (pid < 0) {
LOG_ERROR("Fork failed: %s", strerror(errno));
return -1;
}
/* Wait for child */
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
int spawn_async(const char *cmd)
{
if (cmd == NULL) {
return -1;
}
LOG_DEBUG("Spawning async: %s", cmd);
pid_t pid = fork();
if (pid == 0) {
/* Child process */
setsid();
/* Double fork to avoid zombies */
pid_t pid2 = fork();
if (pid2 == 0) {
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
} else if (pid < 0) {
LOG_ERROR("Fork failed: %s", strerror(errno));
return -1;
}
/* Wait for first child (which exits immediately) */
int status;
waitpid(pid, &status, 0);
return 0;
}
char *spawn_capture(const char *cmd)
{
if (cmd == NULL) {
return NULL;
}
LOG_DEBUG("Spawning capture: %s", cmd);
FILE *fp = popen(cmd, "r");
if (fp == NULL) {
LOG_ERROR("popen failed: %s", strerror(errno));
return NULL;
}
/* Read output */
size_t buf_size = 1024;
size_t len = 0;
char *output = dwn_malloc(buf_size);
output[0] = '\0';
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
size_t line_len = strlen(line);
if (len + line_len + 1 > buf_size) {
buf_size *= 2;
output = dwn_realloc(output, buf_size);
}
/* Use memcpy with explicit bounds instead of strcpy */
memcpy(output + len, line, line_len + 1);
len += line_len;
}
int status = pclose(fp);
if (status != 0) {
LOG_DEBUG("Command exited with status %d", status);
}
/* Trim trailing newline */
if (len > 0 && output[len - 1] == '\n') {
output[len - 1] = '\0';
}
return output;
}