From d34afc088700c3d38b5db7b1c84ca1d1a94827a6 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 7 Oct 2025 22:11:59 +0545 Subject: [PATCH] feat: implement daemon mode with config auto-reload and CLI reload command --- Makefile | 3 +- keyvibe.c | 349 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 263 insertions(+), 89 deletions(-) diff --git a/Makefile b/Makefile index 7fc21f4..86e7ca0 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ + CC = gcc CFLAGS = -Wall -Wextra -std=c99 PREFIX ?= /usr @@ -28,7 +29,7 @@ all: $(KeyVibe_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET) # Build main launcher (needs json-c) $(KeyVibe_TARGET): $(KeyVibe_SOURCE) - $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< -ljson-c + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< -ljson-c -lpthread $(SOUND_TARGET): $(SOUND_SOURCE) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(LDFLAGS_SOUND) diff --git a/keyvibe.c b/keyvibe.c index ea9e50d..649d34c 100644 --- a/keyvibe.c +++ b/keyvibe.c @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -27,6 +29,12 @@ pid_t keyboard_pid = 0; pid_t sound_pid = 0; static char pidfile_path[MAX_PATH_LENGTH] = {0}; static int is_daemon = 0; +static volatile sig_atomic_t reload_requested = 0; +static int current_volume = 50; +static char current_sound_name[MAX_PATH_LENGTH] = {0}; +static int current_verbose = 0; +static char current_config_path[MAX_PATH_LENGTH] = {0}; +static char current_sound_dir[MAX_PATH_LENGTH] = {0}; void print_usage(const char *program_name) { printf("KeyVibe - Mechanical Keyboard Sound Simulator\n\n"); @@ -38,11 +46,13 @@ void print_usage(const char *program_name) { printf(" -l, --list List available sound packs\n"); printf(" --daemon Run in background (write PID file)\n"); printf(" --stop Stop background daemon\n"); + printf(" --reload Send reload signal to running daemon\n"); printf(" -h, --help Show this help message\n"); printf(" -v, --verbose Enable verbose output\n"); printf("\nConfiguration:\n"); printf(" Reads ~/.keyvibe.json if present. On first run, prompts to create " "it.\n"); + printf(" In daemon mode, editing ~/.keyvibe.json will auto-reload.\n"); printf("\nExamples:\n"); printf(" %s # Use default sound (eg-oreo)\n", program_name); @@ -182,6 +192,29 @@ static const char *get_runtime_dir() { return "/tmp"; } +static int build_paths_for_sound(const char *sound_name, char *out_config_path, + size_t out_config_sz, char *out_sound_dir, + size_t out_sound_sz) { + size_t base_len = strlen(AUDIO_BASE_DIR); + size_t name_len = strlen(sound_name); + const char *cfg_suffix = "/config.json"; + size_t cfg_suffix_len = strlen(cfg_suffix); + // Need: base + '/' + name + cfg_suffix + null + if (base_len + 1 + name_len + cfg_suffix_len + 1 > out_config_sz) { + fprintf(stderr, "Error: sound pack name too long for path buffer.\n"); + return 0; + } + // Need: base + '/' + name + null + if (base_len + 1 + name_len + 1 > out_sound_sz) { + fprintf(stderr, "Error: sound pack name too long for directory buffer.\n"); + return 0; + } + snprintf(out_config_path, out_config_sz, "%s/%s%s", AUDIO_BASE_DIR, + sound_name, cfg_suffix); + snprintf(out_sound_dir, out_sound_sz, "%s/%s", AUDIO_BASE_DIR, sound_name); + return 1; +} + static void build_pidfile_path(char *buffer, size_t buflen) { const char *rd = get_runtime_dir(); snprintf(buffer, buflen, "%s/keyvibe-%d.pid", rd, (int)getuid()); @@ -247,6 +280,124 @@ static void daemonize_self() { } } +static void stop_children() { + if (sound_pid > 0) { + kill(sound_pid, SIGTERM); + waitpid(sound_pid, NULL, 0); + sound_pid = 0; + } + if (keyboard_pid > 0) { + kill(keyboard_pid, SIGTERM); + waitpid(keyboard_pid, NULL, 0); + keyboard_pid = 0; + } +} + +static int start_children(const char *sound_dir, const char *config_path, + int volume, int verbose) { + (void)config_path; + int pipefd[2]; + if (pipe(pipefd) == -1) { + perror("pipe"); + return 0; + } + + // Fork sound player first + sound_pid = fork(); + if (sound_pid == -1) { + perror("fork"); + close(pipefd[0]); + close(pipefd[1]); + return 0; + } + + char sound_player_path[MAX_PATH_LENGTH]; + snprintf(sound_player_path, sizeof(sound_player_path), + "%s/keyboard_sound_player", KeyVibe_BIN_DIR); + + if (sound_pid == 0) { + close(pipefd[1]); + dup2(pipefd[0], STDIN_FILENO); + close(pipefd[0]); + if (chdir(sound_dir) != 0) { + perror("chdir"); + exit(1); + } + char volume_str[32]; + snprintf(volume_str, sizeof(volume_str), "%d", volume); + char verbose_str[8]; + snprintf(verbose_str, sizeof(verbose_str), "%d", verbose); + execl(sound_player_path, "keyboard_sound_player", "config.json", volume_str, + verbose_str, (char *)NULL); + perror("execl keyboard_sound_player"); + exit(1); + } + + // Keyboard listener + keyboard_pid = fork(); + if (keyboard_pid == -1) { + perror("fork"); + kill(sound_pid, SIGTERM); + close(pipefd[0]); + close(pipefd[1]); + return 0; + } + + char get_key_presses_path[MAX_PATH_LENGTH]; + snprintf(get_key_presses_path, sizeof(get_key_presses_path), + "%s/get_key_presses", KeyVibe_BIN_DIR); + + if (keyboard_pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + execl(get_key_presses_path, "get_key_presses", (char *)NULL); + perror("execl get_key_presses"); + exit(1); + } + + close(pipefd[0]); + close(pipefd[1]); + return 1; +} + +static void handle_sighup(int sig) { + (void)sig; + reload_requested = 1; +} + +struct inotify_thread_args { + char path[MAX_PATH_LENGTH]; +}; + +static void *inotify_thread_fn(void *arg) { + struct inotify_thread_args *a = (struct inotify_thread_args *)arg; + int fd = inotify_init1(IN_NONBLOCK); + if (fd < 0) + return NULL; + int wd = + inotify_add_watch(fd, a->path, IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB); + if (wd < 0) { + close(fd); + return NULL; + } + char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); + while (1) { + ssize_t len = read(fd, buf, sizeof(buf)); + if (len <= 0) { + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 200000000L; + nanosleep(&ts, NULL); + continue; + } + reload_requested = 1; + } + inotify_rm_watch(fd, wd); + close(fd); + return NULL; +} + static int write_user_config(const char *path, const char *sound, int volume) { json_object *root = json_object_new_object(); if (!root) @@ -314,6 +465,7 @@ int main(int argc, char *argv[]) { {"list", no_argument, 0, 'l'}, {"daemon", no_argument, 0, 1000}, {"stop", no_argument, 0, 1001}, + {"reload", no_argument, 0, 1002}, {"help", no_argument, 0, 'h'}, {"verbose", no_argument, 0, 'v'}, {0, 0, 0, 0}}; @@ -364,6 +516,23 @@ int main(int argc, char *argv[]) { case 1001: flag_stop = 1; break; + case 1002: + // --reload acts like sending SIGHUP to daemon + { + build_pidfile_path(pidfile_path, sizeof(pidfile_path)); + pid_t running_pid = 0; + if (!read_pidfile(pidfile_path, &running_pid) || + !process_is_running(running_pid)) { + fprintf(stderr, "KeyVibe: not running.\n"); + return 1; + } + if (kill(running_pid, SIGHUP) != 0) { + perror("kill"); + return 1; + } + printf("KeyVibe reload signal sent.\n"); + return 0; + } case 'h': print_usage(argv[0]); return 0; @@ -491,9 +660,20 @@ int main(int argc, char *argv[]) { char config_path[MAX_PATH_LENGTH]; char sound_dir[MAX_PATH_LENGTH]; - snprintf(config_path, sizeof(config_path), "%s/%s/config.json", - AUDIO_BASE_DIR, sound_name); - snprintf(sound_dir, sizeof(sound_dir), "%s/%s", AUDIO_BASE_DIR, sound_name); + if (!build_paths_for_sound(sound_name, config_path, sizeof(config_path), + sound_dir, sizeof(sound_dir))) { + return 1; + } + + // Store current settings for reloads + current_volume = volume; + current_verbose = verbose; + strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1); + current_sound_name[sizeof(current_sound_name) - 1] = '\0'; + strncpy(current_config_path, config_path, sizeof(current_config_path) - 1); + current_config_path[sizeof(current_config_path) - 1] = '\0'; + strncpy(current_sound_dir, sound_dir, sizeof(current_sound_dir) - 1); + current_sound_dir[sizeof(current_sound_dir) - 1] = '\0'; if (verbose && !is_daemon) { printf("KeyVibe starting...\n"); @@ -508,100 +688,93 @@ int main(int argc, char *argv[]) { } } - // Create pipe for communication - int pipefd[2]; - if (pipe(pipefd) == -1) { - perror("pipe"); + // Start children (sound + keyboard) + if (!start_children(sound_dir, config_path, volume, verbose)) { return 1; } - // Fork sound player process FIRST - sound_pid = fork(); - if (sound_pid == -1) { - perror("fork"); - return 1; - } - - if (sound_pid == 0) { - // Child process: sound player - close(pipefd[1]); // Close write end - - // Redirect stdin from pipe - dup2(pipefd[0], STDIN_FILENO); - close(pipefd[0]); - - // Change to sound directory (so relative paths work) - if (chdir(sound_dir) != 0) { - perror("chdir"); - exit(1); - } - - if (verbose) { - fprintf(stderr, "Starting sound player...\n"); - } - - char volume_str[32]; - snprintf(volume_str, sizeof(volume_str), "%d", volume); - char verbose_str[8]; - snprintf(verbose_str, sizeof(verbose_str), "%d", verbose); - execl(sound_player_path, "keyboard_sound_player", "config.json", volume_str, - verbose_str, (char *)NULL); - perror("execl keyboard_sound_player"); - exit(1); - } - - // Then fork keyboard listener process (sudo) - keyboard_pid = fork(); - if (keyboard_pid == -1) { - perror("fork"); - kill(sound_pid, SIGTERM); - return 1; - } - - if (keyboard_pid == 0) { - // Child process: keyboard listener - close(pipefd[0]); // Close read end - - // Redirect stdout to pipe - dup2(pipefd[1], STDOUT_FILENO); - close(pipefd[1]); - - if (verbose) { - fprintf(stderr, "Starting keyboard listener...\n"); - } - - execl(get_key_presses_path, "get_key_presses", (char *)NULL); - perror("execl get_key_presses"); - exit(1); - } - - // Parent process: wait for children - close(pipefd[0]); - close(pipefd[1]); - int status; pid_t finished_pid; - // Wait for either child to exit - while ((finished_pid = wait(&status)) > 0) { - if (finished_pid == keyboard_pid) { - printf("Keyboard listener exited with status %d\n", WEXITSTATUS(status)); - if (WIFSIGNALED(status)) { - printf("Keyboard listener killed by signal %d\n", WTERMSIG(status)); + signal(SIGHUP, handle_sighup); + + pthread_t inotify_thread; + struct inotify_thread_args in_args; + if (is_daemon) { + // Monitor user config for changes + char user_cfg_path[MAX_PATH_LENGTH]; + if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { + strncpy(in_args.path, user_cfg_path, sizeof(in_args.path) - 1); + in_args.path[sizeof(in_args.path) - 1] = '\0'; + pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args); + pthread_detach(inotify_thread); + } + } + + // Main supervise loop: wait for child exits or reload requests + while (1) { + if (reload_requested) { + reload_requested = 0; + // Re-read user config and optionally apply overrides only if + // override_config was set initially + char user_cfg_path[MAX_PATH_LENGTH]; + if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { + char *cfg_sound = NULL; + int cfg_volume = current_volume; + if (read_user_config(user_cfg_path, &cfg_sound, &cfg_volume)) { + if (cfg_sound) { + strncpy(current_sound_name, cfg_sound, + sizeof(current_sound_name) - 1); + free(cfg_sound); + } + current_volume = cfg_volume; + } } - keyboard_pid = 0; - if (sound_pid > 0) { - kill(sound_pid, SIGTERM); + + // Validate new sound pack and paths + if (validate_sound_pack(current_sound_name)) { + if (!build_paths_for_sound(current_sound_name, current_config_path, + sizeof(current_config_path), + current_sound_dir, + sizeof(current_sound_dir))) { + // Skip reload if paths invalid + continue; + } + + // Restart children with new settings + stop_children(); + start_children(current_sound_dir, current_config_path, current_volume, + current_verbose); } - } else if (finished_pid == sound_pid) { - printf("Sound player exited with status %d\n", WEXITSTATUS(status)); - if (WIFSIGNALED(status)) { - printf("Sound player killed by signal %d\n", WTERMSIG(status)); - } - sound_pid = 0; - if (keyboard_pid > 0) { - kill(keyboard_pid, SIGTERM); + continue; + } + + finished_pid = waitpid(-1, &status, WNOHANG); + if (finished_pid == 0) { + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 100000000L; + nanosleep(&ts, NULL); + continue; + } else if (finished_pid > 0) { + if (finished_pid == keyboard_pid) { + if (!is_daemon) + printf("Keyboard listener exited with status %d\n", + WEXITSTATUS(status)); + keyboard_pid = 0; + if (sound_pid > 0) + kill(sound_pid, SIGTERM); + } else if (finished_pid == sound_pid) { + if (!is_daemon) + printf("Sound player exited with status %d\n", WEXITSTATUS(status)); + sound_pid = 0; + if (keyboard_pid > 0) + kill(keyboard_pid, SIGTERM); } + break; + } else { + if (errno == ECHILD) + break; } }