/* * big-brother.c: activity tracker * * Copyright (c) 2016, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ #define LIBERTY_WANT_POLLER #include "config.h" #undef PROGRAM_NAME #define PROGRAM_NAME "big-brother" #include "liberty/liberty.c" #include #include #include #include #include #include // --- Utilities --------------------------------------------------------------- static int64_t clock_msec (clockid_t clock) { struct timespec tp; hard_assert (clock_gettime (clock, &tp) != -1); return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000; } static char * timestamp (int64_t ts) { char buf[24]; struct tm tm; time_t when = ts / 1000; strftime (buf, sizeof buf, "%F %T", gmtime_r (&when, &tm)); return xstrdup_printf ("%s.%03d", buf, (int) (ts % 1000)); } static void log_message_custom (void *user_data, const char *quote, const char *fmt, va_list ap) { (void) user_data; FILE *stream = stdout; char *ts = timestamp (clock_msec (CLOCK_REALTIME)); fprintf (stream, "%s ", ts); free (ts); fputs (quote, stream); vfprintf (stream, fmt, ap); fputs ("\n", stream); } // --- Configuration ----------------------------------------------------------- static struct simple_config_item g_config_table[] = { { "idle_timeout", "600", "Timeout for user inactivity (s)" }, { NULL, NULL, NULL } }; // --- Application ------------------------------------------------------------- struct app_context { struct str_map config; ///< Program configuration struct poller poller; ///< Poller bool running; ///< Event loop is running Display *dpy; ///< X display handle struct poller_fd x_event; ///< X11 event Atom net_active_window; ///< _NET_ACTIVE_WINDOW Atom net_wm_name; ///< _NET_WM_NAME // Window title tracking char *current_title; ///< Current window title or NULL Window current_window; ///< Current window // XSync activity tracking int xsync_base_event_code; ///< XSync base event code XSyncCounter idle_counter; ///< XSync IDLETIME counter XSyncValue idle_timeout; ///< Idle timeout XSyncAlarm idle_alarm_inactive; ///< User is inactive XSyncAlarm idle_alarm_active; ///< User is active }; static void app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); self->config = str_map_make (free); simple_config_load_defaults (&self->config, g_config_table); if (!(self->dpy = XOpenDisplay (NULL))) exit_fatal ("cannot open display"); poller_init (&self->poller); self->x_event = poller_fd_make (&self->poller, ConnectionNumber (self->dpy)); self->net_active_window = XInternAtom (self->dpy, "_NET_ACTIVE_WINDOW", true); self->net_wm_name = XInternAtom (self->dpy, "_NET_WM_NAME", true); // TODO: it is possible to employ a fallback mechanism via XScreenSaver // by polling the XScreenSaverInfo::idle field, see // https://www.x.org/releases/X11R7.5/doc/man/man3/Xss.3.html int n; if (!XSyncQueryExtension (self->dpy, &self->xsync_base_event_code, &n) || !XSyncInitialize (self->dpy, &n, &n)) exit_fatal ("cannot initialize XSync"); // The idle counter is not guaranteed to exist, only SERVERTIME is XSyncSystemCounter *counters = XSyncListSystemCounters (self->dpy, &n); while (n--) { if (!strcmp (counters[n].name, "IDLETIME")) self->idle_counter = counters[n].counter; } if (!self->idle_counter) exit_fatal ("idle counter is missing"); XSyncFreeSystemCounterList (counters); } static void app_context_free (struct app_context *self) { str_map_free (&self->config); cstr_set (&self->current_title, NULL); poller_fd_reset (&self->x_event); XCloseDisplay (self->dpy); poller_free (&self->poller); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static char * x_text_property_to_utf8 (struct app_context *ctx, XTextProperty *prop) { #if ARE_WE_UTF8_YET Atom utf8_string = XInternAtom (ctx->dpy, "UTF8_STRING", true); if (prop->encoding == utf8_string) return xstrdup ((char *) prop->value); #endif int n = 0; char **list = NULL; if (XmbTextPropertyToTextList (ctx->dpy, prop, &list, &n) >= Success && n > 0 && *list) { // TODO: convert from locale encoding into UTF-8 char *result = xstrdup (*list); XFreeStringList (list); return result; } return NULL; } static char * x_text_property (struct app_context *ctx, Window window, Atom atom) { XTextProperty name; XGetTextProperty (ctx->dpy, window, &name, atom); if (!name.value) return NULL; char *result = x_text_property_to_utf8 (ctx, &name); XFree (name.value); return result; } static char * x_window_title (struct app_context *ctx, Window window) { char *title; if (!(title = x_text_property (ctx, window, ctx->net_wm_name)) && !(title = x_text_property (ctx, window, XA_WM_NAME))) title = xstrdup ("broken"); return title; } static bool update_window_title (struct app_context *ctx, char *new_title) { bool changed = !ctx->current_title != !new_title || (new_title && strcmp (ctx->current_title, new_title)); cstr_set (&ctx->current_title, new_title); return changed; } static void update_current_window (struct app_context *ctx) { Window root = DefaultRootWindow (ctx->dpy); Atom dummy_type; int dummy_format; unsigned long nitems, dummy_bytes; unsigned char *p = NULL; if (XGetWindowProperty (ctx->dpy, root, ctx->net_active_window, 0L, 1L, false, XA_WINDOW, &dummy_type, &dummy_format, &nitems, &dummy_bytes, &p) != Success) return; char *new_title = NULL; if (nitems) { Window active_window = *(Window *) p; XFree (p); if (ctx->current_window != active_window && ctx->current_window) XSelectInput (ctx->dpy, ctx->current_window, 0); XSelectInput (ctx->dpy, active_window, PropertyChangeMask); new_title = x_window_title (ctx, active_window); ctx->current_window = active_window; } if (update_window_title (ctx, new_title)) print_status ("Window changed: %s", ctx->current_title ? ctx->current_title : "(none)"); } static void on_x_property_notify (struct app_context *ctx, XPropertyEvent *ev) { // This is from the EWMH specification, set by the window manager if (ev->atom == ctx->net_active_window) update_current_window (ctx); else if (ev->window == ctx->current_window && ev->atom == ctx->net_wm_name) { if (update_window_title (ctx, x_window_title (ctx, ev->window))) print_status ("Title changed: %s", ctx->current_title); } } static void set_idle_alarm (struct app_context *ctx, XSyncAlarm *alarm, XSyncTestType test, XSyncValue value) { XSyncAlarmAttributes attr; attr.trigger.counter = ctx->idle_counter; attr.trigger.test_type = test; attr.trigger.wait_value = value; XSyncIntToValue (&attr.delta, 0); long flags = XSyncCACounter | XSyncCATestType | XSyncCAValue | XSyncCADelta; if (*alarm) XSyncChangeAlarm (ctx->dpy, *alarm, flags, &attr); else *alarm = XSyncCreateAlarm (ctx->dpy, flags, &attr); } static void on_x_alarm_notify (struct app_context *ctx, XSyncAlarmNotifyEvent *ev) { if (ev->alarm == ctx->idle_alarm_inactive) { print_status ("User is inactive"); XSyncValue one, minus_one; XSyncIntToValue (&one, 1); Bool overflow; XSyncValueSubtract (&minus_one, ev->counter_value, one, &overflow); // Set an alarm for IDLETIME <= current_idletime - 1 set_idle_alarm (ctx, &ctx->idle_alarm_active, XSyncNegativeComparison, minus_one); } else if (ev->alarm == ctx->idle_alarm_active) { print_status ("User is active"); set_idle_alarm (ctx, &ctx->idle_alarm_inactive, XSyncPositiveComparison, ctx->idle_timeout); } } static void on_x_ready (const struct pollfd *pfd, void *user_data) { (void) pfd; struct app_context *ctx = user_data; XEvent ev; while (XPending (ctx->dpy)) { if (XNextEvent (ctx->dpy, &ev)) exit_fatal ("XNextEvent returned non-zero"); else if (ev.type == PropertyNotify) on_x_property_notify (ctx, &ev.xproperty); else if (ev.type == ctx->xsync_base_event_code + XSyncAlarmNotify) on_x_alarm_notify (ctx, (XSyncAlarmNotifyEvent *) &ev); } } static XErrorHandler g_default_x_error_handler; static int on_x_error (Display *dpy, XErrorEvent *ee) { // This just is going to happen since those windows aren't ours if (ee->error_code == BadWindow) return 0; return g_default_x_error_handler (dpy, ee); } static void init_events (struct app_context *ctx) { Window root = DefaultRootWindow (ctx->dpy); XSelectInput (ctx->dpy, root, PropertyChangeMask); XSync (ctx->dpy, False); g_default_x_error_handler = XSetErrorHandler (on_x_error); unsigned long n; const char *timeout = str_map_find (&ctx->config, "idle_timeout"); if (!xstrtoul (&n, timeout, 10) || !n || n > INT_MAX / 1000) exit_fatal ("invalid value for the idle timeout"); XSyncIntToValue (&ctx->idle_timeout, n * 1000); update_current_window (ctx); set_idle_alarm (ctx, &ctx->idle_alarm_inactive, XSyncPositiveComparison, ctx->idle_timeout); ctx->x_event.dispatcher = on_x_ready; ctx->x_event.user_data = ctx; poller_fd_set (&ctx->x_event, POLLIN); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int main (int argc, char *argv[]) { g_log_message_real = log_message_custom; static const struct opt opts[] = { { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, { 'w', "write-default-cfg", "FILENAME", OPT_OPTIONAL_ARG | OPT_LONG_ONLY, "write a default configuration file and exit" }, { 0, NULL, NULL, 0, NULL } }; struct opt_handler oh = opt_handler_make (argc, argv, opts, NULL, "Activity tracker."); int c; while ((c = opt_handler_get (&oh)) != -1) switch (c) { case 'd': g_debug_mode = true; break; case 'h': opt_handler_usage (&oh, stdout); exit (EXIT_SUCCESS); case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); case 'w': call_simple_config_write_default (optarg, g_config_table); exit (EXIT_SUCCESS); default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } argc -= optind; argv += optind; opt_handler_free (&oh); if (!setlocale (LC_CTYPE, "")) exit_fatal ("cannot set locale"); if (!XSupportsLocale ()) exit_fatal ("locale not supported by Xlib"); struct app_context ctx; app_context_init (&ctx); struct error *e = NULL; if (!simple_config_update_from_file (&ctx.config, &e)) exit_fatal ("%s", e->message); init_events (&ctx); ctx.running = true; while (ctx.running) poller_run (&ctx.poller); app_context_free (&ctx); return 0; }