diff --git a/scripts/c/Makefile b/scripts/c/Makefile new file mode 100644 index 0000000..8b4c81b --- /dev/null +++ b/scripts/c/Makefile @@ -0,0 +1,21 @@ +CC ?= cc +CFLAGS = -O2 -Wall -Wextra -std=c11 +LDFLAGS = -lz -lpthread -lm +TARGET = hbc_mini +SRC = hbc_mini.c + +# FreeBSD/NetBSD keep zlib in base; no extra flags needed. +# On some NetBSD installs pthreads may need -lpthread from pkgsrc. + +.PHONY: all clean debug + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +debug: $(SRC) + $(CC) -g -fsanitize=address,undefined -o $(TARGET)_dbg $< $(LDFLAGS) + +clean: + rm -f $(TARGET) $(TARGET)_dbg diff --git a/scripts/c/hbc_mini b/scripts/c/hbc_mini new file mode 100755 index 0000000..70ad729 Binary files /dev/null and b/scripts/c/hbc_mini differ diff --git a/scripts/c/hbc_mini.c b/scripts/c/hbc_mini.c new file mode 100644 index 0000000..31f4601 --- /dev/null +++ b/scripts/c/hbc_mini.c @@ -0,0 +1,1422 @@ +/* hbc_mini.c — HeartBeat Client, single-file C port of hbc_mini.py + * + * Build: cc -O2 -o hbc_mini hbc_mini.c -lz -lpthread -lm + * Requires: zlib, pthreads, POSIX + * Supported: Linux, FreeBSD, NetBSD, DragonFly BSD + * + * Drop on any host and run: ./hbc_mini + * Config: ~/.hbc.json (same keys as Python version) + */ + +#ifdef __linux__ +# define _GNU_SOURCE +# define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__) +# include +# include +# include +# include +#endif +#if defined(__NetBSD__) +# include +#endif + +/* version updated by scripts/bumpminor.sh */ +#define HBC_VERSION "5.2.4" +#define DEFAULT_PORT 50003 +#define DEFAULT_INTERVAL 10 +#define MAX_HOSTS 8 +#define MAX_KV 64 +#define MAX_KEY 128 +#define MAX_VAL 4096 +#define PROTO_BUF_SIZE 65536 +#define MAX_PING_HOSTS 32 +#define MAX_NAGIOS_CMDS 32 +#define MAX_DISK_MOUNTS 32 +#define MAX_NET_SKIP 16 + +/* ============================================================ + * Logging + * ============================================================ */ + +static bool g_use_syslog = false; +static int g_log_level = 3; /* 0=debug 1=info 2=warn 3=error */ + +#define LL_DEBUG 0 +#define LL_INFO 1 +#define LL_WARN 2 +#define LL_ERROR 3 + +static void hbc_log(int level, const char *fmt, ...) { + if (level < g_log_level) return; + va_list ap; + va_start(ap, fmt); + if (g_use_syslog) { + int pri = LOG_ERR; + if (level == LL_DEBUG) pri = LOG_DEBUG; + else if (level == LL_INFO) pri = LOG_INFO; + else if (level == LL_WARN) pri = LOG_WARNING; + vsyslog(pri, fmt, ap); + } else { + const char *ln[] = {"DEBUG","INFO","WARN","ERROR"}; + time_t t = time(NULL); + struct tm tm; localtime_r(&t, &tm); + char ts[32]; strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm); + fprintf(stderr, "%s hbc %s: ", ts, ln[level]); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); + } + va_end(ap); +} + +#define LOGD(fmt, ...) hbc_log(LL_DEBUG, fmt, ##__VA_ARGS__) +#define LOGI(fmt, ...) hbc_log(LL_INFO, fmt, ##__VA_ARGS__) +#define LOGW(fmt, ...) hbc_log(LL_WARN, fmt, ##__VA_ARGS__) +#define LOGE(fmt, ...) hbc_log(LL_ERROR, fmt, ##__VA_ARGS__) + +/* ============================================================ + * Key-Value dict (protocol messages) + * ============================================================ */ + +typedef struct { char key[MAX_KEY]; char val[MAX_VAL]; } kv_t; +typedef struct { kv_t pairs[MAX_KV]; int count; } kvdict_t; + +static void kv_clear(kvdict_t *d) { d->count = 0; } + +static bool kv_set(kvdict_t *d, const char *key, const char *val) { + if (d->count >= MAX_KV) return false; + snprintf(d->pairs[d->count].key, MAX_KEY, "%s", key); + snprintf(d->pairs[d->count].val, MAX_VAL, "%s", val); + d->count++; + return true; +} + +static bool kv_set_int(kvdict_t *d, const char *key, long long v) { + char buf[32]; snprintf(buf, sizeof(buf), "%lld", v); + return kv_set(d, key, buf); +} + +static bool kv_set_ull(kvdict_t *d, const char *key, unsigned long long v) { + char buf[32]; snprintf(buf, sizeof(buf), "%llu", v); + return kv_set(d, key, buf); +} + +static bool kv_set_dbl(kvdict_t *d, const char *key, double v) { + char buf[32]; + if (isinf(v)) snprintf(buf, sizeof(buf), "inf"); + else snprintf(buf, sizeof(buf), "%.5f", v); + return kv_set(d, key, buf); +} + +static const char *kv_get(const kvdict_t *d, const char *key) { + for (int i = 0; i < d->count; i++) + if (strcmp(d->pairs[i].key, key) == 0) + return d->pairs[i].val; + return NULL; +} + +/* ============================================================ + * Protocol codec (mirrors hbd/common/proto.py) + * ============================================================ */ + +/* Build wire bytes: !MSG: + zlib-compressed key=val;key=val;... */ +static int proto_encode(const char *msg_id, const kvdict_t *d, + uint8_t *out, size_t cap) { + /* Build payload */ + char *payload = malloc(PROTO_BUF_SIZE); + if (!payload) return -1; + int plen = 0; + for (int i = 0; i < d->count; i++) { + if (i > 0 && plen < PROTO_BUF_SIZE - 1) payload[plen++] = ';'; + plen += snprintf(payload + plen, PROTO_BUF_SIZE - plen, + "%s=%s", d->pairs[i].key, d->pairs[i].val); + if (plen >= PROTO_BUF_SIZE) { plen = PROTO_BUF_SIZE - 1; break; } + } + /* Compress */ + uLongf clen = compressBound(plen); + uint8_t *cbuf = malloc(clen); + if (!cbuf) { free(payload); return -1; } + if (compress2(cbuf, &clen, (uint8_t *)payload, plen, 6) != Z_OK) { + free(payload); free(cbuf); return -1; + } + free(payload); + /* Header: !XXX: (5 bytes) */ + if ((size_t)(5 + clen) > cap) { free(cbuf); return -1; } + out[0] = '!'; + memcpy(out + 1, msg_id, 3); + out[4] = ':'; + memcpy(out + 5, cbuf, clen); + free(cbuf); + return 5 + (int)clen; +} + +/* Parse wire bytes into kvdict */ +static bool proto_decode(const uint8_t *data, int dlen, kvdict_t *out) { + kv_clear(out); + if (dlen < 5 || data[0] != '!') return false; + char id[4] = {0}; + memcpy(id, data + 1, 3); + kv_set(out, "ID", id); + uint8_t *payload = malloc(PROTO_BUF_SIZE); + if (!payload) return false; + uLongf plen = PROTO_BUF_SIZE - 1; + if (uncompress(payload, &plen, data + 5, dlen - 5) != Z_OK) { + free(payload); return false; + } + payload[plen] = '\0'; + char *p = (char *)payload; + while (*p) { + char *semi = strchr(p, ';'); + if (semi) *semi = '\0'; + char *eq = strchr(p, '='); + if (eq) { + *eq = '\0'; + char *kp = p; while (*kp == ' ') kp++; + char *vp = eq+1; while (*vp == ' ') vp++; + kv_set(out, kp, vp); + } + if (semi) p = semi + 1; else break; + } + free(payload); + return true; +} + +/* ============================================================ + * Minimal JSON parser (for config file) + * ============================================================ */ + +typedef enum { JT_NULL,JT_BOOL,JT_INT,JT_FLOAT,JT_STRING,JT_ARRAY,JT_OBJECT } jtype_t; + +typedef struct jval jval_t; +struct jval { + jtype_t type; + union { + bool b; + long long i; + double f; + char *s; + struct { char **keys; jval_t **vals; int count, cap; } obj; + struct { jval_t **items; int count, cap; } arr; + }; +}; + +static void jval_free(jval_t *v) { + if (!v) return; + if (v->type == JT_STRING) { free(v->s); } + else if (v->type == JT_OBJECT) { + for (int i = 0; i < v->obj.count; i++) { free(v->obj.keys[i]); jval_free(v->obj.vals[i]); } + free(v->obj.keys); free(v->obj.vals); + } else if (v->type == JT_ARRAY) { + for (int i = 0; i < v->arr.count; i++) jval_free(v->arr.items[i]); + free(v->arr.items); + } + free(v); +} + +static const char *jskip(const char *p) { + while (*p && isspace((unsigned char)*p)) p++; + return p; +} + +static const char *jparse_value(const char *p, jval_t **out); + +static const char *jparse_string(const char *p, char **out) { + if (*p != '"') return NULL; + p++; + char buf[4096]; int n = 0; + while (*p && *p != '"') { + if (*p == '\\') { + p++; + switch (*p) { + case '"': buf[n++]='"'; break; case '\\': buf[n++]='\\'; break; + case 'n': buf[n++]='\n'; break; case 'r': buf[n++]='\r'; break; + case 't': buf[n++]='\t'; break; default: buf[n++]=*p; break; + } + } else { buf[n++] = *p; } + p++; + if (n >= (int)sizeof(buf)-1) break; + } + buf[n] = '\0'; + if (*p == '"') p++; + *out = strdup(buf); + return p; +} + +static const char *jparse_object(const char *p, jval_t **out) { + if (*p != '{') return NULL; + p++; + jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_OBJECT; + p = jskip(p); + while (*p && *p != '}') { + p = jskip(p); + char *key = NULL; + p = jparse_string(p, &key); + if (!p) break; + p = jskip(p); if (*p == ':') p++; + p = jskip(p); + jval_t *val = NULL; + p = jparse_value(p, &val); + if (key && val) { + if (v->obj.count >= v->obj.cap) { + v->obj.cap = v->obj.cap ? v->obj.cap*2 : 4; + v->obj.keys = realloc(v->obj.keys, v->obj.cap * sizeof(char*)); + v->obj.vals = realloc(v->obj.vals, v->obj.cap * sizeof(jval_t*)); + } + v->obj.keys[v->obj.count] = key; + v->obj.vals[v->obj.count] = val; + v->obj.count++; + } else { free(key); jval_free(val); } + p = jskip(p); if (*p == ',') p++; + p = jskip(p); + } + if (*p == '}') p++; + *out = v; return p; +} + +static const char *jparse_array(const char *p, jval_t **out) { + if (*p != '[') return NULL; + p++; + jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_ARRAY; + p = jskip(p); + while (*p && *p != ']') { + jval_t *item = NULL; + p = jparse_value(jskip(p), &item); + if (item) { + if (v->arr.count >= v->arr.cap) { + v->arr.cap = v->arr.cap ? v->arr.cap*2 : 4; + v->arr.items = realloc(v->arr.items, v->arr.cap * sizeof(jval_t*)); + } + v->arr.items[v->arr.count++] = item; + } + p = jskip(p); if (*p == ',') p++; + } + if (*p == ']') p++; + *out = v; return p; +} + +static const char *jparse_value(const char *p, jval_t **out) { + p = jskip(p); + if (!*p) { *out = NULL; return p; } + if (*p == '{') return jparse_object(p, out); + if (*p == '[') return jparse_array(p, out); + if (*p == '"') { + char *s = NULL; p = jparse_string(p, &s); + jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_STRING; v->s = s; + *out = v; return p; + } + if (strncmp(p,"true",4)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_BOOL; v->b=true; *out=v; return p+4; } + if (strncmp(p,"false",5)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_BOOL; v->b=false; *out=v; return p+5; } + if (strncmp(p,"null",4)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_NULL; *out=v; return p+4; } + if (isdigit((unsigned char)*p) || *p == '-') { + char *e1, *e2; + long long iv = strtoll(p, &e1, 10); + double fv = strtod(p, &e2); + if (e2 > e1) { + jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_FLOAT; v->f=fv; *out=v; return e2; + } else { + jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_INT; v->i=iv; *out=v; return e1; + } + } + *out = NULL; return p + 1; +} + +static jval_t *json_parse(const char *src) { + jval_t *v = NULL; jparse_value(src, &v); return v; +} + +static jval_t *jget(const jval_t *obj, const char *key) { + if (!obj || obj->type != JT_OBJECT) return NULL; + for (int i = 0; i < obj->obj.count; i++) + if (strcmp(obj->obj.keys[i], key) == 0) return obj->obj.vals[i]; + return NULL; +} + +static long long jint(const jval_t *v, long long def) { + if (!v) return def; + if (v->type==JT_INT) return v->i; + if (v->type==JT_FLOAT) return (long long)v->f; + if (v->type==JT_BOOL) return v->b ? 1 : 0; + return def; +} +static const char *jstr(const jval_t *v, const char *def) { + return (!v || v->type!=JT_STRING) ? def : v->s; +} + +/* ============================================================ + * Config + * ============================================================ */ + +typedef struct { + int hb_port, interval; + char owner[256]; + + char ping_hosts[MAX_PING_HOSTS][256]; + int ping_host_count, ping_interval, ping_count, ping_timeout; + + struct { char name[64]; char cmd[512]; } nagios[MAX_NAGIOS_CMDS]; + int nagios_count, nagios_interval, nagios_timeout; + + int cpu_interval, mem_interval, disk_interval, net_interval; + + char disk_mounts[MAX_DISK_MOUNTS][256]; + int disk_mount_count; + + char net_skip[MAX_NET_SKIP][64]; + int net_skip_count; +} config_t; + +static void config_defaults(config_t *c) { + memset(c, 0, sizeof(*c)); + c->hb_port = DEFAULT_PORT; + c->interval = DEFAULT_INTERVAL; + c->ping_interval = 60; c->ping_count = 3; c->ping_timeout = 5; + c->nagios_interval = 300; c->nagios_timeout = 30; + c->cpu_interval = 300; + c->mem_interval = 300; + c->disk_interval = 300; + c->net_interval = 300; + snprintf(c->net_skip[0], 64, "lo"); + c->net_skip_count = 1; +} + +static void config_load(config_t *cfg, const char *path) { + config_defaults(cfg); + char fpath[512]; + if (!path) { + const char *home = getenv("HOME"); + snprintf(fpath, sizeof(fpath), "%s/.hbc.json", home ? home : ""); + path = fpath; + } + FILE *f = fopen(path, "r"); + if (!f) return; + fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); + char *buf = malloc(sz + 1); + if (!buf) { fclose(f); return; } + size_t _nr = fread(buf, 1, sz, f); buf[_nr] = '\0'; fclose(f); + jval_t *root = json_parse(buf); free(buf); + if (!root || root->type != JT_OBJECT) { LOGW("cannot parse %s", path); jval_free(root); return; } + + jval_t *v; + if ((v = jget(root, "hb_port"))) cfg->hb_port = jint(v, cfg->hb_port); + if ((v = jget(root, "interval"))) cfg->interval = jint(v, cfg->interval); + if ((v = jget(root, "owner"))) snprintf(cfg->owner, sizeof(cfg->owner), "%s", jstr(v, "")); + + jval_t *plugins = jget(root, "plugins"); + + /* ping_monitor */ + jval_t *pm = plugins ? jget(plugins,"ping_monitor") : jget(root,"ping_monitor"); + if (pm && pm->type == JT_OBJECT) { + if ((v=jget(pm,"interval"))) cfg->ping_interval = jint(v, cfg->ping_interval); + if ((v=jget(pm,"count"))) cfg->ping_count = jint(v, cfg->ping_count); + if ((v=jget(pm,"timeout"))) cfg->ping_timeout = jint(v, cfg->ping_timeout); + jval_t *h = jget(pm, "hosts"); + if (h && h->type == JT_ARRAY) { + for (int i = 0; i < h->arr.count && cfg->ping_host_count < MAX_PING_HOSTS; i++) { + const char *s = jstr(h->arr.items[i], NULL); + if (s) snprintf(cfg->ping_hosts[cfg->ping_host_count++], 256, "%s", s); + } + } else if (h && h->type == JT_OBJECT) { + for (int i = 0; i < h->obj.count && cfg->ping_host_count < MAX_PING_HOSTS; i++) + snprintf(cfg->ping_hosts[cfg->ping_host_count++], 256, "%s", h->obj.keys[i]); + } + } + + /* nagios_runner */ + jval_t *nr = plugins ? jget(plugins,"nagios_runner") : jget(root,"nagios_runner"); + if (nr && nr->type == JT_OBJECT) { + if ((v=jget(nr,"interval"))) cfg->nagios_interval = jint(v, cfg->nagios_interval); + if ((v=jget(nr,"timeout"))) cfg->nagios_timeout = jint(v, cfg->nagios_timeout); + jval_t *cmds = jget(nr, "commands"); + if (cmds && cmds->type == JT_ARRAY) { + for (int i = 0; i < cmds->arr.count && cfg->nagios_count < MAX_NAGIOS_CMDS; i++) { + jval_t *c = cmds->arr.items[i]; + if (!c || c->type != JT_OBJECT) continue; + jval_t *n = jget(c,"name"), *cmd = jget(c,"command"); + if (n && cmd) { + int idx = cfg->nagios_count++; + snprintf(cfg->nagios[idx].name, 64, "%s", jstr(n, "")); + snprintf(cfg->nagios[idx].cmd, 512, "%s", jstr(cmd, "")); + } + } + } + } + + /* cpu/memory/disk/network */ + jval_t *cpu = plugins ? jget(plugins,"cpu_monitor") : jget(root,"cpu_monitor"); + if (cpu && (v=jget(cpu,"interval"))) cfg->cpu_interval = jint(v, cfg->cpu_interval); + + jval_t *mem = plugins ? jget(plugins,"memory_monitor") : jget(root,"memory_monitor"); + if (mem && (v=jget(mem,"interval"))) cfg->mem_interval = jint(v, cfg->mem_interval); + + jval_t *disk = plugins ? jget(plugins,"disk_monitor") : jget(root,"disk_monitor"); + if (disk && disk->type == JT_OBJECT) { + if ((v=jget(disk,"interval"))) cfg->disk_interval = jint(v, cfg->disk_interval); + jval_t *m = jget(disk,"mounts"); + if (m && m->type == JT_ARRAY) + for (int i = 0; i < m->arr.count && cfg->disk_mount_count < MAX_DISK_MOUNTS; i++) { + const char *s = jstr(m->arr.items[i], NULL); + if (s) snprintf(cfg->disk_mounts[cfg->disk_mount_count++], 256, "%s", s); + } + } + + jval_t *net = plugins ? jget(plugins,"network_monitor") : jget(root,"network_monitor"); + if (net && net->type == JT_OBJECT) { + if ((v=jget(net,"interval"))) cfg->net_interval = jint(v, cfg->net_interval); + jval_t *sk = jget(net,"skip_interfaces"); + if (sk && sk->type == JT_ARRAY) { + cfg->net_skip_count = 0; + for (int i = 0; i < sk->arr.count && cfg->net_skip_count < MAX_NET_SKIP; i++) { + const char *s = jstr(sk->arr.items[i], NULL); + if (s) snprintf(cfg->net_skip[cfg->net_skip_count++], 64, "%s", s); + } + } + } + + LOGI("loaded config from %s", path); + jval_free(root); +} + +/* ============================================================ + * UDP connection + * ============================================================ */ + +typedef struct { + int conn_id; + char addr[INET6_ADDRSTRLEN]; + int port, af, sockfd; + char name[256]; + long long ack_count; + double last_send, rtt; + volatile bool request_info; + pthread_mutex_t mu; +} conn_t; + +static double now_ts(void) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return ts.tv_sec + ts.tv_nsec * 1e-9; +} + +static bool conn_open(conn_t *c) { + c->sockfd = socket(c->af, SOCK_DGRAM, 0); + if (c->sockfd < 0) { LOGE("socket: %s", strerror(errno)); return false; } + int fl = fcntl(c->sockfd, F_GETFL, 0); + fcntl(c->sockfd, F_SETFL, fl | O_NONBLOCK); + pthread_mutex_init(&c->mu, NULL); + return true; +} + +static void conn_close(conn_t *c) { + if (c->sockfd >= 0) { close(c->sockfd); c->sockfd = -1; } +} + +static int conn_send(conn_t *c, const char *msg_id, kvdict_t *d) { + kv_set(d, "name", c->name); + char buf[32]; + snprintf(buf, sizeof(buf), "%d", c->conn_id); kv_set(d, "id", buf); + snprintf(buf, sizeof(buf), "%.3f", now_ts()); kv_set(d, "time", buf); + + uint8_t wire[PROTO_BUF_SIZE]; + int wlen = proto_encode(msg_id, d, wire, sizeof(wire)); + if (wlen < 0) { LOGW("proto_encode failed"); return -1; } + + struct sockaddr_storage sa; socklen_t salen = 0; + memset(&sa, 0, sizeof(sa)); + if (c->af == AF_INET) { + struct sockaddr_in *s4 = (struct sockaddr_in *)&sa; + s4->sin_family = AF_INET; + s4->sin_port = htons(c->port); + inet_pton(AF_INET, c->addr, &s4->sin_addr); + salen = sizeof(*s4); + } else { + struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&sa; + s6->sin6_family = AF_INET6; + s6->sin6_port = htons(c->port); + inet_pton(AF_INET6, c->addr, &s6->sin6_addr); + salen = sizeof(*s6); + } + + pthread_mutex_lock(&c->mu); + c->last_send = now_ts(); + ssize_t sent = sendto(c->sockfd, wire, wlen, 0, (struct sockaddr *)&sa, salen); + pthread_mutex_unlock(&c->mu); + return (sent == wlen) ? 0 : -1; +} + +static void conn_recv(conn_t *c) { + uint8_t buf[PROTO_BUF_SIZE]; + struct sockaddr_storage sa; socklen_t sl = sizeof(sa); + ssize_t n = recvfrom(c->sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&sa, &sl); + if (n <= 0) return; + + kvdict_t msg; + if (!proto_decode(buf, (int)n, &msg)) return; + const char *id = kv_get(&msg, "ID"); + if (!id) return; + + if (strcmp(id, "ACK") == 0) { + c->rtt = (now_ts() - c->last_send) * 1000.0; + c->ack_count++; + const char *ru = kv_get(&msg, "request_update"); + if (ru && strcmp(ru,"1") == 0) c->request_info = true; + LOGD("ACK rtt=%.1fms", c->rtt); + } else if (strcmp(id, "CMD") == 0) { + const char *cmd = kv_get(&msg, "cmd"); + if (cmd) { + LOGI("CMD: %s", cmd); + char out[4096] = ""; + FILE *p = popen(cmd, "r"); + if (p) { size_t _r = fread(out, 1, sizeof(out)-1, p); (void)_r; pclose(p); } + kvdict_t rep; kv_clear(&rep); + kv_set(&rep, "service", "command"); + kv_set(&rep, "msg", out[0] ? out : "OK"); + conn_send(c, "HTB", &rep); + } + } else if (strcmp(id, "UPD") == 0) { + LOGI("UPD: update requested (not implemented in C version)"); + } +} + +/* ============================================================ + * Global state + * ============================================================ */ + +static volatile bool g_running = true; +static volatile bool g_restart = false; +static conn_t g_conns[MAX_HOSTS]; +static int g_nconns = 0; + +static void sig_handler(int sig) { + if (sig == SIGHUP) g_restart = true; + g_running = false; +} + +/* ============================================================ + * Plugin: os_info + * ============================================================ */ + +static void plugin_os_info(conn_t *c, const config_t *cfg) { + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "os_info"); + struct utsname u; + if (uname(&u) == 0) { + kv_set(&d, "system", u.sysname); + kv_set(&d, "node", u.nodename); + kv_set(&d, "release", u.release); + kv_set(&d, "machine", u.machine); + } + kv_set(&d, "hbc_version", HBC_VERSION); + kv_set(&d, "hbc_type", "mini-c"); + if (cfg->owner[0]) kv_set(&d, "owner", cfg->owner); + +#ifdef __linux__ + { + FILE *f = fopen("/etc/os-release", "r"); + if (!f) f = fopen("/etc/lsb-release", "r"); + if (f) { + char line[512]; + while (fgets(line, sizeof(line), f)) { + char *eq = strchr(line, '='); + if (!eq || line[0] == '#') continue; + *eq = '\0'; + char *val = eq + 1; + int vl = strlen(val); + while (vl > 0 && (val[vl-1]=='\n'||val[vl-1]=='\r'||val[vl-1]=='"'||val[vl-1]=='\'')) val[--vl]='\0'; + if (*val == '"' || *val == '\'') val++; + if (strcmp(line,"PRETTY_NAME")==0) kv_set(&d, "distro_pretty_name", val); + else if (strcmp(line,"NAME")==0) kv_set(&d, "distro_name", val); + else if (strcmp(line,"VERSION_ID")==0) kv_set(&d, "distro_version_id", val); + else if (strcmp(line,"ID")==0) kv_set(&d, "distro_id", val); + } + fclose(f); + } + } +#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__) + { + char osver[128] = ""; size_t len = sizeof(osver); + sysctlbyname("kern.osrelease", osver, &len, NULL, 0); + if (osver[0]) kv_set(&d, "distro_version_id", osver); + } +#endif + conn_send(c, "PLG", &d); + LOGI("sent os_info"); +} + +/* ============================================================ + * Plugin: cpu_monitor + * Linux: /proc/stat (user nice sys idle iowait irq softirq steal) + * BSD: sysctl kern.cp_time (CP_USER CP_NICE CP_SYS CP_INTR CP_IDLE) + * ============================================================ */ + +typedef struct { + long long user, sys, idle, iowait, total; +} cpu_sample_t; + +#ifdef __linux__ + +static bool read_cpu_sample(cpu_sample_t *s) { + FILE *f = fopen("/proc/stat", "r"); + if (!f) return false; + char line[256]; bool ok = false; + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "cpu ", 4) != 0) continue; + long long u=0, ni=0, sy=0, id=0, iow=0, irq=0, sirq=0; + int n = sscanf(line+4, "%lld %lld %lld %lld %lld %lld %lld", + &u, &ni, &sy, &id, &iow, &irq, &sirq); + if (n < 4) break; + s->user = u + ni; + s->sys = sy + irq + sirq; + s->idle = id; + s->iowait = (n >= 5) ? iow : 0; + s->total = u + ni + sy + id + iow + irq + sirq; + ok = true; break; + } + fclose(f); return ok; +} + +static void read_cpu_extras(kvdict_t *d) { + FILE *fi = fopen("/proc/cpuinfo", "r"); + if (fi) { + char ln[256]; int cores = 0; + while (fgets(ln, sizeof(ln), fi)) if (strncmp(ln,"processor",9)==0) cores++; + fclose(fi); + if (cores > 0) kv_set_int(d, "cpu_core_count", cores); + } + fi = fopen("/proc/uptime", "r"); + if (fi) { + double up = 0; + if (fscanf(fi, "%lf", &up) == 1) kv_set_int(d, "uptime_seconds", (long long)up); + fclose(fi); + } +} + +#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__) + +static bool read_cpu_sample(cpu_sample_t *s) { + /* kern.cp_time: CP_USER(0) CP_NICE(1) CP_SYS(2) CP_INTR(3) CP_IDLE(4) */ + unsigned long cp[5] = {0}; + size_t len = sizeof(cp); + if (sysctlbyname("kern.cp_time", cp, &len, NULL, 0) != 0) return false; + s->user = (long long)cp[0] + (long long)cp[1]; + s->sys = (long long)cp[2] + (long long)cp[3]; /* SYS + INTR */ + s->idle = (long long)cp[4]; + s->iowait = 0; + s->total = s->user + (long long)cp[2] + (long long)cp[3] + s->idle; + return true; +} + +static void read_cpu_extras(kvdict_t *d) { + int ncpu = 0; size_t len = sizeof(ncpu); + if (sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0) == 0 && ncpu > 0) + kv_set_int(d, "cpu_core_count", ncpu); + struct timeval bt; len = sizeof(bt); + if (sysctlbyname("kern.boottime", &bt, &len, NULL, 0) == 0) + kv_set_int(d, "uptime_seconds", (long long)(time(NULL) - bt.tv_sec)); +} + +#endif /* platform cpu */ + +static cpu_sample_t g_cpu_prev; +static bool g_cpu_have_prev = false; + +static void plugin_cpu_monitor(conn_t *c, const config_t *cfg) { + (void)cfg; + cpu_sample_t t1, t2; + if (!g_cpu_have_prev) { + if (!read_cpu_sample(&t1)) return; + sleep(1); + if (!read_cpu_sample(&t2)) return; + } else { + t1 = g_cpu_prev; + if (!read_cpu_sample(&t2)) return; + } + g_cpu_prev = t2; g_cpu_have_prev = true; + + long long idle_delta = (t2.idle + t2.iowait) - (t1.idle + t1.iowait); + long long total_delta = t2.total - t1.total; + if (total_delta == 0) return; + + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "cpu_monitor"); + kv_set_dbl(&d, "cpu_percent", round(100.0*(1.0-(double)idle_delta/total_delta)*10)/10.0); + kv_set_dbl(&d, "cpu_user", round(100.0*(t2.user - t1.user) / total_delta*10)/10.0); + kv_set_dbl(&d, "cpu_system", round(100.0*(t2.sys - t1.sys) / total_delta*10)/10.0); + kv_set_dbl(&d, "cpu_idle", round(100.0*(t2.idle - t1.idle) / total_delta*10)/10.0); + if (t2.iowait || t1.iowait) + kv_set_dbl(&d, "cpu_iowait", round(100.0*(t2.iowait - t1.iowait) / total_delta*10)/10.0); + double la[3]; + if (getloadavg(la, 3) == 3) { + kv_set_dbl(&d, "load_1min", round(la[0]*100)/100.0); + kv_set_dbl(&d, "load_5min", round(la[1]*100)/100.0); + kv_set_dbl(&d, "load_15min", round(la[2]*100)/100.0); + } + read_cpu_extras(&d); + kv_set_dbl(&d, "_timestamp", now_ts()); + conn_send(c, "PLG", &d); + LOGD("sent cpu_monitor"); +} + +/* ============================================================ + * Plugin: memory_monitor + * Linux: /proc/meminfo + * FreeBSD: sysctl vm.stats.vm.* + * NetBSD: sysctl vm.uvmexp (struct uvmexp) + * ============================================================ */ + +/* emit the common kvdict fields and send */ +static void mem_send(conn_t *c, + long long tot, long long used, long long av, long long fr, + long long act, long long ina, long long cac, long long buf, + long long stot, long long sused) { + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "memory_monitor"); + kv_set_ull(&d, "memory_total", (unsigned long long)tot); + kv_set_ull(&d, "memory_used", (unsigned long long)used); + kv_set_ull(&d, "memory_available", (unsigned long long)av); + kv_set_ull(&d, "memory_free", (unsigned long long)fr); + char pct[32]; + snprintf(pct, sizeof(pct), "%.1f", tot ? 100.0*used/tot : 0.0); + kv_set(&d, "memory_percent", pct); + if (buf) kv_set_ull(&d, "memory_buffers", (unsigned long long)buf); + if (cac) kv_set_ull(&d, "memory_cached", (unsigned long long)cac); + if (act) kv_set_ull(&d, "memory_active", (unsigned long long)act); + if (ina) kv_set_ull(&d, "memory_inactive", (unsigned long long)ina); + if (stot > 0) { + long long sfr = stot - sused; + kv_set_ull(&d, "swap_total", (unsigned long long)stot); + kv_set_ull(&d, "swap_used", (unsigned long long)sused); + kv_set_ull(&d, "swap_free", (unsigned long long)sfr); + snprintf(pct, sizeof(pct), "%.1f", 100.0*sused/stot); + kv_set(&d, "swap_percent", pct); + } + kv_set_dbl(&d, "_timestamp", now_ts()); + conn_send(c, "PLG", &d); + LOGD("sent memory_monitor"); +} + +#ifdef __linux__ + +static void plugin_memory_monitor(conn_t *c, const config_t *cfg) { + (void)cfg; + FILE *f = fopen("/proc/meminfo", "r"); + if (!f) return; + long long tot=0, fr=0, av=0, buf=0, cac=0, act=0, ina=0, stot=0, sfr=0; + char line[256]; + while (fgets(line, sizeof(line), f)) { + long long v; char key[64]; + if (sscanf(line, "%63s %lld", key, &v) != 2) continue; + int kl = strlen(key); if (key[kl-1]==':') key[kl-1]='\0'; + if (!strcmp(key,"MemTotal")) tot=v; + else if (!strcmp(key,"MemFree")) fr=v; + else if (!strcmp(key,"MemAvailable")) av=v; + else if (!strcmp(key,"Buffers")) buf=v; + else if (!strcmp(key,"Cached")) cac=v; + else if (!strcmp(key,"Active")) act=v; + else if (!strcmp(key,"Inactive")) ina=v; + else if (!strcmp(key,"SwapTotal")) stot=v; + else if (!strcmp(key,"SwapFree")) sfr=v; + } + fclose(f); + if (!tot) return; + if (!av) av = fr; + + /* ZFS ARC is reclaimable memory not counted in MemAvailable */ + long long arc_kb = 0; + FILE *arc = fopen("/proc/spl/kstat/zfs/arcstats", "r"); + if (arc) { + char ak[64]; long long av2; int at; + while (fscanf(arc, "%63s %d %lld\n", ak, &at, &av2) == 3) + if (!strcmp(ak,"size")) { arc_kb = av2/1024; break; } + fclose(arc); + } + av = (av + arc_kb < tot) ? av + arc_kb : tot; + long long used = tot - av; + /* values from /proc/meminfo are in kB */ + mem_send(c, tot*1024, used*1024, av*1024, fr*1024, + act*1024, ina*1024, cac*1024, buf*1024, + stot*1024, (stot-sfr)*1024); +} + +#elif defined(__FreeBSD__) || defined(__DragonFly__) + +static void plugin_memory_monitor(conn_t *c, const config_t *cfg) { + (void)cfg; + long ps = getpagesize(); + + unsigned long physmem = 0; size_t len = sizeof(physmem); + sysctlbyname("hw.physmem", &physmem, &len, NULL, 0); + if (!physmem) return; + + unsigned int v_free=0, v_inactive=0, v_active=0, v_cache=0; + len = sizeof(v_free); sysctlbyname("vm.stats.vm.v_free_count", &v_free, &len, NULL, 0); + len = sizeof(v_inactive); sysctlbyname("vm.stats.vm.v_inactive_count", &v_inactive, &len, NULL, 0); + len = sizeof(v_active); sysctlbyname("vm.stats.vm.v_active_count", &v_active, &len, NULL, 0); + len = sizeof(v_cache); sysctlbyname("vm.stats.vm.v_cache_count", &v_cache, &len, NULL, 0); + + long long tot = (long long)physmem; + long long fr = (long long)v_free * ps; + long long act = (long long)v_active * ps; + long long ina = (long long)v_inactive * ps; + long long cac = (long long)v_cache * ps; + long long av = fr + ina + cac; if (av > tot) av = tot; + long long used = tot - av; + mem_send(c, tot, used, av, fr, act, ina, cac, 0, 0, 0); +} + +#elif defined(__NetBSD__) + +static void plugin_memory_monitor(conn_t *c, const config_t *cfg) { + (void)cfg; + struct uvmexp uvm; + size_t len = sizeof(uvm); + int mib[2] = {CTL_VM, VM_UVMEXP}; + if (sysctl(mib, 2, &uvm, &len, NULL, 0) != 0) return; + + long long ps = uvm.pagesize; + long long tot = (long long)uvm.npages * ps; + long long fr = (long long)uvm.free * ps; + long long act = (long long)uvm.active * ps; + long long ina = (long long)uvm.inactive * ps; + long long av = fr + ina; if (av > tot) av = tot; + long long used = tot - av; + long long stot = (long long)uvm.swpages * ps; + long long sinuse = (long long)uvm.swpginuse * ps; + mem_send(c, tot, used, av, fr, act, ina, 0, 0, stot, sinuse); +} + +#endif /* platform memory */ + +/* ============================================================ + * Plugin: disk_monitor (df -P) + * ============================================================ */ + +static void plugin_disk_monitor(conn_t *c, const config_t *cfg) { + FILE *f = popen("df -P", "r"); + if (!f) return; + + char *json = malloc(MAX_VAL); if (!json) { pclose(f); return; } + int jlen = 0; bool first = true; + json[jlen++] = '{'; + + char line[512]; + if (!fgets(line, sizeof(line), f)) { pclose(f); free(json); return; } /* skip header */ + while (fgets(line, sizeof(line), f)) { + char fs[256], mount[256], pcts[16]; + long long tk, uk, ak; int pct; + if (sscanf(line, "%255s %lld %lld %lld %15s %255s", fs, &tk, &uk, &ak, pcts, mount) != 6) continue; + pct = atoi(pcts); + if (cfg->disk_mount_count > 0) { + bool found = false; + for (int i = 0; i < cfg->disk_mount_count; i++) + if (!strcmp(cfg->disk_mounts[i], mount)) { found=true; break; } + if (!found) continue; + } + /* JSON-escape the mount point */ + char esc[512]; int ei = 0; + for (int i = 0; mount[i] && ei < (int)sizeof(esc)-2; i++) { + if (mount[i]=='"'||mount[i]=='\\') esc[ei++]='\\'; + esc[ei++] = mount[i]; + } + esc[ei] = '\0'; + char entry[512]; + int el = snprintf(entry, sizeof(entry), + "%s\"%s\":{\"total\":%lld,\"used\":%lld,\"free\":%lld,\"percent\":%d}", + first ? "" : ",", esc, tk*1024, uk*1024, ak*1024, pct); + if (jlen + el < MAX_VAL - 2) { memcpy(json+jlen, entry, el); jlen+=el; first=false; } + } + pclose(f); + json[jlen++] = '}'; json[jlen] = '\0'; + if (first) { free(json); return; } + + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "disk_monitor"); + char *jval = malloc(MAX_VAL + 1); + if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "partitions", jval); free(jval); } + kv_set_dbl(&d, "_timestamp", now_ts()); + conn_send(c, "PLG", &d); + free(json); + LOGD("sent disk_monitor"); +} + +/* ============================================================ + * Plugin: network_monitor + * Linux: /proc/net/dev + * BSD: getifaddrs() + struct if_data (AF_LINK) + * ============================================================ */ + +typedef struct { char iface[64]; unsigned long long rx, tx; } net_stat_t; +static net_stat_t g_net_prev[64]; +static int g_net_prev_n = 0; +static double g_net_prev_ts = 0; + +#ifdef __linux__ + +static int read_net_stats(net_stat_t *out, int max) { + FILE *f = fopen("/proc/net/dev", "r"); + if (!f) return 0; + char line[512]; int n = 0; + if (!fgets(line, sizeof(line), f)) { fclose(f); return 0; } /* header 1 */ + if (!fgets(line, sizeof(line), f)) { fclose(f); return 0; } /* header 2 */ + while (fgets(line, sizeof(line), f) && n < max) { + char *col = strchr(line, ':'); + if (!col) continue; + *col = '\0'; + char *iface = line; while (*iface==' ') iface++; + unsigned long long rx, tx, dummy; + if (sscanf(col+1, "%llu %llu %llu %llu %llu %llu %llu %llu %llu", + &rx,&dummy,&dummy,&dummy,&dummy,&dummy,&dummy,&dummy,&tx) == 9) { + snprintf(out[n].iface, 64, "%.*s", 63, iface); + out[n].rx = rx; out[n].tx = tx; n++; + } + } + fclose(f); return n; +} + +#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__) + +static int read_net_stats(net_stat_t *out, int max) { + struct ifaddrs *ifap; + if (getifaddrs(&ifap) != 0) return 0; + int n = 0; + for (struct ifaddrs *ifa = ifap; ifa && n < max; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK) continue; + struct if_data *ifd = (struct if_data *)ifa->ifa_data; + if (!ifd) continue; + snprintf(out[n].iface, 64, "%.*s", 63, ifa->ifa_name); + out[n].rx = (unsigned long long)ifd->ifi_ibytes; + out[n].tx = (unsigned long long)ifd->ifi_obytes; + n++; + } + freeifaddrs(ifap); + return n; +} + +#endif /* platform network */ + +static void plugin_network_monitor(conn_t *c, const config_t *cfg) { + net_stat_t curr[64]; + int cn = read_net_stats(curr, 64); + double now = now_ts(); + + if (g_net_prev_n == 0) { + memcpy(g_net_prev, curr, cn * sizeof(net_stat_t)); + g_net_prev_n = cn; g_net_prev_ts = now; + return; + } + double dt = now - g_net_prev_ts; + if (dt <= 0) return; + + char *json = malloc(MAX_VAL); if (!json) return; + int jlen = 0; bool first = true; + json[jlen++] = '{'; + + for (int i = 0; i < cn; i++) { + bool skip = false; + for (int s = 0; s < cfg->net_skip_count; s++) + if (!strcmp(cfg->net_skip[s], curr[i].iface)) { skip=true; break; } + if (skip) continue; + net_stat_t *prev = NULL; + for (int j = 0; j < g_net_prev_n; j++) + if (!strcmp(g_net_prev[j].iface, curr[i].iface)) { prev=&g_net_prev[j]; break; } + if (!prev) continue; + long long rd = (long long)(curr[i].rx - prev->rx); + long long td = (long long)(curr[i].tx - prev->tx); + char entry[256]; + int el = snprintf(entry, sizeof(entry), + "%s\"%s\":{\"bytes_recv\":%llu,\"bytes_sent\":%llu" + ",\"bytes_recv_delta\":%lld,\"bytes_sent_delta\":%lld}", + first?"":"," , curr[i].iface, curr[i].rx, curr[i].tx, rd, td); + if (jlen + el < MAX_VAL - 2) { memcpy(json+jlen, entry, el); jlen+=el; first=false; } + } + json[jlen++] = '}'; json[jlen] = '\0'; + memcpy(g_net_prev, curr, cn * sizeof(net_stat_t)); + g_net_prev_n = cn; g_net_prev_ts = now; + if (first) { free(json); return; } + + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "network_monitor"); + char *jval = malloc(MAX_VAL + 1); + if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "interfaces", jval); free(jval); } + kv_set_dbl(&d, "_timestamp", now_ts()); + conn_send(c, "PLG", &d); + free(json); + LOGD("sent network_monitor"); +} + +/* ============================================================ + * Plugin: ping_monitor + * ============================================================ */ + +static bool do_ping(const char *host, int count, int timeout, + double *rmin, double *ravg, double *rmax, double *loss) { + *rmin = *ravg = *rmax = 1.0/0.0; /* inf */ + *loss = 100.0; + char cmd[512]; + snprintf(cmd, sizeof(cmd), "ping -c %d -W %d %s 2>/dev/null", count, timeout, host); + FILE *f = popen(cmd, "r"); + if (!f) return false; + char line[512]; bool got = false; + while (fgets(line, sizeof(line), f)) { + char *p = strstr(line, "% packet loss"); + if (p) { + while (p > line && (isdigit((unsigned char)*(p-1)) || *(p-1)=='.')) p--; + *loss = atof(p); + } + p = strstr(line, "min/avg/max"); + if (p) { + p = strchr(p, '='); + if (p) { sscanf(p+1, " %lf/%lf/%lf", rmin, ravg, rmax); got=true; } + } + } + pclose(f); return got; +} + +static void plugin_ping_monitor(conn_t *c, const config_t *cfg) { + if (!cfg->ping_host_count) return; + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "ping_monitor"); + for (int i = 0; i < cfg->ping_host_count; i++) { + double rmin, ravg, rmax, loss; + bool ok = do_ping(cfg->ping_hosts[i], cfg->ping_count, cfg->ping_timeout, + &rmin, &ravg, &rmax, &loss); + char key[300]; + snprintf(key, sizeof(key), "%s", cfg->ping_hosts[i]); + for (char *p = key; *p; p++) if (!isalnum((unsigned char)*p) && *p!='_') *p='_'; + char kb[512]; + if (ok) { + snprintf(kb,sizeof(kb),"%s_rtt_min",key); kv_set_dbl(&d,kb,rmin); + snprintf(kb,sizeof(kb),"%s_rtt_avg",key); kv_set_dbl(&d,kb,ravg); + snprintf(kb,sizeof(kb),"%s_rtt_max",key); kv_set_dbl(&d,kb,rmax); + snprintf(kb,sizeof(kb),"%s_loss",key); kv_set_dbl(&d,kb,loss); + } else { + snprintf(kb,sizeof(kb),"%s_rtt_min",key); kv_set(&d,kb,"inf"); + snprintf(kb,sizeof(kb),"%s_rtt_avg",key); kv_set(&d,kb,"inf"); + snprintf(kb,sizeof(kb),"%s_rtt_max",key); kv_set(&d,kb,"inf"); + snprintf(kb,sizeof(kb),"%s_loss",key); kv_set_dbl(&d,kb,100.0); + } + } + kv_set_dbl(&d, "_timestamp", now_ts()); + conn_send(c, "PLG", &d); + LOGD("sent ping_monitor"); +} + +/* ============================================================ + * Plugin: nagios_runner + * ============================================================ */ + +static const char *nagios_status_str(int rc) { + switch (rc) { case 0:return"OK"; case 1:return"WARNING"; case 2:return"CRITICAL"; default:return"UNKNOWN"; } +} + +static void parse_perfdata(const char *output, kvdict_t *d, const char *prefix) { + const char *pipe = strchr(output, '|'); + if (!pipe) return; + const char *p = pipe + 1; + while (*p) { + while (*p==' ') p++; + if (!*p) break; + char label[128]; int li = 0; + if (*p == '\'') { + p++; + while (*p && *p != '\'') label[li++] = *p++; + if (*p == '\'') p++; + } else { + while (*p && *p != '=') label[li++] = *p++; + } + label[li] = '\0'; + if (*p != '=') { while (*p && *p!=' ') p++; continue; } + p++; + char val[64]; int vi = 0; + while (*p && (isdigit((unsigned char)*p) || *p=='.' || *p=='-')) val[vi++] = *p++; + val[vi] = '\0'; + char uom[16]; int ui = 0; + while (*p && *p!=';' && *p!=' ') uom[ui++] = *p++; + uom[ui] = '\0'; + char key[256]; + snprintf(key, sizeof(key), "%s_%s", prefix, label); kv_set(d, key, val); + if (uom[0]) { snprintf(key,sizeof(key),"%s_%s_uom",prefix,label); kv_set(d,key,uom); } + for (int n=0; n<4 && *p==';'; n++) { p++; while (*p && *p!=';' && *p!=' ') p++; } + while (*p==' ') p++; + } +} + +static void plugin_nagios_runner(conn_t *c, const config_t *cfg) { + if (!cfg->nagios_count) return; + kvdict_t d; kv_clear(&d); + kv_set(&d, "plugin", "nagios_runner"); + for (int i = 0; i < cfg->nagios_count; i++) { + const char *name = cfg->nagios[i].name; + char output[4096] = ""; int rc = 3; + FILE *f = popen(cfg->nagios[i].cmd, "r"); + if (f) { + size_t _nr = fread(output, 1, sizeof(output)-1, f); (void)_nr; + int ret = pclose(f); + rc = WIFEXITED(ret) ? WEXITSTATUS(ret) : 3; + if (rc < 0 || rc > 3) rc = 3; + } + char *nl = strchr(output, '\n'); if (nl) *nl = '\0'; + char msg[512]; char *pipe = strchr(output,'|'); + if (pipe) snprintf(msg, sizeof(msg), "%.*s", (int)(pipe-output), output); + else snprintf(msg, sizeof(msg), "%s", output); + char key[128]; + snprintf(key,sizeof(key),"%s_status",name); kv_set(&d,key,nagios_status_str(rc)); + snprintf(key,sizeof(key),"%s_status_code",name); kv_set_int(&d,key,rc); + snprintf(key,sizeof(key),"%s_output",name); kv_set(&d,key,msg); + parse_perfdata(output, &d, name); + } + kv_set_dbl(&d, "_timestamp", now_ts()); + conn_send(c, "PLG", &d); + LOGD("sent nagios_runner"); +} + +/* ============================================================ + * Monitor thread: all periodic plugins in one thread + * ============================================================ */ + +typedef struct { conn_t *conn; const config_t *cfg; } thread_ctx_t; + +static void *monitor_thread(void *arg) { + thread_ctx_t *ctx = arg; + conn_t *conn = ctx->conn; + const config_t *cfg = ctx->cfg; + + time_t next_cpu = time(NULL); + time_t next_mem = time(NULL); + time_t next_disk = time(NULL); + time_t next_net = time(NULL); + time_t next_ping = time(NULL); + time_t next_nagios = time(NULL); + + /* Prime network baseline */ + plugin_network_monitor(conn, cfg); + + while (g_running) { + time_t now = time(NULL); + if (now >= next_cpu) { plugin_cpu_monitor(conn, cfg); next_cpu = now + cfg->cpu_interval; } + if (now >= next_mem) { plugin_memory_monitor(conn, cfg); next_mem = now + cfg->mem_interval; } + if (now >= next_disk) { plugin_disk_monitor(conn, cfg); next_disk = now + cfg->disk_interval; } + if (now >= next_net) { plugin_network_monitor(conn, cfg); next_net = now + cfg->net_interval; } + if (cfg->ping_host_count && now >= next_ping) { + plugin_ping_monitor(conn, cfg); next_ping = now + cfg->ping_interval; + } + if (cfg->nagios_count && now >= next_nagios) { + plugin_nagios_runner(conn, cfg); next_nagios = now + cfg->nagios_interval; + } + sleep(1); + } + return NULL; +} + +/* ============================================================ + * Daemonize + * ============================================================ */ + +static void daemonize(void) { + for (int n = 0; n < 2; n++) { + pid_t pid = fork(); + if (pid < 0) { perror("fork"); exit(1); } + if (pid > 0) exit(0); + if (n == 0) { setsid(); if (chdir("/")) {}; umask(0); } + } + int fd; + fd = open("/dev/zero", O_RDONLY); if (fd>=0) { dup2(fd,0); close(fd); } + fd = open("/dev/null", O_WRONLY); if (fd>=0) { dup2(fd,1); close(fd); } + fd = open("/dev/null", O_WRONLY); if (fd>=0) { dup2(fd,2); close(fd); } +} + +/* ============================================================ + * CLI + * ============================================================ */ + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [options] host [host...]\n" + " -b Send boot message\n" + " -c FILE Config file (JSON)\n" + " -m MSG Send one-shot message\n" + " -n NAME Override hostname\n" + " -d Daemonize\n" + " -v Verbose (info)\n" + " -x Debug\n" + " -h Help\n", prog); +} + +int main(int argc, char **argv) { + bool do_boot = false; + bool do_daemon = false; + const char *cfgpath = NULL; + const char *message = NULL; + const char *nameov = NULL; + + int opt; + while ((opt = getopt(argc, argv, "bc:m:n:dvxh")) != -1) { + switch (opt) { + case 'b': do_boot = true; break; + case 'c': cfgpath = optarg; break; + case 'm': message = optarg; break; + case 'n': nameov = optarg; break; + case 'd': do_daemon = true; break; + case 'v': g_log_level = LL_INFO; break; + case 'x': g_log_level = LL_DEBUG; break; + case 'h': usage(argv[0]); return 0; + default: usage(argv[0]); return 1; + } + } + if (optind >= argc) { fprintf(stderr,"error: host required\n"); usage(argv[0]); return 1; } + + const char *hosts[MAX_HOSTS]; int nhost = 0; + for (int i = optind; i < argc && nhost < MAX_HOSTS; i++) hosts[nhost++] = argv[i]; + + config_t cfg; + config_load(&cfg, cfgpath); + + if (do_daemon) { + daemonize(); + g_use_syslog = true; + openlog("hbc", LOG_PID, LOG_DAEMON); + } + + char iam[256]; + if (nameov) { + snprintf(iam, sizeof(iam), "%s", nameov); + } else { + gethostname(iam, sizeof(iam)); + char *dot = strchr(iam, '.'); if (dot) *dot = '\0'; + } + + int conn_id = 1; + for (int i = 0; i < nhost; i++) { + struct addrinfo hints = {0}, *res = NULL; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + char ps[16]; snprintf(ps, sizeof(ps), "%d", cfg.hb_port); + if (getaddrinfo(hosts[i], ps, &hints, &res) != 0) { + LOGE("cannot resolve %s", hosts[i]); continue; + } + for (struct addrinfo *ai = res; ai && g_nconns < MAX_HOSTS; ai = ai->ai_next) { + conn_t *c = &g_conns[g_nconns]; + memset(c, 0, sizeof(*c)); + c->conn_id = conn_id++; c->port = cfg.hb_port; + c->af = ai->ai_family; c->sockfd = -1; + snprintf(c->name, sizeof(c->name), "%s", iam); + void *addr = (ai->ai_family == AF_INET) + ? (void *)&((struct sockaddr_in *)ai->ai_addr)->sin_addr + : (void *)&((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr; + inet_ntop(ai->ai_family, addr, c->addr, sizeof(c->addr)); + if (conn_open(c)) { g_nconns++; LOGI("connected to %s", c->addr); } + } + freeaddrinfo(res); + } + if (!g_nconns) { LOGE("no connections established"); return 1; } + + struct sigaction sa = {0}; + sa.sa_handler = sig_handler; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGHUP, &sa, NULL); + + conn_t *primary = &g_conns[0]; + LOGI("hbc_mini-c %s on %s -> %s port=%d interval=%ds", + HBC_VERSION, iam, hosts[0], cfg.hb_port, cfg.interval); + + /* Boot / one-shot message */ + bool send_shutdown = false; + if (do_boot || message) { + kvdict_t bm; kv_clear(&bm); + kv_set_int(&bm, "acks", 0); + if (do_boot) { kv_set_int(&bm,"boot",1); send_shutdown=true; } + if (message) { kv_set(&bm,"service","service"); kv_set(&bm,"msg",message); } + conn_send(primary, "HTB", &bm); + if (message && !do_daemon) { + usleep(300000); + for (int i=0;i= cfg.interval) { + for (int i = 0; i < g_nconns; i++) { + kvdict_t hb; kv_clear(&hb); + kv_set_int(&hb, "acks", g_conns[i].ack_count); + kv_set_dbl(&hb, "rtt", g_conns[i].rtt); + kv_set_int(&hb, "interval", cfg.interval); + conn_send(&g_conns[i], "HTB", &hb); + } + last_hb = now; + } + + fd_set rset; FD_ZERO(&rset); int maxfd = -1; + for (int i = 0; i < g_nconns; i++) { + if (g_conns[i].sockfd >= 0) { FD_SET(g_conns[i].sockfd,&rset); if(g_conns[i].sockfd>maxfd) maxfd=g_conns[i].sockfd; } + } + if (maxfd >= 0) { + struct timeval tv = {1, 0}; + if (select(maxfd+1, &rset, NULL, NULL, &tv) > 0) + for (int i = 0; i < g_nconns; i++) + if (g_conns[i].sockfd>=0 && FD_ISSET(g_conns[i].sockfd,&rset)) + conn_recv(&g_conns[i]); + } else { sleep(1); } + + if (primary->request_info) { + primary->request_info = false; + LOGI("refreshing info plugins on server request"); + plugin_os_info(primary, &cfg); + } + } + + pthread_cancel(tid); + pthread_join(tid, NULL); + + if (send_shutdown) { + kvdict_t sh; kv_clear(&sh); + kv_set_int(&sh, "shutdown", 1); + kv_set_int(&sh, "acks", primary->ack_count); + conn_send(primary, "HTB", &sh); + usleep(300000); + } + for (int i = 0; i < g_nconns; i++) conn_close(&g_conns[i]); + if (g_use_syslog) closelog(); + if (g_restart) { LOGI("restarting..."); execv(argv[0], argv); } + return 0; +}