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