summaryrefslogtreecommitdiff
path: root/src/core/monitor.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /src/core/monitor.c
#2
Diffstat (limited to 'src/core/monitor.c')
-rw-r--r--src/core/monitor.c497
1 files changed, 497 insertions, 0 deletions
diff --git a/src/core/monitor.c b/src/core/monitor.c
new file mode 100644
index 0000000..0b6d0c5
--- /dev/null
+++ b/src/core/monitor.c
@@ -0,0 +1,497 @@
+#if defined(__linux__)
+#define _DEFAULT_SOURCE
+#endif
+
+#include "hybbx/monitor.h"
+#include "hybbx/auth.h"
+#include "hybbx/config.h"
+#include "hybbx/log.h"
+#include "hybbx/security.h"
+#include "hybbx/service.h"
+#include "hybbx/session.h"
+#include "hybbx/util.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#define MONITOR_LINE_MAX 512u
+#define MONITOR_READ_CHUNK 4096u
+
+typedef struct monitor_tail {
+ char path[HYBBX_PATH_MAX];
+ off_t offset;
+ int active;
+ char carry[MONITOR_LINE_MAX];
+ size_t carry_len;
+} monitor_tail_t;
+
+static hybbx_monitor_config_t g_mon_cfg;
+static int g_mon_ready;
+static monitor_tail_t g_tail_hybbx;
+static monitor_tail_t g_tail_security;
+static int g_any_monitor_active;
+
+void hybbx_monitor_config_defaults(hybbx_monitor_config_t *cfg)
+{
+ if (cfg == NULL) {
+ return;
+ }
+
+ memset(cfg, 0, sizeof(*cfg));
+ cfg->enabled = 1;
+ cfg->follow_hybbx = 1;
+ cfg->follow_security = 1;
+ cfg->invisible_sysop = 0;
+ cfg->invite_timeout_sec = 20u;
+}
+
+static void monitor_parse_allow(hybbx_monitor_config_t *cfg, const char *raw)
+{
+ char buf[HYBBX_CONFIG_VALUE_MAX];
+ char *p;
+ char *comma;
+
+ cfg->allow_count = 0;
+ if (raw == NULL || raw[0] == '\0') {
+ return;
+ }
+
+ hybbx_strlcpy(buf, raw, sizeof(buf));
+ p = buf;
+ while (p != NULL && *p != '\0' &&
+ cfg->allow_count < HYBBX_MONITOR_ALLOW_MAX) {
+ char *tok = p;
+ comma = strchr(p, ',');
+ if (comma != NULL) {
+ *comma = '\0';
+ p = comma + 1;
+ } else {
+ p = NULL;
+ }
+ while (*tok == ' ' || *tok == '\t') {
+ tok++;
+ }
+ if (*tok == '\0') {
+ continue;
+ }
+ {
+ char *end = tok + strlen(tok);
+ while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) {
+ *--end = '\0';
+ }
+ }
+ if (tok[0] == '\0') {
+ continue;
+ }
+ hybbx_strlcpy(cfg->allow[cfg->allow_count], tok,
+ sizeof(cfg->allow[cfg->allow_count]));
+ cfg->allow_count++;
+ }
+}
+
+void hybbx_monitor_config_apply(const struct hybbx_config *config)
+{
+ const char *allow_raw;
+
+ hybbx_monitor_shutdown();
+ hybbx_monitor_config_defaults(&g_mon_cfg);
+
+ if (config != NULL) {
+ g_mon_cfg.enabled =
+ hybbx_config_get_bool(config, "monitor", "enabled", 1);
+ g_mon_cfg.follow_hybbx =
+ hybbx_config_get_bool(config, "monitor", "follow_hybbx", 1);
+ g_mon_cfg.follow_security =
+ hybbx_config_get_bool(config, "monitor", "follow_security", 1);
+ /* Prefer hyphen key; accept underscore alias. */
+ if (hybbx_config_get(config, "monitor", "invisible-sysop", NULL) !=
+ NULL) {
+ g_mon_cfg.invisible_sysop = hybbx_config_get_bool(
+ config, "monitor", "invisible-sysop", 0);
+ } else {
+ g_mon_cfg.invisible_sysop = hybbx_config_get_bool(
+ config, "monitor", "invisible_sysop", 0);
+ }
+ g_mon_cfg.invite_timeout_sec = hybbx_config_get_uint(
+ config, "monitor", "invite_timeout_sec", 20u, 5u, 300u);
+ allow_raw = hybbx_config_get(config, "monitor", "allow", NULL);
+ monitor_parse_allow(&g_mon_cfg, allow_raw);
+ }
+
+ g_mon_ready = 1;
+ hybbx_log_info("[monitor] enabled=%s follow_hybbx=%s follow_security=%s "
+ "invisible-sysop=%s invite_timeout_sec=%u allow=%u",
+ g_mon_cfg.enabled ? "yes" : "no",
+ g_mon_cfg.follow_hybbx ? "yes" : "no",
+ g_mon_cfg.follow_security ? "yes" : "no",
+ g_mon_cfg.invisible_sysop ? "yes" : "no",
+ g_mon_cfg.invite_timeout_sec,
+ g_mon_cfg.allow_count);
+}
+
+const hybbx_monitor_config_t *hybbx_monitor_config_get(void)
+{
+ return g_mon_ready ? &g_mon_cfg : NULL;
+}
+
+int hybbx_monitor_enabled(void)
+{
+ return g_mon_ready && g_mon_cfg.enabled;
+}
+
+int hybbx_monitor_invisible_sysop(void)
+{
+ return g_mon_ready && g_mon_cfg.invisible_sysop;
+}
+
+int hybbx_monitor_user_allowed(const char *username)
+{
+ unsigned i;
+
+ if (!g_mon_ready || username == NULL || username[0] == '\0') {
+ return 0;
+ }
+
+ for (i = 0; i < g_mon_cfg.allow_count; i++) {
+ if (strcasecmp(g_mon_cfg.allow[i], username) == 0) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+int hybbx_monitor_session_may_use(const struct hybbx_session *session)
+{
+ const char *user;
+
+ if (!hybbx_monitor_enabled() || session == NULL) {
+ return 0;
+ }
+
+ if (hybbx_user_level_is_sysop(hybbx_session_user_level(session))) {
+ return 1;
+ }
+
+ user = hybbx_session_username(session);
+ return hybbx_monitor_user_allowed(user);
+}
+
+typedef struct mon_count_ctx {
+ unsigned count;
+} mon_count_ctx_t;
+
+static void mon_count_visitor(hybbx_session_t *session, void *userdata)
+{
+ mon_count_ctx_t *ctx = (mon_count_ctx_t *)userdata;
+
+ if (session != NULL && hybbx_monitor_is_active(session)) {
+ ctx->count++;
+ }
+}
+
+static void monitor_refresh_active_flag(hybbx_service_t *service)
+{
+ mon_count_ctx_t ctx;
+
+ ctx.count = 0;
+ if (service != NULL) {
+ hybbx_service_visit_sessions(service, mon_count_visitor, &ctx);
+ }
+ g_any_monitor_active = ctx.count > 0;
+}
+
+static void monitor_tail_seek_end(monitor_tail_t *tail, const char *path)
+{
+ struct stat st;
+
+ tail->path[0] = '\0';
+ tail->offset = 0;
+ tail->active = 0;
+
+ if (path == NULL || path[0] == '\0') {
+ return;
+ }
+
+ hybbx_strlcpy(tail->path, path, sizeof(tail->path));
+ if (stat(tail->path, &st) != 0) {
+ return;
+ }
+
+ tail->offset = st.st_size;
+ tail->active = 1;
+}
+
+static void monitor_reset_tails(void)
+{
+ char path[HYBBX_PATH_MAX];
+
+ memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx));
+ memset(&g_tail_security, 0, sizeof(g_tail_security));
+
+ if (g_mon_cfg.follow_hybbx &&
+ hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK) {
+ monitor_tail_seek_end(&g_tail_hybbx, path);
+ }
+ if (g_mon_cfg.follow_security &&
+ hybbx_security_log_current_path(path, sizeof(path)) == HYBBX_OK) {
+ monitor_tail_seek_end(&g_tail_security, path);
+ }
+}
+
+hybbx_result_t hybbx_monitor_set_active(hybbx_session_t *session, int on)
+{
+ hybbx_service_t *service;
+
+ if (session == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (!hybbx_monitor_enabled()) {
+ hybbx_session_write_line(session, "Monitor is disabled in config.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ hybbx_session_set_monitor_active(session, on ? 1 : 0);
+ service = hybbx_session_service(session);
+ monitor_refresh_active_flag(service);
+
+ if (on) {
+ /* Fresh follow from current EOF — no backlog. */
+ monitor_reset_tails();
+ hybbx_session_write_line(session, "Monitor on.");
+ if (hybbx_user_level_is_sysop(hybbx_session_user_level(session))) {
+ if (hybbx_monitor_invisible_sysop()) {
+ hybbx_session_write_line(session,
+ "Sysop: hidden from /who (/monitor invisible-sysop=yes).");
+ } else {
+ hybbx_session_write_line(session,
+ "Sysop: hidden from /who and /online while monitor is on.");
+ }
+ }
+ } else {
+ hybbx_session_write_line(session, "Monitor off.");
+ monitor_refresh_active_flag(service);
+ if (!g_any_monitor_active) {
+ memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx));
+ memset(&g_tail_security, 0, sizeof(g_tail_security));
+ }
+ }
+
+ return HYBBX_OK;
+}
+
+int hybbx_monitor_is_active(const hybbx_session_t *session)
+{
+ return hybbx_session_monitor_active(session);
+}
+
+typedef struct mon_bcast_ctx {
+ const char *line;
+ hybbx_session_t *skip;
+} mon_bcast_ctx_t;
+
+static void mon_bcast_visitor(hybbx_session_t *session, void *userdata)
+{
+ mon_bcast_ctx_t *ctx = (mon_bcast_ctx_t *)userdata;
+ char out[MONITOR_LINE_MAX + 16];
+
+ if (session == NULL || ctx == NULL || ctx->line == NULL) {
+ return;
+ }
+ if (session == ctx->skip) {
+ return;
+ }
+ if (!hybbx_monitor_is_active(session)) {
+ return;
+ }
+
+ snprintf(out, sizeof(out), "[monitor] %s", ctx->line);
+ hybbx_session_write_line(session, out);
+}
+
+void hybbx_monitor_broadcast(hybbx_service_t *service, const char *line)
+{
+ mon_bcast_ctx_t ctx;
+
+ if (service == NULL || line == NULL || line[0] == '\0') {
+ return;
+ }
+
+ ctx.line = line;
+ ctx.skip = NULL;
+ hybbx_service_visit_sessions(service, mon_bcast_visitor, &ctx);
+}
+
+void hybbx_monitor_event(hybbx_service_t *service,
+ const char *username,
+ const char *plugin,
+ const char *detail)
+{
+ char line[MONITOR_LINE_MAX];
+ const char *user = (username != NULL && username[0] != '\0') ? username : "?";
+ const char *plug = (plugin != NULL && plugin[0] != '\0') ? plugin : "?";
+
+ if (service == NULL || !g_any_monitor_active) {
+ /* Still compute flag lazily */
+ monitor_refresh_active_flag(service);
+ if (!g_any_monitor_active) {
+ return;
+ }
+ }
+
+ if (detail != NULL && detail[0] != '\0') {
+ snprintf(line, sizeof(line), "%s@%s %s", user, plug, detail);
+ } else {
+ snprintf(line, sizeof(line), "%s@%s", user, plug);
+ }
+
+ hybbx_monitor_broadcast(service, line);
+}
+
+static void monitor_push_raw_line(hybbx_service_t *service, const char *tag,
+ const char *text)
+{
+ char line[MONITOR_LINE_MAX];
+
+ if (text == NULL) {
+ return;
+ }
+ while (*text == '\r' || *text == '\n') {
+ text++;
+ }
+ if (*text == '\0') {
+ return;
+ }
+
+ snprintf(line, sizeof(line), "%s %s", tag, text);
+ hybbx_monitor_broadcast(service, line);
+}
+
+static void monitor_tail_read(hybbx_service_t *service, monitor_tail_t *tail,
+ const char *tag)
+{
+ FILE *fp;
+ struct stat st;
+ char chunk[MONITOR_READ_CHUNK];
+ size_t n;
+ char *p;
+ char *nl;
+
+ if (tail == NULL || !tail->active || tail->path[0] == '\0') {
+ return;
+ }
+
+ if (stat(tail->path, &st) != 0) {
+ return;
+ }
+
+ if (st.st_size < tail->offset) {
+ /* Truncated / rotated — follow from start of new content. */
+ tail->offset = 0;
+ tail->carry_len = 0;
+ }
+
+ if (st.st_size == tail->offset) {
+ return;
+ }
+
+ fp = fopen(tail->path, "r");
+ if (fp == NULL) {
+ return;
+ }
+
+ if (fseeko(fp, tail->offset, SEEK_SET) != 0) {
+ fclose(fp);
+ return;
+ }
+
+ while ((n = fread(chunk, 1, sizeof(chunk) - 1, fp)) > 0) {
+ chunk[n] = '\0';
+ p = chunk;
+ while (*p != '\0') {
+ nl = strchr(p, '\n');
+ if (nl == NULL) {
+ size_t left = strlen(p);
+ if (tail->carry_len + left >= sizeof(tail->carry)) {
+ tail->carry_len = 0;
+ }
+ memcpy(tail->carry + tail->carry_len, p, left);
+ tail->carry_len += left;
+ tail->carry[tail->carry_len] = '\0';
+ break;
+ }
+ *nl = '\0';
+ if (tail->carry_len > 0) {
+ size_t room = sizeof(tail->carry) - 1u - tail->carry_len;
+ size_t take = strlen(p);
+ if (take > room) {
+ take = room;
+ }
+ memcpy(tail->carry + tail->carry_len, p, take);
+ tail->carry_len += take;
+ tail->carry[tail->carry_len] = '\0';
+ monitor_push_raw_line(service, tag, tail->carry);
+ tail->carry_len = 0;
+ } else {
+ monitor_push_raw_line(service, tag, p);
+ }
+ p = nl + 1;
+ }
+ tail->offset = ftello(fp);
+ }
+
+ fclose(fp);
+
+ /* Refresh path if weekly rotate changed hybbx log name. */
+ if (tail == &g_tail_hybbx) {
+ char path[HYBBX_PATH_MAX];
+ if (hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK &&
+ strcmp(path, tail->path) != 0) {
+ monitor_tail_seek_end(tail, path);
+ }
+ }
+}
+
+void hybbx_monitor_tick(hybbx_service_t *service)
+{
+ if (!g_mon_ready || !g_mon_cfg.enabled || service == NULL) {
+ return;
+ }
+
+ monitor_refresh_active_flag(service);
+ if (!g_any_monitor_active) {
+ return;
+ }
+
+ if (!g_tail_hybbx.active && g_mon_cfg.follow_hybbx) {
+ char path[HYBBX_PATH_MAX];
+ if (hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK) {
+ monitor_tail_seek_end(&g_tail_hybbx, path);
+ }
+ }
+ if (!g_tail_security.active && g_mon_cfg.follow_security) {
+ char path[HYBBX_PATH_MAX];
+ if (hybbx_security_log_current_path(path, sizeof(path)) == HYBBX_OK) {
+ monitor_tail_seek_end(&g_tail_security, path);
+ }
+ }
+
+ if (g_mon_cfg.follow_hybbx) {
+ monitor_tail_read(service, &g_tail_hybbx, "hybbx:");
+ }
+ if (g_mon_cfg.follow_security) {
+ monitor_tail_read(service, &g_tail_security, "security:");
+ }
+}
+
+void hybbx_monitor_shutdown(void)
+{
+ memset(&g_mon_cfg, 0, sizeof(g_mon_cfg));
+ memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx));
+ memset(&g_tail_security, 0, sizeof(g_tail_security));
+ g_mon_ready = 0;
+ g_any_monitor_active = 0;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com