425 lines
11 KiB
C
425 lines
11 KiB
C
/*
|
|
* big-brother.c: activity tracker
|
|
*
|
|
* Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
|
|
*
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* 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 <locale.h>
|
|
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/keysym.h>
|
|
#include <X11/extensions/sync.h>
|
|
|
|
// --- 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);
|
|
free (self->current_title);
|
|
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));
|
|
free (ctx->current_title);
|
|
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;
|
|
}
|