Initial commit.
This commit is contained in:
commit
d7d699059f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.sh
|
||||||
|
*.log
|
||||||
45
Makefile
Normal file
45
Makefile
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Makefile for rproc
|
||||||
|
|
||||||
|
CC = gcc
|
||||||
|
# Compiling with optimizations and all warnings enabled for robustness.
|
||||||
|
CFLAGS = -Wall -Wextra -O2 -std=c11
|
||||||
|
# No special libraries needed for linking.
|
||||||
|
LDFLAGS =
|
||||||
|
TARGET = rproc
|
||||||
|
|
||||||
|
.PHONY: all clean install uninstall
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
$(TARGET): main.c
|
||||||
|
$(CC) $(CFLAGS) -o $(TARGET) main.c $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(TARGET) *.log
|
||||||
|
|
||||||
|
# Installs the application and the systemd service file.
|
||||||
|
# Requires sudo privileges.
|
||||||
|
install: $(TARGET)
|
||||||
|
@echo "Installing rproc binary to /usr/local/bin..."
|
||||||
|
sudo cp $(TARGET) /usr/local/bin/
|
||||||
|
@echo "Installing systemd service file to /etc/systemd/system..."
|
||||||
|
sudo cp rproc.service /etc/systemd/system/
|
||||||
|
@echo "Reloading systemd daemon..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
@echo "Installation complete."
|
||||||
|
@echo "IMPORTANT: Please edit /etc/systemd/system/rproc.service and set the correct WorkingDirectory."
|
||||||
|
@echo "Then run 'sudo systemctl daemon-reload' again."
|
||||||
|
|
||||||
|
# Uninstalls the application and the systemd service file.
|
||||||
|
# Requires sudo privileges.
|
||||||
|
uninstall:
|
||||||
|
@echo "Stopping and disabling rproc service..."
|
||||||
|
sudo systemctl stop rproc.service || true
|
||||||
|
sudo systemctl disable rproc.service || true
|
||||||
|
@echo "Removing rproc binary from /usr/local/bin..."
|
||||||
|
sudo rm -f /usr/local/bin/rproc
|
||||||
|
@echo "Removing systemd service file..."
|
||||||
|
sudo rm -f /etc/systemd/system/rproc.service
|
||||||
|
@echo "Reloading systemd daemon..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
@echo "Uninstallation complete."
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# rproc - A Zero-Configuration Process Manager
|
||||||
|
|
||||||
|
**Authored by:** retoor
|
||||||
|
**Version:** 1.1
|
||||||
|
**Date:** 2025-09-26
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
**rproc** is a lightweight, file-based, and zero-configuration process manager designed for simplicity and robustness. It monitors a single directory, treating the presence, absence, and modification of executable shell scripts (`.sh` files) as the source of truth for managing long-running processes.
|
||||||
|
|
||||||
|
Its design is predicated on providing a resilient service management layer with minimal operational overhead.
|
||||||
|
|
||||||
|
### Key Advantages
|
||||||
|
|
||||||
|
* **Simplicity and Intuitiveness:** No complex commands, APIs, or configuration files are required. Process management is performed using standard shell commands (`touch`, `rm`, `mv`, `chmod`), making it immediately accessible.
|
||||||
|
* **Extreme Resource Efficiency:** As a compiled C binary using event-driven I/O (`inotify`, `select`), `rproc` has a negligible memory and CPU footprint. It consumes system resources only when actively handling a file event or a process state change.
|
||||||
|
* **High Portability:** The application is a single, self-contained executable with no external dependencies beyond the standard C library. This allows it to be deployed on nearly any modern Linux system without an installation procedure.
|
||||||
|
* **GitOps-Friendly Workflow:** Because the desired state of all processes is defined by files on disk, `rproc` integrates perfectly with version control systems. Managing services can be as simple as a `git pull` in the working directory to automatically add, remove, or restart processes based on the committed changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Core Concepts
|
||||||
|
|
||||||
|
The fundamental principle of `rproc` is that the state of the executable `.sh` files in its working directory directly represents the desired state of the managed processes.
|
||||||
|
|
||||||
|
- **To run a process:** Create an executable `.sh` file.
|
||||||
|
- **To stop a process:** Delete the `.sh` file.
|
||||||
|
- **To restart a process:** Modify the `.sh` file.
|
||||||
|
|
||||||
|
`rproc` uses the Linux `inotify` subsystem to watch for these file system events in real-time and acts accordingly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Installation
|
||||||
|
|
||||||
|
`rproc` is a single-file C application.
|
||||||
|
|
||||||
|
### Compilation
|
||||||
|
|
||||||
|
Compile the source code using a standard C compiler like GCC:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc -Wall -Wextra -O2 -std=c11 -o rproc rproc.c
|
||||||
495
main.c
Normal file
495
main.c
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/inotify.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <libgen.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
|
||||||
|
#define EVENT_SIZE (sizeof(struct inotify_event))
|
||||||
|
#define EVENT_BUF_LEN (1024 * (EVENT_SIZE + 16))
|
||||||
|
#define LOG_MAX_SIZE (10 * 1024 * 1024)
|
||||||
|
#define CRASH_RESTART_DELAY 15
|
||||||
|
#define TERM_TIMEOUT 5
|
||||||
|
#define APP_LOG_FILE "rproc.log"
|
||||||
|
#define PID_LOCK_FILE "/tmp/rproc.pid"
|
||||||
|
#define MAX_LOG_LINE_LEN 4096
|
||||||
|
|
||||||
|
typedef struct Process {
|
||||||
|
pid_t pid;
|
||||||
|
char* script_name;
|
||||||
|
time_t crashed_at;
|
||||||
|
struct Process* next;
|
||||||
|
} Process;
|
||||||
|
|
||||||
|
static Process* process_list = NULL;
|
||||||
|
static volatile sig_atomic_t running = 1;
|
||||||
|
|
||||||
|
static void app_log(const char* format, ...);
|
||||||
|
static void terminate_process(pid_t pid, const char* script_name);
|
||||||
|
static void remove_process_by_pid(pid_t pid);
|
||||||
|
static void start_script(const char* script_name);
|
||||||
|
|
||||||
|
static void check_and_truncate_file(const char* path, off_t max_size) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(path, &st) == 0) {
|
||||||
|
if (st.st_size > max_size) {
|
||||||
|
if (truncate(path, 0) == -1) {
|
||||||
|
fprintf(stderr, "rproc warning: could not truncate %s: %s\n", path, strerror(errno));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_log(const char* format, ...) {
|
||||||
|
check_and_truncate_file(APP_LOG_FILE, LOG_MAX_SIZE);
|
||||||
|
FILE* fp = fopen(APP_LOG_FILE, "a");
|
||||||
|
if (!fp) return;
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
char time_buf[20];
|
||||||
|
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", localtime(&now));
|
||||||
|
|
||||||
|
fprintf(fp, "[%s] ", time_buf);
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vfprintf(fp, format, args);
|
||||||
|
va_end(args);
|
||||||
|
fputc('\n', fp);
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_process(pid_t pid, const char* script_name) {
|
||||||
|
Process* p = malloc(sizeof(Process));
|
||||||
|
if (!p) {
|
||||||
|
app_log("FATAL: malloc failed in add_process.");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
p->pid = pid;
|
||||||
|
p->script_name = strdup(script_name);
|
||||||
|
if (!p->script_name) {
|
||||||
|
app_log("FATAL: strdup failed for script %s.", script_name);
|
||||||
|
free(p);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
p->crashed_at = 0;
|
||||||
|
p->next = process_list;
|
||||||
|
process_list = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Process* find_process_by_pid(pid_t pid) {
|
||||||
|
for (Process* p = process_list; p; p = p->next) {
|
||||||
|
if (p->pid == pid) return p;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Process* find_process_by_name(const char* script_name) {
|
||||||
|
for (Process* p = process_list; p; p = p->next) {
|
||||||
|
if (strcmp(p->script_name, script_name) == 0) return p;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_process_by_pid(pid_t pid) {
|
||||||
|
Process** current = &process_list;
|
||||||
|
while (*current) {
|
||||||
|
Process* entry = *current;
|
||||||
|
if (entry->pid == pid) {
|
||||||
|
*current = entry->next;
|
||||||
|
free(entry->script_name);
|
||||||
|
free(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current = &entry->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void terminate_process(pid_t pid, const char* script_name) {
|
||||||
|
if (pid <= 0) {
|
||||||
|
if (pid == -1) {
|
||||||
|
remove_process_by_pid(pid);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_log("Stopping process for '%s' (PID: %d)", script_name, pid);
|
||||||
|
if (kill(pid, SIGTERM) == -1) {
|
||||||
|
if (errno == ESRCH) {
|
||||||
|
app_log("Process %d for '%s' did not exist. Removing from tracking.", pid, script_name);
|
||||||
|
remove_process_by_pid(pid);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < TERM_TIMEOUT; ++i) {
|
||||||
|
int status;
|
||||||
|
const pid_t result = waitpid(pid, &status, WNOHANG);
|
||||||
|
if (result == pid) {
|
||||||
|
app_log("Process %d for '%s' terminated gracefully.", pid, script_name);
|
||||||
|
remove_process_by_pid(pid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == -1 && errno != ECHILD) {
|
||||||
|
remove_process_by_pid(pid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
app_log("Process %d for '%s' did not terminate in time. Sending SIGKILL.", pid, script_name);
|
||||||
|
if (kill(pid, SIGKILL) == -1 && errno == ESRCH) {
|
||||||
|
app_log("Process %d for '%s' disappeared before SIGKILL.", pid, script_name);
|
||||||
|
}
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
remove_process_by_pid(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void create_slug_from_script_name(const char* script_name, char* slug_buffer, size_t buffer_size) {
|
||||||
|
strncpy(slug_buffer, script_name, buffer_size - 1);
|
||||||
|
slug_buffer[buffer_size - 1] = '\0';
|
||||||
|
|
||||||
|
char* dot = strrchr(slug_buffer, '.');
|
||||||
|
if (dot && strcmp(dot, ".sh") == 0) {
|
||||||
|
*dot = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (char* p = slug_buffer; *p; ++p) {
|
||||||
|
if (*p == '_') {
|
||||||
|
*p = '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void start_script(const char* script_name) {
|
||||||
|
Process *existing = find_process_by_name(script_name);
|
||||||
|
if (existing && existing->pid > 0) {
|
||||||
|
app_log("Script '%s' is already running with PID %d.", script_name, existing->pid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_log("Starting script: '%s'", script_name);
|
||||||
|
const pid_t pid = fork();
|
||||||
|
if (pid == -1) {
|
||||||
|
app_log("Failed to fork for script '%s': %s", script_name, strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
char slug[128];
|
||||||
|
char new_proc_name[256];
|
||||||
|
create_slug_from_script_name(script_name, slug, sizeof(slug));
|
||||||
|
snprintf(new_proc_name, sizeof(new_proc_name), "rproc-%s-process", slug);
|
||||||
|
|
||||||
|
if (prctl(PR_SET_NAME, new_proc_name, 0, 0, 0) == -1) {
|
||||||
|
perror("prctl");
|
||||||
|
}
|
||||||
|
|
||||||
|
char stdout_log[FILENAME_MAX], stderr_log[FILENAME_MAX];
|
||||||
|
snprintf(stdout_log, sizeof(stdout_log), "%s.stdout.log", script_name);
|
||||||
|
snprintf(stderr_log, sizeof(stderr_log), "%s.stderr.log", script_name);
|
||||||
|
|
||||||
|
check_and_truncate_file(stdout_log, LOG_MAX_SIZE);
|
||||||
|
check_and_truncate_file(stderr_log, LOG_MAX_SIZE);
|
||||||
|
|
||||||
|
if (!freopen(stdout_log, "a", stdout) || !freopen(stderr_log, "a", stderr)) {
|
||||||
|
exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
char* const argv[] = {"/bin/bash", (char*)script_name, NULL};
|
||||||
|
char* const envp[] = { "PATH=/usr/bin:/bin", NULL };
|
||||||
|
execve("/bin/bash", argv, envp);
|
||||||
|
|
||||||
|
fprintf(stderr, "Failed to execute script '%s': %s\n", script_name, strerror(errno));
|
||||||
|
exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_process(pid, script_name);
|
||||||
|
app_log("Started '%s' with PID %d", script_name, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stop_script(const char* script_name) {
|
||||||
|
Process* p = find_process_by_name(script_name);
|
||||||
|
if (p) {
|
||||||
|
terminate_process(p->pid, p->script_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_sigchld(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
int saved_errno = errno;
|
||||||
|
int status;
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
|
||||||
|
Process* p = find_process_by_pid(pid);
|
||||||
|
if (p) {
|
||||||
|
if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
|
||||||
|
app_log("Script '%s' (PID: %d) crashed with status %d.", p->script_name, pid, WEXITSTATUS(status));
|
||||||
|
p->crashed_at = time(NULL);
|
||||||
|
p->pid = -1;
|
||||||
|
} else if (WIFSIGNALED(status)) {
|
||||||
|
app_log("Script '%s' (PID: %d) terminated by signal %d.", p->script_name, pid, WTERMSIG(status));
|
||||||
|
p->crashed_at = time(NULL);
|
||||||
|
p->pid = -1;
|
||||||
|
} else {
|
||||||
|
app_log("Script '%s' (PID: %d) exited cleanly.", p->script_name, pid);
|
||||||
|
remove_process_by_pid(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errno = saved_errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_shutdown_signals(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
char msg[] = "Shutdown signal received.\n";
|
||||||
|
ssize_t bytes_written = write(STDOUT_FILENO, msg, sizeof(msg) - 1);
|
||||||
|
(void)bytes_written;
|
||||||
|
running = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_signal_handlers(void) {
|
||||||
|
struct sigaction sa_chld = {0}, sa_term = {0};
|
||||||
|
sa_chld.sa_handler = handle_sigchld;
|
||||||
|
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
|
||||||
|
sigemptyset(&sa_chld.sa_mask);
|
||||||
|
if (sigaction(SIGCHLD, &sa_chld, 0) == -1) {
|
||||||
|
perror("sigaction SIGCHLD");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
sa_term.sa_handler = handle_shutdown_signals;
|
||||||
|
sigemptyset(&sa_term.sa_mask);
|
||||||
|
if (sigaction(SIGTERM, &sa_term, 0) == -1 || sigaction(SIGINT, &sa_term, 0) == -1) {
|
||||||
|
perror("sigaction SIGTERM/SIGINT");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run_daemon(void) {
|
||||||
|
DIR* d = opendir(".");
|
||||||
|
if (d) {
|
||||||
|
struct dirent* dir;
|
||||||
|
while ((dir = readdir(d)) != NULL) {
|
||||||
|
size_t len = strlen(dir->d_name);
|
||||||
|
if (len > 3 && strcmp(dir->d_name + len - 3, ".sh") == 0) {
|
||||||
|
start_script(dir->d_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = inotify_init1(IN_NONBLOCK);
|
||||||
|
if (fd < 0) {
|
||||||
|
app_log("inotify_init1 failed: %s", strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inotify_add_watch(fd, ".", IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_TO | IN_MOVED_FROM);
|
||||||
|
app_log("Daemon started. Monitoring current directory.");
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
fd_set read_fds;
|
||||||
|
FD_ZERO(&read_fds);
|
||||||
|
FD_SET(fd, &read_fds);
|
||||||
|
struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
|
||||||
|
|
||||||
|
int retval = select(fd + 1, &read_fds, NULL, NULL, &tv);
|
||||||
|
|
||||||
|
if (retval > 0 && FD_ISSET(fd, &read_fds)) {
|
||||||
|
char buffer[EVENT_BUF_LEN];
|
||||||
|
ssize_t length = read(fd, buffer, EVENT_BUF_LEN);
|
||||||
|
if (length > 0) {
|
||||||
|
for (int i = 0; i < length; ) {
|
||||||
|
struct inotify_event* event = (struct inotify_event*)&buffer[i];
|
||||||
|
if (event->len) {
|
||||||
|
size_t name_len = strlen(event->name);
|
||||||
|
if (name_len > 3 && strcmp(event->name + name_len - 3, ".sh") == 0) {
|
||||||
|
if (event->mask & (IN_CREATE | IN_MOVED_TO)) {
|
||||||
|
app_log("Detected new script: '%s'", event->name);
|
||||||
|
start_script(event->name);
|
||||||
|
} else if (event->mask & IN_MODIFY) {
|
||||||
|
app_log("Detected modification: '%s'. Restarting.", event->name);
|
||||||
|
stop_script(event->name);
|
||||||
|
start_script(event->name);
|
||||||
|
} else if (event->mask & (IN_DELETE | IN_MOVED_FROM)) {
|
||||||
|
app_log("Detected deletion: '%s'. Stopping.", event->name);
|
||||||
|
stop_script(event->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += EVENT_SIZE + event->len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process* p = process_list;
|
||||||
|
while (p) {
|
||||||
|
Process* next = p->next;
|
||||||
|
if (p->pid == -1 && p->crashed_at > 0) {
|
||||||
|
if (time(NULL) - p->crashed_at >= CRASH_RESTART_DELAY) {
|
||||||
|
app_log("Restarting crashed script '%s'.", p->script_name);
|
||||||
|
char* name_copy = strdup(p->script_name);
|
||||||
|
if (name_copy) {
|
||||||
|
remove_process_by_pid(p->pid);
|
||||||
|
start_script(name_copy);
|
||||||
|
free(name_copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app_log("Shutdown sequence initiated. Terminating all processes.");
|
||||||
|
while (process_list) {
|
||||||
|
terminate_process(process_list->pid, process_list->script_name);
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
app_log("Daemon stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct LogFile {
|
||||||
|
char* name;
|
||||||
|
off_t offset;
|
||||||
|
struct LogFile* next;
|
||||||
|
} LogFile;
|
||||||
|
|
||||||
|
static void run_monitor(void) {
|
||||||
|
char cwd[PATH_MAX] = {0};
|
||||||
|
char line_buffer[PATH_MAX];
|
||||||
|
|
||||||
|
FILE* lock_fp = fopen(PID_LOCK_FILE, "r");
|
||||||
|
if (lock_fp) {
|
||||||
|
if (fgets(line_buffer, sizeof(line_buffer), lock_fp)) {
|
||||||
|
if (fgets(cwd, sizeof(cwd), lock_fp)) {
|
||||||
|
cwd[strcspn(cwd, "\n")] = 0;
|
||||||
|
if (chdir(cwd) == -1) {
|
||||||
|
perror("Could not change to daemon's directory");
|
||||||
|
fclose(lock_fp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(lock_fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Another instance is running in %s. Attaching as a live monitor...\n", cwd);
|
||||||
|
printf("--- Tailing all *.log files. Press Ctrl+C to exit. ---\n");
|
||||||
|
|
||||||
|
LogFile* log_list = NULL;
|
||||||
|
int fd = inotify_init();
|
||||||
|
if (fd < 0) { perror("inotify_init"); return; }
|
||||||
|
|
||||||
|
inotify_add_watch(fd, ".", IN_CREATE | IN_MODIFY);
|
||||||
|
|
||||||
|
while(1) {
|
||||||
|
DIR* d = opendir(".");
|
||||||
|
if (d) {
|
||||||
|
struct dirent* dir;
|
||||||
|
while ((dir = readdir(d)) != NULL) {
|
||||||
|
size_t len = strlen(dir->d_name);
|
||||||
|
if (len > 4 && strcmp(dir->d_name + len - 4, ".log") == 0) {
|
||||||
|
int found = 0;
|
||||||
|
for (LogFile* l = log_list; l; l = l->next) {
|
||||||
|
if (strcmp(l->name, dir->d_name) == 0) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
LogFile* new_log = malloc(sizeof(LogFile));
|
||||||
|
new_log->name = strdup(dir->d_name);
|
||||||
|
new_log->offset = 0;
|
||||||
|
new_log->next = log_list;
|
||||||
|
log_list = new_log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LogFile* l = log_list; l; l = l->next) {
|
||||||
|
FILE* fp = fopen(l->name, "r");
|
||||||
|
if (fp) {
|
||||||
|
fseek(fp, l->offset, SEEK_SET);
|
||||||
|
char line[MAX_LOG_LINE_LEN];
|
||||||
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
|
printf("[%s] %s", l->name, line);
|
||||||
|
}
|
||||||
|
l->offset = ftell(fp);
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fd_set read_fds;
|
||||||
|
FD_ZERO(&read_fds);
|
||||||
|
FD_SET(fd, &read_fds);
|
||||||
|
struct timeval tv = { .tv_sec = 2, .tv_usec = 0 };
|
||||||
|
select(fd + 1, &read_fds, NULL, NULL, &tv);
|
||||||
|
|
||||||
|
if (FD_ISSET(fd, &read_fds)) {
|
||||||
|
char buffer[EVENT_BUF_LEN];
|
||||||
|
ssize_t bytes_read = read(fd, buffer, EVENT_BUF_LEN);
|
||||||
|
(void)bytes_read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int pid_fd = open(PID_LOCK_FILE, O_CREAT | O_RDWR, 0666);
|
||||||
|
if (pid_fd == -1) {
|
||||||
|
perror("Could not open PID lock file");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flock(pid_fd, LOCK_EX | LOCK_NB) == -1) {
|
||||||
|
if (errno == EWOULDBLOCK) {
|
||||||
|
close(pid_fd);
|
||||||
|
run_monitor();
|
||||||
|
} else {
|
||||||
|
perror("flock");
|
||||||
|
close(pid_fd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ftruncate(pid_fd, 0) == -1) {
|
||||||
|
perror("Could not truncate PID lock file");
|
||||||
|
goto cleanup_lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
char cwd[PATH_MAX];
|
||||||
|
if (!getcwd(cwd, sizeof(cwd))) {
|
||||||
|
perror("Could not get current working directory");
|
||||||
|
goto cleanup_lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
char lock_content[PATH_MAX + 16];
|
||||||
|
int len = snprintf(lock_content, sizeof(lock_content), "%d\n%s\n", getpid(), cwd);
|
||||||
|
if (write(pid_fd, lock_content, len) != (long int)len) {
|
||||||
|
perror("Could not write to PID lock file");
|
||||||
|
goto cleanup_lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_signal_handlers();
|
||||||
|
run_daemon();
|
||||||
|
|
||||||
|
cleanup_lock:
|
||||||
|
flock(pid_fd, LOCK_UN);
|
||||||
|
close(pid_fd);
|
||||||
|
unlink(PID_LOCK_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
26
rproc.service
Normal file
26
rproc.service
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=rproc - Zero-Config C Process Runner
|
||||||
|
# Ensures the service starts after the system is ready for general use.
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# The service is simple and does not fork. systemd handles daemonization.
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
# --- IMPORTANT ---
|
||||||
|
# Change this path to the directory where your .sh scripts are located.
|
||||||
|
WorkingDirectory=/opt/scripts
|
||||||
|
|
||||||
|
# The command to start the service.
|
||||||
|
ExecStart=/usr/local/bin/rproc
|
||||||
|
|
||||||
|
# The service will be automatically restarted if it fails.
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
# On stop, systemd will only send a signal to the main rproc process.
|
||||||
|
# This allows rproc to handle the graceful shutdown of its children itself.
|
||||||
|
KillMode=process
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
# This enables the service to be started at boot.
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Reference in New Issue
Block a user