mirror of https://github.com/stascorp/rdpwrap
Adds a self-contained governance/ops shell (RdpCoexist/) that drives the sergiye/rdpWrapper engine to enable concurrent RDP + local console (AnyDesk) without modifying System32\termsrv.dll on disk (ServiceDll -> TermWrap proxy). Closes the 5 gaps the bare engine leaves open: 1. No license grant to redistribute the engine -> engine is fetched from its official release and SHA256-pinned; nothing binary is committed here (.gitignore blocks engine/, *.exe, *.dll). 2. No self-heal after Windows Update swaps termsrv.dll -> -Heal action plus a SYSTEM at-startup scheduled task: verify, re-install only if listener dropped. 3. Verification was GUI-only -> -Verify returns a machine-checkable verdict; COEXIST-OK requires a real 3389 listener AND >=2 active sessions, not just registry values. 4. Engine phones home / version drift -> engine always invoked with -offline and the version pinned in config.psd1. 5. Engine DLL flagged HackTool:Win32/RDPWrap -> idempotent Windows Defender exclusion for the engine folder, skipped gracefully if policy-locked. Also includes: self-elevating .cmd launcher, GPO-override warning for fSingleSessionPerUser/fDenyTSConnections, and an optional dedicated local RDP account (avoids same-user dual-session license traps in OneDrive/Outlook/Office). All shipped values are neutral placeholders; no environment-specific data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>pull/4441/head
parent
a5c64a4331
commit
94aeba992d
@ -0,0 +1,6 @@
|
|||||||
|
# Never commit the engine binary (upstream has no redistribution license) or runtime state.
|
||||||
|
engine/
|
||||||
|
logs/
|
||||||
|
state.json
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
# RdpCoexist
|
||||||
|
|
||||||
|
A small, portable **governance/ops shell** that lets **AnyDesk and RDP run side by
|
||||||
|
side** (concurrent RDP sessions + local console) on modern Windows — **without
|
||||||
|
modifying `System32\termsrv.dll` on disk**.
|
||||||
|
|
||||||
|
It does *not* reimplement the wrapper. It drives
|
||||||
|
[`sergiye/rdpWrapper`](https://github.com/sergiye/rdpWrapper) (a portable, .NET
|
||||||
|
Framework 4.7.2, single-file rewrite of RDP Wrapper that redirects the
|
||||||
|
`TermService` `ServiceDll` to a non-destructive proxy — `TermWrap` — with a
|
||||||
|
built-in offset engine) and wraps it with the operational pieces it lacks.
|
||||||
|
|
||||||
|
> Approach: stop RDP service → point `TermService` `ServiceDll` at a wrapper in a
|
||||||
|
> portable folder → set `fDenyTSConnections=0`, `fSingleSessionPerUser=0`,
|
||||||
|
> `EnableConcurrentSessions=1` → start service. The original `termsrv.dll` is
|
||||||
|
> **never touched**, so SFC / WRP / Windows Update leave it alone.
|
||||||
|
|
||||||
|
## Why a shell (the 5 gaps this closes)
|
||||||
|
|
||||||
|
| # | Gap in the bare engine | What this shell adds |
|
||||||
|
|---|------------------------|----------------------|
|
||||||
|
| 1 | No license grant → cannot repackage/redistribute the engine binary | Engine is **fetched from its official release** and **SHA256-pinned**; nothing is bundled here (`.gitignore` blocks `*.exe`/`*.dll`) |
|
||||||
|
| 2 | No self-heal after Windows Update replaces `termsrv.dll` | `-Heal` + a **SYSTEM startup scheduled task**: verify, and only re-install if the listener dropped |
|
||||||
|
| 3 | Verification is GUI-only | `-Verify` returns a **machine-checkable verdict** (`COEXIST-OK` requires a real listener **and** ≥2 active sessions, not just registry values) |
|
||||||
|
| 4 | Engine phones home for updates → version drift | Engine is **always** invoked with `-offline`; version is pinned in `config.psd1` |
|
||||||
|
| 5 | Engine DLL is commonly flagged `HackTool:Win32/RDPWrap` | Idempotent **Defender exclusion** for the engine folder (skipped gracefully if exclusions are policy-locked) |
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
RdpCoexist/
|
||||||
|
├─ RdpCoexist.cmd self-elevating launcher (UAC)
|
||||||
|
├─ RdpCoexist.ps1 -Apply | -Revert | -Verify | -Heal | -Status
|
||||||
|
├─ config.psd1 version pin, SHA256 pin, paths, options
|
||||||
|
└─ modules/
|
||||||
|
├─ Common.psm1 logging, admin check, GPO-override warning
|
||||||
|
├─ Engine.psm1 download + hash-verify + drive the engine
|
||||||
|
├─ Defender.psm1 idempotent exclusion add/remove
|
||||||
|
├─ Verify.psm1 listener + active-session verdict
|
||||||
|
├─ Heal.psm1 scheduled-task register/unregister
|
||||||
|
└─ LocalUser.psm1 optional dedicated RDP account
|
||||||
|
```
|
||||||
|
Engine binary, logs, and state live under `%ProgramData%\RdpCoexist\` — never in this repo.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Edit `config.psd1`. At minimum set **`EngineSha256`** to the hash of the engine
|
||||||
|
release you vetted (or run the first `-Apply` with `-TrustOnFirstUse`, which
|
||||||
|
prints the observed hash for you to paste back and lock).
|
||||||
|
2. Run:
|
||||||
|
|
||||||
|
```
|
||||||
|
RdpCoexist.cmd -Apply :: fetch+verify engine, install, register heal task, verify
|
||||||
|
RdpCoexist.cmd -Verify :: print current verdict
|
||||||
|
RdpCoexist.cmd -Revert :: uninstall engine, remove task + exclusion
|
||||||
|
```
|
||||||
|
|
||||||
|
`COEXIST-OK` only appears once **two** sessions are actually active (e.g. console +
|
||||||
|
one RDP), because a listener alone does not prove coexistence.
|
||||||
|
|
||||||
|
## Requirements & caveats
|
||||||
|
|
||||||
|
- **Admin** required (the launcher elevates).
|
||||||
|
- **.NET Framework 4.7.2** — built into supported Windows 10/11; no install needed.
|
||||||
|
- **Domain GPO**: if `fSingleSessionPerUser` / `fDenyTSConnections` are enforced by
|
||||||
|
policy, they revert on GP refresh. The shell warns; resolve with IT.
|
||||||
|
- **Same-user dual session** trips license locks in OneDrive/Outlook/Office. Prefer
|
||||||
|
the optional dedicated RDP account (`CreateRdpUser`), keeping the console user for AnyDesk.
|
||||||
|
- **Portability vs. resilience**: the engine is copied under `%ProgramData%` by
|
||||||
|
default. Pointing `ServiceDll` at removable media means RDP dies if the media is
|
||||||
|
pulled — don't.
|
||||||
|
- Concurrent-session enablement changes the effective RDP session limit; confirm your
|
||||||
|
own licensing position before use.
|
||||||
|
|
||||||
|
## Configuration keys
|
||||||
|
|
||||||
|
See `config.psd1` for the annotated list. Values shipped here are **neutral
|
||||||
|
placeholders** — set the version pin, hash pin, and (if used) the RDP user name to
|
||||||
|
your own before running.
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set "PS1=%~dp0RdpCoexist.ps1"
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %errorlevel%==0 (
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" %*
|
||||||
|
) else (
|
||||||
|
echo Requesting administrator elevation...
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||||
|
"Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','%PS1%' %*"
|
||||||
|
)
|
||||||
|
endlocal
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Portable AnyDesk + RDP coexistence orchestrator (governance shell around
|
||||||
|
sergiye/rdpWrapper). Enables concurrent RDP sessions without touching
|
||||||
|
System32\termsrv.dll on disk (ServiceDll redirection + TermWrap backend).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This shell adds the operational pieces the engine does not provide:
|
||||||
|
1. Engine is fetched from its official release and SHA256-pinned (no
|
||||||
|
redistribution here; upstream has no license grant).
|
||||||
|
2. Post-Windows-Update self-heal via a SYSTEM startup scheduled task.
|
||||||
|
3. A machine-checkable verdict (listener + >=2 active sessions), not GUI.
|
||||||
|
4. The engine is always run with -offline (no phone-home / version drift).
|
||||||
|
5. Windows Defender exclusion handled idempotently.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
RdpCoexist.cmd -Apply
|
||||||
|
RdpCoexist.cmd -Verify
|
||||||
|
RdpCoexist.cmd -Revert
|
||||||
|
#>
|
||||||
|
[CmdletBinding(DefaultParameterSetName = 'Verify')]
|
||||||
|
param(
|
||||||
|
[Parameter(ParameterSetName = 'Apply')] [switch]$Apply,
|
||||||
|
[Parameter(ParameterSetName = 'Revert')] [switch]$Revert,
|
||||||
|
[Parameter(ParameterSetName = 'Verify')] [switch]$Verify,
|
||||||
|
[Parameter(ParameterSetName = 'Heal')] [switch]$Heal,
|
||||||
|
[Parameter(ParameterSetName = 'Status')] [switch]$Status,
|
||||||
|
[switch]$TrustOnFirstUse
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
Import-Module (Join-Path $here 'modules\Common.psm1') -Force
|
||||||
|
Import-Module (Join-Path $here 'modules\Engine.psm1') -Force
|
||||||
|
Import-Module (Join-Path $here 'modules\Defender.psm1') -Force
|
||||||
|
Import-Module (Join-Path $here 'modules\Verify.psm1') -Force
|
||||||
|
Import-Module (Join-Path $here 'modules\Heal.psm1') -Force
|
||||||
|
Import-Module (Join-Path $here 'modules\LocalUser.psm1') -Force
|
||||||
|
|
||||||
|
$cfg = Import-PowerShellDataFile (Join-Path $here 'config.psd1')
|
||||||
|
New-Item -ItemType Directory -Path $cfg.InstallRoot -Force | Out-Null
|
||||||
|
Initialize-Log -Root $cfg.InstallRoot
|
||||||
|
|
||||||
|
function Do-Apply {
|
||||||
|
Assert-Admin
|
||||||
|
Test-DomainPolicyOverride
|
||||||
|
if ($cfg.AddDefenderExclusion) { Add-DefenderExclusion -Path (Join-Path $cfg.InstallRoot 'engine') }
|
||||||
|
|
||||||
|
$exe = Resolve-Engine -Config $cfg -TrustOnFirstUse:$TrustOnFirstUse
|
||||||
|
Set-PreferredWrapper -Wrapper $cfg.PreferredWrapper
|
||||||
|
$rc = Invoke-Engine -Exe $exe -EngineArgs @('-install') -Offline:$cfg.AlwaysOffline
|
||||||
|
if ($rc -ne 0) { throw "Engine install failed (exit=$rc)." }
|
||||||
|
|
||||||
|
if ($cfg.CreateRdpUser) { New-RdpLocalUser -UserName $cfg.RdpUserName }
|
||||||
|
if ($cfg.RegisterHealTask) { Register-HealTask -Config $cfg -ScriptPath $PSCommandPath }
|
||||||
|
|
||||||
|
$v = Test-Coexist -ActiveSessionPattern $cfg.ActiveSessionPattern
|
||||||
|
$v | Format-List | Out-String | Write-Host
|
||||||
|
if ($v.Verdict -eq 'FAIL') { Write-Log 'Apply finished but listener is down.' 'Error' }
|
||||||
|
elseif ($v.Verdict -eq 'LISTENING-UNVERIFIED') {
|
||||||
|
Write-Log 'Listener up. Open a second concurrent session to confirm COEXIST-OK.' 'Warn'
|
||||||
|
}
|
||||||
|
else { Write-Log 'COEXIST-OK.' 'Ok' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Do-Revert {
|
||||||
|
Assert-Admin
|
||||||
|
$exe = Join-Path (Join-Path $cfg.InstallRoot 'engine') $cfg.EngineAsset
|
||||||
|
if (Test-Path $exe) { Invoke-Engine -Exe $exe -EngineArgs @('-uninstall') -Offline:$cfg.AlwaysOffline | Out-Null }
|
||||||
|
Unregister-HealTask -Config $cfg
|
||||||
|
if ($cfg.AddDefenderExclusion) { Remove-DefenderExclusion -Path (Join-Path $cfg.InstallRoot 'engine') }
|
||||||
|
if ($cfg.CreateRdpUser) { Write-Log "Local user '$($cfg.RdpUserName)' left intact; remove manually if desired." 'Warn' }
|
||||||
|
Write-Log 'Revert complete.' 'Ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Do-Heal {
|
||||||
|
Assert-Admin
|
||||||
|
$v = Test-Coexist -ActiveSessionPattern $cfg.ActiveSessionPattern
|
||||||
|
if ($v.Verdict -ne 'FAIL') { Write-Log "Heal: healthy ($($v.Verdict)); nothing to do." 'Ok'; return }
|
||||||
|
Write-Log 'Heal: listener down (likely post-update). Re-installing engine.' 'Warn'
|
||||||
|
$exe = Resolve-Engine -Config $cfg -TrustOnFirstUse:$TrustOnFirstUse
|
||||||
|
Set-PreferredWrapper -Wrapper $cfg.PreferredWrapper
|
||||||
|
Invoke-Engine -Exe $exe -EngineArgs @('-install') -Offline:$cfg.AlwaysOffline | Out-Null
|
||||||
|
(Test-Coexist -ActiveSessionPattern $cfg.ActiveSessionPattern) | Format-List | Out-String | Write-Host
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($PSCmdlet.ParameterSetName) {
|
||||||
|
'Apply' { Do-Apply }
|
||||||
|
'Revert' { Do-Revert }
|
||||||
|
'Heal' { Do-Heal }
|
||||||
|
default { (Test-Coexist -ActiveSessionPattern $cfg.ActiveSessionPattern) | Format-List }
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
@{
|
||||||
|
# =========================================================================
|
||||||
|
# RdpCoexist configuration
|
||||||
|
# Values below are neutral defaults / placeholders. Fill EngineSha256
|
||||||
|
# before the first -Apply (or use -TrustOnFirstUse to pin interactively).
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
# ---- Engine: sergiye/rdpWrapper (used as-is; not redistributed here) ----
|
||||||
|
EngineRepo = 'sergiye/rdpWrapper'
|
||||||
|
EngineVersion = '2.15' # pin ONE vetted release; never float to "latest"
|
||||||
|
EngineAsset = 'rdpWrapper_x64.exe'
|
||||||
|
|
||||||
|
# REQUIRED before first Apply. SHA256 of the vetted asset, uppercase hex.
|
||||||
|
# Empty => -Apply refuses unless you pass -TrustOnFirstUse (prints the hash
|
||||||
|
# so you can paste it back here and re-run).
|
||||||
|
EngineSha256 = ''
|
||||||
|
|
||||||
|
# ---- Paths ----
|
||||||
|
InstallRoot = 'C:\ProgramData\RdpCoexist'
|
||||||
|
|
||||||
|
# ---- Behaviour ----
|
||||||
|
PreferredWrapper = 'TermWrap' # TermWrap = offset-engine backend, no ini/offset dance
|
||||||
|
AlwaysOffline = $true # always pass -offline so the engine never phones home
|
||||||
|
AddDefenderExclusion = $true # engine dll is commonly flagged HackTool:Win32/RDPWrap
|
||||||
|
RegisterHealTask = $true # self-heal after Windows Update (re-install if listener drops)
|
||||||
|
HealTaskName = 'RdpCoexist-Heal'
|
||||||
|
|
||||||
|
# ---- Optional: dedicated local account for RDP (avoids same-user dual-session
|
||||||
|
# license traps in OneDrive/Outlook/Office). Off by default. ----
|
||||||
|
CreateRdpUser = $false
|
||||||
|
RdpUserName = 'rdpuser' # placeholder; change before enabling
|
||||||
|
|
||||||
|
# ---- Verify: locales that render session state as "Active" in qwinsta ----
|
||||||
|
ActiveSessionPattern = 'Active|使用中|アクティブ|활성'
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
$script:LogPath = $null
|
||||||
|
|
||||||
|
function Initialize-Log {
|
||||||
|
param([string]$Root)
|
||||||
|
$dir = Join-Path $Root 'logs'
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
$script:LogPath = Join-Path $dir ('rdpcoexist-{0:yyyyMMdd}.log' -f (Get-Date))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Log {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$Message,
|
||||||
|
[ValidateSet('Info', 'Warn', 'Error', 'Ok')][string]$Level = 'Info'
|
||||||
|
)
|
||||||
|
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$line = "[$stamp] [$Level] $Message"
|
||||||
|
$color = switch ($Level) { 'Warn' { 'Yellow' } 'Error' { 'Red' } 'Ok' { 'Green' } default { 'Gray' } }
|
||||||
|
Write-Host $line -ForegroundColor $color
|
||||||
|
if ($script:LogPath) { Add-Content -Path $script:LogPath -Value $line -Encoding UTF8 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-Admin {
|
||||||
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$pr = New-Object Security.Principal.WindowsPrincipal($id)
|
||||||
|
if (-not $pr.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
throw 'Administrator rights required. Launch via RdpCoexist.cmd (UAC) or an elevated shell.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-DomainPolicyOverride {
|
||||||
|
# fSingleSessionPerUser / fDenyTSConnections may be enforced by GPO and will
|
||||||
|
# silently revert local changes on the next GP refresh. Warn, do not fail.
|
||||||
|
$p = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services'
|
||||||
|
if (Test-Path $p) {
|
||||||
|
$k = Get-ItemProperty $p -ErrorAction SilentlyContinue
|
||||||
|
foreach ($v in 'fSingleSessionPerUser', 'fDenyTSConnections') {
|
||||||
|
if ($null -ne $k.$v) {
|
||||||
|
Write-Log "GPO sets $v=$($k.$v); it may override local settings on GP refresh. Check with IT." 'Warn'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Initialize-Log, Write-Log, Assert-Admin, Test-DomainPolicyOverride
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
function Add-DefenderExclusion {
|
||||||
|
param([Parameter(Mandatory)][string]$Path)
|
||||||
|
try {
|
||||||
|
$cur = @((Get-MpPreference).ExclusionPath)
|
||||||
|
if ($cur -notcontains $Path) {
|
||||||
|
Add-MpPreference -ExclusionPath $Path
|
||||||
|
Write-Log "Defender exclusion added: $Path" 'Ok'
|
||||||
|
}
|
||||||
|
else { Write-Log "Defender exclusion already present: $Path" }
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# If exclusions are locked by Intune/Tenant, only IT can add them.
|
||||||
|
Write-Log "Could not add Defender exclusion (managed by policy?): $_" 'Warn'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-DefenderExclusion {
|
||||||
|
param([Parameter(Mandatory)][string]$Path)
|
||||||
|
try { Remove-MpPreference -ExclusionPath $Path -ErrorAction SilentlyContinue }
|
||||||
|
catch { Write-Log "Could not remove Defender exclusion: $_" 'Warn' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Add-DefenderExclusion, Remove-DefenderExclusion
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
function Resolve-Engine {
|
||||||
|
<#
|
||||||
|
Ensures the sergiye/rdpWrapper engine exe is present under <InstallRoot>\engine
|
||||||
|
and its SHA256 matches the pin. The engine is downloaded from its official
|
||||||
|
GitHub release (not redistributed in this repo, which has no license grant).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]$Config,
|
||||||
|
[switch]$TrustOnFirstUse
|
||||||
|
)
|
||||||
|
$dir = Join-Path $Config.InstallRoot 'engine'
|
||||||
|
$exe = Join-Path $dir $Config.EngineAsset
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
|
||||||
|
if (-not (Test-Path $exe)) {
|
||||||
|
$url = "https://github.com/$($Config.EngineRepo)/releases/download/$($Config.EngineVersion)/$($Config.EngineAsset)"
|
||||||
|
Write-Log "Downloading engine: $url"
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = (Get-FileHash $exe -Algorithm SHA256).Hash.ToUpper()
|
||||||
|
$pin = ($Config.EngineSha256 | Out-String).Trim().ToUpper()
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($pin)) {
|
||||||
|
if ($TrustOnFirstUse) {
|
||||||
|
Write-Log "TOFU pin: $hash <-- paste into config.psd1 EngineSha256 to lock it." 'Warn'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Remove-Item $exe -Force -ErrorAction SilentlyContinue
|
||||||
|
throw "EngineSha256 is not pinned. Verify the asset out-of-band, set the pin, or re-run with -TrustOnFirstUse. Observed: $hash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($hash -ne $pin) {
|
||||||
|
Remove-Item $exe -Force -ErrorAction SilentlyContinue
|
||||||
|
throw "Engine hash mismatch (deleted). expected=$pin actual=$hash"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Engine hash verified." 'Ok'
|
||||||
|
}
|
||||||
|
return $exe
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Engine {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$Exe,
|
||||||
|
[Parameter(Mandatory)][string[]]$EngineArgs,
|
||||||
|
[bool]$Offline = $true
|
||||||
|
)
|
||||||
|
$a = @($EngineArgs)
|
||||||
|
if ($Offline) { $a += '-offline' } # -offline must be the LAST parameter
|
||||||
|
Write-Log "engine $($a -join ' ')"
|
||||||
|
$p = Start-Process -FilePath $Exe -ArgumentList $a -Wait -PassThru -NoNewWindow
|
||||||
|
Write-Log "engine exit=$($p.ExitCode)"
|
||||||
|
return $p.ExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-PreferredWrapper {
|
||||||
|
# The engine reads preferredWrapper from its own persistent settings; default
|
||||||
|
# is already TermWrap. Mirror the choice into that store so -install is unattended.
|
||||||
|
param([Parameter(Mandatory)][string]$Wrapper)
|
||||||
|
$key = 'HKCU:\Software\rdpWrapper'
|
||||||
|
try {
|
||||||
|
New-Item $key -Force | Out-Null
|
||||||
|
Set-ItemProperty $key -Name 'preferredWrapper' -Value $Wrapper
|
||||||
|
} catch { Write-Log "Could not preset preferredWrapper: $_" 'Warn' }
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Resolve-Engine, Invoke-Engine, Set-PreferredWrapper
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
function Register-HealTask {
|
||||||
|
<#
|
||||||
|
Windows Update replaces termsrv.dll, which can break memory offsets and drop
|
||||||
|
the listener. This registers a SYSTEM startup task that runs -Heal: verify,
|
||||||
|
and only if not COEXIST-OK re-run the engine install (offset engine re-adapts).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]$Config,
|
||||||
|
[Parameter(Mandatory)][string]$ScriptPath
|
||||||
|
)
|
||||||
|
$arg = '-NoProfile -ExecutionPolicy Bypass -File "{0}" -Heal' -f $ScriptPath
|
||||||
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $arg
|
||||||
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries
|
||||||
|
Register-ScheduledTask -TaskName $Config.HealTaskName -Action $action -Trigger $trigger `
|
||||||
|
-Principal $principal -Settings $settings -Force | Out-Null
|
||||||
|
Write-Log "Heal task registered: $($Config.HealTaskName)" 'Ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Unregister-HealTask {
|
||||||
|
param([Parameter(Mandatory)]$Config)
|
||||||
|
Unregister-ScheduledTask -TaskName $Config.HealTaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
Write-Log "Heal task removed: $($Config.HealTaskName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Register-HealTask, Unregister-HealTask
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
function New-RdpLocalUser {
|
||||||
|
<#
|
||||||
|
Optional: a dedicated local account for RDP so the interactive (console)
|
||||||
|
user and the RDP user are different principals. Avoids single-session
|
||||||
|
license locks in OneDrive/Outlook/Office. Prompts for the password; this
|
||||||
|
tool never bakes or transmits credentials.
|
||||||
|
#>
|
||||||
|
param([Parameter(Mandatory)][string]$UserName)
|
||||||
|
|
||||||
|
if (Get-LocalUser -Name $UserName -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Log "Local user '$UserName' already exists." ; return
|
||||||
|
}
|
||||||
|
$sec = Read-Host "Set password for new RDP user '$UserName'" -AsSecureString
|
||||||
|
New-LocalUser -Name $UserName -Password $sec -PasswordNeverExpires:$true `
|
||||||
|
-Description 'RdpCoexist dedicated RDP account' | Out-Null
|
||||||
|
Add-LocalGroupMember -Group 'Remote Desktop Users' -Member $UserName -ErrorAction SilentlyContinue
|
||||||
|
Write-Log "Local user '$UserName' created and added to Remote Desktop Users." 'Ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-RdpLocalUser {
|
||||||
|
param([Parameter(Mandatory)][string]$UserName)
|
||||||
|
if (Get-LocalUser -Name $UserName -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-LocalUser -Name $UserName
|
||||||
|
Write-Log "Local user '$UserName' removed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function New-RdpLocalUser, Remove-RdpLocalUser
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
|
function Get-ActiveSessionCount {
|
||||||
|
param([string]$Pattern = 'Active|使用中|アクティブ|활성')
|
||||||
|
$out = & qwinsta.exe 2>$null
|
||||||
|
if (-not $out) { return 0 }
|
||||||
|
return @($out | Select-String -Pattern $Pattern).Count
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-Coexist {
|
||||||
|
<#
|
||||||
|
COEXIST-OK requires an actual listener AND >=2 concurrent active sessions,
|
||||||
|
not merely the registry values. Registry-only checks lie.
|
||||||
|
#>
|
||||||
|
param([string]$ActiveSessionPattern = 'Active|使用中|アクティブ|활성')
|
||||||
|
|
||||||
|
$svc = (Get-Service TermService -ErrorAction SilentlyContinue).Status
|
||||||
|
$listen = @(Get-NetTCPConnection -LocalPort 3389 -State Listen -ErrorAction SilentlyContinue).Count -gt 0
|
||||||
|
$dll = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\TermService\Parameters' `
|
||||||
|
-Name ServiceDll -ErrorAction SilentlyContinue).ServiceDll
|
||||||
|
$active = Get-ActiveSessionCount -Pattern $ActiveSessionPattern
|
||||||
|
|
||||||
|
$verdict =
|
||||||
|
if ($listen -and $active -ge 2) { 'COEXIST-OK' }
|
||||||
|
elseif ($listen) { 'LISTENING-UNVERIFIED' }
|
||||||
|
else { 'FAIL' }
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
TermService = $svc
|
||||||
|
Listening = $listen
|
||||||
|
ServiceDll = $dll
|
||||||
|
ActiveSessions = $active
|
||||||
|
Verdict = $verdict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Test-Coexist, Get-ActiveSessionCount
|
||||||
Loading…
Reference in new issue