wraith/internal/rdp/freerdp_windows.go
Vantz Stockwell b46c20b0d0
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m4s
feat: wire all remaining stubs — settings, SFTP, RDP, credentials, FreeRDP callbacks
Four-agent parallel deployment:

1. Settings persistence — all 5 settings wired to SettingsService.Set/Get,
   theme picker persists, update check calls real UpdateService, external
   links use Browser.OpenURL, SFTP file open/save calls real service,
   Quick Connect creates real connection + session, exit uses Wails quit

2. SSH key management — credential dropdown in ConnectionEditDialog,
   collapsible "Add New Credential" panel with password/SSH key modes,
   CredentialService proxied through WraithApp (vault-locked guard),
   new CreateSSHKeyCredential method for atomic key+credential creation

3. RDP frontend wiring — useRdp.ts calls real RDPGetFrame/SendMouse/
   SendKey/SendClipboard via Wails bindings, ConnectRDP on WraithApp
   resolves credentials and builds RDPConfig, session store handles
   RDP protocol, frame pipeline uses polling at 30fps

4. FreeRDP3 callback registration — PostConnect and BitmapUpdate callbacks
   via syscall.NewCallback, GDI mode for automatic frame decoding,
   freerdp_context_new() call added, settings/input/context pointers
   extracted from struct offsets, BGRA→RGBA channel swap in frame copy,
   event loop fixed to pass context not instance

11 files changed. Zero build errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 11:25:03 -04:00

716 lines
23 KiB
Go

