Add big-brother

So far this is only rather a prototype.
This commit is contained in:
Přemysl Eric Janouch 2016-10-12 18:21:36 +02:00
parent b96590664a
commit 7a32fb8e55
Signed by: p
GPG Key ID: B715679E3A361BE6
3 changed files with 410 additions and 1 deletions

View File

@ -22,7 +22,7 @@ set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
include (AddThreads) include (AddThreads)
find_package (PkgConfig REQUIRED) find_package (PkgConfig REQUIRED)
pkg_check_modules (dependencies REQUIRED libpulse x11) pkg_check_modules (dependencies REQUIRED libpulse x11 xext xextproto)
pkg_check_modules (gdm gdm glib-2.0 gio-2.0) pkg_check_modules (gdm gdm glib-2.0 gio-2.0)
option (WITH_GDM "Compile with GDM support" ${gdm_FOUND}) option (WITH_GDM "Compile with GDM support" ${gdm_FOUND})
@ -54,6 +54,9 @@ endif (WITH_GDM)
add_executable (siprandom siprandom.c) add_executable (siprandom siprandom.c)
target_link_libraries (siprandom ${project_libraries}) target_link_libraries (siprandom ${project_libraries})
add_executable (big-brother big-brother.c)
target_link_libraries (big-brother ${project_libraries})
# The files to be installed # The files to be installed
include (GNUInstallDirs) include (GNUInstallDirs)

View File

@ -18,6 +18,8 @@ to other people as well:
user screen user screen
- 'siprandom' uses the SipHash 2-4 algorithm to produce a stream of - 'siprandom' uses the SipHash 2-4 algorithm to produce a stream of
pseudo-random data; it should be fast enough to saturate most devices pseudo-random data; it should be fast enough to saturate most devices
- 'big-brother' tracks the title of the active window and the idle state of
the user and writes these events to standard output.
Don't expect them to work under any OS that isn't Linux. Don't expect them to work under any OS that isn't Linux.

404
big-brother.c Normal file
View File

@ -0,0 +1,404 @@
/*
* 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 void
log_message_custom (void *user_data, const char *quote, const char *fmt,
va_list ap)
{
(void) user_data;
FILE *stream = stdout;
fprintf (stream, PROGRAM_NAME ": ");
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);
str_map_init (&self->config);
self->config.free = free;
simple_config_load_defaults (&self->config, g_config_table);
if (!(self->dpy = XOpenDisplay (NULL)))
exit_fatal ("cannot open display");
poller_init (&self->poller);
poller_fd_init (&self->x_event, &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_init (&oh, 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;
}