1// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5/*
6doc:
7https://www.x.org/docs/Xext/recordlib.pdf
8
9build:
10gcc -g -O0 -shared -fPIC -D_GNU_SOURCE -o x11preload.so x11_preload.c -W -Wall -L/usr/X11R6/lib -lX11 -lXtst -ldl -lpthread
11 * */
12
13#include <X11/Xlibint.h>
14#include <X11/extensions/XInput.h>
15#include <X11/extensions/XInput2.h>
16#include "x11preload.h"
17#ifdef SIMPLE_X11_HOOK
18#include <xcb/xcb.h>
19#else
20#include <X11/keysym.h>
21#include <X11/extensions/record.h>
22#include <X11/extensions/XTest.h>
23#endif
24
25#include <sys/time.h>
26#include <signal.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <unistd.h>
30#include <dlfcn.h>
31#include <pthread.h>
32
33#include "shared_mem_dump.h"
34#include "event_man.h"
35
36static bool g_debug = false;
37static char g_x11_event_flag[MapNotify] = {0}; /*form Error to MapNotify*/
38
39inline long my_dump(int type, const char* msg, int len)
40{
41 MemoryDumper* dump = get_memory_dumper();
42 if (dump) {
43 record_event_simple(dump, DUMP_REASON_x11 + type, msg, len);
44 return 0;
45 }
46 else {
47 return syscall(SYS_dump_x11, type, msg, len, 0, 1);
48 }
49}
50
51#ifdef SIMPLE_X11_HOOK
52
53// https://github.com/freedesktop-unofficial-mirror/xorg__app__xinput
54// https://www.clearchain.com/blog/posts/xinput-1-xinput-2-conversion-guide
55// XQueryExtension(display, "XInputExtension", &extension, &event, &error)) {
56//
57
58static XGenericEvent g_prev_ge;
59
60Bool (* Real_XGetEventData)(Display *display, XGenericEventCookie *cookie) = nullptr;
61
62Bool XGetEventData(Display *display, XGenericEventCookie *cookie)
63{
64 char info[EVENT_EXTRA_INFO_SIZE];
65 int len = 0;
66 BOOL ret = 0;
67 if (NULL == Real_XGetEventData){
68 *(void**)&Real_XGetEventData = dlsym(RTLD_NEXT, "XGetEventData");
69 }
70 if (NULL != Real_XGetEventData) {
71 ret = Real_XGetEventData(display, cookie);
72 }
73 if (!ret) {
74 return 0;
75 }
76
77 if (cookie->serial != g_prev_ge.serial ||
78 cookie->evtype != g_prev_ge.evtype ||
79 cookie->extension != g_prev_ge.extension) {
80 return ret;
81 }
82
83 XIDeviceEvent* dev = (XIDeviceEvent*)cookie->data;
84 switch (cookie->evtype) {
85 case KeyPress:
86 case KeyRelease:
87 {
88 len = snprintf(info, sizeof(info),
89 "win=%lx,key=%d", dev->event, dev->detail);
90 }
91 break;
92 case ButtonPress:
93 case ButtonRelease:
94 {
95 len = snprintf(info, sizeof(info), "win=%lx,x=%.2f,y=%.2f",
96 dev->event, dev->event_x, dev->event_y);
97 }
98 break;
99 default:
100 break;
101 }
102 if (len > 0 && g_x11_event_flag[cookie->evtype]) {
103 long res = my_dump(cookie->evtype, info, len + 1);
104 if (g_debug) {
105 printf("X11 XNextEvent GenericEvent %d,%s->%ld\n",
106 cookie->evtype, info, res);
107 }
108 len = 0; //reset for avoiding dump twice
109 }
110
111 return ret;
112}
113
114int (*Real_XNextEvent)(Display *display, XEvent *event_return) = NULL;
115int XNextEvent(Display *display, XEvent *event_return)
116{
117 char info[EVENT_EXTRA_INFO_SIZE];
118 int len = 0;
119 int ret = 0;
120 if (NULL == Real_XNextEvent){
121 *(void**)&Real_XNextEvent = dlsym(RTLD_NEXT, "XNextEvent");
122 }
123 if (NULL != Real_XNextEvent) {
124 event_return->type = 0;
125 ret = Real_XNextEvent(display, event_return);
126 }
127
128 switch (event_return->type) {
129 case GenericEvent:
130 {
131 XGenericEvent* ge = (XGenericEvent*)event_return;
132 // store till the user call XGetEventData(ge->display, xcookie);
133 // so we can get detail info in XEvent.xcookie.data
134 memcpy(&g_prev_ge, ge, sizeof(*ge));
135 }
136 break;
137 case KeyPress:
138 case KeyRelease:
139 {
140 XKeyEvent* key = (XKeyEvent*)event_return;
141 len = snprintf(info, sizeof(info),
142 "win=%lx,key=%d", key->window, key->keycode);
143 }
144 break;
145 case ButtonPress:
146 case ButtonRelease:
147 {
148 XButtonEvent* btn = (XButtonEvent*)event_return;
149 len = snprintf(info, sizeof(info),
150 "win=%lx,x=%d,y=%d", btn->window, btn->x, btn->y);
151 }
152 break;
153#if 0
154 case MotionNotify:
155 {
156 XMotionEvent* motion = (XMotionEvent*)event_return;
157 x = motion->x;
158 y = motion->y;
159 w = motion->window;
160 }
161 break;
162#endif
163 case FocusIn:
164 case FocusOut:
165 {
166 XFocusChangeEvent* focus = (XFocusChangeEvent*)event_return;
167 len = snprintf(info, sizeof(info), "win=%lx", focus->window);
168 }
169 break;
170 case CreateNotify:
171 {
172 XCreateWindowEvent* cr = (XCreateWindowEvent*)event_return;
173 len = snprintf(info, sizeof(info),
174 "win=%lx,x=%d,y=%d,w=%d,h=%d",
175 cr->window, cr->x, cr->y, cr->width, cr->height);
176 }
177 break;
178 case DestroyNotify:
179 {
180 XDestroyWindowEvent* des = (XDestroyWindowEvent*)event_return;
181 len = snprintf(info, sizeof(info), "win=%lx", des->window);
182 }
183 break;
184 case UnmapNotify:
185 {
186 XUnmapEvent* unmap = (XUnmapEvent*)event_return;
187 len = snprintf(info, sizeof(info), "win=%lx", unmap->window);
188 }
189 break;
190 case MapNotify:
191 {
192 XMapEvent* map = (XMapEvent*)event_return;
193 len = snprintf(info, sizeof(info), "win=%lx", map->window);
194 }
195 break;
196 default:
197 break;
198 }
199
200 if (len > 0 && g_x11_event_flag[event_return->type]) {
201 long res = my_dump(event_return->type, info, len + 1);
202 if (g_debug) {
203 printf("X11 XNextEvent %d,%s->%ld\n",
204 event_return->type, info, res);
205 }
206 }
207
208 return ret;
209}
210
211// copy from <xcb/xinput.h>, at libxcb-xinput-dev 1.13
212
213typedef int32_t xcb_input_fp1616_t;
214typedef uint16_t xcb_input_device_id_t;
215typedef struct xcb_input_modifier_info_t {
216 uint32_t base;
217 uint32_t latched;
218 uint32_t locked;
219 uint32_t effective;
220}xcb_input_modifier_info_t;
221
222typedef struct xcb_input_group_info_t {
223 uint8_t base;
224 uint8_t latched;
225 uint8_t locked;
226 uint8_t effective;
227}xcb_input_group_info_t;
228
229typedef struct xcb_input_button_press_event_t {
230 uint8_t response_type;
231 uint8_t extension;
232 uint16_t sequence;
233 uint32_t length;
234 uint16_t event_type;
235 xcb_input_device_id_t deviceid;
236 xcb_timestamp_t time;
237 uint32_t detail;
238 xcb_window_t root;
239 xcb_window_t event;
240 xcb_window_t child;
241 uint32_t full_sequence;
242 xcb_input_fp1616_t root_x;
243 xcb_input_fp1616_t root_y;
244 xcb_input_fp1616_t event_x;
245 xcb_input_fp1616_t event_y;
246 uint16_t buttons_len;
247 uint16_t valuators_len;
248 xcb_input_device_id_t sourceid;
249 uint8_t pad0[2];
250 uint32_t flags;
251 xcb_input_modifier_info_t mods;
252 xcb_input_group_info_t group;
253}xcb_input_button_press_event_t;
254
255typedef struct xcb_input_key_press_event_t {
256 uint8_t response_type;
257 uint8_t extension;
258 uint16_t sequence;
259 uint32_t length;
260 uint16_t event_type;
261 xcb_input_device_id_t deviceid;
262 xcb_timestamp_t time;
263 uint32_t detail;
264 xcb_window_t root;
265 xcb_window_t event;
266 xcb_window_t child;
267 uint32_t full_sequence;
268 xcb_input_fp1616_t root_x;
269 xcb_input_fp1616_t root_y;
270 xcb_input_fp1616_t event_x;
271 xcb_input_fp1616_t event_y;
272 uint16_t buttons_len;
273 uint16_t valuators_len;
274 xcb_input_device_id_t sourceid;
275 uint8_t pad0[2];
276 uint32_t flags;
277 xcb_input_modifier_info_t mods;
278 xcb_input_group_info_t group;
279}xcb_input_key_press_event_t;
280
281static inline int fixed1616ToInt(xcb_input_fp1616_t val)
282{
283 return int(double(val) / 0x10000);
284}
285
286xcb_generic_event_t* (*Real_xcb_wait_for_event)(xcb_connection_t *c) = nullptr;
287xcb_generic_event_t* xcb_wait_for_event(xcb_connection_t *c)
288{
289 char info[EVENT_EXTRA_INFO_SIZE];
290 int len = 0;
291 xcb_generic_event_t* ret = nullptr;
292 if (NULL == Real_xcb_wait_for_event){
293 *(void**)&Real_xcb_wait_for_event = dlsym(RTLD_NEXT, "xcb_wait_for_event");
294 }
295 if (NULL != Real_xcb_wait_for_event) {
296 ret = Real_xcb_wait_for_event(c);
297 }
298 if (nullptr == ret)
299 {
300 return nullptr;
301 }
302
303 switch (ret->response_type & ~0x80) {
304 case XCB_GE_GENERIC:
305 {
306 // xinput extension, refer to QXcbWindow::handleXIMouseEvent at :
307 // https://code.woboq.org/qt5/qtbase/src/plugins/platforms/xcb/qxcbwindow.cpp.html#2099
308 xcb_ge_generic_event_t* ge = (xcb_ge_generic_event_t*)ret;
309 switch (ge->event_type) {
310 case KeyPress:
311 case KeyRelease:
312 if (g_x11_event_flag[ge->event_type]) {
313 xcb_input_key_press_event_t* key = (xcb_input_key_press_event_t*)ge;
314 len = snprintf(info, sizeof(info),
315 "win=%x,key=%d", key->event, key->detail);
316 }
317 break;
318 case ButtonPress:
319 case ButtonRelease:
320 if (g_x11_event_flag[ge->event_type]) {
321 xcb_input_button_press_event_t* btn = (xcb_input_button_press_event_t*)ge;
322 int x = fixed1616ToInt(btn->event_x);
323 int y = fixed1616ToInt(btn->event_y);
324 len = snprintf(info, sizeof(info),
325 "win=%x,x=%d,y=%d", btn->event, x, y);
326 }
327 break;
328 default:
329 break;
330 }
331 if (len > 0) {
332 long res = my_dump(ge->event_type, info, len + 1);
333 if (g_debug) {
334 printf("X11 xcb_wait_for_event GenericEvent:%d,ext=%d,%s->%ld\n",
335 ge->event_type, ge->extension, info, res);
336 }
337 len = 0; //reset for avoiding dump twice
338 }
339 }
340 break;
341 case XCB_KEY_PRESS:
342 case XCB_KEY_RELEASE:
343 {
344 xcb_key_press_event_t* key = (xcb_key_press_event_t*)ret;
345 len = snprintf(info, sizeof(info),
346 "win=%x,key=%d", key->event, key->detail);
347 }
348 break;
349 case XCB_BUTTON_PRESS:
350 case XCB_BUTTON_RELEASE:
351 {
352 xcb_button_press_event_t* btn = (xcb_button_press_event_t*)ret;
353 int x = fixed1616ToInt(btn->event_x);
354 int y = fixed1616ToInt(btn->event_y);
355 len = snprintf(info, sizeof(info),
356 "win=%x,x=%d,y=%d", btn->event, x, y);
357 }
358 break;
359 case XCB_FOCUS_IN:
360 case XCB_FOCUS_OUT:
361 {
362 xcb_focus_in_event_t* focus = (xcb_focus_in_event_t*)ret;
363 len = snprintf(info, sizeof(info), "win=%x", focus->event);
364 }
365 break;
366 case XCB_CREATE_NOTIFY:
367 {
368 xcb_create_notify_event_t* cr = (xcb_create_notify_event_t*)ret;
369 len = snprintf(info, sizeof(info),
370 "win=%x,x=%d,y=%d,w=%d,h=%d",
371 cr->window, cr->x, cr->y, cr->width, cr->height);
372 }
373 break;
374 case XCB_DESTROY_NOTIFY:
375 {
376 xcb_destroy_notify_event_t* des = (xcb_destroy_notify_event_t*)ret;
377 len = snprintf(info, sizeof(info), "win=%x", des->event);
378 }
379 break;
380 case XCB_UNMAP_NOTIFY:
381 {
382 xcb_unmap_notify_event_t* map = (xcb_unmap_notify_event_t*)ret;
383 len = snprintf(info, sizeof(info), "win=%x", map->event);
384 }
385 break;
386 case XCB_MAP_NOTIFY:
387 {
388 xcb_map_notify_event_t* map = (xcb_map_notify_event_t*)ret;
389 len = snprintf(info, sizeof(info), "win=%x", map->event);
390 }
391 break;
392 default:
393 break;
394 }
395 if (len > 0 && g_x11_event_flag[ret->response_type]) {
396 long res = my_dump(ret->response_type, info, len + 1);
397 if (g_debug) {
398 printf("X11 xcb_wait_for_event %d,%s->%ld\n",
399 ret->response_type, info, res);
400 }
401 }
402
403 return ret;
404}
405
406#else
407
408typedef struct tagCallbackClosure {
409 Display *ctrlDisplay;
410 Display *dataDisplay;
411 int curX;
412 int curY;
413}CallbackClosure;
414
415typedef union tagXRecordDatum{
416 unsigned char type;
417 xEvent event;
418 xResourceReq req;
419 xGenericReply reply;
420 xError error;
421 xConnSetupPrefix setup;
422} XRecordDatum;
423
424static bool g_inited = 0;
425static CallbackClosure g_userData;
426static XRecordContext g_recContext;
427static XRecordClientSpec g_recClientSpec;
428
429static int setupRecordExtension(void) {
430 int ver, ver2;
431 XRecordRange* g_recRange[2];
432 XSynchronize(g_userData.ctrlDisplay, True);
433
434 // Record extension exists?
435 if (!XRecordQueryVersion(g_userData.ctrlDisplay, &ver, &ver2)) {
436 return -1;
437 }
438
439 g_recRange[0] = XRecordAllocRange ();
440 g_recRange[1] = XRecordAllocRange ();
441 if (!g_recRange[0] || !g_recRange[1]) {
442 return -2;
443 }
444 g_recRange[0]->device_events.first = KeyPress;
445 g_recRange[0]->device_events.last = ButtonRelease;
446 g_recRange[0]->delivered_events.first = FocusIn;
447 g_recRange[0]->delivered_events.last = FocusOut;
448
449 g_recRange[1]->delivered_events.first = CreateNotify;
450 g_recRange[1]->delivered_events.last = MapNotify;
451
452 // one of the following constants:
453 // XRecordCurrentClients, XRecordFutureClients, or XRecordAllClients.
454 g_recClientSpec = XRecordAllClients;
455
456 // Get context with our configuration
457 g_recContext = XRecordCreateContext(g_userData.ctrlDisplay,
458 0, &g_recClientSpec, 1, g_recRange, 2);
459 if (!g_recContext) {
460 return -3;
461 }
462
463 // TODO: how to free g_recRange ?
464 return 0;
465}
466
467// Called from Xserver when new event occurs.
468static void eventCallback(XPointer priv, XRecordInterceptData *hook)
469{
470 (void) priv;
471 if (hook->category != XRecordFromServer) {
472 XRecordFreeData(hook);
473 return;
474 }
475
476 xEvent * event = (xEvent *)hook->data;
477
478 // TODO: interrupt the tracee to dump its context!
479 if (g_debug) {
480 printf("x11 :%d, %d, %d\n",
481 event->u.u.type,
482 event->u.keyButtonPointer.rootX,
483 event->u.keyButtonPointer.rootY);
484 }
485
486 XRecordFreeData(hook);
487}
488
489static int connect_display(const char* displayName)
490{
491 /* The recommended communication model for a Record application is to open two
492 * connections to the server—one connection for recording control and one connection
493 * for reading recorded protocol data.
494 */
495 if (NULL == (g_userData.ctrlDisplay = XOpenDisplay(displayName)) ) {
496 return -4;
497 }
498 if (NULL == (g_userData.dataDisplay = XOpenDisplay(displayName)) ) {
499 XCloseDisplay(g_userData.ctrlDisplay);
500 g_userData.ctrlDisplay = NULL;
501 return -5;
502 }
503
504 //TODO: You may want to set custom X error handler here
505
506 return setupRecordExtension();
507}
508
509// NOTE: must call this function in a work thread
510int start_record_x11(const char* displayName, const char* filter)
511{
512 if (filter && filter[0] > 0) {
513 const char* walk = filter;
514 char* stop = nullptr;
515 while (*walk > 0) {
516 int v = strtol(walk, &stop, 10);
517 if (v >= 0 && v <= MapNotify) {
518 g_x11_event_flag[v] = 1;
519 }
520 if (0 == *stop) break;
521 walk = stop + 1;
522 }
523 }
524 else {
525 memset(g_x11_event_flag, 0x1, sizeof(g_x11_event_flag));
526 }
527
528 int ret = connect_display(displayName/*":0"*/);
529 if (ret < 0) return ret;
530
531 // block the caller and run into poll mode if successful
532 if (!XRecordEnableContext(g_userData.dataDisplay,
533 g_recContext, eventCallback, (XPointer)&g_userData)) {
534 return -1;
535 }
536 g_inited = true;
537
538 return 0;
539}
540
541void stop_record_x11(void)
542{
543 if (!g_inited) return;
544
545 g_inited = false;
546 XRecordDisableContext (g_userData.ctrlDisplay, g_recContext);
547}
548
549#endif /*end #ifdef SIMPLE_X11_HOOK*/
550
551static void __attribute__((constructor)) init_process(void) {
552 const char* filter = getenv("ST2_X11_FILTER");
553 if (filter && filter[0] > 0) {
554 const char* walk = filter;
555 char* stop = nullptr;
556 while (*walk > 0) {
557 int v = strtol(walk, &stop, 10);
558 if (v >= 0 && v <= MapNotify) {
559 g_x11_event_flag[v] = 1;
560 }
561 if (0 == *stop) break;
562 walk = stop + 1;
563 }
564 }
565 else {
566 memset(g_x11_event_flag, 0x1, sizeof(g_x11_event_flag));
567 }
568
569 if (getenv("ST2_DEBUG_X11") != nullptr) {
570 g_debug = true;
571 printf("x11 preload: ST2_X11_FILTER=%s\n", filter);
572 }
573}
574
575static void __attribute__((destructor)) uninit_process(void) {
576}
577