I am trying to write a script that can record and reproduce, a user input. Technically in the current state of the code its a keylogger.
I am using [DllImport("user32.dll")]
to solve the handling of keyboard and mouse inputs. Everything is working as expected and pretty nicely but I have a weird behaviour that I cannot wrap my head around.
In short how the program works, at init a timer is started that checks ocassionally if you pressed the StartRecordingKey
. In case you did press it a stopwatch is started which enables precisely timed and fast input logging.
The source code:
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Timers;
namespace MouseMoveTracker
{
public partial class Form1 : Form
{
Stopwatch stopwatch = new Stopwatch();
long previousElapsed = 0;
bool isMainTimerRunning = false;
System.Timers.Timer ControlKeyCheckTimer;
DateTime _mainTimerStartTime = DateTime.MinValue;
public static Keys StartRecordingKey = Keys.F;
public static Keys ReplayActionsKey = Keys.R;
const double RecordingInterval = 2;
private Dictionary<string, bool> KeyDownList = new();
private List<MouseMover> MouseMovers = new();
private List<ITimedKey> TimedKeys = new();
int currentIteration = 0;
int replayIteration = -1;
bool tracking = false;
public Form1()
{
InitializeComponent();
ClearKeyDownList();
ControlKeyCheckTimer = new(1000);
ControlKeyCheckTimer.Elapsed = ControlKeyCheckTimer_Elapsed;
ControlKeyCheckTimer.Start();
}
private void ClearKeyDownList()
{
foreach (Keys key in Enum.GetValues(typeof(Keys)))
{
var dictKey = key.ToString();
if (!KeyDownList.ContainsKey(dictKey))
KeyDownList.Add(dictKey, false);
}
}
private void ControlKeyCheckTimer_Elapsed(object? sender, ElapsedEventArgs e)
{
HandleKeyEvents(StartRecordingKey);
HandleKeyEvents(ReplayActionsKey);
}
[DllImport("user32.dll")]
static extern bool GetAsyncKeyState(Keys vKey);
// Import the GetCursorPos function from user32.dll
[DllImport("user32.dll")]
static extern bool GetCursorPos(out Point lpPoint);
[DllImport("user32.dll")]
static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);
private void StopBackgroundKeyCheckerThread()
{
isMainTimerRunning = false;
}
private void CheckIfKeyPressed(bool test = true)
{
if (!tracking)
return;
foreach (Keys key in Enum.GetValues(typeof(Keys)))
{
HandleKeyEvents(key, test);
}
}
private void HandleKeyEvents(Keys key, bool allowTimedKeyAppend = false)
{
var dictKey = key.ToString();
if (GetAsyncKeyState(key))
{
HandleKeyDown(key, dictKey, allowTimedKeyAppend);
}
else if (!GetAsyncKeyState(key))
{
HandleKeyUp(key, dictKey, allowTimedKeyAppend);
}
}
private void HandleKeyUp(Keys key, string dictKey, bool allowTimedKeyAppend)
{
if (KeyDownList[dictKey])
{
if (tracking && key != StartRecordingKey && allowTimedKeyAppend)
{
LogInputData(key, currentIteration, true, GetElapsedTime());
//if(key != Keys.LButton)
TimedKeys.Add(ITimedKey.Create(key, currentIteration, true));
}
KeyDownList[dictKey] = false;
KeyPressed(key);
}
}
private void HandleKeyDown(Keys key, string dictKey, bool allowTimedKeyAppend)
{
if (!KeyDownList[dictKey])
{
if (tracking && key != StartRecordingKey && allowTimedKeyAppend)
{
LogInputData(key, currentIteration, false, GetElapsedTime());
//if (key != Keys.LButton)
TimedKeys.Add(ITimedKey.Create(key, currentIteration, false));
}
KeyDownList[dictKey] = true;
}
}
private void KeyPressed(Keys key)
{
if (key == StartRecordingKey)
{
LogInputData(key, 6999, true, GetElapsedTime());
FlipState();
}
else if (key == ReplayActionsKey)
{
if (!tracking)
{
replayIteration = 0;
StartMainExecutor();
}
}
}
private void StartMainExecutor()
{
isMainTimerRunning = true;
stopwatch.Start();
Task task = new Task(MainKeylogger);
task.Start();
}
private void StopMainExecutor()
{
stopwatch.Stop();
isMainTimerRunning = false;
ClearKeyDownList();
}
private void MainKeylogger()
{
_mainTimerStartTime = DateTime.Now;
//CheckIfKeyPressed(false);
//CheckIfKeyPressed(false);
while (isMainTimerRunning)
{
var numberOfTicks = stopwatch.ElapsedMilliseconds - previousElapsed;
if (numberOfTicks >= RecordingInterval)
{
previousElapsed = stopwatch.ElapsedMilliseconds;
CheckIfKeyPressed();
}
Thread.Sleep(1);
}
}
private void FlipState()
{
tracking = !tracking;
if (tracking)
{
MouseMovers.Clear();
TimedKeys.Clear();
currentIteration = 0;
StartMainExecutor();
Invoke(new Action(() =>
{
this.BackColor = Color.Green;
ClearLogs();
}));
}
else
{
StopMainExecutor();
Invoke(new Action(() =>
{
this.BackColor = Color.White;
}));
}
}
private void FinishReplay()
{
replayIteration = -1;
StopMainExecutor();
}
private void ReplayActions()
{
foreach (var timedKey in TimedKeys)
{
if (timedKey.Iteration > replayIteration)
break;
else if (timedKey.Iteration == replayIteration)
{
LogOutputData(timedKey.Key, timedKey.Iteration, timedKey.IsKeyUpEvent, GetElapsedTime());
timedKey.Execute();
}
}
}
private double GetElapsedTime()
{
return (DateTime.Now - _mainTimerStartTime).TotalMilliseconds;
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
StopBackgroundKeyCheckerThread();
}
private void LogInputData(Keys key, int iteration, bool isKeyUpEvent, double elapsedTime)
{
string tmp = "DOWN";
if (isKeyUpEvent)
tmp = "UP";
Invoke(new Action(() =>
{
listBox1.Items.Add($"Key: {key}, it: {iteration}, dir: {tmp}, elapsedT: {elapsedTime}");
}));
}
private void LogOutputData(Keys key, int iteration, bool isKeyUpEvent, double elapsedTime)
{
string tmp = "DOWN";
if (isKeyUpEvent)
tmp = "UP";
Invoke(new Action(() =>
{
listBox2.Items.Add($"Key: {key}, it: {iteration}, dir: {tmp}, elapsedT: {elapsedTime}");
}));
}
private void ClearLogs()
{
listBox1.Items.Clear();
listBox2.Items.Clear();
}
}
}
Expected behaviour is that I press the record button ( "F" ) then the recording starts lets say I type "test" then it will be logged.
The isssue is that if I press a button lets say "U K" then after pressing record (after already letting go U and K regardless of for how long ago I let go) GetAsyncKeyState at start claims that both U and K were pressed literally just after I pressed the record button.
What causes the issue is this part of the code
private void HandleKeyEvents(Keys key, bool allowTimedKeyAppend = false)
{
var dictKey = key.ToString();
if (GetAsyncKeyState(key))
{
HandleKeyDown(key, dictKey, allowTimedKeyAppend);
}
else if (!GetAsyncKeyState(key))
{
HandleKeyUp(key, dictKey, allowTimedKeyAppend);
}
}
Where GetAsyncKeyState will be for a second true ( indicating that the button was pressed ) then false indicating that the button was released.
I found a workaround during the investigation of this issue, which is to try to force check all keys TWICE which this part of the code would do
_mainTimerStartTime = DateTime.Now;
CheckIfKeyPressed(false); //resetting the state
CheckIfKeyPressed(false); // twice
while (isMainTimerRunning)
{
var numberOfTicks = stopwatch.ElapsedMilliseconds - previousElapsed;
if (numberOfTicks >= RecordingInterval)
{
previousElapsed = stopwatch.ElapsedMilliseconds;
ExecuteActionsOnTick();
CheckIfKeyPressed();
}
Thread.Sleep(1);
}
Its a very weird bug, I don't understand why would it even happen. What I tried:
- Increasing the time on the ControlKeyChecker
ControlKeyCheckTimer = new(1000);
- Slowing down the stopwatch check ( assumed its some overload issue but this had no effect whatsoever)
- By checking the values if they are indeed correct and they are true and false for a second ( which made me realise if you watch it it does not occure)
What is probably an important notice that every button always get fired when I start the log literally milliseconds after the recording started.
CodePudding user response:
The return value of GetAsyncKeyState
is not a simple boolean value.
Documentation says:
If the function succeeds, the return value specifies whether the key was pressed since the last call to GetAsyncKeyState, and whether the key is currently up or down. If the most significant bit is set, the key is down, and if the least significant bit is set, the key was pressed after the previous call to GetAsyncKeyState.
You see the previously pressed keys as down because no other process had in the meantime called GetAsyncKeyState
. Since you can't control what other processes call GetAsyncKeyState
at what time, you should ignore all but the most significant bit of the return value.
Change the definition of the function:
[DllImport("user32.dll")]
static extern short GetAsyncKeyState(Keys vKey);
And then only check the most significant bit of the return value:
if (GetAsyncKeyState(key) < 0)
{
HandleKeyDown(key, dictKey, allowTimedKeyAppend);
}
else
{
HandleKeyUp(key, dictKey, allowTimedKeyAppend);
}
Note that in a signed short, checking for negative values checks for the most significant bit in the short.