786 lines
21 KiB
C
786 lines
21 KiB
C
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "config.h"
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <getopt.h>
|
|
#include <json-c/json.h>
|
|
#include <pthread.h>
|
|
#include <pwd.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/inotify.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#define MAX_PATH_LENGTH 512
|
|
#define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/audio"
|
|
#define USER_CONFIG_FILENAME ".keyvibe.json"
|
|
|
|
// 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;
|
|
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");
|
|
printf("Usage: %s [OPTIONS]\n\n", program_name);
|
|
printf("Options:\n");
|
|
printf(" -s, --sound SOUND_NAME Select sound pack (default: eg-oreo)\n");
|
|
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(" --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);
|
|
printf(" %s -s cherrymx-blue-abs # Use Cherry MX Blue ABS sound\n",
|
|
program_name);
|
|
printf(" %s -l # List all available sounds\n",
|
|
program_name);
|
|
printf("\nPress Ctrl+C to exit.\n");
|
|
}
|
|
|
|
int list_sound_packs() {
|
|
DIR *dir;
|
|
struct dirent *entry;
|
|
char path[MAX_PATH_LENGTH];
|
|
struct stat st;
|
|
|
|
dir = opendir(AUDIO_BASE_DIR);
|
|
if (dir == NULL) {
|
|
fprintf(stderr, "Error: Cannot open audio directory '%s'\n",
|
|
AUDIO_BASE_DIR);
|
|
fprintf(stderr, "Make sure you're running from the correct directory.\n");
|
|
return 1;
|
|
}
|
|
|
|
printf("Available sound packs:\n");
|
|
printf("======================\n");
|
|
|
|
while ((entry = readdir(dir)) != NULL) {
|
|
// Skip . and .. directories
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
|
continue;
|
|
|
|
// Build full path
|
|
snprintf(path, sizeof(path), "%s/%s", AUDIO_BASE_DIR, entry->d_name);
|
|
|
|
// Use stat to check if it's a directory
|
|
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
printf(" %s\n", entry->d_name);
|
|
}
|
|
}
|
|
|
|
closedir(dir);
|
|
return 0;
|
|
}
|
|
|
|
int validate_sound_pack(const char *sound_name) {
|
|
char config_path[MAX_PATH_LENGTH];
|
|
char sound_path[MAX_PATH_LENGTH];
|
|
|
|
snprintf(config_path, sizeof(config_path), "%s/%s/config.json",
|
|
AUDIO_BASE_DIR, sound_name);
|
|
snprintf(sound_path, sizeof(sound_path), "%s/%s", AUDIO_BASE_DIR, sound_name);
|
|
|
|
// Check if directory exists
|
|
if (access(sound_path, F_OK) != 0) {
|
|
fprintf(stderr, "Error: Sound pack '%s' not found in %s/\n", sound_name,
|
|
AUDIO_BASE_DIR);
|
|
fprintf(stderr, "Use --list to see available sound packs.\n");
|
|
return 0;
|
|
}
|
|
|
|
// Check if config.json exists
|
|
if (access(config_path, R_OK) != 0) {
|
|
fprintf(stderr, "Error: Config file not found: %s\n", config_path);
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
void cleanup_processes(int sig) {
|
|
(void)sig; // Suppress unused parameter warning
|
|
|
|
if (!is_daemon) {
|
|
printf("\nShutting down KeyVibe...\n");
|
|
}
|
|
|
|
if (sound_pid > 0) {
|
|
kill(sound_pid, SIGTERM);
|
|
waitpid(sound_pid, NULL, 0);
|
|
}
|
|
|
|
if (keyboard_pid > 0) {
|
|
kill(keyboard_pid, SIGTERM);
|
|
waitpid(keyboard_pid, NULL, 0);
|
|
}
|
|
|
|
if (pidfile_path[0] != '\0') {
|
|
unlink(pidfile_path);
|
|
}
|
|
|
|
exit(0);
|
|
}
|
|
|
|
// Function to check if sudo credentials are cached
|
|
int check_sudo_cached() {
|
|
int status = system("sudo -n true 2>/dev/null");
|
|
return WEXITSTATUS(status) == 0;
|
|
}
|
|
|
|
// Function to prompt for sudo password if needed
|
|
int ensure_sudo_access() {
|
|
if (check_sudo_cached()) {
|
|
return 1; // Already have sudo access
|
|
}
|
|
|
|
printf("This program requires sudo access to monitor keyboard events.\n");
|
|
printf("Please enter your password when prompted:\n");
|
|
|
|
int status = system("sudo -v");
|
|
if (WEXITSTATUS(status) != 0) {
|
|
fprintf(stderr, "Error: Failed to obtain sudo access\n");
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
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 (!home) {
|
|
return NULL;
|
|
}
|
|
snprintf(buffer, buflen, "%s/%s", home, USER_CONFIG_FILENAME);
|
|
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 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());
|
|
}
|
|
|
|
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 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)
|
|
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);
|
|
FILE *f = fopen(path, "w");
|
|
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) {
|
|
FILE *f = fopen(path, "r");
|
|
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;
|
|
}
|
|
fread(buf, 1, sz, f);
|
|
buf[sz] = '\0';
|
|
fclose(f);
|
|
json_object *root = json_tokener_parse(buf);
|
|
free(buf);
|
|
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 (json_object_object_get_ex(root, "volume", &o)) {
|
|
*out_volume = json_object_get_int(o);
|
|
}
|
|
json_object_put(root);
|
|
return 1;
|
|
}
|
|
|
|
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'},
|
|
{"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}};
|
|
|
|
int volume = 50;
|
|
int override_config = 0;
|
|
char *cli_sound = NULL;
|
|
int cli_volume = -1;
|
|
|
|
// Load user config if present (pre-parse so CLI can override)
|
|
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;
|
|
if (read_user_config(user_cfg_path, &cfg_sound, &cfg_volume)) {
|
|
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) {
|
|
switch (opt) {
|
|
case 's':
|
|
cli_sound = optarg;
|
|
break;
|
|
case 'V':
|
|
cli_volume = atoi(optarg);
|
|
if (cli_volume < 0)
|
|
cli_volume = 0;
|
|
if (cli_volume > 100)
|
|
cli_volume = 100;
|
|
break;
|
|
case 'c':
|
|
override_config = 1;
|
|
break;
|
|
case 'l':
|
|
list_sounds = 1;
|
|
break;
|
|
case 1000:
|
|
flag_daemon = 1;
|
|
break;
|
|
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;
|
|
case 'v':
|
|
verbose = 1;
|
|
break;
|
|
default:
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Handle list command
|
|
if (list_sounds) {
|
|
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 no config file existed, create one with defaults
|
|
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
|
|
if (access(user_cfg_path, F_OK) != 0) {
|
|
// Use current values (defaults or CLI overrides) for initial file
|
|
if (!write_user_config(user_cfg_path, sound_name, volume)) {
|
|
fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path);
|
|
} else if (verbose) {
|
|
fprintf(stderr, "Created default config %s\n", user_cfg_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate sound pack
|
|
if (!validate_sound_pack(sound_name)) {
|
|
return 1;
|
|
}
|
|
|
|
// No sudo pre-check; we'll run without sudo by default
|
|
|
|
// Check if required executables exist
|
|
char get_key_presses_path[MAX_PATH_LENGTH];
|
|
char sound_player_path[MAX_PATH_LENGTH];
|
|
|
|
snprintf(get_key_presses_path, sizeof(get_key_presses_path),
|
|
"%s/get_key_presses", KeyVibe_BIN_DIR);
|
|
snprintf(sound_player_path, sizeof(sound_player_path),
|
|
"%s/keyboard_sound_player", KeyVibe_BIN_DIR);
|
|
|
|
if (access(get_key_presses_path, X_OK) != 0) {
|
|
fprintf(stderr, "Error: Cannot find or execute %s\n", get_key_presses_path);
|
|
return 1;
|
|
}
|
|
|
|
if (access(sound_player_path, X_OK) != 0) {
|
|
fprintf(stderr, "Error: Cannot find or execute %s\n", sound_player_path);
|
|
return 1;
|
|
}
|
|
|
|
// Setup signal handler for cleanup
|
|
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];
|
|
|
|
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");
|
|
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 {
|
|
if (!is_daemon) {
|
|
printf("KeyVibe started with sound pack: %s\n", sound_name);
|
|
printf("Press Ctrl+C to exit.\n");
|
|
}
|
|
}
|
|
|
|
// Start children (sound + keyboard)
|
|
if (!start_children(sound_dir, config_path, volume, verbose)) {
|
|
return 1;
|
|
}
|
|
|
|
int status;
|
|
pid_t finished_pid;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (!is_daemon) {
|
|
printf("KeyVibe exited.\n");
|
|
}
|
|
return 0;
|
|
}
|