478 lines
16 KiB
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;
|
|
}
|