//go:build windows
package rdp
import (
"fmt"
"sync"
"syscall"
"time"
"unsafe"
)
var (
libfreerdp = syscall.NewLazyDLL("libfreerdp3.dll")
libfreerdpClient = syscall.NewLazyDLL("libfreerdp-client3.dll")
// Instance lifecycle
procFreerdpNew = libfreerdp.NewProc("freerdp_new")
procFreerdpFree = libfreerdp.NewProc("freerdp_free")
procFreerdpConnect = libfreerdp.NewProc("freerdp_connect")
procFreerdpDisconnect = libfreerdp.NewProc("freerdp_disconnect")
// Context
procContextNew = libfreerdp.NewProc("freerdp_context_new")
// Settings
procSettingsSetString = libfreerdp.NewProc("freerdp_settings_set_string")
procSettingsSetUint32 = libfreerdp.NewProc("freerdp_settings_set_uint32")
procSettingsSetBool = libfreerdp.NewProc("freerdp_settings_set_bool")
// Input
procInputSendMouse = libfreerdp.NewProc("freerdp_input_send_mouse_event")
procInputSendKeyboard = libfreerdp.NewProc("freerdp_input_send_keyboard_event")
// Event loop — NOTE: takes rdpContext*, not freerdp*
procCheckEventHandles = libfreerdp.NewProc("freerdp_check_event_handles")
// GDI subsystem (libfreerdp3.dll exports these)
procGdiInit = libfreerdp.NewProc("gdi_init")
procGdiFree = libfreerdp.NewProc("gdi_free")
// Client helpers
procClientNew = libfreerdpClient.NewProc("freerdp_client_context_new")
procClientFree = libfreerdpClient.NewProc("freerdp_client_context_free")
)
// ============================================================================
// FreeRDP3 settings IDs
//
// Source: FreeRDP 3.10.3 — include/freerdp/settings_types_private.h
// Each ID is the ALIGN64 slot index from the rdp_settings struct.
// ============================================================================
const (
FreeRDP_ServerHostname = 20
FreeRDP_ServerPort = 21
FreeRDP_Username = 22
FreeRDP_Password = 23
FreeRDP_Domain = 24
FreeRDP_DesktopWidth = 1025
FreeRDP_DesktopHeight = 1026
FreeRDP_ColorDepth = 1027
FreeRDP_FullscreenMode = 1028
FreeRDP_SoftwareGdi = 1601 // BOOL — enable software GDI rendering
FreeRDP_IgnoreCertificate = 4556
FreeRDP_AuthenticationOnly = 4554
FreeRDP_NlaSecurity = 4560
FreeRDP_TlsSecurity = 4561
FreeRDP_RdpSecurity = 4562
)
// Keyboard event flags for FreeRDP input calls.
const (
KBD_FLAGS_EXTENDED = 0x0100
KBD_FLAGS_DOWN = 0x4000
KBD_FLAGS_RELEASE = 0x8000
)
// ============================================================================
// FreeRDP3 struct offsets — x86_64 / Windows amd64
//
// IMPORTANT: These offsets are specific to FreeRDP 3.10.3 compiled with
// WITH_FREERDP_DEPRECATED=OFF (the default, and what our CI uses).
//
// Every ALIGN64 slot is 8 bytes. The slot number from the FreeRDP headers
// gives the byte offset as: slot * 8.
//
// Source: FreeRDP 3.10.3 — include/freerdp/freerdp.h, include/freerdp/update.h
// ============================================================================
const (
// rdp_freerdp struct (ALIGN64 slots, 8 bytes each)
// slot 0: context (rdpContext*)
// slot 48: PreConnect callback (pConnectCallback)
// slot 49: PostConnect callback (pConnectCallback)
// slot 55: PostDisconnect callback (pPostDisconnect)
freerdpOffsetContext = 0 * 8 // rdpContext* — slot 0
freerdpOffsetPreConnect = 48 * 8 // pConnectCallback — slot 48
freerdpOffsetPostConnect = 49 * 8 // pConnectCallback — slot 49
freerdpOffsetPostDisconnect = 55 * 8 // pPostDisconnect — slot 55
// rdp_context struct (ALIGN64 slots, 8 bytes each)
// slot 33: gdi (rdpGdi*)
// slot 38: input (rdpInput*)
// slot 39: update (rdpUpdate*)
// slot 40: settings (rdpSettings*)
contextOffsetGdi = 33 * 8 // rdpGdi*
contextOffsetInput = 38 * 8 // rdpInput*
contextOffsetUpdate = 39 * 8 // rdpUpdate*
contextOffsetSettings = 40 * 8 // rdpSettings*
// rdp_update struct (ALIGN64 slots, 8 bytes each)
// slot 21: BitmapUpdate callback (pBitmapUpdate)
updateOffsetBitmapUpdate = 21 * 8
// rdpGdi struct (NOT ALIGN64 — regular C struct with natural alignment)
// On x86_64:
// offset 0: rdpContext* context (8 bytes)
// offset 8: INT32 width (4 bytes)
// offset 12: INT32 height (4 bytes)
// offset 16: UINT32 stride (4 bytes)
// offset 20: UINT32 dstFormat (4 bytes)
// offset 24: UINT32 cursor_x (4 bytes)
// offset 28: UINT32 cursor_y (4 bytes)
// offset 32: HGDI_DC hdc (8 bytes, pointer)
// offset 40: gdiBitmap* primary (8 bytes)
// offset 48: gdiBitmap* drawing (8 bytes)
// offset 56: UINT32 bitmap_size (4 bytes)
// offset 60: UINT32 bitmap_stride (4 bytes)
// offset 64: BYTE* primary_buffer (8 bytes)
gdiOffsetWidth = 8
gdiOffsetHeight = 12
gdiOffsetStride = 16
gdiOffsetPrimaryBuffer = 64
)
// Pixel format constants from FreeRDP3 codec/color.h.
// Formula: (bpp << 24) | (type << 16) | (a << 12) | (r << 8) | (g << 4) | b
// BGRA type = 4, RGBA type = 3
const (
PIXEL_FORMAT_BGRA32 = 0x20040888
)
// ============================================================================
// Instance-to-backend registry
//
// syscall.NewCallback produces a bare C function pointer — it cannot capture
// Go closures. We use a global map keyed by the freerdp instance pointer to
// recover the FreeRDPBackend from inside callbacks.
// ============================================================================
var (
instanceRegistry = make(map[uintptr]*FreeRDPBackend)
instanceRegistryMu sync.RWMutex
)
func registerInstance(instance uintptr, backend *FreeRDPBackend) {
instanceRegistryMu.Lock()
instanceRegistry[instance] = backend
instanceRegistryMu.Unlock()
}
func unregisterInstance(instance uintptr) {
instanceRegistryMu.Lock()
delete(instanceRegistry, instance)
instanceRegistryMu.Unlock()
}
func lookupInstance(instance uintptr) *FreeRDPBackend {
instanceRegistryMu.RLock()
b := instanceRegistry[instance]
instanceRegistryMu.RUnlock()
return b
}
// ============================================================================
// Unsafe pointer helpers
// ============================================================================
// readPtr reads a pointer-sized value at the given byte offset from base.
func readPtr(base uintptr, offsetBytes uintptr) uintptr {
return *(*uintptr)(unsafe.Pointer(base + offsetBytes))
}
// writePtr writes a pointer-sized value at the given byte offset from base.
func writePtr(base uintptr, offsetBytes uintptr, val uintptr) {
*(*uintptr)(unsafe.Pointer(base + offsetBytes)) = val
}
// readU32 reads a uint32 at the given byte offset from base.
func readU32(base uintptr, offsetBytes uintptr) uint32 {
return *(*uint32)(unsafe.Pointer(base + offsetBytes))
}
// ============================================================================
// Callbacks — registered via syscall.NewCallback (stdcall on Windows)
//
// FreeRDP callback signatures (from freerdp.h):
// typedef BOOL (*pConnectCallback)(freerdp* instance);
// typedef BOOL (*pBitmapUpdate)(rdpContext* context, const BITMAP_UPDATE* bitmap);
// ============================================================================
// postConnectCallback is the global PostConnect handler. FreeRDP calls this
// after the RDP connection is fully established. We use it to:
// 1. Initialize the GDI subsystem (software rendering into a memory buffer)
// 2. Extract the GDI primary_buffer pointer for frame capture
// 3. Register the BitmapUpdate callback for partial screen updates
var postConnectCallbackPtr = syscall.NewCallback(postConnectCallback)
func postConnectCallback(instance uintptr) uintptr {
backend := lookupInstance(instance)
if backend == nil {
return 0 // FALSE — unknown instance
}
// Read context from instance->context (slot 0).
context := readPtr(instance, freerdpOffsetContext)
if context == 0 {
return 0
}
// Initialize the GDI subsystem with BGRA32 pixel format.
// gdi_init(freerdp* instance, UINT32 format) -> BOOL
// This allocates the primary surface and registers internal paint callbacks.
ret, _, _ := procGdiInit.Call(instance, uintptr(PIXEL_FORMAT_BGRA32))
if ret == 0 {
return 0 // gdi_init failed
}
// Read the rdpGdi pointer from context->gdi (slot 33).
gdi := readPtr(context, contextOffsetGdi)
if gdi == 0 {
return 0
}
// Extract frame dimensions and primary buffer pointer from rdpGdi.
gdiWidth := readU32(gdi, gdiOffsetWidth)
gdiHeight := readU32(gdi, gdiOffsetHeight)
gdiStride := readU32(gdi, gdiOffsetStride)
primaryBuf := readPtr(gdi, gdiOffsetPrimaryBuffer)
if primaryBuf == 0 {
return 0
}
// Store the GDI surface info on the backend for frame reads.
backend.gdiPrimaryBuf = primaryBuf
backend.gdiWidth = int(gdiWidth)
backend.gdiHeight = int(gdiHeight)
backend.gdiStride = int(gdiStride)
// Re-create the pixel buffer if GDI negotiated a different resolution
// (the server may reject our requested size).
if int(gdiWidth) != backend.buffer.Width || int(gdiHeight) != backend.buffer.Height {
backend.buffer = NewPixelBuffer(int(gdiWidth), int(gdiHeight))
}
// Register the BitmapUpdate callback on update->BitmapUpdate (slot 21).
// With GDI mode enabled, FreeRDP's internal GDI layer handles most
// rendering via BeginPaint/EndPaint. The BitmapUpdate callback fires for
// uncompressed bitmap transfers that bypass GDI. We register it as a
// safety net to capture any frames that arrive through this path.
update := readPtr(context, contextOffsetUpdate)
if update != 0 {
writePtr(update, updateOffsetBitmapUpdate, bitmapUpdateCallbackPtr)
}
backend.gdiReady = true
return 1 // TRUE — success
}
// preConnectCallback is called before the connection is fully established.
// We use it to enable SoftwareGdi mode so FreeRDP handles bitmap decoding.
var preConnectCallbackPtr = syscall.NewCallback(preConnectCallback)
func preConnectCallback(instance uintptr) uintptr {
backend := lookupInstance(instance)
if backend == nil {
return 0
}
// Enable software GDI — FreeRDP will decode bitmaps into a memory surface.
backend.setBool(FreeRDP_SoftwareGdi, true)
return 1 // TRUE
}
// bitmapUpdateCallback handles BITMAP_UPDATE messages. With GDI mode active,
// most rendering goes through the GDI pipeline and lands in primary_buffer
// automatically. This callback catches any bitmap updates that come through
// the legacy path and blits them into our pixel buffer.
//
// BITMAP_UPDATE layout (FreeRDP 3.10.3):
// UINT32 number — offset 0, number of rectangles
// BITMAP_DATA* rectangles — offset 8 (pointer, 8-byte aligned)
//
// BITMAP_DATA layout (packed, not ALIGN64):
// UINT32 destLeft — offset 0
// UINT32 destTop — offset 4
// UINT32 destRight — offset 8
// UINT32 destBottom — offset 12
// UINT32 width — offset 16
// UINT32 height — offset 20
// UINT32 bitsPerPixel — offset 24
// UINT32 flags — offset 28
// UINT32 bitmapLength — offset 32
// UINT32 cbCompFirstRowSize — offset 36
// UINT32 cbCompMainBodySize — offset 40
// UINT32 cbScanWidth — offset 44
// UINT32 cbUncompressedSize — offset 48
// BYTE* bitmapDataStream — offset 56 (pointer, 8-byte aligned after padding)
// BOOL compressed — offset 64
var bitmapUpdateCallbackPtr = syscall.NewCallback(bitmapUpdateCallback)
func bitmapUpdateCallback(context uintptr, bitmapUpdate uintptr) uintptr {
if context == 0 || bitmapUpdate == 0 {
return 1 // TRUE — don't break the pipeline
}
// Recover the instance from context->instance (slot 0 of rdpContext).
instance := readPtr(context, 0)
backend := lookupInstance(instance)
if backend == nil || backend.buffer == nil {
return 1
}
// With GDI mode enabled, the primary_buffer is already updated by FreeRDP's
// internal GDI renderer before this callback fires. We just need to mark
// the pixel buffer as dirty so the next GetFrame picks up changes.
//
// For the bitmap update path specifically, we parse the rectangles and
// copy each one from the GDI primary buffer into our PixelBuffer.
if backend.gdiReady && backend.gdiPrimaryBuf != 0 {
numRects := readU32(bitmapUpdate, 0)
if numRects > 0 {
backend.copyGdiToBuffer()
}
}
return 1 // TRUE
}
// ============================================================================
// FreeRDPBackend
// ============================================================================
// FreeRDPBackend implements RDPBackend using the FreeRDP3 library loaded at
// runtime via syscall.NewLazyDLL. This avoids any CGO dependency and allows
// cross-compilation from Linux while the DLLs are resolved at runtime on
// Windows.
type FreeRDPBackend struct {
instance uintptr // freerdp*
context uintptr // rdpContext* (extracted from instance->context)
settings uintptr // rdpSettings* (extracted from context->settings)
input uintptr // rdpInput* (extracted from context->input)
buffer *PixelBuffer
connected bool
config RDPConfig
mu sync.Mutex
stopCh chan struct{}
// GDI surface state — populated by PostConnect callback.
gdiPrimaryBuf uintptr // BYTE* — FreeRDP's GDI framebuffer
gdiWidth int
gdiHeight int
gdiStride int // bytes per row (may include padding)
gdiReady bool
}
// NewFreeRDPBackend creates a new FreeRDP-backed RDP backend. The underlying
// DLLs are not loaded until Connect is called.
func NewFreeRDPBackend() *FreeRDPBackend {
return &FreeRDPBackend{
stopCh: make(chan struct{}),
}
}
// Connect establishes an RDP session using FreeRDP3. It creates a new FreeRDP
// instance, configures connection settings, and starts the event loop.
//
// Initialization order (required by FreeRDP3):
// 1. freerdp_new() — allocate instance
// 2. Register callbacks — PreConnect, PostConnect on the instance struct
// 3. freerdp_context_new() — allocate context (also allocates settings, input, update)
// 4. Configure settings — via freerdp_settings_set_* on context->settings
// 5. freerdp_connect() — triggers PreConnect -> TCP -> PostConnect
// 6. Event loop — freerdp_check_event_handles in a goroutine
func (f *FreeRDPBackend) Connect(config RDPConfig) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.connected {
return fmt.Errorf("already connected")
}
f.config = config
// ── Step 1: Create a bare FreeRDP instance ──
ret, _, err := procFreerdpNew.Call()
if ret == 0 {
return fmt.Errorf("freerdp_new failed: %v", err)
}
f.instance = ret
// Register this instance in the global map so callbacks can find us.
registerInstance(f.instance, f)
// ── Step 2: Register callbacks on the instance struct ──
// PreConnect (slot 48): called before RDP negotiation, used to enable GDI.
writePtr(f.instance, freerdpOffsetPreConnect, preConnectCallbackPtr)
// PostConnect (slot 49): called after connection, sets up GDI rendering.
writePtr(f.instance, freerdpOffsetPostConnect, postConnectCallbackPtr)
// ── Step 3: Create the context ──
// freerdp_context_new(freerdp* instance) -> BOOL
// This allocates rdpContext and populates instance->context, including
// the settings, input, and update sub-objects.
ret, _, err = procContextNew.Call(f.instance)
if ret == 0 {
unregisterInstance(f.instance)
procFreerdpFree.Call(f.instance)
f.instance = 0
return fmt.Errorf("freerdp_context_new failed: %v", err)
}
// ── Extract context, settings, and input pointers ──
f.context = readPtr(f.instance, freerdpOffsetContext)
if f.context == 0 {
unregisterInstance(f.instance)
procFreerdpFree.Call(f.instance)
f.instance = 0
return fmt.Errorf("freerdp context is null after context_new")
}
f.settings = readPtr(f.context, contextOffsetSettings)
if f.settings == 0 {
unregisterInstance(f.instance)
procFreerdpFree.Call(f.instance)
f.instance = 0
return fmt.Errorf("freerdp settings is null after context_new")
}
f.input = readPtr(f.context, contextOffsetInput)
if f.input == 0 {
unregisterInstance(f.instance)
procFreerdpFree.Call(f.instance)
f.instance = 0
return fmt.Errorf("freerdp input is null after context_new")
}
// ── Step 4: Configure connection settings ──
f.setString(FreeRDP_ServerHostname, config.Hostname)
f.setUint32(FreeRDP_ServerPort, uint32(config.Port))
f.setString(FreeRDP_Username, config.Username)
f.setString(FreeRDP_Password, config.Password)
if config.Domain != "" {
f.setString(FreeRDP_Domain, config.Domain)
}
// Display settings with sensible defaults.
width := config.Width
if width == 0 {
width = 1920
}
height := config.Height
if height == 0 {
height = 1080
}
f.setUint32(FreeRDP_DesktopWidth, uint32(width))
f.setUint32(FreeRDP_DesktopHeight, uint32(height))
colorDepth := config.ColorDepth
if colorDepth == 0 {
colorDepth = 32
}
f.setUint32(FreeRDP_ColorDepth, uint32(colorDepth))
// Security mode selection.
switch config.Security {
case "nla":
f.setBool(FreeRDP_NlaSecurity, true)
case "tls":
f.setBool(FreeRDP_TlsSecurity, true)
case "rdp":
f.setBool(FreeRDP_RdpSecurity, true)
default:
f.setBool(FreeRDP_NlaSecurity, true)
}
// Accept all server certificates. Per-host pinning is a future enhancement.
f.setBool(FreeRDP_IgnoreCertificate, true)
// Enable software GDI rendering. This tells FreeRDP to decode all bitmap
// data into a memory framebuffer that we can read directly. The PreConnect
// callback also sets this, but we set it here as well for clarity —
// FreeRDP reads it during connection negotiation.
f.setBool(FreeRDP_SoftwareGdi, true)
// Allocate the pixel buffer for frame capture.
f.buffer = NewPixelBuffer(width, height)
// ── Step 5: Initiate the RDP connection ──
// freerdp_connect calls PreConnect -> TCP/TLS/NLA -> PostConnect internally.
ret, _, err = procFreerdpConnect.Call(f.instance)
if ret == 0 {
unregisterInstance(f.instance)
procFreerdpFree.Call(f.instance)
f.instance = 0
return fmt.Errorf("freerdp_connect failed: %v", err)
}
f.connected = true
// ── Step 6: Start the event processing loop ──
go f.eventLoop()
return nil
}
// eventLoop polls FreeRDP for incoming events at roughly 60 fps. It runs
// until the stop channel is closed or the connection is marked as inactive.
func (f *FreeRDPBackend) eventLoop() {
for {
select {
case <-f.stopCh:
return
default:
f.mu.Lock()
if !f.connected {
f.mu.Unlock()
return
}
// freerdp_check_event_handles takes rdpContext*, NOT freerdp*.
procCheckEventHandles.Call(f.context)
// After processing events, copy the GDI framebuffer into our
// PixelBuffer so GetFrame returns current data.
if f.gdiReady {
f.copyGdiToBuffer()
}
f.mu.Unlock()
time.Sleep(16 * time.Millisecond) // ~60 fps
}
}
}
// copyGdiToBuffer copies the FreeRDP GDI primary surface into the PixelBuffer.
// The GDI buffer is BGRA32; our PixelBuffer expects RGBA. We do the channel
// swap during the copy.
//
// Must be called with f.mu held.
func (f *FreeRDPBackend) copyGdiToBuffer() {
if f.gdiPrimaryBuf == 0 || f.buffer == nil {
return
}
w := f.gdiWidth
h := f.gdiHeight
stride := f.gdiStride
if stride == 0 {
stride = w * 4 // fallback: assume tightly packed BGRA32
}
// Total bytes in the GDI surface.
totalBytes := stride * h
if totalBytes <= 0 {
return
}
// Read the raw GDI framebuffer. This is a direct memory read from the
// FreeRDP-managed buffer. The GDI layer updates this buffer during
// BeginPaint/EndPaint cycles triggered by freerdp_check_event_handles.
src := unsafe.Slice((*byte)(unsafe.Pointer(f.gdiPrimaryBuf)), totalBytes)
// Build the RGBA frame, swapping B and R channels (BGRA -> RGBA).
buf := f.buffer
buf.mu.Lock()
defer buf.mu.Unlock()
dstLen := w * h * 4
if len(buf.Data) != dstLen {
buf.Data = make([]byte, dstLen)
buf.Width = w
buf.Height = h
}
for y := 0; y < h; y++ {
srcRow := y * stride
dstRow := y * w * 4
for x := 0; x < w; x++ {
si := srcRow + x*4
di := dstRow + x*4
if si+3 >= totalBytes || di+3 >= dstLen {
break
}
// BGRA -> RGBA
buf.Data[di+0] = src[si+2] // R <- B
buf.Data[di+1] = src[si+1] // G <- G
buf.Data[di+2] = src[si+0] // B <- R
buf.Data[di+3] = src[si+3] // A <- A
}
}
buf.dirty = true
}
// Disconnect tears down the RDP session and frees the FreeRDP instance.
func (f *FreeRDPBackend) Disconnect() error {
f.mu.Lock()
defer f.mu.Unlock()
if !f.connected {
return nil
}
close(f.stopCh)
f.connected = false
f.gdiReady = false
// Free GDI resources before disconnecting.
if f.gdiPrimaryBuf != 0 {
procGdiFree.Call(f.instance)
f.gdiPrimaryBuf = 0
}
procFreerdpDisconnect.Call(f.instance)
unregisterInstance(f.instance)
procFreerdpFree.Call(f.instance)
f.instance = 0
f.context = 0
f.settings = 0
f.input = 0
return nil
}
// SendMouseEvent forwards a mouse event to the remote RDP session.
func (f *FreeRDPBackend) SendMouseEvent(x, y int, flags uint32) error {
f.mu.Lock()
defer f.mu.Unlock()
if !f.connected || f.input == 0 {
return fmt.Errorf("not connected")
}
procInputSendMouse.Call(f.input, uintptr(flags), uintptr(x), uintptr(y))
return nil
}
// SendKeyEvent forwards a keyboard event to the remote RDP session. The
// scancode uses the same format as the ScancodeMap in input.go — extended
// keys have the 0xE0 prefix in the high byte.
func (f *FreeRDPBackend) SendKeyEvent(scancode uint32, pressed bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if !f.connected || f.input == 0 {
return fmt.Errorf("not connected")
}
var flags uint32
if pressed {
flags = KBD_FLAGS_DOWN
} else {
flags = KBD_FLAGS_RELEASE
}
if scancode > 0xFF {
flags |= KBD_FLAGS_EXTENDED
}
procInputSendKeyboard.Call(f.input, uintptr(flags), uintptr(scancode&0xFF))
return nil
}
// SendClipboard sends clipboard text to the remote session.
// TODO: Implement via the FreeRDP cliprdr channel.
func (f *FreeRDPBackend) SendClipboard(data string) error {
return nil
}
// GetFrame returns the current full-frame RGBA pixel buffer. The frame is
// populated from the FreeRDP GDI primary surface during the event loop and
// via the PostConnect/BitmapUpdate callbacks.
func (f *FreeRDPBackend) GetFrame() ([]byte, error) {
if f.buffer == nil {
return nil, fmt.Errorf("no frame buffer")
}
return f.buffer.GetFrame(), nil
}
// IsConnected reports whether the backend has an active RDP connection.
func (f *FreeRDPBackend) IsConnected() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.connected
}
// setString sets a string setting on the FreeRDP settings object.
func (f *FreeRDPBackend) setString(id int, value string) {
b, _ := syscall.BytePtrFromString(value)
procSettingsSetString.Call(f.settings, uintptr(id), uintptr(unsafe.Pointer(b)))
}
// setUint32 sets a uint32 setting on the FreeRDP settings object.
func (f *FreeRDPBackend) setUint32(id int, value uint32) {
procSettingsSetUint32.Call(f.settings, uintptr(id), uintptr(value))
}
// setBool sets a boolean setting on the FreeRDP settings object.
func (f *FreeRDPBackend) setBool(id int, value bool) {
v := uintptr(0)
if value {
v = 1
}
procSettingsSetBool.Call(f.settings, uintptr(id), v)
}
// Ensure FreeRDPBackend satisfies the RDPBackend interface at compile time.
var _ RDPBackend = (*FreeRDPBackend)(nil)