Add detailed analysis and documentation for RSNet.dll download functionality
- Introduced a comprehensive markdown document outlining the API surface of RSNet.dll related to downloading video files. - Documented initialization, connection, record querying, and download processes. - Provided insights into the exported functions, their parameters, and expected behaviors. - Included practical implications and recommendations for implementing a downloader script using C# interop. - Highlighted the necessary struct layouts and callback mechanisms for effective integration with the DLL.
This commit is contained in:
parent
0272c5ac3a
commit
de077ca5d5
34 changed files with 2084 additions and 152 deletions
74
SurveillanceClient/.gitignore
vendored
Normal file
74
SurveillanceClient/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Build results
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# User-specific files
|
||||
*.user
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.rsuser
|
||||
|
||||
# Visual Studio
|
||||
.vs/
|
||||
*.VisualState.xml
|
||||
*.pidb
|
||||
*.svclog
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.DotSettings.user
|
||||
|
||||
# VS Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
.nuget/
|
||||
packages/
|
||||
!packages/build/
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# ReSharper
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
|
||||
# Test results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Local secrets / configuration overrides (NEVER COMMIT CREDENTIALS)
|
||||
appsettings.Development.json
|
||||
appsettings.Local.json
|
||||
appsettings.*.Local.json
|
||||
secrets.json
|
||||
secrets.*.json
|
||||
*.secrets
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
.DS_Store
|
||||
|
||||
# Downloaded video output (local runtime artifact)
|
||||
downloads/
|
||||
output/
|
||||
14
SurveillanceClient/Directory.Build.props
Normal file
14
SurveillanceClient/Directory.Build.props
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Required by source-generated P/Invoke (LibraryImport). -->
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- RSNet.dll is 32-bit (x86). Consumers of the native interop must run as x86. -->
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Platforms>x86</Platforms>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
62
SurveillanceClient/README.md
Normal file
62
SurveillanceClient/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# SurveillanceClient
|
||||
|
||||
.NET 10 host for driving the vendor `RSNet.dll` (32-bit) to download
|
||||
recorded video from a surveillance NVR/DVR.
|
||||
|
||||
## Layout
|
||||
|
||||
- `src/SurveillanceClient.Core` — class library that wraps the `RSNet.dll`
|
||||
native exports behind a service interface (`IRsNetClient`).
|
||||
- `src/SurveillanceClient.Cli` — console host used for iterative,
|
||||
manual testing of the Core library against a real device.
|
||||
|
||||
## Platform
|
||||
|
||||
`RSNet.dll` is a 32-bit Visual Studio DLL using `__stdcall`. Every
|
||||
project in this solution targets `x86` via `Directory.Build.props`.
|
||||
|
||||
## Running
|
||||
|
||||
The vendor DLL and its dependencies live in
|
||||
`i:\Apps\Surveillance_client`. The CLI sets that directory as the
|
||||
native DLL search path at startup so the DLL and its `.ini` config
|
||||
are found.
|
||||
|
||||
## Credentials
|
||||
|
||||
Credentials are **never committed**. Configure them via .NET user
|
||||
secrets (recommended) or a `appsettings.Local.json` file (ignored by
|
||||
`.gitignore`).
|
||||
|
||||
### User secrets (recommended)
|
||||
|
||||
```powershell
|
||||
cd src/SurveillanceClient.Cli
|
||||
dotnet user-secrets set "Device:Host" "192.168.100.92"
|
||||
dotnet user-secrets set "Device:Port" "9000"
|
||||
dotnet user-secrets set "Device:Username" "admin"
|
||||
dotnet user-secrets set "Device:Password" "your-password-here"
|
||||
```
|
||||
|
||||
### appsettings.Local.json (alternative)
|
||||
|
||||
Create `src/SurveillanceClient.Cli/appsettings.Local.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Device": {
|
||||
"Host": "192.168.100.92",
|
||||
"Port": 9000,
|
||||
"Username": "admin",
|
||||
"Password": "your-password-here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build / Run
|
||||
|
||||
```powershell
|
||||
cd SurveillanceClient
|
||||
dotnet build
|
||||
dotnet run --project src/SurveillanceClient.Cli
|
||||
```
|
||||
11
SurveillanceClient/SurveillanceClient.slnx
Normal file
11
SurveillanceClient/SurveillanceClient.slnx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SurveillanceClient.Core/SurveillanceClient.Core.csproj" />
|
||||
<Project Path="src/SurveillanceClient.Cli/SurveillanceClient.Cli.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/solution-items/">
|
||||
<File Path=".gitignore" />
|
||||
<File Path="Directory.Build.props" />
|
||||
<File Path="README.md" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
37
SurveillanceClient/src/SurveillanceClient.Cli/Program.cs
Normal file
37
SurveillanceClient/src/SurveillanceClient.Cli/Program.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SurveillanceClient.Cli;
|
||||
using SurveillanceClient.Core;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Local, never-committed overrides for credentials etc.
|
||||
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false);
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
builder.Configuration.AddUserSecrets<Program>(optional: true);
|
||||
}
|
||||
|
||||
builder.Services.AddSurveillanceClientCore();
|
||||
builder.Services.AddHostedService<TestHarness>();
|
||||
|
||||
using var host = builder.Build();
|
||||
|
||||
try
|
||||
{
|
||||
await host.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
host.Services.GetRequiredService<ILogger<Program>>()
|
||||
.LogCritical(ex, "Unhandled exception");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
/// <summary>Marker type used to anchor user-secrets and logger categories.</summary>
|
||||
public partial class Program;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>SurveillanceClient.Cli</RootNamespace>
|
||||
<AssemblyName>SurveillanceClient.Cli</AssemblyName>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UserSecretsId>surveillance-client-cli-4f0a1c0d</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SurveillanceClient.Core\SurveillanceClient.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
87
SurveillanceClient/src/SurveillanceClient.Cli/TestHarness.cs
Normal file
87
SurveillanceClient/src/SurveillanceClient.Cli/TestHarness.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SurveillanceClient.Core.Configuration;
|
||||
using SurveillanceClient.Core.Services;
|
||||
|
||||
namespace SurveillanceClient.Cli;
|
||||
|
||||
/// <summary>
|
||||
/// Iterative manual-test harness. Runs once at startup against the
|
||||
/// configured device, logs status callbacks, then requests shutdown.
|
||||
/// Extended step-by-step as the native API is proven out.
|
||||
/// </summary>
|
||||
internal sealed class TestHarness : BackgroundService
|
||||
{
|
||||
private readonly ILogger<TestHarness> _logger;
|
||||
private readonly IRsNetClient _client;
|
||||
private readonly DeviceOptions _device;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
|
||||
public TestHarness(
|
||||
ILogger<TestHarness> logger,
|
||||
IRsNetClient client,
|
||||
IOptions<DeviceOptions> device,
|
||||
IHostApplicationLifetime lifetime)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
_device = device.Value;
|
||||
_lifetime = lifetime;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
// graceful shutdown
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Test harness failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("=== Stage 1: Initialize RSNet.dll ===");
|
||||
await _client.InitializeAsync(ct).ConfigureAwait(false);
|
||||
_logger.LogInformation("RSNet initialized successfully.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_device.Host) ||
|
||||
string.IsNullOrWhiteSpace(_device.Username))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Device credentials not configured. Set them via user-secrets or appsettings.Local.json " +
|
||||
"to run the connection stage. Skipping connect.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"=== Stage 2: Connect to {Host}:{Port} as {User} ===",
|
||||
_device.Host, _device.Port, _device.Username);
|
||||
|
||||
using var session = _client.Connect(_device);
|
||||
session.StatusChanged += (_, e) =>
|
||||
_logger.LogInformation("Session status callback: code=0x{Code:X} ({CodeDec})", e.Code, e.Code);
|
||||
|
||||
_logger.LogInformation("Session handle: 0x{Handle:X}. Waiting up to 15s for login callbacks...",
|
||||
session.Handle.ToInt64());
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(15), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
|
||||
_logger.LogInformation("=== Stage 2 complete. Stop here until login status codes are mapped. ===");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Warning",
|
||||
"SurveillanceClient": "Debug"
|
||||
}
|
||||
},
|
||||
"RsNet": {
|
||||
"NativeDirectory": "i:\\Apps\\Surveillance_client"
|
||||
},
|
||||
"Device": {
|
||||
"Host": "",
|
||||
"Port": 9000,
|
||||
"Username": "",
|
||||
"Password": ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace SurveillanceClient.Core.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Credentials and endpoint for a target surveillance device.
|
||||
/// Supplied via user secrets or local config — never committed.
|
||||
/// </summary>
|
||||
public sealed class DeviceOptions
|
||||
{
|
||||
public const string SectionName = "Device";
|
||||
|
||||
public string Host { get; set; } = string.Empty;
|
||||
public ushort Port { get; set; } = 9000;
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
namespace SurveillanceClient.Core.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime options for locating and loading <c>RSNet.dll</c>.
|
||||
/// </summary>
|
||||
public sealed class RsNetOptions
|
||||
{
|
||||
public const string SectionName = "RsNet";
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to the directory that contains <c>RSNet.dll</c>
|
||||
/// and its companion files (e.g. <c>RSNet.ini</c>, P2P helpers).
|
||||
/// Typically the vendor install directory.
|
||||
/// </summary>
|
||||
public string NativeDirectory { get; set; } = @"i:\Apps\Surveillance_client";
|
||||
|
||||
/// <summary>
|
||||
/// Optional encryption value passed to <c>RSNetSetEncription</c>.
|
||||
/// <c>null</c> skips the call.
|
||||
/// </summary>
|
||||
public uint? Encryption { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SurveillanceClient.Core.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Raw P/Invoke bindings for <c>RSNet.dll</c>.
|
||||
///
|
||||
/// ABI notes (see docs/surveillance-client-rsnet-download-analysis.md):
|
||||
/// <list type="bullet">
|
||||
/// <item>Architecture: x86/32-bit. The host process MUST run as x86.</item>
|
||||
/// <item>Calling convention: <c>StdCall</c> (callee cleans stack).</item>
|
||||
/// <item>Strings: ANSI (<c>char*</c>), not UTF-16.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Request structs are intentionally passed as opaque <see cref="IntPtr"/>
|
||||
/// during the reverse-engineering phase. Strongly-typed wrappers may be
|
||||
/// introduced as the native layouts are confirmed.
|
||||
/// </summary>
|
||||
internal static partial class RsNetNative
|
||||
{
|
||||
private const string Dll = "RSNet.dll";
|
||||
|
||||
[LibraryImport(Dll, StringMarshalling = StringMarshalling.Utf8)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial void RSNetInit();
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial void RSNetRelease();
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial void RSNetSetEncription(uint value);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial IntPtr RSNetConnectionStart(IntPtr request);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial IntPtr RSNetConnectionStartEx(IntPtr request);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial int RSNetQueryRecord(IntPtr session, IntPtr queryRequest);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial int RSNetQueryRecordEx(IntPtr session, IntPtr queryRequest);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial IntPtr RSNetStartDownloadRecord(IntPtr session, IntPtr downloadRequest);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial IntPtr RSNetStartDownloadRecordEx(IntPtr session, IntPtr downloadRequest);
|
||||
|
||||
[LibraryImport(Dll)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvStdcall)])]
|
||||
internal static partial void RSNetReqRecordData(IntPtr recordPlayHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Session/status callback dispatched by the DLL's login/worker thread.
|
||||
/// Exact signature is provisional — see the analysis doc.
|
||||
/// </summary>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
internal delegate void RsNetStatusCallback(uint code, IntPtr userContext);
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SurveillanceClient.Core.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Provisional layout for <c>RSNetConnectionStart</c>'s request struct.
|
||||
///
|
||||
/// Based on decompilation of the copy-in path inside RSNet.dll:
|
||||
/// <list type="bullet">
|
||||
/// <item>+0x00 pointer to ANSI address string</item>
|
||||
/// <item>+0x04 16-bit port</item>
|
||||
/// <item>+0x08 pointer to ANSI username string</item>
|
||||
/// <item>+0x0C pointer to ANSI password string</item>
|
||||
/// <item>+0x10..+0x28 misc 32-bit fields (mode/callback-related)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// This is NOT fully validated. Treat as a best-effort starting point.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
internal struct RsNetConnectRequest
|
||||
{
|
||||
public IntPtr AddressAnsi;
|
||||
public ushort Port;
|
||||
public ushort Padding0;
|
||||
public IntPtr UsernameAnsi;
|
||||
public IntPtr PasswordAnsi;
|
||||
public uint Field10;
|
||||
public uint Field14;
|
||||
public uint Field18;
|
||||
public uint Field1C;
|
||||
public uint Field20;
|
||||
public uint Field24;
|
||||
public uint Field28;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SurveillanceClient.Core.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Win32 entry points used to control the native DLL search path before
|
||||
/// loading <c>RSNet.dll</c>. The vendor DLL expects its install directory
|
||||
/// on the search path so it can find <c>RSNet.ini</c> and helper binaries.
|
||||
/// </summary>
|
||||
internal static partial class Win32
|
||||
{
|
||||
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetDllDirectoryW(string? lpPathName);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial IntPtr AddDllDirectory(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string NewDirectory);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetDefaultDllDirectories(uint DirectoryFlags);
|
||||
|
||||
internal const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;
|
||||
internal const uint LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using SurveillanceClient.Core.Configuration;
|
||||
using SurveillanceClient.Core.Services;
|
||||
|
||||
namespace SurveillanceClient.Core;
|
||||
|
||||
/// <summary>
|
||||
/// DI registration helpers for the SurveillanceClient Core services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers <see cref="IRsNetClient"/> and binds its options from
|
||||
/// configuration. The client is a singleton because <c>RSNet.dll</c>
|
||||
/// holds process-wide global state.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurveillanceClientCore(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<RsNetOptions>().BindConfiguration(RsNetOptions.SectionName);
|
||||
services.AddOptions<DeviceOptions>().BindConfiguration(DeviceOptions.SectionName);
|
||||
|
||||
services.TryAddSingleton<IRsNetClient, RsNetClient>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using SurveillanceClient.Core.Configuration;
|
||||
|
||||
namespace SurveillanceClient.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// High-level, managed facade over <c>RSNet.dll</c>. Intended lifetime:
|
||||
/// singleton per process. <see cref="InitializeAsync"/> must be called
|
||||
/// before any session is opened.
|
||||
/// </summary>
|
||||
public interface IRsNetClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>Whether <c>RSNetInit</c> has been called successfully.</summary>
|
||||
bool IsInitialized { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads <c>RSNet.dll</c> from the configured directory and calls
|
||||
/// <c>RSNetInit</c>. Safe to call more than once; subsequent calls are no-ops.
|
||||
/// </summary>
|
||||
Task InitializeAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a direct-IP session by calling <c>RSNetConnectionStart</c>.
|
||||
/// The returned session has not yet necessarily completed login — the
|
||||
/// DLL performs login asynchronously on a worker thread.
|
||||
/// </summary>
|
||||
IRsNetSession Connect(DeviceOptions device);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle-bearing wrapper around a single <c>RSNet.dll</c> session.
|
||||
/// Disposing closes the native session and releases any pinned buffers.
|
||||
/// </summary>
|
||||
public interface IRsNetSession : IDisposable
|
||||
{
|
||||
/// <summary>Native session handle returned by <c>RSNetConnectionStart</c>.</summary>
|
||||
IntPtr Handle { get; }
|
||||
|
||||
/// <summary>Raised for each native status/event callback.</summary>
|
||||
event EventHandler<RsNetStatusEventArgs>? StatusChanged;
|
||||
}
|
||||
|
||||
/// <summary>Status event payload forwarded from the native callback.</summary>
|
||||
public sealed class RsNetStatusEventArgs(uint code) : EventArgs
|
||||
{
|
||||
public uint Code { get; } = code;
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SurveillanceClient.Core.Configuration;
|
||||
using SurveillanceClient.Core.Native;
|
||||
|
||||
namespace SurveillanceClient.Core.Services;
|
||||
|
||||
/// <inheritdoc cref="IRsNetClient"/>
|
||||
internal sealed class RsNetClient : IRsNetClient
|
||||
{
|
||||
private readonly ILogger<RsNetClient> _logger;
|
||||
private readonly RsNetOptions _options;
|
||||
private readonly SemaphoreSlim _initLock = new(1, 1);
|
||||
private bool _initialized;
|
||||
private bool _disposed;
|
||||
|
||||
public RsNetClient(ILogger<RsNetClient> logger, IOptions<RsNetOptions> options)
|
||||
{
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public bool IsInitialized => _initialized;
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_initialized) return;
|
||||
|
||||
await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
throw new PlatformNotSupportedException("RSNet.dll is Windows-only.");
|
||||
|
||||
if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
|
||||
throw new PlatformNotSupportedException(
|
||||
$"RSNet.dll is 32-bit (x86). Current process is {RuntimeInformation.ProcessArchitecture}. " +
|
||||
"Build and run the host as x86.");
|
||||
|
||||
var dir = _options.NativeDirectory;
|
||||
if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
|
||||
throw new DirectoryNotFoundException(
|
||||
$"RsNetOptions.NativeDirectory not found: '{dir}'. " +
|
||||
"Configure it to the vendor install directory containing RSNet.dll.");
|
||||
|
||||
var dllPath = Path.Combine(dir, "RSNet.dll");
|
||||
if (!File.Exists(dllPath))
|
||||
throw new FileNotFoundException("RSNet.dll not found in NativeDirectory.", dllPath);
|
||||
|
||||
_logger.LogInformation("Configuring native DLL search path: {Directory}", dir);
|
||||
|
||||
// Use the modern search list so both RSNet.dll and its implicit
|
||||
// dependencies are resolved from the vendor directory.
|
||||
Win32.SetDefaultDllDirectories(
|
||||
Win32.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | Win32.LOAD_LIBRARY_SEARCH_USER_DIRS);
|
||||
Win32.AddDllDirectory(dir);
|
||||
|
||||
// Legacy fallback for any implicit LoadLibrary calls by RSNet.dll itself.
|
||||
Win32.SetDllDirectoryW(dir);
|
||||
|
||||
// Many vendor DLLs read <Name>.ini from the *current* working directory
|
||||
// rather than the DLL's own directory. Switch CWD so RSNet.ini resolves.
|
||||
var previousCwd = Environment.CurrentDirectory;
|
||||
Environment.CurrentDirectory = dir;
|
||||
_logger.LogDebug("CWD changed {Previous} -> {New}", previousCwd, dir);
|
||||
|
||||
_logger.LogInformation("Calling RSNetInit()");
|
||||
RsNetNative.RSNetInit();
|
||||
|
||||
if (_options.Encryption is { } enc)
|
||||
{
|
||||
_logger.LogInformation("Calling RSNetSetEncription({Value})", enc);
|
||||
RsNetNative.RSNetSetEncription(enc);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_initLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public IRsNetSession Connect(DeviceOptions device)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_initialized)
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(RsNetClient)} is not initialized. Call InitializeAsync first.");
|
||||
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(device.Host);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(device.Username);
|
||||
|
||||
return RsNetSession.Open(_logger, device);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
if (_initialized)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Calling RSNetRelease()");
|
||||
RsNetNative.RSNetRelease();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "RSNetRelease threw during shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
_initLock.Dispose();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SurveillanceClient.Core.Configuration;
|
||||
using SurveillanceClient.Core.Native;
|
||||
|
||||
namespace SurveillanceClient.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Owns a single <c>RSNetConnectionStart</c> session handle plus the
|
||||
/// unmanaged buffers (ANSI strings, request struct) that the DLL reads
|
||||
/// from during the asynchronous login worker's lifetime.
|
||||
/// </summary>
|
||||
internal sealed class RsNetSession : IRsNetSession
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly List<IntPtr> _unmanagedAllocs = [];
|
||||
private readonly RsNetNative.RsNetStatusCallback? _callback;
|
||||
private IntPtr _handle;
|
||||
private bool _disposed;
|
||||
|
||||
public IntPtr Handle => _handle;
|
||||
public event EventHandler<RsNetStatusEventArgs>? StatusChanged;
|
||||
|
||||
private RsNetSession(ILogger logger, RsNetNative.RsNetStatusCallback callback)
|
||||
{
|
||||
_logger = logger;
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
internal static RsNetSession Open(ILogger logger, DeviceOptions device)
|
||||
{
|
||||
RsNetSession? session = null;
|
||||
RsNetNative.RsNetStatusCallback callback = null!;
|
||||
try
|
||||
{
|
||||
// The callback MUST be held by a managed field so the GC does not
|
||||
// collect the thunk while the native worker still holds the pointer.
|
||||
session = new RsNetSession(logger, (uint code, IntPtr ctx) =>
|
||||
{
|
||||
logger.LogDebug("RSNet status callback: code={Code} ctx={Ctx}", code, ctx);
|
||||
session?.RaiseStatus(code);
|
||||
});
|
||||
callback = session._callback!;
|
||||
|
||||
var hostPtr = MarshalAnsi(session, device.Host);
|
||||
var userPtr = MarshalAnsi(session, device.Username);
|
||||
var passPtr = MarshalAnsi(session, device.Password);
|
||||
|
||||
var request = new RsNetConnectRequest
|
||||
{
|
||||
AddressAnsi = hostPtr,
|
||||
Port = device.Port,
|
||||
Padding0 = 0,
|
||||
UsernameAnsi = userPtr,
|
||||
PasswordAnsi = passPtr,
|
||||
// Fields 0x10..0x28 are mode/callback-related and not yet
|
||||
// fully validated. Leaving them zero matches the minimal
|
||||
// direct-IP login path observed in Surveillance_client.exe.
|
||||
};
|
||||
|
||||
var requestPtr = Marshal.AllocHGlobal(Marshal.SizeOf<RsNetConnectRequest>());
|
||||
session._unmanagedAllocs.Add(requestPtr);
|
||||
Marshal.StructureToPtr(request, requestPtr, fDeleteOld: false);
|
||||
|
||||
logger.LogInformation(
|
||||
"RSNetConnectionStart host={Host} port={Port} user={User}",
|
||||
device.Host, device.Port, device.Username);
|
||||
|
||||
var handle = RsNetNative.RSNetConnectionStart(requestPtr);
|
||||
if (handle == IntPtr.Zero)
|
||||
throw new InvalidOperationException("RSNetConnectionStart returned NULL.");
|
||||
|
||||
session._handle = handle;
|
||||
return session;
|
||||
}
|
||||
catch
|
||||
{
|
||||
session?.Dispose();
|
||||
GC.KeepAlive(callback);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseStatus(uint code)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusChanged?.Invoke(this, new RsNetStatusEventArgs(code));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Status handler threw for code {Code}", code);
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr MarshalAnsi(RsNetSession session, string? value)
|
||||
{
|
||||
var ptr = Marshal.StringToHGlobalAnsi(value ?? string.Empty);
|
||||
session._unmanagedAllocs.Add(ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// The exported stop-session API has not been positively identified in
|
||||
// the current RSNet.dll symbol recovery (see analysis doc). For now
|
||||
// we simply release native buffers and let RSNetRelease() at shutdown
|
||||
// tear down session state. Once the stop export is confirmed, call it
|
||||
// here with _handle before freeing buffers.
|
||||
foreach (var p in _unmanagedAllocs)
|
||||
{
|
||||
if (p != IntPtr.Zero) Marshal.FreeHGlobal(p);
|
||||
}
|
||||
_unmanagedAllocs.Clear();
|
||||
|
||||
// Keep the callback alive until after native cleanup would have run.
|
||||
GC.KeepAlive(_callback);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>SurveillanceClient.Core</RootNamespace>
|
||||
<AssemblyName>SurveillanceClient.Core</AssemblyName>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Add table
Add a link
Reference in a new issue