Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection and enhanced agent documentation framework. VPN Configuration (PST-NW-VPN): - Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS - Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24) - Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment - Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2 - Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic - Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes) - Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper - vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts - OpenVPN config files (Windows-compatible, abandoned for L2TP) Key VPN Implementation Details: - L2TP creates PPP adapter with connection name as interface description - UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24 - Split-tunnel enabled (only remote traffic through VPN) - All-user connection for pre-login auto-connect via scheduled task - Authentication: CHAP + MSChapv2 for UniFi compatibility Agent Documentation: - AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents - documentation-squire.md: Documentation and task management specialist agent - Updated all agent markdown files with standardized formatting Project Organization: - Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs) - Cleaned up old session JSONL files from projects/msp-tools/ - Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows) - Added guru-rmm server components and deployment configs Technical Notes: - VPN IP pool: 192.168.4.x (client gets 192.168.4.6) - Remote network: 192.168.0.0/24 (router at 192.168.0.10) - PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7 - Credentials: pst-admin / 24Hearts$ Files: 15 VPN scripts, 2 agent docs, conversation log reorganization, guru-connect/guru-rmm infrastructure additions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
159
projects/msp-tools/guru-connect/agent/src/capture/display.rs
Normal file
159
projects/msp-tools/guru-connect/agent/src/capture/display.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
//! Display enumeration and information
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Information about a display/monitor
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Display {
|
||||
/// Unique display ID
|
||||
pub id: u32,
|
||||
|
||||
/// Display name (e.g., "\\\\.\\DISPLAY1")
|
||||
pub name: String,
|
||||
|
||||
/// X position in virtual screen coordinates
|
||||
pub x: i32,
|
||||
|
||||
/// Y position in virtual screen coordinates
|
||||
pub y: i32,
|
||||
|
||||
/// Width in pixels
|
||||
pub width: u32,
|
||||
|
||||
/// Height in pixels
|
||||
pub height: u32,
|
||||
|
||||
/// Whether this is the primary display
|
||||
pub is_primary: bool,
|
||||
|
||||
/// Platform-specific handle (HMONITOR on Windows)
|
||||
#[cfg(windows)]
|
||||
pub handle: isize,
|
||||
}
|
||||
|
||||
/// Display info for protocol messages
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DisplayInfo {
|
||||
pub displays: Vec<Display>,
|
||||
pub primary_id: u32,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
/// Total pixels in the display
|
||||
pub fn pixel_count(&self) -> u32 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
/// Bytes needed for BGRA frame buffer
|
||||
pub fn buffer_size(&self) -> usize {
|
||||
(self.width * self.height * 4) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate all connected displays
|
||||
#[cfg(windows)]
|
||||
pub fn enumerate_displays() -> Result<Vec<Display>> {
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITORINFOEXW,
|
||||
};
|
||||
use windows::Win32::Foundation::{BOOL, LPARAM, RECT};
|
||||
use std::mem;
|
||||
|
||||
let mut displays = Vec::new();
|
||||
let mut display_id = 0u32;
|
||||
|
||||
// Callback for EnumDisplayMonitors
|
||||
unsafe extern "system" fn enum_callback(
|
||||
hmonitor: HMONITOR,
|
||||
_hdc: windows::Win32::Graphics::Gdi::HDC,
|
||||
_rect: *mut RECT,
|
||||
lparam: LPARAM,
|
||||
) -> BOOL {
|
||||
let displays = &mut *(lparam.0 as *mut Vec<(HMONITOR, u32)>);
|
||||
let id = displays.len() as u32;
|
||||
displays.push((hmonitor, id));
|
||||
BOOL(1) // Continue enumeration
|
||||
}
|
||||
|
||||
// Collect all monitor handles
|
||||
let mut monitors: Vec<(windows::Win32::Graphics::Gdi::HMONITOR, u32)> = Vec::new();
|
||||
unsafe {
|
||||
let result = EnumDisplayMonitors(
|
||||
None,
|
||||
None,
|
||||
Some(enum_callback),
|
||||
LPARAM(&mut monitors as *mut _ as isize),
|
||||
);
|
||||
if !result.as_bool() {
|
||||
anyhow::bail!("EnumDisplayMonitors failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Get detailed info for each monitor
|
||||
for (hmonitor, id) in monitors {
|
||||
let mut info: MONITORINFOEXW = unsafe { mem::zeroed() };
|
||||
info.monitorInfo.cbSize = mem::size_of::<MONITORINFOEXW>() as u32;
|
||||
|
||||
unsafe {
|
||||
if GetMonitorInfoW(hmonitor, &mut info.monitorInfo as *mut _ as *mut _).as_bool() {
|
||||
let rect = info.monitorInfo.rcMonitor;
|
||||
let name = String::from_utf16_lossy(
|
||||
&info.szDevice[..info.szDevice.iter().position(|&c| c == 0).unwrap_or(info.szDevice.len())]
|
||||
);
|
||||
|
||||
let is_primary = (info.monitorInfo.dwFlags & 1) != 0; // MONITORINFOF_PRIMARY
|
||||
|
||||
displays.push(Display {
|
||||
id,
|
||||
name,
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: (rect.right - rect.left) as u32,
|
||||
height: (rect.bottom - rect.top) as u32,
|
||||
is_primary,
|
||||
handle: hmonitor.0 as isize,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by position (left to right, top to bottom)
|
||||
displays.sort_by(|a, b| {
|
||||
if a.y != b.y {
|
||||
a.y.cmp(&b.y)
|
||||
} else {
|
||||
a.x.cmp(&b.x)
|
||||
}
|
||||
});
|
||||
|
||||
// Reassign IDs after sorting
|
||||
for (i, display) in displays.iter_mut().enumerate() {
|
||||
display.id = i as u32;
|
||||
}
|
||||
|
||||
if displays.is_empty() {
|
||||
anyhow::bail!("No displays found");
|
||||
}
|
||||
|
||||
Ok(displays)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn enumerate_displays() -> Result<Vec<Display>> {
|
||||
anyhow::bail!("Display enumeration only supported on Windows")
|
||||
}
|
||||
|
||||
/// Get display info for protocol
|
||||
pub fn get_display_info() -> Result<DisplayInfo> {
|
||||
let displays = enumerate_displays()?;
|
||||
let primary_id = displays
|
||||
.iter()
|
||||
.find(|d| d.is_primary)
|
||||
.map(|d| d.id)
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(DisplayInfo {
|
||||
displays,
|
||||
primary_id,
|
||||
})
|
||||
}
|
||||
326
projects/msp-tools/guru-connect/agent/src/capture/dxgi.rs
Normal file
326
projects/msp-tools/guru-connect/agent/src/capture/dxgi.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
//! DXGI Desktop Duplication screen capture
|
||||
//!
|
||||
//! Uses the Windows Desktop Duplication API (available on Windows 8+) for
|
||||
//! high-performance, low-latency screen capture with hardware acceleration.
|
||||
//!
|
||||
//! Reference: RustDesk's scrap library implementation
|
||||
|
||||
use super::{CapturedFrame, Capturer, DirtyRect, Display};
|
||||
use anyhow::{Context, Result};
|
||||
use std::ptr;
|
||||
use std::time::Instant;
|
||||
|
||||
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_UNKNOWN;
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
|
||||
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
|
||||
D3D11_USAGE_STAGING, D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_READ,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIOutput, IDXGIOutput1,
|
||||
IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_ACCESS_LOST,
|
||||
DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
|
||||
DXGI_RESOURCE_PRIORITY_MAXIMUM,
|
||||
};
|
||||
use windows::core::Interface;
|
||||
|
||||
/// DXGI Desktop Duplication capturer
|
||||
pub struct DxgiCapturer {
|
||||
display: Display,
|
||||
device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
duplication: IDXGIOutputDuplication,
|
||||
staging_texture: Option<ID3D11Texture2D>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
last_frame: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl DxgiCapturer {
|
||||
/// Create a new DXGI capturer for the specified display
|
||||
pub fn new(display: Display) -> Result<Self> {
|
||||
let (device, context, duplication, desc) = Self::create_duplication(&display)?;
|
||||
|
||||
Ok(Self {
|
||||
display,
|
||||
device,
|
||||
context,
|
||||
duplication,
|
||||
staging_texture: None,
|
||||
width: desc.ModeDesc.Width,
|
||||
height: desc.ModeDesc.Height,
|
||||
last_frame: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create D3D device and output duplication
|
||||
fn create_duplication(
|
||||
target_display: &Display,
|
||||
) -> Result<(ID3D11Device, ID3D11DeviceContext, IDXGIOutputDuplication, DXGI_OUTDUPL_DESC)> {
|
||||
unsafe {
|
||||
// Create DXGI factory
|
||||
let factory: IDXGIFactory1 = CreateDXGIFactory1()
|
||||
.context("Failed to create DXGI factory")?;
|
||||
|
||||
// Find the adapter and output for this display
|
||||
let (adapter, output) = Self::find_adapter_output(&factory, target_display)?;
|
||||
|
||||
// Create D3D11 device
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let mut context: Option<ID3D11DeviceContext> = None;
|
||||
|
||||
D3D11CreateDevice(
|
||||
&adapter,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
None,
|
||||
Default::default(),
|
||||
None,
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
Some(&mut context),
|
||||
)
|
||||
.context("Failed to create D3D11 device")?;
|
||||
|
||||
let device = device.context("D3D11 device is None")?;
|
||||
let context = context.context("D3D11 context is None")?;
|
||||
|
||||
// Get IDXGIOutput1 interface
|
||||
let output1: IDXGIOutput1 = output.cast()
|
||||
.context("Failed to get IDXGIOutput1 interface")?;
|
||||
|
||||
// Create output duplication
|
||||
let duplication = output1.DuplicateOutput(&device)
|
||||
.context("Failed to create output duplication")?;
|
||||
|
||||
// Get duplication description
|
||||
let desc = duplication.GetDesc();
|
||||
|
||||
tracing::info!(
|
||||
"Created DXGI duplication: {}x{}, display: {}",
|
||||
desc.ModeDesc.Width,
|
||||
desc.ModeDesc.Height,
|
||||
target_display.name
|
||||
);
|
||||
|
||||
Ok((device, context, duplication, desc))
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the adapter and output for the specified display
|
||||
fn find_adapter_output(
|
||||
factory: &IDXGIFactory1,
|
||||
display: &Display,
|
||||
) -> Result<(IDXGIAdapter1, IDXGIOutput)> {
|
||||
unsafe {
|
||||
let mut adapter_idx = 0u32;
|
||||
|
||||
loop {
|
||||
// Enumerate adapters
|
||||
let adapter: IDXGIAdapter1 = match factory.EnumAdapters1(adapter_idx) {
|
||||
Ok(a) => a,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let mut output_idx = 0u32;
|
||||
|
||||
loop {
|
||||
// Enumerate outputs for this adapter
|
||||
let output: IDXGIOutput = match adapter.EnumOutputs(output_idx) {
|
||||
Ok(o) => o,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// Check if this is the display we want
|
||||
let desc = output.GetDesc()?;
|
||||
|
||||
let name = String::from_utf16_lossy(
|
||||
&desc.DeviceName[..desc.DeviceName.iter().position(|&c| c == 0).unwrap_or(desc.DeviceName.len())]
|
||||
);
|
||||
|
||||
if name == display.name || desc.Monitor.0 as isize == display.handle {
|
||||
return Ok((adapter, output));
|
||||
}
|
||||
|
||||
output_idx += 1;
|
||||
}
|
||||
|
||||
adapter_idx += 1;
|
||||
}
|
||||
|
||||
// If we didn't find the specific display, use the first one
|
||||
let adapter: IDXGIAdapter1 = factory.EnumAdapters1(0)
|
||||
.context("No adapters found")?;
|
||||
let output: IDXGIOutput = adapter.EnumOutputs(0)
|
||||
.context("No outputs found")?;
|
||||
|
||||
Ok((adapter, output))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create or get the staging texture for CPU access
|
||||
fn get_staging_texture(&mut self, src_texture: &ID3D11Texture2D) -> Result<&ID3D11Texture2D> {
|
||||
if self.staging_texture.is_none() {
|
||||
unsafe {
|
||||
let mut desc = D3D11_TEXTURE2D_DESC::default();
|
||||
src_texture.GetDesc(&mut desc);
|
||||
|
||||
desc.Usage = D3D11_USAGE_STAGING;
|
||||
desc.BindFlags = Default::default();
|
||||
desc.CPUAccessFlags = 0x20000; // D3D11_CPU_ACCESS_READ
|
||||
desc.MiscFlags = Default::default();
|
||||
|
||||
let mut staging: Option<ID3D11Texture2D> = None;
|
||||
self.device.CreateTexture2D(&desc, None, Some(&mut staging))
|
||||
.context("Failed to create staging texture")?;
|
||||
|
||||
let staging = staging.context("Staging texture is None")?;
|
||||
|
||||
// Set high priority
|
||||
let resource: IDXGIResource = staging.cast()?;
|
||||
resource.SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM)?;
|
||||
|
||||
self.staging_texture = Some(staging);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.staging_texture.as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// Acquire the next frame from the desktop
|
||||
fn acquire_frame(&mut self, timeout_ms: u32) -> Result<Option<(ID3D11Texture2D, DXGI_OUTDUPL_FRAME_INFO)>> {
|
||||
unsafe {
|
||||
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
|
||||
let mut desktop_resource: Option<IDXGIResource> = None;
|
||||
|
||||
let result = self.duplication.AcquireNextFrame(
|
||||
timeout_ms,
|
||||
&mut frame_info,
|
||||
&mut desktop_resource,
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let resource = desktop_resource.context("Desktop resource is None")?;
|
||||
|
||||
// Check if there's actually a new frame
|
||||
if frame_info.LastPresentTime == 0 {
|
||||
self.duplication.ReleaseFrame().ok();
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let texture: ID3D11Texture2D = resource.cast()
|
||||
.context("Failed to cast to ID3D11Texture2D")?;
|
||||
|
||||
Ok(Some((texture, frame_info)))
|
||||
}
|
||||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
||||
// No new frame available
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
|
||||
// Desktop duplication was invalidated, need to recreate
|
||||
tracing::warn!("Desktop duplication access lost, will need to recreate");
|
||||
Err(anyhow::anyhow!("Access lost"))
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e).context("Failed to acquire frame")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy frame data to CPU-accessible memory
|
||||
fn copy_frame_data(&mut self, texture: &ID3D11Texture2D) -> Result<Vec<u8>> {
|
||||
unsafe {
|
||||
// Get or create staging texture
|
||||
let staging = self.get_staging_texture(texture)?.clone();
|
||||
|
||||
// Copy from GPU texture to staging texture
|
||||
self.context.CopyResource(&staging, texture);
|
||||
|
||||
// Map the staging texture for CPU read
|
||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||
self.context
|
||||
.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
|
||||
.context("Failed to map staging texture")?;
|
||||
|
||||
// Copy pixel data
|
||||
let src_pitch = mapped.RowPitch as usize;
|
||||
let dst_pitch = (self.width * 4) as usize;
|
||||
let height = self.height as usize;
|
||||
|
||||
let mut data = vec![0u8; dst_pitch * height];
|
||||
|
||||
let src_ptr = mapped.pData as *const u8;
|
||||
for y in 0..height {
|
||||
let src_row = src_ptr.add(y * src_pitch);
|
||||
let dst_row = data.as_mut_ptr().add(y * dst_pitch);
|
||||
ptr::copy_nonoverlapping(src_row, dst_row, dst_pitch);
|
||||
}
|
||||
|
||||
// Unmap
|
||||
self.context.Unmap(&staging, 0);
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract dirty rectangles from frame info
|
||||
fn extract_dirty_rects(&self, _frame_info: &DXGI_OUTDUPL_FRAME_INFO) -> Option<Vec<DirtyRect>> {
|
||||
// TODO: Implement dirty rectangle extraction using
|
||||
// IDXGIOutputDuplication::GetFrameDirtyRects and GetFrameMoveRects
|
||||
// For now, return None to indicate full frame update
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Capturer for DxgiCapturer {
|
||||
fn capture(&mut self) -> Result<Option<CapturedFrame>> {
|
||||
// Try to acquire a frame with 100ms timeout
|
||||
let frame_result = self.acquire_frame(100)?;
|
||||
|
||||
let (texture, frame_info) = match frame_result {
|
||||
Some((t, f)) => (t, f),
|
||||
None => return Ok(None), // No new frame
|
||||
};
|
||||
|
||||
// Copy frame data to CPU memory
|
||||
let data = self.copy_frame_data(&texture)?;
|
||||
|
||||
// Release the frame
|
||||
unsafe {
|
||||
self.duplication.ReleaseFrame().ok();
|
||||
}
|
||||
|
||||
// Extract dirty rectangles if available
|
||||
let dirty_rects = self.extract_dirty_rects(&frame_info);
|
||||
|
||||
Ok(Some(CapturedFrame {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
data,
|
||||
timestamp: Instant::now(),
|
||||
display_id: self.display.id,
|
||||
dirty_rects,
|
||||
}))
|
||||
}
|
||||
|
||||
fn display(&self) -> &Display {
|
||||
&self.display
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
// Could check if duplication is still valid
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DxgiCapturer {
|
||||
fn drop(&mut self) {
|
||||
// Release any held frame
|
||||
unsafe {
|
||||
self.duplication.ReleaseFrame().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
148
projects/msp-tools/guru-connect/agent/src/capture/gdi.rs
Normal file
148
projects/msp-tools/guru-connect/agent/src/capture/gdi.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
//! GDI screen capture fallback
|
||||
//!
|
||||
//! Uses Windows GDI (Graphics Device Interface) for screen capture.
|
||||
//! Slower than DXGI but works on older systems and edge cases.
|
||||
|
||||
use super::{CapturedFrame, Capturer, Display};
|
||||
use anyhow::Result;
|
||||
use std::time::Instant;
|
||||
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject,
|
||||
GetDIBits, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
|
||||
SRCCOPY, GetDC, ReleaseDC,
|
||||
};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
/// GDI-based screen capturer
|
||||
pub struct GdiCapturer {
|
||||
display: Display,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl GdiCapturer {
|
||||
/// Create a new GDI capturer for the specified display
|
||||
pub fn new(display: Display) -> Result<Self> {
|
||||
Ok(Self {
|
||||
width: display.width,
|
||||
height: display.height,
|
||||
display,
|
||||
})
|
||||
}
|
||||
|
||||
/// Capture the screen using GDI
|
||||
fn capture_gdi(&self) -> Result<Vec<u8>> {
|
||||
unsafe {
|
||||
// Get device context for the entire screen
|
||||
let screen_dc = GetDC(HWND::default());
|
||||
if screen_dc.is_invalid() {
|
||||
anyhow::bail!("Failed to get screen DC");
|
||||
}
|
||||
|
||||
// Create compatible DC and bitmap
|
||||
let mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if mem_dc.is_invalid() {
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
anyhow::bail!("Failed to create compatible DC");
|
||||
}
|
||||
|
||||
let bitmap = CreateCompatibleBitmap(screen_dc, self.width as i32, self.height as i32);
|
||||
if bitmap.is_invalid() {
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
anyhow::bail!("Failed to create compatible bitmap");
|
||||
}
|
||||
|
||||
// Select bitmap into memory DC
|
||||
let old_bitmap = SelectObject(mem_dc, bitmap);
|
||||
|
||||
// Copy screen to memory DC
|
||||
if let Err(e) = BitBlt(
|
||||
mem_dc,
|
||||
0,
|
||||
0,
|
||||
self.width as i32,
|
||||
self.height as i32,
|
||||
screen_dc,
|
||||
self.display.x,
|
||||
self.display.y,
|
||||
SRCCOPY,
|
||||
) {
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(bitmap);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
anyhow::bail!("BitBlt failed: {}", e);
|
||||
}
|
||||
|
||||
// Prepare bitmap info for GetDIBits
|
||||
let mut bmi = BITMAPINFO {
|
||||
bmiHeader: BITMAPINFOHEADER {
|
||||
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
|
||||
biWidth: self.width as i32,
|
||||
biHeight: -(self.height as i32), // Negative for top-down
|
||||
biPlanes: 1,
|
||||
biBitCount: 32,
|
||||
biCompression: BI_RGB.0,
|
||||
biSizeImage: 0,
|
||||
biXPelsPerMeter: 0,
|
||||
biYPelsPerMeter: 0,
|
||||
biClrUsed: 0,
|
||||
biClrImportant: 0,
|
||||
},
|
||||
bmiColors: [Default::default()],
|
||||
};
|
||||
|
||||
// Allocate buffer for pixel data
|
||||
let buffer_size = (self.width * self.height * 4) as usize;
|
||||
let mut data = vec![0u8; buffer_size];
|
||||
|
||||
// Get the bits
|
||||
let lines = GetDIBits(
|
||||
mem_dc,
|
||||
bitmap,
|
||||
0,
|
||||
self.height,
|
||||
Some(data.as_mut_ptr() as *mut _),
|
||||
&mut bmi,
|
||||
DIB_RGB_COLORS,
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(bitmap);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(HWND::default(), screen_dc);
|
||||
|
||||
if lines == 0 {
|
||||
anyhow::bail!("GetDIBits failed");
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Capturer for GdiCapturer {
|
||||
fn capture(&mut self) -> Result<Option<CapturedFrame>> {
|
||||
let data = self.capture_gdi()?;
|
||||
|
||||
Ok(Some(CapturedFrame {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
data,
|
||||
timestamp: Instant::now(),
|
||||
display_id: self.display.id,
|
||||
dirty_rects: None, // GDI doesn't provide dirty rects
|
||||
}))
|
||||
}
|
||||
|
||||
fn display(&self) -> &Display {
|
||||
&self.display
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
102
projects/msp-tools/guru-connect/agent/src/capture/mod.rs
Normal file
102
projects/msp-tools/guru-connect/agent/src/capture/mod.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! Screen capture module
|
||||
//!
|
||||
//! Provides DXGI Desktop Duplication for high-performance screen capture on Windows 8+,
|
||||
//! with GDI fallback for legacy systems or edge cases.
|
||||
|
||||
#[cfg(windows)]
|
||||
mod dxgi;
|
||||
#[cfg(windows)]
|
||||
mod gdi;
|
||||
mod display;
|
||||
|
||||
pub use display::{Display, DisplayInfo};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Captured frame data
|
||||
#[derive(Debug)]
|
||||
pub struct CapturedFrame {
|
||||
/// Frame width in pixels
|
||||
pub width: u32,
|
||||
|
||||
/// Frame height in pixels
|
||||
pub height: u32,
|
||||
|
||||
/// Raw BGRA pixel data (4 bytes per pixel)
|
||||
pub data: Vec<u8>,
|
||||
|
||||
/// Timestamp when frame was captured
|
||||
pub timestamp: Instant,
|
||||
|
||||
/// Display ID this frame is from
|
||||
pub display_id: u32,
|
||||
|
||||
/// Regions that changed since last frame (if available)
|
||||
pub dirty_rects: Option<Vec<DirtyRect>>,
|
||||
}
|
||||
|
||||
/// Rectangular region that changed
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DirtyRect {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
/// Screen capturer trait
|
||||
pub trait Capturer: Send {
|
||||
/// Capture the next frame
|
||||
///
|
||||
/// Returns None if no new frame is available (screen unchanged)
|
||||
fn capture(&mut self) -> Result<Option<CapturedFrame>>;
|
||||
|
||||
/// Get the current display info
|
||||
fn display(&self) -> &Display;
|
||||
|
||||
/// Check if capturer is still valid (display may have changed)
|
||||
fn is_valid(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Create a capturer for the specified display
|
||||
#[cfg(windows)]
|
||||
pub fn create_capturer(display: Display, use_dxgi: bool, gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
|
||||
if use_dxgi {
|
||||
match dxgi::DxgiCapturer::new(display.clone()) {
|
||||
Ok(capturer) => {
|
||||
tracing::info!("Using DXGI Desktop Duplication for capture");
|
||||
return Ok(Box::new(capturer));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("DXGI capture failed: {}, trying fallback", e);
|
||||
if !gdi_fallback {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GDI fallback
|
||||
tracing::info!("Using GDI for capture");
|
||||
Ok(Box::new(gdi::GdiCapturer::new(display)?))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn create_capturer(_display: Display, _use_dxgi: bool, _gdi_fallback: bool) -> Result<Box<dyn Capturer>> {
|
||||
anyhow::bail!("Screen capture only supported on Windows")
|
||||
}
|
||||
|
||||
/// Get all available displays
|
||||
pub fn enumerate_displays() -> Result<Vec<Display>> {
|
||||
display::enumerate_displays()
|
||||
}
|
||||
|
||||
/// Get the primary display
|
||||
pub fn primary_display() -> Result<Display> {
|
||||
let displays = enumerate_displays()?;
|
||||
displays
|
||||
.into_iter()
|
||||
.find(|d| d.is_primary)
|
||||
.ok_or_else(|| anyhow::anyhow!("No primary display found"))
|
||||
}
|
||||
Reference in New Issue
Block a user