/* libUIOHook: Cross-platform keyboard and mouse hooking from userland.
* Copyright (C) 2006-2023 Alexander Barker. All Rights Reserved.
* https://github.com/kwhat/libuiohook/
*
* libUIOHook is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* libUIOHook is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef _WIN32
#include
#else
#if defined(__APPLE__) && defined(__MACH__)
#include
#endif
#include
#endif
// Native thread errors.
#define UIOHOOK_ERROR_THREAD_CREATE 0x10
// Thread and mutex variables.
#ifdef _WIN32
static HANDLE hook_thread;
static CRITICAL_SECTION hook_running_mutex;
static CRITICAL_SECTION hook_control_mutex;
static CONDITION_VARIABLE hook_control_cond;
#else
static pthread_t hook_thread;
static pthread_mutex_t hook_running_mutex;
static pthread_mutex_t hook_control_mutex;
static pthread_cond_t hook_control_cond;
#endif
bool logger_proc(unsigned int level, const char *format, ...) {
bool status = false;
va_list args;
switch (level) {
case LOG_LEVEL_INFO:
va_start(args, format);
status = vfprintf(stdout, format, args) >= 0;
va_end(args);
break;
case LOG_LEVEL_WARN:
case LOG_LEVEL_ERROR:
va_start(args, format);
status = vfprintf(stderr, format, args) >= 0;
va_end(args);
break;
}
return status;
}
// NOTE: The following callback executes on the same thread that hook_run() is called
// from. This is important because hook_run() attaches to the operating systems
// event dispatcher and may delay event delivery to the target application.
// Furthermore, some operating systems may choose to disable your hook if it
// takes too long to process. If you need to do any extended processing, please
// do so by copying the event to your own queued dispatch thread.
void dispatch_proc(uiohook_event * const event) {
char buffer[256] = { 0 };
size_t length = snprintf(buffer, sizeof(buffer),
"id=%i,when=%" PRIu64 ",mask=0x%X",
event->type, event->time, event->mask);
switch (event->type) {
case EVENT_HOOK_ENABLED:
// Lock the running mutex so we know if the hook is enabled.
#ifdef _WIN32
EnterCriticalSection(&hook_running_mutex);
#else
pthread_mutex_lock(&hook_running_mutex);
#endif
// Unlock the control mutex so hook_enable() can continue.
#ifdef _WIN32
WakeConditionVariable(&hook_control_cond);
LeaveCriticalSection(&hook_control_mutex);
#else
pthread_cond_signal(&hook_control_cond);
pthread_mutex_unlock(&hook_control_mutex);
#endif
break;
case EVENT_HOOK_DISABLED:
// Lock the control mutex until we exit.
#ifdef _WIN32
EnterCriticalSection(&hook_control_mutex);
#else
pthread_mutex_lock(&hook_control_mutex);
#endif
// Unlock the running mutex so we know if the hook is disabled.
#ifdef _WIN32
LeaveCriticalSection(&hook_running_mutex);
#else
#if defined(__APPLE__) && defined(__MACH__)
// Stop the main runloop so that this program ends.
CFRunLoopStop(CFRunLoopGetMain());
#endif
pthread_mutex_unlock(&hook_running_mutex);
#endif
break;
case EVENT_KEY_PRESSED:
// If the escape key is pressed, naturally terminate the program.
if (event->data.keyboard.keycode == VC_ESCAPE) {
int status = hook_stop();
switch (status) {
case UIOHOOK_SUCCESS:
// Everything is ok.
break;
// System level errors.
case UIOHOOK_ERROR_OUT_OF_MEMORY:
logger_proc(LOG_LEVEL_ERROR, "Failed to allocate memory. (%#X)", status);
break;
case UIOHOOK_ERROR_X_RECORD_GET_CONTEXT:
// NOTE This is the only platform specific error that occurs on hook_stop().
logger_proc(LOG_LEVEL_ERROR, "Failed to get XRecord context. (%#X)", status);
break;
// Default error.
case UIOHOOK_FAILURE:
default:
logger_proc(LOG_LEVEL_ERROR, "An unknown hook error occurred. (%#X)", status);
break;
}
}
case EVENT_KEY_RELEASED:
snprintf(buffer + length, sizeof(buffer) - length,
",keycode=%u,rawcode=0x%X",
event->data.keyboard.keycode, event->data.keyboard.rawcode);
break;
case EVENT_KEY_TYPED:
snprintf(buffer + length, sizeof(buffer) - length,
",keychar=%lc,rawcode=%u",
(wint_t) event->data.keyboard.keychar,
event->data.keyboard.rawcode);
break;
case EVENT_MOUSE_PRESSED:
case EVENT_MOUSE_RELEASED:
case EVENT_MOUSE_CLICKED:
case EVENT_MOUSE_MOVED:
case EVENT_MOUSE_DRAGGED:
snprintf(buffer + length, sizeof(buffer) - length,
",x=%i,y=%i,button=%i,clicks=%i",
event->data.mouse.x, event->data.mouse.y,
event->data.mouse.button, event->data.mouse.clicks);
break;
case EVENT_MOUSE_WHEEL:
snprintf(buffer + length, sizeof(buffer) - length,
",type=%i,amount=%i,rotation=%i",
event->data.wheel.type, event->data.wheel.amount,
event->data.wheel.rotation);
break;
default:
break;
}
fprintf(stdout, "%s\n", buffer);
}
#ifdef _WIN32
DWORD WINAPI hook_thread_proc(LPVOID arg) {
#else
void *hook_thread_proc(void *arg) {
#endif
// Set the hook status.
int status = hook_run();
if (status != UIOHOOK_SUCCESS) {
#ifdef _WIN32
*(DWORD *) arg = status;
#else
*(int *) arg = status;
#endif
}
// Make sure we signal that we have passed any exception throwing code for
// the waiting hook_enable().
#ifdef _WIN32
WakeConditionVariable(&hook_control_cond);
LeaveCriticalSection(&hook_control_mutex);
return status;
#else
// Make sure we signal that we have passed any exception throwing code for
// the waiting hook_enable().
pthread_cond_signal(&hook_control_cond);
pthread_mutex_unlock(&hook_control_mutex);
return arg;
#endif
}
int hook_enable() {
// Lock the thread control mutex. This will be unlocked when the
// thread has finished starting, or when it has fully stopped.
#ifdef _WIN32
EnterCriticalSection(&hook_control_mutex);
#else
pthread_mutex_lock(&hook_control_mutex);
#endif
// Set the initial status.
int status = UIOHOOK_FAILURE;
#ifndef _WIN32
// Create the thread attribute.
pthread_attr_t hook_thread_attr;
pthread_attr_init(&hook_thread_attr);
// Get the policy and priority for the thread attr.
int policy;
pthread_attr_getschedpolicy(&hook_thread_attr, &policy);
int priority = sched_get_priority_max(policy);
#endif
#if defined(_WIN32)
DWORD hook_thread_id;
DWORD *hook_thread_status = malloc(sizeof(DWORD));
hook_thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) hook_thread_proc, hook_thread_status, 0, &hook_thread_id);
if (hook_thread != INVALID_HANDLE_VALUE) {
#else
int *hook_thread_status = malloc(sizeof(int));
if (pthread_create(&hook_thread, &hook_thread_attr, hook_thread_proc, hook_thread_status) == 0) {
#endif
#if defined(_WIN32)
// Attempt to set the thread priority to time critical.
if (SetThreadPriority(hook_thread, THREAD_PRIORITY_TIME_CRITICAL) == 0) {
logger_proc(LOG_LEVEL_WARN, "%s [%u]: Could not set thread priority %li for thread %#p! (%#lX)\n",
__FUNCTION__, __LINE__, (long) THREAD_PRIORITY_TIME_CRITICAL,
hook_thread , (unsigned long) GetLastError());
}
#elif (defined(__APPLE__) && defined(__MACH__)) || _POSIX_C_SOURCE >= 200112L || defined(__FreeBSD__)
// Some POSIX revisions do not support pthread_setschedprio so we will
// use pthread_setschedparam instead.
struct sched_param param = { .sched_priority = priority };
if (pthread_setschedparam(hook_thread, SCHED_OTHER, ¶m) != 0) {
logger_proc(LOG_LEVEL_WARN, "%s [%u]: Could not set thread priority %i for thread 0x%lX!\n",
__FUNCTION__, __LINE__, priority, (unsigned long) hook_thread);
}
#else
// Raise the thread priority using glibc pthread_setschedprio.
if (pthread_setschedprio(hook_thread, priority) != 0) {
logger_proc(LOG_LEVEL_WARN, "%s [%u]: Could not set thread priority %i for thread 0x%lX!\n",
__FUNCTION__, __LINE__, priority, (unsigned long) hook_thread);
}
#endif
// Wait for the thread to indicate that it has passed the
// initialization portion by blocking until either a EVENT_HOOK_ENABLED
// event is received or the thread terminates.
// NOTE This unlocks the hook_control_mutex while we wait.
#ifdef _WIN32
SleepConditionVariableCS(&hook_control_cond, &hook_control_mutex, INFINITE);
#else
pthread_cond_wait(&hook_control_cond, &hook_control_mutex);
#endif
#ifdef _WIN32
if (TryEnterCriticalSection(&hook_running_mutex) != FALSE) {
#else
if (pthread_mutex_trylock(&hook_running_mutex) == 0) {
#endif
// Lock Successful; The hook is not running but the hook_control_cond
// was signaled! This indicates that there was a startup problem!
// Get the status back from the thread.
#ifdef _WIN32
WaitForSingleObject(hook_thread, INFINITE);
GetExitCodeThread(hook_thread, hook_thread_status);
#else
pthread_join(hook_thread, (void **) &hook_thread_status);
status = *hook_thread_status;
#endif
} else {
// Lock Failure; The hook is currently running and wait was signaled
// indicating that we have passed all possible start checks. We can
// always assume a successful startup at this point.
status = UIOHOOK_SUCCESS;
}
free(hook_thread_status);
logger_proc(LOG_LEVEL_DEBUG, "%s [%u]: Thread Result: (%#X).\n",
__FUNCTION__, __LINE__, status);
}
else {
status = UIOHOOK_ERROR_THREAD_CREATE;
}
// Make sure the control mutex is unlocked.
#ifdef _WIN32
LeaveCriticalSection(&hook_control_mutex);
#else
pthread_mutex_unlock(&hook_control_mutex);
#endif
return status;
}
int main() {
// Lock the thread control mutex. This will be unlocked when the
// thread has finished starting, or when it has fully stopped.
#ifdef _WIN32
// Create event handles for the thread hook.
InitializeCriticalSection(&hook_running_mutex);
InitializeCriticalSection(&hook_control_mutex);
InitializeConditionVariable(&hook_control_cond);
#else
pthread_mutex_init(&hook_running_mutex, NULL);
pthread_mutex_init(&hook_control_mutex, NULL);
pthread_cond_init(&hook_control_cond, NULL);
#endif
// Set the logger callback for library output.
hook_set_logger_proc(&logger_proc);
// Set the event callback for uiohook events.
hook_set_dispatch_proc(&dispatch_proc);
// Start the hook and block.
// NOTE If EVENT_HOOK_ENABLED was delivered, the status will always succeed.
int status = hook_enable();
switch (status) {
case UIOHOOK_SUCCESS:
// We no longer block, so we need to explicitly wait for the thread to die.
#ifdef _WIN32
WaitForSingleObject(hook_thread, INFINITE);
#else
#if defined(__APPLE__) && defined(__MACH__)
// NOTE Darwin requires that you start your own runloop from main.
CFRunLoopRun();
#endif
pthread_join(hook_thread, NULL);
#endif
break;
// System level errors.
case UIOHOOK_ERROR_OUT_OF_MEMORY:
logger_proc(LOG_LEVEL_ERROR, "Failed to allocate memory. (%#X)\n", status);
break;
// X11 specific errors.
case UIOHOOK_ERROR_X_OPEN_DISPLAY:
logger_proc(LOG_LEVEL_ERROR, "Failed to open X11 display. (%#X)\n", status);
break;
case UIOHOOK_ERROR_X_RECORD_NOT_FOUND:
logger_proc(LOG_LEVEL_ERROR, "Unable to locate XRecord extension. (%#X)\n", status);
break;
case UIOHOOK_ERROR_X_RECORD_ALLOC_RANGE:
logger_proc(LOG_LEVEL_ERROR, "Unable to allocate XRecord range. (%#X)\n", status);
break;
case UIOHOOK_ERROR_X_RECORD_CREATE_CONTEXT:
logger_proc(LOG_LEVEL_ERROR, "Unable to allocate XRecord context. (%#X)\n", status);
break;
case UIOHOOK_ERROR_X_RECORD_ENABLE_CONTEXT:
logger_proc(LOG_LEVEL_ERROR, "Failed to enable XRecord context. (%#X)\n", status);
break;
// Windows specific errors.
case UIOHOOK_ERROR_SET_WINDOWS_HOOK_EX:
logger_proc(LOG_LEVEL_ERROR, "Failed to register low level windows hook. (%#X)\n", status);
break;
// Darwin specific errors.
case UIOHOOK_ERROR_AXAPI_DISABLED:
logger_proc(LOG_LEVEL_ERROR, "Failed to enable access for assistive devices. (%#X)\n", status);
break;
case UIOHOOK_ERROR_CREATE_EVENT_PORT:
logger_proc(LOG_LEVEL_ERROR, "Failed to create apple event port. (%#X)\n", status);
break;
case UIOHOOK_ERROR_CREATE_RUN_LOOP_SOURCE:
logger_proc(LOG_LEVEL_ERROR, "Failed to create apple run loop source. (%#X)\n", status);
break;
case UIOHOOK_ERROR_GET_RUNLOOP:
logger_proc(LOG_LEVEL_ERROR, "Failed to acquire apple run loop. (%#X)\n", status);
break;
case UIOHOOK_ERROR_CREATE_OBSERVER:
logger_proc(LOG_LEVEL_ERROR, "Failed to create apple run loop observer. (%#X)\n", status);
break;
// Default error.
case UIOHOOK_FAILURE:
default:
logger_proc(LOG_LEVEL_ERROR, "An unknown hook error occurred. (%#X)\n", status);
break;
}
#ifdef _WIN32
// Create event handles for the thread hook.
CloseHandle(hook_thread);
DeleteCriticalSection(&hook_running_mutex);
DeleteCriticalSection(&hook_control_mutex);
#else
pthread_mutex_destroy(&hook_running_mutex);
pthread_mutex_destroy(&hook_control_mutex);
pthread_cond_destroy(&hook_control_cond);
#endif
return status;
}