al is a an activity logger for linux.
This commit is contained in:
296
al.c
Normal file
296
al.c
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user