aboutsummaryrefslogtreecommitdiff
path: root/unsaver.c
diff options
context:
space:
mode:
Diffstat (limited to 'unsaver.c')
-rw-r--r--unsaver.c754
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;
+}