mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
feat(ffi): add a client example using Avalonia (#443)
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Ignore directories
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Ignore files
|
||||
*.user
|
||||
*.userosscache
|
||||
*.suo
|
||||
*.userprefs
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.cache
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
@@ -0,0 +1,10 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Devolutions.IronRdp.AvaloniaExample.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Devolutions.IronRdp.AvaloniaExample;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||
<ProjectReference Include="../Devolutions.IronRdp/Devolutions.IronRdp.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.IronRdp.AvaloniaExample", "Devolutions.IronRdp.AvaloniaExample.csproj", "{B374556F-70F4-4B70-90AE-6DF00C532240}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B374556F-70F4-4B70-90AE-6DF00C532240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B374556F-70F4-4B70-90AE-6DF00C532240}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B374556F-70F4-4B70-90AE-6DF00C532240}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B374556F-70F4-4B70-90AE-6DF00C532240}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {1DD5DE59-5AB4-4F5F-A2AC-CA4D7012F56E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,104 @@
|
||||
using Avalonia.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public static class KeyCodeMapper
|
||||
{
|
||||
private static readonly Dictionary<PhysicalKey, ushort> KeyToScancodeMap = new Dictionary<PhysicalKey, ushort>
|
||||
{
|
||||
{PhysicalKey.Escape, 0x01},
|
||||
{PhysicalKey.Digit1, 0x02},
|
||||
{PhysicalKey.Digit2, 0x03},
|
||||
{PhysicalKey.Digit3, 0x04},
|
||||
{PhysicalKey.Digit4, 0x05},
|
||||
{PhysicalKey.Digit5, 0x06},
|
||||
{PhysicalKey.Digit6, 0x07},
|
||||
{PhysicalKey.Digit7, 0x08},
|
||||
{PhysicalKey.Digit8, 0x09},
|
||||
{PhysicalKey.Digit9, 0x0A},
|
||||
{PhysicalKey.Digit0, 0x0B},
|
||||
{PhysicalKey.Minus, 0x0C},
|
||||
{PhysicalKey.Equal, 0x0D},
|
||||
{PhysicalKey.Backspace, 0x0E},
|
||||
{PhysicalKey.Tab, 0x0F},
|
||||
{PhysicalKey.Q, 0x10},
|
||||
{PhysicalKey.W, 0x11},
|
||||
{PhysicalKey.E, 0x12},
|
||||
{PhysicalKey.R, 0x13},
|
||||
{PhysicalKey.T, 0x14},
|
||||
{PhysicalKey.Y, 0x15},
|
||||
{PhysicalKey.U, 0x16},
|
||||
{PhysicalKey.I, 0x17},
|
||||
{PhysicalKey.O, 0x18},
|
||||
{PhysicalKey.P, 0x19},
|
||||
{PhysicalKey.BracketLeft, 0x1A},
|
||||
{PhysicalKey.BracketRight, 0x1B},
|
||||
{PhysicalKey.Enter, 0x1C},
|
||||
{PhysicalKey.ControlLeft, 0x1D},
|
||||
{PhysicalKey.A, 0x1E},
|
||||
{PhysicalKey.S, 0x1F},
|
||||
{PhysicalKey.D, 0x20},
|
||||
{PhysicalKey.F, 0x21},
|
||||
{PhysicalKey.G, 0x22},
|
||||
{PhysicalKey.H, 0x23},
|
||||
{PhysicalKey.J, 0x24},
|
||||
{PhysicalKey.K, 0x25},
|
||||
{PhysicalKey.L, 0x26},
|
||||
{PhysicalKey.Semicolon, 0x27},
|
||||
{PhysicalKey.Quote, 0x28},
|
||||
{PhysicalKey.ShiftLeft, 0x2A},
|
||||
{PhysicalKey.Backslash, 0x2B},
|
||||
{PhysicalKey.Z, 0x2C},
|
||||
{PhysicalKey.X, 0x2D},
|
||||
{PhysicalKey.C, 0x2E},
|
||||
{PhysicalKey.V, 0x2F},
|
||||
{PhysicalKey.B, 0x30},
|
||||
{PhysicalKey.N, 0x31},
|
||||
{PhysicalKey.M, 0x32},
|
||||
{PhysicalKey.Comma, 0x33},
|
||||
{PhysicalKey.Period, 0x34},
|
||||
{PhysicalKey.Slash, 0x35},
|
||||
{PhysicalKey.ShiftRight, 0x36},
|
||||
{PhysicalKey.PrintScreen, 0x37},
|
||||
{PhysicalKey.AltLeft, 0x38},
|
||||
{PhysicalKey.Space, 0x39},
|
||||
{PhysicalKey.CapsLock, 0x3A},
|
||||
{PhysicalKey.F1, 0x3B},
|
||||
{PhysicalKey.F2, 0x3C},
|
||||
{PhysicalKey.F3, 0x3D},
|
||||
{PhysicalKey.F4, 0x3E},
|
||||
{PhysicalKey.F5, 0x3F},
|
||||
{PhysicalKey.F6, 0x40},
|
||||
{PhysicalKey.F7, 0x41},
|
||||
{PhysicalKey.F8, 0x42},
|
||||
{PhysicalKey.F9, 0x43},
|
||||
{PhysicalKey.F10, 0x44},
|
||||
{PhysicalKey.NumLock, 0x45},
|
||||
{PhysicalKey.ScrollLock, 0x46},
|
||||
{PhysicalKey.Home, 0x47},
|
||||
{PhysicalKey.ArrowUp, 0x48},
|
||||
{PhysicalKey.PageUp, 0x49},
|
||||
{PhysicalKey.NumPadSubtract, 0x4A},
|
||||
{PhysicalKey.ArrowLeft, 0x4B},
|
||||
{PhysicalKey.NumPad5, 0x4C},
|
||||
{PhysicalKey.ArrowRight, 0x4D},
|
||||
{PhysicalKey.NumPadAdd, 0x4E},
|
||||
{PhysicalKey.End, 0x4F},
|
||||
{PhysicalKey.ArrowDown, 0x50},
|
||||
{PhysicalKey.PageDown, 0x51},
|
||||
{PhysicalKey.Insert, 0x52},
|
||||
{PhysicalKey.Delete, 0x53},
|
||||
{PhysicalKey.F11, 0x57},
|
||||
{PhysicalKey.F12, 0x58}
|
||||
};
|
||||
|
||||
|
||||
public static ushort? GetScancode(PhysicalKey key)
|
||||
{
|
||||
if (KeyToScancodeMap.TryGetValue(key, out ushort scancode))
|
||||
{
|
||||
return scancode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="1980" d:DesignHeight="1080"
|
||||
x:Class="Devolutions.IronRdp.AvaloniaExample.MainWindow"
|
||||
Title="Devolutions.IronRdp.AvaloniaExample">
|
||||
|
||||
<Canvas Name="MainCanvas" Width="1280" Height="800" Background="Black"
|
||||
PointerPressed="Canvas_OnPointerPressed" PointerMoved="Canvas_PointerMoved" PointerReleased="Canvas_PointerReleased"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
/>
|
||||
</Window>
|
||||
@@ -0,0 +1,273 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Security;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Devolutions.IronRdp.AvaloniaExample;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
|
||||
WriteableBitmap? bitmap;
|
||||
Canvas? canvas;
|
||||
Image? image;
|
||||
InputDatabase? inputDatabase = InputDatabase.New();
|
||||
ActiveStage? activeStage;
|
||||
DecodedImage? decodedImage;
|
||||
Framed<SslStream>? framed;
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.Opened += OnOpened;
|
||||
|
||||
}
|
||||
|
||||
private void OnOpened(object? sender, EventArgs e)
|
||||
{
|
||||
WindowState = WindowState.Maximized;
|
||||
|
||||
var username = Environment.GetEnvironmentVariable("IRONRDP_USERNAME");
|
||||
var password = Environment.GetEnvironmentVariable("IRONRDP_PASSWORD");
|
||||
var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN");
|
||||
var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER");
|
||||
|
||||
if (username == null || password == null || domain == null || server == null)
|
||||
{
|
||||
Trace.TraceError("Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, IRONRDP_DOMAIN, and IRONRDP_SERVER environment variables");
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var width = 1280;
|
||||
var height = 800;
|
||||
|
||||
var config = buildConfig(username, password, domain, width, height);
|
||||
|
||||
var task = Connection.Connect(config, server);
|
||||
bitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), Avalonia.Platform.PixelFormat.Rgba8888, AlphaFormat.Opaque);
|
||||
canvas = this.FindControl<Canvas>("MainCanvas")!;
|
||||
canvas.Focusable = true;
|
||||
image = new Image { Width = width, Height = height, Source = this.bitmap };
|
||||
canvas.Children.Add(image);
|
||||
|
||||
canvas.KeyDown += Canvas_KeyDown;
|
||||
canvas.KeyUp += Canvas_KeyUp;
|
||||
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Exception e = t.Exception!;
|
||||
Trace.TraceError("Error connecting to server: " + e.Message);
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
var (res, framed) = t.Result;
|
||||
this.decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(), res.GetDesktopSize().GetHeight());
|
||||
this.activeStage = ActiveStage.New(res);
|
||||
this.framed = framed;
|
||||
ReadPduAndProcessActiveStage();
|
||||
});
|
||||
}
|
||||
|
||||
private async void WriteDecodedImageToCanvas()
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var data = decodedImage!.GetData();
|
||||
var bufferSize = (int)data.GetSize();
|
||||
|
||||
var buffer = new byte[bufferSize];
|
||||
data.Fill(buffer);
|
||||
|
||||
using (var bitmap = this.bitmap!.Lock())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var bitmapSpan = new Span<byte>((void*)bitmap.Address, bufferSize);
|
||||
var bufferSpan = new Span<byte>(buffer);
|
||||
bufferSpan.CopyTo(bitmapSpan);
|
||||
}
|
||||
}
|
||||
|
||||
image!.InvalidateVisual();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void ReadPduAndProcessActiveStage()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var keepLooping = true;
|
||||
while (keepLooping)
|
||||
{
|
||||
var readPduTask = await framed!.ReadPdu();
|
||||
Action action = readPduTask.Item1;
|
||||
byte[] payload = readPduTask.Item2;
|
||||
var outputIterator = activeStage!.Process(decodedImage!, action, payload);
|
||||
keepLooping = await HandleActiveStageOutput(outputIterator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Config buildConfig(string username, string password, string domain, int width, int height)
|
||||
{
|
||||
ConfigBuilder configBuilder = ConfigBuilder.New();
|
||||
|
||||
configBuilder.WithUsernameAndPassword(username, password);
|
||||
configBuilder.SetDomain(domain);
|
||||
configBuilder.SetDesktopSize((ushort)height, (ushort)width);
|
||||
configBuilder.SetClientName("IronRdp");
|
||||
configBuilder.SetClientDir("C:\\");
|
||||
configBuilder.SetPerformanceFlags(PerformanceFlags.NewDefault());
|
||||
|
||||
return configBuilder.Build();
|
||||
}
|
||||
|
||||
private void Canvas_OnPointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||
{
|
||||
PointerUpdateKind mouseButton = e.GetCurrentPoint((Visual?)sender).Properties.PointerUpdateKind;
|
||||
|
||||
MouseButtonType buttonType = mouseButton switch
|
||||
{
|
||||
PointerUpdateKind.LeftButtonPressed => MouseButtonType.Left,
|
||||
PointerUpdateKind.RightButtonPressed => MouseButtonType.Right,
|
||||
PointerUpdateKind.MiddleButtonPressed => MouseButtonType.Middle,
|
||||
PointerUpdateKind.XButton1Pressed => MouseButtonType.X1,
|
||||
PointerUpdateKind.XButton2Pressed => MouseButtonType.X2,
|
||||
PointerUpdateKind.LeftButtonReleased => MouseButtonType.Left,
|
||||
PointerUpdateKind.MiddleButtonReleased => MouseButtonType.Middle,
|
||||
PointerUpdateKind.RightButtonReleased => MouseButtonType.Right,
|
||||
PointerUpdateKind.XButton1Released => MouseButtonType.X1,
|
||||
PointerUpdateKind.XButton2Released => MouseButtonType.X2,
|
||||
PointerUpdateKind.Other => throw new NotImplementedException(),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
var buttonOperation = MouseButton.New(buttonType).AsOperationMouseButtonPressed();
|
||||
var fastpath = inputDatabase!.Apply(buttonOperation);
|
||||
var output = activeStage!.ProcessFastpathInput(decodedImage!, fastpath);
|
||||
var _ = HandleActiveStageOutput(output);
|
||||
}
|
||||
|
||||
private void Canvas_PointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (this.activeStage == null || this.decodedImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var position = e.GetPosition((Visual?)sender);
|
||||
var x = (ushort)position.X;
|
||||
var y = (ushort)position.Y;
|
||||
var mouseMovedEvent = MousePosition.New(x, y).AsMoveOperation();
|
||||
var fastpath = inputDatabase!.Apply(mouseMovedEvent);
|
||||
var output = activeStage.ProcessFastpathInput(decodedImage, fastpath);
|
||||
var _ = HandleActiveStageOutput(output);
|
||||
}
|
||||
|
||||
private void Canvas_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
PointerUpdateKind mouseButton = e.GetCurrentPoint((Visual?)sender).Properties.PointerUpdateKind;
|
||||
|
||||
MouseButtonType buttonType = mouseButton switch
|
||||
{
|
||||
PointerUpdateKind.LeftButtonPressed => MouseButtonType.Left,
|
||||
PointerUpdateKind.RightButtonPressed => MouseButtonType.Right,
|
||||
PointerUpdateKind.MiddleButtonPressed => MouseButtonType.Middle,
|
||||
PointerUpdateKind.XButton1Pressed => MouseButtonType.X1,
|
||||
PointerUpdateKind.XButton2Pressed => MouseButtonType.X2,
|
||||
PointerUpdateKind.LeftButtonReleased => MouseButtonType.Left,
|
||||
PointerUpdateKind.MiddleButtonReleased => MouseButtonType.Middle,
|
||||
PointerUpdateKind.RightButtonReleased => MouseButtonType.Right,
|
||||
PointerUpdateKind.XButton1Released => MouseButtonType.X1,
|
||||
PointerUpdateKind.XButton2Released => MouseButtonType.X2,
|
||||
PointerUpdateKind.Other => throw new NotImplementedException(),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
var buttonOperation = MouseButton.New(buttonType).AsOperationMouseButtonReleased();
|
||||
var fastpath = inputDatabase!.Apply(buttonOperation);
|
||||
var output = activeStage!.ProcessFastpathInput(decodedImage!, fastpath);
|
||||
var _ = HandleActiveStageOutput(output);
|
||||
}
|
||||
|
||||
private void Canvas_KeyDown(object? sender, KeyEventArgs? e)
|
||||
{
|
||||
if (activeStage == null || decodedImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
PhysicalKey physicalKey = e!.PhysicalKey;
|
||||
|
||||
var keyOperation = Scancode.FromU16((ushort)KeyCodeMapper.GetScancode(physicalKey)!).AsOperationKeyPressed();
|
||||
var fastpath = inputDatabase!.Apply(keyOperation);
|
||||
var output = activeStage.ProcessFastpathInput(decodedImage, fastpath);
|
||||
var _ = HandleActiveStageOutput(output);
|
||||
}
|
||||
|
||||
private void Canvas_KeyUp(object? sender, KeyEventArgs? e)
|
||||
{
|
||||
if (this.activeStage == null || this.decodedImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Key key = e!.Key;
|
||||
var keyOperation = Scancode.FromU16((ushort)key).AsOperationKeyReleased();
|
||||
var fastpath = inputDatabase!.Apply(keyOperation);
|
||||
var output = activeStage.ProcessFastpathInput(decodedImage, fastpath);
|
||||
var _ = HandleActiveStageOutput(output);
|
||||
}
|
||||
|
||||
private async Task<bool> HandleActiveStageOutput(ActiveStageOutputIterator outputIterator)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
while (!outputIterator.IsEmpty())
|
||||
{
|
||||
var output = outputIterator.Next()!; // outputIterator.Next() is not null since outputIterator.IsEmpty() is false
|
||||
if (output.GetEnumType() == ActiveStageOutputType.Terminate)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (output.GetEnumType() == ActiveStageOutputType.ResponseFrame)
|
||||
{
|
||||
// render the decoded image to canvas
|
||||
WriteDecodedImageToCanvas();
|
||||
// Send the response frame to the server
|
||||
var responseFrame = output.GetResponseFrame()!;
|
||||
byte[] responseFrameBytes = new byte[responseFrame.GetSize()];
|
||||
responseFrame.Fill(responseFrameBytes);
|
||||
await framed!.Write(responseFrameBytes);
|
||||
}
|
||||
else if (output.GetEnumType() == ActiveStageOutputType.GraphicsUpdate)
|
||||
{
|
||||
WriteDecodedImageToCanvas();
|
||||
}
|
||||
else if (output.GetEnumType() == ActiveStageOutputType.PointerPosition)
|
||||
{
|
||||
WriteDecodedImageToCanvas();
|
||||
}
|
||||
else if (output.GetEnumType() == ActiveStageOutputType.PointerBitmap)
|
||||
{
|
||||
WriteDecodedImageToCanvas();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Avalonia;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Devolutions.IronRdp.AvaloniaExample;
|
||||
|
||||
class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
InitializeLogging();
|
||||
try{
|
||||
BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
}catch(Exception e){
|
||||
Trace.TraceError(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
public static void InitializeLogging()
|
||||
{
|
||||
Trace.AutoFlush = true;
|
||||
TextWriterTraceListener myListener = new TextWriterTraceListener(System.IO.File.CreateText("AvaloniaExample.log"));
|
||||
Trace.Listeners.Add(myListener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="Devolutions.IronRdp.AvaloniaExample.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.IronRdp.ConnectExample", "Devolutions.IronRdp.ConnectExample.csproj", "{EB812E03-EDBE-4576-A84A-A26982D46BE1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{EB812E03-EDBE-4576-A84A-A26982D46BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EB812E03-EDBE-4576-A84A-A26982D46BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EB812E03-EDBE-4576-A84A-A26982D46BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EB812E03-EDBE-4576-A84A-A26982D46BE1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C47D6FCB-C82B-4C07-AA17-839B0B0CF0D2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -47,13 +47,13 @@ namespace Devolutions.IronRdp.ConnectExample
|
||||
{
|
||||
var output = outputIterator.Next()!; // outputIterator.Next() is not null since outputIterator.IsEmpty() is false
|
||||
Console.WriteLine($"Output type: {output.GetType()}");
|
||||
if (output.GetType() == ActiveStageOutputType.Terminate)
|
||||
if (output.GetEnumType() == ActiveStageOutputType.Terminate)
|
||||
{
|
||||
Console.WriteLine("Connection terminated.");
|
||||
keepLooping = false;
|
||||
}
|
||||
|
||||
if (output.GetType() == ActiveStageOutputType.ResponseFrame)
|
||||
if (output.GetEnumType() == ActiveStageOutputType.ResponseFrame)
|
||||
{
|
||||
var responseFrame = output.GetResponseFrame()!;
|
||||
byte[] responseFrameBytes = new byte[responseFrame.GetSize()];
|
||||
|
||||
@@ -91,6 +91,40 @@ public partial class ActiveStage: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="IronRdpException"></exception>
|
||||
/// <returns>
|
||||
/// A <c>ActiveStageOutputIterator</c> allocated on Rust side.
|
||||
/// </returns>
|
||||
public ActiveStageOutputIterator ProcessFastpathInput(DecodedImage image, FastPathInputEventIterator fastpathInput)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
throw new ObjectDisposedException("ActiveStage");
|
||||
}
|
||||
Raw.DecodedImage* imageRaw;
|
||||
imageRaw = image.AsFFI();
|
||||
if (imageRaw == null)
|
||||
{
|
||||
throw new ObjectDisposedException("DecodedImage");
|
||||
}
|
||||
Raw.FastPathInputEventIterator* fastpathInputRaw;
|
||||
fastpathInputRaw = fastpathInput.AsFFI();
|
||||
if (fastpathInputRaw == null)
|
||||
{
|
||||
throw new ObjectDisposedException("FastPathInputEventIterator");
|
||||
}
|
||||
Raw.SessionFfiResultBoxActiveStageOutputIteratorBoxIronRdpError result = Raw.ActiveStage.ProcessFastpathInput(_inner, imageRaw, fastpathInputRaw);
|
||||
if (!result.isOk)
|
||||
{
|
||||
throw new IronRdpException(new IronRdpError(result.Err));
|
||||
}
|
||||
Raw.ActiveStageOutputIterator* retVal = result.Ok;
|
||||
return new ActiveStageOutputIterator(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the underlying raw handle.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,6 +23,14 @@ public partial class ActiveStageOutput: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ActiveStageOutputType EnumType
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetEnumType();
|
||||
}
|
||||
}
|
||||
|
||||
public InclusiveRectangle GraphicsUpdate
|
||||
{
|
||||
get
|
||||
@@ -63,14 +71,6 @@ public partial class ActiveStageOutput: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ActiveStageOutputType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetType();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a managed <c>ActiveStageOutput</c> from a raw handle.
|
||||
/// </summary>
|
||||
@@ -88,7 +88,7 @@ public partial class ActiveStageOutput: IDisposable
|
||||
/// <returns>
|
||||
/// A <c>ActiveStageOutputType</c> allocated on C# side.
|
||||
/// </returns>
|
||||
public ActiveStageOutputType GetType()
|
||||
public ActiveStageOutputType GetEnumType()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
@@ -96,7 +96,7 @@ public partial class ActiveStageOutput: IDisposable
|
||||
{
|
||||
throw new ObjectDisposedException("ActiveStageOutput");
|
||||
}
|
||||
Raw.ActiveStageOutputType retVal = Raw.ActiveStageOutput.GetType(_inner);
|
||||
Raw.ActiveStageOutputType retVal = Raw.ActiveStageOutput.GetEnumType(_inner);
|
||||
return (ActiveStageOutputType)retVal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// <auto-generated/> by Diplomat
|
||||
|
||||
#pragma warning disable 0105
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Devolutions.IronRdp.Diplomat;
|
||||
#pragma warning restore 0105
|
||||
|
||||
namespace Devolutions.IronRdp;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public partial class BitmapConfig: IDisposable
|
||||
{
|
||||
private unsafe Raw.BitmapConfig* _inner;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a managed <c>BitmapConfig</c> from a raw handle.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
|
||||
/// <br/>
|
||||
/// This constructor assumes the raw struct is allocated on Rust side.
|
||||
/// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
|
||||
/// </remarks>
|
||||
public unsafe BitmapConfig(Raw.BitmapConfig* handle)
|
||||
{
|
||||
_inner = handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the underlying raw handle.
|
||||
/// </summary>
|
||||
public unsafe Raw.BitmapConfig* AsFFI()
|
||||
{
|
||||
return _inner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the underlying object immediately.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Raw.BitmapConfig.Destroy(_inner);
|
||||
_inner = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
~BitmapConfig()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// <auto-generated/> by Diplomat
|
||||
|
||||
#pragma warning disable 0105
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Devolutions.IronRdp.Diplomat;
|
||||
#pragma warning restore 0105
|
||||
|
||||
namespace Devolutions.IronRdp;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public partial class Char: IDisposable
|
||||
{
|
||||
private unsafe Raw.Char* _inner;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a managed <c>Char</c> from a raw handle.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
|
||||
/// <br/>
|
||||
/// This constructor assumes the raw struct is allocated on Rust side.
|
||||
/// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
|
||||
/// </remarks>
|
||||
public unsafe Char(Raw.Char* handle)
|
||||
{
|
||||
_inner = handle;
|
||||
}
|
||||
|
||||
/// <exception cref="IronRdpException"></exception>
|
||||
/// <returns>
|
||||
/// A <c>Char</c> allocated on Rust side.
|
||||
/// </returns>
|
||||
public static Char New(uint c)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
Raw.InputFfiResultBoxCharBoxIronRdpError result = Raw.Char.New(c);
|
||||
if (!result.isOk)
|
||||
{
|
||||
throw new IronRdpException(new IronRdpError(result.Err));
|
||||
}
|
||||
Raw.Char* retVal = result.Ok;
|
||||
return new Char(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>
|
||||
/// A <c>Operation</c> allocated on Rust side.
|
||||
/// </returns>
|
||||
public Operation AsOperationUnicodeKeyPressed()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
throw new ObjectDisposedException("Char");
|
||||
}
|
||||
Raw.Operation* retVal = Raw.Char.AsOperationUnicodeKeyPressed(_inner);
|
||||
return new Operation(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>
|
||||
/// A <c>Operation</c> allocated on Rust side.
|
||||
/// </returns>
|
||||
public Operation AsOperationUnicodeKeyReleased()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
throw new ObjectDisposedException("Char");
|
||||
}
|
||||
Raw.Operation* retVal = Raw.Char.AsOperationUnicodeKeyReleased(_inner);
|
||||
return new Operation(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the underlying raw handle.
|
||||
/// </summary>
|
||||
public unsafe Raw.Char* AsFFI()
|
||||
{
|
||||
return _inner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the underlying object immediately.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Raw.Char.Destroy(_inner);
|
||||
_inner = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
~Char()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,14 @@ public partial class ClientConnectorState: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionActivationSequence ConnectionFinalizationResult
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetConnectionFinalizationResult();
|
||||
}
|
||||
}
|
||||
|
||||
public SecurityProtocol ConnectionInitiationWaitConfirmRequestedProtocol
|
||||
{
|
||||
get
|
||||
@@ -63,11 +71,11 @@ public partial class ClientConnectorState: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ClientConnectorStateType Type
|
||||
public ClientConnectorStateType EnumType
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetType();
|
||||
return GetEnumType();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +97,7 @@ public partial class ClientConnectorState: IDisposable
|
||||
/// <returns>
|
||||
/// A <c>ClientConnectorStateType</c> allocated on C# side.
|
||||
/// </returns>
|
||||
public ClientConnectorStateType GetType()
|
||||
public ClientConnectorStateType GetEnumType()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
@@ -97,7 +105,7 @@ public partial class ClientConnectorState: IDisposable
|
||||
{
|
||||
throw new ObjectDisposedException("ClientConnectorState");
|
||||
}
|
||||
Raw.ConnectorStateFfiResultClientConnectorStateTypeBoxIronRdpError result = Raw.ClientConnectorState.GetType(_inner);
|
||||
Raw.ConnectorStateFfiResultClientConnectorStateTypeBoxIronRdpError result = Raw.ClientConnectorState.GetEnumType(_inner);
|
||||
if (!result.isOk)
|
||||
{
|
||||
throw new IronRdpException(new IronRdpError(result.Err));
|
||||
@@ -239,6 +247,28 @@ public partial class ClientConnectorState: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="IronRdpException"></exception>
|
||||
/// <returns>
|
||||
/// A <c>ConnectionActivationSequence</c> allocated on Rust side.
|
||||
/// </returns>
|
||||
public ConnectionActivationSequence GetConnectionFinalizationResult()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
throw new ObjectDisposedException("ClientConnectorState");
|
||||
}
|
||||
Raw.ConnectorStateFfiResultBoxConnectionActivationSequenceBoxIronRdpError result = Raw.ClientConnectorState.GetConnectionFinalizationResult(_inner);
|
||||
if (!result.isOk)
|
||||
{
|
||||
throw new IronRdpException(new IronRdpError(result.Err));
|
||||
}
|
||||
Raw.ConnectionActivationSequence* retVal = result.Ok;
|
||||
return new ConnectionActivationSequence(retVal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the underlying raw handle.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,6 +23,14 @@ public partial class ConfigBuilder: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapConfig BitmapConfig
|
||||
{
|
||||
set
|
||||
{
|
||||
SetBitmapConfig(value);
|
||||
}
|
||||
}
|
||||
|
||||
public uint ClientBuild
|
||||
{
|
||||
set
|
||||
@@ -326,6 +334,24 @@ public partial class ConfigBuilder: IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBitmapConfig(BitmapConfig bitmap)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
throw new ObjectDisposedException("ConfigBuilder");
|
||||
}
|
||||
Raw.BitmapConfig* bitmapRaw;
|
||||
bitmapRaw = bitmap.AsFFI();
|
||||
if (bitmapRaw == null)
|
||||
{
|
||||
throw new ObjectDisposedException("BitmapConfig");
|
||||
}
|
||||
Raw.ConfigBuilder.SetBitmapConfig(_inner, bitmapRaw);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetClientBuild(uint clientBuild)
|
||||
{
|
||||
unsafe
|
||||
|
||||
@@ -46,11 +46,11 @@ public struct DiplomatWriteable : IDisposable
|
||||
|
||||
IntPtr flushFuncPtr = Marshal.GetFunctionPointerForDelegate(flushFunc);
|
||||
IntPtr growFuncPtr = Marshal.GetFunctionPointerForDelegate(growFunc);
|
||||
|
||||
|
||||
// flushFunc and growFunc are managed objects and might be disposed of by the garbage collector.
|
||||
// To prevent this, we make the context hold the references and protect the context itself
|
||||
// for automatic disposal by moving it behind a GCHandle.
|
||||
DiplomatWriteableContext ctx = new DiplomatWriteableContext();
|
||||
DiplomatWriteableContext ctx = new DiplomatWriteableContext();
|
||||
ctx.flushFunc = flushFunc;
|
||||
ctx.growFunc = growFunc;
|
||||
GCHandle ctxHandle = GCHandle.Alloc(ctx);
|
||||
@@ -81,7 +81,7 @@ public struct DiplomatWriteable : IDisposable
|
||||
{
|
||||
throw new IndexOutOfRangeException("DiplomatWriteable buffer is too big");
|
||||
}
|
||||
return Marshal.PtrToStringUTF8(buf, (int)len);
|
||||
return Marshal.PtrToStringUTF8(buf, (int) len);
|
||||
#else
|
||||
byte[] utf8 = ToUtf8Bytes();
|
||||
return DiplomatUtils.Utf8ToString(utf8);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// <auto-generated/> by Diplomat
|
||||
|
||||
#pragma warning disable 0105
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Devolutions.IronRdp.Diplomat;
|
||||
#pragma warning restore 0105
|
||||
|
||||
namespace Devolutions.IronRdp;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public partial class FastPathInputEvent: IDisposable
|
||||
{
|
||||
private unsafe Raw.FastPathInputEvent* _inner;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a managed <c>FastPathInputEvent</c> from a raw handle.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free).
|
||||
/// <br/>
|
||||
/// This constructor assumes the raw struct is allocated on Rust side.
|
||||
/// If implemented, the custom Drop implementation on Rust side WILL run on destruction.
|
||||
/// </remarks>
|
||||
public unsafe FastPathInputEvent(Raw.FastPathInputEvent* handle)
|
||||
{
|
||||
_inner = handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the underlying raw handle.
|
||||
/// </summary>
|
||||
public unsafe Raw.FastPathInputEvent* AsFFI()
|
||||
{
|
||||
return _inner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the underlying object immediately.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (_inner == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Raw.FastPathInputEvent.Destroy(_inner);
|
||||
_inner = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
~FastPathInputEvent()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user