/* Copyright (C) 2021 Kasm. All Rights Reserved. * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "xxhash.h" #define min(a, b) ((a) < (b) ? (a) : (b)) static void help(const char name[]) { printf("Usage: %s [opts]\n\n" "-a --app-display disp App display, default :0\n" "-v --vnc-display disp VNC display, default :1\n" "\n" "-f --fps fps FPS, default 30\n" "-r --resize Enable resize, default disabled.\n" " Do not enable this if there's a physical screen\n" " connected to the app display.\n", name); exit(1); } #define CUT_MAX (16 * 1024) static uint8_t cutbuf[CUT_MAX]; static void supplyselection(Display *disp, const XEvent * const ev, const Atom xa_targets) { XSelectionEvent sev; sev.type = SelectionNotify; sev.display = disp; sev.requestor = ev->xselectionrequest.requestor; sev.selection = ev->xselectionrequest.selection; sev.target = ev->xselectionrequest.target; sev.time = ev->xselectionrequest.time; /*printf("someone wants our clipboard, sel %lu, tgt %lu, prop %lu\n", sev.selection, sev.target, ev.xselectionrequest.property);*/ if (ev->xselectionrequest.property == None) sev.property = sev.target; else sev.property = ev->xselectionrequest.property; const uint32_t len = strlen((char *) cutbuf); if (xa_targets != None && sev.target == xa_targets) { // Which formats can we do Atom tgt[2] = { xa_targets, XA_STRING }; XChangeProperty(disp, sev.requestor, ev->xselectionrequest.property, XA_ATOM, 32, PropModeReplace, (unsigned char *) tgt, 2); //puts("sent targets"); } else { // Data XChangeProperty(disp, sev.requestor, ev->xselectionrequest.property, sev.target, 8, PropModeReplace, cutbuf, len); //printf("sent data, of len %u\n", len); } // Send the notify event XSendEvent(disp, sev.requestor, False, 0, (XEvent *) &sev); } int main(int argc, char **argv) { const char *appstr = ":0"; const char *vncstr = ":1"; uint8_t resize = 0; uint8_t fps = 30; const struct option longargs[] = { {"app-display", 1, NULL, 'a'}, {"vnc-display", 1, NULL, 'v'}, {"resize", 0, NULL, 'r'}, {"fps", 1, NULL, 'f'}, {NULL, 0, NULL, 0}, }; while (1) { int c = getopt_long(argc, argv, "a:v:rf:", longargs, NULL); if (c == -1) break; switch (c) { case 'a': appstr = strdup(optarg); break; case 'v': vncstr = strdup(optarg); break; case 'r': resize = 1; break; case 'f': fps = atoi(optarg); if (fps < 1 || fps > 120) { printf("Invalid fps\n"); return 1; } break; default: help(argv[0]); break; } } Display *appdisp = XOpenDisplay(appstr); if (!appdisp) { printf("Cannot open display %s\n", appstr); return 1; } if (!XShmQueryExtension(appdisp)) { printf("Display %s lacks SHM extension\n", appstr); return 1; } Display *vncdisp = XOpenDisplay(vncstr); if (!vncdisp) { printf("Cannot open display %s\n", vncstr); return 1; } if (!XShmQueryExtension(vncdisp)) { printf("Display %s lacks SHM extension\n", vncstr); return 1; } const int appscreen = DefaultScreen(appdisp); const int vncscreen = DefaultScreen(vncdisp); Visual *appvis = DefaultVisual(appdisp, appscreen); //Visual *vncvis = DefaultVisual(vncdisp, vncscreen); const int appdepth = DefaultDepth(appdisp, appscreen); const int vncdepth = DefaultDepth(vncdisp, vncscreen); if (appdepth != vncdepth) { printf("Depths don't match, app %u vnc %u\n", appdepth, vncdepth); return 1; } Window approot = DefaultRootWindow(appdisp); Window vncroot = DefaultRootWindow(vncdisp); XWindowAttributes appattr, vncattr; XGCValues gcval; gcval.plane_mask = AllPlanes; gcval.function = GXcopy; GC gc = XCreateGC(vncdisp, vncroot, GCFunction | GCPlaneMask, &gcval); XImage *img = NULL; XShmSegmentInfo shminfo; unsigned imgw = 0, imgh = 0; if (XGrabPointer(vncdisp, vncroot, False, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime) != Success) return 1; if (XGrabKeyboard(vncdisp, vncroot, False, GrabModeAsync, GrabModeAsync, CurrentTime) != Success) return 1; int xfixesbase, xfixeserrbase; XFixesQueryExtension(appdisp, &xfixesbase, &xfixeserrbase); XFixesSelectSelectionInput(appdisp, approot, XA_PRIMARY, XFixesSetSelectionOwnerNotifyMask); int xfixesbasevnc, xfixeserrbasevnc; XFixesQueryExtension(vncdisp, &xfixesbasevnc, &xfixeserrbasevnc); XFixesSelectSelectionInput(vncdisp, vncroot, XA_PRIMARY, XFixesSetSelectionOwnerNotifyMask); Atom xa_targets_vnc = XInternAtom(vncdisp, "TARGETS", False); Atom xa_targets_app = XInternAtom(appdisp, "TARGETS", False); Window selwin = XCreateSimpleWindow(appdisp, approot, 3, 2, 1, 1, 0, 0, 0); Window vncselwin = XCreateSimpleWindow(vncdisp, vncroot, 3, 2, 1, 1, 0, 0, 0); XFixesCursorImage *cursor = NULL; uint64_t cursorhash = 0; Cursor xcursor = None; const unsigned sleeptime = 1000 * 1000 / fps; while (1) { if (!XGetWindowAttributes(appdisp, approot, &appattr)) break; if (!XGetWindowAttributes(vncdisp, vncroot, &vncattr)) break; if (resize && (appattr.width != vncattr.width || appattr.height != vncattr.height)) { // resize app display to VNC display size XRRScreenConfiguration *config = XRRGetScreenInfo(appdisp, approot); int nsizes, i, match = -1; XRRScreenSize *sizes = XRRConfigSizes(config, &nsizes); //printf("%u sizes\n", nsizes); for (i = 0; i < nsizes; i++) { if (sizes[i].width == vncattr.width && sizes[i].height == vncattr.height) { //printf("match %u\n", i); match = i; break; } } if (match >= 0) { XRRSetScreenConfig(appdisp, config, approot, match, RR_Rotate_0, CurrentTime); } else { /*XRRSetScreenSize(appdisp, approot, vncattr.width, vncattr.height, sizes[0].mwidth, sizes[0].mheight);*/ XRRScreenResources *res = XRRGetScreenResources(appdisp, approot); //printf("%u outputs, %u crtcs\n", res->noutput, res->ncrtc); // Nvidia crap uses a *different* list for 1.0 and 1.2! unsigned oldmode = 0xffff; //printf("1.2 modes %u\n", res->nmode); for (i = 0; i < res->nmode; i++) { if (res->modes[i].width == vncattr.width && res->modes[i].height == vncattr.height) { oldmode = i; //printf("old mode %u matched\n", i); break; } } unsigned tgt = 0; if (res->noutput > 1) { for (i = 0; i < res->noutput; i++) { XRROutputInfo *info = XRRGetOutputInfo(appdisp, res, res->outputs[i]); if (info->connection == RR_Connected) tgt = i; //printf("%u %s %u\n", i, info->name, info->connection); XRRFreeOutputInfo(info); } } if (oldmode < 0xffff) { Status s; // nvidia needs this weird dance s = XRRSetCrtcConfig(appdisp, res, res->crtcs[tgt], CurrentTime, 0, 0, None, RR_Rotate_0, NULL, 0); //printf("disable %u\n", s); XRRSetScreenSize(appdisp, approot, vncattr.width, vncattr.height, sizes[0].mwidth, sizes[0].mheight); s = XRRSetCrtcConfig(appdisp, res, res->crtcs[tgt], CurrentTime, 0, 0, res->modes[oldmode].id, RR_Rotate_0, &res->outputs[tgt], 1); //printf("set %u\n", s); } else { char name[32]; sprintf(name, "%ux%u_60", vncattr.width, vncattr.height); printf("Creating new Mode %s\n", name); XRRModeInfo *mode = XRRAllocModeInfo(name, strlen(name)); mode->width = vncattr.width; mode->height = vncattr.height; RRMode rmode = XRRCreateMode(appdisp, approot, mode); XRRAddOutputMode(appdisp, res->outputs[tgt], rmode); XRRFreeModeInfo(mode); } XRRFreeScreenResources(res); } XRRFreeScreenConfigInfo(config); } const unsigned w = min(appattr.width, vncattr.width); const unsigned h = min(appattr.height, vncattr.height); if (w != imgw || h != imgh) { if (img) { XShmDetach(appdisp, &shminfo); XDestroyImage(img); shmdt(shminfo.shmaddr); shmctl(shminfo.shmid, IPC_RMID, NULL); } img = XShmCreateImage(appdisp, appvis, appdepth, ZPixmap, NULL, &shminfo, w, h); if (!img) break; shminfo.shmid = shmget(IPC_PRIVATE, img->bytes_per_line * img->height, IPC_CREAT | 0666); if (shminfo.shmid == -1) break; shminfo.shmaddr = img->data = shmat(shminfo.shmid, 0, 0); shminfo.readOnly = False; if (!XShmAttach(appdisp, &shminfo)) break; imgw = w; imgh = h; } XShmGetImage(appdisp, approot, img, 0, 0, 0xffffffff); XPutImage(vncdisp, vncroot, gc, img, 0, 0, 0, 0, w, h); // Handle events while (XPending(vncdisp)) { XEvent ev; XNextEvent(vncdisp, &ev); if (ev.type == xfixesbasevnc + XFixesSelectionNotify) { XFixesSelectionNotifyEvent *xfe = (XFixesSelectionNotifyEvent *) &ev; // printf("vnc disp did a copy, owner %lu, root %lu\n", // xfe->owner, vncroot); if (xfe->owner == None || xfe->owner == vncroot) continue; XConvertSelection(vncdisp, XA_PRIMARY, XA_STRING, XA_STRING, vncselwin, CurrentTime); } else switch (ev.type) { case KeyPress: case KeyRelease: XTestFakeKeyEvent(appdisp, ev.xkey.keycode, ev.type == KeyPress, CurrentTime); break; case ButtonPress: case ButtonRelease: XTestFakeButtonEvent(appdisp, ev.xbutton.button, ev.type == ButtonPress, CurrentTime); break; case MotionNotify: XTestFakeMotionEvent(appdisp, appscreen, ev.xmotion.x, ev.xmotion.y, CurrentTime); break; case SelectionRequest: supplyselection(vncdisp, &ev, xa_targets_vnc); break; case SelectionNotify: { Atom realtype; int fmt; unsigned long nitems, bytes_rem; unsigned char *data; if (XGetWindowProperty(vncdisp, vncselwin, XA_STRING, 0, CUT_MAX / 4, False, AnyPropertyType, &realtype, &fmt, &nitems, &bytes_rem, &data) == Success) { if (bytes_rem) { printf("Clipboard too large, ignoring\n"); } else { const uint32_t len = nitems * (fmt / 8); //printf("realtype %lu, fmt %u, nitems %lu\n", // realtype, fmt, nitems); memcpy(cutbuf, data, len); if (len < CUT_MAX) cutbuf[len] = 0; else cutbuf[CUT_MAX - 1] = 0; // Send it to the app screen XSetSelectionOwner(appdisp, XA_PRIMARY, approot, CurrentTime); } XFree(data); } else { printf("Failed to fetch vnc clipboard\n"); } } break; case SelectionClear: cutbuf[0] = '\0'; XSetSelectionOwner(appdisp, XA_PRIMARY, None, CurrentTime); break; default: printf("Unexpected event type %u\n", ev.type); break; } } // App-side events while (XPending(appdisp)) { XEvent ev; XNextEvent(appdisp, &ev); if (ev.type == xfixesbase + XFixesSelectionNotify) { XFixesSelectionNotifyEvent *xfe = (XFixesSelectionNotifyEvent *) &ev; //printf("app disp did a copy, owner %lu\n", xfe->owner); if (xfe->owner == None || xfe->owner == approot) continue; XConvertSelection(appdisp, XA_PRIMARY, XA_STRING, XA_STRING, selwin, CurrentTime); } else switch (ev.type) { case SelectionNotify: { Atom realtype; int fmt; unsigned long nitems, bytes_rem; unsigned char *data; if (XGetWindowProperty(appdisp, selwin, XA_STRING, 0, CUT_MAX / 4, False, AnyPropertyType, &realtype, &fmt, &nitems, &bytes_rem, &data) == Success) { if (bytes_rem) { printf("Clipboard too large, ignoring\n"); } else { const uint32_t len = nitems * (fmt / 8); //printf("realtype %lu, fmt %u, nitems %lu\n", // realtype, fmt, nitems); memcpy(cutbuf, data, len); if (len < CUT_MAX) cutbuf[len] = 0; else cutbuf[CUT_MAX - 1] = 0; // Send it to the VNC screen XSetSelectionOwner(vncdisp, XA_PRIMARY, vncroot, CurrentTime); } XFree(data); } else { printf("Failed to fetch app clipboard\n"); } } break; case SelectionRequest: supplyselection(appdisp, &ev, xa_targets_app); break; default: printf("Unexpected app event type %u\n", ev.type); break; } } // Cursors cursor = XFixesGetCursorImage(appdisp); uint64_t newhash = XXH64(cursor->pixels, cursor->width * cursor->height * sizeof(unsigned long), 0); if (cursorhash != newhash) { if (cursorhash) XFreeCursor(vncdisp, xcursor); XcursorImage *converted = XcursorImageCreate(cursor->width, cursor->height); converted->xhot = cursor->xhot; converted->yhot = cursor->yhot; unsigned i; for (i = 0; i < cursor->width * cursor->height; i++) { converted->pixels[i] = cursor->pixels[i]; } xcursor = XcursorImageLoadCursor(vncdisp, converted); XDefineCursor(vncdisp, vncroot, xcursor); XcursorImageDestroy(converted); cursorhash = newhash; } usleep(sleeptime); } XCloseDisplay(appdisp); XCloseDisplay(vncdisp); return 0; }