/* unsaver, by B. Watson, released under the WTFPL. the latest version of this file can be found at: http://urchlay.naptime.net/repos/unsaver/ */ /* need this, or else we don't get a prototype for strdup() from string.h */ #define _XOPEN_SOURCE 500 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* microseconds to wait between sending a keypresss or mouse click event and its release */ #define EVENT_DELAY 100000 /* if we can't find an unused keycode in the X keymap, use this one */ #define FALLBACK_KEYCODE 255 /* min/max allowed keycodes. really this should be queried from the X server, but for Xorg this value is good enough for argument checking. the max is 0xff as keycodes are 8 bit unsigned. */ #define MIN_KEYCODE 8 #define MAX_KEYCODE 255 /* same as above, for mouse buttons. */ #define MIN_BUTTON 1 #define MAX_BUTTON 10 /* command for -x */ #define X_COMMAND "xscreensaver-command -deactivate" /* default "-i 250ms" */ #define DEFAULT_INTERVAL 250 typedef enum { fm_auto_keycode, fm_keycode, fm_button, fm_motion, fm_cmd } fake_mode_t; /* user options */ int interval = DEFAULT_INTERVAL; /* -i (always in millisec) */ int debug = 0; /* -d */ int keycode = -1; /* -k */ int button = -1; /* -b */ const char *event_dir = EVENTDIR; /* joydev args can change this */ char *js_node_name = JSNODE; /* -j */ int autodiscover = 1; /* cleared if user supplies joydev args */ fake_mode_t fake_mode = fm_auto_keycode; /* -k, -b, -m */ char *c_command = NULL; /* -c, -x */ int monitor_joysticks = 1; /* -f clears this */ int monitor_fullscreen = 1; /* -j clears this */ /* joystick stuff */ char *joynames[MAX_STICKS + 1]; int last_joyname = 0; int joyfds[MAX_STICKS + 1]; /* executable name, for warnings/errors */ const char *self; /* X stuff */ Display *xdisp; /* inotify stuff */ int inotify_fd, watch_fd; char inotify_buf[sizeof(struct inotify_event) + NAME_MAX + 1]; struct inotify_event *inotify_ev = (struct inotify_event *)inotify_buf; /* not going to include pages of usage info duplicating the man page */ void usage(void) { printf(PROJ " v" VERSION " by B. Watson, WTFPL\n"); printf("Usage: %s [-i interval] [-k keycode | -b button | -m | -c command | -x]\n", self); printf(" [-j | -f ] [-d] [joystick [...]]\n"); printf("Built-in defaults: MAX_STICKS=%d, -d " EVENTDIR ", -j " JSNODE "\n", MAX_STICKS); printf("See man page for details.\n"); exit(0); } void die(const char *msg) { fprintf(stderr, "%s: %s\n", self, msg); exit(1); } /* make some trivial Xlib call that will result in Xlib killing this process if it fails. XNoOp looks like it was meant for exactly this, but it keeps returning 1 even if the X server is killed. */ void ping_x_server(void) { (void)XPending(xdisp); if(debug) fprintf(stderr, "X server is still alive\n"); } void send_fake_key(void) { if(debug) fprintf(stderr, "sending keycode %d\n", keycode); /* press... */ XTestFakeKeyEvent(xdisp, keycode, 1, 0L); XSync(xdisp, 0); /* ...wait 1/10 sec... */ usleep(EVENT_DELAY); /* ...release */ XTestFakeKeyEvent(xdisp, keycode, 0, 0L); XSync(xdisp, 0); } void send_fake_click(void) { if(debug) fprintf(stderr, "sending button %d click\n", button); /* press... */ XTestFakeButtonEvent(xdisp, button, 1, 0L); XSync(xdisp, 0); /* ...wait 1/10 sec... */ usleep(EVENT_DELAY); /* ...release */ XTestFakeButtonEvent(xdisp, button, 0, 0L); XSync(xdisp, 0); } /* XXX: the man page documents XTestFakeRelativeMotionEvent() as taking 5 arguments (2nd one is screen_number), but Xtst.h only has 4 (with no screen_number arg). */ void send_fake_motion(void) { int oldx, oldy, ignoreme; Window win_ignoreme; if(debug) fprintf(stderr, "sending mouse motion\n"); XQueryPointer( xdisp, DefaultRootWindow(xdisp), &win_ignoreme, &win_ignoreme, &oldx, &oldy, &ignoreme, &ignoreme, (unsigned int *)&ignoreme); if(debug) fprintf(stderr, "oldx %d, oldy %d\n", oldx, oldy); /* move... */ XTestFakeRelativeMotionEvent(xdisp, 10, 10, 0L); XSync(xdisp, 0); /* ...wait 1/10 sec... */ usleep(EVENT_DELAY); /* ...move back */ XTestFakeRelativeMotionEvent(xdisp, -10, -10, 0L); XSync(xdisp, 0); /* we've moved all 4 directions now. but if were were too close to an edge of the screen, the mouse pointer isn't where it started. so: */ XWarpPointer( xdisp, None, DefaultRootWindow(xdisp), 0, 0, 0, 0, oldx, oldy); XSync(xdisp, 0); } void exec_command(void) { if(!c_command) die("BUG: c_command is NULL\n"); if(debug) fprintf(stderr, "executing \"%s\"\n", c_command); system(c_command); } void send_fake_x_event(void) { switch(fake_mode) { case fm_keycode: send_fake_key(); break; case fm_button: send_fake_click(); break; case fm_motion: send_fake_motion(); break; case fm_cmd: exec_command(); break; default: /* This Never Happens(tm) */ fprintf(stderr, "%s: BUG: fake_mode %d isn't a valid fake_mode_t!\n", self, fake_mode); exit(1); break; } } /* wrapper for open(2), does an ioctl to make sure the thing it just opened really is a joystick. also warns on 'permission denied'. */ int js_open(const char *path, int mode) { int fd, res, ver; fd = open(path, mode); if(fd < 0 && errno == EACCES) fprintf(stderr, "%s: warning: %s exists, but: %s\n", self, path, strerror(errno)); if(fd > -1) { res = ioctl(fd, JSIOCGVERSION, &ver); if(res < 0) { if(errno == ENOTTY || errno == EINVAL) { fprintf(stderr, "%s: file %s is not a joystick\n", self, path); exit(1); } else { fprintf(stderr, "%s: %s: %s\n", self, path, strerror(errno)); exit(1); } } /* don't really need this much detail: if(debug) fprintf(stderr, "joystick driver version 0x%x\n", ver); */ } return fd; } void open_joysticks(void) { int fdcount, i; struct js_event e; fdcount = 0; for(i = 0; i < last_joyname; i++) { int synthev = 0; if(joyfds[i] > -1) close(joyfds[i]); joyfds[i] = js_open(joynames[i], O_RDONLY | O_NONBLOCK); if(joyfds[i] > -1) { fdcount++; while( (read(joyfds[i], &e, sizeof(e)) > 0) && (e.type & JS_EVENT_INIT) ) synthev++; if(debug) fprintf(stderr, "opened %s, fd %d, skipped %d synthetic events\n", joynames[i], joyfds[i], synthev); } } if(debug) fprintf(stderr, "opened %d devices\n", fdcount); } void check_inotify() { int res; res = read(inotify_fd, inotify_buf, sizeof(inotify_buf) + NAME_MAX + 1); if(res > 0) { if(debug) { fprintf(stderr, "read %d bytes from inotify_fd\n", res); if(!inotify_ev->len) { fprintf(stderr, "got event with no name: 0x%x\n", inotify_ev->mask); } else { fprintf(stderr, "got event with name %s: 0x%x\n", inotify_ev->name, inotify_ev->mask); } } /* not much we can do to recover from this: */ if(inotify_ev->mask & IN_DELETE_SELF) { die("someone deleted our device directory!"); } /* ...or this: */ if(inotify_ev->mask & IN_UNMOUNT) { die("someone unmounted our device directory!"); } usleep(EVENT_DELAY); /* might not need, be paranoid */ open_joysticks(); } else { if(errno != EAGAIN) { /* not sure what could cause this, so no idea how/if we recover from it */ fprintf(stderr, "%s: failed to read from inotify_fd: %s\n", self, strerror(errno)); exit(1); } } } int get_joystick_activity(void) { struct js_event e; int i, active = 0; for(i = 0; i < last_joyname; i++) { if(joyfds[i] < 0) continue; if(debug) fprintf(stderr, "reading device %d, fd %d\n", i, joyfds[i]); /* count events. any we get here *should* be real, but it doesn't hurt to mask out synthetic ones again */ while(read(joyfds[i], &e, sizeof(e)) > 0) { if(!(e.type & JS_EVENT_INIT)) active++; } /* EAGAIN just means there are no more events to read. anything else is a problem, though not fatal */ if(errno != EAGAIN) { if(debug) fprintf(stderr, "got error on device %d: %s\n", i, strerror(errno)); } } return active; } /* supposedly we could use the X window property _NET_WINDOW_FULLSCREEN to detect a fullscreen window, but not everything actually sets that. so a fullscreen window is defined here as: - visible - is the size of the root window - and has input focus. */ int have_fullscreen_window(void) { Window w; XWindowAttributes w_attrs, root_attrs; int revert_to; /* man page doesn't document the actual return value, it says "int", is it really a Status? */ if(!XGetInputFocus(xdisp, &w, &revert_to)) { if(debug) fprintf(stderr, "XGetInputFocus() failed\n"); return 0; } /* no window has focus (not an error) */ if(w == None) return 0; /* TODO: under what conditions does this fail? if it does fail once, will it ever succeed again? Hopefully, using w_attrs.root will do the right thing in multi-screen environments. I can't easily test it though. */ if( XGetWindowAttributes(xdisp, w, &w_attrs) && XGetWindowAttributes(xdisp, w_attrs.root, &root_attrs) ) { if(debug) fprintf(stderr, "window id 0x%x (root 0x%x) has focus, map_state %d, geometry %dx%d\n", (int)w, (int)w_attrs.root, w_attrs.map_state, w_attrs.width, w_attrs.height); if( (w_attrs.map_state == IsViewable) && (root_attrs.width == w_attrs.width) && (root_attrs.height == w_attrs.height) ) { if(debug) fprintf(stderr, "window id 0x%x has input focus and looks fullscreen (%dx%d)\n", (int)w, w_attrs.width, w_attrs.height); return 1; } } else if(debug) { if(debug) fprintf(stderr, "XGetWindowAttributes() failed\n"); return 0; } return 0; } /* this is the point of no return. the only way out of main_loop() is process death. */ void main_loop(void) { int sleep_us; sleep_us = interval * 1000; fprintf(stderr, "entering main_loop(), sleep_us %d\n", sleep_us); while(1) { int active = 0; /* Xlib will obligingly kill us, if X goes away */ ping_x_server(); /* check_inotify() will close & reopen the joystick fds as needed */ if(monitor_joysticks) check_inotify(); /* let the fds gather events as we slumber */ if(debug) fprintf(stderr, "sleeping %dms...\n", interval); usleep(sleep_us); /* now see if any of them got any events while we took a nap */ if(monitor_joysticks) active += get_joystick_activity(); if(monitor_fullscreen) active += have_fullscreen_window(); /* if we got any activity on any of the fds, do our thing */ if(active) { if(debug) fprintf(stderr, "*** got activity!\n"); send_fake_x_event(); } else if(debug) fprintf(stderr, "no activity\n"); } } /* make self point to our executable name without any directory */ void set_exe_name(const char *argv0) { const char *p; self = argv0; for(p = self; *p; p++) if(p[0] == '/' && p[1]) self = p + 1; } char *joy_num_to_name(int js) { static char buf[PATH_MAX + 1]; sprintf(buf, "%s/%s%d", event_dir, js_node_name, js); return buf; } /* add a joystick by pathname or number. note this only gets called once, at startup: the list never gets modified after that. */ void add_joystick_name(const char *name) { if(last_joyname >= MAX_STICKS) { fprintf(stderr, "%s: too many device names, monitoring only the first %d\n", self, last_joyname); return; } if(*name >= '0' && *name <= '9') { add_joystick_name(joy_num_to_name(atoi(name))); return; } if( !(joynames[last_joyname] = strdup(name)) ) die(strerror(errno)); if(debug) fprintf(stderr, "added device %d: %s\n", last_joyname, joynames[last_joyname]); last_joyname++; } void add_joystick_num(char *arg) { int i; char *end; i = strtol(arg, &end, 0); if(end == arg || *end || i < 0) { fprintf(stderr, "%s: invalid joystick number: %s\n", self, arg); exit(1); } add_joystick_name(joy_num_to_name(i)); } void populate_joynames(void) { int i; for(i = 0; i < MAX_STICKS; i++) { add_joystick_name(joy_num_to_name(i)); } } void set_fake_mode(fake_mode_t newmode) { if(fake_mode != fm_auto_keycode) die("use only one of the -k -b -m options"); else fake_mode = newmode; } int set_interval(const char *arg) { char *end; interval = strtol(arg, &end, 0); /* no digits at all */ if(end == arg) return 0; /* no 0 or negative */ if(interval < 1) return 0; /* digits only */ switch(*end) { case '\0': /* digits only, try to guess */ if(interval < 100) { fprintf(stderr, "%s: treating -i %d as %d seconds (use -i %dms if you meant %dms)\n", self, interval, interval, interval, interval); /* fall through */ } else { break; } case 's': interval *= 1000; break; /* sec */ case 'm': break; /* ms, use as-is */ } if(debug) fprintf(stderr, "interval set to %dms\n", interval); return 1; } void parse_args(int argc, char **argv) { char *nextarg; if(argc > 1 && strcmp(argv[1], "--help") == 0) usage(); while(++argv, --argc) { if(argv[0][0] != '-') { /* no dash, treat as a joystick device */ add_joystick_num(*argv); autodiscover = 0; } else { if(argv[0][1] && argv[0][2]) die("spaces required between options and arguments, please"); nextarg = argv[1]; switch(argv[0][1]) { case 'm': set_fake_mode(fm_motion); break; case 'k': if(nextarg && (keycode = atoi(nextarg)) && (keycode >= MIN_KEYCODE) && (keycode <= MAX_KEYCODE) ) { set_fake_mode(fm_keycode); argv++, argc--; } else { fprintf(stderr, "%s: -k requires a keycode argument, %d-%d\n", self, MIN_KEYCODE, MAX_KEYCODE); exit(1); } break; case 'b': if(nextarg) button = atoi(nextarg); if ( (button >= MIN_BUTTON) && (button <= MAX_BUTTON) ) { set_fake_mode(fm_button); argv++, argc--; } else { fprintf(stderr, "%s: -b requires a mouse button argument, %d-%d\n", self, MIN_BUTTON, MAX_BUTTON); exit(1); } break; case 'i': if(nextarg && set_interval(nextarg)) argv++, argc--; else die("-i requires a positive integer argument in milliseconds"); break; case 'd': debug = 1; break; case 'c': if(nextarg) { set_fake_mode(fm_cmd); c_command = nextarg; argv++, argc--; } else { die("-c requires a command argument"); } break; case 'x': set_fake_mode(fm_cmd); c_command = X_COMMAND; break; case 'f': monitor_joysticks = 0; break; case 'j': monitor_fullscreen = 0; break; default: die("unrecognized argument, try --help"); break; } } } if(!monitor_joysticks && !monitor_fullscreen) die("nothing to monitor (can't use -f and -j together)"); } /* make ourselves a daemon, double-fork technique. verbose comments are not here because I think you need them, they're here for me so I don't forget what this gibberish is for... */ void daemonize(void) { pid_t pid; if((pid = fork()) < 0) { /* in parent, fork failed */ fprintf(stderr, "%s: %s\n", self, strerror(errno)); die("failed to daemonize"); } else if(pid) { /* in parent, fork succeeded */ exit(0); } /* now we're in the child, aka the 2nd generation parent. the only reason for the first fork() was because setsid() can't be run in the parent process */ if( (setsid() < 0) ) { fprintf(stderr, "%s: %s\n", self, strerror(errno)); die("failed to daemonize"); } /* go on, do it again */ if((pid = fork()) < 0) { /* in 2nd gen parent, fork failed */ fprintf(stderr, "%s: %s\n", self, strerror(errno)); die("failed to daemonize"); } else if(pid) { /* in 2nd gen parent, fork succeeded */ fprintf(stderr, "%s: daemonized, PID %u\n", self, pid); exit(0); } /* now we're in the grandchild, and we no longer need stdin/out/err */ close(0); close(1); close(2); /* do this so we don't keep a filesystem from being umounted because it's still busy (as our cwd) */ chdir("/"); } /* helpers for init_* functions */ void connect_to_x(void) { int ignoreme; if(debug) fprintf(stderr, "connecting to X server...\n"); xdisp = XOpenDisplay(getenv("DISPLAY")); if(!xdisp) die("can't connect to X server"); if(debug) fprintf(stderr, "connected to X server\n"); if(XTestQueryExtension(xdisp, &ignoreme, &ignoreme, &ignoreme, &ignoreme)) { if(debug) fprintf(stderr, "X server supports XTest extension, good\n"); } else { die("X server doesn't support XTest extension\n"); } } /* XKeycodeToKeysym() is deprecated, hence the pragma fugliness. Deprecated or not, the xmodmap command still uses it, and it still works. However, XKB is the new hotness, we'll use that if it's available. */ KeySym keycode_is_mapped(XkbDescPtr xkbd, int i) { if(xkbd) { return XkbKeyNumGroups(xkbd, i) > 0; } else { /* if xkbd is NULL, the caller failed to init XKB. */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" return XKeycodeToKeysym(xdisp, i, 0) != NoSymbol; #pragma GCC diagnostic pop } } /* The goggles, they do nothing! Find an unused keycode in the keymap. From the shell, this would be something like: xmodmap -pke|grep '= *$'|head -1 */ void find_keycode() { int i, ignoreme, min_code = 0, max_code = 0; int major = XkbMajorVersion, minor = XkbMinorVersion; XkbDescPtr xkbd = NULL; /* not sure any X server still running on planet Earth will lack the XKB extension, but in case I'm wrong... */ if(XkbQueryExtension( xdisp, &ignoreme, &ignoreme, &ignoreme, &major, &minor)) { xkbd = XkbGetMap(xdisp, XkbAllClientInfoMask, XkbUseCoreKbd); fprintf(stderr, "using XKB extension major %d, minor %d\n", major, minor); } else { fprintf(stderr, "XKB extension not supported, fall back to XKeycodeToKeysym\n"); } /* man page doesn't document the return value */ (void)XDisplayKeycodes(xdisp, &min_code, &max_code); if(debug) fprintf(stderr, "XDisplayKeycodes min %d, max %d\n", min_code, max_code); for(i = min_code; i <= max_code; i++) { if(!keycode_is_mapped(xkbd, i)) { keycode = i; if(debug) fprintf(stderr, "using keycode %d\n", keycode); break; } } if(xkbd) XkbFreeKeyboard(xkbd, XkbAllClientInfoMask, 1); if(keycode < 0) { fprintf(stderr, "%s: can't find a free keycode in the keymap, using %d\n", self, FALLBACK_KEYCODE); keycode = FALLBACK_KEYCODE; } } /* the device name check is mostly to guard against typos */ void check_joyname(int i) { if(joynames[i] && (strncmp(event_dir, joynames[i], strlen(event_dir)) != 0)) { fprintf(stderr, "%s: device %s is not in event dir %s\n", self, joynames[i], event_dir); exit(1); } } /* init_* functions. these are only called once each, from main() */ void init_joysticks(void) { int i; for(i = 0; i < MAX_STICKS; i++) { joyfds[i] = -1; check_joyname(i); } open_joysticks(); } void init_inotify(void) { inotify_fd = inotify_init1(IN_NONBLOCK); if(inotify_fd < 0) { fprintf(stderr, "%s: inotify_init1() failed: %s", self, strerror(errno)); exit(1); } if(debug) fprintf(stderr, "inotify_init1() returned %d\n", inotify_fd); watch_fd = inotify_add_watch( inotify_fd, event_dir, IN_CREATE | IN_DELETE | IN_DELETE_SELF); if(watch_fd < 0) { fprintf(stderr, "%s: inotify_add_watch(\"%s\") failed: %s\n", self, event_dir, strerror(errno)); exit(1); } if(debug) fprintf(stderr, "inotify_add_watch() on %s returned %d\n", event_dir, inotify_fd); } void init_x(void) { connect_to_x(); if(fake_mode == fm_auto_keycode) { find_keycode(); fake_mode = fm_keycode; } /* do this now, before daemonizing. if something's wrong, Xlib will kill us. we'd better let that happen while we still have a stderr, so the user can see what happened. */ send_fake_x_event(); } void print_banner() { const char *m; if(monitor_joysticks && monitor_fullscreen) { m = "joysticks and fullscreen"; } else if(monitor_joysticks) { m = "joysticks only"; } else if(monitor_fullscreen) { m = "fullscreen only"; } else { die("nothing to monitor!"); } fprintf(stderr, "%s: v%s monitoring %s, interval %dms\n", self, VERSION, m, interval); } void read_env_vars(void) { char *tmp; if( (tmp = getenv("UNSAVER_JS_DIR")) ) event_dir = tmp; if( (tmp = getenv("UNSAVER_JS_NODE")) ) js_node_name = tmp; } int main(int argc, char **argv) { set_exe_name(argv[0]); read_env_vars(); parse_args(argc, argv); print_banner(); if(monitor_joysticks) { if(autodiscover) populate_joynames(); init_joysticks(); init_inotify(); } init_x(); if(!debug) daemonize(); main_loop(); return 0; }