diff options
Diffstat (limited to 'unsaver.c')
-rw-r--r-- | unsaver.c | 754 |
1 files changed, 754 insertions, 0 deletions
diff --git a/unsaver.c b/unsaver.c new file mode 100644 index 0000000..eceb4d3 --- /dev/null +++ b/unsaver.c @@ -0,0 +1,754 @@ +/* need this, or else we don't get a prototype for strdup() from string.h */ +#define _XOPEN_SOURCE 500 + +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <limits.h> +#include <errno.h> + +#include <X11/X.h> +#include <X11/Xlib.h> +#include <X11/extensions/XTest.h> + +#include <linux/joystick.h> +#include <sys/inotify.h> + +/* 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 */ +char *event_dir = EVENTDIR; /* -d */ +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 */ +int monitor_fullscreen = 0; /* -f, -F */ + +/* 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] [-b button | -k keycode] [-j name]\n", self); + printf(" [-d dir] [-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; + } +} + +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] = 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 *make_joystick_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(make_joystick_name(atoi(name))); + return; + } + + if( !(joynames[last_joyname] = strdup(name)) ) + die(strerror(errno)); + + if(debug) + fprintf(stderr, "added device name %d: %s\n", last_joyname, name); + + last_joyname++; +} + +void populate_joynames(void) { + int i; + + for(i = 0; i < MAX_STICKS; i++) { + add_joystick_name(make_joystick_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_name(*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': + if(nextarg) { + event_dir = nextarg; + argv++, argc--; + } else { + die("-d requires a directory argument"); + } + break; + case 'j': + if(nextarg) { + js_node_name = nextarg; + argv++, argc--; + } else { + die("-j requires a string argument"); + } + 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; /* and fall thru */ + case 'f': + monitor_fullscreen = 1; + break; + default: + die("unrecognized argument, try --help"); + break; + } + } + } +} + +/* 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"); + } +} + +/* XXX: XKeycodeToKeysym() is deprecated. I must learn XKB. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +KeySym get_keysym(int i) { + return XKeycodeToKeysym(xdisp, i, 0); +} +#pragma GCC diagnostic pop + +/* the goggles, they do nothing! */ +void find_keycode() { + int i, min_code = 0, max_code = 0; + + /* 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(get_keysym(i) == NoSymbol) { + keycode = i; + if(debug) fprintf(stderr, "using keycode %d\n", keycode); + break; + } + } + + 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); +} + +int main(int argc, char **argv) { + set_exe_name(argv[0]); + + 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; +} |