/* 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_sysctl) * ============================================================ */ /* 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_sysctl uvm; size_t len = sizeof(uvm); int mib[2] = {CTL_VM, VM_UVMEXP2}; 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" " -4 Use IPv4 only\n" " -6 Use IPv6 only\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 af_filter = 0; int opt; while ((opt = getopt(argc, argv, "bc:m:n:dvxh46")) != -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 '4': af_filter = AF_INET; break; case '6': af_filter = AF_INET6; 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'; } struct sigaction sa = {0}; sa.sa_handler = sig_handler; sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGHUP, &sa, NULL); int conn_id = 1; int retry_delay = 5; while (g_running && !g_nconns) { for (int i = 0; i < nhost; i++) { struct addrinfo hints = {0}, *res = NULL; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; hints.ai_family = af_filter; char ps[16]; snprintf(ps, sizeof(ps), "%d", cfg.hb_port); if (getaddrinfo(hosts[i], ps, &hints, &res) != 0) { LOGW("cannot resolve %s — retrying in %ds", hosts[i], retry_delay); 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) { sleep(retry_delay); if (retry_delay < 60) retry_delay *= 2; } } if (!g_nconns) return 1; 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; }