mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-26 04:36:37 +03:00
vmsig: a neutral signaling layer between sensors/input and controls
An epoll-driven, neutral transfer-event bus that connects sensors and input actuators to one or more controls, bidirectionally. It owns the transfer context and events — delivery order, priority, protocol-level timing, and an interrupt-driven event model over fd sources (eventfd/timerfd/sockets) — and stays agnostic to both the sensor/input drivers and the control. What lives here: - memctx: a coherent address-space context per endpoint — the guest address-space root paired with a pre-opened read-only RAM-region fd, with per-endpoint epoch invalidation and retained replay to late subscribers. Perception lives in out-of-tree sensor libraries that consume this datum read-only. - exclusive-ownership leases for destructive resource classes (input, power, memory-write). - write-signaled memory writes (MEMWRITE): an atomic write to guest memory routed through the seam under an exclusive lease, never a writable mapping. - a host-management seam for VM lifecycle/status and a neutral input-injection command path. - multi-VM endpoints; capability-gated, audited control authorization over an in-process or unix-socket transport. Builds against headers only by default (a stub mode that exercises the seam without a VM); armed builds link the real sensor/input libraries behind flags.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
/* input.c — input/actuator adapter for vmctl (input + power/lifecycle).
|
||||
*
|
||||
* Mechanism (recommended): vmctl is a blocking QMP round-trip; we run it on a
|
||||
* worker thread, completion ack via a completion-eventfd. The uinput path is a
|
||||
* local instantaneous write; when armed it would be done inline (see comment in submit).
|
||||
* Real actuation is under VMSIG_WITH_VMCTL; otherwise the stub acks (spine without a VM). */
|
||||
#include "vmsig_adapter.h"
|
||||
#include "adapter_util.h"
|
||||
#include "input.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/epoll.h>
|
||||
|
||||
#ifdef VMSIG_WITH_VMCTL
|
||||
#include "vmctl.h"
|
||||
#endif
|
||||
|
||||
/* POD request/result of the worker. */
|
||||
typedef struct {
|
||||
int cmd; /* 0 = input event, 1 = lifecycle */
|
||||
uint32_t corr;
|
||||
uint32_t origin; /* initiator (addressed ACK) */
|
||||
int kind; /* vmsig_input_kind (for cmd==0) */
|
||||
int code; /* axis/btn/evdev-code */
|
||||
int value; /* abs/rel/down */
|
||||
double scroll;
|
||||
int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */
|
||||
} input_req;
|
||||
typedef struct { int ok; uint32_t corr; uint32_t origin; } input_res;
|
||||
|
||||
/* signaling does NOT track held state: the record of what is pressed lives in the
|
||||
* ACTUATOR (vmctl); we hand it to control on request (CMD_QUERY_INPUT), release is control's decision. */
|
||||
struct vmsig_adapter {
|
||||
uint32_t endpoint;
|
||||
int stub;
|
||||
vmsig_emit emit;
|
||||
vmsig_worker* worker;
|
||||
int driver; /* 0=QMP, 1=UINPUT (VMCTL_DRIVER_*); carried open->attach */
|
||||
const char* qmp_path; /* borrowed from cfg (valid through attach) */
|
||||
const char* input_bus;
|
||||
int ptr_mode;
|
||||
#ifdef VMSIG_WITH_VMCTL
|
||||
vmctl_t* vmctl;
|
||||
#endif
|
||||
};
|
||||
|
||||
static int input_job(void* user, const void* reqp, void* resp) {
|
||||
struct vmsig_adapter* a = user;
|
||||
const input_req* rq = reqp;
|
||||
input_res* rs = resp;
|
||||
memset(rs, 0, sizeof *rs);
|
||||
rs->corr = rq->corr;
|
||||
rs->origin = rq->origin;
|
||||
#ifdef VMSIG_WITH_VMCTL
|
||||
if (a->vmctl) {
|
||||
int r = -1;
|
||||
if (rq->cmd == 0) {
|
||||
vmctl_batch b; vmctl_batch_init(&b);
|
||||
switch (rq->kind) {
|
||||
case VMSIG_INPUT_ABS: vmctl_batch_abs(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_REL: vmctl_batch_rel(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_BTN: vmctl_batch_btn(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_KEY: vmctl_batch_key(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_SCROLL: vmctl_batch_scroll(&b, rq->code, rq->scroll); break;
|
||||
default: break;
|
||||
}
|
||||
r = vmctl_batch_send(a->vmctl, &b);
|
||||
} else {
|
||||
switch (rq->life_op) {
|
||||
case 0: r = vmctl_powerdown(a->vmctl); break;
|
||||
case 1: r = vmctl_reset(a->vmctl); break;
|
||||
case 2: r = vmctl_wakeup(a->vmctl); break;
|
||||
case 3: r = vmctl_pause(a->vmctl); break;
|
||||
case 4: r = vmctl_resume(a->vmctl); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
rs->ok = (r == 0);
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
(void)a;
|
||||
rs->ok = 1; /* stub: ack without actuation */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static vmsig_adapter* in_open(const void* cfg, uint32_t endpoint) {
|
||||
const vmsig_input_cfg* c = cfg;
|
||||
struct vmsig_adapter* a = calloc(1, sizeof *a);
|
||||
if (!a) return NULL;
|
||||
a->endpoint = endpoint;
|
||||
a->stub = c ? c->stub : 1;
|
||||
if (c) { /* carry the driver selection to attach (cfg not passed there) */
|
||||
a->driver = c->driver;
|
||||
a->qmp_path = c->qmp_path;
|
||||
a->input_bus = c->input_bus;
|
||||
a->ptr_mode = c->ptr_mode;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
static int in_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg, int cap) {
|
||||
if (cap < 1) return -1;
|
||||
a->emit = *emit;
|
||||
a->worker = vmsig_worker_new(input_job, a, 1, 64); /* QMP is a serial channel, cap 64 */
|
||||
if (!a->worker) return -1;
|
||||
|
||||
#ifdef VMSIG_WITH_VMCTL
|
||||
if (!a->stub) {
|
||||
/* armed: build vmctl_config from the carried cfg and open the actuator. UINPUT
|
||||
* (host uinput + optional virtio-input-host-pci passthrough via QMP) is the primary
|
||||
* input driver; QMP input-send-event is the fallback. */
|
||||
vmctl_config vcfg;
|
||||
memset(&vcfg, 0, sizeof vcfg);
|
||||
vcfg.driver = (a->driver == 1) ? VMCTL_DRIVER_UINPUT : VMCTL_DRIVER_QMP;
|
||||
vcfg.qmp_path = a->qmp_path;
|
||||
vcfg.input_bus = a->input_bus;
|
||||
vcfg.ptr_mode = a->ptr_mode;
|
||||
vcfg.uinput_id = NULL; /* built-in HID identity defaults */
|
||||
a->vmctl = vmctl_open(&vcfg);
|
||||
if (!a->vmctl) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; }
|
||||
}
|
||||
#endif
|
||||
|
||||
reg[0].fd = vmsig_worker_evfd(a->worker);
|
||||
reg[0].epoll_events = EPOLLIN;
|
||||
reg[0].shape = VMSIG_RDY_EVENTFD;
|
||||
reg[0].cookie = 0;
|
||||
|
||||
vmsig_event up;
|
||||
memset(&up, 0, sizeof up);
|
||||
up.kind = VMSIG_EV_SEAM_UP; up.source = VMSIG_SRC_INPUT; up.dir = VMSIG_DIR_UP;
|
||||
up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint;
|
||||
a->emit.emit(a->emit.token, &up);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int in_on_ready(vmsig_adapter* a, uint32_t cookie, uint32_t events) {
|
||||
(void)cookie; (void)events;
|
||||
vmsig_worker_ack(a->worker);
|
||||
input_res rs; int rc;
|
||||
while (vmsig_worker_poll(a->worker, &rs, sizeof rs, &rc) == 1) {
|
||||
vmsig_event up;
|
||||
memset(&up, 0, sizeof up);
|
||||
up.kind = VMSIG_EV_ACT_ACK; up.source = VMSIG_SRC_INPUT; up.dir = VMSIG_DIR_UP;
|
||||
up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint;
|
||||
up.corr = rs.corr; up.origin = rs.origin;
|
||||
up.payload.flags = VMSIG_PL_INLINE;
|
||||
memcpy(up.inln, &rs, sizeof up.inln < sizeof rs ? sizeof up.inln : sizeof rs);
|
||||
a->emit.emit(a->emit.token, &up);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int in_submit(vmsig_adapter* a, const vmsig_event* ev) {
|
||||
if (ev->kind == VMSIG_EV_CMD_QUERY_INPUT) {
|
||||
/* Return what is PRESSED from the vmctl ACTUATOR's record (signaling does NOT track
|
||||
* held itself). The read is read-only (no QMP round-trip) => on the loop thread;
|
||||
* addressed reply to the initiator. stub without vmctl => empty set (nothing to
|
||||
* actuate — nothing to hold). */
|
||||
vmsig_input_held h;
|
||||
memset(&h, 0, sizeof h);
|
||||
#ifdef VMSIG_WITH_VMCTL
|
||||
if (a->vmctl) {
|
||||
const uint32_t capn = (uint32_t)(sizeof h.ent / sizeof h.ent[0]);
|
||||
unsigned char bits[VMCTL_KEYS_SNAPSHOT_BYTES];
|
||||
int n = vmctl_keys_snapshot(a->vmctl, bits, sizeof bits);
|
||||
for (int code = 0; n > 0 && code <= VMCTL_KEY_CODE_MAX; code++)
|
||||
if (bits[code >> 3] & (1u << (code & 7))) {
|
||||
if (h.count < capn) { h.ent[h.count].kind = VMSIG_INPUT_KEY;
|
||||
h.ent[h.count].code = (uint16_t)code; h.count++; }
|
||||
else h.flags |= VMSIG_INPUT_HELD_TRUNC;
|
||||
}
|
||||
unsigned bm = vmctl_btns_snapshot(a->vmctl);
|
||||
for (int b = 0; b < 8; b++) if (bm & (1u << b)) {
|
||||
if (h.count < capn) { h.ent[h.count].kind = VMSIG_INPUT_BTN;
|
||||
h.ent[h.count].code = (uint16_t)b; h.count++; }
|
||||
else h.flags |= VMSIG_INPUT_HELD_TRUNC;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
vmsig_event up;
|
||||
memset(&up, 0, sizeof up);
|
||||
up.kind = VMSIG_EV_INPUT_HELD; up.source = VMSIG_SRC_INPUT; up.dir = VMSIG_DIR_UP;
|
||||
up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint; up.origin = ev->origin;
|
||||
up.payload.flags = VMSIG_PL_INLINE;
|
||||
memcpy(up.inln, &h, sizeof up.inln < sizeof h ? sizeof up.inln : sizeof h);
|
||||
a->emit.emit(a->emit.token, &up);
|
||||
return 0;
|
||||
}
|
||||
|
||||
input_req rq;
|
||||
memset(&rq, 0, sizeof rq);
|
||||
rq.corr = ev->corr; rq.origin = ev->origin;
|
||||
if (ev->kind == VMSIG_EV_CMD_INPUT) {
|
||||
rq.cmd = 0;
|
||||
/* Decode the NEUTRAL public input contract from inln (vmsig_input). We do NOT track
|
||||
* held — that is the vmctl actuator's record (returned via CMD_QUERY_INPUT). */
|
||||
vmsig_input in;
|
||||
memcpy(&in, ev->inln, sizeof in <= sizeof ev->inln ? sizeof in : sizeof ev->inln);
|
||||
rq.kind = (int)in.kind;
|
||||
rq.code = (int)in.code;
|
||||
rq.value = (int)in.value;
|
||||
rq.scroll = in.scroll;
|
||||
} else if (ev->kind == VMSIG_EV_CMD_LIFECYCLE) {
|
||||
rq.cmd = 1;
|
||||
rq.life_op = (int)(unsigned char)ev->inln[0];
|
||||
} else {
|
||||
return 1; /* not for this seam */
|
||||
}
|
||||
return vmsig_worker_submit(a->worker, &rq, sizeof rq) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static void in_close(vmsig_adapter* a) {
|
||||
if (!a) return;
|
||||
vmsig_worker_free(a->worker);
|
||||
#ifdef VMSIG_WITH_VMCTL
|
||||
if (a->vmctl) vmctl_close(a->vmctl);
|
||||
#endif
|
||||
free(a);
|
||||
}
|
||||
|
||||
static const vmsig_adapter_ops IN_OPS = {
|
||||
.name = "input", .source = VMSIG_SRC_INPUT, .codec = VMSIG_CODEC_INPUT,
|
||||
.open = in_open, .attach = in_attach, .on_readiness = in_on_ready,
|
||||
.submit = in_submit, .close = in_close
|
||||
};
|
||||
|
||||
const vmsig_adapter_ops* vmsig_input_ops(void) { return &IN_OPS; }
|
||||
Reference in New Issue
Block a user