al is a an activity logger for linux.

This commit is contained in:
2026-04-11 22:26:58 +01:00
commit 92231f823e
3 changed files with 319 additions and 0 deletions

296
al.c Normal file
View File

@@ -0,0 +1,296 @@
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <time.h>
#include <math.h>
#include <stdbool.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#define INPUT_BY_PATH_DIR "/dev/input/by-path"
typedef struct {
int fd;
char *path;
bool isMouse;
} InputDevice;
typedef struct {
pthread_mutex_t mutex;
double windowStartTime;
double nextWindowTime;
double windowDuration;
unsigned int windowClicks;
bool windowMouseActivity;
double windowFirstClickTime;
double windowLastClickTime;
double windowFirstMouseTime;
double windowLastMouseTime;
} ActivityState;
typedef struct {
const InputDevice *device;
ActivityState *state;
} DeviceThreadCtx;
static atomic_bool g_running = true;
static void onSignal(int signum) {
(void)signum;
atomic_store(&g_running, false);
}
static void freeDevices(InputDevice *devices, size_t count) {
if (!devices) return;
for (size_t i = 0; i < count; i++) {
if (devices[i].fd >= 0) close(devices[i].fd);
free(devices[i].path);
}
free(devices);
}
static int openInputDevicesByPath(InputDevice **outDevices, size_t *outCount) {
if (!outDevices || !outCount) return -1;
*outDevices = NULL;
*outCount = 0;
DIR *dir = opendir(INPUT_BY_PATH_DIR);
if (!dir) {
fprintf(stderr, "Failed to open %s: %s\n", INPUT_BY_PATH_DIR, strerror(errno));
return -1;
}
size_t cap = 16;
InputDevice *devices = calloc(cap, sizeof(*devices));
if (!devices) {
closedir(dir);
fprintf(stderr, "Out of memory\n");
return -1;
}
for (size_t i = 0; i < cap; i++) devices[i].fd = -1;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue;
// Most useful nodes are symlinks ending in "-event-*" (e.g. platform-...-event-kbd)
if (!strstr(ent->d_name, "event")) continue;
size_t fullLen = strlen(INPUT_BY_PATH_DIR) + 1 + strlen(ent->d_name) + 1;
char *fullPath = malloc(fullLen);
if (!fullPath) {
fprintf(stderr, "Out of memory\n");
continue;
}
int n = snprintf(fullPath, fullLen, "%s/%s", INPUT_BY_PATH_DIR, ent->d_name);
if (n < 0 || (size_t) n >= fullLen) {
free(fullPath);
continue;
}
struct stat st;
if (lstat(fullPath, &st) != 0) {
fprintf(stderr, "Skipping %s: %s\n", fullPath, strerror(errno));
free(fullPath);
continue;
}
if (!S_ISLNK(st.st_mode) && !S_ISCHR(st.st_mode) && !S_ISREG(st.st_mode)) {
free(fullPath);
continue;
}
int fd = open(fullPath, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "Failed to open %s: %s\n", fullPath, strerror(errno));
free(fullPath);
continue;
}
if (*outCount == cap) {
size_t newCap = cap * 2;
InputDevice *newDevices = realloc(devices, newCap * sizeof(*newDevices));
if (!newDevices) {
close(fd);
free(fullPath);
freeDevices(devices, *outCount);
closedir(dir);
fprintf(stderr, "Out of memory\n");
return -1;
}
devices = newDevices;
for (size_t i = cap; i < newCap; i++) {
devices[i].fd = -1;
devices[i].path = NULL;
}
cap = newCap;
}
devices[*outCount].fd = fd;
devices[*outCount].path = fullPath;
devices[*outCount].isMouse = (strstr(fullPath, "mouse") != NULL);
(*outCount)++;
}
closedir(dir);
if (*outCount == 0) {
free(devices);
fprintf(stderr, "No input devices opened from %s\n", INPUT_BY_PATH_DIR);
return -1;
}
*outDevices = devices;
return 0;
}
double getCurrentTime() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (double) ts.tv_sec + ts.tv_nsec / 1e9;
}
static void *watchDeviceThread(void *arg) {
DeviceThreadCtx *ctx = (DeviceThreadCtx *)arg;
struct input_event ev;
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
while (atomic_load(&g_running)) {
ssize_t r = read(ctx->device->fd, &ev, sizeof(ev));
if (r == (ssize_t) sizeof(ev)) {
pthread_mutex_lock(&ctx->state->mutex);
if (ctx->device->isMouse) {
ctx->state->windowMouseActivity = true;
ctx->state->windowLastMouseTime = getCurrentTime();
if (ctx->state->windowFirstMouseTime == 0) {
ctx->state->windowFirstMouseTime = ctx->state->windowLastMouseTime;
}
} else {
if (ev.type == EV_KEY && ev.value == 1) {
ctx->state->windowClicks++;
ctx->state->windowLastClickTime = getCurrentTime();
if (ctx->state->windowFirstClickTime == 0) {
ctx->state->windowFirstClickTime = ctx->state->windowLastClickTime;
}
}
}
pthread_mutex_unlock(&ctx->state->mutex);
continue;
}
if (r < 0 && errno == EINTR) continue;
// Device closed/removed or error
break;
}
return NULL;
}
int main(int argc, char *argv[]) {
if (argc < 1 || argc > 1) {
fprintf(stderr, "Usage: [sudo] %s\n", argv[0]);
return 1;
}
// Ensure running as root or with appropriate permissions
InputDevice *devices = NULL;
size_t deviceCount = 0;
if (openInputDevicesByPath(&devices, &deviceCount) != 0) {
fprintf(stderr, "Tip: try running with sudo or add yourself to the input group.\n");
return 1;
}
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = onSignal;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
ActivityState state;
pthread_mutex_init(&state.mutex, NULL);
state.windowDuration = 15.0;
double now = getCurrentTime();
state.windowStartTime = floor(now);
state.nextWindowTime = state.windowStartTime + state.windowDuration;
state.windowClicks = 0;
state.windowFirstClickTime = 0;
state.windowLastClickTime = 0;
state.windowFirstMouseTime = 0;
state.windowLastMouseTime = 0;
state.windowMouseActivity = false;
pthread_t *threads = calloc(deviceCount, sizeof(*threads));
DeviceThreadCtx *ctxs = calloc(deviceCount, sizeof(*ctxs));
if (!threads || !ctxs) {
fprintf(stderr, "Out of memory\n");
free(threads);
free(ctxs);
pthread_mutex_destroy(&state.mutex);
freeDevices(devices, deviceCount);
return 1;
}
for (size_t i = 0; i < deviceCount; i++) {
ctxs[i].device = &devices[i];
ctxs[i].state = &state;
int rc = pthread_create(&threads[i], NULL, watchDeviceThread, &ctxs[i]);
if (rc != 0) {
fprintf(stderr, "Failed to create thread for %s: %s\n", devices[i].path, strerror(rc));
atomic_store(&g_running, false);
deviceCount = i;
break;
}
}
fprintf(stderr, "Watching %zu input devices for activity (Ctrl-C to stop)...\n", deviceCount);
while (atomic_load(&g_running)) {
double nextTime;
pthread_mutex_lock(&state.mutex);
nextTime = state.nextWindowTime;
pthread_mutex_unlock(&state.mutex);
now = getCurrentTime();
double sleepSec = nextTime - now;
if (sleepSec > 0) {
struct timespec req;
req.tv_sec = (time_t)sleepSec;
req.tv_nsec = (long)((sleepSec - (double)req.tv_sec) * 1e9);
while (atomic_load(&g_running) && nanosleep(&req, &req) != 0 && errno == EINTR) {
// retry
}
}
now = getCurrentTime();
pthread_mutex_lock(&state.mutex);
if (now >= state.nextWindowTime) {
printf("%llu,%u,%u,%lf,%lf,%lf,%lf\n",
(unsigned long long)state.windowStartTime, state.windowClicks, state.windowMouseActivity ? 1 : 0,
state.windowFirstClickTime, state.windowLastClickTime,
state.windowFirstMouseTime, state.windowLastMouseTime);
fflush(stdout);
state.windowStartTime = floor(now);
state.nextWindowTime = state.windowStartTime + state.windowDuration;
state.windowClicks = 0;
state.windowFirstClickTime = 0;
state.windowLastClickTime = 0;
state.windowFirstMouseTime = 0;
state.windowLastMouseTime = 0;
state.windowMouseActivity = false;
}
pthread_mutex_unlock(&state.mutex);
}
for (size_t i = 0; i < deviceCount; i++) {
pthread_cancel(threads[i]);
}
for (size_t i = 0; i < deviceCount; i++) {
pthread_join(threads[i], NULL);
}
free(threads);
free(ctxs);
pthread_mutex_destroy(&state.mutex);
freeDevices(devices, deviceCount);
return 0;
}