mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-25 20:36:36 +03:00
9bde398b6c
- 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
227 lines
9.6 KiB
C
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; }
|