add experiment of macro recorder and replayer - randomcrap - random crap programs of varying quality
HTML git clone git://git.codemadness.org/randomcrap
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
DIR commit 941ecd1ec38a5d0b4e7b4f3ed3e9ea6234b6ed8b
DIR parent c25c282d817b2fde16f5dedb4c6112f9c5771164
HTML Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date: Thu, 9 Mar 2023 19:53:13 +0100
add experiment of macro recorder and replayer
Diffstat:
A rec.c | 708 +++++++++++++++++++++++++++++++
1 file changed, 708 insertions(+), 0 deletions(-)
---
DIR diff --git a/rec.c b/rec.c
@@ -0,0 +1,708 @@
+/* serialize and replay keyboard and mouse events
+ Dependencies: Xlib, Xtst, XInput2, etc
+ Some code adapted from the examples in xorg-xinput/src/ test.c and test_xi2.c.
+ Compile: cc -o main main.c -lX11 -lXtst -lXi -lrt -D_GNU_SOURCE -Wall */
+
+#include <sys/time.h>
+
+#include <X11/extensions/XInput.h>
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/XTest.h>
+
+#include <X11/Xlib.h>
+
+#include <err.h>
+#include <errno.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+enum EventType { EventNone = 0, EventMotion, EventKeyPress, EventKeyRelease,
+ EventButtonPress, EventButtonRelease, EventSleep };
+
+struct event {
+ enum EventType evtype;
+ int button; /* button or key */
+ int x, y; /* mouse x, y */
+ float sleeptime;
+};
+
+volatile sig_atomic_t state_sigcont = 0, state_sighup = 0, state_sigabrt = 0;
+volatile sig_atomic_t state_sigusr1 = 0, state_sigusr2 = 0;
+
+Window win, root;
+Display *dpy;
+int screen;
+int xi_opcode;
+
+int isrecording = 0;
+int isreplaying = 0;
+int isrunning = 0;
+int xfd;
+
+struct event *events = NULL;
+size_t nevents = 0;
+
+const char *replay_file = NULL;
+
+/* configuration */
+int replay_usebuttons = 1; /* use button events? */
+int replay_usemotion = 1; /* use motion events? */
+int replay_usekeys = 1; /* use keyboard events */
+int replay_usesleep = 1; /* use timings/sleep for replaying? */
+
+int record_usebuttons = 1; /* use button events? */
+int record_usemotion = 1; /* use motion events? */
+int record_usekeys = 1; /* use keyboard events */
+int record_usesleep = 1; /* use timings/sleep for recording? */
+
+int replay_key = 134; /* Super_R */
+int replay_repeat = 0; /* repeat replay? */
+int record_key = 127; /* pause/rec key on my keyboard */
+int timefirstevent = 1; /* start timer at first event or since program startup */
+int exit_key = 96; /* F12 */
+
+/* button states */
+char buttons[32], keys[256];
+
+void replay_events(struct event *events, size_t nevents);
+
+int
+process_event(struct event *ev)
+{
+ unsigned long delay = CurrentTime; /* no delay */
+ unsigned long keydelay = 1; /* delay atleast some time (ms) to process the key */
+
+ switch (ev->evtype) {
+ case EventMotion:
+ if (!replay_usemotion)
+ return 0;
+ XTestFakeMotionEvent(dpy, -1, ev->x, ev->y, delay);
+ XSync(dpy, False);
+ return 1;
+ case EventKeyPress:
+ if (!replay_usekeys)
+ return 0;
+ XTestFakeKeyEvent(dpy, ev->button, 1, keydelay);
+ XSync(dpy, False);
+ return 1;
+ case EventKeyRelease:
+ if (!replay_usekeys)
+ return 0;
+ XTestFakeKeyEvent(dpy, ev->button, 0, keydelay);
+ XSync(dpy, False);
+ return 1;
+ case EventButtonPress:
+ if (!replay_usebuttons)
+ return 0;
+ XTestFakeButtonEvent(dpy, ev->button, 1, delay);
+ XSync(dpy, False);
+ return 1;
+ case EventButtonRelease:
+ if (!replay_usebuttons)
+ return 0;
+ XTestFakeButtonEvent(dpy, ev->button, 0, delay);
+ XSync(dpy, False);
+ return 1;
+ case EventSleep:
+ if (!replay_usesleep)
+ return 0;
+ usleep(ev->sleeptime * 1000000);
+ return 2; /* not an X11 event */
+ default:
+ return 0;
+ }
+}
+
+int
+line2event(const char *line, struct event *ev)
+{
+ char *p, *end;
+
+ if (strncmp(line, "motion ", strlen("motion ")) == 0) {
+ if (isrecording && !record_usemotion)
+ return 0;
+
+ ev->evtype = EventMotion;
+
+ /* parse arguments */
+ p = strchr(line, ' ');
+ ev->x = strtol(p, &end, 10);
+
+ p = end;
+ ev->y = strtol(p, &end, 10);
+ return 1;
+ } else if (strncmp(line, "keypress ", strlen("keypress ")) == 0 ||
+ strncmp(line, "keyrelease ", strlen("keyrelease ")) == 0) {
+
+ if (isrecording && !record_usekeys)
+ return 0;
+
+ ev->evtype = line[3] == 'p' ? EventKeyPress : EventKeyRelease;
+
+ /* parse arguments */
+ p = strchr(line, ' ');
+ ev->button = strtol(p, &end, 10);
+
+ return 1;
+ } else if (strncmp(line, "press ", strlen("press ")) == 0 ||
+ strncmp(line, "release ", strlen("release ")) == 0) {
+ if (isrecording && !record_usebuttons)
+ return 0;
+
+ ev->evtype = line[0] == 'p' ? EventButtonPress : EventButtonRelease;
+
+ /* parse arguments */
+ p = strchr(line, ' ');
+ ev->button = strtol(p, &end, 10);
+
+ return 1;
+ } else if (strncmp(line, "sleep ", strlen("sleep ")) == 0) {
+ if (isrecording && !record_usesleep)
+ return 0;
+
+ ev->evtype = EventSleep;
+
+ /* parse arguments */
+ p = strchr(line, ' '); /* there is always a space */
+ ev->sleeptime = strtof(p, &end);
+ return 2;
+ }
+ return -1;
+}
+
+int
+xevent2event(XGenericEventCookie *cookie, struct event *ev)
+{
+ XIDeviceEvent *dev_event;
+
+ dev_event = (XIDeviceEvent *)(cookie->data);
+
+ /* NOTE: the "detail" attribute is "the button number, key code, touch ID, or 0." */
+ switch (cookie->evtype) {
+ /* device event */
+ case XI_ButtonPress:
+ ev->evtype = EventButtonPress;
+ ev->button = dev_event->detail;
+ return 1;
+ case XI_ButtonRelease:
+ ev->evtype = EventButtonRelease;
+ ev->button = dev_event->detail;
+ return 1;
+ case XI_KeyPress:
+ ev->evtype = EventKeyPress;
+ ev->button = dev_event->detail;
+ return 1;
+ case XI_KeyRelease:
+ ev->evtype = EventKeyRelease;
+ ev->button = dev_event->detail;
+ return 1;
+ case XI_Motion:
+ ev->evtype = EventMotion;
+ ev->x = (int)dev_event->root_x;
+ ev->y = (int)dev_event->root_y;
+ return 1;
+ }
+ return 0;
+}
+
+int
+filterevent(struct event *ev)
+{
+ switch (ev->evtype) {
+ case EventButtonPress:
+ if (ev->button < 0 || ev->button >= sizeof(buttons) / sizeof(buttons[0]))
+ break;
+ buttons[ev->button] = 1;
+ break;
+ case EventButtonRelease:
+ if (ev->button < 0 || ev->button >= sizeof(buttons) / sizeof(buttons[0]))
+ break;
+ /* ignore if button was not registered as pressed beforehand */
+ if (buttons[ev->button] == 0)
+ return 1;
+ buttons[ev->button] = 0;
+ break;
+ case EventKeyPress:
+ if (ev->button < 0 || ev->button >= sizeof(keys) / sizeof(keys[0]))
+ break;
+ keys[ev->button] = 1;
+ break;
+ case EventKeyRelease:
+ if (ev->button < 0 || ev->button >= sizeof(keys) / sizeof(keys[0]))
+ break;
+ /* ignore if button was not registered as pressed beforehand */
+ if (keys[ev->button] == 0)
+ return 1;
+ keys[ev->button] = 0;
+ break;
+ default:
+ break;
+ }
+ return 0;
+}
+
+int
+event2line(struct event *ev)
+{
+ switch (ev->evtype) {
+ case EventButtonPress:
+ if (isreplaying && !replay_usebuttons)
+ break;
+ printf("press %d\n", ev->button);
+ return 1;
+ case EventButtonRelease:
+ if (isreplaying && !replay_usebuttons)
+ break;
+ printf("release %d\n", ev->button);
+ return 1;
+ case EventKeyPress:
+ if (isreplaying && !replay_usekeys)
+ break;
+ printf("keypress %d\n", ev->button);
+ return 1;
+ case EventKeyRelease:
+ if (isreplaying && !replay_usekeys)
+ break;
+ printf("keyrelease %d\n", ev->button);
+ return 1;
+ case EventMotion:
+ if (isreplaying && !replay_usemotion)
+ break;
+ printf("motion %d %d\n", ev->x, ev->y);
+ return 1;
+ case EventSleep:
+ if (isreplaying && !replay_usesleep)
+ break;
+ printf("sleep %f\n", ev->sleeptime);
+ return 1;
+ default:
+ break;
+ }
+ return 0;
+}
+
+void
+read_events_from_file(const char *path)
+{
+ struct event ev;
+ FILE *fp;
+ char *line = NULL;
+ size_t linesiz = 0;
+ ssize_t n;
+ int r;
+
+ if ((fp = fopen(path, "rb")) == NULL)
+ err(1, "fopen: %s", path);
+
+ nevents = 0;
+ while ((n = getline(&line, &linesiz, fp)) > 0) {
+ if (n && line[n - 1] == '\n')
+ line[--n] = '\0';
+
+ if ((r = line2event(line, &ev)) > 0) {
+ /* add event */
+ if (!(events = realloc(events, (nevents + 1) * sizeof(ev))))
+ err(1, "realloc");
+ memcpy(&events[nevents], &ev, sizeof(ev));
+ nevents++;
+ }
+ }
+ if (ferror(fp))
+ err(1, "getline");
+ free(line);
+}
+
+/* fake events of buttons that are still pressed. This prevents some annoying issue when the
+ recording is stopped and later replayed the button or key states or held */
+void
+fake_release_button_states(void)
+{
+ struct event ev;
+ int i;
+
+ ev.x = 0;
+ ev.y = 0;
+ ev.sleeptime = 0;
+
+ ev.evtype = EventButtonRelease;
+ for (i = 0; i < sizeof(buttons) / sizeof(buttons[0]); i++) {
+ if (!buttons[i])
+ continue;
+ ev.button = i;
+ event2line(&ev);
+ }
+
+ ev.evtype = EventKeyRelease;
+ for (i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
+ if (!keys[i])
+ continue;
+ ev.button = i;
+ event2line(&ev);
+ }
+}
+
+void
+record_stop(void)
+{
+ if (!isrecording)
+ return;
+
+ isrecording = 0;
+
+ fake_release_button_states();
+ fputs("recording stopped\n", stderr);
+}
+
+void
+record_reset(void)
+{
+ /* start new buffer for recording */
+ free(events);
+ events = NULL;
+ nevents = 0;
+}
+
+void
+record_start(void)
+{
+ isrecording = 1;
+
+ record_reset();
+
+ /* reset currently pressed button states */
+ memset(buttons, 0, sizeof(buttons));
+ memset(keys, 0, sizeof(keys));
+
+ fputs("recording started\n", stderr);
+}
+
+void
+replay_stop(void)
+{
+ if (!isreplaying)
+ return;
+
+ isreplaying = 0;
+ fputs("replay stopped\n", stderr);
+}
+
+void
+replay_start(void)
+{
+ isreplaying = 1;
+ fputs("replay started\n", stderr);
+
+ replay_events(events, nevents);
+ if (isreplaying)
+ replay_stop();
+}
+
+int
+eventpoll(void)
+{
+ struct pollfd pfds[1];
+ int r, timeout = 16; /* timeout in ms */
+
+ pfds[0].fd = xfd;
+ pfds[0].events = POLLIN | POLLHUP | POLLNVAL;
+ pfds[0].revents = 0;
+
+ if ((r = poll(pfds, 1, timeout)) == -1) {
+ /* interruptions by signals are OK */
+ if (errno != EINTR)
+ err(1, "poll");
+ }
+ return r;
+}
+
+/* check for a X11 event or handle a received signal */
+int
+eventloop(int dopoll)
+{
+ if (state_sigusr1) {
+ state_sigusr1 = 0;
+ record_start();
+ }
+ if (state_sigusr2) {
+ state_sigusr2 = 0;
+ record_stop();
+ }
+ if (state_sigcont) {
+ state_sigcont = 0;
+ replay_start();
+ }
+ if (state_sigabrt) {
+ state_sigabrt = 0;
+ replay_stop();
+ }
+ if (state_sighup) {
+ state_sighup = 0;
+ record_reset();
+ read_events_from_file(replay_file);
+ }
+ if (XPending(dpy))
+ return 1;
+ if (dopoll)
+ eventpoll();
+
+ return 0;
+}
+
+void
+handle_event(XEvent *xev)
+{
+ struct event ev;
+ XGenericEventCookie *cookie;
+ XIDeviceEvent *dev_event;
+
+ cookie = (XGenericEventCookie *)&(xev->xcookie);
+ if (cookie->type == GenericEvent &&
+ cookie->extension == xi_opcode &&
+ XGetEventData(dpy, cookie)) {
+ dev_event = (XIDeviceEvent *)(cookie->data);
+
+ switch (cookie->evtype) {
+ case XI_KeyRelease:
+ /* exit immediately */
+ if (dev_event->detail == exit_key) {
+ isrecording = 0;
+ isreplaying = 0;
+ isrunning = 0;
+ goto endevent;
+ } else if (dev_event->detail == record_key) {
+ if (isreplaying)
+ replay_stop();
+ if (!isrecording)
+ record_start();
+ else
+ record_stop();
+ goto endevent;
+ } else if (dev_event->detail == replay_key) {
+ if (isrecording)
+ record_stop();
+ if (!isreplaying)
+ replay_start();
+ else
+ replay_stop();
+ goto endevent;
+ }
+ break;
+ case XI_KeyPress:
+ if (dev_event->detail == exit_key ||
+ dev_event->detail == record_key ||
+ dev_event->detail == replay_key)
+ goto endevent;
+ break;
+ }
+
+ if (isrecording && xevent2event(cookie, &ev) > 0 && !filterevent(&ev)) {
+ if (event2line(&ev)) {
+ fflush(stdout); /* force flushing the stream line buffer */
+
+ /* add event */
+ if (!(events = realloc(events, (nevents + 1) * sizeof(ev))))
+ err(1, "realloc");
+ memcpy(&events[nevents], &ev, sizeof(ev));
+ nevents++;
+ }
+ }
+endevent:
+ XFreeEventData(dpy, cookie);
+ }
+}
+
+void
+xinput_setup(void)
+{
+ int xi_major = 2, xi_minor = 0;
+ XIEventMask masks[2];
+ XIEventMask *m;
+ XDeviceInfo *devs;
+ Status status;
+ /* bits storage for event masks */
+ unsigned char nmask[XIMaskLen(XI_LASTEVENT)];
+ int i, event, error, ndevs;
+
+ if (!XQueryExtension(dpy, "XInputExtension", &xi_opcode, &event, &error))
+ errx(1, "X Input extension not available");
+
+ status = XIQueryVersion(dpy, &xi_major, &xi_minor);
+ switch (status) {
+ case Success:
+ break;
+ case BadRequest:
+ errx(1, "no XI2 support. (%d.%d only)", xi_major, xi_minor);
+ default:
+ errx(1, "XI2: unknown error");
+ }
+
+ memset(nmask, 0, sizeof(nmask));
+
+ /* Select for motion events */
+ if (record_usebuttons) {
+ XISetMask(nmask, XI_ButtonPress);
+ XISetMask(nmask, XI_ButtonRelease);
+ }
+ /* always need key events for activation */
+ XISetMask(nmask, XI_KeyPress);
+ XISetMask(nmask, XI_KeyRelease);
+ if (record_usemotion)
+ XISetMask(nmask, XI_Motion);
+ m = &masks[0];
+ /* m->deviceid = XIAllDevices; */ /* normal events requires XIAllDevices (not XIAllMasterDevices) */
+ m->mask_len = sizeof(nmask);
+ m->mask = nmask;
+
+ /* select events individually per device, this allows to ignore certain devices */
+ if ((devs = XListInputDevices(dpy, &ndevs))) {
+ for (i = 0; i < ndevs; i++) {
+ m->deviceid = devs[i].id;
+ if (strstr(devs[i].name, " XTEST "))
+ continue; /* ignore XTEST devices */
+ XISelectEvents(dpy, root, masks, 1);
+ XSync(dpy, False);
+ }
+ XFreeDeviceList(devs);
+ }
+}
+
+void
+replay_events(struct event *events, size_t nevents)
+{
+ XEvent xev;
+ size_t i;
+
+ do {
+ for (i = 0; i < nevents && isreplaying; i++) {
+ process_event(&events[i]);
+
+ while (eventloop(0)) {
+ XNextEvent(dpy, &xev);
+ handle_event(&xev);
+ }
+ }
+ } while (replay_repeat);
+}
+
+void
+run(void)
+{
+ struct event ev;
+ XEvent xev;
+ struct timespec tc, tp;
+ struct timeval tv1, tv2, tvr;
+ int isfirst;
+ float sleeptime;
+
+ /* initial clock / timing */
+ if (!timefirstevent) {
+ clock_gettime(CLOCK_MONOTONIC, &tc);
+ memcpy(&tp, &tc, sizeof(tp));
+ }
+
+ isfirst = 1;
+ isrunning = 1; /* why are you running? */
+ while (isrunning) {
+ if (!eventloop(1))
+ continue;
+ XNextEvent(dpy, &xev);
+
+ /* timing relative to previous event */
+ memcpy(&tp, &tc, sizeof(tp));
+ clock_gettime(CLOCK_MONOTONIC, &tc);
+
+ if (timefirstevent && isfirst) {
+ memcpy(&tp, &tc, sizeof(tp));
+ isfirst = 0;
+ }
+
+ TIMESPEC_TO_TIMEVAL(&tv1, &tp);
+ TIMESPEC_TO_TIMEVAL(&tv2, &tc);
+ timersub(&tv2, &tv1, &tvr);
+ if (record_usesleep) {
+ sleeptime = tvr.tv_sec + (tvr.tv_usec / 1000000.0);
+ if (sleeptime > 0.0) {
+ ev.evtype = EventSleep;
+ ev.sleeptime = sleeptime;
+
+ if (isrecording && event2line(&ev)) {
+ fflush(stdout); /* force flushing the stream line buffer */
+
+ /* add event */
+ if (!(events = realloc(events, (nevents + 1) * sizeof(ev))))
+ err(1, "realloc");
+ memcpy(&events[nevents], &ev, sizeof(ev));
+ nevents++;
+ }
+ }
+ }
+
+ handle_event(&xev);
+ }
+}
+
+/* prevent errors from ever stopping the program */
+int
+xerror(Display *dpy, XErrorEvent *ee)
+{
+ return 0;
+}
+
+void
+sighandler(int signo)
+{
+ switch (signo) {
+ case SIGABRT: state_sigabrt = 1; break;
+ case SIGCONT: state_sigcont = 1; break;
+ case SIGHUP: state_sighup = 1; break;
+ case SIGUSR1: state_sigusr1 = 1; break;
+ case SIGUSR2: state_sigusr2 = 1; break;
+ }
+}
+
+int
+main(int argc, char **argv)
+{
+ struct sigaction sa = { 0 };
+
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = sighandler;
+
+ sigaction(SIGABRT, &sa, NULL);
+ sigaction(SIGCONT, &sa, NULL);
+ sigaction(SIGHUP, &sa, NULL);
+ sigaction(SIGUSR1, &sa, NULL);
+ sigaction(SIGUSR2, &sa, NULL);
+
+ if ((dpy = XOpenDisplay(NULL)) == NULL)
+ errx(1, "XOpenDisplay");
+
+ screen = DefaultScreen(dpy);
+ root = RootWindow(dpy, screen);
+ win = root;
+ if ((xfd = ConnectionNumber(dpy)) == -1)
+ errx(1, "No file descriptor returned from ConnectionNumber()");
+
+ XSetErrorHandler(xerror);
+
+ xinput_setup();
+
+ if (argc > 1) {
+ replay_file = argv[1];
+ read_events_from_file(replay_file);
+ replay_start();
+ replay_stop();
+ run(); /* keep running */
+ } else {
+ run(); /* keep running */
+ }
+
+ XCloseDisplay(dpy);
+
+ return 0;
+}