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