Initial project snapshot
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# RAP RDP Service C# Skeleton
|
||||
|
||||
This directory contains an inactive C# RDP service research scaffold.
|
||||
|
||||
Status: inactive research scaffold. It is not wired into the runtime stack and
|
||||
does not replace the current C++ worker.
|
||||
|
||||
## Purpose
|
||||
|
||||
The current C++ worker remains the active RDP runtime. The active performance
|
||||
direction is C++ RDP service internals, documented in:
|
||||
|
||||
- `docs/architecture/RDP_SERVICE_CPP_PERFORMANCE_TARGET.md`
|
||||
|
||||
This C# scaffold is retained only as non-runtime research context. It must not
|
||||
be used for implementation unless explicitly re-approved.
|
||||
|
||||
Any future alternative service would still need to keep:
|
||||
|
||||
- backend control-plane contracts
|
||||
- worker lease/assignment model
|
||||
- direct worker WSS data-plane contract
|
||||
- fallback backend gateway
|
||||
- session lifecycle semantics
|
||||
|
||||
## Rules
|
||||
|
||||
- No FreeRDP dependency in this service.
|
||||
- No third-party RDP protocol library.
|
||||
- RDP protocol details stay behind service boundaries.
|
||||
- Cluster transport is not redesigned here.
|
||||
- Current C++ worker remains the active and intended runtime direction.
|
||||
- Do not continue this C# scaffold unless explicitly re-approved.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
dotnet build workers/rdp-service-csharp/src/Rap.Rdp.Service/Rap.Rdp.Service.csproj
|
||||
```
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- No RDP handshake yet.
|
||||
- No NLA/CredSSP yet.
|
||||
- No graphics protocol implementation yet.
|
||||
- No direct WSS binding yet.
|
||||
- No worker registration yet.
|
||||
|
||||
See:
|
||||
|
||||
- `docs/architecture/RDP_SERVICE_CPP_PERFORMANCE_TARGET.md`
|
||||
- `docs/architecture/RDP_SERVICE_CSHARP_TARGET.md` for superseded historical context
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Rap.Rdp.Core;
|
||||
|
||||
public interface IRapDataPlaneBridge : IAsyncDisposable
|
||||
{
|
||||
ValueTask StartAsync(RdpSessionDescriptor session, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask StopAsync(CancellationToken cancellationToken);
|
||||
|
||||
event Func<RdpInputEvent, CancellationToken, ValueTask>? InputReceived;
|
||||
|
||||
event Func<string, CancellationToken, ValueTask>? ClipboardTextReceived;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace Rap.Rdp.Core;
|
||||
|
||||
public interface IRdpGraphicsSink
|
||||
{
|
||||
ValueTask PublishSurfaceAsync(RdpSurfaceUpdate update, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask PublishCursorAsync(RdpCursorUpdate update, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public abstract record RdpSurfaceUpdate(
|
||||
long Sequence,
|
||||
string UpdateKind,
|
||||
DateTimeOffset CapturedAt);
|
||||
|
||||
public sealed record RdpRawBitmapUpdate(
|
||||
long Sequence,
|
||||
DateTimeOffset CapturedAt,
|
||||
int X,
|
||||
int Y,
|
||||
int Width,
|
||||
int Height,
|
||||
int DesktopWidth,
|
||||
int DesktopHeight,
|
||||
string PixelFormat,
|
||||
ReadOnlyMemory<byte> Pixels) : RdpSurfaceUpdate(Sequence, "raw_bitmap", CapturedAt);
|
||||
|
||||
public sealed record RdpEncodedGraphicsUpdate(
|
||||
long Sequence,
|
||||
DateTimeOffset CapturedAt,
|
||||
int SurfaceId,
|
||||
string Codec,
|
||||
int X,
|
||||
int Y,
|
||||
int Width,
|
||||
int Height,
|
||||
ReadOnlyMemory<byte> Payload) : RdpSurfaceUpdate(Sequence, "encoded_graphics", CapturedAt);
|
||||
|
||||
public sealed record RdpCursorUpdate(
|
||||
long Sequence,
|
||||
int X,
|
||||
int Y,
|
||||
bool Visible,
|
||||
DateTimeOffset CapturedAt);
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Rap.Rdp.Core;
|
||||
|
||||
public interface IRdpProtocolEngine : IAsyncDisposable
|
||||
{
|
||||
ValueTask ConnectAsync(RdpSessionDescriptor session, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask DisconnectAsync(bool terminateRemoteSession, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask SendInputAsync(RdpInputEvent inputEvent, CancellationToken cancellationToken);
|
||||
|
||||
ValueTask SendClipboardTextAsync(string text, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public abstract record RdpInputEvent(string CorrelationId);
|
||||
|
||||
public sealed record RdpKeyboardEvent(
|
||||
string CorrelationId,
|
||||
ushort ScanCode,
|
||||
bool KeyDown,
|
||||
bool Extended) : RdpInputEvent(CorrelationId);
|
||||
|
||||
public sealed record RdpPointerMoveEvent(
|
||||
string CorrelationId,
|
||||
double NormalizedX,
|
||||
double NormalizedY) : RdpInputEvent(CorrelationId);
|
||||
|
||||
public sealed record RdpPointerButtonEvent(
|
||||
string CorrelationId,
|
||||
string Button,
|
||||
bool Pressed,
|
||||
double NormalizedX,
|
||||
double NormalizedY) : RdpInputEvent(CorrelationId);
|
||||
|
||||
public sealed record RdpPointerWheelEvent(
|
||||
string CorrelationId,
|
||||
int Delta,
|
||||
bool Horizontal,
|
||||
double NormalizedX,
|
||||
double NormalizedY) : RdpInputEvent(CorrelationId);
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace Rap.Rdp.Core;
|
||||
|
||||
public sealed class RdpServiceSession(
|
||||
RdpSessionDescriptor descriptor,
|
||||
IRdpProtocolEngine protocolEngine,
|
||||
IRapDataPlaneBridge dataPlaneBridge)
|
||||
: IAsyncDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _lifetime = new();
|
||||
private int _started;
|
||||
|
||||
public RdpSessionDescriptor Descriptor { get; } = descriptor;
|
||||
|
||||
public async ValueTask StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(_lifetime.Token, cancellationToken);
|
||||
dataPlaneBridge.InputReceived += OnInputReceivedAsync;
|
||||
dataPlaneBridge.ClipboardTextReceived += OnClipboardTextReceivedAsync;
|
||||
await protocolEngine.ConnectAsync(Descriptor, linked.Token).ConfigureAwait(false);
|
||||
await dataPlaneBridge.StartAsync(Descriptor, linked.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask StopAsync(bool terminateRemoteSession, CancellationToken cancellationToken)
|
||||
{
|
||||
_lifetime.Cancel();
|
||||
await dataPlaneBridge.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||
await protocolEngine.DisconnectAsync(terminateRemoteSession, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private ValueTask OnInputReceivedAsync(RdpInputEvent inputEvent, CancellationToken cancellationToken)
|
||||
{
|
||||
return protocolEngine.SendInputAsync(inputEvent, cancellationToken);
|
||||
}
|
||||
|
||||
private ValueTask OnClipboardTextReceivedAsync(string text, CancellationToken cancellationToken)
|
||||
{
|
||||
return protocolEngine.SendClipboardTextAsync(text, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_lifetime.Cancel();
|
||||
dataPlaneBridge.InputReceived -= OnInputReceivedAsync;
|
||||
dataPlaneBridge.ClipboardTextReceived -= OnClipboardTextReceivedAsync;
|
||||
await dataPlaneBridge.DisposeAsync().ConfigureAwait(false);
|
||||
await protocolEngine.DisposeAsync().ConfigureAwait(false);
|
||||
_lifetime.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Rap.Rdp.Core;
|
||||
|
||||
public sealed record RdpSessionDescriptor(
|
||||
string SessionId,
|
||||
string AttachmentId,
|
||||
string OrganizationId,
|
||||
string ResourceId,
|
||||
string WorkerId,
|
||||
RdpTargetEndpoint Target,
|
||||
RdpSecurityPolicy SecurityPolicy,
|
||||
RdpRuntimePolicy RuntimePolicy);
|
||||
|
||||
public sealed record RdpTargetEndpoint(
|
||||
string Host,
|
||||
int Port,
|
||||
string UserName,
|
||||
string Password);
|
||||
|
||||
public sealed record RdpSecurityPolicy(
|
||||
CertificateVerificationMode CertificateVerificationMode);
|
||||
|
||||
public enum CertificateVerificationMode
|
||||
{
|
||||
Strict,
|
||||
Ignore
|
||||
}
|
||||
|
||||
public sealed record RdpRuntimePolicy(
|
||||
IReadOnlySet<RapChannel> AllowedChannels,
|
||||
ClipboardMode ClipboardMode,
|
||||
FileTransferMode FileTransferMode);
|
||||
|
||||
public enum RapChannel
|
||||
{
|
||||
Control,
|
||||
Input,
|
||||
Render,
|
||||
Clipboard,
|
||||
FileUpload,
|
||||
Telemetry
|
||||
}
|
||||
|
||||
public enum ClipboardMode
|
||||
{
|
||||
Disabled,
|
||||
ClientToServer,
|
||||
ServerToClient,
|
||||
Bidirectional
|
||||
}
|
||||
|
||||
public enum FileTransferMode
|
||||
{
|
||||
Disabled,
|
||||
ClientToServer,
|
||||
ServerToClient,
|
||||
Bidirectional
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Rap.Rdp.Core;
|
||||
|
||||
Console.WriteLine("RAP RDP C# service skeleton");
|
||||
Console.WriteLine("Status: protocol engine not implemented; current C++ worker remains active.");
|
||||
Console.WriteLine($"WorkerId={Environment.GetEnvironmentVariable("RDP_SERVICE_WORKER_ID") ?? "rdp-worker-csharp-1"}");
|
||||
|
||||
if (args.Contains("--self-test", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var descriptor = new RdpSessionDescriptor(
|
||||
"self-test-session",
|
||||
"self-test-attachment",
|
||||
"self-test-org",
|
||||
"self-test-resource",
|
||||
"rdp-worker-csharp-1",
|
||||
new RdpTargetEndpoint("127.0.0.1", 3389, "user", "password"),
|
||||
new RdpSecurityPolicy(CertificateVerificationMode.Strict),
|
||||
new RdpRuntimePolicy(
|
||||
new HashSet<RapChannel> { RapChannel.Control, RapChannel.Input, RapChannel.Render },
|
||||
ClipboardMode.Disabled,
|
||||
FileTransferMode.Disabled));
|
||||
|
||||
Console.WriteLine($"Self-test descriptor created for session={descriptor.SessionId}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Use --self-test to validate skeleton startup.");
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Rap.Rdp.Core\Rap.Rdp.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
build/
|
||||
.git/
|
||||
.vs/
|
||||
.vscode/
|
||||
*.obj
|
||||
*.o
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.log
|
||||
@@ -0,0 +1,146 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
project(rdp_worker LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(Boost REQUIRED)
|
||||
pkg_check_modules(FREERDP REQUIRED IMPORTED_TARGET freerdp2)
|
||||
pkg_check_modules(FREERDP_CLIENT REQUIRED IMPORTED_TARGET freerdp-client2)
|
||||
pkg_check_modules(WINPR REQUIRED IMPORTED_TARGET winpr2)
|
||||
|
||||
set(RDP_WORKER_COMMON_SOURCES
|
||||
src/adapter/adapter_event_router.cpp
|
||||
src/adapter/rdp_adapter_runtime.cpp
|
||||
src/adapter/service_adapter_protocol.cpp
|
||||
src/common/json.cpp
|
||||
src/common/logger.cpp
|
||||
src/common/time.cpp
|
||||
src/cursor/cursor_adapter.cpp
|
||||
src/cursor/cursor_update.cpp
|
||||
src/graphics/graphics_adapter.cpp
|
||||
src/graphics/render_update.cpp
|
||||
src/config/config.cpp
|
||||
src/coordination/redis_client.cpp
|
||||
src/coordination/control_plane.cpp
|
||||
src/dataplane/direct_wss_server.cpp
|
||||
src/dataplane/token_validator.cpp
|
||||
src/runtime/direct_bind_policy.cpp
|
||||
src/runtime/session_manager.cpp
|
||||
src/runtime/session_runtime.cpp
|
||||
src/freerdp/rdp_runtime.cpp
|
||||
)
|
||||
|
||||
add_executable(rdp-worker
|
||||
src/main.cpp
|
||||
${RDP_WORKER_COMMON_SOURCES}
|
||||
)
|
||||
|
||||
add_executable(rdp-worker-dataplane-token-probe
|
||||
src/tools/data_plane_token_probe.cpp
|
||||
src/common/json.cpp
|
||||
src/dataplane/token_validator.cpp
|
||||
)
|
||||
|
||||
add_executable(rdp-worker-dataplane-bind-probe
|
||||
src/tools/data_plane_bind_probe.cpp
|
||||
src/runtime/direct_bind_policy.cpp
|
||||
)
|
||||
|
||||
add_executable(rdp-worker-graphics-adapter-probe
|
||||
src/tools/graphics_adapter_probe.cpp
|
||||
src/graphics/graphics_adapter.cpp
|
||||
src/graphics/render_update.cpp
|
||||
)
|
||||
|
||||
add_executable(rdp-worker-cursor-adapter-probe
|
||||
src/tools/cursor_adapter_probe.cpp
|
||||
src/common/json.cpp
|
||||
src/cursor/cursor_adapter.cpp
|
||||
src/cursor/cursor_update.cpp
|
||||
)
|
||||
|
||||
add_executable(rdp-worker-service-adapter-protocol-probe
|
||||
src/tools/service_adapter_protocol_probe.cpp
|
||||
src/adapter/service_adapter_protocol.cpp
|
||||
)
|
||||
|
||||
target_include_directories(rdp-worker
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_include_directories(rdp-worker-dataplane-token-probe
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_include_directories(rdp-worker-dataplane-bind-probe
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_include_directories(rdp-worker-graphics-adapter-probe
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_include_directories(rdp-worker-cursor-adapter-probe
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_include_directories(rdp-worker-service-adapter-protocol-probe
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(rdp-worker
|
||||
PRIVATE
|
||||
Threads::Threads
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
Boost::headers
|
||||
PkgConfig::FREERDP
|
||||
PkgConfig::FREERDP_CLIENT
|
||||
PkgConfig::WINPR
|
||||
)
|
||||
|
||||
target_link_libraries(rdp-worker-dataplane-token-probe
|
||||
PRIVATE
|
||||
OpenSSL::Crypto
|
||||
Boost::headers
|
||||
)
|
||||
|
||||
target_link_libraries(rdp-worker-dataplane-bind-probe
|
||||
PRIVATE
|
||||
Boost::headers
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(rdp-worker PRIVATE ws2_32)
|
||||
endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
install(TARGETS rdp-worker rdp-worker-dataplane-token-probe rdp-worker-dataplane-bind-probe
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(TARGETS rdp-worker-graphics-adapter-probe
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(TARGETS rdp-worker-cursor-adapter-probe
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(TARGETS rdp-worker-service-adapter-protocol-probe
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 25,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "dev",
|
||||
"displayName": "rdp-worker dev",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "dev",
|
||||
"configurePreset": "dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
FROM debian:12.10-slim AS freerdp-build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN printf '%s\n' \
|
||||
'Types: deb-src' \
|
||||
'URIs: http://deb.debian.org/debian' \
|
||||
'Suites: bookworm bookworm-updates' \
|
||||
'Components: main' \
|
||||
'Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
|
||||
'' \
|
||||
'Types: deb-src' \
|
||||
'URIs: http://deb.debian.org/debian-security' \
|
||||
'Suites: bookworm-security' \
|
||||
'Components: main' \
|
||||
'Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
|
||||
>/etc/apt/sources.list.d/debian-src.sources
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
binutils \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
dpkg-dev \
|
||||
git \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
&& apt-get build-dep -y freerdp2 \
|
||||
&& git clone --depth 1 --branch 2.11.7 https://github.com/FreeRDP/FreeRDP.git /tmp/freerdp-src \
|
||||
&& sed -i 's/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE/' /tmp/freerdp-src/channels/cliprdr/client/CMakeLists.txt \
|
||||
&& sed -i 's/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE/' /tmp/freerdp-src/channels/drdynvc/client/CMakeLists.txt \
|
||||
&& sed -i 's/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE/' /tmp/freerdp-src/channels/rdpdr/client/CMakeLists.txt \
|
||||
&& sed -i 's/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE/add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE/' /tmp/freerdp-src/channels/rdpsnd/client/CMakeLists.txt \
|
||||
&& sed -i 's/#define VirtualChannelEntryEx cliprdr_VirtualChannelEntryEx/#define VirtualChannelEntryEx FREERDP_API VirtualChannelEntryEx/' /tmp/freerdp-src/channels/cliprdr/client/cliprdr_main.c \
|
||||
&& sed -i 's/#define VirtualChannelEntryEx drdynvc_VirtualChannelEntryEx/#define VirtualChannelEntryEx FREERDP_API VirtualChannelEntryEx/' /tmp/freerdp-src/channels/drdynvc/client/drdynvc_main.c \
|
||||
&& sed -i 's/#define VirtualChannelEntryEx rdpdr_VirtualChannelEntryEx/#define VirtualChannelEntryEx FREERDP_API VirtualChannelEntryEx/' /tmp/freerdp-src/channels/rdpdr/client/rdpdr_main.c \
|
||||
&& sed -i 's/BOOL VCAPITYPE rdpsnd_VirtualChannelEntryEx/BOOL VCAPITYPE FREERDP_API VirtualChannelEntryEx/' /tmp/freerdp-src/channels/rdpsnd/client/rdpsnd_main.c \
|
||||
&& cmake -S /tmp/freerdp-src -B /tmp/freerdp-build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/opt/freerdp \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DWITH_CHANNELS=ON \
|
||||
-DBUILTIN_CHANNELS=OFF \
|
||||
-DWITH_CLIENT=ON \
|
||||
-DWITH_CLIENT_COMMON=ON \
|
||||
-DWITH_SERVER=OFF \
|
||||
-DWITH_SHADOW=OFF \
|
||||
-DWITH_X11=OFF \
|
||||
-DWITH_WAYLAND=OFF \
|
||||
&& cmake --build /tmp/freerdp-build \
|
||||
&& cmake --install /tmp/freerdp-build \
|
||||
&& test -f /opt/freerdp/lib/freerdp2/libcliprdr-client.so \
|
||||
&& test -f /opt/freerdp/lib/freerdp2/libdrdynvc-client.so \
|
||||
&& test -f /opt/freerdp/lib/freerdp2/librdpgfx-client.so \
|
||||
&& test -f /opt/freerdp/lib/freerdp2/librdpdr-client.so \
|
||||
&& test -f /opt/freerdp/lib/freerdp2/librdpsnd-client.so \
|
||||
&& readelf --dyn-syms /opt/freerdp/lib/freerdp2/libcliprdr-client.so | grep VirtualChannelEntryEx \
|
||||
&& readelf --dyn-syms /opt/freerdp/lib/freerdp2/libdrdynvc-client.so | grep VirtualChannelEntryEx \
|
||||
&& readelf --dyn-syms /opt/freerdp/lib/freerdp2/librdpdr-client.so | grep VirtualChannelEntryEx \
|
||||
&& readelf --dyn-syms /opt/freerdp/lib/freerdp2/librdpsnd-client.so | grep VirtualChannelEntryEx \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/freerdp-src /tmp/freerdp-build
|
||||
|
||||
FROM debian:12.10-slim AS build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV PKG_CONFIG_PATH=/opt/freerdp/lib/pkgconfig
|
||||
ENV LD_LIBRARY_PATH=/opt/freerdp/lib
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
freerdp2-x11 \
|
||||
freerdp2-dev \
|
||||
libboost-dev \
|
||||
libssl-dev \
|
||||
libwinpr2-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/rap-rdp-worker
|
||||
|
||||
COPY --from=freerdp-build /opt/freerdp /opt/freerdp
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cmake --preset dev \
|
||||
&& cmake --build --preset dev \
|
||||
&& cmake --install build
|
||||
|
||||
FROM debian:12.10-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV FREERDP_PLUGIN_PATH=/opt/freerdp/lib/freerdp2
|
||||
ENV LD_LIBRARY_PATH=/opt/freerdp/lib
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
freerdp2-x11 \
|
||||
freerdp2-dev \
|
||||
libssl3 \
|
||||
libwinpr2-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=freerdp-build /opt/freerdp /opt/freerdp
|
||||
COPY --from=build /usr/local/bin/rdp-worker /usr/local/bin/rdp-worker
|
||||
COPY --from=build /usr/local/bin/rdp-worker-dataplane-token-probe /usr/local/bin/rdp-worker-dataplane-token-probe
|
||||
COPY --from=build /usr/local/bin/rdp-worker-dataplane-bind-probe /usr/local/bin/rdp-worker-dataplane-bind-probe
|
||||
COPY --from=build /usr/local/bin/rdp-worker-graphics-adapter-probe /usr/local/bin/rdp-worker-graphics-adapter-probe
|
||||
COPY --from=build /usr/local/bin/rdp-worker-cursor-adapter-probe /usr/local/bin/rdp-worker-cursor-adapter-probe
|
||||
COPY --from=build /usr/local/bin/rdp-worker-service-adapter-protocol-probe /usr/local/bin/rdp-worker-service-adapter-protocol-probe
|
||||
|
||||
CMD ["/usr/local/bin/rdp-worker"]
|
||||
@@ -0,0 +1,484 @@
|
||||
# RDP Worker
|
||||
|
||||
Active C++ RDP Adapter worker for the Remote Access Platform.
|
||||
|
||||
Current test Docker deployment:
|
||||
|
||||
```text
|
||||
rap-rdp-worker:stage5-2-download
|
||||
```
|
||||
|
||||
This image is deployed on the canonical test Docker host
|
||||
`docker-test` / `192.168.200.61` for Stage 5.2 proof. The accepted
|
||||
ordered-region RDP path remains protected; the current focus is finishing
|
||||
server-to-client file download desktop UI proof, not new platform expansion.
|
||||
|
||||
## Scope
|
||||
|
||||
- standalone C++ worker service
|
||||
- Redis-backed worker registration, heartbeat, assignment consumption, lease renewal, and worker event publishing
|
||||
- FreeRDP-based server-side RDP connection runtime
|
||||
- persistent remote session process that stays connected when the client detaches
|
||||
- reattach and takeover-aware assignment updates without recreating the server-side RDP session
|
||||
- terminate and failure reporting back into the backend control plane
|
||||
|
||||
## Reproducible build environments
|
||||
|
||||
### Devcontainer
|
||||
|
||||
The repository now includes a worker-focused devcontainer in [`.devcontainer/devcontainer.json`](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\.devcontainer\devcontainer.json) and [`.devcontainer/Dockerfile`](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\.devcontainer\Dockerfile).
|
||||
|
||||
Environment assumptions:
|
||||
|
||||
- Debian `12.10-slim`
|
||||
- CMake from Debian 12 packages
|
||||
- Ninja from Debian 12 packages
|
||||
- GCC/G++ from `build-essential`
|
||||
- FreeRDP headers/libs from `freerdp2-dev`
|
||||
- WinPR headers/libs from `libwinpr2-dev`
|
||||
- no extra Redis client library is required because the worker uses its own socket-based RESP client
|
||||
|
||||
The devcontainer runs:
|
||||
|
||||
```sh
|
||||
cmake --preset dev --directory workers/rdp-worker
|
||||
```
|
||||
|
||||
after creation, so configure success is part of the expected environment contract.
|
||||
|
||||
### Docker image build
|
||||
|
||||
The worker Docker build is defined in [Dockerfile](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\workers\rdp-worker\Dockerfile).
|
||||
|
||||
Environment assumptions:
|
||||
|
||||
- Debian `12.10-slim`
|
||||
- `build-essential`
|
||||
- `cmake`
|
||||
- `ninja-build`
|
||||
- `pkg-config`
|
||||
- `freerdp2-dev`
|
||||
- `libwinpr2-dev`
|
||||
|
||||
The image uses [CMakePresets.json](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\workers\rdp-worker\CMakePresets.json) and installs the final binary to the deterministic path:
|
||||
|
||||
```text
|
||||
/usr/local/bin/rdp-worker
|
||||
```
|
||||
|
||||
## Exact build commands
|
||||
|
||||
### Local native build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```powershell
|
||||
cmake --preset dev --directory workers/rdp-worker
|
||||
cmake --build --preset dev --directory workers/rdp-worker
|
||||
```
|
||||
|
||||
Expected binary path:
|
||||
|
||||
```text
|
||||
workers/rdp-worker/build/rdp-worker
|
||||
```
|
||||
|
||||
### Docker build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```powershell
|
||||
docker build --tag rap-rdp-worker:dev --file workers/rdp-worker/Dockerfile workers/rdp-worker
|
||||
docker run --rm --entrypoint /bin/sh rap-rdp-worker:dev -lc "test -x /usr/local/bin/rdp-worker"
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
- `docker build` completes successfully
|
||||
- the second command exits with code `0`
|
||||
- the runtime binary path inside the image is `/usr/local/bin/rdp-worker`
|
||||
|
||||
## Runtime configuration
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `RDP_WORKER_ID`
|
||||
- `RDP_WORKER_REDIS_HOST`
|
||||
- `RDP_WORKER_REDIS_PORT`
|
||||
- `RDP_WORKER_REDIS_PASSWORD`
|
||||
- `RDP_WORKER_REDIS_DB`
|
||||
- `RDP_WORKER_HEARTBEAT_INTERVAL_SECONDS`
|
||||
- `RDP_WORKER_LEASE_RENEW_INTERVAL_SECONDS`
|
||||
- `RDP_WORKER_ASSIGNMENT_POLL_INTERVAL_SECONDS`
|
||||
- `RDP_WORKER_CAPABILITIES`
|
||||
- `RDP_WORKER_INSECURE_SKIP_VERIFY`
|
||||
- `RDP_WORKER_DATA_PLANE_ENABLED`
|
||||
- `RDP_WORKER_DATA_PLANE_LISTEN_HOST`
|
||||
- `RDP_WORKER_DATA_PLANE_LISTEN_PORT`
|
||||
- `RDP_WORKER_DATA_PLANE_PUBLIC_KEY_FILE`
|
||||
- `RDP_WORKER_DATA_PLANE_PUBLIC_KEY_PEM`
|
||||
- `RDP_WORKER_DATA_PLANE_TLS_CERT_FILE`
|
||||
- `RDP_WORKER_DATA_PLANE_TLS_KEY_FILE`
|
||||
- `RDP_WORKER_RDPGFX_ENABLED`
|
||||
|
||||
## Data Plane v1 Direct WSS Boundary
|
||||
|
||||
Stage DP-1C adds an optional direct worker WSS endpoint at
|
||||
`/rap/v1/data-plane`. It is disabled by default and does not change Windows
|
||||
client behavior by itself; the existing backend gateway remains the active
|
||||
fallback path unless the backend advertises a data-capable direct candidate.
|
||||
|
||||
When enabled, the endpoint:
|
||||
|
||||
- requires TLS certificate and private key files
|
||||
- validates the backend-issued RS256 `data_plane_token` with a public key
|
||||
- verifies token scope for session, attachment, user, organization, worker,
|
||||
resource, allowed channels, expiry, audience, and `jti`
|
||||
- binds only to an already-running `SessionRuntime`
|
||||
- rejects token `jti` replay with a bounded in-memory TTL cache
|
||||
- rejects missing runtime, wrong worker, expired token, invalid signature, and
|
||||
malformed token cases without creating a new RDP session
|
||||
- accepts the existing JSON realtime envelopes for `input`, `control`,
|
||||
`clipboard`, and `file_upload`
|
||||
- emits the existing JSON session state, render frame, clipboard, and file
|
||||
upload progress events
|
||||
- emits `session.frame` as DP-2 binary WebSocket frames when the client
|
||||
requests `render_transport=binary_v1`; JSON/base64 render remains available
|
||||
for backend gateway fallback/debug compatibility
|
||||
- drains direct input before Redis fallback input, keeps the direct inbound
|
||||
queue bounded, and coalesces stale mouse moves
|
||||
- keeps render latest-frame-only and droppable on the direct WSS writer
|
||||
- tags direct inbound envelopes with token-bound session/attachment/user/org
|
||||
claims so old attachments after takeover cannot keep controlling the runtime
|
||||
|
||||
Required environment:
|
||||
|
||||
```text
|
||||
RDP_WORKER_DATA_PLANE_ENABLED=true
|
||||
RDP_WORKER_DATA_PLANE_LISTEN_HOST=0.0.0.0
|
||||
RDP_WORKER_DATA_PLANE_LISTEN_PORT=8443
|
||||
RDP_WORKER_DATA_PLANE_PUBLIC_KEY_FILE=/path/to/data-plane-public.pem
|
||||
RDP_WORKER_DATA_PLANE_TLS_CERT_FILE=/path/to/cert.pem
|
||||
RDP_WORKER_DATA_PLANE_TLS_KEY_FILE=/path/to/key.pem
|
||||
```
|
||||
|
||||
Production direct worker WSS trust is coordinated by the backend candidate
|
||||
metadata described in `docs/architecture/DIRECT_WORKER_TLS_PKI.md`.
|
||||
|
||||
- Smoke/dev can use self-signed certificates only when the backend advertises
|
||||
the candidate as `smoke_only=true` and the Windows client has the explicit
|
||||
smoke override enabled.
|
||||
- Production workers must use certificates valid for their advertised direct
|
||||
WSS hostname/IP subject alternative names.
|
||||
- Production direct candidates should be advertised by the backend only with
|
||||
`DATA_PLANE_DIRECT_WORKER_TLS_TRUST_MODE=public_ca` or `platform_ca`.
|
||||
|
||||
The image also installs a token validation smoke helper:
|
||||
|
||||
```sh
|
||||
rdp-worker-dataplane-token-probe --token <jwt> --public-key-file <public.pem> --worker-id <worker_id>
|
||||
rdp-worker-dataplane-bind-probe --scenario valid
|
||||
rdp-worker-dataplane-bind-probe --scenario wrong-attachment
|
||||
rdp-worker-dataplane-bind-probe --scenario channels-too-broad
|
||||
```
|
||||
|
||||
Run the endpoint validation smoke from the repository root after building
|
||||
`rap-rdp-worker:dp1c-hardened` on the test Docker host:
|
||||
|
||||
```powershell
|
||||
pwsh -ExecutionPolicy Bypass -File scripts/smoke/data-plane-v1c-smoke.ps1
|
||||
```
|
||||
|
||||
Stage DP-2.1 removes the internal base64 encode/decode hop from the direct
|
||||
render path. FreeRDP frame capture now carries raw BGRA bytes through the worker
|
||||
runtime to the direct binary render sink, and direct WSS sends those bytes as
|
||||
`RAP2` binary WebSocket frames. Compatibility JSON/base64 `session_frame` events
|
||||
are still generated for the backend-gateway fallback boundary only. Backend
|
||||
gateway fallback remains supported and must stay enabled.
|
||||
|
||||
Stage DP-3A adds direct binary color mode selection. `full_color` sends the raw
|
||||
BGRA frame unchanged. `grayscale` converts the direct binary frame to grayscale
|
||||
inside the worker direct render sink, preserves BGRA32 output, and does not
|
||||
modify the backend-gateway JSON/base64 fallback path. Direct frame diagnostics
|
||||
include `requested_color_mode`, `applied_color_mode`,
|
||||
`grayscale_conversion_applied`, `raw_frame_bytes_before`,
|
||||
`raw_frame_bytes_after`, `binary_direct_bytes`, and `conversion_time_ms`.
|
||||
|
||||
## RDPGFX Gated Smoke Mode
|
||||
|
||||
`RDP_WORKER_RDPGFX_ENABLED=true` enables an experimental RDPGFX advertisement
|
||||
path for target compatibility testing only.
|
||||
|
||||
Default behavior:
|
||||
|
||||
- RDPGFX is disabled.
|
||||
- Classic GDI region-first rendering remains the safe production/dev path.
|
||||
- Direct binary WSS and backend gateway fallback continue to work without this
|
||||
flag.
|
||||
|
||||
When the flag is enabled, the worker logs:
|
||||
|
||||
- `rdp.gfx config requested=true`
|
||||
- `rdp.gfx channel_subscription ...`
|
||||
- `rdp.gfx channel_connected ...` if the target opens the RDPGFX channel
|
||||
- `rdp.gfx pipeline_init_success` or `rdp.gfx pipeline_init_failed`
|
||||
- `rdp.gfx surface_event ...` when FreeRDP surface callbacks are available
|
||||
- RDPGFX counters in `rdp.perf callback_summary`
|
||||
|
||||
The current live smoke target resets the connection when RDPGFX is advertised,
|
||||
so this flag must not be enabled for normal operation. See
|
||||
`artifacts/rdp-perf4-report.md`.
|
||||
|
||||
## Cursor Adapter Boundary
|
||||
|
||||
RDP-A4 adds a dedicated cursor boundary inside the C++ RDP Adapter.
|
||||
|
||||
The worker installs FreeRDP pointer callbacks and normalizes them into
|
||||
`session_cursor_updated` events. Direct worker WSS exposes those events as
|
||||
`cursor.update` envelopes. Cursor is scheduled as latest-only/droppable and does
|
||||
not wait behind binary display frames. Backend gateway fallback remains
|
||||
compatible with the same worker event payload.
|
||||
|
||||
The current implementation publishes cursor metadata:
|
||||
|
||||
- update kind
|
||||
- sequence
|
||||
- desktop width/height
|
||||
- x/y position
|
||||
- visibility
|
||||
- shape changed flag
|
||||
- cache index
|
||||
- hotspot
|
||||
- cursor width/height
|
||||
- XOR bpp
|
||||
- mask byte size
|
||||
- system cursor type
|
||||
|
||||
Smoke-proven image:
|
||||
|
||||
```text
|
||||
rap-rdp-worker:rdp-a4-cursor-adapter
|
||||
```
|
||||
|
||||
Probe:
|
||||
|
||||
```powershell
|
||||
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-a4-cursor-adapter /usr/local/bin/rdp-worker-cursor-adapter-probe
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
```text
|
||||
cursor_adapter_probe ok
|
||||
```
|
||||
|
||||
See `artifacts/rdp-a4-cursor-adapter-report.md`.
|
||||
|
||||
## GDI Repaint Cadence Hardening
|
||||
|
||||
RDP-Perf-5A keeps the default classic GDI region-first path and improves its
|
||||
runtime cadence without changing backend contracts or session lifecycle.
|
||||
|
||||
The worker now:
|
||||
|
||||
- drains already-signaled FreeRDP event handles in a bounded loop
|
||||
- rate-limits no-change detector logs
|
||||
- renews worker leases outside the hot render/input loop
|
||||
- publishes region and interactive render frames at a 33 ms interval
|
||||
- keeps full-frame fallback at a 100 ms interval
|
||||
- keeps direct worker WSS binary render and backend gateway JSON/base64 fallback
|
||||
compatible
|
||||
|
||||
Smoke-proven image:
|
||||
|
||||
```text
|
||||
rap-rdp-worker:rdp-perf5a-repaint-cadence
|
||||
```
|
||||
|
||||
Report:
|
||||
|
||||
```text
|
||||
artifacts/rdp-perf5a-report.md
|
||||
```
|
||||
|
||||
Manual UX validation after subsequent adapter work showed that keyboard, mouse,
|
||||
idle repaint, and general interaction are usable enough for the current MVP
|
||||
baseline, but small redraw artifacts remain and are the next hardening target.
|
||||
|
||||
## Direct Attach Baseline And Region Repair
|
||||
|
||||
After P1 manual visual smoke, the current accepted baseline image is:
|
||||
|
||||
```text
|
||||
rap-rdp-worker:rdp-p1-region-order2
|
||||
```
|
||||
|
||||
This baseline keeps the classic GDI region-first path and adds two important
|
||||
visual correctness safeguards:
|
||||
|
||||
- direct attach baseline full-frame capture so a newly attached client does not
|
||||
start from a black or incomplete framebuffer
|
||||
- throttled full-frame repair after detected region loss so dropped dirty
|
||||
regions do not leave persistent holes
|
||||
|
||||
The direct worker WSS writer treats attach-baseline frames as non-droppable
|
||||
reliable events. Normal display updates remain region-first and droppable where
|
||||
safe. Full frames are reserved for baseline/attach/recovery/fallback repair, not
|
||||
normal rendering.
|
||||
|
||||
Probes verified during the P1 baseline freeze:
|
||||
|
||||
```powershell
|
||||
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-graphics-adapter-probe
|
||||
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-cursor-adapter-probe
|
||||
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-service-adapter-protocol-probe
|
||||
docker -H ssh://docker-test run --rm rap-rdp-worker:rdp-p1-region-order2 rdp-worker-dataplane-bind-probe --scenario valid
|
||||
```
|
||||
|
||||
Expected results:
|
||||
|
||||
- `graphics_adapter_probe ok`
|
||||
- `cursor_adapter_probe ok`
|
||||
- service adapter channel list printed without error
|
||||
- `PASS scenario=valid`
|
||||
|
||||
Known remaining visual limitation:
|
||||
|
||||
- drag/release repaint is usable but not polished; drag behaves like an older
|
||||
RDP client on a weak link by moving a frame rather than continuously
|
||||
repainting the full window.
|
||||
|
||||
## Certificate Verification Policy
|
||||
|
||||
- `strict` is the default per-resource mode and keeps normal FreeRDP certificate validation enabled
|
||||
- `ignore` is a saved per-resource mode coming from backend session assignment and enables `FreeRDP_IgnoreCertificate` only for that connection
|
||||
- `RDP_WORKER_INSECURE_SKIP_VERIFY=true` remains available only as a smoke/debug fallback when assignment data does not include an explicit mode; it is not the production configuration path
|
||||
|
||||
## Resource metadata contract
|
||||
|
||||
The worker currently expects target connection data inside `resources.metadata`:
|
||||
|
||||
```json
|
||||
{
|
||||
"rdp_host": "10.0.0.10",
|
||||
"rdp_port": 3389,
|
||||
"username": "Administrator",
|
||||
"password": "secret",
|
||||
"domain": ""
|
||||
}
|
||||
```
|
||||
|
||||
`address` is used as a fallback host when `rdp_host` is absent.
|
||||
|
||||
## Current limitations
|
||||
|
||||
- rendering is implemented as a classic GDI region-first BGRA path with direct
|
||||
binary WSS support and baseline/region-loss repair; encoded graphics, codecs,
|
||||
tiles, and RDPGFX production mode are not accepted yet
|
||||
- clipboard is text-only; image/HTML/RTF/binary clipboard formats are
|
||||
intentionally blocked
|
||||
- file upload is implemented through controlled worker storage and restricted
|
||||
drive visibility; server-to-client download is build-proven through the
|
||||
restricted `RAP_Transfers\ToClient` drop zone, with live runtime acceptance
|
||||
still pending
|
||||
- audio, printer, webcam, multi-monitor, and advanced graphics pipeline
|
||||
optimizations are intentionally not implemented yet
|
||||
- worker-to-backend communication is currently Redis-only; no dedicated worker API exists yet
|
||||
- connection secrets are still sourced from resource metadata for this minimal end-to-end proof and are not yet integrated with encrypted secret storage
|
||||
- FreeRDP session loop remains focused on lifecycle correctness and the
|
||||
region-first adapter path; remaining redraw artifacts must be fixed before
|
||||
starting another graphics feature
|
||||
- the Docker/native build can now be reproduced in the defined environment, but a full real-target smoke run still depends on a reachable RDP host and a machine where Docker can run
|
||||
- `RDP_WORKER_INSECURE_SKIP_VERIFY=true` is only a smoke/debug fallback and should not be used instead of the per-resource certificate verification policy
|
||||
|
||||
## Verification levels
|
||||
|
||||
- local native: expected on machines that match the documented Debian/devcontainer package set
|
||||
- container-proven: the Dockerfile is the canonical reproducible worker build environment and a successful `docker build` proves configure + compile + install
|
||||
- CI-defined: [`.github/workflows/build.yml`](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\.github\workflows\build.yml) is prepared to build backend and worker image and verify the installed worker binary path
|
||||
- smoke-path: see [scripts/smoke/README.md](/\\?\UNC\192.168.220.200\mst\codex\rdp-proxy\scripts\smoke\README.md) for what is and is not proven yet
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If `pkg-config` cannot find `freerdp2` or `winpr2`, verify that the development packages are installed and that `pkg-config --list-all` includes both modules.
|
||||
- If `cmake --preset dev --directory workers/rdp-worker` fails, confirm that the environment matches the Debian 12 package set from the Dockerfile or devcontainer.
|
||||
- If `docker build` succeeds but runtime startup fails, verify Redis connectivity and worker environment variables.
|
||||
- If `freerdp_connect` fails, confirm that the seeded metadata contains `rdp_host`, `rdp_port`, `username`, `password`, and optional `domain`.
|
||||
- If the worker registers but never receives assignments, inspect Redis keys `worker:registration:<worker_id>` and `worker:control:<worker_id>`.
|
||||
|
||||
## Clipboard Text Boundary
|
||||
|
||||
The worker receives `clipboard` control envelopes from the backend session
|
||||
gateway only after backend policy checks have passed. It enforces
|
||||
`assignment.policy.clipboard_mode` again before touching the FreeRDP runtime
|
||||
boundary, so UI or gateway mistakes do not become the only clipboard guard.
|
||||
|
||||
Supported policy values are `disabled`, `client_to_server`,
|
||||
`server_to_client`, and `bidirectional`. The default is `disabled`.
|
||||
Current implementation is text-only. It deliberately rejects images, HTML,
|
||||
RTF, binary payloads, and file-like clipboard data by never accepting those
|
||||
formats in the worker envelope contract.
|
||||
|
||||
Stage 4.1 integrates the FreeRDP `cliprdr` virtual channel for text clipboard.
|
||||
The worker advertises `CF_UNICODETEXT`, requests server `CF_UNICODETEXT` data,
|
||||
and converts UTF-16LE clipboard payloads to UTF-8 before publishing normalized
|
||||
`session_clipboard_text` events to the backend.
|
||||
|
||||
Current limitation: only text is supported. Images, HTML, RTF, file lists,
|
||||
binary formats, and file-like clipboard payloads remain intentionally blocked.
|
||||
|
||||
## File Upload Boundary
|
||||
|
||||
Stage 5.1.1 supports client-to-server upload into a restricted per-session
|
||||
directory that is exposed to the remote Windows session through FreeRDP drive
|
||||
redirection. It does not expose a remote Windows filesystem browser, arbitrary
|
||||
paths, shared worker folders, or server-to-client download.
|
||||
|
||||
The worker receives `file_upload` control envelopes only after backend gateway
|
||||
policy checks pass, then enforces `assignment.policy.file_transfer_mode` again
|
||||
before writing any bytes. Supported policy values are `disabled`,
|
||||
`client_to_server`, `server_to_client`, and `bidirectional`; only
|
||||
`client_to_server` and `bidirectional` allow upload in this stage. Default is
|
||||
`disabled`.
|
||||
|
||||
Uploaded files are finalized under:
|
||||
|
||||
```text
|
||||
${RDP_WORKER_TRANSFER_ROOT:-/tmp/rap-rdp-worker-transfers}/<session_id>/visible/<sanitized_file_name>
|
||||
```
|
||||
|
||||
Only this `visible` directory is mapped to the remote RDP session as the
|
||||
restricted drive `RAP_Transfers`. The worker passes only that path to FreeRDP's
|
||||
RDPDR drive-redirection channel; it never maps `${RDP_WORKER_TRANSFER_ROOT}`,
|
||||
the session parent directory, or arbitrary client-provided paths.
|
||||
|
||||
Safety constraints in the worker:
|
||||
|
||||
- maximum file size is 25 MiB
|
||||
- maximum chunk size is 256 KiB
|
||||
- file names must be simple names only, with no path separators, drive
|
||||
prefixes, `..`, or empty names
|
||||
- existing target files are never overwritten silently
|
||||
- chunks must arrive in order with the expected offset
|
||||
- final FNV-1a content hash must match when the client supplies one
|
||||
- files are stored only; the worker never executes uploaded content
|
||||
- upload temp files are kept inside the same restricted `visible` directory and
|
||||
renamed into place only after validation completes
|
||||
- the `visible` directory is cleaned up on detach, and the full per-session
|
||||
transfer directory is removed on termination or session failure
|
||||
|
||||
Stage 5.2 adds a server-to-client download path using only the
|
||||
restricted `ToClient` directory inside the existing `RAP_Transfers` drive. A
|
||||
remote user copies a regular file into `RAP_Transfers\ToClient`; the worker
|
||||
detects stable files in that directory only, publishes `file_download.available`,
|
||||
and streams chunks after client `file_download.start` / `file_download.ack`.
|
||||
|
||||
The worker still does not expose a general remote filesystem browser, arbitrary
|
||||
paths, shared worker folders, SMB/WebDAV, or a Windows agent. Core download
|
||||
transport is runtime-proven for direct worker WSS and backend gateway fallback;
|
||||
lifecycle blocking is runtime-proven for detach, old-controller takeover, and
|
||||
worker failure. Full Stage 5.2 acceptance still needs manual desktop UI proof.
|
||||
|
||||
Runtime proof report:
|
||||
|
||||
- `artifacts/stage5-2-file-download-runtime-report.md`
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "rdp_worker/adapter/service_adapter_protocol.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::adapter {
|
||||
|
||||
struct AdapterEventDescriptor {
|
||||
AdapterChannel channel;
|
||||
std::string_view normalized_type;
|
||||
bool adapter_origin;
|
||||
bool reliable;
|
||||
bool droppable;
|
||||
};
|
||||
|
||||
class AdapterEventRouter {
|
||||
public:
|
||||
[[nodiscard]] AdapterEventDescriptor DescribeRenderNotification(const runtime::RenderNotification& notification) const;
|
||||
[[nodiscard]] AdapterEventDescriptor DescribeClipboardNotification(const runtime::ClipboardNotification& notification) const;
|
||||
[[nodiscard]] AdapterEventDescriptor DescribeClientEnvelope(std::string_view envelope_type,
|
||||
std::string_view payload_kind,
|
||||
std::string_view payload_action) const;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::string AdapterEventDescriptorLogLine(const AdapterEventDescriptor& descriptor);
|
||||
|
||||
} // namespace rdp_worker::adapter
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "rdp_worker/adapter/adapter_event_router.hpp"
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
#include "rdp_worker/freerdp/rdp_runtime.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::adapter {
|
||||
|
||||
class RdpAdapterRuntime {
|
||||
public:
|
||||
explicit RdpAdapterRuntime(std::shared_ptr<common::Logger> logger);
|
||||
|
||||
bool Start(const runtime::ConnectionSpec& spec);
|
||||
void Disconnect(bool terminate);
|
||||
bool IsConnected() const;
|
||||
bool PumpEvents(std::chrono::milliseconds timeout);
|
||||
int DesktopWidth() const;
|
||||
int DesktopHeight() const;
|
||||
bool SendFocusEvent(bool focused);
|
||||
bool SendKeyboardInput(uint16_t scan_code, bool key_down, bool extended);
|
||||
bool SendMouseMove(double normalized_x, double normalized_y);
|
||||
bool SendMouseButton(const std::string& button, bool pressed, double normalized_x, double normalized_y);
|
||||
bool SendMouseWheel(int wheel_delta, bool horizontal, double normalized_x, double normalized_y);
|
||||
bool SetClipboardText(const std::string& text);
|
||||
void MarkInputAppliedForGraphicsTrace(const std::string& correlation_id);
|
||||
std::optional<runtime::RenderNotification> CaptureFullFrameNotification(
|
||||
const std::string& state,
|
||||
const std::string& capture_source);
|
||||
std::optional<runtime::RenderNotification> PopRenderNotification();
|
||||
std::optional<runtime::ClipboardNotification> PopClipboardNotification();
|
||||
const std::string& RenderQualityProfile() const;
|
||||
|
||||
[[nodiscard]] const AdapterEventRouter& EventRouter() const;
|
||||
|
||||
private:
|
||||
void TraceClientEnvelope(std::string_view envelope_type,
|
||||
std::string_view payload_kind,
|
||||
std::string_view payload_action);
|
||||
void TraceAdapterEvent(const AdapterEventDescriptor& descriptor);
|
||||
|
||||
std::shared_ptr<common::Logger> logger_;
|
||||
freerdp_runtime::RdpRuntime freerdp_;
|
||||
AdapterEventRouter event_router_;
|
||||
bool lifecycle_logged_{false};
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::adapter
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace rdp_worker::adapter {
|
||||
|
||||
enum class AdapterChannel {
|
||||
kInput,
|
||||
kControl,
|
||||
kDisplay,
|
||||
kCursor,
|
||||
kClipboard,
|
||||
kFileTransfer,
|
||||
kAudio,
|
||||
kDevice,
|
||||
kTelemetry,
|
||||
};
|
||||
|
||||
enum class ChannelDirection {
|
||||
kClientToAdapter,
|
||||
kAdapterToClient,
|
||||
kBidirectional,
|
||||
};
|
||||
|
||||
enum class ChannelReliability {
|
||||
kReliableOrdered,
|
||||
kReliableChunked,
|
||||
kDroppableLatest,
|
||||
kAdaptiveDroppable,
|
||||
kSampledDroppable,
|
||||
};
|
||||
|
||||
enum class ChannelPriority {
|
||||
kCritical = 0,
|
||||
kHigh = 10,
|
||||
kMedium = 50,
|
||||
kLow = 90,
|
||||
};
|
||||
|
||||
struct ChannelSpec {
|
||||
AdapterChannel channel;
|
||||
std::string_view name;
|
||||
ChannelDirection direction;
|
||||
ChannelReliability reliability;
|
||||
ChannelPriority priority;
|
||||
bool stale_updates_droppable;
|
||||
bool may_block_input;
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr std::array<ChannelSpec, 9> AllChannelSpecs() {
|
||||
return {{
|
||||
{AdapterChannel::kInput, "input", ChannelDirection::kClientToAdapter, ChannelReliability::kReliableOrdered, ChannelPriority::kCritical, true, false},
|
||||
{AdapterChannel::kControl, "control", ChannelDirection::kBidirectional, ChannelReliability::kReliableOrdered, ChannelPriority::kHigh, false, false},
|
||||
{AdapterChannel::kDisplay, "display", ChannelDirection::kAdapterToClient, ChannelReliability::kDroppableLatest, ChannelPriority::kHigh, true, false},
|
||||
{AdapterChannel::kCursor, "cursor", ChannelDirection::kAdapterToClient, ChannelReliability::kDroppableLatest, ChannelPriority::kHigh, true, false},
|
||||
{AdapterChannel::kClipboard, "clipboard", ChannelDirection::kBidirectional, ChannelReliability::kReliableOrdered, ChannelPriority::kMedium, false, false},
|
||||
{AdapterChannel::kFileTransfer, "file_transfer", ChannelDirection::kBidirectional, ChannelReliability::kReliableChunked, ChannelPriority::kMedium, false, false},
|
||||
{AdapterChannel::kAudio, "audio", ChannelDirection::kAdapterToClient, ChannelReliability::kAdaptiveDroppable, ChannelPriority::kMedium, true, false},
|
||||
{AdapterChannel::kDevice, "device", ChannelDirection::kBidirectional, ChannelReliability::kReliableOrdered, ChannelPriority::kMedium, false, false},
|
||||
{AdapterChannel::kTelemetry, "telemetry", ChannelDirection::kAdapterToClient, ChannelReliability::kSampledDroppable, ChannelPriority::kLow, true, false},
|
||||
}};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<ChannelSpec> FindChannelSpec(std::string_view name);
|
||||
[[nodiscard]] std::string_view ChannelName(AdapterChannel channel);
|
||||
[[nodiscard]] std::string_view DirectionName(ChannelDirection direction);
|
||||
[[nodiscard]] std::string_view ReliabilityName(ChannelReliability reliability);
|
||||
[[nodiscard]] int PriorityValue(ChannelPriority priority);
|
||||
[[nodiscard]] bool IsDroppable(AdapterChannel channel);
|
||||
[[nodiscard]] bool IsReliable(AdapterChannel channel);
|
||||
[[nodiscard]] bool ValidateAdapterChannelInvariants();
|
||||
|
||||
} // namespace rdp_worker::adapter
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace rdp_worker::common {
|
||||
|
||||
struct JsonValue;
|
||||
|
||||
using JsonObject = std::map<std::string, JsonValue>;
|
||||
using JsonArray = std::vector<JsonValue>;
|
||||
|
||||
struct JsonValue {
|
||||
using Variant = std::variant<std::nullptr_t, bool, double, std::string, JsonArray, JsonObject>;
|
||||
|
||||
Variant value;
|
||||
|
||||
JsonValue();
|
||||
JsonValue(std::nullptr_t);
|
||||
JsonValue(bool input);
|
||||
JsonValue(double input);
|
||||
JsonValue(int input);
|
||||
JsonValue(const char* input);
|
||||
JsonValue(std::string input);
|
||||
JsonValue(JsonArray input);
|
||||
JsonValue(JsonObject input);
|
||||
|
||||
[[nodiscard]] bool IsObject() const;
|
||||
[[nodiscard]] bool IsArray() const;
|
||||
[[nodiscard]] bool IsString() const;
|
||||
[[nodiscard]] bool IsBool() const;
|
||||
[[nodiscard]] bool IsNumber() const;
|
||||
|
||||
[[nodiscard]] const JsonObject& AsObject() const;
|
||||
[[nodiscard]] const JsonArray& AsArray() const;
|
||||
[[nodiscard]] const std::string& AsString() const;
|
||||
[[nodiscard]] bool AsBool() const;
|
||||
[[nodiscard]] double AsNumber() const;
|
||||
};
|
||||
|
||||
JsonValue ParseJson(const std::string& input);
|
||||
std::string SerializeJson(const JsonValue& value);
|
||||
|
||||
std::optional<std::string> GetString(const JsonObject& object, const std::string& key);
|
||||
std::optional<bool> GetBool(const JsonObject& object, const std::string& key);
|
||||
std::optional<double> GetNumber(const JsonObject& object, const std::string& key);
|
||||
const JsonObject* GetObject(const JsonObject& object, const std::string& key);
|
||||
const JsonArray* GetArray(const JsonObject& object, const std::string& key);
|
||||
|
||||
} // namespace rdp_worker::common
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace rdp_worker::common {
|
||||
|
||||
enum class LogLevel {
|
||||
kDebug,
|
||||
kInfo,
|
||||
kWarn,
|
||||
kError
|
||||
};
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
explicit Logger(std::string service_name);
|
||||
|
||||
void Debug(const std::string& message);
|
||||
void Info(const std::string& message);
|
||||
void Warn(const std::string& message);
|
||||
void Error(const std::string& message);
|
||||
|
||||
private:
|
||||
void Write(LogLevel level, const std::string& message);
|
||||
|
||||
std::string service_name_;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::common
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
namespace rdp_worker::common {
|
||||
|
||||
using Clock = std::chrono::system_clock;
|
||||
|
||||
std::string ToRfc3339(Clock::time_point time_point);
|
||||
Clock::time_point NowUtc();
|
||||
Clock::time_point ParseRfc3339(const std::string& value);
|
||||
|
||||
} // namespace rdp_worker::common
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace rdp_worker::config {
|
||||
|
||||
struct Config {
|
||||
std::string worker_id;
|
||||
std::string redis_host;
|
||||
int redis_port;
|
||||
std::string redis_password;
|
||||
int redis_db;
|
||||
std::chrono::seconds worker_heartbeat_interval;
|
||||
std::chrono::seconds lease_renew_interval;
|
||||
std::chrono::seconds assignment_poll_interval;
|
||||
bool insecure_skip_verify;
|
||||
std::vector<std::string> capabilities;
|
||||
bool data_plane_enabled;
|
||||
std::string data_plane_listen_host;
|
||||
int data_plane_listen_port;
|
||||
std::string data_plane_public_key_pem;
|
||||
std::string data_plane_public_key_file;
|
||||
std::string data_plane_tls_cert_file;
|
||||
std::string data_plane_tls_key_file;
|
||||
};
|
||||
|
||||
Config LoadFromEnv();
|
||||
|
||||
} // namespace rdp_worker::config
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rdp_worker/common/json.hpp"
|
||||
#include "rdp_worker/config/config.hpp"
|
||||
#include "rdp_worker/coordination/redis_client.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::common {
|
||||
class Logger;
|
||||
}
|
||||
|
||||
namespace rdp_worker::coordination {
|
||||
|
||||
class ControlPlane {
|
||||
public:
|
||||
ControlPlane(config::Config config, std::shared_ptr<common::Logger> logger);
|
||||
|
||||
void Connect();
|
||||
void RegisterWorker();
|
||||
void ReleaseOwnedLeasesOnStartup();
|
||||
void SendHeartbeat();
|
||||
std::optional<runtime::Assignment> PollAssignment(std::chrono::seconds timeout);
|
||||
std::optional<common::JsonObject> PollSessionEnvelope(const std::string& session_id, std::chrono::seconds timeout);
|
||||
std::vector<common::JsonObject> DrainSessionEnvelopes(const std::string& session_id, std::size_t max_count);
|
||||
int64_t SessionEnvelopeQueueLength(const std::string& session_id);
|
||||
std::optional<runtime::WorkerLease> GetLeaseBySession(const std::string& session_id);
|
||||
void RenewLease(const runtime::WorkerLease& lease);
|
||||
void ReleaseLease(const runtime::WorkerLease& lease);
|
||||
void PublishEvent(const runtime::WorkerEvent& event);
|
||||
|
||||
private:
|
||||
runtime::Assignment ParseAssignment(const common::JsonObject& object) const;
|
||||
runtime::WorkerLease ParseLease(const common::JsonObject& object) const;
|
||||
std::string WorkerRegistrationPayload() const;
|
||||
std::string LeasePayload(const runtime::WorkerLease& lease) const;
|
||||
std::string EventPayload(const runtime::WorkerEvent& event) const;
|
||||
|
||||
config::Config config_;
|
||||
std::shared_ptr<common::Logger> logger_;
|
||||
mutable std::mutex mutex_;
|
||||
std::unique_ptr<RedisClient> redis_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::coordination
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace rdp_worker::coordination {
|
||||
|
||||
struct RedisReply {
|
||||
using Array = std::vector<RedisReply>;
|
||||
std::variant<std::nullptr_t, std::string, int64_t, Array> value;
|
||||
|
||||
[[nodiscard]] bool IsNull() const;
|
||||
[[nodiscard]] bool IsString() const;
|
||||
[[nodiscard]] bool IsInteger() const;
|
||||
[[nodiscard]] bool IsArray() const;
|
||||
|
||||
[[nodiscard]] const std::string& AsString() const;
|
||||
[[nodiscard]] int64_t AsInteger() const;
|
||||
[[nodiscard]] const Array& AsArray() const;
|
||||
};
|
||||
|
||||
class RedisClient {
|
||||
public:
|
||||
RedisClient(std::string host, int port, std::string password, int db);
|
||||
~RedisClient();
|
||||
|
||||
void Connect();
|
||||
void Close();
|
||||
|
||||
RedisReply Command(const std::vector<std::string>& parts);
|
||||
std::optional<std::string> Get(const std::string& key);
|
||||
void Set(const std::string& key, const std::string& value, std::chrono::seconds ttl);
|
||||
void SAdd(const std::string& key, const std::string& value);
|
||||
void SRem(const std::string& key, const std::string& value);
|
||||
std::vector<std::string> SMembers(const std::string& key);
|
||||
std::optional<std::vector<std::string>> BLPop(const std::string& key, std::chrono::seconds timeout);
|
||||
std::optional<std::string> LPop(const std::string& key);
|
||||
int64_t LLen(const std::string& key);
|
||||
void RPush(const std::string& key, const std::string& value);
|
||||
void Expire(const std::string& key, std::chrono::seconds ttl);
|
||||
void Delete(const std::string& key);
|
||||
|
||||
private:
|
||||
std::string ReadLine();
|
||||
std::string ReadBytes(std::size_t count);
|
||||
RedisReply ReadReply();
|
||||
void WriteAll(const std::string& data);
|
||||
std::string EncodeCommand(const std::vector<std::string>& parts) const;
|
||||
|
||||
std::string host_;
|
||||
int port_;
|
||||
std::string password_;
|
||||
int db_;
|
||||
int socket_fd_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::coordination
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "rdp_worker/cursor/cursor_update.hpp"
|
||||
|
||||
namespace rdp_worker::cursor {
|
||||
|
||||
class CursorAdapter {
|
||||
public:
|
||||
CursorUpdate MakePosition(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
bool visible) const;
|
||||
|
||||
CursorUpdate MakeSystem(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
std::uint32_t system_type) const;
|
||||
|
||||
CursorUpdate MakeColor(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int cache_index,
|
||||
std::uint64_t mask_bytes) const;
|
||||
|
||||
CursorUpdate MakeNew(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int cache_index,
|
||||
int xor_bpp,
|
||||
std::uint64_t mask_bytes) const;
|
||||
|
||||
CursorUpdate MakeCached(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int cache_index) const;
|
||||
|
||||
CursorUpdate MakeLarge(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int cache_index,
|
||||
int hot_spot_x,
|
||||
int hot_spot_y,
|
||||
int xor_bpp,
|
||||
std::uint64_t mask_bytes) const;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::cursor
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "rdp_worker/common/json.hpp"
|
||||
|
||||
namespace rdp_worker::cursor {
|
||||
|
||||
enum class CursorUpdateKind {
|
||||
kPosition,
|
||||
kSystem,
|
||||
kColor,
|
||||
kNew,
|
||||
kCached,
|
||||
kLarge,
|
||||
};
|
||||
|
||||
struct CursorUpdate {
|
||||
CursorUpdateKind kind{CursorUpdateKind::kPosition};
|
||||
std::uint64_t sequence{0};
|
||||
int desktop_width{0};
|
||||
int desktop_height{0};
|
||||
int x{0};
|
||||
int y{0};
|
||||
bool visible{true};
|
||||
bool shape_changed{false};
|
||||
int cache_index{-1};
|
||||
int hot_spot_x{0};
|
||||
int hot_spot_y{0};
|
||||
int width{0};
|
||||
int height{0};
|
||||
int xor_bpp{0};
|
||||
std::uint64_t mask_bytes{0};
|
||||
std::uint32_t system_type{0};
|
||||
};
|
||||
|
||||
const char* CursorUpdateKindName(CursorUpdateKind kind);
|
||||
|
||||
common::JsonObject CursorUpdateToPayload(const CursorUpdate& update,
|
||||
const std::string& render_quality_profile);
|
||||
|
||||
} // namespace rdp_worker::cursor
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
#include "rdp_worker/config/config.hpp"
|
||||
#include "rdp_worker/dataplane/token_validator.hpp"
|
||||
#include "rdp_worker/runtime/session_manager.hpp"
|
||||
|
||||
namespace rdp_worker::dataplane {
|
||||
|
||||
class DirectWssServer {
|
||||
public:
|
||||
DirectWssServer(config::Config config,
|
||||
std::shared_ptr<runtime::SessionManager> session_manager,
|
||||
std::shared_ptr<common::Logger> logger);
|
||||
~DirectWssServer();
|
||||
|
||||
DirectWssServer(const DirectWssServer&) = delete;
|
||||
DirectWssServer& operator=(const DirectWssServer&) = delete;
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void Run();
|
||||
void HandleConnection(boost::asio::ip::tcp::socket socket);
|
||||
bool ConsumeJti(const DataPlaneTokenClaims& claims);
|
||||
|
||||
config::Config config_;
|
||||
std::shared_ptr<runtime::SessionManager> session_manager_;
|
||||
std::shared_ptr<common::Logger> logger_;
|
||||
DataPlaneTokenValidator token_validator_;
|
||||
boost::asio::io_context io_context_;
|
||||
boost::asio::ssl::context ssl_context_;
|
||||
std::optional<boost::asio::ip::tcp::acceptor> acceptor_;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> stop_requested_{false};
|
||||
std::mutex jti_mutex_;
|
||||
std::map<std::string, std::chrono::system_clock::time_point> used_jti_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::dataplane
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace rdp_worker::dataplane {
|
||||
|
||||
struct DataPlaneTokenClaims {
|
||||
std::string session_id;
|
||||
std::string attachment_id;
|
||||
std::string user_id;
|
||||
std::string organization_id;
|
||||
std::string worker_id;
|
||||
std::string resource_id;
|
||||
std::vector<std::string> allowed_channels;
|
||||
std::int64_t expires_at_unix{0};
|
||||
std::string jti;
|
||||
};
|
||||
|
||||
struct TokenValidationResult {
|
||||
bool ok{false};
|
||||
std::string reason;
|
||||
DataPlaneTokenClaims claims;
|
||||
};
|
||||
|
||||
class DataPlaneTokenValidator {
|
||||
public:
|
||||
DataPlaneTokenValidator(std::string public_key_pem, std::string expected_worker_id);
|
||||
|
||||
[[nodiscard]] TokenValidationResult Validate(const std::string& token) const;
|
||||
|
||||
private:
|
||||
std::string public_key_pem_;
|
||||
std::string expected_worker_id_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::dataplane
|
||||
@@ -0,0 +1,268 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <freerdp/client/cliprdr.h>
|
||||
#include <freerdp/client/rdpgfx.h>
|
||||
#include <freerdp/event.h>
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/gdi/gdi.h>
|
||||
#include <freerdp/pointer.h>
|
||||
#include <freerdp/update.h>
|
||||
|
||||
#include "rdp_worker/cursor/cursor_adapter.hpp"
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::freerdp_runtime {
|
||||
|
||||
class RdpRuntime {
|
||||
public:
|
||||
explicit RdpRuntime(std::shared_ptr<common::Logger> logger);
|
||||
~RdpRuntime();
|
||||
|
||||
bool Start(const runtime::ConnectionSpec& spec);
|
||||
void Disconnect(bool terminate);
|
||||
bool IsConnected() const;
|
||||
bool PumpEvents(std::chrono::milliseconds timeout);
|
||||
int DesktopWidth() const;
|
||||
int DesktopHeight() const;
|
||||
bool SendFocusEvent(bool focused);
|
||||
bool SendKeyboardInput(uint16_t scan_code, bool key_down, bool extended);
|
||||
bool SendMouseMove(double normalized_x, double normalized_y);
|
||||
bool SendMouseButton(const std::string& button, bool pressed, double normalized_x, double normalized_y);
|
||||
bool SendMouseWheel(int wheel_delta, bool horizontal, double normalized_x, double normalized_y);
|
||||
bool SetClipboardText(const std::string& text);
|
||||
void MarkInputAppliedForGraphicsTrace(const std::string& correlation_id);
|
||||
std::optional<runtime::RenderNotification> CaptureFullFrameNotification(
|
||||
const std::string& state,
|
||||
const std::string& capture_source);
|
||||
std::optional<runtime::RenderNotification> PopRenderNotification();
|
||||
std::optional<runtime::ClipboardNotification> PopClipboardNotification();
|
||||
const std::string& RenderQualityProfile() const;
|
||||
|
||||
void InstallRenderHooks();
|
||||
void RemoveRenderHooks();
|
||||
void EnqueueRenderNotification(const std::string& type, common::JsonObject payload);
|
||||
void OnBeginPaint(rdpContext* context);
|
||||
void OnEndPaint(rdpContext* context);
|
||||
void OnDesktopResize(rdpContext* context);
|
||||
void OnBitmapUpdate(rdpContext* context, const BITMAP_UPDATE* bitmap);
|
||||
void OnRefreshRect(rdpContext* context, BYTE count, const RECTANGLE_16* areas);
|
||||
void OnSurfaceBits(rdpContext* context, const SURFACE_BITS_COMMAND* surface_bits);
|
||||
void OnSurfaceFrameMarker(rdpContext* context, const SURFACE_FRAME_MARKER* surface_frame_marker);
|
||||
void OnSurfaceFrameBits(rdpContext* context, const SURFACE_BITS_COMMAND* surface_bits, bool first, bool last, UINT32 frame_id);
|
||||
void OnChannelConnected(rdpContext* context, ChannelConnectedEventArgs* event_args);
|
||||
void OnChannelDisconnected(rdpContext* context, ChannelDisconnectedEventArgs* event_args);
|
||||
void OnPointerPosition(rdpContext* context, const POINTER_POSITION_UPDATE* pointer_position);
|
||||
void OnPointerSystem(rdpContext* context, const POINTER_SYSTEM_UPDATE* pointer_system);
|
||||
void OnPointerColor(rdpContext* context, const POINTER_COLOR_UPDATE* pointer_color);
|
||||
void OnPointerNew(rdpContext* context, const POINTER_NEW_UPDATE* pointer_new);
|
||||
void OnPointerCached(rdpContext* context, const POINTER_CACHED_UPDATE* pointer_cached);
|
||||
void OnPointerLarge(rdpContext* context, const POINTER_LARGE_UPDATE* pointer_large);
|
||||
UINT OnCliprdrServerCapabilities(CliprdrClientContext* context, const CLIPRDR_CAPABILITIES* capabilities);
|
||||
UINT OnCliprdrMonitorReady(CliprdrClientContext* context, const CLIPRDR_MONITOR_READY* monitor_ready);
|
||||
UINT OnCliprdrServerFormatList(CliprdrClientContext* context, const CLIPRDR_FORMAT_LIST* format_list);
|
||||
UINT OnCliprdrServerFormatListResponse(CliprdrClientContext* context, const CLIPRDR_FORMAT_LIST_RESPONSE* response);
|
||||
UINT OnCliprdrServerLockClipboardData(CliprdrClientContext* context, const CLIPRDR_LOCK_CLIPBOARD_DATA* lock_data);
|
||||
UINT OnCliprdrServerUnlockClipboardData(CliprdrClientContext* context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlock_data);
|
||||
UINT OnCliprdrServerFormatDataRequest(CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_REQUEST* request);
|
||||
UINT OnCliprdrServerFormatDataResponse(CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_RESPONSE* response);
|
||||
UINT OnCliprdrServerFileContentsRequest(CliprdrClientContext* context, const CLIPRDR_FILE_CONTENTS_REQUEST* request);
|
||||
UINT OnCliprdrServerFileContentsResponse(CliprdrClientContext* context, const CLIPRDR_FILE_CONTENTS_RESPONSE* response);
|
||||
|
||||
private:
|
||||
using DesktopResizeCallback = BOOL (*)(rdpContext*);
|
||||
using BitmapUpdateCallback = BOOL (*)(rdpContext*, const BITMAP_UPDATE*);
|
||||
using RefreshRectCallback = BOOL (*)(rdpContext*, BYTE, const RECTANGLE_16*);
|
||||
using PointerPositionCallback = BOOL (*)(rdpContext*, const POINTER_POSITION_UPDATE*);
|
||||
using BeginPaintCallback = BOOL (*)(rdpContext*);
|
||||
using EndPaintCallback = BOOL (*)(rdpContext*);
|
||||
using PointerSystemCallback = BOOL (*)(rdpContext*, const POINTER_SYSTEM_UPDATE*);
|
||||
using PointerColorCallback = BOOL (*)(rdpContext*, const POINTER_COLOR_UPDATE*);
|
||||
using PointerNewCallback = BOOL (*)(rdpContext*, const POINTER_NEW_UPDATE*);
|
||||
using PointerCachedCallback = BOOL (*)(rdpContext*, const POINTER_CACHED_UPDATE*);
|
||||
using PointerLargeCallback = BOOL (*)(rdpContext*, const POINTER_LARGE_UPDATE*);
|
||||
using SurfaceBitsCallback = BOOL (*)(rdpContext*, const SURFACE_BITS_COMMAND*);
|
||||
using SurfaceFrameMarkerCallback = BOOL (*)(rdpContext*, const SURFACE_FRAME_MARKER*);
|
||||
using SurfaceFrameBitsCallback = BOOL (*)(rdpContext*, const SURFACE_BITS_COMMAND*, BOOL, BOOL, UINT32);
|
||||
|
||||
struct RenderHooks {
|
||||
BeginPaintCallback begin_paint{};
|
||||
EndPaintCallback end_paint{};
|
||||
DesktopResizeCallback desktop_resize{};
|
||||
BitmapUpdateCallback bitmap_update{};
|
||||
RefreshRectCallback refresh_rect{};
|
||||
SurfaceBitsCallback surface_bits{};
|
||||
SurfaceFrameMarkerCallback surface_frame_marker{};
|
||||
SurfaceFrameBitsCallback surface_frame_bits{};
|
||||
PointerPositionCallback pointer_position{};
|
||||
PointerSystemCallback pointer_system{};
|
||||
PointerColorCallback pointer_color{};
|
||||
PointerNewCallback pointer_new{};
|
||||
PointerCachedCallback pointer_cached{};
|
||||
PointerLargeCallback pointer_large{};
|
||||
};
|
||||
|
||||
struct DirtyRegion {
|
||||
int x{};
|
||||
int y{};
|
||||
int width{};
|
||||
int height{};
|
||||
int rectangles{};
|
||||
};
|
||||
|
||||
struct CallbackPerfStats {
|
||||
std::uint64_t begin_paint{};
|
||||
std::uint64_t end_paint{};
|
||||
std::uint64_t desktop_resize{};
|
||||
std::uint64_t bitmap_update{};
|
||||
std::uint64_t refresh_rect{};
|
||||
std::uint64_t surface_bits{};
|
||||
std::uint64_t surface_frame_marker{};
|
||||
std::uint64_t surface_frame_bits{};
|
||||
std::uint64_t pointer_position{};
|
||||
std::uint64_t pointer_system{};
|
||||
std::uint64_t pointer_color{};
|
||||
std::uint64_t pointer_new{};
|
||||
std::uint64_t pointer_cached{};
|
||||
std::uint64_t pointer_large{};
|
||||
std::uint64_t frame_capture_full{};
|
||||
std::uint64_t frame_capture_region{};
|
||||
std::uint64_t periodic_polls{};
|
||||
std::uint64_t periodic_changes{};
|
||||
std::uint64_t periodic_no_changes{};
|
||||
std::uint64_t interactive_refresh_requests{};
|
||||
std::uint64_t bitmap_update_deferred{};
|
||||
std::uint64_t paint_flush_region{};
|
||||
std::uint64_t paint_flush_full{};
|
||||
std::uint64_t end_paint_change_fallback{};
|
||||
std::uint64_t end_paint_noop{};
|
||||
std::uint64_t rdpgfx_channel_connected{};
|
||||
std::uint64_t rdpgfx_channel_disconnected{};
|
||||
std::uint64_t rdpgfx_pipeline_init_success{};
|
||||
std::uint64_t rdpgfx_pipeline_init_failed{};
|
||||
std::uint64_t rdpgfx_fallback_to_gdi{};
|
||||
std::uint64_t cursor_updates_enqueued{};
|
||||
std::uint64_t event_pump_drained_checks{};
|
||||
std::uint64_t event_pump_wait_timeouts{};
|
||||
std::chrono::steady_clock::time_point first_callback_at{};
|
||||
std::chrono::steady_clock::time_point last_callback_at{};
|
||||
std::chrono::steady_clock::time_point last_summary_at{};
|
||||
std::string last_callback_name;
|
||||
};
|
||||
|
||||
bool ConfigureSettings(const runtime::ConnectionSpec& spec);
|
||||
void ConfigureQualityProfile(const runtime::ConnectionSpec& spec);
|
||||
bool ConfigureRestrictedDrive(const runtime::ConnectionSpec& spec);
|
||||
bool LoadClipboardChannel();
|
||||
bool SubscribeChannelEvents();
|
||||
void UnsubscribeChannelEvents();
|
||||
bool ConfigureClipboardChannel();
|
||||
bool SendClientClipboardCapabilities();
|
||||
bool SendClientClipboardFormatList();
|
||||
bool SendClientClipboardFormatListResponse(bool ok);
|
||||
bool SendClientClipboardDataRequest(UINT32 format_id);
|
||||
bool SendClientClipboardDataResponse(UINT32 format_id);
|
||||
void EnqueueClipboardText(std::string text, std::string origin);
|
||||
std::optional<runtime::RenderNotification> CaptureFrameNotification(
|
||||
const std::string& state,
|
||||
const std::optional<DirtyRegion>& dirty_region = std::nullopt,
|
||||
const std::string& capture_source = "explicit");
|
||||
std::optional<runtime::RenderNotification> CaptureChangedFrameNotification(
|
||||
const std::string& state,
|
||||
const std::string& detection_source = "periodic_change_detector");
|
||||
bool EnsureInputReady() const;
|
||||
uint16_t ScaleCoordinate(double normalized, int size) const;
|
||||
void RequestInteractiveFrameRefresh();
|
||||
void TryEnqueueInteractiveFrameCapture(const char* reason);
|
||||
std::optional<std::pair<std::string, std::int64_t>> RecentInputTraceDelay() const;
|
||||
void AccumulatePendingPaintRegion(const std::optional<DirtyRegion>& dirty_region, int rectangle_count);
|
||||
void ClearPendingPaintCycle();
|
||||
bool SyncPollSnapshotFromCapture(const std::vector<std::uint8_t>& frame_bytes,
|
||||
int frame_width,
|
||||
int frame_height,
|
||||
int frame_stride,
|
||||
const std::optional<DirtyRegion>& dirty_region,
|
||||
bool is_region);
|
||||
void MaybeLogMouseMoveRate();
|
||||
void RecordRdpCallback(const std::string& callback_name);
|
||||
void RecordFrameCapture(bool region);
|
||||
void RecordPeriodicPoll(bool changed);
|
||||
void MaybeLogPeriodicNoChange(const std::string& detection_source,
|
||||
std::chrono::steady_clock::time_point poll_started,
|
||||
std::chrono::steady_clock::time_point poll_completed);
|
||||
void MaybeLogCallbackSummary(const std::string& trigger);
|
||||
void MaybeLogFirstInputCallback(const std::string& callback_name, std::chrono::steady_clock::time_point callback_at);
|
||||
void LogRdpgfxFallbackIfNeeded(const std::string& reason);
|
||||
void LogSurfaceBitsEvent(const std::string& callback_name, const SURFACE_BITS_COMMAND* surface_bits) const;
|
||||
void EnqueueCursorUpdate(const cursor::CursorUpdate& update);
|
||||
void MaybeLogCursorRate(const cursor::CursorUpdate& update);
|
||||
void Cleanup();
|
||||
|
||||
std::shared_ptr<common::Logger> logger_;
|
||||
freerdp* instance_;
|
||||
std::atomic<bool> connected_;
|
||||
int desktop_width_;
|
||||
int desktop_height_;
|
||||
std::string render_quality_profile_;
|
||||
RenderHooks render_hooks_;
|
||||
mutable std::mutex render_mutex_;
|
||||
std::queue<runtime::RenderNotification> render_notifications_;
|
||||
std::queue<runtime::ClipboardNotification> clipboard_notifications_;
|
||||
cursor::CursorAdapter cursor_adapter_;
|
||||
CliprdrClientContext* cliprdr_context_{nullptr};
|
||||
RdpgfxClientContext* rdpgfx_context_{nullptr};
|
||||
bool rdpgfx_enabled_{false};
|
||||
bool channel_events_subscribed_{false};
|
||||
bool rdpgfx_pipeline_active_{false};
|
||||
bool rdpgfx_channel_seen_{false};
|
||||
bool rdpgfx_fallback_logged_{false};
|
||||
std::string client_clipboard_text_;
|
||||
std::string last_client_clipboard_hash_;
|
||||
UINT32 requested_server_clipboard_format_{0};
|
||||
std::uint64_t clipboard_sequence_{0};
|
||||
int cursor_x_{0};
|
||||
int cursor_y_{0};
|
||||
bool cursor_visible_{true};
|
||||
std::uint64_t cursor_sequence_{0};
|
||||
std::chrono::steady_clock::time_point last_cursor_rate_log_at_{};
|
||||
std::uint64_t cursor_updates_since_log_{0};
|
||||
int64_t frame_sequence_{0};
|
||||
std::atomic<bool> interactive_frame_refresh_requested_{false};
|
||||
std::atomic<std::uint32_t> interactive_frame_refresh_budget_{0};
|
||||
std::chrono::steady_clock::time_point last_interactive_frame_capture_at_{};
|
||||
std::chrono::steady_clock::time_point last_mouse_move_refresh_request_at_{};
|
||||
std::chrono::steady_clock::time_point post_input_capture_until_{};
|
||||
std::chrono::steady_clock::time_point next_post_input_capture_at_{};
|
||||
std::chrono::steady_clock::time_point last_mouse_move_rate_log_at_{};
|
||||
std::uint64_t mouse_moves_sent_since_log_{0};
|
||||
std::vector<std::uint8_t> last_polled_frame_;
|
||||
int last_polled_frame_width_{0};
|
||||
int last_polled_frame_height_{0};
|
||||
int last_polled_frame_stride_{0};
|
||||
std::chrono::steady_clock::time_point last_periodic_frame_poll_at_{};
|
||||
std::chrono::steady_clock::time_point last_periodic_no_change_log_at_{};
|
||||
std::uint64_t periodic_no_change_suppressed_since_log_{0};
|
||||
bool paint_cycle_active_{false};
|
||||
bool pending_paint_has_bitmap_update_{false};
|
||||
bool pending_paint_force_full_frame_{false};
|
||||
std::optional<DirtyRegion> pending_paint_dirty_region_;
|
||||
int pending_paint_dirty_rectangles_{0};
|
||||
std::uint64_t pending_paint_bitmap_updates_{0};
|
||||
mutable std::mutex input_trace_mutex_;
|
||||
std::string last_input_trace_correlation_id_;
|
||||
std::chrono::steady_clock::time_point last_input_trace_at_{};
|
||||
bool last_input_waiting_for_first_callback_{false};
|
||||
CallbackPerfStats callback_perf_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::freerdp_runtime
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "rdp_worker/graphics/render_update.hpp"
|
||||
|
||||
namespace rdp_worker::graphics {
|
||||
|
||||
struct GraphicsAdapterPolicy {
|
||||
int max_region_area_percent{60};
|
||||
bool allow_full_frame_fallback{true};
|
||||
bool prefer_encoded_updates{true};
|
||||
};
|
||||
|
||||
class GraphicsAdapter {
|
||||
public:
|
||||
explicit GraphicsAdapter(GraphicsAdapterPolicy policy = {});
|
||||
|
||||
[[nodiscard]] const GraphicsAdapterPolicy& Policy() const;
|
||||
|
||||
[[nodiscard]] RenderUpdate MakeFullBgraFrame(std::uint64_t sequence,
|
||||
int width,
|
||||
int height,
|
||||
int stride,
|
||||
std::vector<std::uint8_t> pixels,
|
||||
bool baseline) const;
|
||||
|
||||
[[nodiscard]] std::optional<RenderUpdate> TryMakeBgraRegion(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int stride,
|
||||
Rect region,
|
||||
std::vector<std::uint8_t> pixels) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool RegionAllowed(int desktop_width, int desktop_height, const Rect& region) const;
|
||||
|
||||
GraphicsAdapterPolicy policy_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::graphics
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace rdp_worker::graphics {
|
||||
|
||||
enum class RenderUpdateKind {
|
||||
kFullBgraFrame,
|
||||
kBgraRegion,
|
||||
kSurfaceCreate,
|
||||
kSurfaceDelete,
|
||||
kSurfaceBits,
|
||||
kEncodedFrame,
|
||||
kCursorUpdate,
|
||||
};
|
||||
|
||||
struct Rect {
|
||||
int x{0};
|
||||
int y{0};
|
||||
int width{0};
|
||||
int height{0};
|
||||
};
|
||||
|
||||
struct RenderUpdate {
|
||||
RenderUpdateKind kind{RenderUpdateKind::kFullBgraFrame};
|
||||
std::uint64_t sequence{0};
|
||||
int desktop_width{0};
|
||||
int desktop_height{0};
|
||||
int frame_width{0};
|
||||
int frame_height{0};
|
||||
int stride{0};
|
||||
Rect region;
|
||||
std::string pixel_format{"bgra32"};
|
||||
std::string codec;
|
||||
std::vector<std::uint8_t> payload;
|
||||
bool droppable{true};
|
||||
bool baseline{false};
|
||||
};
|
||||
|
||||
const char* RenderUpdateKindName(RenderUpdateKind kind);
|
||||
|
||||
bool IsFullFrameUpdate(const RenderUpdate& update);
|
||||
|
||||
bool IsRegionUpdate(const RenderUpdate& update);
|
||||
|
||||
bool IsEncodedUpdate(const RenderUpdate& update);
|
||||
|
||||
} // namespace rdp_worker::graphics
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "rdp_worker/dataplane/token_validator.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
struct DirectBindValidationResult {
|
||||
bool ok{false};
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
DirectBindValidationResult ValidateDirectDataPlaneBind(const Assignment& assignment,
|
||||
const dataplane::DataPlaneTokenClaims& claims);
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
class DirectEventSink {
|
||||
public:
|
||||
virtual ~DirectEventSink() = default;
|
||||
virtual std::string AttachmentId() const = 0;
|
||||
virtual void EnqueueEvent(const WorkerEvent& event) = 0;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rdp_worker/common/json.hpp"
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
enum class SessionState {
|
||||
kStarting,
|
||||
kActive,
|
||||
kDetached,
|
||||
kReconnecting,
|
||||
kTerminated,
|
||||
kFailed
|
||||
};
|
||||
|
||||
struct ConnectionSpec {
|
||||
std::string resource_id;
|
||||
std::string resource_name;
|
||||
std::string host;
|
||||
uint16_t port;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string domain;
|
||||
std::string certificate_verification_mode{"strict"};
|
||||
std::string render_quality_profile{"balanced"};
|
||||
std::string redirected_drive_name;
|
||||
std::string redirected_drive_path;
|
||||
bool insecure_skip_verify{false};
|
||||
};
|
||||
|
||||
struct SessionPolicy {
|
||||
std::chrono::seconds detach_grace_period{1800};
|
||||
std::string clipboard_mode{"disabled"};
|
||||
std::string file_transfer_mode{"disabled"};
|
||||
};
|
||||
|
||||
struct Assignment {
|
||||
std::string session_id;
|
||||
std::string worker_id;
|
||||
std::string attachment_id;
|
||||
std::string user_id;
|
||||
std::string organization_id;
|
||||
std::string device_id;
|
||||
std::optional<std::string> takeover_of;
|
||||
SessionState state{SessionState::kStarting};
|
||||
ConnectionSpec connection;
|
||||
SessionPolicy policy;
|
||||
};
|
||||
|
||||
struct WorkerLease {
|
||||
std::string lease_id;
|
||||
std::string worker_id;
|
||||
std::string session_id;
|
||||
std::string resource_id;
|
||||
std::string control_stream;
|
||||
std::vector<std::string> capabilities;
|
||||
std::string expires_at;
|
||||
};
|
||||
|
||||
struct WorkerEvent {
|
||||
std::string type;
|
||||
std::string session_id;
|
||||
std::string worker_id;
|
||||
std::string reason;
|
||||
int width{0};
|
||||
int height{0};
|
||||
common::JsonObject payload;
|
||||
std::vector<std::uint8_t> raw_frame_bytes;
|
||||
};
|
||||
|
||||
struct RenderNotification {
|
||||
std::string type;
|
||||
common::JsonObject payload;
|
||||
std::vector<std::uint8_t> raw_frame_bytes;
|
||||
};
|
||||
|
||||
struct ClipboardNotification {
|
||||
std::string text;
|
||||
std::string origin;
|
||||
std::string content_hash;
|
||||
std::uint64_t sequence_id{0};
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
#include "rdp_worker/coordination/control_plane.hpp"
|
||||
#include "rdp_worker/dataplane/token_validator.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
#include "rdp_worker/runtime/session_runtime.hpp"
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
class SessionManager {
|
||||
public:
|
||||
SessionManager(std::shared_ptr<coordination::ControlPlane> control_plane,
|
||||
std::shared_ptr<common::Logger> logger);
|
||||
|
||||
void ApplyAssignment(const Assignment& assignment);
|
||||
void StopAll();
|
||||
bool BindDirectDataPlaneAttachment(const dataplane::DataPlaneTokenClaims& claims, std::string& reason);
|
||||
std::shared_ptr<SessionRuntime> BindDirectDataPlaneRuntime(const dataplane::DataPlaneTokenClaims& claims, std::string& reason);
|
||||
|
||||
private:
|
||||
std::shared_ptr<coordination::ControlPlane> control_plane_;
|
||||
std::shared_ptr<common::Logger> logger_;
|
||||
std::mutex mutex_;
|
||||
std::map<std::string, std::shared_ptr<SessionRuntime>> sessions_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "rdp_worker/adapter/rdp_adapter_runtime.hpp"
|
||||
#include "rdp_worker/coordination/control_plane.hpp"
|
||||
#include "rdp_worker/runtime/direct_event_sink.hpp"
|
||||
#include "rdp_worker/runtime/models.hpp"
|
||||
|
||||
namespace rdp_worker::common {
|
||||
class Logger;
|
||||
}
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
class SessionRuntime {
|
||||
public:
|
||||
SessionRuntime(Assignment assignment,
|
||||
std::shared_ptr<coordination::ControlPlane> control_plane,
|
||||
std::shared_ptr<common::Logger> logger);
|
||||
~SessionRuntime();
|
||||
|
||||
void Start();
|
||||
void ApplyAssignment(const Assignment& assignment);
|
||||
void Stop(bool terminate, const std::string& reason);
|
||||
std::string SessionId() const;
|
||||
Assignment Snapshot() const;
|
||||
bool EnqueueDirectEnvelope(common::JsonObject envelope);
|
||||
void AddDirectEventSink(std::weak_ptr<DirectEventSink> sink);
|
||||
void RequestDirectFullFrameRepair(std::string reason);
|
||||
|
||||
private:
|
||||
void Run();
|
||||
void DrainAndHandleEnvelopes(const std::string& session_id);
|
||||
std::vector<common::JsonObject> DrainDirectEnvelopes(std::size_t max_count);
|
||||
void HandleEnvelopeBatch(const std::vector<common::JsonObject>& envelopes);
|
||||
void HandleEnvelope(const common::JsonObject& envelope);
|
||||
void HandleFileUpload(const common::JsonObject& payload);
|
||||
void HandleFileDownload(const common::JsonObject& payload);
|
||||
void ScanOutboundDownloadDirectory(const std::string& session_id);
|
||||
void PublishFileDownloadBlocked(const std::string& transfer_id, const std::string& file_id, const std::string& reason);
|
||||
std::filesystem::path PrepareVisibleTransferDirectory(const std::string& session_id);
|
||||
void CleanupVisibleTransferDirectory(const std::string& session_id);
|
||||
void CleanupSessionTransferDirectory(const std::string& session_id);
|
||||
void PublishDirectAttachBaselineIfRequested(const std::string& session_id);
|
||||
void DrainAndPublishRenderNotifications(const std::string& session_id);
|
||||
void PublishEvent(const std::string& type, const std::string& reason = {});
|
||||
void PublishEvent(const std::string& type, const std::string& reason, common::JsonObject payload);
|
||||
void DispatchDirectEvent(const WorkerEvent& event);
|
||||
bool HasCurrentDirectEventSink();
|
||||
|
||||
struct FileUploadState {
|
||||
std::string transfer_id;
|
||||
std::string file_name;
|
||||
std::filesystem::path temp_path;
|
||||
std::filesystem::path final_path;
|
||||
std::int64_t file_size{0};
|
||||
std::int64_t total_chunks{0};
|
||||
std::int64_t received{0};
|
||||
std::int64_t next_index{0};
|
||||
std::uint64_t hash{1469598103934665603ULL};
|
||||
std::string expected_hash;
|
||||
};
|
||||
|
||||
struct FileDownloadCandidate {
|
||||
std::string file_id;
|
||||
std::string file_name;
|
||||
std::filesystem::path path;
|
||||
std::filesystem::file_time_type modified_at{};
|
||||
std::int64_t file_size{0};
|
||||
std::uint64_t content_hash_value{1469598103934665603ULL};
|
||||
std::string content_hash;
|
||||
std::int64_t last_observed_size{-1};
|
||||
std::filesystem::file_time_type last_observed_modified_at{};
|
||||
int stable_observations{0};
|
||||
bool available_published{false};
|
||||
};
|
||||
|
||||
struct FileDownloadState {
|
||||
std::string transfer_id;
|
||||
std::string file_id;
|
||||
std::string file_name;
|
||||
std::filesystem::path path;
|
||||
std::filesystem::file_time_type modified_at{};
|
||||
std::int64_t file_size{0};
|
||||
std::int64_t sent{0};
|
||||
std::int64_t next_sequence{0};
|
||||
std::string content_hash;
|
||||
bool active{false};
|
||||
};
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
Assignment assignment_;
|
||||
std::shared_ptr<coordination::ControlPlane> control_plane_;
|
||||
std::shared_ptr<common::Logger> logger_;
|
||||
adapter::RdpAdapterRuntime rdp_adapter_;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> stop_requested_;
|
||||
std::atomic<bool> attached_;
|
||||
std::atomic<bool> direct_attach_baseline_requested_{false};
|
||||
bool focus_forward_logged_{false};
|
||||
bool keyboard_forward_logged_{false};
|
||||
bool mouse_forward_logged_{false};
|
||||
std::string last_input_correlation_id_;
|
||||
std::chrono::steady_clock::time_point last_input_applied_at_{};
|
||||
std::deque<RenderNotification> pending_render_frames_;
|
||||
std::optional<WorkerEvent> last_direct_render_frame_;
|
||||
bool direct_full_repair_requested_{false};
|
||||
std::string direct_full_repair_reason_;
|
||||
std::chrono::steady_clock::time_point last_frame_published_at_{};
|
||||
std::chrono::steady_clock::time_point last_region_loss_full_repair_at_{};
|
||||
std::chrono::steady_clock::time_point last_render_rate_logged_at_{};
|
||||
std::size_t render_frames_seen_since_log_{0};
|
||||
std::size_t render_frames_published_since_log_{0};
|
||||
std::size_t render_frames_dropped_since_log_{0};
|
||||
std::unordered_map<std::string, FileUploadState> uploads_;
|
||||
std::unordered_map<std::string, FileDownloadCandidate> download_candidates_;
|
||||
std::unordered_map<std::string, FileDownloadState> downloads_;
|
||||
std::chrono::steady_clock::time_point last_download_scan_at_{};
|
||||
std::int64_t file_download_event_sequence_{0};
|
||||
std::deque<common::JsonObject> direct_envelopes_;
|
||||
std::vector<std::weak_ptr<DirectEventSink>> direct_event_sinks_;
|
||||
};
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "rdp_worker/adapter/adapter_event_router.hpp"
|
||||
|
||||
#include "rdp_worker/common/json.hpp"
|
||||
|
||||
namespace rdp_worker::adapter {
|
||||
|
||||
namespace {
|
||||
|
||||
AdapterEventDescriptor MakeDescriptor(AdapterChannel channel,
|
||||
std::string_view type,
|
||||
bool adapter_origin) {
|
||||
return AdapterEventDescriptor{
|
||||
channel,
|
||||
type,
|
||||
adapter_origin,
|
||||
IsReliable(channel),
|
||||
IsDroppable(channel),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AdapterEventDescriptor AdapterEventRouter::DescribeRenderNotification(
|
||||
const runtime::RenderNotification& notification) const {
|
||||
if (notification.type == "session_frame") {
|
||||
const auto update_kind = common::GetString(notification.payload, "frame_update_kind").value_or("full");
|
||||
return MakeDescriptor(
|
||||
AdapterChannel::kDisplay,
|
||||
update_kind == "region" ? "display.region_bgra" : "display.baseline_full_bgra",
|
||||
true);
|
||||
}
|
||||
|
||||
if (notification.type == "session_cursor_updated") {
|
||||
return MakeDescriptor(AdapterChannel::kCursor, "cursor.update", true);
|
||||
}
|
||||
|
||||
if (notification.type == "session_render_resized") {
|
||||
return MakeDescriptor(AdapterChannel::kDisplay, "display.resize", true);
|
||||
}
|
||||
|
||||
if (notification.type == "session_render_dirty") {
|
||||
return MakeDescriptor(AdapterChannel::kDisplay, "display.dirty", true);
|
||||
}
|
||||
|
||||
return MakeDescriptor(AdapterChannel::kTelemetry, notification.type, true);
|
||||
}
|
||||
|
||||
AdapterEventDescriptor AdapterEventRouter::DescribeClipboardNotification(
|
||||
const runtime::ClipboardNotification&) const {
|
||||
return MakeDescriptor(AdapterChannel::kClipboard, "clipboard.server_text", true);
|
||||
}
|
||||
|
||||
AdapterEventDescriptor AdapterEventRouter::DescribeClientEnvelope(std::string_view envelope_type,
|
||||
std::string_view payload_kind,
|
||||
std::string_view payload_action) const {
|
||||
if (envelope_type == "input") {
|
||||
if (payload_kind == "mouse" && payload_action == "move") {
|
||||
return MakeDescriptor(AdapterChannel::kInput, "input.pointer_move", false);
|
||||
}
|
||||
if (payload_kind == "mouse") {
|
||||
return MakeDescriptor(AdapterChannel::kInput, "input.pointer", false);
|
||||
}
|
||||
if (payload_kind == "keyboard") {
|
||||
return MakeDescriptor(AdapterChannel::kInput, "input.keyboard", false);
|
||||
}
|
||||
if (payload_kind == "focus") {
|
||||
return MakeDescriptor(AdapterChannel::kInput, "input.focus", false);
|
||||
}
|
||||
return MakeDescriptor(AdapterChannel::kInput, "input.unknown", false);
|
||||
}
|
||||
|
||||
if (envelope_type == "clipboard") {
|
||||
return MakeDescriptor(AdapterChannel::kClipboard, "clipboard.client_text", false);
|
||||
}
|
||||
|
||||
if (envelope_type == "file_upload") {
|
||||
return MakeDescriptor(AdapterChannel::kFileTransfer, "file_upload.client_to_server", false);
|
||||
}
|
||||
|
||||
if (envelope_type == "control") {
|
||||
return MakeDescriptor(AdapterChannel::kControl, "control.command", false);
|
||||
}
|
||||
|
||||
return MakeDescriptor(AdapterChannel::kTelemetry, envelope_type, false);
|
||||
}
|
||||
|
||||
std::string AdapterEventDescriptorLogLine(const AdapterEventDescriptor& descriptor) {
|
||||
std::string line;
|
||||
line.reserve(160);
|
||||
line += "adapter_event channel=";
|
||||
line += ChannelName(descriptor.channel);
|
||||
line += " type=";
|
||||
line += descriptor.normalized_type;
|
||||
line += " origin=";
|
||||
line += descriptor.adapter_origin ? "adapter" : "client";
|
||||
line += " reliable=";
|
||||
line += descriptor.reliable ? "true" : "false";
|
||||
line += " droppable=";
|
||||
line += descriptor.droppable ? "true" : "false";
|
||||
return line;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::adapter
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "rdp_worker/adapter/rdp_adapter_runtime.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace rdp_worker::adapter {
|
||||
|
||||
RdpAdapterRuntime::RdpAdapterRuntime(std::shared_ptr<common::Logger> logger)
|
||||
: logger_(std::move(logger)),
|
||||
freerdp_(logger_) {}
|
||||
|
||||
bool RdpAdapterRuntime::Start(const runtime::ConnectionSpec& spec) {
|
||||
logger_->Info("rdp_adapter.runtime_start substrate=freerdp resource_id=" + spec.resource_id +
|
||||
" host=" + spec.host +
|
||||
" render_quality_profile=" + spec.render_quality_profile);
|
||||
lifecycle_logged_ = true;
|
||||
return freerdp_.Start(spec);
|
||||
}
|
||||
|
||||
void RdpAdapterRuntime::Disconnect(bool terminate) {
|
||||
logger_->Info("rdp_adapter.runtime_disconnect terminate=" + (terminate ? std::string("true") : std::string("false")));
|
||||
freerdp_.Disconnect(terminate);
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::IsConnected() const {
|
||||
return freerdp_.IsConnected();
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::PumpEvents(std::chrono::milliseconds timeout) {
|
||||
if (!lifecycle_logged_) {
|
||||
logger_->Info("rdp_adapter.event_pump_start substrate=freerdp");
|
||||
lifecycle_logged_ = true;
|
||||
}
|
||||
return freerdp_.PumpEvents(timeout);
|
||||
}
|
||||
|
||||
int RdpAdapterRuntime::DesktopWidth() const {
|
||||
return freerdp_.DesktopWidth();
|
||||
}
|
||||
|
||||
int RdpAdapterRuntime::DesktopHeight() const {
|
||||
return freerdp_.DesktopHeight();
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::SendFocusEvent(bool focused) {
|
||||
TraceClientEnvelope("input", "focus", focused ? "focus_in" : "focus_out");
|
||||
return freerdp_.SendFocusEvent(focused);
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::SendKeyboardInput(uint16_t scan_code, bool key_down, bool extended) {
|
||||
TraceClientEnvelope("input", "keyboard", key_down ? "key_down" : "key_up");
|
||||
return freerdp_.SendKeyboardInput(scan_code, key_down, extended);
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::SendMouseMove(double normalized_x, double normalized_y) {
|
||||
TraceClientEnvelope("input", "mouse", "move");
|
||||
return freerdp_.SendMouseMove(normalized_x, normalized_y);
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::SendMouseButton(const std::string& button,
|
||||
bool pressed,
|
||||
double normalized_x,
|
||||
double normalized_y) {
|
||||
TraceClientEnvelope("input", "mouse", pressed ? "button_down" : "button_up");
|
||||
return freerdp_.SendMouseButton(button, pressed, normalized_x, normalized_y);
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::SendMouseWheel(int wheel_delta, bool horizontal, double normalized_x, double normalized_y) {
|
||||
TraceClientEnvelope("input", "mouse", "wheel");
|
||||
return freerdp_.SendMouseWheel(wheel_delta, horizontal, normalized_x, normalized_y);
|
||||
}
|
||||
|
||||
bool RdpAdapterRuntime::SetClipboardText(const std::string& text) {
|
||||
TraceClientEnvelope("clipboard", "text", "client_to_server");
|
||||
return freerdp_.SetClipboardText(text);
|
||||
}
|
||||
|
||||
void RdpAdapterRuntime::MarkInputAppliedForGraphicsTrace(const std::string& correlation_id) {
|
||||
freerdp_.MarkInputAppliedForGraphicsTrace(correlation_id);
|
||||
}
|
||||
|
||||
std::optional<runtime::RenderNotification> RdpAdapterRuntime::CaptureFullFrameNotification(
|
||||
const std::string& state,
|
||||
const std::string& capture_source) {
|
||||
auto notification = freerdp_.CaptureFullFrameNotification(state, capture_source);
|
||||
if (notification.has_value()) {
|
||||
TraceAdapterEvent(event_router_.DescribeRenderNotification(*notification));
|
||||
}
|
||||
return notification;
|
||||
}
|
||||
|
||||
std::optional<runtime::RenderNotification> RdpAdapterRuntime::PopRenderNotification() {
|
||||
auto notification = freerdp_.PopRenderNotification();
|
||||
if (notification.has_value()) {
|
||||
TraceAdapterEvent(event_router_.DescribeRenderNotification(*notification));
|
||||
}
|
||||
return notification;
|
||||
}
|
||||
|
||||
std::optional<runtime::ClipboardNotification> RdpAdapterRuntime::PopClipboardNotification() {
|
||||
auto notification = freerdp_.PopClipboardNotification();
|
||||
if (notification.has_value()) {
|
||||
TraceAdapterEvent(event_router_.DescribeClipboardNotification(*notification));
|
||||
}
|
||||
return notification;
|
||||
}
|
||||
|
||||
const std::string& RdpAdapterRuntime::RenderQualityProfile() const {
|
||||
return freerdp_.RenderQualityProfile();
|
||||
}
|
||||
|
||||
const AdapterEventRouter& RdpAdapterRuntime::EventRouter() const {
|
||||
return event_router_;
|
||||
}
|
||||
|
||||
void RdpAdapterRuntime::TraceClientEnvelope(std::string_view envelope_type,
|
||||
std::string_view payload_kind,
|
||||
std::string_view payload_action) {
|
||||
const auto descriptor = event_router_.DescribeClientEnvelope(envelope_type, payload_kind, payload_action);
|
||||
if (descriptor.channel == AdapterChannel::kInput && descriptor.normalized_type == "input.pointer_move") {
|
||||
return;
|
||||
}
|
||||
TraceAdapterEvent(descriptor);
|
||||
}
|
||||
|
||||
void RdpAdapterRuntime::TraceAdapterEvent(const AdapterEventDescriptor& descriptor) {
|
||||
if (descriptor.channel == AdapterChannel::kDisplay &&
|
||||
descriptor.normalized_type != "display.resize" &&
|
||||
descriptor.normalized_type != "display.baseline_full_bgra") {
|
||||
return;
|
||||
}
|
||||
logger_->Info(AdapterEventDescriptorLogLine(descriptor));
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::adapter
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "rdp_worker/adapter/service_adapter_protocol.hpp"
|
||||
|
||||
namespace rdp_worker::adapter {
|
||||
|
||||
std::optional<ChannelSpec> FindChannelSpec(std::string_view name) {
|
||||
for (const auto& spec : AllChannelSpecs()) {
|
||||
if (spec.name == name) {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string_view ChannelName(AdapterChannel channel) {
|
||||
for (const auto& spec : AllChannelSpecs()) {
|
||||
if (spec.channel == channel) {
|
||||
return spec.name;
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::string_view DirectionName(ChannelDirection direction) {
|
||||
switch (direction) {
|
||||
case ChannelDirection::kClientToAdapter:
|
||||
return "client_to_adapter";
|
||||
case ChannelDirection::kAdapterToClient:
|
||||
return "adapter_to_client";
|
||||
case ChannelDirection::kBidirectional:
|
||||
return "bidirectional";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::string_view ReliabilityName(ChannelReliability reliability) {
|
||||
switch (reliability) {
|
||||
case ChannelReliability::kReliableOrdered:
|
||||
return "reliable_ordered";
|
||||
case ChannelReliability::kReliableChunked:
|
||||
return "reliable_chunked";
|
||||
case ChannelReliability::kDroppableLatest:
|
||||
return "droppable_latest";
|
||||
case ChannelReliability::kAdaptiveDroppable:
|
||||
return "adaptive_droppable";
|
||||
case ChannelReliability::kSampledDroppable:
|
||||
return "sampled_droppable";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
int PriorityValue(ChannelPriority priority) {
|
||||
return static_cast<int>(priority);
|
||||
}
|
||||
|
||||
bool IsDroppable(AdapterChannel channel) {
|
||||
for (const auto& spec : AllChannelSpecs()) {
|
||||
if (spec.channel == channel) {
|
||||
return spec.stale_updates_droppable;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsReliable(AdapterChannel channel) {
|
||||
for (const auto& spec : AllChannelSpecs()) {
|
||||
if (spec.channel == channel) {
|
||||
return spec.reliability == ChannelReliability::kReliableOrdered ||
|
||||
spec.reliability == ChannelReliability::kReliableChunked;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateAdapterChannelInvariants() {
|
||||
const auto input = FindChannelSpec("input");
|
||||
if (!input.has_value() || input->priority != ChannelPriority::kCritical || input->may_block_input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& spec : AllChannelSpecs()) {
|
||||
if (spec.channel != AdapterChannel::kInput &&
|
||||
PriorityValue(spec.priority) <= PriorityValue(ChannelPriority::kCritical)) {
|
||||
return false;
|
||||
}
|
||||
if (spec.may_block_input) {
|
||||
return false;
|
||||
}
|
||||
if ((spec.channel == AdapterChannel::kDisplay || spec.channel == AdapterChannel::kCursor) &&
|
||||
!spec.stale_updates_droppable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::adapter
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
#include "rdp_worker/common/json.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace rdp_worker::common {
|
||||
|
||||
JsonValue::JsonValue() : value(nullptr) {}
|
||||
JsonValue::JsonValue(std::nullptr_t) : value(nullptr) {}
|
||||
JsonValue::JsonValue(bool input) : value(input) {}
|
||||
JsonValue::JsonValue(double input) : value(input) {}
|
||||
JsonValue::JsonValue(int input) : value(static_cast<double>(input)) {}
|
||||
JsonValue::JsonValue(const char* input) : value(std::string(input)) {}
|
||||
JsonValue::JsonValue(std::string input) : value(std::move(input)) {}
|
||||
JsonValue::JsonValue(JsonArray input) : value(std::move(input)) {}
|
||||
JsonValue::JsonValue(JsonObject input) : value(std::move(input)) {}
|
||||
|
||||
bool JsonValue::IsObject() const { return std::holds_alternative<JsonObject>(value); }
|
||||
bool JsonValue::IsArray() const { return std::holds_alternative<JsonArray>(value); }
|
||||
bool JsonValue::IsString() const { return std::holds_alternative<std::string>(value); }
|
||||
bool JsonValue::IsBool() const { return std::holds_alternative<bool>(value); }
|
||||
bool JsonValue::IsNumber() const { return std::holds_alternative<double>(value); }
|
||||
const JsonObject& JsonValue::AsObject() const { return std::get<JsonObject>(value); }
|
||||
const JsonArray& JsonValue::AsArray() const { return std::get<JsonArray>(value); }
|
||||
const std::string& JsonValue::AsString() const { return std::get<std::string>(value); }
|
||||
bool JsonValue::AsBool() const { return std::get<bool>(value); }
|
||||
double JsonValue::AsNumber() const { return std::get<double>(value); }
|
||||
|
||||
namespace {
|
||||
|
||||
void AppendUtf8(std::string& output, std::uint32_t codepoint) {
|
||||
if (codepoint <= 0x7F) {
|
||||
output.push_back(static_cast<char>(codepoint));
|
||||
} else if (codepoint <= 0x7FF) {
|
||||
output.push_back(static_cast<char>(0xC0 | (codepoint >> 6)));
|
||||
output.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
|
||||
} else if (codepoint <= 0xFFFF) {
|
||||
output.push_back(static_cast<char>(0xE0 | (codepoint >> 12)));
|
||||
output.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F)));
|
||||
output.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
|
||||
} else if (codepoint <= 0x10FFFF) {
|
||||
output.push_back(static_cast<char>(0xF0 | (codepoint >> 18)));
|
||||
output.push_back(static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F)));
|
||||
output.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F)));
|
||||
output.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
|
||||
} else {
|
||||
throw std::runtime_error("invalid JSON unicode escape");
|
||||
}
|
||||
}
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
explicit Parser(const std::string& input) : input_(input), index_(0) {}
|
||||
|
||||
JsonValue Parse() {
|
||||
SkipWhitespace();
|
||||
JsonValue value = ParseValue();
|
||||
SkipWhitespace();
|
||||
if (index_ != input_.size()) {
|
||||
throw std::runtime_error("unexpected trailing JSON data");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
JsonValue ParseValue() {
|
||||
SkipWhitespace();
|
||||
if (Match("null")) {
|
||||
return JsonValue(nullptr);
|
||||
}
|
||||
if (Match("true")) {
|
||||
return JsonValue(true);
|
||||
}
|
||||
if (Match("false")) {
|
||||
return JsonValue(false);
|
||||
}
|
||||
if (Peek() == '"') {
|
||||
return JsonValue(ParseString());
|
||||
}
|
||||
if (Peek() == '{') {
|
||||
return JsonValue(ParseObject());
|
||||
}
|
||||
if (Peek() == '[') {
|
||||
return JsonValue(ParseArray());
|
||||
}
|
||||
if (Peek() == '-' || std::isdigit(static_cast<unsigned char>(Peek()))) {
|
||||
return JsonValue(ParseNumber());
|
||||
}
|
||||
throw std::runtime_error("unexpected JSON token");
|
||||
}
|
||||
|
||||
JsonObject ParseObject() {
|
||||
Expect('{');
|
||||
JsonObject object;
|
||||
SkipWhitespace();
|
||||
if (Peek() == '}') {
|
||||
Advance();
|
||||
return object;
|
||||
}
|
||||
while (true) {
|
||||
const std::string key = ParseString();
|
||||
SkipWhitespace();
|
||||
Expect(':');
|
||||
object.emplace(key, ParseValue());
|
||||
SkipWhitespace();
|
||||
if (Peek() == '}') {
|
||||
Advance();
|
||||
break;
|
||||
}
|
||||
Expect(',');
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
JsonArray ParseArray() {
|
||||
Expect('[');
|
||||
JsonArray array;
|
||||
SkipWhitespace();
|
||||
if (Peek() == ']') {
|
||||
Advance();
|
||||
return array;
|
||||
}
|
||||
while (true) {
|
||||
array.emplace_back(ParseValue());
|
||||
SkipWhitespace();
|
||||
if (Peek() == ']') {
|
||||
Advance();
|
||||
break;
|
||||
}
|
||||
Expect(',');
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
std::string ParseString() {
|
||||
Expect('"');
|
||||
std::string output;
|
||||
while (index_ < input_.size()) {
|
||||
const char current = input_[index_++];
|
||||
if (current == '"') {
|
||||
return output;
|
||||
}
|
||||
if (current == '\\') {
|
||||
if (index_ >= input_.size()) {
|
||||
throw std::runtime_error("invalid JSON escape");
|
||||
}
|
||||
const char escaped = input_[index_++];
|
||||
switch (escaped) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
output.push_back(escaped);
|
||||
break;
|
||||
case 'b':
|
||||
output.push_back('\b');
|
||||
break;
|
||||
case 'f':
|
||||
output.push_back('\f');
|
||||
break;
|
||||
case 'n':
|
||||
output.push_back('\n');
|
||||
break;
|
||||
case 'r':
|
||||
output.push_back('\r');
|
||||
break;
|
||||
case 't':
|
||||
output.push_back('\t');
|
||||
break;
|
||||
case 'u':
|
||||
AppendUtf8(output, ParseUnicodeEscape());
|
||||
break;
|
||||
default:
|
||||
throw std::runtime_error("unsupported JSON escape");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
output.push_back(current);
|
||||
}
|
||||
throw std::runtime_error("unterminated JSON string");
|
||||
}
|
||||
|
||||
std::uint32_t ParseUnicodeEscape() {
|
||||
const std::uint32_t first = ParseHexQuad();
|
||||
if (first >= 0xD800 && first <= 0xDBFF) {
|
||||
if (index_ + 1 >= input_.size() || input_[index_] != '\\' || input_[index_ + 1] != 'u') {
|
||||
throw std::runtime_error("invalid JSON unicode surrogate");
|
||||
}
|
||||
index_ += 2;
|
||||
const std::uint32_t second = ParseHexQuad();
|
||||
if (second < 0xDC00 || second > 0xDFFF) {
|
||||
throw std::runtime_error("invalid JSON unicode surrogate");
|
||||
}
|
||||
return 0x10000 + (((first - 0xD800) << 10) | (second - 0xDC00));
|
||||
}
|
||||
if (first >= 0xDC00 && first <= 0xDFFF) {
|
||||
throw std::runtime_error("invalid JSON unicode surrogate");
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
std::uint32_t ParseHexQuad() {
|
||||
if (index_ + 4 > input_.size()) {
|
||||
throw std::runtime_error("invalid JSON unicode escape");
|
||||
}
|
||||
std::uint32_t value = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const char ch = input_[index_++];
|
||||
value <<= 4;
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
value |= static_cast<std::uint32_t>(ch - '0');
|
||||
} else if (ch >= 'a' && ch <= 'f') {
|
||||
value |= static_cast<std::uint32_t>(ch - 'a' + 10);
|
||||
} else if (ch >= 'A' && ch <= 'F') {
|
||||
value |= static_cast<std::uint32_t>(ch - 'A' + 10);
|
||||
} else {
|
||||
throw std::runtime_error("invalid JSON unicode escape");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
double ParseNumber() {
|
||||
const std::size_t start = index_;
|
||||
if (Peek() == '-') {
|
||||
Advance();
|
||||
}
|
||||
while (std::isdigit(static_cast<unsigned char>(Peek()))) {
|
||||
Advance();
|
||||
}
|
||||
if (Peek() == '.') {
|
||||
Advance();
|
||||
while (std::isdigit(static_cast<unsigned char>(Peek()))) {
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
return std::stod(input_.substr(start, index_ - start));
|
||||
}
|
||||
|
||||
void SkipWhitespace() {
|
||||
while (index_ < input_.size() && std::isspace(static_cast<unsigned char>(input_[index_]))) {
|
||||
++index_;
|
||||
}
|
||||
}
|
||||
|
||||
char Peek() const {
|
||||
if (index_ >= input_.size()) {
|
||||
return '\0';
|
||||
}
|
||||
return input_[index_];
|
||||
}
|
||||
|
||||
void Advance() {
|
||||
if (index_ < input_.size()) {
|
||||
++index_;
|
||||
}
|
||||
}
|
||||
|
||||
void Expect(char expected) {
|
||||
SkipWhitespace();
|
||||
if (Peek() != expected) {
|
||||
throw std::runtime_error("unexpected JSON character");
|
||||
}
|
||||
Advance();
|
||||
}
|
||||
|
||||
bool Match(const char* keyword) {
|
||||
const std::size_t length = std::char_traits<char>::length(keyword);
|
||||
if (input_.substr(index_, length) == keyword) {
|
||||
index_ += length;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& input_;
|
||||
std::size_t index_;
|
||||
};
|
||||
|
||||
std::string Escape(const std::string& value) {
|
||||
std::ostringstream output;
|
||||
for (const char ch : value) {
|
||||
switch (ch) {
|
||||
case '"':
|
||||
output << "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
output << "\\\\";
|
||||
break;
|
||||
case '\n':
|
||||
output << "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
output << "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
output << "\\t";
|
||||
break;
|
||||
default:
|
||||
output << ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::string SerializeInternal(const JsonValue& value) {
|
||||
if (std::holds_alternative<std::nullptr_t>(value.value)) {
|
||||
return "null";
|
||||
}
|
||||
if (std::holds_alternative<bool>(value.value)) {
|
||||
return std::get<bool>(value.value) ? "true" : "false";
|
||||
}
|
||||
if (std::holds_alternative<double>(value.value)) {
|
||||
std::ostringstream output;
|
||||
output << std::get<double>(value.value);
|
||||
return output.str();
|
||||
}
|
||||
if (std::holds_alternative<std::string>(value.value)) {
|
||||
return "\"" + Escape(std::get<std::string>(value.value)) + "\"";
|
||||
}
|
||||
if (std::holds_alternative<JsonArray>(value.value)) {
|
||||
std::ostringstream output;
|
||||
output << "[";
|
||||
const auto& array = std::get<JsonArray>(value.value);
|
||||
for (std::size_t i = 0; i < array.size(); ++i) {
|
||||
if (i > 0) {
|
||||
output << ",";
|
||||
}
|
||||
output << SerializeInternal(array[i]);
|
||||
}
|
||||
output << "]";
|
||||
return output.str();
|
||||
}
|
||||
std::ostringstream output;
|
||||
output << "{";
|
||||
const auto& object = std::get<JsonObject>(value.value);
|
||||
bool first = true;
|
||||
for (const auto& [key, child] : object) {
|
||||
if (!first) {
|
||||
output << ",";
|
||||
}
|
||||
first = false;
|
||||
output << "\"" << Escape(key) << "\":" << SerializeInternal(child);
|
||||
}
|
||||
output << "}";
|
||||
return output.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
JsonValue ParseJson(const std::string& input) {
|
||||
return Parser(input).Parse();
|
||||
}
|
||||
|
||||
std::string SerializeJson(const JsonValue& value) {
|
||||
return SerializeInternal(value);
|
||||
}
|
||||
|
||||
std::optional<std::string> GetString(const JsonObject& object, const std::string& key) {
|
||||
auto iterator = object.find(key);
|
||||
if (iterator == object.end() || !iterator->second.IsString()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return iterator->second.AsString();
|
||||
}
|
||||
|
||||
std::optional<bool> GetBool(const JsonObject& object, const std::string& key) {
|
||||
auto iterator = object.find(key);
|
||||
if (iterator == object.end() || !iterator->second.IsBool()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return iterator->second.AsBool();
|
||||
}
|
||||
|
||||
std::optional<double> GetNumber(const JsonObject& object, const std::string& key) {
|
||||
auto iterator = object.find(key);
|
||||
if (iterator == object.end() || !iterator->second.IsNumber()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return iterator->second.AsNumber();
|
||||
}
|
||||
|
||||
const JsonObject* GetObject(const JsonObject& object, const std::string& key) {
|
||||
auto iterator = object.find(key);
|
||||
if (iterator == object.end() || !iterator->second.IsObject()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &iterator->second.AsObject();
|
||||
}
|
||||
|
||||
const JsonArray* GetArray(const JsonObject& object, const std::string& key) {
|
||||
auto iterator = object.find(key);
|
||||
if (iterator == object.end() || !iterator->second.IsArray()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &iterator->second.AsArray();
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::common
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "rdp_worker/common/time.hpp"
|
||||
|
||||
namespace rdp_worker::common {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string LevelToString(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel::kDebug:
|
||||
return "DEBUG";
|
||||
case LogLevel::kInfo:
|
||||
return "INFO";
|
||||
case LogLevel::kWarn:
|
||||
return "WARN";
|
||||
case LogLevel::kError:
|
||||
return "ERROR";
|
||||
}
|
||||
return "INFO";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Logger::Logger(std::string service_name) : service_name_(std::move(service_name)) {}
|
||||
|
||||
void Logger::Debug(const std::string& message) {
|
||||
Write(LogLevel::kDebug, message);
|
||||
}
|
||||
|
||||
void Logger::Info(const std::string& message) {
|
||||
Write(LogLevel::kInfo, message);
|
||||
}
|
||||
|
||||
void Logger::Warn(const std::string& message) {
|
||||
Write(LogLevel::kWarn, message);
|
||||
}
|
||||
|
||||
void Logger::Error(const std::string& message) {
|
||||
Write(LogLevel::kError, message);
|
||||
}
|
||||
|
||||
void Logger::Write(LogLevel level, const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::cout << "{\"ts\":\"" << ToRfc3339(NowUtc()) << "\",\"service\":\"" << service_name_
|
||||
<< "\",\"level\":\"" << LevelToString(level) << "\",\"message\":\"" << message
|
||||
<< "\"}" << std::endl;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::common
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "rdp_worker/common/time.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace rdp_worker::common {
|
||||
|
||||
Clock::time_point NowUtc() {
|
||||
return Clock::now();
|
||||
}
|
||||
|
||||
std::string ToRfc3339(Clock::time_point time_point) {
|
||||
const std::time_t raw = Clock::to_time_t(time_point);
|
||||
std::tm utc_tm{};
|
||||
#if defined(_WIN32)
|
||||
gmtime_s(&utc_tm, &raw);
|
||||
#else
|
||||
gmtime_r(&raw, &utc_tm);
|
||||
#endif
|
||||
std::ostringstream output;
|
||||
output << std::put_time(&utc_tm, "%Y-%m-%dT%H:%M:%SZ");
|
||||
return output.str();
|
||||
}
|
||||
|
||||
Clock::time_point ParseRfc3339(const std::string& value) {
|
||||
std::tm utc_tm{};
|
||||
std::istringstream input(value);
|
||||
input >> std::get_time(&utc_tm, "%Y-%m-%dT%H:%M:%SZ");
|
||||
if (input.fail()) {
|
||||
throw std::runtime_error("failed to parse RFC3339 timestamp: " + value);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const std::time_t raw = _mkgmtime(&utc_tm);
|
||||
#else
|
||||
const std::time_t raw = timegm(&utc_tm);
|
||||
#endif
|
||||
return Clock::from_time_t(raw);
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::common
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "rdp_worker/config/config.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace rdp_worker::config {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string GetEnvOrDefault(const char* key, const char* fallback) {
|
||||
const char* value = std::getenv(key);
|
||||
if (value == nullptr || std::string(value).empty()) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int GetInt(const char* key, int fallback) {
|
||||
const char* value = std::getenv(key);
|
||||
if (value == nullptr || std::string(value).empty()) {
|
||||
return fallback;
|
||||
}
|
||||
return std::stoi(value);
|
||||
}
|
||||
|
||||
bool GetBool(const char* key, bool fallback) {
|
||||
const char* value = std::getenv(key);
|
||||
if (value == nullptr || std::string(value).empty()) {
|
||||
return fallback;
|
||||
}
|
||||
const std::string raw(value);
|
||||
return raw == "1" || raw == "true" || raw == "TRUE" || raw == "yes" || raw == "on";
|
||||
}
|
||||
|
||||
std::string ReadFileIfConfigured(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return "";
|
||||
}
|
||||
std::ifstream input(path);
|
||||
if (!input.good()) {
|
||||
throw std::runtime_error("failed to read file " + path);
|
||||
}
|
||||
std::stringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::vector<std::string> Split(const std::string& value) {
|
||||
std::vector<std::string> parts;
|
||||
std::stringstream stream(value);
|
||||
std::string part;
|
||||
while (std::getline(stream, part, ',')) {
|
||||
if (!part.empty()) {
|
||||
parts.push_back(part);
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Config LoadFromEnv() {
|
||||
Config config{};
|
||||
config.worker_id = GetEnvOrDefault("RDP_WORKER_ID", "rdp-worker-1");
|
||||
config.redis_host = GetEnvOrDefault("RDP_WORKER_REDIS_HOST", "127.0.0.1");
|
||||
config.redis_port = GetInt("RDP_WORKER_REDIS_PORT", 6379);
|
||||
config.redis_password = GetEnvOrDefault("RDP_WORKER_REDIS_PASSWORD", "");
|
||||
config.redis_db = GetInt("RDP_WORKER_REDIS_DB", 0);
|
||||
config.worker_heartbeat_interval = std::chrono::seconds(GetInt("RDP_WORKER_HEARTBEAT_INTERVAL_SECONDS", 5));
|
||||
config.lease_renew_interval = std::chrono::seconds(GetInt("RDP_WORKER_LEASE_RENEW_INTERVAL_SECONDS", 10));
|
||||
config.assignment_poll_interval = std::chrono::seconds(GetInt("RDP_WORKER_ASSIGNMENT_POLL_INTERVAL_SECONDS", 2));
|
||||
config.insecure_skip_verify = GetBool("RDP_WORKER_INSECURE_SKIP_VERIFY", false);
|
||||
config.capabilities = Split(GetEnvOrDefault("RDP_WORKER_CAPABILITIES", "adaptive-quality,dirty-rects,clipboard,file-transfer"));
|
||||
config.data_plane_enabled = GetBool("RDP_WORKER_DATA_PLANE_ENABLED", false);
|
||||
config.data_plane_listen_host = GetEnvOrDefault("RDP_WORKER_DATA_PLANE_LISTEN_HOST", "0.0.0.0");
|
||||
config.data_plane_listen_port = GetInt("RDP_WORKER_DATA_PLANE_LISTEN_PORT", 8443);
|
||||
config.data_plane_public_key_pem = GetEnvOrDefault("RDP_WORKER_DATA_PLANE_PUBLIC_KEY_PEM", "");
|
||||
config.data_plane_public_key_file = GetEnvOrDefault("RDP_WORKER_DATA_PLANE_PUBLIC_KEY_FILE", "");
|
||||
if (config.data_plane_public_key_pem.empty()) {
|
||||
config.data_plane_public_key_pem = ReadFileIfConfigured(config.data_plane_public_key_file);
|
||||
}
|
||||
config.data_plane_tls_cert_file = GetEnvOrDefault("RDP_WORKER_DATA_PLANE_TLS_CERT_FILE", "");
|
||||
config.data_plane_tls_key_file = GetEnvOrDefault("RDP_WORKER_DATA_PLANE_TLS_KEY_FILE", "");
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::config
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "rdp_worker/coordination/control_plane.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
#include "rdp_worker/common/time.hpp"
|
||||
|
||||
namespace rdp_worker::coordination {
|
||||
|
||||
using common::GetArray;
|
||||
using common::GetBool;
|
||||
using common::GetNumber;
|
||||
using common::GetObject;
|
||||
using common::GetString;
|
||||
using common::JsonArray;
|
||||
using common::JsonObject;
|
||||
using common::JsonValue;
|
||||
|
||||
ControlPlane::ControlPlane(config::Config config, std::shared_ptr<common::Logger> logger)
|
||||
: config_(std::move(config)),
|
||||
logger_(std::move(logger)),
|
||||
redis_(std::make_unique<RedisClient>(config_.redis_host, config_.redis_port, config_.redis_password, config_.redis_db)) {}
|
||||
|
||||
void ControlPlane::Connect() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
redis_->Connect();
|
||||
}
|
||||
|
||||
void ControlPlane::RegisterWorker() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
redis_->Set("worker:registration:" + config_.worker_id, WorkerRegistrationPayload(), config_.worker_heartbeat_interval * 3);
|
||||
redis_->SAdd("worker:registrations", config_.worker_id);
|
||||
}
|
||||
|
||||
void ControlPlane::ReleaseOwnedLeasesOnStartup() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
int released = 0;
|
||||
for (const auto& lease_id : redis_->SMembers("worker:leases")) {
|
||||
auto encoded = redis_->Get("worker:lease:" + lease_id);
|
||||
if (!encoded.has_value()) {
|
||||
redis_->SRem("worker:leases", lease_id);
|
||||
continue;
|
||||
}
|
||||
auto lease = ParseLease(common::ParseJson(*encoded).AsObject());
|
||||
if (lease.worker_id != config_.worker_id) {
|
||||
continue;
|
||||
}
|
||||
redis_->Delete("worker:lease:" + lease_id);
|
||||
redis_->SRem("worker:leases", lease_id);
|
||||
if (!lease.session_id.empty()) {
|
||||
redis_->Delete("worker:session-lease:" + lease.session_id);
|
||||
redis_->Delete("worker:queue:" + lease.session_id);
|
||||
}
|
||||
++released;
|
||||
}
|
||||
if (released > 0) {
|
||||
logger_->Warn("released stale owned worker leases on startup worker=" + config_.worker_id +
|
||||
" released_count=" + std::to_string(released));
|
||||
}
|
||||
}
|
||||
|
||||
void ControlPlane::SendHeartbeat() {
|
||||
RegisterWorker();
|
||||
}
|
||||
|
||||
std::optional<runtime::Assignment> ControlPlane::PollAssignment(std::chrono::seconds timeout) {
|
||||
RedisClient stream_client(config_.redis_host, config_.redis_port, config_.redis_password, config_.redis_db);
|
||||
stream_client.Connect();
|
||||
auto entry = stream_client.BLPop("worker:control:" + config_.worker_id, timeout);
|
||||
if (!entry.has_value() || entry->size() != 2) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const JsonObject object = common::ParseJson((*entry)[1]).AsObject();
|
||||
return ParseAssignment(object);
|
||||
}
|
||||
|
||||
std::optional<common::JsonObject> ControlPlane::PollSessionEnvelope(const std::string& session_id, std::chrono::seconds timeout) {
|
||||
RedisClient stream_client(config_.redis_host, config_.redis_port, config_.redis_password, config_.redis_db);
|
||||
stream_client.Connect();
|
||||
auto entry = stream_client.BLPop("worker:queue:" + session_id, timeout);
|
||||
if (!entry.has_value() || entry->size() != 2) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto object = common::ParseJson((*entry)[1]).AsObject();
|
||||
const JsonObject* payload = GetObject(object, "payload");
|
||||
if (payload != nullptr) {
|
||||
const std::string type = GetString(object, "type").value_or("");
|
||||
const std::string correlation_id = GetString(*payload, "correlation_id").value_or("");
|
||||
if (type == "input" && !correlation_id.empty()) {
|
||||
logger_->Info("input.trace worker_queue_pop session=" + session_id +
|
||||
" correlation_id=" + correlation_id +
|
||||
" trace_stage=worker_queue_pop");
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
std::vector<common::JsonObject> ControlPlane::DrainSessionEnvelopes(const std::string& session_id, std::size_t max_count) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<common::JsonObject> output;
|
||||
output.reserve(max_count);
|
||||
const std::string key = "worker:queue:" + session_id;
|
||||
for (std::size_t i = 0; i < max_count; ++i) {
|
||||
auto encoded = redis_->LPop(key);
|
||||
if (!encoded.has_value()) {
|
||||
break;
|
||||
}
|
||||
auto object = common::ParseJson(*encoded).AsObject();
|
||||
const JsonObject* payload = GetObject(object, "payload");
|
||||
if (payload != nullptr) {
|
||||
const std::string type = GetString(object, "type").value_or("");
|
||||
const std::string correlation_id = GetString(*payload, "correlation_id").value_or("");
|
||||
if (type == "input" && !correlation_id.empty()) {
|
||||
logger_->Info("input.trace worker_queue_pop session=" + session_id +
|
||||
" correlation_id=" + correlation_id +
|
||||
" trace_stage=worker_queue_pop");
|
||||
}
|
||||
}
|
||||
output.push_back(std::move(object));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
int64_t ControlPlane::SessionEnvelopeQueueLength(const std::string& session_id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return redis_->LLen("worker:queue:" + session_id);
|
||||
}
|
||||
|
||||
std::optional<runtime::WorkerLease> ControlPlane::GetLeaseBySession(const std::string& session_id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto lease_id = redis_->Get("worker:session-lease:" + session_id);
|
||||
if (!lease_id.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto encoded = redis_->Get("worker:lease:" + *lease_id);
|
||||
if (!encoded.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return ParseLease(common::ParseJson(*encoded).AsObject());
|
||||
}
|
||||
|
||||
void ControlPlane::RenewLease(const runtime::WorkerLease& lease) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
redis_->Set("worker:lease:" + lease.lease_id, LeasePayload(lease), std::chrono::seconds(45));
|
||||
redis_->Set("worker:session-lease:" + lease.session_id, lease.lease_id, std::chrono::seconds(45));
|
||||
}
|
||||
|
||||
void ControlPlane::ReleaseLease(const runtime::WorkerLease& lease) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
redis_->Delete("worker:lease:" + lease.lease_id);
|
||||
redis_->Delete("worker:session-lease:" + lease.session_id);
|
||||
}
|
||||
|
||||
void ControlPlane::PublishEvent(const runtime::WorkerEvent& event) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const std::string encoded = EventPayload(event);
|
||||
redis_->RPush("worker:events", encoded);
|
||||
redis_->Expire("worker:events", std::chrono::minutes(10));
|
||||
}
|
||||
|
||||
runtime::Assignment ControlPlane::ParseAssignment(const JsonObject& object) const {
|
||||
runtime::Assignment assignment{};
|
||||
assignment.session_id = GetString(object, "session_id").value_or("");
|
||||
assignment.worker_id = GetString(object, "worker_id").value_or("");
|
||||
assignment.attachment_id = GetString(object, "attachment_id").value_or("");
|
||||
assignment.user_id = GetString(object, "user_id").value_or("");
|
||||
assignment.device_id = GetString(object, "device_id").value_or("");
|
||||
assignment.takeover_of = GetString(object, "takeover_of");
|
||||
const std::string state = GetString(object, "state").value_or("starting");
|
||||
if (state == "active") {
|
||||
assignment.state = runtime::SessionState::kActive;
|
||||
} else if (state == "detached") {
|
||||
assignment.state = runtime::SessionState::kDetached;
|
||||
} else if (state == "reconnecting") {
|
||||
assignment.state = runtime::SessionState::kReconnecting;
|
||||
} else {
|
||||
assignment.state = runtime::SessionState::kStarting;
|
||||
}
|
||||
|
||||
const JsonObject* metadata = GetObject(object, "metadata");
|
||||
if (metadata == nullptr) {
|
||||
throw std::runtime_error("assignment metadata is required");
|
||||
}
|
||||
const JsonObject* resource = GetObject(*metadata, "resource");
|
||||
if (resource == nullptr) {
|
||||
throw std::runtime_error("assignment resource metadata is required");
|
||||
}
|
||||
assignment.organization_id = GetString(*resource, "organization_id").value_or("");
|
||||
assignment.connection.resource_id = GetString(*resource, "id").value_or("");
|
||||
assignment.connection.resource_name = GetString(*resource, "name").value_or("");
|
||||
assignment.connection.host = GetString(*resource, "address").value_or("");
|
||||
assignment.connection.port = 3389;
|
||||
assignment.connection.username = "";
|
||||
assignment.connection.password = "";
|
||||
assignment.connection.domain = "";
|
||||
assignment.connection.certificate_verification_mode = GetString(*resource, "certificate_verification_mode").value_or("strict");
|
||||
assignment.connection.render_quality_profile = GetString(*resource, "render_quality_profile").value_or("balanced");
|
||||
assignment.connection.insecure_skip_verify = config_.insecure_skip_verify;
|
||||
const JsonObject* resource_meta = GetObject(*resource, "metadata");
|
||||
if (resource_meta != nullptr) {
|
||||
assignment.connection.host = GetString(*resource_meta, "rdp_host").value_or(assignment.connection.host);
|
||||
assignment.connection.port = static_cast<uint16_t>(GetNumber(*resource_meta, "rdp_port").value_or(3389));
|
||||
assignment.connection.username = GetString(*resource_meta, "username").value_or("");
|
||||
assignment.connection.password = GetString(*resource_meta, "password").value_or("");
|
||||
assignment.connection.domain = GetString(*resource_meta, "domain").value_or("");
|
||||
assignment.connection.certificate_verification_mode =
|
||||
GetString(*resource_meta, "certificate_verification_mode").value_or(assignment.connection.certificate_verification_mode);
|
||||
assignment.connection.render_quality_profile =
|
||||
GetString(*resource_meta, "render_quality_profile").value_or(assignment.connection.render_quality_profile);
|
||||
}
|
||||
const JsonObject* policy = GetObject(*metadata, "policy");
|
||||
if (policy != nullptr) {
|
||||
assignment.policy.detach_grace_period = std::chrono::seconds(static_cast<int>(GetNumber(*policy, "detach_grace_period_seconds").value_or(1800)));
|
||||
assignment.policy.clipboard_mode = GetString(*policy, "clipboard_mode").value_or("disabled");
|
||||
if (assignment.policy.clipboard_mode.empty()) {
|
||||
assignment.policy.clipboard_mode = GetBool(*policy, "clipboard_enabled").value_or(false) ? "bidirectional" : "disabled";
|
||||
}
|
||||
assignment.policy.file_transfer_mode = GetString(*policy, "file_transfer_mode").value_or("disabled");
|
||||
if (assignment.policy.file_transfer_mode.empty()) {
|
||||
assignment.policy.file_transfer_mode = GetBool(*policy, "file_transfer_enabled").value_or(false) ? "client_to_server" : "disabled";
|
||||
}
|
||||
}
|
||||
return assignment;
|
||||
}
|
||||
|
||||
runtime::WorkerLease ControlPlane::ParseLease(const JsonObject& object) const {
|
||||
runtime::WorkerLease lease{};
|
||||
lease.lease_id = GetString(object, "lease_id").value_or("");
|
||||
lease.worker_id = GetString(object, "worker_id").value_or("");
|
||||
lease.session_id = GetString(object, "session_id").value_or("");
|
||||
lease.resource_id = GetString(object, "resource_id").value_or("");
|
||||
lease.control_stream = GetString(object, "control_stream").value_or("");
|
||||
lease.expires_at = GetString(object, "expires_at").value_or("");
|
||||
if (const JsonArray* capabilities = GetArray(object, "capabilities"); capabilities != nullptr) {
|
||||
for (const auto& item : *capabilities) {
|
||||
if (item.IsString()) {
|
||||
lease.capabilities.push_back(item.AsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return lease;
|
||||
}
|
||||
|
||||
std::string ControlPlane::WorkerRegistrationPayload() const {
|
||||
JsonArray capabilities;
|
||||
for (const auto& item : config_.capabilities) {
|
||||
capabilities.emplace_back(item);
|
||||
}
|
||||
return common::SerializeJson(JsonObject{
|
||||
{"worker_id", config_.worker_id},
|
||||
{"protocol", "rdp"},
|
||||
{"status", "online"},
|
||||
{"capabilities", capabilities},
|
||||
{"control_stream", "worker://control/" + config_.worker_id},
|
||||
{"last_heartbeat_at", common::ToRfc3339(common::NowUtc())},
|
||||
});
|
||||
}
|
||||
|
||||
std::string ControlPlane::LeasePayload(const runtime::WorkerLease& lease) const {
|
||||
JsonArray capabilities;
|
||||
for (const auto& capability : lease.capabilities) {
|
||||
capabilities.emplace_back(capability);
|
||||
}
|
||||
return common::SerializeJson(JsonObject{
|
||||
{"lease_id", lease.lease_id},
|
||||
{"worker_id", lease.worker_id},
|
||||
{"protocol", "rdp"},
|
||||
{"resource_id", lease.resource_id},
|
||||
{"session_id", lease.session_id},
|
||||
{"capabilities", capabilities},
|
||||
{"control_stream", lease.control_stream},
|
||||
{"expires_at", common::ToRfc3339(common::NowUtc() + std::chrono::seconds(45))},
|
||||
});
|
||||
}
|
||||
|
||||
std::string ControlPlane::EventPayload(const runtime::WorkerEvent& event) const {
|
||||
JsonObject payload{
|
||||
{"type", event.type},
|
||||
{"session_id", event.session_id},
|
||||
{"worker_id", event.worker_id},
|
||||
};
|
||||
JsonObject detail;
|
||||
if (!event.reason.empty()) {
|
||||
detail.emplace("reason", event.reason);
|
||||
}
|
||||
if (event.width > 0) {
|
||||
detail.emplace("width", event.width);
|
||||
}
|
||||
if (event.height > 0) {
|
||||
detail.emplace("height", event.height);
|
||||
}
|
||||
for (const auto& [key, value] : event.payload) {
|
||||
detail[key] = value;
|
||||
}
|
||||
payload.emplace("payload", detail);
|
||||
return common::SerializeJson(payload);
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::coordination
|
||||
@@ -0,0 +1,264 @@
|
||||
#include "rdp_worker/coordination/redis_client.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace rdp_worker::coordination {
|
||||
|
||||
namespace {
|
||||
|
||||
void EnsureSuccess(bool condition, const std::string& message) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
void CloseSocket(int socket_fd) {
|
||||
if (socket_fd < 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
closesocket(socket_fd);
|
||||
#else
|
||||
close(socket_fd);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool RedisReply::IsNull() const { return std::holds_alternative<std::nullptr_t>(value); }
|
||||
bool RedisReply::IsString() const { return std::holds_alternative<std::string>(value); }
|
||||
bool RedisReply::IsInteger() const { return std::holds_alternative<int64_t>(value); }
|
||||
bool RedisReply::IsArray() const { return std::holds_alternative<Array>(value); }
|
||||
const std::string& RedisReply::AsString() const { return std::get<std::string>(value); }
|
||||
int64_t RedisReply::AsInteger() const { return std::get<int64_t>(value); }
|
||||
const RedisReply::Array& RedisReply::AsArray() const { return std::get<Array>(value); }
|
||||
|
||||
RedisClient::RedisClient(std::string host, int port, std::string password, int db)
|
||||
: host_(std::move(host)),
|
||||
port_(port),
|
||||
password_(std::move(password)),
|
||||
db_(db),
|
||||
socket_fd_(-1) {}
|
||||
|
||||
RedisClient::~RedisClient() {
|
||||
Close();
|
||||
}
|
||||
|
||||
void RedisClient::Connect() {
|
||||
#if defined(_WIN32)
|
||||
WSADATA wsa_data{};
|
||||
EnsureSuccess(WSAStartup(MAKEWORD(2, 2), &wsa_data) == 0, "WSAStartup failed");
|
||||
#endif
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
addrinfo* result = nullptr;
|
||||
EnsureSuccess(getaddrinfo(host_.c_str(), std::to_string(port_).c_str(), &hints, &result) == 0, "getaddrinfo failed");
|
||||
|
||||
for (addrinfo* node = result; node != nullptr; node = node->ai_next) {
|
||||
socket_fd_ = static_cast<int>(socket(node->ai_family, node->ai_socktype, node->ai_protocol));
|
||||
if (socket_fd_ < 0) {
|
||||
continue;
|
||||
}
|
||||
if (connect(socket_fd_, node->ai_addr, static_cast<int>(node->ai_addrlen)) == 0) {
|
||||
break;
|
||||
}
|
||||
CloseSocket(socket_fd_);
|
||||
socket_fd_ = -1;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
EnsureSuccess(socket_fd_ >= 0, "failed to connect to Redis");
|
||||
|
||||
if (!password_.empty()) {
|
||||
Command({"AUTH", password_});
|
||||
}
|
||||
if (db_ != 0) {
|
||||
Command({"SELECT", std::to_string(db_)});
|
||||
}
|
||||
}
|
||||
|
||||
void RedisClient::Close() {
|
||||
CloseSocket(socket_fd_);
|
||||
socket_fd_ = -1;
|
||||
#if defined(_WIN32)
|
||||
WSACleanup();
|
||||
#endif
|
||||
}
|
||||
|
||||
RedisReply RedisClient::Command(const std::vector<std::string>& parts) {
|
||||
WriteAll(EncodeCommand(parts));
|
||||
return ReadReply();
|
||||
}
|
||||
|
||||
std::optional<std::string> RedisClient::Get(const std::string& key) {
|
||||
RedisReply reply = Command({"GET", key});
|
||||
if (reply.IsNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return reply.AsString();
|
||||
}
|
||||
|
||||
void RedisClient::Set(const std::string& key, const std::string& value, std::chrono::seconds ttl) {
|
||||
Command({"SET", key, value, "EX", std::to_string(ttl.count())});
|
||||
}
|
||||
|
||||
void RedisClient::SAdd(const std::string& key, const std::string& value) {
|
||||
Command({"SADD", key, value});
|
||||
}
|
||||
|
||||
void RedisClient::SRem(const std::string& key, const std::string& value) {
|
||||
Command({"SREM", key, value});
|
||||
}
|
||||
|
||||
std::vector<std::string> RedisClient::SMembers(const std::string& key) {
|
||||
RedisReply reply = Command({"SMEMBERS", key});
|
||||
std::vector<std::string> output;
|
||||
if (!reply.IsArray()) {
|
||||
return output;
|
||||
}
|
||||
for (const auto& item : reply.AsArray()) {
|
||||
if (item.IsString()) {
|
||||
output.push_back(item.AsString());
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
std::optional<std::vector<std::string>> RedisClient::BLPop(const std::string& key, std::chrono::seconds timeout) {
|
||||
RedisReply reply = Command({"BLPOP", key, std::to_string(timeout.count())});
|
||||
if (reply.IsNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<std::string> output;
|
||||
for (const auto& item : reply.AsArray()) {
|
||||
if (item.IsString()) {
|
||||
output.push_back(item.AsString());
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
std::optional<std::string> RedisClient::LPop(const std::string& key) {
|
||||
RedisReply reply = Command({"LPOP", key});
|
||||
if (reply.IsNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return reply.AsString();
|
||||
}
|
||||
|
||||
int64_t RedisClient::LLen(const std::string& key) {
|
||||
RedisReply reply = Command({"LLEN", key});
|
||||
if (!reply.IsInteger()) {
|
||||
return 0;
|
||||
}
|
||||
return reply.AsInteger();
|
||||
}
|
||||
|
||||
void RedisClient::RPush(const std::string& key, const std::string& value) {
|
||||
Command({"RPUSH", key, value});
|
||||
}
|
||||
|
||||
void RedisClient::Expire(const std::string& key, std::chrono::seconds ttl) {
|
||||
Command({"EXPIRE", key, std::to_string(ttl.count())});
|
||||
}
|
||||
|
||||
void RedisClient::Delete(const std::string& key) {
|
||||
Command({"DEL", key});
|
||||
}
|
||||
|
||||
std::string RedisClient::ReadLine() {
|
||||
std::string output;
|
||||
char ch = '\0';
|
||||
while (true) {
|
||||
const int received = recv(socket_fd_, &ch, 1, 0);
|
||||
EnsureSuccess(received == 1, "failed to read from Redis");
|
||||
if (ch == '\r') {
|
||||
char lf = '\0';
|
||||
EnsureSuccess(recv(socket_fd_, &lf, 1, 0) == 1 && lf == '\n', "invalid Redis line ending");
|
||||
break;
|
||||
}
|
||||
output.push_back(ch);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
std::string RedisClient::ReadBytes(std::size_t count) {
|
||||
std::string output(count, '\0');
|
||||
std::size_t offset = 0;
|
||||
while (offset < count) {
|
||||
const int received = recv(socket_fd_, output.data() + offset, static_cast<int>(count - offset), 0);
|
||||
EnsureSuccess(received > 0, "failed to read bulk Redis payload");
|
||||
offset += static_cast<std::size_t>(received);
|
||||
}
|
||||
char suffix[2];
|
||||
EnsureSuccess(recv(socket_fd_, suffix, 2, 0) == 2 && suffix[0] == '\r' && suffix[1] == '\n', "invalid Redis bulk suffix");
|
||||
return output;
|
||||
}
|
||||
|
||||
RedisReply RedisClient::ReadReply() {
|
||||
const std::string line = ReadLine();
|
||||
EnsureSuccess(!line.empty(), "empty Redis reply");
|
||||
|
||||
const char prefix = line[0];
|
||||
const std::string payload = line.substr(1);
|
||||
switch (prefix) {
|
||||
case '+':
|
||||
return RedisReply{payload};
|
||||
case ':':
|
||||
return RedisReply{std::stoll(payload)};
|
||||
case '$': {
|
||||
const long long size = std::stoll(payload);
|
||||
if (size < 0) {
|
||||
return RedisReply{nullptr};
|
||||
}
|
||||
return RedisReply{ReadBytes(static_cast<std::size_t>(size))};
|
||||
}
|
||||
case '*': {
|
||||
const long long size = std::stoll(payload);
|
||||
if (size < 0) {
|
||||
return RedisReply{nullptr};
|
||||
}
|
||||
RedisReply::Array values;
|
||||
for (long long i = 0; i < size; ++i) {
|
||||
values.push_back(ReadReply());
|
||||
}
|
||||
return RedisReply{values};
|
||||
}
|
||||
case '-':
|
||||
throw std::runtime_error("Redis error: " + payload);
|
||||
default:
|
||||
throw std::runtime_error("unknown Redis reply type");
|
||||
}
|
||||
}
|
||||
|
||||
void RedisClient::WriteAll(const std::string& data) {
|
||||
std::size_t offset = 0;
|
||||
while (offset < data.size()) {
|
||||
const int sent = send(socket_fd_, data.data() + offset, static_cast<int>(data.size() - offset), 0);
|
||||
EnsureSuccess(sent > 0, "failed to send Redis command");
|
||||
offset += static_cast<std::size_t>(sent);
|
||||
}
|
||||
}
|
||||
|
||||
std::string RedisClient::EncodeCommand(const std::vector<std::string>& parts) const {
|
||||
std::string output = "*" + std::to_string(parts.size()) + "\r\n";
|
||||
for (const auto& part : parts) {
|
||||
output += "$" + std::to_string(part.size()) + "\r\n" + part + "\r\n";
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::coordination
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "rdp_worker/cursor/cursor_adapter.hpp"
|
||||
|
||||
namespace rdp_worker::cursor {
|
||||
|
||||
CursorUpdate CursorAdapter::MakePosition(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
bool visible) const {
|
||||
CursorUpdate update;
|
||||
update.kind = CursorUpdateKind::kPosition;
|
||||
update.sequence = sequence;
|
||||
update.desktop_width = desktop_width;
|
||||
update.desktop_height = desktop_height;
|
||||
update.x = x;
|
||||
update.y = y;
|
||||
update.visible = visible;
|
||||
return update;
|
||||
}
|
||||
|
||||
CursorUpdate CursorAdapter::MakeSystem(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
std::uint32_t system_type) const {
|
||||
CursorUpdate update = MakePosition(sequence, desktop_width, desktop_height, x, y, system_type != 0);
|
||||
update.kind = CursorUpdateKind::kSystem;
|
||||
update.shape_changed = true;
|
||||
update.system_type = system_type;
|
||||
return update;
|
||||
}
|
||||
|
||||
CursorUpdate CursorAdapter::MakeColor(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int cache_index,
|
||||
std::uint64_t mask_bytes) const {
|
||||
CursorUpdate update = MakePosition(sequence, desktop_width, desktop_height, x, y, true);
|
||||
update.kind = CursorUpdateKind::kColor;
|
||||
update.shape_changed = true;
|
||||
update.width = width;
|
||||
update.height = height;
|
||||
update.cache_index = cache_index;
|
||||
update.mask_bytes = mask_bytes;
|
||||
return update;
|
||||
}
|
||||
|
||||
CursorUpdate CursorAdapter::MakeNew(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int cache_index,
|
||||
int xor_bpp,
|
||||
std::uint64_t mask_bytes) const {
|
||||
CursorUpdate update = MakeColor(sequence, desktop_width, desktop_height, x, y, width, height, cache_index, mask_bytes);
|
||||
update.kind = CursorUpdateKind::kNew;
|
||||
update.xor_bpp = xor_bpp;
|
||||
return update;
|
||||
}
|
||||
|
||||
CursorUpdate CursorAdapter::MakeCached(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int cache_index) const {
|
||||
CursorUpdate update = MakePosition(sequence, desktop_width, desktop_height, x, y, true);
|
||||
update.kind = CursorUpdateKind::kCached;
|
||||
update.shape_changed = true;
|
||||
update.cache_index = cache_index;
|
||||
return update;
|
||||
}
|
||||
|
||||
CursorUpdate CursorAdapter::MakeLarge(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int cache_index,
|
||||
int hot_spot_x,
|
||||
int hot_spot_y,
|
||||
int xor_bpp,
|
||||
std::uint64_t mask_bytes) const {
|
||||
CursorUpdate update = MakeNew(sequence, desktop_width, desktop_height, x, y, width, height, cache_index, xor_bpp, mask_bytes);
|
||||
update.kind = CursorUpdateKind::kLarge;
|
||||
update.hot_spot_x = hot_spot_x;
|
||||
update.hot_spot_y = hot_spot_y;
|
||||
return update;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::cursor
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "rdp_worker/cursor/cursor_update.hpp"
|
||||
|
||||
namespace rdp_worker::cursor {
|
||||
|
||||
const char* CursorUpdateKindName(CursorUpdateKind kind) {
|
||||
switch (kind) {
|
||||
case CursorUpdateKind::kPosition:
|
||||
return "position";
|
||||
case CursorUpdateKind::kSystem:
|
||||
return "system";
|
||||
case CursorUpdateKind::kColor:
|
||||
return "color";
|
||||
case CursorUpdateKind::kNew:
|
||||
return "new";
|
||||
case CursorUpdateKind::kCached:
|
||||
return "cached";
|
||||
case CursorUpdateKind::kLarge:
|
||||
return "large";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
common::JsonObject CursorUpdateToPayload(const CursorUpdate& update,
|
||||
const std::string& render_quality_profile) {
|
||||
return common::JsonObject{
|
||||
{"render_quality_profile", render_quality_profile},
|
||||
{"render_state", "cursor"},
|
||||
{"cursor_update_kind", CursorUpdateKindName(update.kind)},
|
||||
{"cursor_sequence", static_cast<double>(update.sequence)},
|
||||
{"width", update.desktop_width},
|
||||
{"height", update.desktop_height},
|
||||
{"cursor_x", update.x},
|
||||
{"cursor_y", update.y},
|
||||
{"cursor_visible", update.visible},
|
||||
{"cursor_shape_changed", update.shape_changed},
|
||||
{"cursor_cache_index", update.cache_index},
|
||||
{"cursor_hot_spot_x", update.hot_spot_x},
|
||||
{"cursor_hot_spot_y", update.hot_spot_y},
|
||||
{"cursor_width", update.width},
|
||||
{"cursor_height", update.height},
|
||||
{"cursor_xor_bpp", update.xor_bpp},
|
||||
{"cursor_mask_bytes", static_cast<double>(update.mask_bytes)},
|
||||
{"cursor_system_type", static_cast<double>(update.system_type)},
|
||||
{"dirty_rectangles", 0},
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::cursor
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
#include "rdp_worker/dataplane/token_validator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
#include "rdp_worker/common/json.hpp"
|
||||
|
||||
namespace rdp_worker::dataplane {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::string> SplitJwt(const std::string& token) {
|
||||
std::vector<std::string> parts;
|
||||
std::size_t start = 0;
|
||||
while (true) {
|
||||
const std::size_t dot = token.find('.', start);
|
||||
parts.push_back(token.substr(start, dot == std::string::npos ? std::string::npos : dot - start));
|
||||
if (dot == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = dot + 1;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Base64UrlDecode(const std::string& input) {
|
||||
static constexpr unsigned char kInvalid = 255;
|
||||
std::array<unsigned char, 256> table{};
|
||||
table.fill(kInvalid);
|
||||
const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
for (std::size_t i = 0; i < alphabet.size(); ++i) {
|
||||
table[static_cast<unsigned char>(alphabet[i])] = static_cast<unsigned char>(i);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> out;
|
||||
int value = 0;
|
||||
int value_bits = -8;
|
||||
for (unsigned char ch : input) {
|
||||
if (ch == '=') {
|
||||
break;
|
||||
}
|
||||
if (table[ch] == kInvalid) {
|
||||
throw std::runtime_error("invalid base64url token segment");
|
||||
}
|
||||
value = (value << 6) + table[ch];
|
||||
value_bits += 6;
|
||||
if (value_bits >= 0) {
|
||||
out.push_back(static_cast<std::uint8_t>((value >> value_bits) & 0xFF));
|
||||
value_bits -= 8;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string DecodeStringSegment(const std::string& input) {
|
||||
const auto bytes = Base64UrlDecode(input);
|
||||
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
}
|
||||
|
||||
struct EvpKeyDeleter {
|
||||
void operator()(EVP_PKEY* key) const {
|
||||
EVP_PKEY_free(key);
|
||||
}
|
||||
};
|
||||
|
||||
struct BioDeleter {
|
||||
void operator()(BIO* bio) const {
|
||||
BIO_free(bio);
|
||||
}
|
||||
};
|
||||
|
||||
struct EvpMdCtxDeleter {
|
||||
void operator()(EVP_MD_CTX* context) const {
|
||||
EVP_MD_CTX_free(context);
|
||||
}
|
||||
};
|
||||
|
||||
using EvpKeyPtr = std::unique_ptr<EVP_PKEY, EvpKeyDeleter>;
|
||||
using BioPtr = std::unique_ptr<BIO, BioDeleter>;
|
||||
using EvpMdCtxPtr = std::unique_ptr<EVP_MD_CTX, EvpMdCtxDeleter>;
|
||||
|
||||
bool VerifyRs256(const std::string& public_key_pem, const std::string& signing_input, const std::vector<std::uint8_t>& signature) {
|
||||
BioPtr bio(BIO_new_mem_buf(public_key_pem.data(), static_cast<int>(public_key_pem.size())));
|
||||
if (!bio) {
|
||||
throw std::runtime_error("public_key_bio_unavailable");
|
||||
}
|
||||
EvpKeyPtr key(PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr));
|
||||
if (!key) {
|
||||
throw std::runtime_error("public_key_parse_failed");
|
||||
}
|
||||
EvpMdCtxPtr context(EVP_MD_CTX_new());
|
||||
if (!context) {
|
||||
throw std::runtime_error("signature_context_unavailable");
|
||||
}
|
||||
if (EVP_DigestVerifyInit(context.get(), nullptr, EVP_sha256(), nullptr, key.get()) != 1) {
|
||||
throw std::runtime_error("signature_verify_init_failed");
|
||||
}
|
||||
if (EVP_DigestVerifyUpdate(context.get(), signing_input.data(), signing_input.size()) != 1) {
|
||||
throw std::runtime_error("signature_verify_update_failed");
|
||||
}
|
||||
return EVP_DigestVerifyFinal(context.get(), signature.data(), signature.size()) == 1;
|
||||
}
|
||||
|
||||
std::int64_t UnixNow() {
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
bool IsKnownChannel(const std::string& channel) {
|
||||
return channel == "control" ||
|
||||
channel == "input" ||
|
||||
channel == "render" ||
|
||||
channel == "clipboard" ||
|
||||
channel == "file_upload" ||
|
||||
channel == "file_download" ||
|
||||
channel == "telemetry";
|
||||
}
|
||||
|
||||
bool ArrayContainsString(const rdp_worker::common::JsonArray* array, const std::string& expected) {
|
||||
if (array == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return std::any_of(array->begin(), array->end(), [&](const auto& item) {
|
||||
return item.IsString() && item.AsString() == expected;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseAllowedChannels(const rdp_worker::common::JsonObject& payload) {
|
||||
const auto* array = rdp_worker::common::GetArray(payload, "allowed_channels");
|
||||
if (array == nullptr || array->empty()) {
|
||||
throw std::runtime_error("allowed_channels is required");
|
||||
}
|
||||
std::vector<std::string> channels;
|
||||
for (const auto& item : *array) {
|
||||
if (!item.IsString() || !IsKnownChannel(item.AsString())) {
|
||||
throw std::runtime_error("allowed_channels contains unsupported channel");
|
||||
}
|
||||
channels.push_back(item.AsString());
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
std::string RequiredString(const rdp_worker::common::JsonObject& object, const std::string& key) {
|
||||
const auto value = rdp_worker::common::GetString(object, key);
|
||||
if (!value.has_value() || value->empty()) {
|
||||
throw std::runtime_error(key + " is required");
|
||||
}
|
||||
return *value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DataPlaneTokenValidator::DataPlaneTokenValidator(std::string public_key_pem, std::string expected_worker_id)
|
||||
: public_key_pem_(std::move(public_key_pem)),
|
||||
expected_worker_id_(std::move(expected_worker_id)) {}
|
||||
|
||||
TokenValidationResult DataPlaneTokenValidator::Validate(const std::string& token) const {
|
||||
TokenValidationResult result{};
|
||||
try {
|
||||
if (public_key_pem_.empty()) {
|
||||
result.reason = "token_public_key_not_configured";
|
||||
return result;
|
||||
}
|
||||
const auto parts = SplitJwt(token);
|
||||
if (parts.size() != 3 || parts[0].empty() || parts[1].empty() || parts[2].empty()) {
|
||||
result.reason = "malformed_token";
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = rdp_worker::common::ParseJson(DecodeStringSegment(parts[0])).AsObject();
|
||||
if (rdp_worker::common::GetString(header, "alg").value_or("") != "RS256" ||
|
||||
rdp_worker::common::GetString(header, "typ").value_or("JWT") != "JWT") {
|
||||
result.reason = "unsupported_token_header";
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::string signing_input = parts[0] + "." + parts[1];
|
||||
const auto actual_signature = Base64UrlDecode(parts[2]);
|
||||
if (!VerifyRs256(public_key_pem_, signing_input, actual_signature)) {
|
||||
result.reason = "invalid_signature";
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto payload = rdp_worker::common::ParseJson(DecodeStringSegment(parts[1])).AsObject();
|
||||
DataPlaneTokenClaims claims{};
|
||||
claims.session_id = RequiredString(payload, "session_id");
|
||||
claims.attachment_id = RequiredString(payload, "attachment_id");
|
||||
claims.user_id = RequiredString(payload, "user_id");
|
||||
claims.organization_id = RequiredString(payload, "organization_id");
|
||||
claims.worker_id = RequiredString(payload, "worker_id");
|
||||
claims.resource_id = RequiredString(payload, "resource_id");
|
||||
claims.jti = RequiredString(payload, "jti");
|
||||
claims.allowed_channels = ParseAllowedChannels(payload);
|
||||
claims.expires_at_unix = static_cast<std::int64_t>(rdp_worker::common::GetNumber(payload, "exp").value_or(0));
|
||||
|
||||
const auto now = UnixNow();
|
||||
if (claims.expires_at_unix <= now) {
|
||||
result.reason = "token_expired";
|
||||
return result;
|
||||
}
|
||||
const auto not_before = static_cast<std::int64_t>(rdp_worker::common::GetNumber(payload, "nbf").value_or(0));
|
||||
if (not_before > 0 && not_before > now) {
|
||||
result.reason = "token_not_yet_valid";
|
||||
return result;
|
||||
}
|
||||
if (!expected_worker_id_.empty() && claims.worker_id != expected_worker_id_) {
|
||||
result.reason = "wrong_worker";
|
||||
return result;
|
||||
}
|
||||
if (!ArrayContainsString(rdp_worker::common::GetArray(payload, "aud"), "rap-data-plane") ||
|
||||
!ArrayContainsString(rdp_worker::common::GetArray(payload, "aud"), "worker:" + claims.worker_id)) {
|
||||
result.reason = "invalid_audience";
|
||||
return result;
|
||||
}
|
||||
if (std::find(claims.allowed_channels.begin(), claims.allowed_channels.end(), "control") == claims.allowed_channels.end()) {
|
||||
result.reason = "control_channel_not_allowed";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.ok = true;
|
||||
result.claims = std::move(claims);
|
||||
return result;
|
||||
} catch (const std::exception& error) {
|
||||
result.reason = error.what();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::dataplane
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
#include "rdp_worker/graphics/graphics_adapter.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace rdp_worker::graphics {
|
||||
|
||||
GraphicsAdapter::GraphicsAdapter(GraphicsAdapterPolicy policy)
|
||||
: policy_(policy) {}
|
||||
|
||||
const GraphicsAdapterPolicy& GraphicsAdapter::Policy() const {
|
||||
return policy_;
|
||||
}
|
||||
|
||||
RenderUpdate GraphicsAdapter::MakeFullBgraFrame(std::uint64_t sequence,
|
||||
int width,
|
||||
int height,
|
||||
int stride,
|
||||
std::vector<std::uint8_t> pixels,
|
||||
bool baseline) const {
|
||||
RenderUpdate update;
|
||||
update.kind = RenderUpdateKind::kFullBgraFrame;
|
||||
update.sequence = sequence;
|
||||
update.desktop_width = width;
|
||||
update.desktop_height = height;
|
||||
update.frame_width = width;
|
||||
update.frame_height = height;
|
||||
update.stride = stride;
|
||||
update.region = Rect{0, 0, width, height};
|
||||
update.payload = std::move(pixels);
|
||||
update.baseline = baseline;
|
||||
update.droppable = !baseline;
|
||||
return update;
|
||||
}
|
||||
|
||||
std::optional<RenderUpdate> GraphicsAdapter::TryMakeBgraRegion(std::uint64_t sequence,
|
||||
int desktop_width,
|
||||
int desktop_height,
|
||||
int stride,
|
||||
Rect region,
|
||||
std::vector<std::uint8_t> pixels) const {
|
||||
if (!RegionAllowed(desktop_width, desktop_height, region)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
RenderUpdate update;
|
||||
update.kind = RenderUpdateKind::kBgraRegion;
|
||||
update.sequence = sequence;
|
||||
update.desktop_width = desktop_width;
|
||||
update.desktop_height = desktop_height;
|
||||
update.frame_width = region.width;
|
||||
update.frame_height = region.height;
|
||||
update.stride = stride;
|
||||
update.region = region;
|
||||
update.payload = std::move(pixels);
|
||||
update.baseline = false;
|
||||
update.droppable = true;
|
||||
return update;
|
||||
}
|
||||
|
||||
bool GraphicsAdapter::RegionAllowed(int desktop_width, int desktop_height, const Rect& region) const {
|
||||
if (desktop_width <= 0 || desktop_height <= 0 ||
|
||||
region.x < 0 || region.y < 0 ||
|
||||
region.width <= 0 || region.height <= 0 ||
|
||||
region.x + region.width > desktop_width ||
|
||||
region.y + region.height > desktop_height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const long long desktop_area = static_cast<long long>(desktop_width) * static_cast<long long>(desktop_height);
|
||||
const long long region_area = static_cast<long long>(region.width) * static_cast<long long>(region.height);
|
||||
return region_area * 100 <= desktop_area * policy_.max_region_area_percent;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::graphics
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "rdp_worker/graphics/render_update.hpp"
|
||||
|
||||
namespace rdp_worker::graphics {
|
||||
|
||||
const char* RenderUpdateKindName(RenderUpdateKind kind) {
|
||||
switch (kind) {
|
||||
case RenderUpdateKind::kFullBgraFrame:
|
||||
return "full_bgra_frame";
|
||||
case RenderUpdateKind::kBgraRegion:
|
||||
return "bgra_region";
|
||||
case RenderUpdateKind::kSurfaceCreate:
|
||||
return "surface_create";
|
||||
case RenderUpdateKind::kSurfaceDelete:
|
||||
return "surface_delete";
|
||||
case RenderUpdateKind::kSurfaceBits:
|
||||
return "surface_bits";
|
||||
case RenderUpdateKind::kEncodedFrame:
|
||||
return "encoded_frame";
|
||||
case RenderUpdateKind::kCursorUpdate:
|
||||
return "cursor_update";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
bool IsFullFrameUpdate(const RenderUpdate& update) {
|
||||
return update.kind == RenderUpdateKind::kFullBgraFrame;
|
||||
}
|
||||
|
||||
bool IsRegionUpdate(const RenderUpdate& update) {
|
||||
return update.kind == RenderUpdateKind::kBgraRegion;
|
||||
}
|
||||
|
||||
bool IsEncodedUpdate(const RenderUpdate& update) {
|
||||
return update.kind == RenderUpdateKind::kEncodedFrame ||
|
||||
update.kind == RenderUpdateKind::kSurfaceBits;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::graphics
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include "rdp_worker/common/logger.hpp"
|
||||
#include "rdp_worker/config/config.hpp"
|
||||
#include "rdp_worker/coordination/control_plane.hpp"
|
||||
#include "rdp_worker/dataplane/direct_wss_server.hpp"
|
||||
#include "rdp_worker/runtime/session_manager.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool> g_stop{false};
|
||||
|
||||
void HandleSignal(int) {
|
||||
g_stop.store(true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
std::signal(SIGINT, HandleSignal);
|
||||
std::signal(SIGTERM, HandleSignal);
|
||||
|
||||
auto logger = std::make_shared<rdp_worker::common::Logger>("rdp-worker");
|
||||
const auto config = rdp_worker::config::LoadFromEnv();
|
||||
auto control_plane = std::make_shared<rdp_worker::coordination::ControlPlane>(config, logger);
|
||||
control_plane->Connect();
|
||||
control_plane->ReleaseOwnedLeasesOnStartup();
|
||||
control_plane->RegisterWorker();
|
||||
|
||||
auto session_manager = std::make_shared<rdp_worker::runtime::SessionManager>(control_plane, logger);
|
||||
rdp_worker::dataplane::DirectWssServer direct_wss_server(config, session_manager, logger);
|
||||
direct_wss_server.Start();
|
||||
|
||||
std::thread heartbeat_thread([&]() {
|
||||
while (!g_stop.load()) {
|
||||
control_plane->SendHeartbeat();
|
||||
std::this_thread::sleep_for(config.worker_heartbeat_interval);
|
||||
}
|
||||
});
|
||||
|
||||
while (!g_stop.load()) {
|
||||
if (auto assignment = control_plane->PollAssignment(config.assignment_poll_interval); assignment.has_value()) {
|
||||
session_manager->ApplyAssignment(*assignment);
|
||||
}
|
||||
}
|
||||
|
||||
direct_wss_server.Stop();
|
||||
session_manager->StopAll();
|
||||
if (heartbeat_thread.joinable()) {
|
||||
heartbeat_thread.join();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "rdp_worker/runtime/direct_bind_policy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
namespace {
|
||||
|
||||
bool ClipboardAllowsServerOrClient(const std::string& mode) {
|
||||
return mode == "client_to_server" || mode == "server_to_client" || mode == "bidirectional";
|
||||
}
|
||||
|
||||
bool FileTransferAllowsClientToServer(const std::string& mode) {
|
||||
return mode == "client_to_server" || mode == "bidirectional";
|
||||
}
|
||||
|
||||
bool FileTransferAllowsServerToClient(const std::string& mode) {
|
||||
return mode == "server_to_client" || mode == "bidirectional";
|
||||
}
|
||||
|
||||
std::vector<std::string> RuntimeAllowedChannels(const Assignment& assignment) {
|
||||
std::vector<std::string> channels{"control", "input", "render", "telemetry"};
|
||||
if (ClipboardAllowsServerOrClient(assignment.policy.clipboard_mode)) {
|
||||
channels.push_back("clipboard");
|
||||
}
|
||||
if (FileTransferAllowsClientToServer(assignment.policy.file_transfer_mode)) {
|
||||
channels.push_back("file_upload");
|
||||
}
|
||||
if (FileTransferAllowsServerToClient(assignment.policy.file_transfer_mode)) {
|
||||
channels.push_back("file_download");
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
bool RequestedChannelsAllowed(const std::vector<std::string>& requested, const std::vector<std::string>& allowed) {
|
||||
return std::all_of(requested.begin(), requested.end(), [&](const auto& channel) {
|
||||
return std::find(allowed.begin(), allowed.end(), channel) != allowed.end();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DirectBindValidationResult ValidateDirectDataPlaneBind(const Assignment& assignment,
|
||||
const dataplane::DataPlaneTokenClaims& claims) {
|
||||
if (assignment.state != SessionState::kStarting &&
|
||||
assignment.state != SessionState::kActive &&
|
||||
assignment.state != SessionState::kReconnecting) {
|
||||
return {false, "session_not_attachable"};
|
||||
}
|
||||
if (assignment.worker_id != claims.worker_id) {
|
||||
return {false, "worker_mismatch"};
|
||||
}
|
||||
if (assignment.attachment_id != claims.attachment_id) {
|
||||
return {false, "attachment_mismatch"};
|
||||
}
|
||||
if (assignment.user_id != claims.user_id) {
|
||||
return {false, "user_mismatch"};
|
||||
}
|
||||
if (assignment.organization_id != claims.organization_id) {
|
||||
return {false, "organization_mismatch"};
|
||||
}
|
||||
if (assignment.connection.resource_id != claims.resource_id) {
|
||||
return {false, "resource_mismatch"};
|
||||
}
|
||||
if (!RequestedChannelsAllowed(claims.allowed_channels, RuntimeAllowedChannels(assignment))) {
|
||||
return {false, "channels_exceed_runtime_policy"};
|
||||
}
|
||||
return {true, ""};
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "rdp_worker/runtime/session_manager.hpp"
|
||||
|
||||
#include "rdp_worker/runtime/direct_bind_policy.hpp"
|
||||
|
||||
namespace rdp_worker::runtime {
|
||||
|
||||
SessionManager::SessionManager(std::shared_ptr<coordination::ControlPlane> control_plane,
|
||||
std::shared_ptr<common::Logger> logger)
|
||||
: control_plane_(std::move(control_plane)),
|
||||
logger_(std::move(logger)) {}
|
||||
|
||||
void SessionManager::ApplyAssignment(const Assignment& assignment) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const auto iterator = sessions_.find(assignment.session_id);
|
||||
if (iterator != sessions_.end()) {
|
||||
iterator->second->ApplyAssignment(assignment);
|
||||
logger_->Info("updated assignment for existing session " + assignment.session_id);
|
||||
return;
|
||||
}
|
||||
|
||||
auto runtime = std::make_shared<SessionRuntime>(assignment, control_plane_, logger_);
|
||||
runtime->Start();
|
||||
sessions_.emplace(assignment.session_id, runtime);
|
||||
logger_->Info("started new runtime for session " + assignment.session_id);
|
||||
}
|
||||
|
||||
void SessionManager::StopAll() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (auto& [_, runtime] : sessions_) {
|
||||
runtime->Stop(true, "worker_shutdown");
|
||||
}
|
||||
sessions_.clear();
|
||||
}
|
||||
|
||||
bool SessionManager::BindDirectDataPlaneAttachment(const dataplane::DataPlaneTokenClaims& claims, std::string& reason) {
|
||||
return BindDirectDataPlaneRuntime(claims, reason) != nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<SessionRuntime> SessionManager::BindDirectDataPlaneRuntime(const dataplane::DataPlaneTokenClaims& claims, std::string& reason) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const auto iterator = sessions_.find(claims.session_id);
|
||||
if (iterator == sessions_.end()) {
|
||||
reason = "missing_runtime";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Assignment snapshot = iterator->second->Snapshot();
|
||||
const auto validation = ValidateDirectDataPlaneBind(snapshot, claims);
|
||||
if (!validation.ok) {
|
||||
reason = validation.reason;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
reason.clear();
|
||||
logger_->Info("event=data_plane_bind_success session=" + claims.session_id +
|
||||
" attachment=" + claims.attachment_id +
|
||||
" user=" + claims.user_id +
|
||||
" organization=" + claims.organization_id +
|
||||
" resource=" + claims.resource_id);
|
||||
return iterator->second;
|
||||
}
|
||||
|
||||
} // namespace rdp_worker::runtime
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "rdp_worker/cursor/cursor_adapter.hpp"
|
||||
#include "rdp_worker/cursor/cursor_update.hpp"
|
||||
|
||||
int main() {
|
||||
rdp_worker::cursor::CursorAdapter adapter;
|
||||
|
||||
auto position = adapter.MakePosition(1, 1280, 720, 10, 20, true);
|
||||
if (position.kind != rdp_worker::cursor::CursorUpdateKind::kPosition ||
|
||||
!position.visible ||
|
||||
position.x != 10 ||
|
||||
position.y != 20) {
|
||||
std::cerr << "cursor position normalization failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto hidden = adapter.MakeSystem(2, 1280, 720, 10, 20, 0);
|
||||
if (hidden.kind != rdp_worker::cursor::CursorUpdateKind::kSystem ||
|
||||
hidden.visible ||
|
||||
!hidden.shape_changed) {
|
||||
std::cerr << "cursor system visibility normalization failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto large = adapter.MakeLarge(3, 1280, 720, 5, 6, 32, 32, 7, 1, 2, 32, 128);
|
||||
if (large.kind != rdp_worker::cursor::CursorUpdateKind::kLarge ||
|
||||
large.cache_index != 7 ||
|
||||
large.hot_spot_x != 1 ||
|
||||
large.hot_spot_y != 2 ||
|
||||
large.mask_bytes != 128) {
|
||||
std::cerr << "cursor shape normalization failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto payload = rdp_worker::cursor::CursorUpdateToPayload(large, "balanced");
|
||||
if (payload.empty()) {
|
||||
std::cerr << "cursor payload conversion failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "cursor_adapter_probe ok\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "rdp_worker/runtime/direct_bind_policy.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ArgValue(int argc, char** argv, const std::string& name) {
|
||||
for (int i = 1; i + 1 < argc; ++i) {
|
||||
if (argv[i] == name) {
|
||||
return argv[i + 1];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
rdp_worker::runtime::Assignment BaseAssignment() {
|
||||
rdp_worker::runtime::Assignment assignment{};
|
||||
assignment.session_id = "session-1";
|
||||
assignment.worker_id = "rdp-worker-1";
|
||||
assignment.attachment_id = "attachment-current";
|
||||
assignment.user_id = "user-1";
|
||||
assignment.organization_id = "org-1";
|
||||
assignment.state = rdp_worker::runtime::SessionState::kActive;
|
||||
assignment.connection.resource_id = "resource-1";
|
||||
assignment.policy.clipboard_mode = "disabled";
|
||||
assignment.policy.file_transfer_mode = "disabled";
|
||||
return assignment;
|
||||
}
|
||||
|
||||
rdp_worker::dataplane::DataPlaneTokenClaims BaseClaims() {
|
||||
rdp_worker::dataplane::DataPlaneTokenClaims claims{};
|
||||
claims.session_id = "session-1";
|
||||
claims.worker_id = "rdp-worker-1";
|
||||
claims.attachment_id = "attachment-current";
|
||||
claims.user_id = "user-1";
|
||||
claims.organization_id = "org-1";
|
||||
claims.resource_id = "resource-1";
|
||||
claims.allowed_channels = {"control", "input", "render", "telemetry"};
|
||||
claims.jti = "jti-1";
|
||||
claims.expires_at_unix = 4102444800;
|
||||
return claims;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const auto scenario = ArgValue(argc, argv, "--scenario");
|
||||
if (scenario.empty()) {
|
||||
std::cerr << "usage: rdp-worker-dataplane-bind-probe --scenario valid|starting|wrong-worker|wrong-attachment|wrong-user|wrong-organization|wrong-resource|channels-too-broad|failed-state|terminated-state\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto assignment = BaseAssignment();
|
||||
auto claims = BaseClaims();
|
||||
std::string expected_reason;
|
||||
if (scenario == "valid") {
|
||||
expected_reason = "";
|
||||
} else if (scenario == "starting") {
|
||||
assignment.state = rdp_worker::runtime::SessionState::kStarting;
|
||||
expected_reason = "";
|
||||
} else if (scenario == "wrong-attachment") {
|
||||
claims.attachment_id = "attachment-old";
|
||||
expected_reason = "attachment_mismatch";
|
||||
} else if (scenario == "wrong-worker") {
|
||||
claims.worker_id = "rdp-worker-other";
|
||||
expected_reason = "worker_mismatch";
|
||||
} else if (scenario == "wrong-user") {
|
||||
claims.user_id = "user-other";
|
||||
expected_reason = "user_mismatch";
|
||||
} else if (scenario == "wrong-organization") {
|
||||
claims.organization_id = "org-other";
|
||||
expected_reason = "organization_mismatch";
|
||||
} else if (scenario == "wrong-resource") {
|
||||
claims.resource_id = "resource-other";
|
||||
expected_reason = "resource_mismatch";
|
||||
} else if (scenario == "channels-too-broad") {
|
||||
claims.allowed_channels.push_back("clipboard");
|
||||
expected_reason = "channels_exceed_runtime_policy";
|
||||
} else if (scenario == "failed-state") {
|
||||
assignment.state = rdp_worker::runtime::SessionState::kFailed;
|
||||
expected_reason = "session_not_attachable";
|
||||
} else if (scenario == "terminated-state") {
|
||||
assignment.state = rdp_worker::runtime::SessionState::kTerminated;
|
||||
expected_reason = "session_not_attachable";
|
||||
} else {
|
||||
std::cerr << "unknown scenario=" << scenario << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto result = rdp_worker::runtime::ValidateDirectDataPlaneBind(assignment, claims);
|
||||
if ((scenario == "valid" || scenario == "starting") && result.ok) {
|
||||
std::cout << "PASS scenario=" << scenario << "\n";
|
||||
return 0;
|
||||
}
|
||||
if (!expected_reason.empty() && !result.ok && result.reason == expected_reason) {
|
||||
std::cout << "PASS scenario=" << scenario << " reason=" << result.reason << "\n";
|
||||
return 0;
|
||||
}
|
||||
std::cerr << "FAIL scenario=" << scenario << " ok=" << (result.ok ? "true" : "false") << " reason=" << result.reason << "\n";
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "rdp_worker/dataplane/token_validator.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ArgValue(int argc, char** argv, const std::string& name) {
|
||||
for (int i = 1; i + 1 < argc; ++i) {
|
||||
if (argv[i] == name) {
|
||||
return argv[i + 1];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::string& path) {
|
||||
std::ifstream input(path);
|
||||
if (!input.good()) {
|
||||
return {};
|
||||
}
|
||||
std::stringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const auto token = ArgValue(argc, argv, "--token");
|
||||
auto public_key = ArgValue(argc, argv, "--public-key-pem");
|
||||
const auto public_key_file = ArgValue(argc, argv, "--public-key-file");
|
||||
if (public_key.empty() && !public_key_file.empty()) {
|
||||
public_key = ReadFile(public_key_file);
|
||||
}
|
||||
const auto worker_id = ArgValue(argc, argv, "--worker-id");
|
||||
if (token.empty() || public_key.empty() || worker_id.empty()) {
|
||||
std::cerr << "usage: rdp-worker-dataplane-token-probe --token <jwt> --public-key-file <public.pem> --worker-id <worker_id>\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
rdp_worker::dataplane::DataPlaneTokenValidator validator(public_key, worker_id);
|
||||
const auto result = validator.Validate(token);
|
||||
if (!result.ok) {
|
||||
std::cerr << "FAIL reason=" << result.reason << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "PASS session_id=" << result.claims.session_id
|
||||
<< " attachment_id=" << result.claims.attachment_id
|
||||
<< " worker_id=" << result.claims.worker_id
|
||||
<< " resource_id=" << result.claims.resource_id
|
||||
<< " channels=" << result.claims.allowed_channels.size()
|
||||
<< "\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "rdp_worker/graphics/graphics_adapter.hpp"
|
||||
|
||||
int main() {
|
||||
rdp_worker::graphics::GraphicsAdapter adapter;
|
||||
|
||||
auto full = adapter.MakeFullBgraFrame(1, 1280, 720, 5120, std::vector<std::uint8_t>(1280 * 720 * 4), true);
|
||||
if (!rdp_worker::graphics::IsFullFrameUpdate(full) || !full.baseline || full.droppable) {
|
||||
std::cerr << "full frame baseline policy failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto region = adapter.TryMakeBgraRegion(
|
||||
2,
|
||||
1280,
|
||||
720,
|
||||
200 * 4,
|
||||
rdp_worker::graphics::Rect{10, 20, 200, 100},
|
||||
std::vector<std::uint8_t>(200 * 100 * 4));
|
||||
if (!region.has_value() || !rdp_worker::graphics::IsRegionUpdate(*region) || !region->droppable) {
|
||||
std::cerr << "region policy failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto too_large = adapter.TryMakeBgraRegion(
|
||||
3,
|
||||
1280,
|
||||
720,
|
||||
1280 * 4,
|
||||
rdp_worker::graphics::Rect{0, 0, 1280, 720},
|
||||
std::vector<std::uint8_t>(1280 * 720 * 4));
|
||||
if (too_large.has_value()) {
|
||||
std::cerr << "oversized region should be rejected\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "graphics_adapter_probe ok\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "rdp_worker/adapter/service_adapter_protocol.hpp"
|
||||
|
||||
int main() {
|
||||
using namespace rdp_worker::adapter;
|
||||
|
||||
for (const auto& spec : AllChannelSpecs()) {
|
||||
std::cout << spec.name
|
||||
<< " direction=" << DirectionName(spec.direction)
|
||||
<< " reliability=" << ReliabilityName(spec.reliability)
|
||||
<< " priority=" << PriorityValue(spec.priority)
|
||||
<< " droppable=" << (spec.stale_updates_droppable ? "true" : "false")
|
||||
<< " may_block_input=" << (spec.may_block_input ? "true" : "false")
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
if (!ValidateAdapterChannelInvariants()) {
|
||||
std::cerr << "adapter channel invariants failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user