2020-09-23 14:12:58 +02:00
|
|
|
//
|
|
|
|
// wdmtg.c: activity tracker
|
|
|
|
//
|
|
|
|
// Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
//
|
|
|
|
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdarg.h>
|
|
|
|
#include <locale.h>
|
|
|
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <sys/socket.h>
|
|
|
|
#include <sys/un.h>
|
|
|
|
|
2020-10-02 02:07:42 +02:00
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <glib.h>
|
|
|
|
#include <sqlite3.h>
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
#include <xcb/xcb.h>
|
|
|
|
#include <xcb/sync.h>
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
#include "gui.h"
|
2020-09-23 16:58:30 +02:00
|
|
|
#include "compound-text.h"
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
// --- Utilities ---------------------------------------------------------------
|
|
|
|
|
|
|
|
static void
|
|
|
|
exit_fatal(const gchar *format, ...) G_GNUC_PRINTF(1, 2);
|
|
|
|
|
|
|
|
static void
|
|
|
|
exit_fatal(const gchar *format, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
va_start(ap, format);
|
|
|
|
|
|
|
|
gchar *format_nl = g_strdup_printf("%s\n", format);
|
|
|
|
vfprintf(stderr, format_nl, ap);
|
|
|
|
free(format_nl);
|
|
|
|
|
|
|
|
va_end(ap);
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
}
|
|
|
|
|
|
|
|
static GStrv
|
|
|
|
get_xdg_config_dirs(void)
|
|
|
|
{
|
|
|
|
GPtrArray *paths = g_ptr_array_new();
|
|
|
|
g_ptr_array_add(paths, g_strdup(g_get_user_config_dir()));
|
|
|
|
for (const gchar *const *sys = g_get_system_config_dirs(); *sys; sys++)
|
|
|
|
g_ptr_array_add(paths, g_strdup(*sys));
|
|
|
|
g_ptr_array_add(paths, NULL);
|
|
|
|
return (GStrv) g_ptr_array_free(paths, false);
|
|
|
|
}
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
static GString *
|
|
|
|
latin1_to_utf8(const gchar *latin1, gsize len)
|
|
|
|
{
|
|
|
|
GString *s = g_string_new(NULL);
|
|
|
|
while (len--) {
|
|
|
|
guchar c = *latin1++;
|
|
|
|
if (c < 0x80) {
|
|
|
|
g_string_append_c(s, c);
|
|
|
|
} else {
|
|
|
|
g_string_append_c(s, 0xC0 | (c >> 6));
|
|
|
|
g_string_append_c(s, 0x80 | (c & 0x3F));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2020-09-23 14:12:58 +02:00
|
|
|
// --- Globals -----------------------------------------------------------------
|
|
|
|
|
|
|
|
struct event {
|
|
|
|
gint64 timestamp; // When the event happened
|
|
|
|
gchar *title; // Current title at the time
|
2020-10-02 01:31:27 +02:00
|
|
|
gchar *class; // Current class at the time
|
2020-09-23 14:12:58 +02:00
|
|
|
gboolean idle; // Whether the user is idle
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
event_free(struct event *self)
|
|
|
|
{
|
|
|
|
g_free(self->title);
|
2020-10-02 01:31:27 +02:00
|
|
|
g_free(self->class);
|
2020-09-23 14:12:58 +02:00
|
|
|
g_slice_free(struct event, self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct {
|
|
|
|
GAsyncQueue *queue; // Async queue of `struct event`
|
2020-09-23 16:22:52 +02:00
|
|
|
sqlite3 *db; // Event database
|
2020-09-25 07:20:49 +02:00
|
|
|
sqlite3_stmt *add_event; // Prepared statement: add event
|
2020-09-23 14:12:58 +02:00
|
|
|
} g;
|
|
|
|
|
|
|
|
struct {
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_connection_t *X; // X display handle
|
|
|
|
xcb_screen_t *screen; // X screen information
|
2020-09-23 16:22:52 +02:00
|
|
|
GThread *thread; // Worker thread
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_atom_t atom_net_active_window; // _NET_ACTIVE_WINDOW
|
|
|
|
xcb_atom_t atom_net_wm_name; // _NET_WM_NAME
|
|
|
|
xcb_atom_t atom_utf8_string; // UTF8_STRING
|
|
|
|
xcb_atom_t atom_compound_text; // COMPOUND_TEXT
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
// Window title tracking
|
|
|
|
|
|
|
|
gchar *current_title; // Current window title or NULL
|
2020-10-02 01:31:27 +02:00
|
|
|
gchar *current_class; // Current window class or NULL
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_window_t current_window; // Current window
|
2020-09-23 14:12:58 +02:00
|
|
|
gboolean current_idle; // Current idle status
|
|
|
|
|
|
|
|
// XSync activity tracking
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
const xcb_query_extension_reply_t *sync; // Sync extension
|
|
|
|
xcb_sync_counter_t idle_counter; // Sync IDLETIME counter
|
|
|
|
xcb_sync_int64_t idle_timeout; // Idle timeout
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_sync_alarm_t idle_alarm_inactive; // User is inactive
|
|
|
|
xcb_sync_alarm_t idle_alarm_active; // User is active
|
2020-09-23 14:12:58 +02:00
|
|
|
} gen;
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
// --- XCB helpers -------------------------------------------------------------
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
static xcb_atom_t
|
|
|
|
intern_atom(const char *atom)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_intern_atom_reply_t *iar = xcb_intern_atom_reply(gen.X,
|
|
|
|
xcb_intern_atom(gen.X, false, strlen(atom), atom), NULL);
|
|
|
|
xcb_atom_t result = iar ? iar->atom : XCB_NONE;
|
|
|
|
free(iar);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static xcb_sync_counter_t
|
|
|
|
find_counter(xcb_sync_list_system_counters_reply_t *slsr, const char *name)
|
|
|
|
{
|
|
|
|
// FIXME: https://gitlab.freedesktop.org/xorg/lib/libxcb/-/issues/36
|
|
|
|
const size_t xcb_sync_systemcounter_t_len = 14;
|
|
|
|
|
|
|
|
xcb_sync_systemcounter_iterator_t slsi =
|
|
|
|
xcb_sync_list_system_counters_counters_iterator(slsr);
|
|
|
|
while (slsi.rem--) {
|
|
|
|
xcb_sync_systemcounter_t *counter = slsi.data;
|
|
|
|
char *counter_name = (char *) counter + xcb_sync_systemcounter_t_len;
|
|
|
|
if (!strncmp(counter_name, name, counter->name_len) &&
|
|
|
|
!name[counter->name_len])
|
|
|
|
return counter->counter;
|
|
|
|
|
|
|
|
slsi.data = (void *) counter +
|
|
|
|
((xcb_sync_systemcounter_t_len + counter->name_len + 3) & ~3);
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
2020-09-23 16:58:30 +02:00
|
|
|
return XCB_NONE;
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
static xcb_sync_counter_t
|
|
|
|
get_counter(const char *name)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_sync_list_system_counters_reply_t *slsr =
|
|
|
|
xcb_sync_list_system_counters_reply(gen.X,
|
|
|
|
xcb_sync_list_system_counters(gen.X), NULL);
|
|
|
|
|
|
|
|
xcb_sync_counter_t counter = XCB_NONE;
|
|
|
|
if (slsr) {
|
|
|
|
counter = find_counter(slsr, name);
|
|
|
|
free(slsr);
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
2020-09-23 16:58:30 +02:00
|
|
|
return counter;
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
// --- X helpers ---------------------------------------------------------------
|
|
|
|
|
|
|
|
static GString *
|
|
|
|
x_text_property_to_utf8(GString *value, xcb_atom_t encoding)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
if (encoding == gen.atom_utf8_string)
|
|
|
|
return value;
|
|
|
|
|
|
|
|
if (encoding == XCB_ATOM_STRING) {
|
|
|
|
// Could use g_convert() but this will certainly never fail
|
|
|
|
GString *utf8 = latin1_to_utf8(value->str, value->len);
|
|
|
|
g_string_free(value, true);
|
|
|
|
return utf8;
|
|
|
|
}
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
// COMPOUND_TEXT doesn't deserve support for multiple NUL-separated items
|
|
|
|
int *ucs4 = NULL;
|
|
|
|
if (encoding == gen.atom_compound_text &&
|
|
|
|
(ucs4 = compound_text_to_ucs4((char *) value->str, value->len))) {
|
|
|
|
g_string_free(value, true);
|
|
|
|
glong len = 0;
|
|
|
|
gchar *utf8 = g_ucs4_to_utf8((gunichar *) ucs4, -1, NULL, &len, NULL);
|
|
|
|
free(ucs4);
|
|
|
|
|
|
|
|
// malloc failure or, rather theoretically, an out of range codepoint
|
|
|
|
if (utf8) {
|
|
|
|
value = g_string_new_len(utf8, len);
|
|
|
|
free(utf8);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
g_string_free(value, true);
|
|
|
|
return NULL;
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
static GString *
|
|
|
|
x_text_property(xcb_window_t window, xcb_atom_t property)
|
|
|
|
{
|
|
|
|
GString *buffer = g_string_new(NULL);
|
|
|
|
xcb_atom_t type = XCB_NONE;
|
|
|
|
uint32_t offset = 0;
|
|
|
|
|
|
|
|
xcb_get_property_reply_t *gpr = NULL;
|
|
|
|
while ((gpr = xcb_get_property_reply(gen.X,
|
|
|
|
xcb_get_property(gen.X, false /* delete */, window,
|
|
|
|
property, XCB_GET_PROPERTY_TYPE_ANY, offset, 0x8000), NULL))) {
|
|
|
|
if (gpr->format != 8 || (type && gpr->type != type)) {
|
|
|
|
free(gpr);
|
|
|
|
break;
|
|
|
|
}
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
int len = xcb_get_property_value_length(gpr);
|
|
|
|
g_string_append_len(buffer, xcb_get_property_value(gpr), len);
|
|
|
|
offset += len >> 2;
|
|
|
|
type = gpr->type;
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
bool last = !gpr->bytes_after;
|
|
|
|
free(gpr);
|
|
|
|
if (last)
|
|
|
|
return x_text_property_to_utf8(buffer, type);
|
|
|
|
}
|
|
|
|
g_string_free(buffer, true);
|
|
|
|
return NULL;
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 06:45:27 +02:00
|
|
|
// --- Async Queue Source ------------------------------------------------------
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
async_queue_source_prepare(G_GNUC_UNUSED GSource *source,
|
|
|
|
G_GNUC_UNUSED gint *timeout_)
|
|
|
|
{
|
|
|
|
return g_async_queue_length(g.queue) > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
async_queue_source_dispatch(G_GNUC_UNUSED GSource *source,
|
|
|
|
GSourceFunc callback, gpointer user_data)
|
|
|
|
{
|
|
|
|
// I don't want to call it once per message, prefer batch processing
|
|
|
|
if (callback)
|
|
|
|
return callback(user_data);
|
|
|
|
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static GSource *
|
|
|
|
async_queue_source_new(void)
|
|
|
|
{
|
|
|
|
static GSourceFuncs funcs = {
|
|
|
|
.prepare = async_queue_source_prepare,
|
|
|
|
.check = NULL,
|
|
|
|
.dispatch = async_queue_source_dispatch,
|
|
|
|
.finalize = NULL,
|
|
|
|
};
|
|
|
|
|
|
|
|
GSource *source = g_source_new(&funcs, sizeof *source);
|
|
|
|
g_source_set_name(source, "AsyncQueueSource");
|
|
|
|
return source;
|
|
|
|
}
|
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
// --- Generator ---------------------------------------------------------------
|
|
|
|
|
|
|
|
static void
|
|
|
|
push_event(void) {
|
|
|
|
struct event *event = g_slice_new0(struct event);
|
|
|
|
event->timestamp = g_get_real_time();
|
|
|
|
event->title = g_strdup(gen.current_title);
|
2020-10-02 01:31:27 +02:00
|
|
|
event->class = g_strdup(gen.current_class);
|
2020-09-23 16:22:52 +02:00
|
|
|
event->idle = gen.current_idle;
|
|
|
|
g_async_queue_push(g.queue, event);
|
2020-09-25 06:45:27 +02:00
|
|
|
|
|
|
|
// This is the best thing GLib exposes (GWakeUp is internal)
|
|
|
|
g_main_context_wakeup(g_main_context_default());
|
2020-09-23 16:22:52 +02:00
|
|
|
}
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
static char *
|
2020-09-23 16:58:30 +02:00
|
|
|
x_window_title(xcb_window_t window)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
GString *title;
|
|
|
|
if (!(title = x_text_property(window, gen.atom_net_wm_name))
|
|
|
|
&& !(title = x_text_property(window, XCB_ATOM_WM_NAME)))
|
2020-10-02 01:50:37 +02:00
|
|
|
return g_strdup("");
|
2020-09-23 16:58:30 +02:00
|
|
|
return g_string_free(title, false);
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
update_window_title(char *new_title)
|
|
|
|
{
|
|
|
|
bool changed = !gen.current_title != !new_title
|
|
|
|
|| (new_title && strcmp(gen.current_title, new_title));
|
|
|
|
free(gen.current_title);
|
|
|
|
gen.current_title = new_title;
|
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
|
2020-10-02 01:31:27 +02:00
|
|
|
static char *
|
|
|
|
x_window_class(xcb_window_t window)
|
|
|
|
{
|
|
|
|
GString *title;
|
|
|
|
if (!(title = x_text_property(window, XCB_ATOM_WM_CLASS)))
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
// First is an "instance name", followed by a NUL and a "class name".
|
|
|
|
// Strongly prefer Firefox/Thunderbird over Navigator/Mail.
|
|
|
|
size_t skip = strlen(title->str);
|
|
|
|
if (++skip >= title->len)
|
|
|
|
return g_string_free(title, true);
|
|
|
|
|
|
|
|
g_string_erase(title, 0, skip);
|
|
|
|
return g_string_free(title, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
update_window_class(char *new_class)
|
|
|
|
{
|
|
|
|
bool changed = !gen.current_class != !new_class
|
|
|
|
|| (new_class && strcmp(gen.current_class, new_class));
|
|
|
|
free(gen.current_class);
|
|
|
|
gen.current_class = new_class;
|
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
|
2020-09-23 14:12:58 +02:00
|
|
|
static void
|
|
|
|
update_current_window(void)
|
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_get_property_reply_t *gpr = xcb_get_property_reply(gen.X,
|
|
|
|
xcb_get_property(gen.X, false /* delete */, gen.screen->root,
|
|
|
|
gen.atom_net_active_window, XCB_ATOM_WINDOW, 0, 1), NULL);
|
|
|
|
if (!gpr)
|
2020-09-23 14:12:58 +02:00
|
|
|
return;
|
|
|
|
|
|
|
|
char *new_title = NULL;
|
2020-10-02 01:31:27 +02:00
|
|
|
char *new_class = NULL;
|
2020-09-23 16:58:30 +02:00
|
|
|
if (xcb_get_property_value_length(gpr)) {
|
|
|
|
xcb_window_t active_window =
|
|
|
|
*(xcb_window_t *) xcb_get_property_value(gpr);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
const uint32_t disable[] = { 0 };
|
2020-09-23 14:12:58 +02:00
|
|
|
if (gen.current_window != active_window && gen.current_window)
|
2020-09-23 16:58:30 +02:00
|
|
|
(void) xcb_change_window_attributes(gen.X,
|
|
|
|
gen.current_window, XCB_CW_EVENT_MASK, disable);
|
|
|
|
|
|
|
|
const uint32_t enable[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
|
|
|
|
(void) xcb_change_window_attributes(gen.X,
|
|
|
|
active_window, XCB_CW_EVENT_MASK, enable);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
new_title = x_window_title(active_window);
|
2020-10-02 01:31:27 +02:00
|
|
|
new_class = x_window_class(active_window);
|
2020-09-23 14:12:58 +02:00
|
|
|
gen.current_window = active_window;
|
|
|
|
}
|
2020-09-23 16:58:30 +02:00
|
|
|
free(gpr);
|
2020-10-02 01:31:27 +02:00
|
|
|
|
|
|
|
// We need to absorb both pointers, so we need a bitwise OR
|
|
|
|
if (update_window_title(new_title) |
|
|
|
|
update_window_class(new_class))
|
2020-09-23 14:12:58 +02:00
|
|
|
push_event();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2020-09-23 16:58:30 +02:00
|
|
|
on_x_property_notify(const xcb_property_notify_event_t *ev)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
|
|
|
// This is from the EWMH specification, set by the window manager
|
2020-09-23 16:58:30 +02:00
|
|
|
if (ev->atom == gen.atom_net_active_window) {
|
2020-09-23 14:12:58 +02:00
|
|
|
update_current_window();
|
|
|
|
} else if (ev->window == gen.current_window &&
|
2020-09-23 16:58:30 +02:00
|
|
|
ev->atom == gen.atom_net_wm_name) {
|
2020-09-25 06:45:27 +02:00
|
|
|
if (update_window_title(x_window_title(ev->window)))
|
2020-09-23 14:12:58 +02:00
|
|
|
push_event();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2020-09-23 16:58:30 +02:00
|
|
|
set_idle_alarm(xcb_sync_alarm_t *alarm, xcb_sync_testtype_t test,
|
|
|
|
xcb_sync_int64_t value)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
// TODO: consider xcb_sync_{change,create}_alarm_aux()
|
|
|
|
uint32_t values[] = {
|
|
|
|
gen.idle_counter,
|
|
|
|
value.hi, value.lo,
|
|
|
|
test,
|
|
|
|
0, 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
xcb_sync_ca_t flags = XCB_SYNC_CA_COUNTER | XCB_SYNC_CA_VALUE |
|
|
|
|
XCB_SYNC_CA_TEST_TYPE | XCB_SYNC_CA_DELTA;
|
|
|
|
if (*alarm) {
|
|
|
|
xcb_sync_change_alarm(gen.X, *alarm, flags, values);
|
|
|
|
} else {
|
|
|
|
*alarm = xcb_generate_id(gen.X);
|
|
|
|
xcb_sync_create_alarm(gen.X, *alarm, flags, values);
|
|
|
|
}
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2020-09-23 16:58:30 +02:00
|
|
|
on_x_alarm_notify(const xcb_sync_alarm_notify_event_t *ev)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
|
|
|
if (ev->alarm == gen.idle_alarm_inactive) {
|
|
|
|
gen.current_idle = true;
|
|
|
|
push_event();
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_sync_int64_t minus_one = ev->counter_value;
|
|
|
|
if (!~(--minus_one.lo))
|
|
|
|
minus_one.hi--;
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
// Set an alarm for IDLETIME <= current_idletime - 1
|
|
|
|
set_idle_alarm(&gen.idle_alarm_active,
|
2020-09-23 16:58:30 +02:00
|
|
|
XCB_SYNC_TESTTYPE_NEGATIVE_COMPARISON, minus_one);
|
2020-09-23 14:12:58 +02:00
|
|
|
} else if (ev->alarm == gen.idle_alarm_active) {
|
|
|
|
gen.current_idle = false;
|
|
|
|
push_event();
|
|
|
|
|
|
|
|
set_idle_alarm(&gen.idle_alarm_inactive,
|
2020-09-23 16:58:30 +02:00
|
|
|
XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON, gen.idle_timeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-25 07:20:49 +02:00
|
|
|
static void
|
2020-09-23 16:58:30 +02:00
|
|
|
process_x11_event(xcb_generic_event_t *ev)
|
|
|
|
{
|
|
|
|
int event_code = ev->response_type & 0x7f;
|
|
|
|
if (!event_code) {
|
|
|
|
xcb_generic_error_t *err = (xcb_generic_error_t *) ev;
|
|
|
|
// TODO: report the error
|
|
|
|
} else if (event_code == XCB_PROPERTY_NOTIFY) {
|
|
|
|
on_x_property_notify((const xcb_property_notify_event_t *) ev);
|
|
|
|
} else if (event_code == gen.sync->first_event + XCB_SYNC_ALARM_NOTIFY) {
|
|
|
|
on_x_alarm_notify((const xcb_sync_alarm_notify_event_t *) ev);
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
on_x_ready(G_GNUC_UNUSED gpointer user_data)
|
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_generic_event_t *event;
|
|
|
|
while ((event = xcb_poll_for_event(gen.X))) {
|
|
|
|
process_x11_event(event);
|
|
|
|
free(event);
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
2020-09-23 16:58:30 +02:00
|
|
|
(void) xcb_flush(gen.X);
|
|
|
|
// TODO: some form of error handling, this just silently stops working
|
|
|
|
return xcb_connection_has_error(gen.X) == 0;
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2020-09-23 16:22:52 +02:00
|
|
|
generator_thread(void)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
GIOChannel *channel = g_io_channel_unix_new(xcb_get_file_descriptor(gen.X));
|
2020-09-23 14:12:58 +02:00
|
|
|
GSource *watch = g_io_create_watch(channel, G_IO_IN);
|
|
|
|
g_source_set_callback(watch, on_x_ready, NULL, NULL);
|
|
|
|
|
|
|
|
GMainLoop *loop =
|
|
|
|
g_main_loop_new(g_main_context_get_thread_default(), false);
|
|
|
|
g_source_attach(watch, g_main_loop_get_context(loop));
|
|
|
|
g_main_loop_run(loop);
|
|
|
|
}
|
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
static void
|
|
|
|
generator_init(void)
|
2020-09-23 14:12:58 +02:00
|
|
|
{
|
2020-09-23 16:58:30 +02:00
|
|
|
int which_screen = -1, xcb_error;
|
|
|
|
gen.X = xcb_connect(NULL, &which_screen);
|
|
|
|
if ((xcb_error = xcb_connection_has_error(gen.X)))
|
|
|
|
exit_fatal("cannot open display (code %d)", xcb_error);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
if (!(gen.atom_net_active_window = intern_atom("_NET_ACTIVE_WINDOW")) ||
|
|
|
|
!(gen.atom_net_wm_name = intern_atom("_NET_WM_NAME")) ||
|
|
|
|
!(gen.atom_utf8_string = intern_atom("UTF8_STRING")) ||
|
|
|
|
!(gen.atom_compound_text = intern_atom("COMPOUND_TEXT")))
|
|
|
|
exit_fatal("unable to resolve atoms");
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
gen.sync = xcb_get_extension_data(gen.X, &xcb_sync_id);
|
|
|
|
if (!gen.sync->present)
|
|
|
|
exit_fatal("missing Sync extension");
|
|
|
|
|
|
|
|
xcb_generic_error_t *err = NULL;
|
|
|
|
xcb_sync_initialize_cookie_t sic = xcb_sync_initialize(gen.X,
|
|
|
|
XCB_SYNC_MAJOR_VERSION, XCB_SYNC_MINOR_VERSION);
|
|
|
|
xcb_sync_initialize_reply_t *sir =
|
|
|
|
xcb_sync_initialize_reply(gen.X, sic, &err);
|
|
|
|
if (!sir)
|
|
|
|
exit_fatal("failed to initialise Sync extension");
|
|
|
|
free(sir);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
// The idle counter is not guaranteed to exist, only SERVERTIME is
|
|
|
|
if (!(gen.idle_counter = get_counter("IDLETIME")))
|
|
|
|
exit_fatal("idle counter is missing");
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
const xcb_setup_t *setup = xcb_get_setup(gen.X);
|
|
|
|
xcb_screen_iterator_t setup_iter = xcb_setup_roots_iterator(setup);
|
|
|
|
while (which_screen--)
|
|
|
|
xcb_screen_next(&setup_iter);
|
|
|
|
|
|
|
|
gen.screen = setup_iter.data;
|
|
|
|
xcb_window_t root = gen.screen->root;
|
|
|
|
|
|
|
|
const uint32_t values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
|
|
|
|
(void) xcb_change_window_attributes(gen.X, root, XCB_CW_EVENT_MASK, values);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
GKeyFile *kf = g_key_file_new();
|
|
|
|
gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL);
|
|
|
|
GStrv dirs = get_xdg_config_dirs();
|
|
|
|
|
|
|
|
int timeout = 600;
|
|
|
|
if (g_key_file_load_from_dirs(kf,
|
|
|
|
subpath, (const gchar **) dirs, NULL, 0, NULL)) {
|
|
|
|
guint64 n = g_key_file_get_uint64(kf, "Settings", "idle_timeout", NULL);
|
|
|
|
if (n > 0 && n <= G_MAXINT / 1000)
|
|
|
|
timeout = n;
|
|
|
|
}
|
|
|
|
|
|
|
|
g_strfreev(dirs);
|
|
|
|
g_free(subpath);
|
|
|
|
g_key_file_free(kf);
|
|
|
|
|
2020-10-02 01:55:46 +02:00
|
|
|
// Write a start marker so that we can reliably detect interruptions
|
|
|
|
struct event *event = g_slice_new0(struct event);
|
|
|
|
event->timestamp = -1;
|
|
|
|
g_async_queue_push(g.queue, event);
|
|
|
|
|
|
|
|
update_current_window();
|
|
|
|
|
2020-09-23 16:58:30 +02:00
|
|
|
gint64 timeout_ms = timeout * 1000;
|
|
|
|
gen.idle_timeout.hi = timeout_ms >> 32;
|
|
|
|
gen.idle_timeout.lo = timeout_ms;
|
2020-09-23 14:12:58 +02:00
|
|
|
set_idle_alarm(&gen.idle_alarm_inactive,
|
2020-09-23 16:58:30 +02:00
|
|
|
XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON, gen.idle_timeout);
|
2020-10-02 01:32:19 +02:00
|
|
|
|
|
|
|
(void) xcb_flush(gen.X);
|
|
|
|
// TODO: how are XCB errors handled? What if the last xcb_flush() fails?
|
2020-09-23 16:22:52 +02:00
|
|
|
}
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
static void
|
|
|
|
generator_launch(void)
|
|
|
|
{
|
|
|
|
gen.thread =
|
|
|
|
g_thread_new("generator", (GThreadFunc) generator_thread, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
generator_cleanup(void)
|
|
|
|
{
|
|
|
|
g_thread_join(gen.thread);
|
|
|
|
free(gen.current_title);
|
2020-09-23 16:58:30 +02:00
|
|
|
xcb_disconnect(gen.X);
|
2020-09-23 16:22:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// --- Main --------------------------------------------------------------------
|
|
|
|
|
2020-09-25 06:45:27 +02:00
|
|
|
static gboolean
|
|
|
|
on_queue_incoming(G_GNUC_UNUSED gpointer user_data)
|
|
|
|
{
|
2020-09-25 07:20:49 +02:00
|
|
|
int rc = 0;
|
|
|
|
char *errmsg = NULL;
|
|
|
|
if ((rc = sqlite3_exec(g.db, "BEGIN", NULL, NULL, &errmsg))) {
|
|
|
|
g_printerr("DB BEGIN error: %s\n", errmsg);
|
|
|
|
free(errmsg);
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: there should ideally be a limit to how many things can end up
|
|
|
|
// in a transaction at once (the amount of dequeues here)
|
2020-09-25 06:45:27 +02:00
|
|
|
struct event *event = NULL;
|
|
|
|
while ((event = g_async_queue_try_pop(g.queue))) {
|
2020-10-02 01:31:27 +02:00
|
|
|
printf("Event: ts: %ld, title: %s, class: %s, idle: %d\n",
|
|
|
|
event->timestamp, event->title ?: "(none)",
|
|
|
|
event->class ?: "(none)", event->idle);
|
2020-09-25 07:20:49 +02:00
|
|
|
|
|
|
|
if ((rc = sqlite3_bind_int64(g.add_event, 1, event->timestamp)) ||
|
|
|
|
(rc = sqlite3_bind_text(g.add_event, 2, event->title, -1,
|
|
|
|
SQLITE_STATIC)) ||
|
2020-10-02 01:31:27 +02:00
|
|
|
(rc = sqlite3_bind_text(g.add_event, 3, event->class, -1,
|
|
|
|
SQLITE_STATIC)) ||
|
|
|
|
(rc = sqlite3_bind_int(g.add_event, 4, event->idle)))
|
2020-09-25 07:20:49 +02:00
|
|
|
g_printerr("DB bind error: %s\n", sqlite3_errmsg(g.db));
|
|
|
|
|
|
|
|
if (((rc = sqlite3_step(g.add_event)) && rc != SQLITE_DONE) ||
|
|
|
|
(rc = sqlite3_reset(g.add_event)))
|
|
|
|
g_printerr("DB write error: %s\n", sqlite3_errmsg(g.db));
|
|
|
|
|
2020-09-25 06:45:27 +02:00
|
|
|
event_free(event);
|
|
|
|
}
|
2020-09-25 07:20:49 +02:00
|
|
|
if ((rc = sqlite3_exec(g.db, "COMMIT", NULL, NULL, &errmsg))) {
|
|
|
|
g_printerr("DB commit error: %s\n", errmsg);
|
|
|
|
free(errmsg);
|
|
|
|
}
|
2020-09-25 06:45:27 +02:00
|
|
|
return G_SOURCE_CONTINUE;
|
|
|
|
}
|
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
static sqlite3 *
|
|
|
|
database_init(const gchar *db_path)
|
|
|
|
{
|
|
|
|
sqlite3 *db = NULL;
|
|
|
|
int rc = sqlite3_open(db_path, &db);
|
|
|
|
if (rc != SQLITE_OK)
|
|
|
|
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
|
|
|
|
|
|
|
|
// This shouldn't normally happen but external applications may decide
|
|
|
|
// to read things out, and mess with us. When that takes too long, we may
|
|
|
|
// a/ wait for it to finish, b/ start with a whole-database lock or, even
|
|
|
|
// more simply, c/ crash on the BUSY error.
|
|
|
|
sqlite3_busy_timeout(db, 1000);
|
|
|
|
|
|
|
|
char *errmsg = NULL;
|
|
|
|
if ((rc = sqlite3_exec(db, "BEGIN", NULL, NULL, &errmsg)))
|
|
|
|
exit_fatal("%s: %s", db_path, errmsg);
|
|
|
|
|
|
|
|
sqlite3_stmt *stmt = NULL;
|
|
|
|
if ((rc = sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &stmt, NULL)))
|
|
|
|
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
|
|
|
|
if ((rc = sqlite3_step(stmt)) != SQLITE_ROW ||
|
|
|
|
sqlite3_data_count(stmt) != 1)
|
|
|
|
exit_fatal("%s: %s", db_path, "cannot retrieve user version");
|
|
|
|
|
|
|
|
int user_version = sqlite3_column_int(stmt, 0);
|
|
|
|
if ((rc = sqlite3_finalize(stmt)))
|
|
|
|
exit_fatal("%s: %s", db_path, "cannot retrieve user version");
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
if (user_version == 0) {
|
|
|
|
if ((rc = sqlite3_exec(db, "CREATE TABLE events ("
|
|
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
|
|
"timestamp INTEGER, "
|
|
|
|
"title TEXT, "
|
2020-10-02 01:31:27 +02:00
|
|
|
"class TEXT, "
|
2020-09-23 16:22:52 +02:00
|
|
|
"idle BOOLEAN)", NULL, NULL, &errmsg)))
|
|
|
|
exit_fatal("%s: %s", db_path, errmsg);
|
|
|
|
if ((rc = sqlite3_exec(db, "PRAGMA user_version = 1", NULL, NULL,
|
|
|
|
&errmsg)))
|
|
|
|
exit_fatal("%s: %s", db_path, errmsg);
|
|
|
|
} else if (user_version != 1) {
|
|
|
|
exit_fatal("%s: unsupported DB version: %d", db_path, user_version);
|
|
|
|
}
|
|
|
|
if ((rc = sqlite3_exec(db, "COMMIT", NULL, NULL, &errmsg)))
|
|
|
|
exit_fatal("%s: %s", db_path, errmsg);
|
2020-09-25 07:20:49 +02:00
|
|
|
|
|
|
|
if ((rc = sqlite3_prepare_v2(db,
|
2020-10-02 01:31:27 +02:00
|
|
|
"INSERT INTO events (timestamp, title, class, idle) "
|
|
|
|
"VALUES (?, ?, ?, ?)", -1, &g.add_event, NULL)))
|
2020-09-25 07:20:49 +02:00
|
|
|
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
|
2020-09-23 16:22:52 +02:00
|
|
|
return db;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
socket_init(const gchar *socket_path)
|
|
|
|
{
|
2020-09-23 14:12:58 +02:00
|
|
|
// TODO: try exclusivity/invocation either via DBus directly,
|
|
|
|
// or via GApplication or GtkApplication:
|
|
|
|
// - GtkApplication calls Gtk.init automatically during "startup" signal,
|
|
|
|
// Gtk.init doesn't get command line args
|
|
|
|
// - "inhibiting" makes no sense, it can't be used for mere delays
|
|
|
|
// - actually, the "query-end" signal
|
|
|
|
// - should check whether it tries to exit cleanly
|
|
|
|
// - what is the session manager, do I have it?
|
|
|
|
// - "register-session" looks useful
|
|
|
|
// - GTK+ keeps the application running as long as it has windows,
|
|
|
|
// though I want to keep it running forever
|
|
|
|
// - g_application_hold(), perhaps
|
|
|
|
// - so maybe just use GApplication, that will provide more control
|
|
|
|
|
|
|
|
struct flock lock =
|
|
|
|
{
|
|
|
|
.l_type = F_WRLCK,
|
|
|
|
.l_start = 0,
|
|
|
|
.l_whence = SEEK_SET,
|
|
|
|
.l_len = 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
gchar *lock_path = g_strdup_printf("%s.lock", socket_path);
|
|
|
|
int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644);
|
|
|
|
if (fcntl(lock_fd, F_SETLK, &lock))
|
|
|
|
exit_fatal("failed to acquire lock: %s", strerror(errno));
|
|
|
|
unlink(socket_path);
|
2020-09-23 16:22:52 +02:00
|
|
|
g_free(lock_path);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
|
|
if (socket_fd < 0)
|
|
|
|
exit_fatal("%s: %s", socket_path, strerror(errno));
|
|
|
|
|
|
|
|
struct sockaddr_un sun;
|
|
|
|
sun.sun_family = AF_UNIX;
|
|
|
|
strncpy(sun.sun_path, socket_path, sizeof sun.sun_path);
|
|
|
|
if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun))
|
|
|
|
exit_fatal("%s: %s", socket_path, strerror(errno));
|
|
|
|
if (listen(socket_fd, 10))
|
|
|
|
exit_fatal("%s: %s", socket_path, strerror(errno));
|
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
// lock_fd stays open as long as the application is running--leak
|
|
|
|
return socket_fd;
|
|
|
|
}
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
int
|
|
|
|
main(int argc, char *argv[])
|
|
|
|
{
|
|
|
|
if (!setlocale(LC_CTYPE, ""))
|
|
|
|
exit_fatal("cannot set locale");
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
gboolean show_version = false;
|
|
|
|
const GOptionEntry options[] = {
|
|
|
|
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
|
|
|
|
&show_version, "output version information and exit", NULL},
|
|
|
|
{},
|
|
|
|
};
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
GError *error = NULL;
|
|
|
|
if (!gtk_init_with_args(&argc, &argv, " - activity tracker",
|
|
|
|
options, NULL, &error))
|
|
|
|
exit_fatal("%s", error->message);
|
|
|
|
if (show_version) {
|
|
|
|
printf(PROJECT_NAME " " PROJECT_VERSION "\n");
|
|
|
|
return 0;
|
2020-09-23 14:12:58 +02:00
|
|
|
}
|
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
g.queue = g_async_queue_new_full((GDestroyNotify) event_free);
|
|
|
|
generator_init();
|
|
|
|
|
|
|
|
gchar *data_path =
|
|
|
|
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
|
|
|
|
g_mkdir_with_parents(data_path, 0755);
|
|
|
|
|
2020-10-26 23:08:28 +01:00
|
|
|
// Bind to a control socket, also ensuring only one instance is running.
|
|
|
|
// We're intentionally not using XDG_RUNTIME_DIR so that what is effectively
|
|
|
|
// the database lock is right next to the database.
|
2020-09-23 16:22:52 +02:00
|
|
|
gchar *socket_path = g_build_filename(data_path, "socket", NULL);
|
|
|
|
int socket_fd = socket_init(socket_path);
|
2020-09-23 14:12:58 +02:00
|
|
|
g_free(socket_path);
|
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL);
|
|
|
|
g_free(data_path);
|
|
|
|
g.db = database_init(db_path);
|
|
|
|
g_free(db_path);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-25 06:45:27 +02:00
|
|
|
GSource *queue_source = async_queue_source_new();
|
|
|
|
g_source_set_callback(queue_source, on_queue_incoming, NULL, NULL);
|
|
|
|
g_source_attach(queue_source, g_main_context_default());
|
2020-09-23 14:12:58 +02:00
|
|
|
|
|
|
|
// TODO: listen for connections on the control socket
|
2020-09-25 07:20:49 +02:00
|
|
|
// - just show/map the window when anything connects at all
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
WdmtgWindow *window = wdmtg_window_new_with_db(g.db);
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
generator_launch();
|
2020-09-23 14:12:58 +02:00
|
|
|
gtk_main();
|
2020-09-23 16:22:52 +02:00
|
|
|
generator_cleanup();
|
2020-09-23 14:12:58 +02:00
|
|
|
|
2020-09-23 16:22:52 +02:00
|
|
|
sqlite3_close(g.db);
|
|
|
|
close(socket_fd);
|
2020-09-23 14:12:58 +02:00
|
|
|
return 0;
|
|
|
|
}
|