From 18b6e5190eed01617992f0944608be2bb7046954 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 7 Oct 2025 22:04:37 +0545 Subject: [PATCH] feat: add daemon mode with PID file support and stop command --- keyvibe.c | 235 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 206 insertions(+), 29 deletions(-) diff --git a/keyvibe.c b/keyvibe.c index c288f78..ea9e50d 100644 --- a/keyvibe.c +++ b/keyvibe.c @@ -1,8 +1,13 @@ + #define _POSIX_C_SOURCE 200809L #include "config.h" #include +#include +#include #include +#include +#include #include #include #include @@ -10,9 +15,8 @@ #include #include #include +#include #include -#include -#include #define MAX_PATH_LENGTH 512 #define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/audio" @@ -21,6 +25,8 @@ // Global variables for cleanup pid_t keyboard_pid = 0; pid_t sound_pid = 0; +static char pidfile_path[MAX_PATH_LENGTH] = {0}; +static int is_daemon = 0; void print_usage(const char *program_name) { printf("KeyVibe - Mechanical Keyboard Sound Simulator\n\n"); @@ -30,10 +36,13 @@ void print_usage(const char *program_name) { printf(" -V, --volume VOLUME Set volume [0-100] (default: 50)\n"); printf(" -c, --override-config Apply -s/-V to override loaded config\n"); printf(" -l, --list List available sound packs\n"); + printf(" --daemon Run in background (write PID file)\n"); + printf(" --stop Stop background 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(" Reads ~/.keyvibe.json if present. On first run, prompts to create " + "it.\n"); printf("\nExamples:\n"); printf(" %s # Use default sound (eg-oreo)\n", program_name); @@ -107,7 +116,9 @@ int validate_sound_pack(const char *sound_name) { void cleanup_processes(int sig) { (void)sig; // Suppress unused parameter warning - printf("\nShutting down KeyVibe...\n"); + if (!is_daemon) { + printf("\nShutting down KeyVibe...\n"); + } if (sound_pid > 0) { kill(sound_pid, SIGTERM); @@ -119,6 +130,10 @@ void cleanup_processes(int sig) { waitpid(keyboard_pid, NULL, 0); } + if (pidfile_path[0] != '\0') { + unlink(pidfile_path); + } + exit(0); } @@ -150,7 +165,8 @@ static char *get_user_config_path(char *buffer, size_t buflen) { const char *home = getenv("HOME"); if (!home || strlen(home) == 0) { struct passwd *pw = getpwuid(getuid()); - if (pw && pw->pw_dir) home = pw->pw_dir; + if (pw && pw->pw_dir) + home = pw->pw_dir; } if (!home) { return NULL; @@ -159,38 +175,122 @@ static char *get_user_config_path(char *buffer, size_t buflen) { return buffer; } +static const char *get_runtime_dir() { + const char *rd = getenv("XDG_RUNTIME_DIR"); + if (rd && strlen(rd) > 0) + return rd; + return "/tmp"; +} + +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()); +} + +static int read_pidfile(const char *path, pid_t *out_pid) { + FILE *f = fopen(path, "r"); + if (!f) + return 0; + long val = -1; + if (fscanf(f, "%ld", &val) == 1 && val > 0) { + *out_pid = (pid_t)val; + fclose(f); + return 1; + } + fclose(f); + return 0; +} + +static int write_pidfile(const char *path, pid_t pid) { + FILE *f = fopen(path, "w"); + if (!f) + return 0; + fprintf(f, "%ld\n", (long)pid); + fclose(f); + return 1; +} + +static int process_is_running(pid_t pid) { + if (pid <= 0) + return 0; + if (kill(pid, 0) == 0) + return 1; + return 0; +} + +static void daemonize_self() { + pid_t pid = fork(); + if (pid < 0) + exit(1); + if (pid > 0) + exit(0); // Parent exits + + if (setsid() < 0) + exit(1); + + pid = fork(); + if (pid < 0) + exit(1); + if (pid > 0) + exit(0); + + umask(0); + chdir("/"); + + int fd = open("/dev/null", O_RDWR); + if (fd >= 0) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > 2) + close(fd); + } +} + static int write_user_config(const char *path, const char *sound, int volume) { json_object *root = json_object_new_object(); - if (!root) return 0; + if (!root) + return 0; json_object_object_add(root, "sound", json_object_new_string(sound)); json_object_object_add(root, "volume", json_object_new_int(volume)); - const char *json_str = json_object_to_json_string_ext(root, JSON_C_TO_STRING_PRETTY); + const char *json_str = + json_object_to_json_string_ext(root, JSON_C_TO_STRING_PRETTY); FILE *f = fopen(path, "w"); - if (!f) { json_object_put(root); return 0; } + if (!f) { + json_object_put(root); + return 0; + } fprintf(f, "%s\n", json_str); fclose(f); json_object_put(root); return 1; } -static int read_user_config(const char *path, char **out_sound, int *out_volume) { +static int read_user_config(const char *path, char **out_sound, + int *out_volume) { FILE *f = fopen(path, "r"); - if (!f) return 0; + if (!f) + return 0; fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f); char *buf = malloc(sz + 1); - if (!buf) { fclose(f); return 0; } + if (!buf) { + fclose(f); + return 0; + } fread(buf, 1, sz, f); buf[sz] = '\0'; fclose(f); json_object *root = json_tokener_parse(buf); free(buf); - if (!root) return 0; + if (!root) + return 0; json_object *o; if (json_object_object_get_ex(root, "sound", &o)) { const char *s = json_object_get_string(o); - if (s) *out_sound = strdup(s); + if (s) + *out_sound = strdup(s); } if (json_object_object_get_ex(root, "volume", &o)) { *out_volume = json_object_get_int(o); @@ -203,15 +303,20 @@ int main(int argc, char *argv[]) { char *sound_name = "eg-oreo"; // Default sound pack int verbose = 0; int list_sounds = 0; + int flag_daemon = 0; + int flag_stop = 0; // Parse command line arguments - static struct option long_options[] = {{"sound", required_argument, 0, 's'}, - {"volume", required_argument, 0, 'V'}, - {"override-config", no_argument, 0, 'c'}, - {"list", no_argument, 0, 'l'}, - {"help", no_argument, 0, 'h'}, - {"verbose", no_argument, 0, 'v'}, - {0, 0, 0, 0}}; + static struct option long_options[] = { + {"sound", required_argument, 0, 's'}, + {"volume", required_argument, 0, 'V'}, + {"override-config", no_argument, 0, 'c'}, + {"list", no_argument, 0, 'l'}, + {"daemon", no_argument, 0, 1000}, + {"stop", no_argument, 0, 1001}, + {"help", no_argument, 0, 'h'}, + {"verbose", no_argument, 0, 'v'}, + {0, 0, 0, 0}}; int volume = 50; int override_config = 0; @@ -222,16 +327,20 @@ int main(int argc, char *argv[]) { char user_cfg_path[MAX_PATH_LENGTH]; if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) { if (access(user_cfg_path, R_OK) == 0) { - char *cfg_sound = NULL; int cfg_volume = volume; + char *cfg_sound = NULL; + int cfg_volume = volume; if (read_user_config(user_cfg_path, &cfg_sound, &cfg_volume)) { - if (cfg_sound) { sound_name = cfg_sound; } + if (cfg_sound) { + sound_name = cfg_sound; + } volume = cfg_volume; } } } int opt; - while ((opt = getopt_long(argc, argv, "s:V:clhv", long_options, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "s:V:clhv", long_options, NULL)) != + -1) { switch (opt) { case 's': cli_sound = optarg; @@ -249,6 +358,12 @@ int main(int argc, char *argv[]) { case 'l': list_sounds = 1; break; + case 1000: + flag_daemon = 1; + break; + case 1001: + flag_stop = 1; + break; case 'h': print_usage(argv[0]); return 0; @@ -266,10 +381,49 @@ int main(int argc, char *argv[]) { return list_sound_packs(); } + // PID file path + build_pidfile_path(pidfile_path, sizeof(pidfile_path)); + + // Handle stop command early + if (flag_stop) { + pid_t running_pid = 0; + if (!read_pidfile(pidfile_path, &running_pid)) { + fprintf(stderr, "KeyVibe: no pidfile found at %s\n", pidfile_path); + return 1; + } + if (!process_is_running(running_pid)) { + fprintf(stderr, + "KeyVibe: process %ld not running, removing stale pidfile\n", + (long)running_pid); + unlink(pidfile_path); + return 1; + } + if (kill(running_pid, SIGTERM) != 0) { + perror("kill"); + return 1; + } + // Wait up to ~3s for it to exit + for (int i = 0; i < 30; i++) { + if (!process_is_running(running_pid)) { + unlink(pidfile_path); + printf("KeyVibe stopped.\n"); + return 0; + } + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 100000000L; // 100ms + nanosleep(&ts, NULL); + } + fprintf(stderr, "KeyVibe: process did not stop in time\n"); + return 1; + } + // Apply CLI overrides only if -c/--override-config was provided if (override_config) { - if (cli_sound != NULL) sound_name = cli_sound; - if (cli_volume >= 0) volume = cli_volume; + if (cli_sound != NULL) + sound_name = cli_sound; + if (cli_volume >= 0) + volume = cli_volume; } // If no config file existed, create one with defaults @@ -314,6 +468,25 @@ int main(int argc, char *argv[]) { signal(SIGINT, cleanup_processes); signal(SIGTERM, cleanup_processes); + // If daemon mode requested, ensure not already running, then daemonize + if (flag_daemon) { + pid_t existing = 0; + if (read_pidfile(pidfile_path, &existing) && process_is_running(existing)) { + fprintf(stderr, + "KeyVibe already running (pid %ld). Use --stop to stop it.\n", + (long)existing); + return 1; + } + daemonize_self(); + is_daemon = 1; + // Write our pidfile (this is the supervising process) + if (!write_pidfile(pidfile_path, getpid())) { + // If we cannot write pidfile, still continue but warn and run in + // foreground to avoid orphaning However we're already daemonized; best we + // can do is proceed. + } + } + // Build paths char config_path[MAX_PATH_LENGTH]; char sound_dir[MAX_PATH_LENGTH]; @@ -322,15 +495,17 @@ int main(int argc, char *argv[]) { AUDIO_BASE_DIR, sound_name); snprintf(sound_dir, sizeof(sound_dir), "%s/%s", AUDIO_BASE_DIR, sound_name); - if (verbose) { + if (verbose && !is_daemon) { printf("KeyVibe starting...\n"); printf("Sound pack: %s\n", sound_name); printf("Config file: %s\n", config_path); printf("Working directory: %s\n", sound_dir); printf("Press Ctrl+C to exit.\n\n"); } else { - printf("KeyVibe started with sound pack: %s\n", sound_name); - printf("Press Ctrl+C to exit.\n"); + if (!is_daemon) { + printf("KeyVibe started with sound pack: %s\n", sound_name); + printf("Press Ctrl+C to exit.\n"); + } } // Create pipe for communication @@ -430,6 +605,8 @@ int main(int argc, char *argv[]) { } } - printf("KeyVibe exited.\n"); + if (!is_daemon) { + printf("KeyVibe exited.\n"); + } return 0; }