2026-06-20 21:21:20 +03:00
|
|
|
/* core.c — core lifecycle and registration of adapters/controls.
|
|
|
|
|
* The loop and pumps live in loop.c. */
|
|
|
|
|
#include "core_internal.h"
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <sys/epoll.h>
|
|
|
|
|
#include <sys/eventfd.h>
|
|
|
|
|
|
|
|
|
|
core_slot* core_register_fd(vmsig_core* c, int fd, uint32_t epoll_events, slot_role role) {
|
|
|
|
|
/* reuse a detached (SLOT_DEAD) slot so c->slots[] does not grow on every
|
|
|
|
|
* connection */
|
|
|
|
|
core_slot* s = NULL;
|
|
|
|
|
for (int i = 0; i < c->nslots; i++)
|
|
|
|
|
if (c->slots[i]->role == SLOT_DEAD) { s = c->slots[i]; break; }
|
|
|
|
|
|
|
|
|
|
if (!s) {
|
|
|
|
|
if (c->nslots == c->cap_slots) {
|
|
|
|
|
int ncap = c->cap_slots ? c->cap_slots * 2 : 16;
|
|
|
|
|
core_slot** ns = realloc(c->slots, (size_t)ncap * sizeof *ns);
|
|
|
|
|
if (!ns) return NULL;
|
|
|
|
|
c->slots = ns;
|
|
|
|
|
c->cap_slots = ncap;
|
|
|
|
|
}
|
|
|
|
|
s = calloc(1, sizeof *s);
|
|
|
|
|
if (!s) return NULL;
|
|
|
|
|
c->slots[c->nslots++] = s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(s, 0, sizeof *s);
|
|
|
|
|
s->role = role;
|
|
|
|
|
s->fd = fd;
|
|
|
|
|
|
|
|
|
|
struct epoll_event ee;
|
|
|
|
|
memset(&ee, 0, sizeof ee);
|
|
|
|
|
ee.events = epoll_events;
|
|
|
|
|
ee.data.ptr = s;
|
|
|
|
|
if (epoll_ctl(c->epfd, EPOLL_CTL_ADD, fd, &ee) < 0) { s->role = SLOT_DEAD; return NULL; }
|
|
|
|
|
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vmsig_core* vmsig_core_new(vmsig_ctx* ctx) {
|
|
|
|
|
if (!ctx) return NULL;
|
|
|
|
|
vmsig_core* c = calloc(1, sizeof *c);
|
|
|
|
|
if (!c) return NULL;
|
|
|
|
|
c->ctx = ctx;
|
|
|
|
|
c->epfd = -1;
|
|
|
|
|
c->wake_fd = -1;
|
|
|
|
|
|
|
|
|
|
c->epfd = epoll_create1(EPOLL_CLOEXEC);
|
|
|
|
|
if (c->epfd < 0) { free(c); return NULL; }
|
|
|
|
|
|
|
|
|
|
c->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
|
|
|
|
|
if (c->wake_fd < 0) { close(c->epfd); free(c); return NULL; }
|
|
|
|
|
if (!core_register_fd(c, c->wake_fd, EPOLLIN, SLOT_WAKEUP)) {
|
|
|
|
|
close(c->wake_fd); close(c->epfd); free(c); return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* context pacing timerfds (created in ctx_new) as loop sources */
|
|
|
|
|
for (int d = VMSIG_DIR_UP; d <= VMSIG_DIR_DOWN; d++) {
|
|
|
|
|
int tfd = vmsig_ctx_timing_fd(ctx, (vmsig_dir)d);
|
|
|
|
|
if (tfd >= 0) core_register_fd(c, tfd, EPOLLIN, SLOT_CTX_TIMING);
|
|
|
|
|
}
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int vmsig_core_add_adapter(vmsig_core* c, const vmsig_adapter_ops* ops,
|
|
|
|
|
const void* cfg, uint32_t endpoint) {
|
2026-06-22 17:25:06 +03:00
|
|
|
if (!c || !ops) return -1;
|
|
|
|
|
|
|
|
|
|
/* Reuse a reaped (inactive) adapter entry so runtime detach/re-attach churn does
|
|
|
|
|
* not exhaust the fixed table; otherwise grow up to the ceiling. */
|
|
|
|
|
int id = -1;
|
|
|
|
|
for (int i = 0; i < c->nadapters; i++)
|
|
|
|
|
if (!c->adapters[i].active) { id = i; break; }
|
|
|
|
|
if (id < 0) {
|
|
|
|
|
if (c->nadapters >= VMSIG_MAX_ADAPTERS) return -1;
|
|
|
|
|
id = c->nadapters++;
|
|
|
|
|
}
|
|
|
|
|
core_adapter_ent* e = &c->adapters[id];
|
|
|
|
|
uint16_t gen = e->gen; /* generation survives the memset below */
|
2026-06-20 21:21:20 +03:00
|
|
|
|
|
|
|
|
vmsig_adapter* a = ops->open(cfg, endpoint);
|
2026-06-22 17:25:06 +03:00
|
|
|
if (!a) return -1; /* entry stays inactive (reusable) */
|
2026-06-20 21:21:20 +03:00
|
|
|
|
|
|
|
|
vmsig_emit emit = { core_emit_up, core_register_memctx, core_unregister_memctx, c };
|
|
|
|
|
vmsig_fd_reg reg[VMSIG_ADAPTER_FDS];
|
|
|
|
|
memset(reg, 0, sizeof reg);
|
|
|
|
|
|
|
|
|
|
int n = ops->attach(a, &emit, reg, VMSIG_ADAPTER_FDS);
|
|
|
|
|
if (n < 0) { ops->close(a); return -1; }
|
|
|
|
|
|
2026-06-22 17:25:06 +03:00
|
|
|
memset(e, 0, sizeof *e);
|
|
|
|
|
e->ops = ops;
|
|
|
|
|
e->a = a;
|
|
|
|
|
e->endpoint = endpoint;
|
|
|
|
|
e->active = 1;
|
|
|
|
|
e->gen = (uint16_t)(gen + 1);
|
|
|
|
|
e->nslot = 0;
|
|
|
|
|
|
2026-06-20 21:21:20 +03:00
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
|
uint32_t events = reg[i].epoll_events ? reg[i].epoll_events : (uint32_t)EPOLLIN;
|
|
|
|
|
core_slot* s = core_register_fd(c, reg[i].fd, events, SLOT_ADAPTER);
|
2026-06-22 17:25:06 +03:00
|
|
|
if (!s) {
|
|
|
|
|
/* roll back: deregister the fds enrolled so far, then close + free the entry. */
|
|
|
|
|
for (int k = 0; k < e->nslot; k++) {
|
|
|
|
|
epoll_ctl(c->epfd, EPOLL_CTL_DEL, e->slots[k]->fd, NULL);
|
|
|
|
|
e->slots[k]->role = SLOT_DEAD;
|
|
|
|
|
}
|
|
|
|
|
ops->close(a);
|
|
|
|
|
e->active = 0; e->a = NULL; e->nslot = 0;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
2026-06-20 21:21:20 +03:00
|
|
|
s->ops = ops;
|
|
|
|
|
s->adapter = a;
|
|
|
|
|
s->cookie = reg[i].cookie;
|
2026-06-22 17:25:06 +03:00
|
|
|
if (e->nslot < VMSIG_ADAPTER_FDS) e->slots[e->nslot++] = s;
|
2026-06-20 21:21:20 +03:00
|
|
|
}
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 17:25:06 +03:00
|
|
|
/* Request runtime detach of every adapter on `endpoint` (deferred reap after the batch,
|
|
|
|
|
* mirrors core_request_drop). The teardown itself (epoch settle, SEAM_DOWN, lease release,
|
|
|
|
|
* epoll DEL, ops->close) runs in core_reap_adapters on the loop thread. */
|
|
|
|
|
void vmsig_core_detach_endpoint(vmsig_core* c, uint32_t endpoint) {
|
|
|
|
|
if (!c || endpoint >= 64) return;
|
|
|
|
|
int any = 0;
|
|
|
|
|
for (int i = 0; i < c->nadapters; i++) {
|
|
|
|
|
core_adapter_ent* e = &c->adapters[i];
|
|
|
|
|
if (e->active && e->endpoint == endpoint) { e->reap = 1; any = 1; }
|
|
|
|
|
}
|
|
|
|
|
if (any) core_wake(c);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 21:21:20 +03:00
|
|
|
int vmsig_core_add_control(vmsig_core* c, const vmsig_control_ops* ops, void* ctl,
|
|
|
|
|
const vmsig_grant* grant) {
|
|
|
|
|
if (!c || !ops) return -1;
|
|
|
|
|
|
|
|
|
|
/* reuse a freed (reaped) slot; otherwise grow up to the ceiling */
|
|
|
|
|
int id = -1;
|
|
|
|
|
for (int i = 0; i < c->ncontrols; i++)
|
|
|
|
|
if (!c->controls[i].active) { id = i; break; }
|
|
|
|
|
if (id < 0) {
|
|
|
|
|
if (c->ncontrols >= VMSIG_MAX_CONTROLS) return -1;
|
|
|
|
|
id = c->ncontrols++;
|
|
|
|
|
}
|
|
|
|
|
core_control_ent* e = &c->controls[id];
|
|
|
|
|
uint16_t gen = e->gen; /* generation survives the slot memset */
|
|
|
|
|
memset(e, 0, sizeof *e);
|
|
|
|
|
e->gen = (uint16_t)(gen + 1); /* new generation for this (re)use */
|
|
|
|
|
e->ops = ops;
|
|
|
|
|
e->ctl = ctl;
|
|
|
|
|
e->active = 1;
|
|
|
|
|
if (grant) e->grant = *grant; /* otherwise stays zero => default-deny */
|
|
|
|
|
e->dctx.core = c;
|
|
|
|
|
e->dctx.ctl_id = id;
|
|
|
|
|
|
|
|
|
|
if (ops->subscribe) ops->subscribe(ctl, &e->sub);
|
|
|
|
|
/* emit_down token is our down_ctx, so emit_down can find this control's grant */
|
|
|
|
|
if (ops->set_emit_down) ops->set_emit_down(ctl, core_emit_down, &e->dctx);
|
|
|
|
|
|
|
|
|
|
int fd = ops->fd ? ops->fd(ctl) : -1;
|
|
|
|
|
if (fd >= 0) {
|
|
|
|
|
core_slot* s = core_register_fd(c, fd, EPOLLIN, SLOT_CONTROL);
|
|
|
|
|
if (!s) return -1;
|
|
|
|
|
s->cops = ops;
|
|
|
|
|
s->ctl = ctl;
|
|
|
|
|
e->slot = s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Late subscriber: replay retained MEMCTX (if a context is already published and
|
|
|
|
|
* this control is qualified). For a control added BEFORE the first publication,
|
|
|
|
|
* the cell is not yet valid — it receives MEMCTX via the normal multicast in pump_up. */
|
|
|
|
|
core_memctx_replay(c, id);
|
2026-06-22 17:25:06 +03:00
|
|
|
core_roster_replay(c, id); /* late subscriber: retained VM roster (CAP_ROSTER) */
|
2026-06-20 21:21:20 +03:00
|
|
|
|
|
|
|
|
return id; /* ncontrols already bumped when picking id (on growth); reuse does not grow it */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== MEMCTX registration: per-endpoint retain cell (called by the adapter on the loop thread) =====
|
|
|
|
|
* Registers the address-space context adapter's reg hooks. The core holds THIS and does
|
|
|
|
|
* NOT store a copy of the locator: on delivery/replay it calls reg.describe/share_fd.
|
|
|
|
|
* valid/epoch are maintained in route/epoch_bump (not here): register only records that
|
|
|
|
|
* "the adapter is connected". */
|
|
|
|
|
int core_register_memctx(void* token, const vmsig_memctx_reg* reg) {
|
|
|
|
|
vmsig_core* c = token;
|
|
|
|
|
if (!c || !reg || reg->endpoint >= 64) return -1;
|
|
|
|
|
core_memctx_cell* cell = &c->memctx[reg->endpoint];
|
|
|
|
|
cell->reg = *reg;
|
|
|
|
|
cell->registered = 1;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void core_unregister_memctx(void* token, uint32_t endpoint) {
|
|
|
|
|
vmsig_core* c = token;
|
|
|
|
|
if (!c || endpoint >= 64) return;
|
|
|
|
|
core_memctx_cell* cell = &c->memctx[endpoint];
|
|
|
|
|
cell->registered = 0;
|
|
|
|
|
cell->valid = 0;
|
|
|
|
|
memset(&cell->reg, 0, sizeof cell->reg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void vmsig_core_set_audit(vmsig_core* c, void (*cb)(void* ud, const vmsig_audit* a), void* ud) {
|
|
|
|
|
if (!c) return;
|
|
|
|
|
c->audit_cb = cb;
|
|
|
|
|
c->audit_ud = ud;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void core_audit(vmsig_core* c, const vmsig_audit* a) {
|
|
|
|
|
if (c && c->audit_cb) c->audit_cb(c->audit_ud, a);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void vmsig_core_set_arb_policy(vmsig_core* c, vmsig_arb_policy cb, void* ud) {
|
|
|
|
|
if (!c) return;
|
|
|
|
|
c->arb_cb = cb;
|
|
|
|
|
c->arb_ud = ud;
|
|
|
|
|
/* lease[][] is zeroed in vmsig_core_new (calloc) => all cells free. */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int core_add_source(vmsig_core* c, int fd, void (*cb)(void* user, uint32_t events),
|
|
|
|
|
void* user, void (*on_free)(void* user)) {
|
|
|
|
|
if (!c || fd < 0 || !cb) return -1;
|
|
|
|
|
core_slot* s = core_register_fd(c, fd, EPOLLIN, SLOT_SOURCE);
|
|
|
|
|
if (!s) return -1;
|
|
|
|
|
s->on_source = cb;
|
|
|
|
|
s->on_free = on_free;
|
|
|
|
|
s->source_user = user;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void core_request_drop(vmsig_core* c, int ctl_id) {
|
|
|
|
|
if (!c || ctl_id < 0 || ctl_id >= c->ncontrols) return;
|
|
|
|
|
c->controls[ctl_id].reap = 1;
|
|
|
|
|
core_wake(c); /* wake the loop for a reap pass (without stop) */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void vmsig_core_free(vmsig_core* c) {
|
|
|
|
|
if (!c) return;
|
|
|
|
|
/* graceful: stop workers and close SI handles / sockets. Adapters are closed
|
|
|
|
|
* FIRST: their close stops off-loop workers and unregisters their seams (e.g.
|
|
|
|
|
* memctx) BEFORE destruction. */
|
|
|
|
|
for (int i = 0; i < c->nadapters; i++)
|
2026-06-22 17:25:06 +03:00
|
|
|
if (c->adapters[i].active && c->adapters[i].ops->close)
|
|
|
|
|
c->adapters[i].ops->close(c->adapters[i].a);
|
2026-06-20 21:21:20 +03:00
|
|
|
for (int i = 0; i < c->ncontrols; i++)
|
|
|
|
|
if (c->controls[i].active && c->controls[i].ops->close)
|
|
|
|
|
c->controls[i].ops->close(c->controls[i].ctl);
|
|
|
|
|
|
|
|
|
|
/* cleanup of fd sources (e.g. unix listener: close listen/janitor fd + free) */
|
|
|
|
|
for (int i = 0; i < c->nslots; i++)
|
|
|
|
|
if (c->slots[i]->role == SLOT_SOURCE && c->slots[i]->on_free)
|
|
|
|
|
c->slots[i]->on_free(c->slots[i]->source_user);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < c->nslots; i++) free(c->slots[i]);
|
|
|
|
|
free(c->slots);
|
|
|
|
|
if (c->wake_fd >= 0) close(c->wake_fd);
|
|
|
|
|
if (c->epfd >= 0) close(c->epfd);
|
|
|
|
|
/* ctx is not ours: its owner frees it */
|
|
|
|
|
free(c);
|
|
|
|
|
}
|