From ff8955506d3d4391b1efdcb8ebc77293f79d1ff9 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 25 Aug 2021 22:53:44 -0600 Subject: [PATCH 01/30] Add initial README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b7395ba..c5bedbe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # symkey -A simple play tool for capturing keyboard and mouse and replaying them. Useful for Hypixel Skyblock \ No newline at end of file +This is a quick-and-dirty project for programmatically controlling keyboard and mouse. +I use it primarily to autoplay Minecraft on Hypixel Skyblock. +Shhh, don't tell. From 342926610b12558c24d937a3735d5d1b77cae21e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 25 Aug 2021 22:54:05 -0600 Subject: [PATCH 02/30] Add initial working implementation of capture and playback. This only works on my system with my modified version of the mouse library. --- capture.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++ playback.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 capture.py create mode 100644 playback.py diff --git a/capture.py b/capture.py new file mode 100644 index 0000000..cdaf786 --- /dev/null +++ b/capture.py @@ -0,0 +1,103 @@ +import argparse +import functools +import keyboard +import logging +import mouse +import pickle +import queue +import sys +import threading +import time + +LOGGER = logging.getLogger("capture") + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "-d", "--delay", + default=0, + type=int, + help="Seconds to wait before capture." + ) + parser.add_argument( + "-t", "--trigger", + default=None, + help="The key to use to trigger start and stop of capture." + ) + parser.add_argument( + "-o", "--output", + default="dump.capture", + help="Name of the file to capture to." + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Show verbose logging.", + ) + args = parser.parse_args() + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO) + if args.delay and args.trigger: + print("You cannot specify 'delay' and 'trigger'") + sys.exit(1) + + now = time.time() + _do_delay(args.delay) + _do_trigger(args.trigger) + # Add dummy events to lock in the time start + event_queue = queue.Queue() + event_queue.put( + mouse.ButtonEvent( + event_type=mouse.UP, + button=mouse.RIGHT, + time=now, + )) + event_queue.put( + keyboard.KeyboardEvent( + event_type=keyboard.KEY_UP, + name=" ", + scan_code=57, + time=now, + ) + ) + hook = functools.partial(_on_hook, event_queue) + keyhook = keyboard.hook(hook) + mousehook = mouse.hook(hook) + print("Capturing...") + try: + keyboard.wait(args.trigger) + except KeyboardInterrupt: + keyboard.unhook(keyhook) + mouse.unhook(mousehook) + + _save_events(event_queue, args.output) + +def _on_hook(event_queue, event): + LOGGER.debug(str(event)) + event_queue.put(event, block=False) + +def _do_delay(delay: int) -> None: + if not delay: + return + print("\n") + for i in range(delay): + print(f"\rStarting in {delay-i} seconds") + time.sleep(1) + +def _do_trigger(trigger: str) -> None: + if not trigger: + return + print(f"Waiting for '{trigger}'") + keyboard.wait(trigger) + +def _save_events(event_q: queue.Queue, filename: str) -> None: + events = [] + while not event_q.empty(): + event = event_q.get(block=False) + events.append(event) + with open(filename, "wb") as output: + pickle.dump(events, output) + print(f"Wrote to {filename}") + +if __name__ == "__main__": + main() diff --git a/playback.py b/playback.py new file mode 100644 index 0000000..d7f72e9 --- /dev/null +++ b/playback.py @@ -0,0 +1,95 @@ +import argparse +import keyboard +import logging +import mouse +import pickle +import threading +import time + +LOGGER = logging.getLogger("playback") + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "-d", "--delay", + default=5, + type=int, + help="Seconds to wait before replay." + ) + parser.add_argument( + "-i", "--input", + default="dump.capture", + help="Name of the file to replay from." + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Show verbose messages." + ) + args = parser.parse_args() + logging.basicConfig( + level = logging.DEBUG if args.verbose else logging.INFO + ) + _do_delay(args.delay) + + events = _load_events(args.input) + _play_events(events) + +def _play_events(events) -> None: + LOGGER.info("Playback started.") + key_state = keyboard.stash_state() + last_time = None + for event in events: + if last_time is not None: + to_sleep = event.time - last_time + if to_sleep > 0: + time.sleep(to_sleep) + last_time = event.time + if isinstance(event, keyboard.KeyboardEvent): + _play_event_keyboard(event) + elif any([ + isinstance(event, mouse.ButtonEvent), + isinstance(event, mouse.MoveEvent), + isinstance(event, mouse.WheelEvent), + ]): + _play_event_mouse(event) + else: + raise ValueError(f"Not a recognized event {event}") + + keyboard.restore_modifiers(key_state) + LOGGER.info("Done.") + +def _play_event_keyboard(event) -> None: + LOGGER.debug("Key %s", event) + key = event.scan_code or event.name + keyboard.press(key) if event.event_type == keyboard.KEY_DOWN else keyboard.release(key) + +def _play_event_mouse(event) -> None: + LOGGER.debug("Mouse %s", event) + if isinstance(event, mouse.ButtonEvent): + if event.event_type == mouse.UP: + mouse.release(event.button) + else: + mouse.press(event.button) + elif isinstance(event, mouse.MoveEvent): + mouse.move(event.x, event.y, absolute=True) + elif isinstance(event, mouse.WheelEvent): + mouse.wheel(event.delta) + +def _do_delay(delay: int) -> None: + if not delay: + return + print("\n") + for i in range(delay): + print(f"\rStarting in {delay-i} seconds") + time.sleep(1) + +def _load_events(filename: str): + with open(filename, "rb") as input_: + events = pickle.load(input_) + LOGGER.debug("Loaded %s", filename) + return events + + +if __name__ == "__main__": + main() From 17bcf23de234221282ce47af6d69a46eb3d5dc09 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 25 Aug 2021 22:54:53 -0600 Subject: [PATCH 03/30] Add initial gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af8b968 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ve/ From 76f9b495e2a40f3045d9928dc8c381f11be0e9fd Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 25 Aug 2021 22:55:13 -0600 Subject: [PATCH 04/30] Add a short program to dump key information. --- show-keys | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 show-keys diff --git a/show-keys b/show-keys new file mode 100755 index 0000000..84e846a --- /dev/null +++ b/show-keys @@ -0,0 +1,15 @@ +#!env python3 +import keyboard + +def main() -> None: + keyboard.hook(_on_event) + try: + keyboard.wait() + except KeyboardInterrupt: + print("End.") + +def _on_event(event: keyboard.KeyboardEvent) -> None: + print(event.to_json()) + +if __name__ == "__main__": + main() From 71553e0648debf21cb07d39b5c6190b9dc2ad90b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 25 Aug 2021 22:55:29 -0600 Subject: [PATCH 05/30] Add my uinput test program. I used this to debug the interations with the kernel module in the Python code and determine which structs or constants had changed. --- uinput-test.cc | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 uinput-test.cc diff --git a/uinput-test.cc b/uinput-test.cc new file mode 100644 index 0000000..750bc5d --- /dev/null +++ b/uinput-test.cc @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include +#include + +void emit(int fd, int type, int code, int val) { + struct input_event ie; + ie.type = type; + ie.code = code; + ie.value = val; + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + write(fd, &ie, sizeof(ie)); +} + + +int main(void) { + struct uinput_setup usetup; + // int i = 50; + int i = 0; + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + +/* enable mouse button left and relative events */ + printf("EV_KEY 0x%08x\n", EV_KEY); + printf("UI_SET_EVBIT 0x%08lx\n", UI_SET_EVBIT); + printf("UI_SET_KEYBIT 0x%08lx\n", UI_SET_KEYBIT); + printf("BTN_LEFT 0x%08x\n", BTN_LEFT); + printf("EV_REL 0x%08x\n", EV_REL); + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x1234; /* sample vendor */ + usetup.id.product = 0x5678; /* sample product */ + strcpy(usetup.name, "Example device"); + + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + + /* + * On UI_DEV_CREATE the kernel will create the device node for this + * device. We are inserting a pause here so that userspace has time + * to detect, initialize the new device, and can start listening to + * the event, otherwise it will not notice the event we are about + * to send. This pause is only needed in our example code! + */ + sleep(1); + + /* Move the mouse diagonally, 5 units per axis */ + while (i--) { + emit(fd, EV_REL, REL_X, 5); + emit(fd, EV_REL, REL_Y, 5); + emit(fd, EV_SYN, SYN_REPORT, 0); + usleep(15000); + } + + // Click left mouse button. + + emit(fd, EV_KEY, BTN_MOUSE, 1); + emit(fd, EV_SYN, SYN_REPORT, 0); + emit(fd, EV_KEY, BTN_MOUSE, 0); + emit(fd, EV_SYN, SYN_REPORT, 0); + /* + * Give userspace some time to read the events before we destroy the + * device with UI_DEV_DESTOY. + */ + sleep(1); + + ioctl(fd, UI_DEV_DESTROY); + close(fd); + + return 0; +} From 02f04b78a66f7c130e90bfa37340ca388a2e1468 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 26 Aug 2021 07:36:41 -0600 Subject: [PATCH 06/30] Show mouse raw input from mouse library. I'm missing a bunch of precision which is no good for capture. --- show-mouse | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 show-mouse diff --git a/show-mouse b/show-mouse new file mode 100755 index 0000000..63713a9 --- /dev/null +++ b/show-mouse @@ -0,0 +1,19 @@ +#!env python3 +import keyboard +import logging +import mouse + +def main() -> None: + logging.basicConfig(level=logging.DEBUG) + mouse.hook(_on_event) + try: + mouse.wait() + except KeyboardInterrupt: + pass + print("End.") + +def _on_event(event: keyboard.KeyboardEvent) -> None: + print(event) + +if __name__ == "__main__": + main() From 692772f8094deb88614f5f5f3a3b323dffd3bb97 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 26 Aug 2021 10:49:08 -0600 Subject: [PATCH 07/30] Add working C program to get tiny mouse movements. Turns out that the Python mouse library is losing the fine-grained mouse position data, which we really need. --- show-mouse.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 show-mouse.c diff --git a/show-mouse.c b/show-mouse.c new file mode 100644 index 0000000..87a944c --- /dev/null +++ b/show-mouse.c @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include + +#define MOUSEFILE "/dev/input/mouse1" +// +int main() +{ + int fd; + struct input_event ie; + // + unsigned char button,bLeft,bMiddle,bRight; + char x,y; + int absolute_x,absolute_y; + + if((fd = open(MOUSEFILE, O_RDONLY)) == -1) { + printf("Device open ERROR\n"); + exit(EXIT_FAILURE); + } + else + { + printf("Device open OK\n"); + } + // + printf("right-click to set absolute x,y coordinates origin (0,0)\n"); + while(read(fd, &ie, sizeof(struct input_event))) + { + unsigned char *ptr = (unsigned char*)&ie; + int i; + // + button=ptr[0]; + bLeft = button & 0x1; + bMiddle = ( button & 0x4 ) > 0; + bRight = ( button & 0x2 ) > 0; + x=(char) ptr[1];y=(char) ptr[2]; + // printf("bLEFT:%d, bMIDDLE: %d, bRIGHT: %d, rx: %d ry=%d\n",bLeft,bMiddle,bRight, x,y); + // + absolute_x+=x; + absolute_y-=y; + if (bRight==1) + { + absolute_x=0; + absolute_y=0; + printf("Absolute x,y coords origin recorded\n"); + } + // + printf("Absolute coords from TOP_LEFT= %i %i\n",absolute_x,absolute_y); + // + // comment to disable the display of raw event structure datas + // + for(i=0; i Date: Thu, 26 Aug 2021 11:26:53 -0600 Subject: [PATCH 08/30] Add working beginnings of capture program. Only does a single mouse at this point, but it's super fast and the resolution is perfect. --- capture.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 capture.c diff --git a/capture.c b/capture.c new file mode 100644 index 0000000..f41f5d7 --- /dev/null +++ b/capture.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include + +void dump_event(struct timespec* start, struct input_event* event); +int stream_events(char* device_path); + +static inline void timespec_diff(struct timespec* a, struct timespec* b, struct timespec* result) { + result->tv_sec = a->tv_sec - b->tv_sec; + result->tv_nsec = a->tv_nsec - b->tv_nsec; + if (result->tv_nsec < 0) { + --result->tv_sec; + result->tv_nsec += 1000000000L; + } +} + +void dump_event(struct timespec* start, struct input_event* event) { + unsigned char button, bLeft, bMiddle, bRight; + unsigned char *ptr = (unsigned char*)event; + int i; + char x, y; + struct timespec now; + struct timespec diff; + + clock_gettime(CLOCK_MONOTONIC, &now); + timespec_diff(&now, start, &diff); + + button = ptr[0]; + bLeft = button & 0x1; + bMiddle = ( button & 0x4 ) > 0; + bRight = ( button & 0x2 ) > 0; + x=(char) ptr[1];y=(char) ptr[2]; + printf("%ld.%ld,m,l%d,m%d,r%d,x%d,y%d\n", + diff.tv_sec, + diff.tv_nsec, + bLeft, bMiddle, bRight, x, y); + + // + // comment to disable the display of raw event structure datas + // + // for(i=0; i Date: Fri, 27 Aug 2021 10:09:32 -0600 Subject: [PATCH 09/30] Add epoll implementation to capture keyboard and mouse. Yay, double capture! --- capture.c | 154 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 31 deletions(-) diff --git a/capture.c b/capture.c index f41f5d7..3cf6c95 100644 --- a/capture.c +++ b/capture.c @@ -1,12 +1,25 @@ +#include #include #include #include #include #include -#include -void dump_event(struct timespec* start, struct input_event* event); -int stream_events(char* device_path); +#include +#include + +#define MAX_EVENTS 16 +#define CONTENT_BUFFER_SIZE 32 + +static char* KEYBOARD = "k"; +static char* MOUSE = "m"; + +int dump_event(struct timespec* start, int fd, char* type); + +int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event); +int event_content_mouse(char* buffer, int buffer_size, struct input_event* event); + +int stream_events(char* mouse_path, char* keyboard_path); static inline void timespec_diff(struct timespec* a, struct timespec* b, struct timespec* result) { result->tv_sec = a->tv_sec - b->tv_sec; @@ -17,59 +30,138 @@ static inline void timespec_diff(struct timespec* a, struct timespec* b, struct } } -void dump_event(struct timespec* start, struct input_event* event) { - unsigned char button, bLeft, bMiddle, bRight; - unsigned char *ptr = (unsigned char*)event; - int i; - char x, y; +int dump_event(struct timespec* start, int fd, char* type) { + struct input_event event; struct timespec now; struct timespec diff; + char content_buffer[CONTENT_BUFFER_SIZE]; + + int result = read(fd, &event, sizeof(struct input_event)); + if(result < 0) { + fprintf(stderr, "Failed to read an event: %d", errno); + return 1; + } clock_gettime(CLOCK_MONOTONIC, &now); timespec_diff(&now, start, &diff); + if(type == MOUSE) { + if(event_content_mouse(content_buffer, CONTENT_BUFFER_SIZE, &event)) { + return 1; + } + } else if(type == KEYBOARD) { + if(event.type == EV_SYN) { + return 0; + } + if(event_content_keyboard(content_buffer, CONTENT_BUFFER_SIZE, &event)) { + return 1; + } + } else { + fprintf(stderr, "Unknown event type.\n"); + return 1; + } + + printf("%ld.%ld,%s,%s\n", + diff.tv_sec, + diff.tv_nsec, + type, + content_buffer); + + return 0; +} + +int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event) { + sprintf(buffer, "?"); + return 0; +} + +int event_content_mouse(char* buffer, int buffer_size, struct input_event* event) { + unsigned char button, bLeft, bMiddle, bRight; + unsigned char *ptr = (unsigned char*)event; + int i; + char x, y; button = ptr[0]; bLeft = button & 0x1; bMiddle = ( button & 0x4 ) > 0; bRight = ( button & 0x2 ) > 0; - x=(char) ptr[1];y=(char) ptr[2]; - printf("%ld.%ld,m,l%d,m%d,r%d,x%d,y%d\n", - diff.tv_sec, - diff.tv_nsec, + x=(char) ptr[1]; + y=(char) ptr[2]; + + int chars = sprintf(buffer, "l%d,m%d,r%d,x%d,y%d", bLeft, bMiddle, bRight, x, y); - - // - // comment to disable the display of raw event structure datas - // - // for(i=0; i Date: Fri, 27 Aug 2021 10:33:08 -0600 Subject: [PATCH 10/30] Show event content for keyboard. --- capture.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/capture.c b/capture.c index 3cf6c95..60b90f1 100644 --- a/capture.c +++ b/capture.c @@ -50,7 +50,8 @@ int dump_event(struct timespec* start, int fd, char* type) { return 1; } } else if(type == KEYBOARD) { - if(event.type == EV_SYN) { + // Ignore all but EV_KEY events on keyboard, they have no useful content. + if(event.type != EV_KEY) { return 0; } if(event_content_keyboard(content_buffer, CONTENT_BUFFER_SIZE, &event)) { @@ -71,7 +72,10 @@ int dump_event(struct timespec* start, int fd, char* type) { } int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event) { - sprintf(buffer, "?"); + sprintf(buffer, "%d,%d,%d", + event->type, + event->code, + event->value); return 0; } From 165623fa20f1dbeba7f449890b998c5933ef8121 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 27 Aug 2021 10:56:44 -0600 Subject: [PATCH 11/30] Add basic Makefile and output directory --- .gitignore | 1 + Makefile | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index af8b968..dbeaf58 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +bin/ ve/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e969b20 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: capture + +bin: + mkdir -p bin + +capture: bin capture.c + gcc capture.c -o bin/capture + +clean: + rm -Rf bin From 4a0e7834b3a8f372067fb729ebc56bbc80b24aea Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 27 Aug 2021 11:10:53 -0600 Subject: [PATCH 12/30] Flush stdout so our file gets written immediately --- capture.c | 1 + 1 file changed, 1 insertion(+) diff --git a/capture.c b/capture.c index 60b90f1..b68a312 100644 --- a/capture.c +++ b/capture.c @@ -68,6 +68,7 @@ int dump_event(struct timespec* start, int fd, char* type) { type, content_buffer); + fflush(stdout); return 0; } From 6ed5162e8ecffb1e161cc2584ce3c4fb1c04bbbf Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 27 Aug 2021 11:12:01 -0600 Subject: [PATCH 13/30] Add playback implementation in C. It doesn't do anything but read lines yet. --- Makefile | 5 ++- playback.c | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 playback.c diff --git a/Makefile b/Makefile index e969b20..3680ec7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: capture +all: capture playback bin: mkdir -p bin @@ -8,3 +8,6 @@ capture: bin capture.c clean: rm -Rf bin + +playback: bin playback.c + gcc playback.c -o bin/playback diff --git a/playback.c b/playback.c new file mode 100644 index 0000000..3c480fb --- /dev/null +++ b/playback.c @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +int read_file(char* filename); + +void emit(int fd, int type, int code, int val) { + struct input_event ie; + ie.type = type; + ie.code = code; + ie.value = val; + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + write(fd, &ie, sizeof(ie)); +} + + +int main(int argc, char* argv[]) { + if(argc < 2) { + printf("Please provide a capture file."); + exit(EXIT_FAILURE); + } + + if(read_file(argv[1])) { + exit(EXIT_FAILURE); + } + return 0; +} + +int read_file(char* filename) { + FILE* fp; + char* line = NULL; + size_t len = 0; + ssize_t read; + + fp = fopen(filename, "r"); + if (fp == NULL) { + printf("Failed to open file %s: %d\n", filename, errno); + return 1; + } + + while((read = getline(&line, &len, fp)) != -1) { + printf("%s", line); + } +} + +int setup_mouse() { + struct uinput_setup usetup; + // int i = 50; + int i = 0; + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + +/* enable mouse button left and relative events */ + printf("EV_KEY 0x%08x\n", EV_KEY); + printf("UI_SET_EVBIT 0x%08lx\n", UI_SET_EVBIT); + printf("UI_SET_KEYBIT 0x%08lx\n", UI_SET_KEYBIT); + printf("BTN_LEFT 0x%08x\n", BTN_LEFT); + printf("EV_REL 0x%08x\n", EV_REL); + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x1234; /* sample vendor */ + usetup.id.product = 0x5678; /* sample product */ + strcpy(usetup.name, "Example device"); + + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + + /* + * On UI_DEV_CREATE the kernel will create the device node for this + * device. We are inserting a pause here so that userspace has time + * to detect, initialize the new device, and can start listening to + * the event, otherwise it will not notice the event we are about + * to send. This pause is only needed in our example code! + */ + sleep(1); + + /* Move the mouse diagonally, 5 units per axis */ + while (i--) { + emit(fd, EV_REL, REL_X, 5); + emit(fd, EV_REL, REL_Y, 5); + emit(fd, EV_SYN, SYN_REPORT, 0); + usleep(15000); + } + + // Click left mouse button. + + emit(fd, EV_KEY, BTN_MOUSE, 1); + emit(fd, EV_SYN, SYN_REPORT, 0); + emit(fd, EV_KEY, BTN_MOUSE, 0); + emit(fd, EV_SYN, SYN_REPORT, 0); + /* + * Give userspace some time to read the events before we destroy the + * device with UI_DEV_DESTOY. + */ + sleep(1); + + ioctl(fd, UI_DEV_DESTROY); + close(fd); + + return 0; +} From 61843f23c353a6d7b3b2368c7fbea90fa4e6212c Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 27 Aug 2021 12:44:14 -0600 Subject: [PATCH 14/30] Add sleep logic in playback. This also removes the "." between the seconds and nanos so that we don't mistake it for a float, which it isn't. --- capture.c | 2 +- playback.c | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/capture.c b/capture.c index b68a312..04ed6fc 100644 --- a/capture.c +++ b/capture.c @@ -62,7 +62,7 @@ int dump_event(struct timespec* start, int fd, char* type) { return 1; } - printf("%ld.%ld,%s,%s\n", + printf("%ld %ld,%s,%s\n", diff.tv_sec, diff.tv_nsec, type, diff --git a/playback.c b/playback.c index 3c480fb..1f47a60 100644 --- a/playback.c +++ b/playback.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,45 @@ void emit(int fd, int type, int code, int val) { write(fd, &ie, sizeof(ie)); } +int handle_line(char* line) { + static time_t timer_seconds = 0; + static long timer_nanos = 0; + + time_t seconds; + long nanos; + char type; + char details[32]; + struct timespec to_sleep; + struct timespec remaining; + + int matched = sscanf(line, "%ld %ld,%c,%s\n", &seconds, &nanos, &type, details); + if(matched == 0) { + printf("Line '%s' appears incorrect. Exiting", line); + return 1; + } else if(matched < 4) { + printf("Only matched %d", matched); + return 1; + } + + to_sleep.tv_sec = seconds - timer_seconds; + to_sleep.tv_nsec = nanos - timer_nanos; + if(to_sleep.tv_nsec < 0) { + --to_sleep.tv_nsec; + to_sleep.tv_nsec += 1000000000L; + } + printf("%s", line); + // printf("Timer %ld %ld\n", timer_seconds, timer_nanos); + // printf("Read %ld %ld\n", seconds, nanos); + // printf("Sleep %ld %ld\n", to_sleep.tv_sec, to_sleep.tv_nsec); + int result = nanosleep(&to_sleep, &remaining); + while(nanosleep(&to_sleep, &remaining) == -1) { + to_sleep.tv_sec = remaining.tv_sec; + to_sleep.tv_nsec = remaining.tv_nsec; + } + timer_seconds = seconds; + timer_nanos = nanos; + return 0; +} int main(int argc, char* argv[]) { if(argc < 2) { @@ -48,7 +88,9 @@ int read_file(char* filename) { } while((read = getline(&line, &len, fp)) != -1) { - printf("%s", line); + if(handle_line(line)) { + return 1; + } } } From 829432d443e60782e05bbf7cbb9aeffef2eafc3e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 30 Aug 2021 10:11:10 -0600 Subject: [PATCH 15/30] Make mouse playback work. This only includes left mouse button clicks, not right mouse button clicks or scrollwheels. --- playback.c | 160 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 60 deletions(-) diff --git a/playback.c b/playback.c index 1f47a60..a95ac96 100644 --- a/playback.c +++ b/playback.c @@ -10,7 +10,13 @@ #include #include -int read_file(char* filename); +int handle_keyboard(char* details, int keyboard_fd); +int handle_line(char* line, int keyboard_fd, int mouse_fd); +int handle_mouse(char* details, int mouse_fd); +int read_file(char* filename, int keyboard_fd, int mouse_fd); +int setup_mouse(); +void teardown_keyboard(int fd); +void teardown_mouse(int fd); void emit(int fd, int type, int code, int val) { struct input_event ie; @@ -23,7 +29,19 @@ void emit(int fd, int type, int code, int val) { write(fd, &ie, sizeof(ie)); } -int handle_line(char* line) { +int handle_keyboard(char* details, int keyboard_fd) { + int code, event_type, value; + int matched = sscanf(details, "%d,%d,%d", &event_type, &code, &value); + if(matched != 3) { + printf("Didn't match enough values for a keyboard event.\n"); + return 1; + } + printf("Event type: %d, Code: %d, Value: %d\n", + event_type, code, value); + return 0; +} + +int handle_line(char* line, int keyboard_fd, int mouse_fd) { static time_t timer_seconds = 0; static long timer_nanos = 0; @@ -49,7 +67,6 @@ int handle_line(char* line) { --to_sleep.tv_nsec; to_sleep.tv_nsec += 1000000000L; } - printf("%s", line); // printf("Timer %ld %ld\n", timer_seconds, timer_nanos); // printf("Read %ld %ld\n", seconds, nanos); // printf("Sleep %ld %ld\n", to_sleep.tv_sec, to_sleep.tv_nsec); @@ -60,6 +77,47 @@ int handle_line(char* line) { } timer_seconds = seconds; timer_nanos = nanos; + + if(type == 'k') { + return handle_keyboard(details, keyboard_fd); + } else if(type == 'm') { + return handle_mouse(details, mouse_fd); + } else { + printf("Unexpected type %c/n", type); + return 1; + } +} + +int handle_mouse(char* details, int mouse_fd) { + static int current_left = 0; + static int current_middle = 0; + static int current_right = 0; + + int left, middle, right, x, y; + int matched = sscanf(details, "l%d,m%d,r%d,x%d,y%d", + &left, &middle, &right, &x, &y); + if(matched != 5) { + printf("Failed to match enough data for a mouse event.\n"); + return 1; + } + printf("L: %d M: %d, R: %d, X: %d, Y: %d\n", + left, middle, right, x, y); + + /* Move the mouse diagonally, 5 units per axis */ + if(x != 0) { + emit(mouse_fd, EV_REL, REL_X, x); + } + if(y != 0) { + emit(mouse_fd, EV_REL, REL_Y, y); + } + + if(left != current_left) { + emit(mouse_fd, EV_KEY, BTN_MOUSE, left); + current_left = left; + } + emit(mouse_fd, EV_SYN, SYN_REPORT, 0); + + return 0; } @@ -69,13 +127,18 @@ int main(int argc, char* argv[]) { exit(EXIT_FAILURE); } - if(read_file(argv[1])) { - exit(EXIT_FAILURE); + int result = 0; + int mouse_fd = setup_mouse(); + int keyboard_fd = 0; + if(read_file(argv[1], keyboard_fd, mouse_fd)) { + result = EXIT_FAILURE; } - return 0; + teardown_keyboard(keyboard_fd); + teardown_mouse(mouse_fd); + return result; } -int read_file(char* filename) { +int read_file(char* filename, int keyboard_fd, int mouse_fd) { FILE* fp; char* line = NULL; size_t len = 0; @@ -88,7 +151,7 @@ int read_file(char* filename) { } while((read = getline(&line, &len, fp)) != -1) { - if(handle_line(line)) { + if(handle_line(line, keyboard_fd, mouse_fd)) { return 1; } } @@ -96,65 +159,42 @@ int read_file(char* filename) { int setup_mouse() { struct uinput_setup usetup; - // int i = 50; - int i = 0; int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); /* enable mouse button left and relative events */ - printf("EV_KEY 0x%08x\n", EV_KEY); - printf("UI_SET_EVBIT 0x%08lx\n", UI_SET_EVBIT); - printf("UI_SET_KEYBIT 0x%08lx\n", UI_SET_KEYBIT); - printf("BTN_LEFT 0x%08x\n", BTN_LEFT); - printf("EV_REL 0x%08x\n", EV_REL); - ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_KEY); - ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); - ioctl(fd, UI_SET_EVBIT, EV_REL); - ioctl(fd, UI_SET_RELBIT, REL_X); - ioctl(fd, UI_SET_RELBIT, REL_Y); + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); - memset(&usetup, 0, sizeof(usetup)); - usetup.id.bustype = BUS_USB; - usetup.id.vendor = 0x1234; /* sample vendor */ - usetup.id.product = 0x5678; /* sample product */ - strcpy(usetup.name, "Example device"); + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x1234; /* sample vendor */ + usetup.id.product = 0x5678; /* sample product */ + strcpy(usetup.name, "Playback mouses"); - ioctl(fd, UI_DEV_SETUP, &usetup); - ioctl(fd, UI_DEV_CREATE); + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); - /* - * On UI_DEV_CREATE the kernel will create the device node for this - * device. We are inserting a pause here so that userspace has time - * to detect, initialize the new device, and can start listening to - * the event, otherwise it will not notice the event we are about - * to send. This pause is only needed in our example code! - */ - sleep(1); - - /* Move the mouse diagonally, 5 units per axis */ - while (i--) { - emit(fd, EV_REL, REL_X, 5); - emit(fd, EV_REL, REL_Y, 5); - emit(fd, EV_SYN, SYN_REPORT, 0); - usleep(15000); - } - - // Click left mouse button. - - emit(fd, EV_KEY, BTN_MOUSE, 1); - emit(fd, EV_SYN, SYN_REPORT, 0); - emit(fd, EV_KEY, BTN_MOUSE, 0); - emit(fd, EV_SYN, SYN_REPORT, 0); - /* - * Give userspace some time to read the events before we destroy the - * device with UI_DEV_DESTOY. - */ - sleep(1); - - ioctl(fd, UI_DEV_DESTROY); - close(fd); - - return 0; + /* + * On UI_DEV_CREATE the kernel will create the device node for this + * device. We are inserting a pause here so that userspace has time + * to detect, initialize the new device, and can start listening to + * the event, otherwise it will not notice the event we are about + * to send. This pause is only needed in our example code! + */ + // sleep(1); + printf("Setup mouse to fd %d", fd); + return fd; } + +void teardown_mouse(int fd) { + ioctl(fd, UI_DEV_DESTROY); + close(fd); +} + +void teardown_keyboard(int fd) {} From a3579fd3395320435bbc68c317e4354a62f389d6 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 31 Aug 2021 15:14:22 -0600 Subject: [PATCH 16/30] Add support for playing back keyboard messages. Also fix a timing bug where I was waiting 1 second too long any time the nansec clock rolled over. --- playback.c | 100 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/playback.c b/playback.c index a95ac96..98b3eaa 100644 --- a/playback.c +++ b/playback.c @@ -10,13 +10,12 @@ #include #include -int handle_keyboard(char* details, int keyboard_fd); -int handle_line(char* line, int keyboard_fd, int mouse_fd); -int handle_mouse(char* details, int mouse_fd); -int read_file(char* filename, int keyboard_fd, int mouse_fd); -int setup_mouse(); -void teardown_keyboard(int fd); -void teardown_mouse(int fd); +int handle_udevice(char* details, int udevice_fd); +int handle_line(char* line, int udevice_fd); +int handle_mouse(char* details, int udevice_fd); +int read_file(char* filename, int udevice_fd); +int setup_udevice(); +void teardown_udevice(int fd); void emit(int fd, int type, int code, int val) { struct input_event ie; @@ -29,21 +28,30 @@ void emit(int fd, int type, int code, int val) { write(fd, &ie, sizeof(ie)); } -int handle_keyboard(char* details, int keyboard_fd) { +int handle_keyboard(char* details, int udevice_fd) { int code, event_type, value; int matched = sscanf(details, "%d,%d,%d", &event_type, &code, &value); if(matched != 3) { printf("Didn't match enough values for a keyboard event.\n"); return 1; } - printf("Event type: %d, Code: %d, Value: %d\n", - event_type, code, value); + // printf("Event type: %d, Code: %d, Value: %d\n", + // event_type, code, value); + if(event_type != 1) { + printf("Not sure what to do with event type %d", event_type); + return 1; + } + emit(udevice_fd, EV_KEY, code, value); + emit(udevice_fd, EV_SYN, SYN_REPORT, 0); + return 0; } -int handle_line(char* line, int keyboard_fd, int mouse_fd) { +int handle_line(char* line, int udevice_fd) { static time_t timer_seconds = 0; static long timer_nanos = 0; + static time_t total_seconds = 0; + static long total_nanos = 0; time_t seconds; long nanos; @@ -61,34 +69,47 @@ int handle_line(char* line, int keyboard_fd, int mouse_fd) { return 1; } + remaining.tv_sec = 0; + remaining.tv_nsec = 0; to_sleep.tv_sec = seconds - timer_seconds; to_sleep.tv_nsec = nanos - timer_nanos; if(to_sleep.tv_nsec < 0) { - --to_sleep.tv_nsec; + --to_sleep.tv_sec; to_sleep.tv_nsec += 1000000000L; } // printf("Timer %ld %ld\n", timer_seconds, timer_nanos); // printf("Read %ld %ld\n", seconds, nanos); // printf("Sleep %ld %ld\n", to_sleep.tv_sec, to_sleep.tv_nsec); - int result = nanosleep(&to_sleep, &remaining); while(nanosleep(&to_sleep, &remaining) == -1) { + printf("Sleep harder\n"); to_sleep.tv_sec = remaining.tv_sec; to_sleep.tv_nsec = remaining.tv_nsec; } + if(remaining.tv_sec != 0 || remaining.tv_nsec != 0) { + printf("oops, remaining.\n"); + } + total_seconds += to_sleep.tv_sec; + total_nanos += to_sleep.tv_nsec; + if(total_nanos > 1000000000L) { + total_nanos -= 1000000000L; + total_seconds += 1; + } timer_seconds = seconds; timer_nanos = nanos; + printf("%ld %ld\tslept %ld %ld\n", + total_seconds, total_nanos, to_sleep.tv_sec, to_sleep.tv_nsec); if(type == 'k') { - return handle_keyboard(details, keyboard_fd); + return handle_keyboard(details, udevice_fd); } else if(type == 'm') { - return handle_mouse(details, mouse_fd); + return handle_mouse(details, udevice_fd); } else { printf("Unexpected type %c/n", type); return 1; } } -int handle_mouse(char* details, int mouse_fd) { +int handle_mouse(char* details, int udevice_fd) { static int current_left = 0; static int current_middle = 0; static int current_right = 0; @@ -100,22 +121,30 @@ int handle_mouse(char* details, int mouse_fd) { printf("Failed to match enough data for a mouse event.\n"); return 1; } - printf("L: %d M: %d, R: %d, X: %d, Y: %d\n", - left, middle, right, x, y); + // printf("L: %d M: %d, R: %d, X: %d, Y: %d\n", + // left, middle, right, x, y); /* Move the mouse diagonally, 5 units per axis */ if(x != 0) { - emit(mouse_fd, EV_REL, REL_X, x); + emit(udevice_fd, EV_REL, REL_X, x); } if(y != 0) { - emit(mouse_fd, EV_REL, REL_Y, y); + emit(udevice_fd, EV_REL, REL_Y, y); } if(left != current_left) { - emit(mouse_fd, EV_KEY, BTN_MOUSE, left); + emit(udevice_fd, EV_KEY, BTN_LEFT, left); current_left = left; } - emit(mouse_fd, EV_SYN, SYN_REPORT, 0); + if(middle != current_middle) { + emit(udevice_fd, EV_KEY, BTN_MIDDLE, middle); + current_middle = middle; + } + if(right != current_right) { + emit(udevice_fd, EV_KEY, BTN_RIGHT, right); + current_right = right; + } + emit(udevice_fd, EV_SYN, SYN_REPORT, 0); return 0; @@ -128,17 +157,15 @@ int main(int argc, char* argv[]) { } int result = 0; - int mouse_fd = setup_mouse(); - int keyboard_fd = 0; - if(read_file(argv[1], keyboard_fd, mouse_fd)) { + int udevice_fd = setup_udevice(); + if(read_file(argv[1], udevice_fd)) { result = EXIT_FAILURE; } - teardown_keyboard(keyboard_fd); - teardown_mouse(mouse_fd); + teardown_udevice(udevice_fd); return result; } -int read_file(char* filename, int keyboard_fd, int mouse_fd) { +int read_file(char* filename, int udevice_fd) { FILE* fp; char* line = NULL; size_t len = 0; @@ -151,13 +178,13 @@ int read_file(char* filename, int keyboard_fd, int mouse_fd) { } while((read = getline(&line, &len, fp)) != -1) { - if(handle_line(line, keyboard_fd, mouse_fd)) { + if(handle_line(line, udevice_fd)) { return 1; } } } -int setup_mouse() { +int setup_udevice() { struct uinput_setup usetup; int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); @@ -165,6 +192,14 @@ int setup_mouse() { ioctl(fd, UI_SET_EVBIT, EV_KEY); + // Add keyboard keys. We could do this individually but we're super + // lazy and it appears a loop should work fine based on the linux/input-event-codes.h header + for(int i = KEY_ESC; i <= KEY_MICMUTE; i++) { + ioctl(fd, UI_SET_KEYBIT, i); + } + + // Add mouse buttons + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); ioctl(fd, UI_SET_EVBIT, EV_REL); @@ -188,13 +223,10 @@ int setup_mouse() { * to send. This pause is only needed in our example code! */ // sleep(1); - printf("Setup mouse to fd %d", fd); return fd; } -void teardown_mouse(int fd) { +void teardown_udevice(int fd) { ioctl(fd, UI_DEV_DESTROY); close(fd); } - -void teardown_keyboard(int fd) {} From e65e4422769f41910cc40ac88df345ed79511178 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 1 Sep 2021 04:37:08 -0600 Subject: [PATCH 17/30] Add support for hotkey on capture. We need this so that we can ensure to line up where our character is before we get going. --- capture.c | 89 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/capture.c b/capture.c index 04ed6fc..8253022 100644 --- a/capture.c +++ b/capture.c @@ -14,12 +14,12 @@ static char* KEYBOARD = "k"; static char* MOUSE = "m"; -int dump_event(struct timespec* start, int fd, char* type); +int dump_event(struct timespec* start, struct input_event* event, char* type); int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event); int event_content_mouse(char* buffer, int buffer_size, struct input_event* event); -int stream_events(char* mouse_path, char* keyboard_path); +int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode); static inline void timespec_diff(struct timespec* a, struct timespec* b, struct timespec* result) { result->tv_sec = a->tv_sec - b->tv_sec; @@ -29,32 +29,44 @@ static inline void timespec_diff(struct timespec* a, struct timespec* b, struct result->tv_nsec += 1000000000L; } } + +static inline void timeval_diff(struct timeval* a, struct timeval* b, struct timeval* result) { + result->tv_sec = a->tv_sec - b->tv_sec; + result->tv_usec = a->tv_usec - b->tv_usec; + if (result->tv_usec < 0) { + --result->tv_sec; + result->tv_usec += 1000000L; + } +} + +static inline void time_diff(struct timeval* a, struct timespec* b, struct timeval* result) { + result->tv_sec = a->tv_sec - b->tv_sec; + result->tv_usec = a->tv_usec - (b->tv_nsec / 1000); + if (result->tv_usec < 0) { + --result->tv_sec; + result->tv_usec += 1000000L; + } +} -int dump_event(struct timespec* start, int fd, char* type) { - struct input_event event; +int dump_event(struct timespec* start, struct input_event* event, char* type) { struct timespec now; struct timespec diff; char content_buffer[CONTENT_BUFFER_SIZE]; - int result = read(fd, &event, sizeof(struct input_event)); - if(result < 0) { - fprintf(stderr, "Failed to read an event: %d", errno); - return 1; - } - clock_gettime(CLOCK_MONOTONIC, &now); timespec_diff(&now, start, &diff); + // time_diff(&(event.time), start, &diff); if(type == MOUSE) { - if(event_content_mouse(content_buffer, CONTENT_BUFFER_SIZE, &event)) { + if(event_content_mouse(content_buffer, CONTENT_BUFFER_SIZE, event)) { return 1; } } else if(type == KEYBOARD) { // Ignore all but EV_KEY events on keyboard, they have no useful content. - if(event.type != EV_KEY) { + if(event->type != EV_KEY) { return 0; } - if(event_content_keyboard(content_buffer, CONTENT_BUFFER_SIZE, &event)) { + if(event_content_keyboard(content_buffer, CONTENT_BUFFER_SIZE, event)) { return 1; } } else { @@ -98,6 +110,7 @@ int event_content_mouse(char* buffer, int buffer_size, struct input_event* event } int main(int argc, char* argv[]) { + int hotkey_scancode; if(argc < 2) { fprintf(stderr, "You must specify a mouse input to track like /dev/input/mouse1."); exit(EXIT_FAILURE); @@ -106,15 +119,25 @@ int main(int argc, char* argv[]) { fprintf(stderr, "You must specify a keyboard input to track like /dev/input/event3. If you're not sure which to use read through /proc/bus/input/devices and look for 'Handlers=eventX'"); exit(EXIT_FAILURE); } - int result = stream_events(argv[1], argv[2]); + if(argc < 4) { + fprintf(stderr, "You must specify a character to indicate when to start and stop capture. 53 for 'z'.\n"); + exit(EXIT_FAILURE); + } + int matched = sscanf(argv[3], "%d", &hotkey_scancode); + if(matched != 1) { + fprintf(stderr, "Failed to read hotkey scancode.\n"); + exit(EXIT_FAILURE); + } + int result = stream_events(argv[1], argv[2], hotkey_scancode); exit(result); } -int stream_events(char* mouse_path, char* keyboard_path) { +int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode) { int keyboard_fd, mouse_fd; struct timespec start; - struct epoll_event event, events[MAX_EVENTS]; + struct epoll_event e_event, events[MAX_EVENTS]; + int has_seen_hotkey = 0; int running = 1; int epoll_fd = epoll_create1(0); if(epoll_fd < 0) { @@ -135,36 +158,54 @@ int stream_events(char* mouse_path, char* keyboard_path) { else { fprintf(stderr, "%s open OK\n", keyboard_path); } - event.events = EPOLLIN; + e_event.events = EPOLLIN; - event.data.fd = keyboard_fd; - if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, keyboard_fd, &event)) { + e_event.data.fd = keyboard_fd; + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, keyboard_fd, &e_event)) { fprintf(stderr, "Failed to add keyboard file descriptor\n"); close(epoll_fd); return 1; } - event.data.fd = mouse_fd; - if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, mouse_fd, &event)) { + e_event.data.fd = mouse_fd; + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, mouse_fd, &e_event)) { fprintf(stderr, "Failed to add mouse file descriptor\n"); close(epoll_fd); return 1; } - fprintf(stderr, "Waiting for events\n"); - clock_gettime(CLOCK_MONOTONIC, &start); + fprintf(stderr, "Waiting for hotkey\n"); + struct input_event i_event; while(running) { int event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); char* type; for(int i = 0; i < event_count; i++) { + int result = read(events[i].data.fd, &i_event, sizeof(struct input_event)); + if(result < 0) { + fprintf(stderr, "Failed to read an event: %d", errno); + return 1; + } + if(events[i].data.fd == keyboard_fd) { type = KEYBOARD; + if(i_event.type == EV_KEY && i_event.code == hotkey_scancode && i_event.value == 1) { + if(has_seen_hotkey) { + fprintf(stderr, "Stop capture\n"); + return 0; + } else { + has_seen_hotkey = 1; + fprintf(stderr, "Start capture\n"); + clock_gettime(CLOCK_MONOTONIC, &start); + continue; + } + } } else if (events[i].data.fd == mouse_fd) { type = MOUSE; } else { fprintf(stderr, "Unknown fd"); return 1; } - if(dump_event(&start, events[i].data.fd, type)) { + // Wait for the hotkey to start capture + if(has_seen_hotkey && dump_event(&start, &i_event, type)) { return 1; } } From 0c3ae88b5a81a7d0192299099818d6e084f7b5f0 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 1 Sep 2021 04:38:12 -0600 Subject: [PATCH 18/30] Invert mouse playback. Without this we go exactly the wrong way in the Y direction. This is likely an issue with my game rather than the playback. --- playback.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback.c b/playback.c index 98b3eaa..030cfec 100644 --- a/playback.c +++ b/playback.c @@ -129,7 +129,7 @@ int handle_mouse(char* details, int udevice_fd) { emit(udevice_fd, EV_REL, REL_X, x); } if(y != 0) { - emit(udevice_fd, EV_REL, REL_Y, y); + emit(udevice_fd, EV_REL, REL_Y, -1 * y); } if(left != current_left) { From d64a981c7687d049fcc9cf7d3034b455e82aa769 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 1 Sep 2021 04:39:05 -0600 Subject: [PATCH 19/30] Wait 3 seconds to start playback. Then I can get the game set up. --- playback.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/playback.c b/playback.c index 030cfec..94689bf 100644 --- a/playback.c +++ b/playback.c @@ -177,6 +177,11 @@ int read_file(char* filename, int udevice_fd) { return 1; } + for(int i = 3; i > 0; i--) { + fprintf(stderr, "playing back in %d seconds\n", i); + sleep(1); + } + while((read = getline(&line, &len, fp)) != -1) { if(handle_line(line, udevice_fd)) { return 1; From 7664f34de5e9bdc16920beb74dfc5ebe96ea441e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 30 Aug 2022 18:35:12 -0600 Subject: [PATCH 20/30] Drop events from the keyboard of type ... Not sure what, can't remember, can't be bothered to look it up. --- capture.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/capture.c b/capture.c index 8253022..1b0b94d 100644 --- a/capture.c +++ b/capture.c @@ -197,6 +197,8 @@ int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode) { clock_gettime(CLOCK_MONOTONIC, &start); continue; } + } else if(i_event.value == 2) { + continue; } } else if (events[i].data.fd == mouse_fd) { type = MOUSE; From a3cb632170c25fb803fd58893aae1c563c5dcb90 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 7 Sep 2022 16:23:30 -0600 Subject: [PATCH 21/30] Get python versions up-to-date. I can't remember now why I did all this, but I did. It's fine. --- capture.py | 57 ++++++++++++++++++++++++++--------------------------- playback.py | 1 + 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/capture.py b/capture.py index cdaf786..05f23fd 100644 --- a/capture.py +++ b/capture.py @@ -41,40 +41,39 @@ def main() -> None: print("You cannot specify 'delay' and 'trigger'") sys.exit(1) - now = time.time() _do_delay(args.delay) _do_trigger(args.trigger) + LOGGER.info("Capturing...") + now = time.time() # Add dummy events to lock in the time start - event_queue = queue.Queue() - event_queue.put( - mouse.ButtonEvent( - event_type=mouse.UP, - button=mouse.RIGHT, - time=now, - )) - event_queue.put( - keyboard.KeyboardEvent( - event_type=keyboard.KEY_UP, - name=" ", - scan_code=57, - time=now, - ) - ) - hook = functools.partial(_on_hook, event_queue) - keyhook = keyboard.hook(hook) - mousehook = mouse.hook(hook) - print("Capturing...") - try: - keyboard.wait(args.trigger) - except KeyboardInterrupt: - keyboard.unhook(keyhook) - mouse.unhook(mousehook) + with open(args.output, "w") as output: + hook = functools.partial(_on_hook, now, output) + keyhook = keyboard.hook(hook) + mousehook = mouse.hook(hook) + try: + keyboard.wait(args.trigger) + except KeyboardInterrupt: + keyboard.unhook(keyhook) + mouse.unhook(mousehook) + LOGGER.info("Wrote %s", args.output) - _save_events(event_queue, args.output) - -def _on_hook(event_queue, event): +def _on_hook(start, output, event): LOGGER.debug(str(event)) - event_queue.put(event, block=False) + relative_time = event.time - start + if isinstance(event, keyboard.KeyboardEvent): + output.write( + f"{relative_time},k,{event.event_type},{event.scan_code},{event.name}\n") + elif isinstance(event, mouse.ButtonEvent): + output.write( + f"{relative_time},mb,{event.event_type},{event.button}\n") + elif isinstance(event, mouse.MoveEvent): + output.write( + f"{relative_time},mm,{event.x},{event.y}\n") + elif isinstance(event, mouse.WheelEvent): + output.write( + f"{relative_time},mw,{event.delta}\n") + else: + raise ValueError(f"{event} is not recognized") def _do_delay(delay: int) -> None: if not delay: diff --git a/playback.py b/playback.py index d7f72e9..26c6867 100644 --- a/playback.py +++ b/playback.py @@ -73,6 +73,7 @@ def _play_event_mouse(event) -> None: mouse.press(event.button) elif isinstance(event, mouse.MoveEvent): mouse.move(event.x, event.y, absolute=True) + # mouse.move(event.x, event.y) elif isinstance(event, mouse.WheelEvent): mouse.wheel(event.delta) From 0d0d14cc39446ccea3c04d6c37758a79baa4736d Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 7 Sep 2022 16:24:11 -0600 Subject: [PATCH 22/30] Add logic to loop and additional error information on sleep failure. --- playback.c | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/playback.c b/playback.c index 94689bf..40ad1af 100644 --- a/playback.c +++ b/playback.c @@ -10,11 +10,14 @@ #include #include +static volatile int is_running = 1; + int handle_udevice(char* details, int udevice_fd); int handle_line(char* line, int udevice_fd); int handle_mouse(char* details, int udevice_fd); int read_file(char* filename, int udevice_fd); int setup_udevice(); +void sigint_handler(int dummy); void teardown_udevice(int fd); void emit(int fd, int type, int code, int val) { @@ -81,7 +84,12 @@ int handle_line(char* line, int udevice_fd) { // printf("Read %ld %ld\n", seconds, nanos); // printf("Sleep %ld %ld\n", to_sleep.tv_sec, to_sleep.tv_nsec); while(nanosleep(&to_sleep, &remaining) == -1) { - printf("Sleep harder\n"); + if(!is_running) { + return 0; + } + perror("nanosleep error"); + printf("Attempted %ld.%ld sleep\n", to_sleep.tv_sec, to_sleep.tv_nsec); + printf("Need %ld.%ld more seconds for total sleep\n", remaining.tv_sec, remaining.tv_nsec); to_sleep.tv_sec = remaining.tv_sec; to_sleep.tv_nsec = remaining.tv_nsec; } @@ -97,8 +105,8 @@ int handle_line(char* line, int udevice_fd) { timer_seconds = seconds; timer_nanos = nanos; - printf("%ld %ld\tslept %ld %ld\n", - total_seconds, total_nanos, to_sleep.tv_sec, to_sleep.tv_nsec); + // printf("%ld %ld\tslept %ld %ld\n", + // total_seconds, total_nanos, to_sleep.tv_sec, to_sleep.tv_nsec); if(type == 'k') { return handle_keyboard(details, udevice_fd); } else if(type == 'm') { @@ -151,15 +159,34 @@ int handle_mouse(char* details, int udevice_fd) { } int main(int argc, char* argv[]) { + int repeat = 1; if(argc < 2) { printf("Please provide a capture file."); exit(EXIT_FAILURE); } + if(argc == 3) { + int matched = sscanf(argv[2], "%d", &repeat); + if(matched != 1) { + fprintf(stderr, "Failed to read repeat value.\n"); + exit(EXIT_FAILURE); + } + printf("Repeating %d times\n", repeat); + } + + signal(SIGINT, sigint_handler); + + for(int i = 3; i > 0; i--) { + fprintf(stderr, "Playing back in %d seconds\n", i); + sleep(1); + } int result = 0; int udevice_fd = setup_udevice(); - if(read_file(argv[1], udevice_fd)) { - result = EXIT_FAILURE; + for(int i = 0; is_running && i < repeat; i++) { + fprintf(stderr, "Repeat %d/%d\n", i+1, repeat); + if(read_file(argv[1], udevice_fd)) { + result = EXIT_FAILURE; + } } teardown_udevice(udevice_fd); return result; @@ -177,16 +204,13 @@ int read_file(char* filename, int udevice_fd) { return 1; } - for(int i = 3; i > 0; i--) { - fprintf(stderr, "playing back in %d seconds\n", i); - sleep(1); - } - - while((read = getline(&line, &len, fp)) != -1) { + while(is_running && (read = getline(&line, &len, fp)) != -1) { if(handle_line(line, udevice_fd)) { return 1; } } + fclose(fp); + return 0; } int setup_udevice() { @@ -205,7 +229,7 @@ int setup_udevice() { // Add mouse buttons ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); - ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); ioctl(fd, UI_SET_EVBIT, EV_REL); ioctl(fd, UI_SET_RELBIT, REL_X); @@ -231,6 +255,10 @@ int setup_udevice() { return fd; } +void sigint_handler(int dummy) { + is_running = 0; +} + void teardown_udevice(int fd) { ioctl(fd, UI_DEV_DESTROY); close(fd); From 8985f990cb911b6c8af2f67ee96835256b74665b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 7 Sep 2022 17:18:47 -0600 Subject: [PATCH 23/30] Switch from storing total seconds to storing deltas. This is conceptually simpler and makes the files easier to manipulate and concatenate. It also avoids a bug where we would send a large negative time when we loop for multiple playbacks. --- capture.c | 2 ++ playback.c | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/capture.c b/capture.c index 1b0b94d..4b4d49e 100644 --- a/capture.c +++ b/capture.c @@ -73,6 +73,8 @@ int dump_event(struct timespec* start, struct input_event* event, char* type) { fprintf(stderr, "Unknown event type.\n"); return 1; } + start->tv_sec = now.tv_sec; + start->tv_nsec = now.tv_nsec; printf("%ld %ld,%s,%s\n", diff.tv_sec, diff --git a/playback.c b/playback.c index 40ad1af..4021053 100644 --- a/playback.c +++ b/playback.c @@ -51,8 +51,6 @@ int handle_keyboard(char* details, int udevice_fd) { } int handle_line(char* line, int udevice_fd) { - static time_t timer_seconds = 0; - static long timer_nanos = 0; static time_t total_seconds = 0; static long total_nanos = 0; @@ -74,8 +72,8 @@ int handle_line(char* line, int udevice_fd) { remaining.tv_sec = 0; remaining.tv_nsec = 0; - to_sleep.tv_sec = seconds - timer_seconds; - to_sleep.tv_nsec = nanos - timer_nanos; + to_sleep.tv_sec = seconds; + to_sleep.tv_nsec = nanos; if(to_sleep.tv_nsec < 0) { --to_sleep.tv_sec; to_sleep.tv_nsec += 1000000000L; @@ -102,8 +100,6 @@ int handle_line(char* line, int udevice_fd) { total_nanos -= 1000000000L; total_seconds += 1; } - timer_seconds = seconds; - timer_nanos = nanos; // printf("%ld %ld\tslept %ld %ld\n", // total_seconds, total_nanos, to_sleep.tv_sec, to_sleep.tv_nsec); From a88c7c654a228b6519e7f19090abaa84421dfd27 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 7 Sep 2022 17:19:47 -0600 Subject: [PATCH 24/30] Use perror instead of our own crapily-implemented version. Yay standards. --- capture.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capture.c b/capture.c index 4b4d49e..692191a 100644 --- a/capture.c +++ b/capture.c @@ -183,7 +183,7 @@ int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode) { for(int i = 0; i < event_count; i++) { int result = read(events[i].data.fd, &i_event, sizeof(struct input_event)); if(result < 0) { - fprintf(stderr, "Failed to read an event: %d", errno); + perror("Failed to read an event"); return 1; } From a461c8eb6aefbcd8783c6c52b7233507ee0c779f Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 7 Sep 2022 17:20:18 -0600 Subject: [PATCH 25/30] Remove uinput-test. I just used it to learn stuff, don't need it now. --- uinput-test.cc | 84 -------------------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 uinput-test.cc diff --git a/uinput-test.cc b/uinput-test.cc deleted file mode 100644 index 750bc5d..0000000 --- a/uinput-test.cc +++ /dev/null @@ -1,84 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -void emit(int fd, int type, int code, int val) { - struct input_event ie; - ie.type = type; - ie.code = code; - ie.value = val; - ie.time.tv_sec = 0; - ie.time.tv_usec = 0; - - write(fd, &ie, sizeof(ie)); -} - - -int main(void) { - struct uinput_setup usetup; - // int i = 50; - int i = 0; - int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); - -/* enable mouse button left and relative events */ - printf("EV_KEY 0x%08x\n", EV_KEY); - printf("UI_SET_EVBIT 0x%08lx\n", UI_SET_EVBIT); - printf("UI_SET_KEYBIT 0x%08lx\n", UI_SET_KEYBIT); - printf("BTN_LEFT 0x%08x\n", BTN_LEFT); - printf("EV_REL 0x%08x\n", EV_REL); - - ioctl(fd, UI_SET_EVBIT, EV_KEY); - - ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); - - ioctl(fd, UI_SET_EVBIT, EV_REL); - ioctl(fd, UI_SET_RELBIT, REL_X); - ioctl(fd, UI_SET_RELBIT, REL_Y); - - memset(&usetup, 0, sizeof(usetup)); - usetup.id.bustype = BUS_USB; - usetup.id.vendor = 0x1234; /* sample vendor */ - usetup.id.product = 0x5678; /* sample product */ - strcpy(usetup.name, "Example device"); - - ioctl(fd, UI_DEV_SETUP, &usetup); - ioctl(fd, UI_DEV_CREATE); - - /* - * On UI_DEV_CREATE the kernel will create the device node for this - * device. We are inserting a pause here so that userspace has time - * to detect, initialize the new device, and can start listening to - * the event, otherwise it will not notice the event we are about - * to send. This pause is only needed in our example code! - */ - sleep(1); - - /* Move the mouse diagonally, 5 units per axis */ - while (i--) { - emit(fd, EV_REL, REL_X, 5); - emit(fd, EV_REL, REL_Y, 5); - emit(fd, EV_SYN, SYN_REPORT, 0); - usleep(15000); - } - - // Click left mouse button. - - emit(fd, EV_KEY, BTN_MOUSE, 1); - emit(fd, EV_SYN, SYN_REPORT, 0); - emit(fd, EV_KEY, BTN_MOUSE, 0); - emit(fd, EV_SYN, SYN_REPORT, 0); - /* - * Give userspace some time to read the events before we destroy the - * device with UI_DEV_DESTOY. - */ - sleep(1); - - ioctl(fd, UI_DEV_DESTROY); - close(fd); - - return 0; -} From 694ef2a4664c67d1598af33181412b22911e309d Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 8 Sep 2022 16:10:07 -0600 Subject: [PATCH 26/30] Switch (badly) to C++ This is so I can use capnproto without adding a 3rd party compiler. --- Makefile | 4 ++-- capture.c | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 3680ec7..3993711 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,10 @@ bin: mkdir -p bin capture: bin capture.c - gcc capture.c -o bin/capture + g++ capture.c -o bin/capture clean: rm -Rf bin playback: bin playback.c - gcc playback.c -o bin/playback + g++ playback.c -o bin/playback diff --git a/capture.c b/capture.c index 692191a..ef04ad7 100644 --- a/capture.c +++ b/capture.c @@ -1,7 +1,9 @@ #include #include +#include #include #include +#include #include #include @@ -11,10 +13,10 @@ #define MAX_EVENTS 16 #define CONTENT_BUFFER_SIZE 32 -static char* KEYBOARD = "k"; -static char* MOUSE = "m"; +constexpr std::string_view KEYBOARD = "k"; +constexpr std::string_view MOUSE = "m"; -int dump_event(struct timespec* start, struct input_event* event, char* type); +int dump_event(struct timespec* start, struct input_event* event, const std::string_view& type); int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event); int event_content_mouse(char* buffer, int buffer_size, struct input_event* event); @@ -48,7 +50,7 @@ static inline void time_diff(struct timeval* a, struct timespec* b, struct timev } } -int dump_event(struct timespec* start, struct input_event* event, char* type) { +int dump_event(struct timespec* start, struct input_event* event, const std::string_view& type) { struct timespec now; struct timespec diff; char content_buffer[CONTENT_BUFFER_SIZE]; @@ -76,11 +78,11 @@ int dump_event(struct timespec* start, struct input_event* event, char* type) { start->tv_sec = now.tv_sec; start->tv_nsec = now.tv_nsec; - printf("%ld %ld,%s,%s\n", - diff.tv_sec, - diff.tv_nsec, - type, - content_buffer); + std::cout << + diff.tv_sec << " " << + diff.tv_nsec << "," << + type << "," << + content_buffer << std::endl; fflush(stdout); return 0; @@ -179,7 +181,7 @@ int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode) { struct input_event i_event; while(running) { int event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); - char* type; + std::string_view type; for(int i = 0; i < event_count; i++) { int result = read(events[i].data.fd, &i_event, sizeof(struct input_event)); if(result < 0) { @@ -214,4 +216,5 @@ int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode) { } } } + return 0; } From 33c73c806087af15b56bd9c6625e8b15c314424c Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 9 Sep 2022 15:31:09 -0600 Subject: [PATCH 27/30] Use a proper file extension for C++ These don't exist on Linux, but they are a fun fiction. --- Makefile | 4 ++-- capture.c => capture.cpp | 0 playback.c => playback.cpp | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename capture.c => capture.cpp (100%) rename playback.c => playback.cpp (100%) diff --git a/Makefile b/Makefile index 3993711..dc994e9 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ all: capture playback bin: mkdir -p bin -capture: bin capture.c +capture: bin capture.cpp g++ capture.c -o bin/capture clean: rm -Rf bin -playback: bin playback.c +playback: bin playback.cpp g++ playback.c -o bin/playback diff --git a/capture.c b/capture.cpp similarity index 100% rename from capture.c rename to capture.cpp diff --git a/playback.c b/playback.cpp similarity index 100% rename from playback.c rename to playback.cpp From 5357c541807499688a53f5d43ed6065508434fa7 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 9 Sep 2022 17:05:43 -0600 Subject: [PATCH 28/30] Add sample client files from capnproto. Still working on the build logic. --- capnproto-example/calculator-client.c++ | 367 ++++++++++++++++++++++++ capnproto-example/calculator.capnp | 118 ++++++++ 2 files changed, 485 insertions(+) create mode 100644 capnproto-example/calculator-client.c++ create mode 100644 capnproto-example/calculator.capnp diff --git a/capnproto-example/calculator-client.c++ b/capnproto-example/calculator-client.c++ new file mode 100644 index 0000000..5d84529 --- /dev/null +++ b/capnproto-example/calculator-client.c++ @@ -0,0 +1,367 @@ +// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "calculator.capnp.h" +#include +#include +#include +#include + +class PowerFunction final: public Calculator::Function::Server { + // An implementation of the Function interface wrapping pow(). Note that + // we're implementing this on the client side and will pass a reference to + // the server. The server will then be able to make calls back to the client. + +public: + kj::Promise call(CallContext context) { + auto params = context.getParams().getParams(); + KJ_REQUIRE(params.size() == 2, "Wrong number of parameters."); + context.getResults().setValue(pow(params[0], params[1])); + return kj::READY_NOW; + } +}; + +int main(int argc, const char* argv[]) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " HOST:PORT\n" + "Connects to the Calculator server at the given address and " + "does some RPCs." << std::endl; + return 1; + } + + capnp::EzRpcClient client(argv[1]); + Calculator::Client calculator = client.getMain(); + + // Keep an eye on `waitScope`. Whenever you see it used is a place where we + // stop and wait for the server to respond. If a line of code does not use + // `waitScope`, then it does not block! + auto& waitScope = client.getWaitScope(); + + { + // Make a request that just evaluates the literal value 123. + // + // What's interesting here is that evaluate() returns a "Value", which is + // another interface and therefore points back to an object living on the + // server. We then have to call read() on that object to read it. + // However, even though we are making two RPC's, this block executes in + // *one* network round trip because of promise pipelining: we do not wait + // for the first call to complete before we send the second call to the + // server. + + std::cout << "Evaluating a literal... "; + std::cout.flush(); + + // Set up the request. + auto request = calculator.evaluateRequest(); + request.getExpression().setLiteral(123); + + // Send it, which returns a promise for the result (without blocking). + auto evalPromise = request.send(); + + // Using the promise, create a pipelined request to call read() on the + // returned object, and then send that. + auto readPromise = evalPromise.getValue().readRequest().send(); + + // Now that we've sent all the requests, wait for the response. Until this + // point, we haven't waited at all! + auto response = readPromise.wait(waitScope); + KJ_ASSERT(response.getValue() == 123); + + std::cout << "PASS" << std::endl; + } + + { + // Make a request to evaluate 123 + 45 - 67. + // + // The Calculator interface requires that we first call getOperator() to + // get the addition and subtraction functions, then call evaluate() to use + // them. But, once again, we can get both functions, call evaluate(), and + // then read() the result -- four RPCs -- in the time of *one* network + // round trip, because of promise pipelining. + + std::cout << "Using add and subtract... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + Calculator::Function::Client subtract = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + { + // Get the "subtract" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::SUBTRACT); + subtract = request.send().getFunc(); + } + + // Build the request to evaluate 123 + 45 - 67. + auto request = calculator.evaluateRequest(); + + auto subtractCall = request.getExpression().initCall(); + subtractCall.setFunction(subtract); + auto subtractParams = subtractCall.initParams(2); + subtractParams[1].setLiteral(67); + + auto addCall = subtractParams[0].initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[0].setLiteral(123); + addParams[1].setLiteral(45); + + // Send the evaluate() request, read() the result, and wait for read() to + // finish. + auto evalPromise = request.send(); + auto readPromise = evalPromise.getValue().readRequest().send(); + + auto response = readPromise.wait(waitScope); + KJ_ASSERT(response.getValue() == 101); + + std::cout << "PASS" << std::endl; + } + + { + // Make a request to evaluate 4 * 6, then use the result in two more + // requests that add 3 and 5. + // + // Since evaluate() returns its result wrapped in a `Value`, we can pass + // that `Value` back to the server in subsequent requests before the first + // `evaluate()` has actually returned. Thus, this example again does only + // one network round trip. + + std::cout << "Pipelining eval() calls... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + Calculator::Function::Client multiply = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + { + // Get the "multiply" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::MULTIPLY); + multiply = request.send().getFunc(); + } + + // Build the request to evaluate 4 * 6 + auto request = calculator.evaluateRequest(); + + auto multiplyCall = request.getExpression().initCall(); + multiplyCall.setFunction(multiply); + auto multiplyParams = multiplyCall.initParams(2); + multiplyParams[0].setLiteral(4); + multiplyParams[1].setLiteral(6); + + auto multiplyResult = request.send().getValue(); + + // Use the result in two calls that add 3 and add 5. + + auto add3Request = calculator.evaluateRequest(); + auto add3Call = add3Request.getExpression().initCall(); + add3Call.setFunction(add); + auto add3Params = add3Call.initParams(2); + add3Params[0].setPreviousResult(multiplyResult); + add3Params[1].setLiteral(3); + auto add3Promise = add3Request.send().getValue().readRequest().send(); + + auto add5Request = calculator.evaluateRequest(); + auto add5Call = add5Request.getExpression().initCall(); + add5Call.setFunction(add); + auto add5Params = add5Call.initParams(2); + add5Params[0].setPreviousResult(multiplyResult); + add5Params[1].setLiteral(5); + auto add5Promise = add5Request.send().getValue().readRequest().send(); + + // Now wait for the results. + KJ_ASSERT(add3Promise.wait(waitScope).getValue() == 27); + KJ_ASSERT(add5Promise.wait(waitScope).getValue() == 29); + + std::cout << "PASS" << std::endl; + } + + { + // Our calculator interface supports defining functions. Here we use it + // to define two functions and then make calls to them as follows: + // + // f(x, y) = x * 100 + y + // g(x) = f(x, x + 1) * 2; + // f(12, 34) + // g(21) + // + // Once again, the whole thing takes only one network round trip. + + std::cout << "Defining functions... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + Calculator::Function::Client multiply = nullptr; + Calculator::Function::Client f = nullptr; + Calculator::Function::Client g = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + { + // Get the "multiply" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::MULTIPLY); + multiply = request.send().getFunc(); + } + + { + // Define f. + auto request = calculator.defFunctionRequest(); + request.setParamCount(2); + + { + // Build the function body. + auto addCall = request.getBody().initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[1].setParameter(1); // y + + auto multiplyCall = addParams[0].initCall(); + multiplyCall.setFunction(multiply); + auto multiplyParams = multiplyCall.initParams(2); + multiplyParams[0].setParameter(0); // x + multiplyParams[1].setLiteral(100); + } + + f = request.send().getFunc(); + } + + { + // Define g. + auto request = calculator.defFunctionRequest(); + request.setParamCount(1); + + { + // Build the function body. + auto multiplyCall = request.getBody().initCall(); + multiplyCall.setFunction(multiply); + auto multiplyParams = multiplyCall.initParams(2); + multiplyParams[1].setLiteral(2); + + auto fCall = multiplyParams[0].initCall(); + fCall.setFunction(f); + auto fParams = fCall.initParams(2); + fParams[0].setParameter(0); + + auto addCall = fParams[1].initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[0].setParameter(0); + addParams[1].setLiteral(1); + } + + g = request.send().getFunc(); + } + + // OK, we've defined all our functions. Now create our eval requests. + + // f(12, 34) + auto fEvalRequest = calculator.evaluateRequest(); + auto fCall = fEvalRequest.initExpression().initCall(); + fCall.setFunction(f); + auto fParams = fCall.initParams(2); + fParams[0].setLiteral(12); + fParams[1].setLiteral(34); + auto fEvalPromise = fEvalRequest.send().getValue().readRequest().send(); + + // g(21) + auto gEvalRequest = calculator.evaluateRequest(); + auto gCall = gEvalRequest.initExpression().initCall(); + gCall.setFunction(g); + gCall.initParams(1)[0].setLiteral(21); + auto gEvalPromise = gEvalRequest.send().getValue().readRequest().send(); + + // Wait for the results. + KJ_ASSERT(fEvalPromise.wait(waitScope).getValue() == 1234); + KJ_ASSERT(gEvalPromise.wait(waitScope).getValue() == 4244); + + std::cout << "PASS" << std::endl; + } + + { + // Make a request that will call back to a function defined locally. + // + // Specifically, we will compute 2^(4 + 5). However, exponent is not + // defined by the Calculator server. So, we'll implement the Function + // interface locally and pass it to the server for it to use when + // evaluating the expression. + // + // This example requires two network round trips to complete, because the + // server calls back to the client once before finishing. In this + // particular case, this could potentially be optimized by using a tail + // call on the server side -- see CallContext::tailCall(). However, to + // keep the example simpler, we haven't implemented this optimization in + // the sample server. + + std::cout << "Using a callback... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + // Build the eval request for 2^(4+5). + auto request = calculator.evaluateRequest(); + + auto powCall = request.getExpression().initCall(); + powCall.setFunction(kj::heap()); + auto powParams = powCall.initParams(2); + powParams[0].setLiteral(2); + + auto addCall = powParams[1].initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[0].setLiteral(4); + addParams[1].setLiteral(5); + + // Send the request and wait. + auto response = request.send().getValue().readRequest() + .send().wait(waitScope); + KJ_ASSERT(response.getValue() == 512); + + std::cout << "PASS" << std::endl; + } + + return 0; +} diff --git a/capnproto-example/calculator.capnp b/capnproto-example/calculator.capnp new file mode 100644 index 0000000..adc8294 --- /dev/null +++ b/capnproto-example/calculator.capnp @@ -0,0 +1,118 @@ +# Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +# Licensed under the MIT License: +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +@0x85150b117366d14b; + +interface Calculator { + # A "simple" mathematical calculator, callable via RPC. + # + # But, to show off Cap'n Proto, we add some twists: + # + # - You can use the result from one call as the input to the next + # without a network round trip. To accomplish this, evaluate() + # returns a `Value` object wrapping the actual numeric value. + # This object may be used in a subsequent expression. With + # promise pipelining, the Value can actually be used before + # the evaluate() call that creates it returns! + # + # - You can define new functions, and then call them. This again + # shows off pipelining, but it also gives the client the + # opportunity to define a function on the client side and have + # the server call back to it. + # + # - The basic arithmetic operators are exposed as Functions, and + # you have to call getOperator() to obtain them from the server. + # This again demonstrates pipelining -- using getOperator() to + # get each operator and then using them in evaluate() still + # only takes one network round trip. + + evaluate @0 (expression :Expression) -> (value :Value); + # Evaluate the given expression and return the result. The + # result is returned wrapped in a Value interface so that you + # may pass it back to the server in a pipelined request. To + # actually get the numeric value, you must call read() on the + # Value -- but again, this can be pipelined so that it incurs + # no additional latency. + + struct Expression { + # A numeric expression. + + union { + literal @0 :Float64; + # A literal numeric value. + + previousResult @1 :Value; + # A value that was (or, will be) returned by a previous + # evaluate(). + + parameter @2 :UInt32; + # A parameter to the function (only valid in function bodies; + # see defFunction). + + call :group { + # Call a function on a list of parameters. + function @3 :Function; + params @4 :List(Expression); + } + } + } + + interface Value { + # Wraps a numeric value in an RPC object. This allows the value + # to be used in subsequent evaluate() requests without the client + # waiting for the evaluate() that returns the Value to finish. + + read @0 () -> (value :Float64); + # Read back the raw numeric value. + } + + defFunction @1 (paramCount :Int32, body :Expression) + -> (func :Function); + # Define a function that takes `paramCount` parameters and returns the + # evaluation of `body` after substituting these parameters. + + interface Function { + # An algebraic function. Can be called directly, or can be used inside + # an Expression. + # + # A client can create a Function that runs on the server side using + # `defFunction()` or `getOperator()`. Alternatively, a client can + # implement a Function on the client side and the server will call back + # to it. However, a function defined on the client side will require a + # network round trip whenever the server needs to call it, whereas + # functions defined on the server and then passed back to it are called + # locally. + + call @0 (params :List(Float64)) -> (value :Float64); + # Call the function on the given parameters. + } + + getOperator @2 (op :Operator) -> (func :Function); + # Get a Function representing an arithmetic operator, which can then be + # used in Expressions. + + enum Operator { + add @0; + subtract @1; + multiply @2; + divide @3; + } +} From 9869fd671824fafc2ad5c3535e2d75984b2f6240 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 9 Sep 2022 18:01:15 -0600 Subject: [PATCH 29/30] Working Makefile for client program. --- capnproto-example/Makefile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 capnproto-example/Makefile diff --git a/capnproto-example/Makefile b/capnproto-example/Makefile new file mode 100644 index 0000000..f520545 --- /dev/null +++ b/capnproto-example/Makefile @@ -0,0 +1,20 @@ +all: client + +CAPNPROTO=/home/eliribble/src/capnproto +CXX=g++ +LIBS=\ + kj \ + kj-async \ + capnp \ + capnp-rpc + +calculator.capnp.h: calculator.capnp + capnp compile -oc++ calculator.capnp + + +CLIENT_SRCS=calculator-client.c++ calculator.capnp.c++ +client: $(CLIENT_SRCS) calculator.capnp.h + $(CXX) $(CLIENT_SRCS) -L $(CAPNPROTO)/c++/.libs $(addprefix -l,$(LIBS)) -o client + +clean: + rm client From 0d4574f205e7b64d78e2b3341caf471b438c035b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 9 Sep 2022 18:04:03 -0600 Subject: [PATCH 30/30] Add working server example. --- capnproto-example/calculator-server.c++ | 215 ++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 capnproto-example/calculator-server.c++ diff --git a/capnproto-example/calculator-server.c++ b/capnproto-example/calculator-server.c++ new file mode 100644 index 0000000..c2593be --- /dev/null +++ b/capnproto-example/calculator-server.c++ @@ -0,0 +1,215 @@ +// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "calculator.capnp.h" +#include +#include +#include +#include + +typedef unsigned int uint; + +kj::Promise readValue(Calculator::Value::Client value) { + // Helper function to asynchronously call read() on a Calculator::Value and + // return a promise for the result. (In the future, the generated code might + // include something like this automatically.) + + return value.readRequest().send() + .then([](capnp::Response result) { + return result.getValue(); + }); +} + +kj::Promise evaluateImpl( + Calculator::Expression::Reader expression, + capnp::List::Reader params = capnp::List::Reader()) { + // Implementation of CalculatorImpl::evaluate(), also shared by + // FunctionImpl::call(). In the latter case, `params` are the parameter + // values passed to the function; in the former case, `params` is just an + // empty list. + + switch (expression.which()) { + case Calculator::Expression::LITERAL: + return expression.getLiteral(); + + case Calculator::Expression::PREVIOUS_RESULT: + return readValue(expression.getPreviousResult()); + + case Calculator::Expression::PARAMETER: { + KJ_REQUIRE(expression.getParameter() < params.size(), + "Parameter index out-of-range."); + return params[expression.getParameter()]; + } + + case Calculator::Expression::CALL: { + auto call = expression.getCall(); + auto func = call.getFunction(); + + // Evaluate each parameter. + kj::Array> paramPromises = + KJ_MAP(param, call.getParams()) { + return evaluateImpl(param, params); + }; + + // Join the array of promises into a promise for an array. + kj::Promise> joinedParams = + kj::joinPromises(kj::mv(paramPromises)); + + // When the parameters are complete, call the function. + return joinedParams.then([KJ_CPCAP(func)](kj::Array&& paramValues) mutable { + auto request = func.callRequest(); + request.setParams(paramValues); + return request.send().then( + [](capnp::Response&& result) { + return result.getValue(); + }); + }); + } + + default: + // Throw an exception. + KJ_FAIL_REQUIRE("Unknown expression type."); + } +} + +class ValueImpl final: public Calculator::Value::Server { + // Simple implementation of the Calculator.Value Cap'n Proto interface. + +public: + ValueImpl(double value): value(value) {} + + kj::Promise read(ReadContext context) { + context.getResults().setValue(value); + return kj::READY_NOW; + } + +private: + double value; +}; + +class FunctionImpl final: public Calculator::Function::Server { + // Implementation of the Calculator.Function Cap'n Proto interface, where the + // function is defined by a Calculator.Expression. + +public: + FunctionImpl(uint paramCount, Calculator::Expression::Reader body) + : paramCount(paramCount) { + this->body.setRoot(body); + } + + kj::Promise call(CallContext context) { + auto params = context.getParams().getParams(); + KJ_REQUIRE(params.size() == paramCount, "Wrong number of parameters."); + + return evaluateImpl(body.getRoot(), params) + .then([KJ_CPCAP(context)](double value) mutable { + context.getResults().setValue(value); + }); + } + +private: + uint paramCount; + // The function's arity. + + capnp::MallocMessageBuilder body; + // Stores a permanent copy of the function body. +}; + +class OperatorImpl final: public Calculator::Function::Server { + // Implementation of the Calculator.Function Cap'n Proto interface, wrapping + // basic binary arithmetic operators. + +public: + OperatorImpl(Calculator::Operator op): op(op) {} + + kj::Promise call(CallContext context) { + auto params = context.getParams().getParams(); + KJ_REQUIRE(params.size() == 2, "Wrong number of parameters."); + + double result; + switch (op) { + case Calculator::Operator::ADD: result = params[0] + params[1]; break; + case Calculator::Operator::SUBTRACT:result = params[0] - params[1]; break; + case Calculator::Operator::MULTIPLY:result = params[0] * params[1]; break; + case Calculator::Operator::DIVIDE: result = params[0] / params[1]; break; + default: + KJ_FAIL_REQUIRE("Unknown operator."); + } + + context.getResults().setValue(result); + return kj::READY_NOW; + } + +private: + Calculator::Operator op; +}; + +class CalculatorImpl final: public Calculator::Server { + // Implementation of the Calculator Cap'n Proto interface. + +public: + kj::Promise evaluate(EvaluateContext context) override { + return evaluateImpl(context.getParams().getExpression()) + .then([KJ_CPCAP(context)](double value) mutable { + context.getResults().setValue(kj::heap(value)); + }); + } + + kj::Promise defFunction(DefFunctionContext context) override { + auto params = context.getParams(); + context.getResults().setFunc(kj::heap( + params.getParamCount(), params.getBody())); + return kj::READY_NOW; + } + + kj::Promise getOperator(GetOperatorContext context) override { + context.getResults().setFunc(kj::heap( + context.getParams().getOp())); + return kj::READY_NOW; + } +}; + +int main(int argc, const char* argv[]) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " ADDRESS[:PORT]\n" + "Runs the server bound to the given address/port.\n" + "ADDRESS may be '*' to bind to all local addresses.\n" + ":PORT may be omitted to choose a port automatically." << std::endl; + return 1; + } + + // Set up a server. + capnp::EzRpcServer server(kj::heap(), argv[1]); + + // Write the port number to stdout, in case it was chosen automatically. + auto& waitScope = server.getWaitScope(); + uint port = server.getPort().wait(waitScope); + if (port == 0) { + // The address format "unix:/path/to/socket" opens a unix domain socket, + // in which case the port will be zero. + std::cout << "Listening on Unix socket..." << std::endl; + } else { + std::cout << "Listening on port " << port << "..." << std::endl; + } + + // Run forever, accepting connections and handling requests. + kj::NEVER_DONE.wait(waitScope); +}