diff --git a/src-csharp/RDPWInst/InstallerEngine.cs b/src-csharp/RDPWInst/InstallerEngine.cs
new file mode 100644
index 0000000..36936e3
--- /dev/null
+++ b/src-csharp/RDPWInst/InstallerEngine.cs
@@ -0,0 +1,730 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+//
+// InstallerEngine — translates every procedure in RDPWInst.dpr to C#.
+// Consult src-installer/RDPWInst.dpr for the authoritative Delphi source.
+
+using System.Reflection;
+using RDPWrap.Common;
+
+namespace RDPWInst;
+
+///
+/// Orchestrates install / uninstall / update / restart of the RDP Wrapper.
+/// All public methods return an exit code (0 = success).
+///
+internal sealed class InstallerEngine
+{
+ // ── State (mirrors Delphi globals) ────────────────────────────────────────
+
+ private bool _installed;
+ private bool _online;
+ private string _wrapPath = string.Empty;
+ private string _termServicePath = string.Empty;
+ private string _termSrvVerTxt = string.Empty;
+ private uint _termServicePid;
+ private string[] _shareServices = Array.Empty();
+
+ private const string TermService = "TermService";
+
+ // Latest release download base URL
+ private const string ReleaseBaseUrl =
+ "https://github.com/sjackson0109/rdpwrap/releases/latest/download/";
+
+ // ── Public entry points ───────────────────────────────────────────────────
+
+ ///
+ /// Install the wrapper. Mirrors the -i branch in RDPWInst.dpr.
+ ///
+ public int Install(bool toSystem32, bool online)
+ {
+ if (_installed)
+ {
+ Console.WriteLine("[*] RDP Wrapper Library is already installed.");
+ return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
+ }
+
+ Console.WriteLine("[*] Notice to user:");
+ Console.WriteLine(" - By using all or any portion of this software, you are agreeing");
+ Console.WriteLine(" to be bound by all the terms and conditions of the license agreement.");
+ Console.WriteLine(" - To read the license agreement, run the installer with -l parameter.");
+ Console.WriteLine(" - If you do not agree to any terms of the license agreement,");
+ Console.WriteLine(" do not use the software.");
+ Console.WriteLine("[*] Installing...");
+
+ _wrapPath = toSystem32
+ ? @"%SystemRoot%\system32\rdpwrap.dll"
+ : @"%ProgramFiles%\RDP Wrapper\rdpwrap.dll";
+
+ if (ArchHelper.Is64Bit) ArchHelper.DisableWow64Redirection();
+
+ CheckTermsrvVersion();
+ CheckTermsrvProcess();
+
+ Console.WriteLine("[*] Extracting files...");
+ _online = online;
+ ExtractFiles();
+
+ Console.WriteLine("[*] Checking INI coverage for installed termsrv.dll version...");
+ TryAutoGenerateOffsets();
+
+ Console.WriteLine("[*] Configuring service library...");
+ SetWrapperDll();
+
+ Console.WriteLine("[*] Checking dependencies...");
+ CheckTermsrvDependencies();
+
+ Console.WriteLine("[*] Terminating service...");
+ SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
+ ProcessHelper.KillProcess(_termServicePid);
+ Thread.Sleep(1000);
+
+ RestartSharedServices();
+ Thread.Sleep(500);
+ ServiceHelper.StartService(TermService);
+ Thread.Sleep(500);
+
+ Console.WriteLine("[*] Configuring registry...");
+ TSConfigRegistry(enable: true);
+ Console.WriteLine("[*] Configuring firewall...");
+ TSConfigFirewall(enable: true);
+
+ Console.WriteLine("[+] Successfully installed.");
+
+ if (ArchHelper.Is64Bit) ArchHelper.RevertWow64Redirection();
+ return 0;
+ }
+
+ ///
+ /// Uninstall the wrapper. Mirrors the -u branch.
+ ///
+ public int Uninstall(bool keepSettings)
+ {
+ if (!_installed)
+ {
+ Console.WriteLine("[*] RDP Wrapper Library is not installed.");
+ return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
+ }
+
+ Console.WriteLine("[*] Uninstalling...");
+ if (ArchHelper.Is64Bit) ArchHelper.DisableWow64Redirection();
+
+ CheckTermsrvProcess();
+
+ Console.WriteLine("[*] Resetting service library...");
+ ResetServiceDll();
+
+ Console.WriteLine("[*] Terminating service...");
+ SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
+ ProcessHelper.KillProcess(_termServicePid);
+ Thread.Sleep(1000);
+
+ Console.WriteLine("[*] Removing files...");
+ DeleteFiles();
+
+ RestartSharedServices();
+ Thread.Sleep(500);
+ ServiceHelper.StartService(TermService);
+ Thread.Sleep(500);
+
+ if (!keepSettings)
+ {
+ Console.WriteLine("[*] Configuring registry...");
+ TSConfigRegistry(enable: false);
+ Console.WriteLine("[*] Configuring firewall...");
+ TSConfigFirewall(enable: false);
+ }
+
+ if (ArchHelper.Is64Bit) ArchHelper.RevertWow64Redirection();
+ Console.WriteLine("[+] Successfully uninstalled.");
+ return 0;
+ }
+
+ ///
+ /// Download the latest rdpwrap.ini. Mirrors the -w / CheckUpdate branch.
+ ///
+ public int Update()
+ {
+ if (!_installed)
+ {
+ Console.WriteLine("[*] RDP Wrapper Library is not installed.");
+ return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
+ }
+
+ Console.WriteLine("[*] Checking for updates...");
+ return CheckUpdate();
+ }
+
+ ///
+ /// Force-restart Terminal Services. Mirrors the -r branch.
+ ///
+ public int Restart()
+ {
+ Console.WriteLine("[*] Restarting...");
+ CheckTermsrvProcess();
+
+ Console.WriteLine("[*] Terminating service...");
+ SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
+ ProcessHelper.KillProcess(_termServicePid);
+ Thread.Sleep(1000);
+
+ RestartSharedServices();
+ Thread.Sleep(500);
+ ServiceHelper.StartService(TermService);
+
+ Console.WriteLine("[+] Done.");
+ return 0;
+ }
+
+ // ── CheckInstall ──────────────────────────────────────────────────────────
+
+ ///
+ /// Validates the TermService registry image path and sets
+ /// and .
+ /// Mirrors the Delphi CheckInstall procedure.
+ ///
+ public void CheckInstall()
+ {
+ const string svcKey = @"SYSTEM\CurrentControlSet\Services\TermService";
+ const string paramsKey = @"SYSTEM\CurrentControlSet\Services\TermService\Parameters";
+
+ var imagePath = RegistryHelper.ReadString(svcKey, "ImagePath") ?? string.Empty;
+ if (!imagePath.Contains("svchost.exe", StringComparison.OrdinalIgnoreCase) &&
+ !imagePath.Contains("svchost -k", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.Error.WriteLine("[-] TermService is hosted in a custom application (BeTwin, etc.) - unsupported.");
+ Console.Error.WriteLine($"[*] ImagePath: \"{imagePath}\".");
+ Environment.Exit(unchecked((int)NativeMethods.ERROR_NOT_SUPPORTED));
+ }
+
+ var serviceDll = RegistryHelper.ReadString(paramsKey, "ServiceDll") ?? string.Empty;
+ if (!serviceDll.Contains("termsrv.dll", StringComparison.OrdinalIgnoreCase) &&
+ !serviceDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.Error.WriteLine("[-] Another third-party TermService library is installed.");
+ Console.Error.WriteLine($"[*] ServiceDll: \"{serviceDll}\".");
+ Environment.Exit(unchecked((int)NativeMethods.ERROR_NOT_SUPPORTED));
+ }
+
+ _termServicePath = serviceDll;
+ _installed = serviceDll.Contains("rdpwrap.dll", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // ── CheckTermsrvProcess ────────────────────────────────────────────────────
+
+ ///
+ /// Finds the TermService process ID, auto-starts the service if needed,
+ /// and collects co-hosted service names. Mirrors CheckTermsrvProcess.
+ ///
+ private void CheckTermsrvProcess()
+ {
+ bool started = false;
+ retry:
+ var services = ServiceHelper.EnumServiceProcesses();
+ var ts = services.FirstOrDefault(s =>
+ s.ServiceName.Equals(TermService, StringComparison.OrdinalIgnoreCase));
+
+ if (ts is null)
+ {
+ Console.Error.WriteLine($"[-] {TermService} not found.");
+ Environment.Exit(unchecked((int)NativeMethods.ERROR_SERVICE_DOES_NOT_EXIST));
+ return;
+ }
+
+ if (ts.ProcessId == 0)
+ {
+ if (started)
+ {
+ Console.Error.WriteLine("[-] Failed to set up TermService. Unknown error.");
+ Environment.Exit(unchecked((int)NativeMethods.ERROR_SERVICE_NOT_ACTIVE));
+ return;
+ }
+ ServiceHelper.SetStartType(TermService, NativeMethods.SERVICE_AUTO_START);
+ ServiceHelper.StartService(TermService);
+ started = true;
+ goto retry;
+ }
+
+ _termServicePid = ts.ProcessId;
+ Console.WriteLine($"[+] TermService found (pid {_termServicePid}).");
+
+ _shareServices = services
+ .Where(s => s.ProcessId == _termServicePid &&
+ !s.ServiceName.Equals(TermService, StringComparison.OrdinalIgnoreCase))
+ .Select(s => s.ServiceName)
+ .ToArray();
+
+ if (_shareServices.Length > 0)
+ Console.WriteLine($"[*] Shared services found: {string.Join(", ", _shareServices)}");
+ else
+ Console.WriteLine("[*] No shared services found.");
+ }
+
+ // ── CheckTermsrvDependencies ───────────────────────────────────────────────
+
+ ///
+ /// Ensures CertPropSvc and SessionEnv are not disabled.
+ /// Mirrors the Delphi CheckTermsrvDependencies procedure.
+ ///
+ private static void CheckTermsrvDependencies()
+ {
+ foreach (var svc in new[] { "CertPropSvc", "SessionEnv" })
+ {
+ if (ServiceHelper.GetStartType(svc) == (int)NativeMethods.SERVICE_DISABLED)
+ ServiceHelper.SetStartType(svc, NativeMethods.SERVICE_DEMAND_START);
+ }
+ }
+
+ // ── CheckTermsrvVersion ────────────────────────────────────────────────────
+
+ ///
+ /// Reads the termsrv.dll version and classifies support level.
+ /// Mirrors the Delphi CheckTermsrvVersion procedure.
+ ///
+ private void CheckTermsrvVersion()
+ {
+ var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
+ if (fv is null)
+ {
+ Console.Error.WriteLine("[-] Could not read termsrv.dll version.");
+ return;
+ }
+
+ _termSrvVerTxt = fv.ToString();
+ Console.WriteLine($"[*] Terminal Services version: {_termSrvVerTxt}");
+
+ // Unsupported legacy versions
+ if (fv.Major == 5)
+ {
+ var label = (ArchHelper.Arch == 32) ? "x86" : "x64";
+ Console.WriteLine($"[!] Windows XP / Server 2003 ({label}) is not supported.");
+ return;
+ }
+
+ // Load the built-in INI to check support level
+ var builtInIni = ResourceHelper.ReadText(
+ "RDPWInst.Resources.rdpwrap.ini",
+ Assembly.GetExecutingAssembly()) ?? string.Empty;
+
+ int level = IniHelper.CheckSupportLevel(builtInIni, fv);
+
+ switch (level)
+ {
+ case 0:
+ Console.WriteLine("[-] This version of Terminal Services is not supported.");
+ Console.WriteLine("Try running \"update.bat\" or \"RDPWInst -w\" to download latest INI file.");
+ break;
+ case 1:
+ Console.WriteLine("[!] This version of Terminal Services is supported partially.");
+ Console.WriteLine("It means you may have some limitations such as only 2 concurrent sessions.");
+ Console.WriteLine("Try running \"update.bat\" or \"RDPWInst -w\" to download latest INI file.");
+ break;
+ case 2:
+ Console.WriteLine("[+] This version of Terminal Services is fully supported.");
+ break;
+ }
+ }
+
+ // ── TSConfigRegistry ──────────────────────────────────────────────────────
+
+ ///
+ /// Writes (or clears) the TS-enable registry values.
+ /// Mirrors the Delphi TSConfigRegistry procedure.
+ ///
+ private static void TSConfigRegistry(bool enable)
+ {
+ const string tsKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server";
+ const string licKey = @"SYSTEM\CurrentControlSet\Control\Terminal Server\Licensing Core";
+ const string winlogon = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon";
+ const string addInsBase = @"SYSTEM\CurrentControlSet\Control\Terminal Server\AddIns";
+
+ RegistryHelper.WriteBool(tsKey, "fDenyTSConnections", !enable);
+
+ if (enable)
+ {
+ RegistryHelper.WriteBool(licKey, "EnableConcurrentSessions", true);
+ RegistryHelper.WriteBool(winlogon, "AllowMultipleTSSessions", true);
+
+ // AddIns sub-keys (only create if the parent key is absent)
+ if (Microsoft.Win32.Registry.LocalMachine.OpenSubKey(addInsBase) is null)
+ {
+ RegistryHelper.WriteString(addInsBase + @"\Clip Redirector", "Name", "RDPClip");
+ RegistryHelper.WriteInt (addInsBase + @"\Clip Redirector", "Type", 3);
+ RegistryHelper.WriteString(addInsBase + @"\DND Redirector", "Name", "RDPDND");
+ RegistryHelper.WriteInt (addInsBase + @"\DND Redirector", "Type", 3);
+ RegistryHelper.WriteInt (addInsBase + @"\Dynamic VC", "Type", -1);
+ }
+ }
+ }
+
+ // ── TSConfigFirewall ──────────────────────────────────────────────────────
+
+ private static void TSConfigFirewall(bool enable)
+ {
+ if (enable)
+ {
+ ProcessHelper.ExecWait(
+ "netsh advfirewall firewall add rule name=\"Remote Desktop\" " +
+ "dir=in protocol=tcp localport=3389 profile=any action=allow");
+ ProcessHelper.ExecWait(
+ "netsh advfirewall firewall add rule name=\"Remote Desktop\" " +
+ "dir=in protocol=udp localport=3389 profile=any action=allow");
+ }
+ else
+ {
+ ProcessHelper.ExecWait(
+ "netsh advfirewall firewall delete rule name=\"Remote Desktop\"");
+ }
+ }
+
+ // ── ExtractFiles ──────────────────────────────────────────────────────────
+
+ ///
+ /// Creates the install directory, sets ACLs, downloads or extracts the
+ /// INI file, and extracts the correct rdpwrap DLL + optional helpers.
+ /// Mirrors the Delphi ExtractFiles procedure.
+ ///
+ private void ExtractFiles()
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ var fullPath = ArchHelper.ExpandPath(_wrapPath);
+ var dir = Path.GetDirectoryName(fullPath)!;
+
+ if (!Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ Console.WriteLine($"[+] Folder created: {dir}");
+ SecurityHelper.GrantSidFullAccess(dir, "S-1-5-18"); // Local System
+ SecurityHelper.GrantSidFullAccess(dir, "S-1-5-6"); // Service group
+ }
+
+ // ── INI file ──
+ var iniPath = Path.Combine(dir, "rdpwrap.ini");
+ if (_online)
+ {
+ Console.WriteLine("[*] Downloading latest INI file...");
+ var content = HttpHelper.DownloadString(ReleaseBaseUrl + "rdpwrap.ini");
+ if (content is not null)
+ {
+ File.WriteAllText(iniPath, content, System.Text.Encoding.UTF8);
+ Console.WriteLine($"[+] Latest INI file -> {iniPath}");
+ }
+ else
+ {
+ Console.WriteLine("[-] Failed to get online INI file, using built-in.");
+ _online = false;
+ }
+ }
+
+ if (!_online)
+ {
+ // Try a local rdpwrap.ini beside the installer first
+ var localIni = Path.Combine(
+ Path.GetDirectoryName(Environment.ProcessPath ?? string.Empty) ?? ".",
+ "rdpwrap.ini");
+
+ if (File.Exists(localIni))
+ {
+ File.Copy(localIni, iniPath, overwrite: true);
+ Console.WriteLine($"[+] Current INI file -> {iniPath}");
+ }
+ else
+ {
+ ResourceHelper.ExtractToDisk("RDPWInst.Resources.rdpwrap.ini", iniPath, asm);
+ }
+ }
+
+ // ── Core DLL ──
+ var dllRes = ArchHelper.Is64Bit ? "RDPWInst.Resources.rdpw64.dll"
+ : "RDPWInst.Resources.rdpw32.dll";
+ ResourceHelper.ExtractToDisk(dllRes, fullPath, asm);
+
+ // ── Optional helpers (Vista / Win7 clipboard redirect, Win10 RFX codec) ──
+ ExtractOptionalHelper(asm, dir);
+ }
+
+ private void ExtractOptionalHelper(Assembly asm, string dir)
+ {
+ var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
+ if (fv is null) return;
+
+ var arch = ArchHelper.Is64Bit ? "64" : "32";
+
+ // rdpclip: Vista 6.0 and Win7 6.1
+ string? clipRes = (fv.Major, fv.Minor) switch
+ {
+ (6, 0) => $"RDPWInst.Resources.rdpclip60{arch}.exe",
+ (6, 1) => $"RDPWInst.Resources.rdpclip61{arch}.exe",
+ _ => null
+ };
+ if (clipRes is not null)
+ {
+ var dest = ArchHelper.ExpandPath(@"%SystemRoot%\System32\rdpclip.exe");
+ if (!File.Exists(dest))
+ ResourceHelper.ExtractToDisk(clipRes, dest, asm);
+ }
+
+ // rfxvmt.dll: Windows 10 (6.10.x maps to 10.0 in NT versioning)
+ if (fv.Major == 10 && fv.Minor == 0)
+ {
+ var rfxRes = $"RDPWInst.Resources.rfxvmt{arch}.dll";
+ var rfxDest = ArchHelper.ExpandPath(@"%SystemRoot%\System32\rfxvmt.dll");
+ if (!File.Exists(rfxDest))
+ ResourceHelper.ExtractToDisk(rfxRes, rfxDest, asm);
+ }
+ }
+
+ // ── SetWrapperDll / ResetServiceDll ──────────────────────────────────────
+
+ private void SetWrapperDll()
+ {
+ const string key = @"SYSTEM\CurrentControlSet\Services\TermService\Parameters";
+ RegistryHelper.WriteExpandString(key, "ServiceDll", _wrapPath);
+
+ // Vista 6.0 workaround — reg.exe write to bypass WOW64 quirk
+ var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
+ if (fv is { Major: 6, Minor: 0 } && ArchHelper.Is64Bit)
+ {
+ ProcessHelper.ExecWait(
+ $"\"{ArchHelper.ExpandPath("%SystemRoot%")}\\system32\\reg.exe\" " +
+ $"add HKLM\\SYSTEM\\CurrentControlSet\\Services\\TermService\\Parameters " +
+ $"/v ServiceDll /t REG_EXPAND_SZ /d \"{_wrapPath}\" /f");
+ }
+ }
+
+ private static void ResetServiceDll()
+ {
+ const string key = @"SYSTEM\CurrentControlSet\Services\TermService\Parameters";
+ RegistryHelper.WriteExpandString(key, "ServiceDll",
+ @"%SystemRoot%\System32\termsrv.dll");
+ }
+
+ // ── DeleteFiles ───────────────────────────────────────────────────────────
+
+ private void DeleteFiles()
+ {
+ var fullPath = ArchHelper.ExpandPath(_termServicePath);
+ var dir = Path.GetDirectoryName(fullPath)!;
+ var iniPath = Path.Combine(dir, "rdpwrap.ini");
+
+ TryDelete(iniPath);
+ TryDelete(fullPath);
+ TryRemoveDir(dir);
+ }
+
+ private static void TryDelete(string path)
+ {
+ try
+ {
+ File.Delete(path);
+ Console.WriteLine($"[+] Removed file: {path}");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[-] DeleteFile error: {ex.Message}");
+ }
+ }
+
+ private static void TryRemoveDir(string dir)
+ {
+ try
+ {
+ Directory.Delete(dir);
+ Console.WriteLine($"[+] Removed folder: {dir}");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[-] RemoveDirectory error: {ex.Message}");
+ }
+ }
+
+ // ── TryAutoGenerateOffsets ────────────────────────────────────────────────
+
+ ///
+ /// If the running termsrv.dll version is absent from rdpwrap.ini, downloads
+ /// RDPWrapOffsetFinder + Zydis from the release assets and runs the finder
+ /// to append generated offsets. Mirrors the Delphi TryAutoGenerateOffsets.
+ ///
+ private void TryAutoGenerateOffsets()
+ {
+ if (string.IsNullOrEmpty(_termSrvVerTxt)) return;
+
+ var fullPath = ArchHelper.ExpandPath(_wrapPath);
+ var iniPath = Path.Combine(Path.GetDirectoryName(fullPath)!, "rdpwrap.ini");
+
+ if (IniHelper.HasSection(iniPath, _termSrvVerTxt))
+ {
+ Console.WriteLine($"[+] Version {_termSrvVerTxt} is covered in INI.");
+ return;
+ }
+
+ Console.WriteLine($"[!] Version {_termSrvVerTxt} not found in INI.");
+ Console.WriteLine("[*] Attempting automatic offset generation via RDPWrapOffsetFinder...");
+
+ var archSuffix = ArchHelper.Is64Bit ? "_x64" : "_x86";
+ var tempDir = Path.Combine(Path.GetTempPath(), "rdpwrapoffset");
+
+ try { Directory.CreateDirectory(tempDir); }
+ catch
+ {
+ Console.Error.WriteLine("[-] Could not create temp directory. Skipping auto-generation.");
+ return;
+ }
+
+ var exePath = Path.Combine(tempDir, "RDPWrapOffsetFinder.exe");
+ var dllPath = Path.Combine(tempDir, "Zydis.dll");
+
+ Console.WriteLine($"[*] Downloading RDPWrapOffsetFinder{archSuffix}.exe ...");
+ if (!HttpHelper.DownloadFile(ReleaseBaseUrl + $"RDPWrapOffsetFinder{archSuffix}.exe", exePath))
+ {
+ Console.Error.WriteLine("[-] Download failed. The release asset may not yet be published.");
+ Console.Error.WriteLine("[!] Run the publish-ini workflow on the sjackson0109/rdpwrap repository,");
+ Console.Error.WriteLine("[!] then re-run this installer to enable auto-generation.");
+ return;
+ }
+
+ Console.WriteLine($"[*] Downloading Zydis{archSuffix}.dll ...");
+ if (!HttpHelper.DownloadFile(ReleaseBaseUrl + $"Zydis{archSuffix}.dll", dllPath))
+ {
+ Console.Error.WriteLine("[-] Zydis download failed. Skipping auto-generation.");
+ File.Delete(exePath);
+ return;
+ }
+
+ Console.WriteLine($"[*] Running offset finder for termsrv.dll {_termSrvVerTxt} ...");
+ // Run via cmd.exe so that >> redirect to the INI file functions correctly
+ var sysCmd = ArchHelper.ExpandPath(@"%SystemRoot%\System32\cmd.exe");
+ ProcessHelper.ExecWait($"\"{sysCmd}\" /c \"\"{exePath}\" >> \"{iniPath}\"\"");
+
+ if (IniHelper.HasSection(iniPath, _termSrvVerTxt))
+ Console.WriteLine($"[+] Offsets generated successfully for version {_termSrvVerTxt}");
+ else
+ Console.WriteLine($"[!] Offset finder ran but [{_termSrvVerTxt}] was not added. " +
+ "Session may be limited or unstable for this build.");
+
+ // Clean up temporary tool files
+ try
+ {
+ File.Delete(exePath);
+ File.Delete(dllPath);
+ Directory.Delete(tempDir);
+ }
+ catch { /* best-effort */ }
+ }
+
+ // ── CheckUpdate (GitINIFile path) ─────────────────────────────────────────
+
+ private int CheckUpdate()
+ {
+ var fullPath = ArchHelper.ExpandPath(_termServicePath);
+ var iniPath = Path.Combine(Path.GetDirectoryName(fullPath)!, "rdpwrap.ini");
+
+ if (!TryGetIniDate(iniPath, null, out int oldDate))
+ return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
+
+ Console.WriteLine($"[*] Current update date: {FormatDate(oldDate)}");
+
+ var latest = HttpHelper.DownloadString(ReleaseBaseUrl + "rdpwrap.ini");
+ if (latest is null)
+ {
+ Console.Error.WriteLine("[-] Failed to download latest INI from GitHub.");
+ return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
+ }
+
+ if (!TryGetIniDate(null, latest, out int newDate))
+ return unchecked((int)NativeMethods.ERROR_ACCESS_DENIED);
+
+ Console.WriteLine($"[*] Latest update date: {FormatDate(newDate)}");
+
+ if (newDate == oldDate)
+ {
+ Console.WriteLine("[*] Everything is up to date.");
+ return 0;
+ }
+
+ if (newDate > oldDate)
+ {
+ Console.WriteLine("[+] New update is available, updating...");
+ CheckTermsrvProcess();
+
+ Console.WriteLine("[*] Terminating service...");
+ SecurityHelper.AddPrivilege(NativeMethods.SE_DEBUG_NAME);
+ ProcessHelper.KillProcess(_termServicePid);
+ Thread.Sleep(1000);
+
+ RestartSharedServices();
+ Thread.Sleep(500);
+
+ File.WriteAllText(iniPath, latest, System.Text.Encoding.UTF8);
+ Console.WriteLine($"[+] INI file updated: {iniPath}");
+
+ // Recompute version for offset generation
+ var fv = FileVersionHelper.GetVersionExpanded(_termServicePath);
+ if (fv is not null) _termSrvVerTxt = fv.ToString();
+
+ Console.WriteLine("[*] Checking INI coverage for installed termsrv.dll version...");
+ TryAutoGenerateOffsets();
+
+ ServiceHelper.StartService(TermService);
+ Console.WriteLine("[+] Update completed.");
+ }
+ else
+ {
+ Console.WriteLine("[*] Your INI file is newer than public file. Are you a developer? :)");
+ }
+
+ return 0;
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────
+
+ private void RestartSharedServices()
+ {
+ foreach (var svc in _shareServices)
+ ServiceHelper.StartService(svc);
+ }
+
+ ///
+ /// Parses the Updated=YYYYMMDD line from an INI file or string.
+ /// Mirrors the Delphi CheckINIDate function.
+ ///
+ private static bool TryGetIniDate(string? filePath, string? content, out int date)
+ {
+ date = 0;
+ IEnumerable lines;
+
+ if (filePath is not null)
+ {
+ if (!File.Exists(filePath))
+ {
+ Console.Error.WriteLine("[-] Failed to read INI file.");
+ return false;
+ }
+ lines = File.ReadLines(filePath);
+ }
+ else
+ {
+ lines = (content ?? string.Empty).Split('\n');
+ }
+
+ foreach (var line in lines)
+ {
+ var trimmed = line.TrimEnd('\r');
+ if (!trimmed.StartsWith("Updated=", StringComparison.Ordinal)) continue;
+
+ var raw = trimmed["Updated=".Length..].Replace("-", "");
+ if (int.TryParse(raw, out date)) return true;
+
+ Console.Error.WriteLine("[-] Wrong INI date format.");
+ return false;
+ }
+
+ Console.Error.WriteLine("[-] Failed to check INI date (Updated= line not found).");
+ return false;
+ }
+
+ private static string FormatDate(int d)
+ {
+ int y = d / 10000, m = (d / 100) % 100, day = d % 100;
+ return $"{y}.{m:D2}.{day:D2}";
+ }
+}
diff --git a/src-csharp/RDPWInst/Program.cs b/src-csharp/RDPWInst/Program.cs
new file mode 100644
index 0000000..69de231
--- /dev/null
+++ b/src-csharp/RDPWInst/Program.cs
@@ -0,0 +1,83 @@
+// Copyright 2024 sjackson0109 — Apache License 2.0
+//
+// RDPWInst — RDP Wrapper Library Installer
+// Direct C# translation of src-installer/RDPWInst.dpr
+
+using RDPWrap.Common;
+
+namespace RDPWInst;
+
+internal static class Program
+{
+ private const string Banner =
+ "RDP Wrapper Library v1.6.2\r\n" +
+ "Installer v3.0 (C# edition)\r\n" +
+ "Copyright (C) Stas'M Corp. 2018 / sjackson0109 2024\r\n";
+
+ private const string Usage =
+ "USAGE:\r\n" +
+ "RDPWInst.exe [-l|-i[-s][-o]|-w|-u[-k]|-r]\r\n\r\n" +
+ "-l display the license agreement\r\n" +
+ "-i install wrapper to Program Files folder (default)\r\n" +
+ "-i -s install wrapper to System32 folder\r\n" +
+ "-i -o online install mode (loads latest INI file)\r\n" +
+ "-w get latest update for INI file\r\n" +
+ "-u uninstall wrapper\r\n" +
+ "-u -k uninstall wrapper and keep settings\r\n" +
+ "-r force restart Terminal Services\r\n";
+
+ internal static int Main(string[] args)
+ {
+ Console.OutputEncoding = System.Text.Encoding.UTF8;
+ Console.WriteLine(Banner);
+
+ // Validate args
+ if (args.Length < 1 ||
+ (args[0] != "-l" &&
+ args[0] != "-i" &&
+ args[0] != "-w" &&
+ args[0] != "-u" &&
+ args[0] != "-r"))
+ {
+ Console.WriteLine(Usage);
+ return 0;
+ }
+
+ // -l print license
+ if (args[0] == "-l")
+ {
+ var license = ResourceHelper.ReadText("RDPWInst.Resources.license.txt",
+ System.Reflection.Assembly.GetExecutingAssembly());
+ Console.WriteLine(license ?? "(license resource not found)");
+ return 0;
+ }
+
+ // Windows Vista / Server 2008 minimum check
+ if (Environment.OSVersion.Version < new Version(6, 0))
+ {
+ Console.Error.WriteLine("[-] Unsupported Windows version:");
+ Console.Error.WriteLine(" only >= 6.0 (Vista, Server 2008 and newer) are supported.");
+ return 1;
+ }
+
+ if (!ArchHelper.IsSupported)
+ {
+ Console.Error.WriteLine("[-] Unsupported processor architecture.");
+ return 1;
+ }
+
+ var engine = new InstallerEngine();
+ engine.CheckInstall();
+
+ return args[0] switch
+ {
+ "-i" => engine.Install(
+ toSystem32: args.Contains("-s"),
+ online: args.Contains("-o")),
+ "-u" => engine.Uninstall(keepSettings: args.Contains("-k")),
+ "-w" => engine.Update(),
+ "-r" => engine.Restart(),
+ _ => 0
+ };
+ }
+}
diff --git a/src-csharp/RDPWInst/RDPWInst.csproj b/src-csharp/RDPWInst/RDPWInst.csproj
index c953051..76e593e 100644
--- a/src-csharp/RDPWInst/RDPWInst.csproj
+++ b/src-csharp/RDPWInst/RDPWInst.csproj
@@ -3,19 +3,38 @@
Exe
RDPWInst
RDPWInst
-
+ app.manifest
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
- -->
diff --git a/src-csharp/RDPWInst/Resources/README.md b/src-csharp/RDPWInst/Resources/README.md
new file mode 100644
index 0000000..797149d
--- /dev/null
+++ b/src-csharp/RDPWInst/Resources/README.md
@@ -0,0 +1,23 @@
+# RDPWInst/Resources
+
+Place the compiled binary payloads here before building RDPWInst.exe.
+These files are embedded as manifest resources at build time.
+
+## Required files
+
+| File | Source | Used when |
+|---------------------|------------------------------------------------|-------------------------------|
+| `rdpw32.dll` | Build output of `src-x86-x64-Fusix/` (x86) | Always (32-bit install) |
+| `rdpw64.dll` | Build output of `src-x86-x64-Fusix/` (x64) | Always (64-bit install) |
+| `rdpwrap.ini` | `res/rdpwrap.ini` (bundled baseline) | Always (fallback / offline) |
+| `rdpclip6032.exe` | Original Stas'M redistributable (x86) | Vista x86 install |
+| `rdpclip6064.exe` | Original Stas'M redistributable (x64) | Vista x64 install |
+| `rdpclip6132.exe` | Original Stas'M redistributable (x86) | Win7 x86 install |
+| `rdpclip6164.exe` | Original Stas'M redistributable (x64) | Win7 x64 install |
+| `rfxvmt32.dll` | Original Stas'M redistributable (x86) | Win10 x86 install |
+| `rfxvmt64.dll` | Original Stas'M redistributable (x64) | Win10 x64 install |
+| `license.txt` | Repo `LICENSE` file (copy here as text) | `-l` flag |
+
+Binary files in this folder are intentionally excluded from version control
+via `.gitignore`. The CI pipeline copies them from the native build output
+before invoking `dotnet publish`.
diff --git a/src-csharp/RDPWInst/app.manifest b/src-csharp/RDPWInst/app.manifest
new file mode 100644
index 0000000..4fcaaba
--- /dev/null
+++ b/src-csharp/RDPWInst/app.manifest
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+