You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
19 KiB
460 lines
19 KiB
#include <windows.h>
|
|
#include <psapi.h>
|
|
#include <deque>
|
|
|
|
#include "globals.hpp"
|
|
#include "sendInput.hpp"
|
|
|
|
using namespace std ;
|
|
|
|
typedef pair< DWORD , InterceptionMouseStroke > TimestampMouseStrokePair ;
|
|
typedef deque<TimestampMouseStrokePair> MouseStrokeHistory ;
|
|
typedef map< InterceptionDevice , MouseStrokeHistory > MouseStrokeHistoryTable ;
|
|
|
|
typedef map< Event::eEventType , size_t > EventTypeSizetMap ;
|
|
typedef map< InterceptionDevice , EventTypeSizetMap > EventCountTable ;
|
|
|
|
// --- LOCAL DATA ------------------------------------------------------
|
|
|
|
// local functions:
|
|
static void doRunMainLoop( int* pExitFlag ) ;
|
|
static bool detectMouseMove( const MouseStrokeHistory* pStrokeHistory , Event::eEventType* pEventType , int* pMagnitude ) ;
|
|
static bool findDevice( InterceptionDevice hDevice , const Device** ppDevice , const DeviceConfig** ppDeviceConfig ) ;
|
|
static const AppProfile* findAppProfile( const DeviceConfig* pDeviceConfig, const AppProfile** ppDefaultAppProfile ) ;
|
|
static bool isAbsolutePath( const string& path ) ;
|
|
static void getFilename( string* pPath ) ;
|
|
|
|
// --- LOCAL DATA ------------------------------------------------------
|
|
|
|
enum eStrokeType { stMouseMove , stMouseWheel , stMouseHorzWheel } ;
|
|
static EnumStringInfo gStrokeTypeStringTable[] = {
|
|
{ stMouseMove , "MOVE" } ,
|
|
{ stMouseWheel , "WHEEL" } ,
|
|
{ stMouseHorzWheel , "H-WHEEL" } ,
|
|
{ -1 , NULL }
|
|
} ;
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
void
|
|
runMainLoop( int* pExitFlag )
|
|
{
|
|
assert( pExitFlag != NULL ) ;
|
|
|
|
// initialize
|
|
*pExitFlag = 0 ;
|
|
assert( gpCallbackFn != NULL ) ;
|
|
(*gpCallbackFn)( CBTYPE_STARTED , "Main loop started." ) ;
|
|
|
|
// run the main loop
|
|
try
|
|
{
|
|
doRunMainLoop( pExitFlag ) ;
|
|
(*gpCallbackFn)( CBTYPE_STOPPED , "Main loop stopped." ) ;
|
|
}
|
|
catch ( exception& xcptn )
|
|
{
|
|
LOG_CMSG( "system" , "Main loop exception: " << xcptn ) ;
|
|
(*gpCallbackFn)( CBTYPE_FATAL_ERROR , MAKE_CSTRING(xcptn) ) ;
|
|
}
|
|
catch ( ... )
|
|
{
|
|
LOG_CMSG( "system" , "Main loop unknown exception." ) ;
|
|
(*gpCallbackFn)( CBTYPE_FATAL_ERROR , "Unknown error." ) ;
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
doRunMainLoop( int* pExitFlag )
|
|
{
|
|
assert( pExitFlag != NULL ) ;
|
|
|
|
// initialize
|
|
LOG_CMSG( "system" , "Main loop started." ) ;
|
|
SetPriorityClass( GetCurrentProcess() , HIGH_PRIORITY_CLASS ) ;
|
|
|
|
// configure Interception
|
|
LOG_CMSG( "system" , "Creating context..." ) ;
|
|
InterceptionContext hContext = interception_create_context() ;
|
|
if ( hContext == NULL )
|
|
throw runtime_error( "Can't create context." ) ;
|
|
LOG_CMSG( "system" , "- hContext = " << hContext ) ;
|
|
LOG_CMSG( "system" , "Setting mouse filter..." ) ;
|
|
interception_set_filter(
|
|
hContext ,
|
|
interception_is_mouse ,
|
|
INTERCEPTION_FILTER_MOUSE_MOVE | INTERCEPTION_FILTER_MOUSE_WHEEL | INTERCEPTION_FILTER_MOUSE_HWHEEL
|
|
) ;
|
|
|
|
// run the main loop
|
|
DWORD startTime = GetTickCount() ;
|
|
MouseStrokeHistoryTable mouseMovesHistoryTable ;
|
|
EventCountTable eventCounts ;
|
|
int nWaitFailures = 0 ;
|
|
for ( ; ; )
|
|
{
|
|
// wait for the next event
|
|
// NOTE: If the driver is not installed, this will return immediately :-/
|
|
InterceptionDevice hDevice = interception_wait_with_timeout( hContext , gEventWaitTimeout ) ;
|
|
if ( hDevice == NULL )
|
|
{
|
|
// check if we should exit
|
|
if ( *pExitFlag != 0 )
|
|
break ;
|
|
// FUDGE! interception_wait_with_timeout() doesn't distinguish between a timeout and a failure, so we try
|
|
// to figure out what's happened here (otherwise we get into a tight infinite loop :-/).
|
|
if ( mouseMovesHistoryTable.empty() && ++nWaitFailures > 10 && GetTickCount()-startTime < 500 )
|
|
throw runtime_error( "Can't connect to the mouse driver." ) ;
|
|
continue ;
|
|
}
|
|
InterceptionStroke stroke ;
|
|
int nStrokes = interception_receive( hContext , hDevice , &stroke , 1 ) ;
|
|
if ( nStrokes <= 0 )
|
|
break ;
|
|
if ( ! interception_is_mouse( hDevice ) )
|
|
continue ;
|
|
|
|
// get the event
|
|
InterceptionMouseStroke* pStroke = (InterceptionMouseStroke*) &stroke ;
|
|
DWORD strokeTimestamp = GetTickCount() ;
|
|
eStrokeType strokeType ;
|
|
if ( pStroke->state & INTERCEPTION_MOUSE_WHEEL )
|
|
strokeType = stMouseWheel ;
|
|
else if ( pStroke->state & INTERCEPTION_MOUSE_HWHEEL )
|
|
strokeType = stMouseHorzWheel ;
|
|
else
|
|
strokeType = stMouseMove ;
|
|
string strokeTypeString = enumString( gStrokeTypeStringTable , strokeType ) ;
|
|
KeyboardState strokeKeyboardState = KeyboardState::getCurrKeyboardState() ;
|
|
|
|
// log the raw event
|
|
if ( isLoggingEnabled( "rawEvents" ) )
|
|
{
|
|
string strokeKeyboardStateString ;
|
|
if ( strokeKeyboardState.isAnythingDown() )
|
|
strokeKeyboardStateString = MAKE_STRING( " " << strokeKeyboardState << " ;" ) ;
|
|
LOG_MSG(
|
|
strokeTypeString << ":" << strokeKeyboardStateString
|
|
<< " hDevice=" << hDevice
|
|
<< " ; state=0x" << hexString(pStroke->state)
|
|
<< " ; flags=0x" << hexString(pStroke->flags)
|
|
<< " ; rolling=" << pStroke->rolling
|
|
<< " ; x=" << pStroke->x
|
|
<< " ; y=" << pStroke->y
|
|
<< " ; info=" << pStroke->information
|
|
) ;
|
|
}
|
|
|
|
// find the device that generated the event
|
|
const Device* pDevice ;
|
|
const DeviceConfig* pDeviceConfig ;
|
|
if ( ! findDevice( hDevice , &pDevice , &pDeviceConfig ) )
|
|
{
|
|
// can't find the the device - check if we've seen it before
|
|
if ( gUnknownDevices.find( hDevice ) == gUnknownDevices.end() )
|
|
{
|
|
// nope - notify the front-end about the new device
|
|
// NOTE: We seem to get multiple hardware ID's back, but the first one seems to be the one we want :shrug:
|
|
WideCharVector buf( 4*1024 ) ;
|
|
size_t nChars = interception_get_hardware_id( hContext , hDevice , &buf[0] , buf.size()*sizeof(wchar_t) ) ;
|
|
(*gpCallbackFn)( CBTYPE_NEW_DEVICE , MAKE_CSTRING(hDevice << "|" << toUtf8(&buf[0],nChars)) ) ;
|
|
gUnknownDevices.insert( hDevice ) ;
|
|
}
|
|
|
|
// forward the event (for normal processing)
|
|
interception_send( hContext , hDevice ,&stroke , 1 ) ;
|
|
continue ;
|
|
}
|
|
|
|
// check if the device is enabled
|
|
if ( ! pDevice->isEnabled() )
|
|
{
|
|
// nope - just forward the event (for normal processing)
|
|
interception_send( hContext , hDevice ,&stroke , 1 ) ;
|
|
continue ;
|
|
}
|
|
|
|
// find the required AppProfile
|
|
const AppProfile* pDefaultAppProfile ;
|
|
const AppProfile* pAppProfile = findAppProfile( pDeviceConfig , &pDefaultAppProfile ) ;
|
|
if ( pAppProfile == NULL )
|
|
pAppProfile = pDefaultAppProfile ;
|
|
if ( pAppProfile == NULL )
|
|
{
|
|
// can't find one - just forward the event (for normal processing)
|
|
interception_send( hContext , hDevice ,&stroke , 1 ) ;
|
|
continue ;
|
|
}
|
|
|
|
// figure out what we should do
|
|
const Event* pEvent = NULL ;
|
|
void* pActionInfo = NULL ;
|
|
if ( strokeType == stMouseMove )
|
|
{
|
|
// record the stroke
|
|
MouseStrokeHistory* pStrokeHistory = & mouseMovesHistoryTable[ hDevice ] ;
|
|
int strokeHistoryResetInterval = pDeviceConfig->strokeHistoryResetInterval() ;
|
|
if ( strokeHistoryResetInterval <= 0 )
|
|
strokeHistoryResetInterval = gDefaultStrokeResetHistoryInterval ;
|
|
if ( ! pStrokeHistory->empty() && (int)(strokeTimestamp-pStrokeHistory->back().first) >= strokeHistoryResetInterval )
|
|
{
|
|
// we haven't seen a move event for a while - reset the history
|
|
pStrokeHistory->clear() ;
|
|
}
|
|
pStrokeHistory->push_back( make_pair( strokeTimestamp , *pStroke ) ) ;
|
|
while ( (int)pStrokeHistory->size() > gMaxStrokeHistory )
|
|
pStrokeHistory->pop_front() ;
|
|
// figure out which way the mouse is moving
|
|
Event::eEventType eventType ;
|
|
int magnitude ;
|
|
if ( detectMouseMove( pStrokeHistory , &eventType , &magnitude ) )
|
|
{
|
|
LOG_CMSG( "events" , strokeTypeString << ": " << eventType << "/" << magnitude )
|
|
// check if this event has been configured
|
|
pEvent = pAppProfile->findEvent( eventType , strokeKeyboardState ) ;
|
|
if ( pEvent == NULL && pAppProfile != pDefaultAppProfile && pAppProfile->fallbackToDefaultAppProfile() )
|
|
pEvent = pDefaultAppProfile->findEvent( eventType , strokeKeyboardState ) ;
|
|
// scale the movement (100 = 1 unit)
|
|
pActionInfo = (void*) (100 * magnitude) ;
|
|
}
|
|
}
|
|
else if ( strokeType == stMouseWheel )
|
|
{
|
|
// figure out which way the wheel is moving
|
|
int dirn = (pStroke->rolling) < 0 ? -1 : +1 ;
|
|
int wheelSize = abs( pStroke->rolling ) ;
|
|
LOG_CMSG( "events" , strokeTypeString << ": " << (dirn < 0?"down":"up") << "/" << wheelSize ) ;
|
|
// check if this event has been configured
|
|
Event::eEventType eventType = (dirn < 0) ? Event::etWheelDown : Event::etWheelUp ;
|
|
pEvent = pAppProfile->findEvent( eventType , strokeKeyboardState ) ;
|
|
if ( pEvent == NULL && pAppProfile != pDefaultAppProfile && pAppProfile->fallbackToDefaultAppProfile() )
|
|
pEvent = pDefaultAppProfile->findEvent( eventType , strokeKeyboardState ) ;
|
|
// scale the movement (100 = 1 unit)
|
|
pActionInfo = (void*) (100 * wheelSize / (WHEEL_DELTA/10)) ; // FIXME! scaling s.b. configurable
|
|
}
|
|
else if ( strokeType == stMouseHorzWheel )
|
|
{
|
|
// figure out which way the wheel is moving
|
|
int dirn = (pStroke->rolling) < 0 ? -1 : +1 ;
|
|
int wheelSize = abs( pStroke->rolling ) ;
|
|
LOG_CMSG( "events" , strokeTypeString << ": " << (dirn < 0?"left":"right") << "/" << wheelSize ) ;
|
|
// check if this event has been configured
|
|
Event::eEventType eventType = (dirn < 0) ? Event::etWheelLeft : Event::etWheelRight ;
|
|
pEvent = pAppProfile->findEvent( eventType , strokeKeyboardState ) ;
|
|
if ( pEvent == NULL && pAppProfile != pDefaultAppProfile && pAppProfile->fallbackToDefaultAppProfile() )
|
|
pEvent = pDefaultAppProfile->findEvent( eventType , strokeKeyboardState ) ;
|
|
// scale the movement (100 = 1 unit)
|
|
pActionInfo = (void*) (100 * wheelSize / (WHEEL_DELTA/10)) ; // FIXME! scaling s.b. configurable
|
|
}
|
|
else
|
|
assert( false ) ;
|
|
|
|
// check for sensitivity
|
|
if ( pEvent != NULL )
|
|
{
|
|
// nb: we reduce sensitivity by ignoring events
|
|
int sensitivity = pAppProfile->sensitivity( pEvent->eventType() ) ;
|
|
if ( sensitivity > 0 && (++eventCounts[hDevice][pEvent->eventType()] % sensitivity) != 0 )
|
|
continue ; // nb: we don't forward the event
|
|
}
|
|
|
|
// handle the event
|
|
if ( pEvent != NULL )
|
|
{
|
|
// NOTE: I tried managing the keyboard state here, so that we don't keep sending up/down events for Ctrl/Alt/Shift
|
|
// before/after every action, but it interferes with event detection, because the keyboard state is now wrong :-/
|
|
CSendInput sendInput ;
|
|
for ( ActionPtrVector::const_iterator it=pEvent->actions().begin() ; it != pEvent->actions().end() ; ++it )
|
|
{
|
|
const Action* pAction = *it ;
|
|
LOG_CMSG( "actions" , "ACTION: " << *pAction << " (" << dec << (int)pActionInfo << ")" ) ;
|
|
pAction->doAction( pActionInfo , &sendInput ) ;
|
|
}
|
|
continue ;
|
|
}
|
|
|
|
// the event wasn't handled - forward the event (for normal processing)
|
|
interception_send( hContext , hDevice , &stroke , 1 ) ;
|
|
}
|
|
|
|
// clean up
|
|
LOG_CMSG( "system" , "Destroying context..." ) ;
|
|
interception_destroy_context( hContext ) ;
|
|
|
|
LOG_CMSG( "system" , "Main loop ended." ) ;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
static bool
|
|
detectMouseMove( const MouseStrokeHistory* pStrokeHistory , Event::eEventType* pEventType , int* pMagnitude )
|
|
{
|
|
// check if we have enough stroke history
|
|
if ( (int)pStrokeHistory->size() < gDetectMouseMove_WindowSize )
|
|
return false ;
|
|
|
|
// figure out which direction the mouse is moving in
|
|
LOG_CMSG( "detectMouseMove" , "DETECT MOUSE MOVE: #=" << gDetectMouseMove_WindowSize << "/" << pStrokeHistory->size() ) ;
|
|
int cumX=0 , cumY=0 , nStrokes=0 ;
|
|
const InterceptionMouseStroke* pPrevStroke = NULL ;
|
|
for ( MouseStrokeHistory::const_reverse_iterator it=pStrokeHistory->rbegin() ; it != pStrokeHistory->rend() ; ++it )
|
|
{
|
|
const InterceptionMouseStroke& stroke = (*it).second ;
|
|
if ( (stroke.flags & MOUSE_MOVE_ABSOLUTE) )
|
|
{
|
|
// VMware sends us absolute mouse co-ordinates. The screen seems to be sized as 65,535x65,535 and this post
|
|
// seems to confirm that: http://stackoverflow.com/a/35141494
|
|
if ( pPrevStroke != NULL )
|
|
{
|
|
// FIXME! handle multiple monitors
|
|
int deltaX = pPrevStroke->x - stroke.x ;
|
|
cumX += deltaX * GetSystemMetrics(SM_CXSCREEN) / 0xFFFF ;
|
|
int deltaY = pPrevStroke->y - stroke.y ;
|
|
cumY += deltaY * GetSystemMetrics(SM_CYSCREEN) / 0xFFFF ;
|
|
LOG_CMSG( "detectMouseMove" , " x=" << stroke.x << " ; y=" << stroke.y << " ; cum=" << cumX << "/" << cumY ) ;
|
|
}
|
|
else
|
|
LOG_CMSG( "detectMouseMove" , " x=" << stroke.x << " ; y=" << stroke.y ) ;
|
|
pPrevStroke = &stroke ;
|
|
}
|
|
else
|
|
{
|
|
// nb: we assume MOUSE_MOVE_RELATIVE (which is defined as 0)
|
|
// FIXME! What is MOUSE_VIRTUAL_DESKTOP? Doco for SM_CXVIRTUALSCREEN implies that
|
|
// it's all the monitors merged into one big desktop.
|
|
cumX += stroke.x ;
|
|
cumY += stroke.y ;
|
|
LOG_CMSG( "detectMouseMove" , " x=" << stroke.x << " ; y=" << stroke.y << " ; cum=" << cumX << "/" << cumY ) ;
|
|
}
|
|
if ( ++nStrokes >= gDetectMouseMove_WindowSize )
|
|
break ;
|
|
}
|
|
// NOTE: It's easier to move a device up/down without drifting left/right, so we bias left/right detection.
|
|
int biasedCumX = (int)(1000*cumX * gDetectMouseMove_HorzBias/100) / 1000 ;
|
|
if ( abs(biasedCumX) >= abs(cumY) )
|
|
{
|
|
*pEventType = biasedCumX < 0 ? Event::etMouseLeft : Event::etMouseRight;
|
|
*pMagnitude = abs( biasedCumX / nStrokes ) ;
|
|
}
|
|
else
|
|
{
|
|
*pEventType = cumY < 0 ? Event::etMouseUp : Event::etMouseDown ;
|
|
*pMagnitude = abs( cumY / nStrokes ) ;
|
|
}
|
|
LOG_CMSG( "detectMouseMove" ,
|
|
" cumX=" << biasedCumX << "(" << cumX << ") ; cumY=" << cumY
|
|
<< " ; " << *pEventType << "/" << *pMagnitude
|
|
) ;
|
|
|
|
return true ;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
static bool
|
|
findDevice( InterceptionDevice hDevice , const Device** ppDevice , const DeviceConfig** ppDeviceConfig )
|
|
{
|
|
// find the specified device
|
|
for( DeviceTable::const_iterator it=gDeviceTable.begin() ; it != gDeviceTable.end() ; ++it )
|
|
{
|
|
const Device* pDevice = (*it).second ;
|
|
// FIXME! Windows seems to occasionally assign new device numbers at boot time, so this check won't work properly
|
|
// if that happens :-/ We should also check the HID (and maybe display name as well), since it's unlikely someone
|
|
// will have multiple identical meeces connected at the same time.
|
|
if ( pDevice->deviceNumber() == hDevice )
|
|
{
|
|
// found it - found the corresponding DeviceConfig
|
|
DeviceConfigTable::const_iterator it2 = gDeviceConfigTable.find( pDevice->deviceId() ) ;
|
|
if ( it2 != gDeviceConfigTable.end() )
|
|
{
|
|
*ppDevice = pDevice ;
|
|
*ppDeviceConfig = (*it2).second ;
|
|
return true ;
|
|
}
|
|
}
|
|
}
|
|
return false ;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
const AppProfile*
|
|
findAppProfile( const DeviceConfig* pDeviceConfig , const AppProfile** ppDefaultAppProfile )
|
|
{
|
|
// get the EXE that owns the foreground window
|
|
string processExeName ;
|
|
HWND hWnd = GetForegroundWindow() ;
|
|
if ( hWnd != NULL )
|
|
{
|
|
DWORD processId ;
|
|
DWORD rc = GetWindowThreadProcessId( hWnd , &processId ) ;
|
|
if ( rc )
|
|
{
|
|
HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION , FALSE , processId ) ;
|
|
if ( hProcess != NULL )
|
|
{
|
|
wchar_t buf[ MAX_PATH+1 ] ;
|
|
DWORD nChars = ARRAY_SIZE( buf ) ;
|
|
BOOL rc = QueryFullProcessImageName( hProcess , 0 , buf , &nChars ) ;
|
|
if ( rc )
|
|
processExeName = toUtf8( buf , nChars ) ;
|
|
CloseHandle( hProcess ) ;
|
|
}
|
|
}
|
|
}
|
|
|
|
// find the AppProfile that belongs to the EXE
|
|
const AppProfile* pAppProfileToReturn = NULL ;
|
|
const AppProfile* pDefaultAppProfile = NULL ;
|
|
for ( AppProfilePtrVector::const_iterator it=pDeviceConfig->appProfiles().begin() ; it != pDeviceConfig->appProfiles().end() ; ++it )
|
|
{
|
|
const AppProfile* pAppProfile = *it ;
|
|
// check if the AppProfile has an absolute path
|
|
if ( !pAppProfile->app().empty() && !isAbsolutePath(pAppProfile->app()) )
|
|
{
|
|
// nope - just compare the EXE filename (without its directory)
|
|
getFilename( &processExeName ) ;
|
|
}
|
|
// check if this is the AppProfile we want
|
|
if ( _stricmp( pAppProfile->app().c_str() , processExeName.c_str() ) == 0 )
|
|
pAppProfileToReturn = pAppProfile ;
|
|
// check if this is the default AppProfile
|
|
if ( pAppProfile->app().empty() )
|
|
pDefaultAppProfile = *it ;
|
|
}
|
|
|
|
*ppDefaultAppProfile = pDefaultAppProfile ;
|
|
return pAppProfileToReturn ;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static bool
|
|
isAbsolutePath( const string& path )
|
|
{
|
|
// check if the specified path is absolute
|
|
for ( string::const_iterator it=path.begin() ; it != path.end() ; ++it )
|
|
{
|
|
if ( *it == ':' || isSlash(*it) )
|
|
return true ;
|
|
}
|
|
return false ;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
static void
|
|
getFilename( string* pPath )
|
|
{
|
|
// extract the filename from the specified path
|
|
for ( int i=pPath->length()-1 ; i >= 0 ; --i )
|
|
{
|
|
if ( isSlash( (*pPath)[i] ) )
|
|
{
|
|
*pPath = pPath->c_str() + i+1 ;
|
|
break ;
|
|
}
|
|
}
|
|
}
|
|
|