parent
b96590664a
commit
7a32fb8e55
|
@ -22,7 +22,7 @@ set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
|
|||
include (AddThreads)
|
||||
|
||||
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)
|
||||
|
||||
option (WITH_GDM "Compile with GDM support" ${gdm_FOUND})
|
||||
|
@ -54,6 +54,9 @@ endif (WITH_GDM)
|
|||
add_executable (siprandom siprandom.c)
|
||||
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
|
||||
include (GNUInstallDirs)
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ to other people as well:
|
|||
user screen
|
||||
- 'siprandom' uses the SipHash 2-4 algorithm to produce a stream of
|
||||
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.
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue