/* 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 #include #include #include /* 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; }