joeylib2/tests/gate/qmp.c

478 lines
16 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// qmp.c - drive a QEMU machine through its QMP socket.
//
// qmp SOCKET screendump FILE.png the screen, as QEMU's display has it
// qmp SOCKET type TEXT type TEXT (US layout, shift as needed)
// qmp SOCKET keys KEY... press the KEYs, one after another
// qmp SOCKET hold KEY MS hold one key down for MS milliseconds
// qmp SOCKET down KEY press one key, and leave it down
// qmp SOCKET up KEY let go of it
// qmp SOCKET move DX DY move the mouse DX, DY (relative, as a PS/2
// mouse moves, in steps a PS/2 packet holds;
// the system stops it at the screen's edge)
// qmp SOCKET quit end QEMU
//
// KEY is a QEMU key name (a qcode: esc, ret, spc, a, 1, ...).
//
// The Be API ports' gates (scripts/lib/beGate.sh) take the screen and press
// keys this way, as a person at the machine would: the keys arrive through
// the emulated PS/2 keyboard. Each command is one QMP request (a typed key,
// a mouse step, one each) after the greeting and the capabilities
// handshake; QMP's events in between are skipped. Each request is one line
// of JSON with a space after every colon and comma. An error reply, or a
// connection that fails, ends the tool with exit 1; bad arguments (a
// character in TEXT no key types among them) exit 2 before anything is
// sent.
#define _DEFAULT_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include "gateArg.h"
#include "gateNet.h"
#include "gateText.h"
#include "toolBuf.h"
#include "toolFile.h"
#include "toolJson.h"
#define TOOL_NAME "qmp"
#define ARG_SOCKET 1
#define ARG_COMMAND 2
#define ARG_FIRST 3
#define ARG_SECOND 4
#define ARGS_BARE 3
#define ARGS_ONE 4
#define ARGS_TWO 5
#define READ_SIZE 4096u
#define NUMBER_CHARS 32u
// How long each typed key is held, and the pause after it: slow enough that
// a program reading its input once a frame sees every key.
#define TYPE_HOLD_MS 100L
#define TYPE_GAP_SECONDS 0.15
#define MS_PER_SECOND 1000.0
// A mouse move goes in steps of at most this many counts on each axis, with
// this pause after each: BeOS R5's mouse driver gets a single long move
// wrong.
#define MOVE_STEP_MAX 100L
#define MOVE_GAP_SECONDS 0.02
// A shifted key is pressed with this one.
#define SHIFT_QCODE "shift"
#define KEYS_MAX 2u
// A one-character qcode (a letter or digit) and its end.
#define LETTER_CHARS 2u
// The shifted digits, in the order of their keys' digits.
#define SHIFTED_DIGITS ")!@#$%^&*("
#define JSON_CONTROL_END 0x20u
// One QMP connection: its socket and what it has sent past the last line.
typedef struct QmpT {
int fd;
ToolBufT in;
} QmpT;
// A character a US keyboard types with a key other than its own.
typedef struct PunctT {
char ch;
const char *qcode;
bool shifted;
} PunctT;
static void bufNumber(ToolBufT *buf, long value);
static void bufText(ToolBufT *buf, const char *text);
static bool charKey(char ch, char *letter, const char **qcode, bool *shifted);
static long floorDiv(long a, long b);
static void jsonString(ToolBufT *buf, const char *text);
static void key(QmpT *q, const char *qcode, bool down);
static void keys(QmpT *q, const char *const *qcodes, uint32_t count, long holdMs);
static void move(QmpT *q, long dx, long dy);
static void qmpExecute(QmpT *q, const char *command, const ToolBufT *arguments);
static void qmpLine(QmpT *q, ToolBufT *line);
static void qmpOpen(QmpT *q, const char *path);
static void tap(QmpT *q, const char *const *qcodes, uint32_t count);
static int usage(void);
static const PunctT kPunct[] = {
{' ', "spc", false},
{'.', "dot", false},
{',', "comma", false},
{'/', "slash", false},
{';', "semicolon", false},
{'\'', "apostrophe", false},
{'-', "minus", false},
{'=', "equal", false},
{'[', "bracket_left", false},
{']', "bracket_right", false},
{'\\', "backslash", false},
{'`', "grave_accent", false},
{':', "semicolon", true},
{'?', "slash", true},
{'>', "dot", true},
{'<', "comma", true},
{'"', "apostrophe", true},
{'_', "minus", true},
{'+', "equal", true},
{'{', "bracket_left", true},
{'}', "bracket_right", true},
{'|', "backslash", true},
{'~', "grave_accent", true}
};
// A whole number, in decimal.
static void bufNumber(ToolBufT *buf, long value) {
char text[NUMBER_CHARS];
snprintf(text, sizeof(text), "%ld", value);
bufText(buf, text);
}
static void bufText(ToolBufT *buf, const char *text) {
toolBufAdd(buf, text, strlen(text));
}
// The key (qcode) that types ch, and whether shifted: a letter or digit is
// its own key, its one-character name written to letter.
static bool charKey(char ch, char *letter, const char **qcode, bool *shifted) {
const char *digit;
size_t i;
letter[1] = '\0';
*qcode = letter;
*shifted = false;
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
letter[0] = ch;
return true;
}
if (ch >= 'A' && ch <= 'Z') {
letter[0] = (char)(ch - 'A' + 'a');
*shifted = true;
return true;
}
digit = strchr(SHIFTED_DIGITS, ch);
if (digit != NULL) {
letter[0] = (char)('0' + (digit - SHIFTED_DIGITS));
*shifted = true;
return true;
}
for (i = 0; i < sizeof(kPunct) / sizeof(kPunct[0]); i++) {
if (kPunct[i].ch == ch) {
*qcode = kPunct[i].qcode;
*shifted = kPunct[i].shifted;
return true;
}
}
return false;
}
// a / b rounded toward minus infinity, so a move's steps add up to the
// whole move whatever its sign.
static long floorDiv(long a, long b) {
long q;
q = a / b;
if ((a % b != 0) && ((a < 0) != (b < 0))) {
q--;
}
return q;
}
// text as a JSON string: a quote, a backslash, and the control characters
// escaped.
static void jsonString(ToolBufT *buf, const char *text) {
char escape[NUMBER_CHARS];
const uint8_t *c;
toolBufByte(buf, '"');
for (c = (const uint8_t *)text; *c != '\0'; c++) {
switch (*c) {
case '"':
bufText(buf, "\\\"");
break;
case '\\':
bufText(buf, "\\\\");
break;
case '\n':
bufText(buf, "\\n");
break;
case '\r':
bufText(buf, "\\r");
break;
case '\t':
bufText(buf, "\\t");
break;
case '\b':
bufText(buf, "\\b");
break;
case '\f':
bufText(buf, "\\f");
break;
default:
if (*c < JSON_CONTROL_END) {
snprintf(escape, sizeof(escape), "\\u%04x", *c);
bufText(buf, escape);
} else {
toolBufByte(buf, *c);
}
break;
}
}
toolBufByte(buf, '"');
}
// Press or let go of one key.
static void key(QmpT *q, const char *qcode, bool down) {
ToolBufT args;
memset(&args, 0, sizeof(args));
bufText(&args, "{\"events\": [{\"type\": \"key\", \"data\": {\"down\": ");
bufText(&args, down ? "true" : "false");
bufText(&args, ", \"key\": {\"type\": \"qcode\", \"data\": ");
jsonString(&args, qcode);
bufText(&args, "}}}]}");
qmpExecute(q, "input-send-event", &args);
toolBufFree(&args);
}
// Press qcodes together, hold them holdMs, let them go.
static void keys(QmpT *q, const char *const *qcodes, uint32_t count, long holdMs) {
ToolBufT args;
uint32_t i;
memset(&args, 0, sizeof(args));
bufText(&args, "{\"keys\": [");
for (i = 0; i < count; i++) {
bufText(&args, i == 0u ? "{\"type\": \"qcode\", \"data\": " : ", {\"type\": \"qcode\", \"data\": ");
jsonString(&args, qcodes[i]);
bufText(&args, "}");
}
bufText(&args, "], \"hold-time\": ");
bufNumber(&args, holdMs);
bufText(&args, "}");
qmpExecute(q, "send-key", &args);
toolBufFree(&args);
}
// Move the mouse dx, dy in steps of at most MOVE_STEP_MAX.
static void move(QmpT *q, long dx, long dy) {
ToolBufT args;
long most;
long steps;
long step;
long stepX;
long stepY;
most = labs(dx) > labs(dy) ? labs(dx) : labs(dy);
steps = (most + MOVE_STEP_MAX - 1) / MOVE_STEP_MAX;
if (steps < 1) {
steps = 1;
}
memset(&args, 0, sizeof(args));
for (step = 0; step < steps; step++) {
stepX = floorDiv(dx * (step + 1), steps) - floorDiv(dx * step, steps);
stepY = floorDiv(dy * (step + 1), steps) - floorDiv(dy * step, steps);
args.len = 0u;
bufText(&args, "{\"events\": [{\"type\": \"rel\", \"data\": {\"axis\": \"x\", \"value\": ");
bufNumber(&args, stepX);
bufText(&args, "}}, {\"type\": \"rel\", \"data\": {\"axis\": \"y\", \"value\": ");
bufNumber(&args, stepY);
bufText(&args, "}}]}");
qmpExecute(q, "input-send-event", &args);
gateNetSleep(MOVE_GAP_SECONDS);
}
toolBufFree(&args);
}
// Run one command (arguments a JSON object, or NULL for none) and wait for
// its reply; an error reply ends the tool.
static void qmpExecute(QmpT *q, const char *command, const ToolBufT *arguments) {
const ToolJsonT *error;
const ToolJsonT *desc;
ToolJsonT *reply;
ToolBufT request;
ToolBufT line;
char err[TOOL_JSON_ERROR_BYTES];
bool done;
memset(&request, 0, sizeof(request));
bufText(&request, "{\"execute\": ");
jsonString(&request, command);
if (arguments != NULL) {
bufText(&request, ", \"arguments\": ");
toolBufAdd(&request, arguments->data, arguments->len);
}
bufText(&request, "}\n");
if (!gateNetSendAll(q->fd, request.data, request.len)) {
toolFail("%s: %s", command, strerror(errno));
}
toolBufFree(&request);
memset(&line, 0, sizeof(line));
done = false;
while (!done) {
qmpLine(q, &line);
reply = toolJsonParse((const char *)line.data, line.len, err);
if (reply == NULL) {
toolFail("%s: QMP's reply is not JSON (%s)", command, err);
}
error = toolJsonGet(reply, "error");
if (error != NULL) {
desc = toolJsonGet(error, "desc");
toolFail("%s: %s", command, desc != NULL && desc->type == TOOL_JSON_STRING ? desc->string : "error");
}
done = toolJsonGet(reply, "return") != NULL;
toolJsonFree(reply);
}
toolBufFree(&line);
}
// The next line QMP sent (without its newline) in line; the connection
// closing first ends the tool.
static void qmpLine(QmpT *q, ToolBufT *line) {
uint8_t chunk[READ_SIZE];
size_t end;
ssize_t got;
for (;;) {
end = gateTextFind(q->in.data, q->in.len, 0u, "\n", 1u);
if (end != GATE_TEXT_NOT_FOUND) {
break;
}
got = read(q->fd, chunk, sizeof(chunk));
if (got <= 0) {
toolFail("QMP closed the connection");
}
toolBufAdd(&q->in, chunk, (size_t)got);
}
line->len = 0u;
toolBufAdd(line, q->in.data, end);
q->in.len -= end + 1u;
memmove(q->in.data, q->in.data + end + 1u, q->in.len);
}
// Connect, read the greeting, and negotiate the capabilities.
static void qmpOpen(QmpT *q, const char *path) {
struct sockaddr_un addr;
ToolBufT greeting;
memset(q, 0, sizeof(*q));
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (strlen(path) >= sizeof(addr.sun_path)) {
toolFail("%s: the socket's path is too long", path);
}
strcpy(addr.sun_path, path);
q->fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (q->fd < 0 || connect(q->fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
toolFail("%s: %s", path, strerror(errno));
}
memset(&greeting, 0, sizeof(greeting));
qmpLine(q, &greeting);
toolBufFree(&greeting);
qmpExecute(q, "qmp_capabilities", NULL);
}
// Press qcodes together for a typed key's time, then pause after them.
static void tap(QmpT *q, const char *const *qcodes, uint32_t count) {
keys(q, qcodes, count, TYPE_HOLD_MS);
gateNetSleep(TYPE_HOLD_MS / MS_PER_SECOND + TYPE_GAP_SECONDS);
}
static int usage(void) {
fprintf(stderr, "usage: %s SOCKET screendump FILE.png the screen, as QEMU's display has it\n %s SOCKET type TEXT type TEXT (US layout, shift as needed)\n %s SOCKET keys KEY... press the KEYs, one after another\n %s SOCKET hold KEY MS hold one key down for MS milliseconds\n %s SOCKET down KEY press one key, and leave it down\n %s SOCKET up KEY let go of it\n %s SOCKET move DX DY move the mouse DX, DY\n %s SOCKET quit end QEMU\nKEY is a QEMU key name (a qcode: esc, ret, spc, a, 1, ...).\n", TOOL_NAME, TOOL_NAME, TOOL_NAME, TOOL_NAME, TOOL_NAME, TOOL_NAME, TOOL_NAME, TOOL_NAME);
return GATE_USAGE_EXIT;
}
int main(int argc, char **argv) {
QmpT q;
ToolBufT args;
const char *codes[KEYS_MAX];
const char *command;
const char *text;
char letter[LETTER_CHARS];
long holdMs;
long dx;
long dy;
bool shifted;
int i;
gateNetStart(TOOL_NAME);
if (argc < ARGS_BARE) {
return usage();
}
command = argv[ARG_COMMAND];
if (strcmp(command, "screendump") == 0 && argc == ARGS_ONE) {
qmpOpen(&q, argv[ARG_SOCKET]);
memset(&args, 0, sizeof(args));
bufText(&args, "{\"filename\": ");
jsonString(&args, argv[ARG_FIRST]);
bufText(&args, ", \"format\": \"png\"}");
qmpExecute(&q, "screendump", &args);
toolBufFree(&args);
} else if (strcmp(command, "type") == 0 && argc == ARGS_ONE) {
for (text = argv[ARG_FIRST]; *text != '\0'; text++) {
if (!charKey(*text, letter, &codes[1], &shifted)) {
fprintf(stderr, "%s: no key types ", TOOL_NAME);
gateNetShowBytes(stderr, text, 1u);
fputc('\n', stderr);
return GATE_USAGE_EXIT;
}
}
qmpOpen(&q, argv[ARG_SOCKET]);
codes[0] = SHIFT_QCODE;
for (text = argv[ARG_FIRST]; *text != '\0'; text++) {
charKey(*text, letter, &codes[1], &shifted);
tap(&q, shifted ? codes : &codes[1], shifted ? KEYS_MAX : 1u);
}
} else if (strcmp(command, "keys") == 0 && argc >= ARGS_ONE) {
qmpOpen(&q, argv[ARG_SOCKET]);
for (i = ARG_FIRST; i < argc; i++) {
tap(&q, (const char *const *)&argv[i], 1u);
}
} else if (strcmp(command, "hold") == 0 && argc == ARGS_TWO) {
holdMs = gateArgLong("hold time", argv[ARG_SECOND]);
if (holdMs < 0) {
return usage();
}
qmpOpen(&q, argv[ARG_SOCKET]);
keys(&q, (const char *const *)&argv[ARG_FIRST], 1u, holdMs);
gateNetSleep(holdMs / MS_PER_SECOND);
} else if ((strcmp(command, "down") == 0 || strcmp(command, "up") == 0) && argc == ARGS_ONE) {
qmpOpen(&q, argv[ARG_SOCKET]);
key(&q, argv[ARG_FIRST], strcmp(command, "down") == 0);
} else if (strcmp(command, "move") == 0 && argc == ARGS_TWO) {
dx = gateArgLong("dx", argv[ARG_FIRST]);
dy = gateArgLong("dy", argv[ARG_SECOND]);
qmpOpen(&q, argv[ARG_SOCKET]);
move(&q, dx, dy);
} else if (strcmp(command, "quit") == 0 && argc == ARGS_BARE) {
qmpOpen(&q, argv[ARG_SOCKET]);
qmpExecute(&q, "quit", NULL);
} else {
return usage();
}
return 0;
}