Files
vatrog-vm-signaling/src/adapter/input/input.c
T
lirent 9bde398b6c vmsig: management daemon, runtime endpoint lifecycle, roster, discovery, in-tree drivers, packaging
- core: runtime attach/detach of a per-endpoint adapter trio (runtime-safe add_adapter + vmsig_core_detach_endpoint, deferred reap)
- roster: VMSIG_EV_ROSTER + CAP_ROSTER, retained per-endpoint and replayed to late subscribers
- discovery: inotify trigger dir, vmid/endpoint slot allocator, host probe; vmsigd daemon with config + per-uid admission
- input driver and vgpu perception built in-tree; vgpu perception as a separate library
- memctx: own the supplied ro_fd (closed at detach)
- deb packaging: install rules, systemd unit, tmpfiles, default config
2026-06-22 17:25:06 +03:00

227 lines
9.6 KiB
C

/* 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 when cfg.stub==0 (vmctl opened); otherwise the stub acks (spine without a VM).
* vmctl is the in-tree input driver (src/si/input/, absorbed); cfg.stub gates opening it. */
#include "vmsig_adapter.h"
#include "adapter_util.h"
#include "input.h"
#include "vmctl.h"
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/epoll.h>
/* 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; /* btn/evdev-code/scroll-axis */
int value; /* pressed(1)/released(0) for btn/key */
int x; /* MOVE_ABS: abs X; MOVE_REL: dx */
int y; /* MOVE_ABS: abs Y; MOVE_REL: dy */
double scroll;
int noack; /* CMD_INPUT fire-and-forget: emit no ACT_ACK */
int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */
} input_req;
typedef struct { int ok; uint32_t corr; uint32_t origin; int noack; } 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;
const char* qmp_path; /* borrowed from cfg (valid through attach); SERVICE power/lifecycle */
vmctl_t* vmctl; /* NULL in stub mode (cfg.stub) — no actuator opened */
};
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;
rs->noack = rq->noack;
if (a->vmctl) {
int r = -1;
if (rq->cmd == 0) {
/* Pointer motion is ONE packet: both axes in a single batch -> one round-trip. */
vmctl_batch b; vmctl_batch_init(&b);
switch (rq->kind) {
case VMSIG_INPUT_MOVE_ABS:
vmctl_batch_abs(&b, VMCTL_AXIS_X, rq->x);
vmctl_batch_abs(&b, VMCTL_AXIS_Y, rq->y);
break;
case VMSIG_INPUT_MOVE_REL:
vmctl_batch_rel(&b, VMCTL_AXIS_X, rq->x);
vmctl_batch_rel(&b, VMCTL_AXIS_Y, rq->y);
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;
}
(void)a;
rs->ok = 1; /* stub: ack without actuation (vmctl not opened) */
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) a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */
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;
if (!a->stub) {
/* armed: open the actuator. Injection is ALWAYS uinput (orphaned host uinput + external
* QEMU input-linux). PTR_BOTH gives both pointer forms a device (A=abs tablet, B=rel
* mouse) — the contract now promises both MOVE_ABS and MOVE_REL, so neither may be
* disabled. qmp_path serves the SERVICE power/lifecycle path, not input injection. */
vmctl_config vcfg;
memset(&vcfg, 0, sizeof vcfg);
vcfg.driver = VMCTL_DRIVER_UINPUT;
vcfg.qmp_path = a->qmp_path;
vcfg.input_bus = "";
vcfg.ptr_mode = VMCTL_PTR_BOTH;
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; }
}
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) {
if (rs.noack) continue; /* fire-and-forget CMD_INPUT: actuated, emit no ACT_ACK */
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);
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;
}
}
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.x = (int)in.x;
rq.y = (int)in.y;
rq.scroll = in.scroll;
rq.noack = (in.flags & VMSIG_INPUT_F_NOACK) ? 1 : 0;
} 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);
if (a->vmctl) vmctl_close(a->vmctl);
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; }