The Guide

Everything you need to deploy DrainCtl, configure it for your environment, and start getting real-time drain mode visibility across your RDSH farm.

1. Installation

DrainCtl ships as a single MSI. Deploy it however you deploy software — your RMM, SCCM, Intune, GPO, or just double-click it.

Interactive install

Double-click the MSI to launch the interactive installer. It walks you through choosing an install mode (dashboard, registration, or standalone) and setting notification URLs, dashboard address, grace period, and other options.

Silent install

msiexec /i LISSTech.DrainCtl.msi /qn

Unattended MSI properties

Pass these properties on the command line (or in your RMM/SCCM transform) to pre-configure the installation:

Property Values / Example Description
INSTALL_MODE dashboard | registration | standalone Sets the operational mode. dashboard enables the web UI, registration registers with an existing dashboard, standalone runs independently.
WEBHOOK_URL https://hooks.example.com/drain Adds a webhook notification target
NTFY_URL https://ntfy.sh/drainctl Adds an ntfy notification target
DASHBOARD_URL https://dash:49470 Dashboard URL to register with (used with registration mode)
DASHBOARD_PORT 49470 Port for the dashboard listener (used with dashboard mode)
DASHBOARD_GROUP Domain Admins AD group authorized for dashboard access
GRACE_PERIOD 120 Grace period in minutes before drain mode becomes an alert

Example — register with a dashboard, no UI:

msiexec /i LISSTech.DrainCtl.msi /qn INSTALL_MODE=registration DASHBOARD_URL=https://dash:49470

What the MSI does

💡 No reboot required. The service starts immediately. Open a new terminal and drainctl check will work right away.

PowerShell module only (no service)

If you just want the PowerShell cmdlets without the Windows Service, install directly from PSGallery:

Install-Module -Name LISSTech.DrainCtl -Scope AllUsers

This gives you Get-RDSHDrainMode, Test-RDSHDrainMode, and friends — but without the service, queries go directly to the registry instead of the named pipe (slightly slower, no persistent audit store).

📦 PSGallery vs MSI The MSI is the full package: service + CLI + PowerShell module + Event Log + notifications. PSGallery is the module alone — great for quick checks, scripting, or machines where you don't need continuous monitoring.

Silent uninstall

msiexec /x LISSTech.DrainCtl.msi /qn

This stops the service, removes all files, cleans up PATH, and deregisters the Event Log source. Your audit data in ProgramData is preserved (delete it manually if you want a clean slate).

2. First Run

After installation, verify everything is working:

# Is the service running?
Get-Service DrainCtl

# Quick status check (instant — talks to the service via named pipe)
drainctl check

# PowerShell way
Get-RDSHDrainMode | Format-List

You should see status=Healthy and connections_allowed=true. If drain mode is active on this server, you'll see the current state and how long it's been active.

⚡ Instant response? If drainctl check responds in under 100ms, it's talking to the service over the named pipe. If it takes a second or two, the service isn't running and it's falling back to a direct registry read. Check Get-Service DrainCtl.

3. Configuration

All settings live in a single JSON file and hot-reload automatically when you save it. No service restart needed.

%ProgramData%\LISS Technologies\LISSTech DrainCtl\config.json

Full config.json structure

{
  "grace_period": 60,
  "retention_days": 90,
  "poll_interval": 300,
  "audit_path": "C:\\ProgramData\\LISS Technologies\\LISSTech DrainCtl\\audit.jsonl",
  "notifications": [
    {
      "type": "webhook",
      "url": "https://hooks.example.com/drainctl",
      "triggers": ["drain_on", "drain_off", "alert", "healthy"],
      "repeat_minutes": 30
    }
  ],
  "dashboard": {
    "enabled": false,
    "port": 49470,
    "group": "Domain Admins"
  },
  "session_warning_threshold": 80
}
Key Default What it does
grace_period 60 Minutes drain mode must be active before it becomes an alert. During the grace period, the status is "Grace" and exit code is 0.
retention_days 90 Days to keep audit records. Maximum 365. Older records are pruned daily.
poll_interval 300 Seconds between safety-net polls. The service also uses real-time registry notifications, so this is a fallback.
audit_path ProgramData path Full path to the JSONL audit trail file.
notifications [] Array of notification targets (see Notifications).
dashboard see above Dashboard settings (see Dashboard).
session_warning_threshold 80 Session utilization percentage that triggers a session_warning notification.

Edit the file with any text editor:

%ProgramData%\LISS Technologies\LISSTech DrainCtl\config.json

The service picks up changes within seconds (it watches config.json with a file system watcher). No restart needed.

🔄 Migrating from a previous version? On first run, DrainCtl automatically migrates your settings from the registry (HKLM\SYSTEM\CurrentControlSet\Services\DrainCtl\Parameters) into config.json. Your old registry values are left in place but are no longer read. No manual migration needed.

4. Audit Setup (Change Attribution)

DrainCtl can tell you who changed drain mode — but it needs Windows to record that information first. Run this once (as admin):

drainctl audit-setup
# or
Install-RDSHDrainAudit

This does two things:

  1. Enables the Registry audit subcategory via auditpol
  2. Sets a SACL on the Terminal Server registry key so that Windows writes Event ID 4657 whenever TSServerDrainMode is modified
⚠️ Domain-joined machines On domain-joined servers, Group Policy refresh (~90 min) can overwrite local auditpol settings. For persistence, configure the equivalent GPO:

Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration > Object Access > Audit Registry > Success

Without audit setup, DrainCtl still detects changes instantly — it just can't tell you who made them. The changed_by field will be empty.

5. CLI Usage

Check current state

# Plain text (default — great for terminal and RMM scripts)
drainctl check

# JSON (great for parsing)
drainctl check --format json

# Table
drainctl check --format table

# Quiet mode (only final status line)
drainctl --quiet check

View audit history

# Last 50 records (table format)
drainctl history

# Only state transitions
drainctl history --changes-only

# Last 10 records as JSON
drainctl history --limit 10 --format json

# Time-bounded queries (RFC 3339 timestamps)
drainctl history --since 2025-01-01T00:00:00Z
drainctl history --since 2025-06-01T00:00:00Z --until 2025-06-30T23:59:59Z

Exit codes

Code Meaning When
0 Healthy / Grace Connections allowed, or drain mode active but within grace period
1 Alert Drain mode active beyond grace period — new connections are blocked
2 Error Registry unreadable or other failure

Output formats

--format plain (default for check), table (default for history), csv, json.

The JSON output from drainctl check --format json includes session data — active sessions, total capacity, and utilization percentage — useful for building custom dashboards or feeding into monitoring systems.

Notification management

# View notification status
drainctl notify status

# Set a webhook target
drainctl notify set-webhook https://hooks.example.com/drainctl

# Set an ntfy target
drainctl notify set-ntfy https://ntfy.sh/my-rdsh-alerts
💡 Multi-target management. To add, remove, or configure individual notification targets with granular triggers and repeat intervals, edit config.json directly or use the dashboard UI. The CLI set-webhook and set-ntfy commands are convenience shortcuts for single-target setups.

Interactive configuration wizard

Run drainctl configure without flags to step through every setting interactively. The current value is shown in brackets — press Enter to keep it.

drainctl configure

When called with flags (e.g., by the MSI installer during silent install), it applies the values directly and writes config.json without prompting:

drainctl configure --grace-period 120 --mode dashboard --dashboard-port 49470

6. PowerShell Module

The module is auto-registered by the MSI. It works on both PowerShell 5.1 and 7+.

# Rich status object
Get-RDSHDrainMode

# Boolean check — perfect for scripts and alerts
if (Test-RDSHDrainMode) { "All good" } else { "ALERT: drain mode active!" }

# Audit history
Get-RDSHDrainHistory -Limit 20

# Only transitions (who changed what, when)
Get-RDSHDrainHistory -ChangesOnly | Format-Table Timestamp, DrainMode, ChangedBy

# Pipeline magic
Get-RDSHDrainHistory -ChangesOnly |
    Where-Object { $_.DrainMode -ne "ALLOW_ALL_CONNECTIONS" } |
    Select-Object Timestamp, ChangedBy, DrainMode

All cmdlets

Cmdlet Returns Description
Get-RDSHDrainMode PSObject Full drain mode state with audit data
Test-RDSHDrainMode bool $true if connections allowed
Get-RDSHDrainHistory PSObject[] Audit trail records
Install-RDSHDrainAudit One-time audit configuration
Get-RDSHDrainNotification PSObject Current notification config
Set-RDSHDrainNotification Update notification settings
Test-RDSHDrainNotification Send test notification
Get-RDSHDrainNotificationTarget PSObject[] Lists all notification targets with type, URL, triggers, repeat interval
Add-RDSHDrainNotificationTarget Adds a notification target (-Type, -URL, -Triggers, -RepeatMinutes)
Remove-RDSHDrainNotificationTarget Removes a notification target by -URL
Enable-RDSHDrainDashboard Enable the multi-server dashboard on this server
Disable-RDSHDrainDashboard Disable the dashboard on this server
Install-RDSHDrainCertificate Install a custom TLS certificate for the dashboard

7. Notifications

Get pushed when something happens. DrainCtl supports multiple notification targets — any combination of webhooks (Slack, Teams, PagerDuty, custom HTTP endpoints) and ntfy.sh topics. Each target has its own triggers and repeat interval.

Setup

# Set a webhook target (Slack, Teams, PagerDuty, custom endpoint, etc.)
drainctl notify set-webhook https://hooks.slack.com/services/T.../B.../xxx

# Set an ntfy target (push notifications on your phone!)
drainctl notify set-ntfy https://ntfy.sh/my-rdsh-alerts

# View notification status
drainctl notify status

# Send a test to make sure it works
drainctl notify test

To manage multiple targets with per-target triggers and repeat intervals, edit config.json directly or use the dashboard UI's notification settings drawer. See the Configuration section for the full config.json structure.

PowerShell way

Set-RDSHDrainNotification -WebhookURL "https://hooks.example.com/drainctl"
Set-RDSHDrainNotification -NtfyURL "https://ntfy.sh/my-alerts"
Test-RDSHDrainNotification

# See current config
Get-RDSHDrainNotification

PowerShell target management

# List all targets
Get-RDSHDrainNotificationTarget

# Add a webhook for alerts only
Add-RDSHDrainNotificationTarget -Type webhook -URL 'https://hooks.example.com/drain' -Triggers alert -RepeatMinutes 15

# Add ntfy for all events
Add-RDSHDrainNotificationTarget -Type ntfy -URL 'https://ntfy.sh/drainctl'

# Remove a target
Remove-RDSHDrainNotificationTarget -URL 'https://hooks.example.com/drain'

Granular triggers

Each notification target can subscribe to specific triggers. Assign them with --triggers when adding a target, or edit config.json directly.

Trigger Description
drain_on Drain mode activated
drain_off Drain mode deactivated
grace_entered Entered grace period
alert Grace period exceeded
healthy Returned to healthy
session_warning Session utilization threshold exceeded (default 80%)

Each target also has a repeat_minutes setting — set it to re-send alerts periodically while the condition persists (0 = notify once).

Webhook payload

{
  "event": "transition",
  "host": "MDS-LDC1-RDS5",
  "drain_mode": "ALLOW_RECONNECTIONS_PREVENT_NEW_LOGONS",
  "previous_mode": "ALLOW_ALL_CONNECTIONS",
  "status": "Grace",
  "message": "Drain mode active, within grace period (45m remaining).",
  "changed_by": "MDS\\lissadmin",
  "state_duration_seconds": 900,
  "timestamp": "2026-04-03T14:30:00-04:00"
}

ntfy messages arrive with priority high for alerts (red notification on your phone) and default for transitions.

8. Session Tracking

DrainCtl monitors active RDS sessions via WTSEnumerateSessionsW and exposes utilization data alongside drain mode state.

Session data in CLI output

Session counts and utilization appear automatically in drainctl check --format json output, including active sessions, total capacity, and utilization percentage. This is useful for feeding into monitoring systems or custom dashboards.

Session utilization alerts

When session utilization exceeds the configured threshold, DrainCtl fires a session_warning notification. The default threshold is 80% — change it in config.json:

{
  "session_warning_threshold": 80
}

Add session_warning to a notification target's triggers to receive these alerts (see Notifications).

Dashboard session gauges

The multi-server dashboard displays per-server session gauges showing current utilization at a glance.

9. Multi-Server Dashboard

Managing multiple RDSH servers? The dashboard gives you a single, live view of drain mode state across your entire farm — with Windows Authentication so only the right people see it.

How it works

  1. One server runs the dashboard — enable it in config.json, the DrainCtl service serves an HTTP dashboard
  2. Other servers register — run drainctl register once, the agent starts reporting its state automatically
  3. Dashboard aggregates — live grid of all servers, color-coded by status, auto-refreshes every 30 seconds

Enable the dashboard (on one server)

# Enable the dashboard server (writes to config.json)
drainctl dashboard enable --port 49470 --group "Domain Admins"

# Or with a custom AD group
drainctl dashboard enable --port 49470 --group "RDS Admins"

You can also edit config.json directly:

{
  "dashboard": {
    "enabled": true,
    "port": 49470,
    "group": "Domain Admins"
  }
}

Changes are hot-reloaded — no service restart needed.

Register servers

On each RDSH server you want to monitor, run this once:

# Register with explicit URL
drainctl register https://dashboard-server:49470

# Or auto-discover via DNS SRV record
drainctl register --auto

This does two things:

  1. Writes dashboard.url to the local config.json (so the service starts reporting automatically on startup)
  2. Sends a registration request to the dashboard (so it knows about this server)

Auto-discovery (DNS SRV)

Instead of configuring dashboard.url on every agent, create a DNS SRV record and agents will find the dashboard automatically. The service checks for _drainctl._tcp.<domain> on startup when no URL is configured.

# PowerShell — create the SRV record in AD-integrated DNS
Add-DnsServerResourceRecord -ZoneName "contoso.com" `
  -Name "_drainctl._tcp" -Srv `
  -DomainName "dashboard.contoso.com" `
  -Port 49470 -Priority 0 -Weight 0

# Verify
Resolve-DnsName -Name "_drainctl._tcp.contoso.com" -Type SRV

Once the SRV record exists, agents discover the dashboard without any per-machine config. Just install the MSI and the service registers itself.

💡 Self-maintaining. After registration, the service reports its state on every check cycle. No cron jobs, no scripts, no manual syncing. If the dashboard server is down, reports are silently skipped and resume when it's back. With SRV discovery, even registration is automatic — install the MSI and you're done.

View the dashboard

# Open in your browser
drainctl dashboard

# Or navigate directly
https://dashboard-server:49470

The dashboard shows a live grid with each server's hostname, drain mode, status, state duration, who last changed it, and when it was last seen. Color-coded: green for healthy, amber for grace, red for alert, gray for offline. Each server card includes a session gauge showing current utilization, and the dashboard features uPlot charts for historical state and session trends.

The notification settings drawer supports multi-target editing — add and remove individual notification targets, each with its own triggers and repeat interval, directly from the dashboard UI.

Manage servers

# List all registered servers
drainctl dashboard list-servers

# Remove a decommissioned server
drainctl dashboard remove-server RDSH-OLD

Authentication

The dashboard uses Windows Authentication (Kerberos/SPNEGO) via SSPI. No passwords, no tokens, no configuration:

HTTPS & TLS Certificates

The dashboard always runs over HTTPS. On first start, the service auto-generates a self-signed TLS certificate (ECDSA P-256, valid 1 year). The certificate and private key are stored in the DrainCtl data directory:

%ProgramData%\LISS Technologies\LISSTech DrainCtl\dashboard-tls.crt   # world-readable
%ProgramData%\LISS Technologies\LISSTech DrainCtl\dashboard-tls.key   # SYSTEM + Admins only

The private key is ACL-restricted to SYSTEM and Administrators. The certificate auto-renews 24 hours before expiry.

Certificate fingerprint

To view the dashboard's current TLS certificate fingerprint:

drainctl dashboard fingerprint

Certificate pinning (optional)

Certificate pinning lets agents verify they're talking to the real dashboard, not an impersonator. Pinning is opt-in — without it, agents use trust-on-first-use (TOFU) for TLS, which is fine for most environments.

Auto-pin on registration: Pass --pin when registering to automatically capture and save the dashboard's certificate fingerprint:

# Register and auto-pin the dashboard's TLS certificate
drainctl register --auto --pin
drainctl register https://dashboard:49470 --pin

The fingerprint is saved to dashboard.tls_fingerprint in the agent's config.json. All subsequent reports will verify the dashboard's certificate matches.

Auto-pin via config (for service auto-registration): Set "auto_pin": true in config.json and the service will capture the fingerprint on its next registration:

{
  "dashboard": {
    "url": "https://dashboard:49470",
    "auto_pin": true
  }
}

MSI deployment: Pass AUTO_PIN=1 as an MSI property:

msiexec /i LISSTech.DrainCtl.msi /qn INSTALL_MODE=registration DASHBOARD_URL=https://dash:49470 AUTO_PIN=1

Manual pinning: If you prefer to set the fingerprint yourself (e.g., distributed via GPO), run drainctl dashboard fingerprint on the dashboard server and set dashboard.tls_fingerprint in each agent's config. A manually-set fingerprint is never overwritten by auto-pin.

⚠️ Certificate renewal and pinning. The auto-generated certificate is valid for 1 year and renews automatically. When it renews, the fingerprint changes. Agents with the old fingerprint pinned will silently fail to report until they re-register. To re-pin after renewal, run drainctl register --auto --pin on each agent, or set "auto_pin": true in config and restart the service. Without pinning enabled, cert renewal is seamless.

Bring your own certificate

To use a CA-issued or internal PKI certificate instead of the auto-generated self-signed one:

# CLI
drainctl dashboard install-cert C:\certs\dashboard.pem C:\certs\dashboard-key.pem

# PowerShell
Install-RDSHDrainCertificate -CertPath C:\certs\dashboard.pem -KeyPath C:\certs\dashboard-key.pem

This copies the PEM files into the DrainCtl data directory and updates config.json with the paths. Restart the service to use the new certificate.

Both files must be PEM-encoded. The service loads them on startup and skips auto-generation when both are set. If the custom cert is invalid or expired, the service falls back to HTTP with a warning in the event log.

You can also set the paths directly in config.json:

{
  "dashboard": {
    "tls_cert": "C:\\certs\\dashboard.pem",
    "tls_key": "C:\\certs\\dashboard-key.pem"
  }
}

Dashboard configuration (config.json)

Key Default Description
dashboard.enabled false Enable the HTTPS dashboard (set to true)
dashboard.port 49470 Port the dashboard listens on
dashboard.group Domain Admins AD group authorized to view the dashboard
dashboard.url empty Agent-side: URL of the dashboard to report to (set by drainctl register)
dashboard.tls_cert empty Path to PEM certificate file. If empty, a self-signed cert is auto-generated.
dashboard.tls_key empty Path to PEM private key file. Required when tls_cert is set.
dashboard.tls_fingerprint empty SHA-256 certificate fingerprint for agent-side pinning. Set manually or via --pin.
dashboard.auto_pin false Auto-capture dashboard cert fingerprint on service registration.

Centralized notification config

When an agent has a dashboard.url configured, it automatically pulls notification targets, grace period, and session warning threshold from the dashboard server. This means you only need to configure notifications once — on the dashboard — and all agents inherit the settings.

How it works:

No dashboard URL? Agents without dashboard.url use their local config.json notification targets as before — nothing changes for standalone deployments.

Tip: For farms with dozens of servers, configure notifications entirely in the dashboard UI and leave notifications empty in each agent's config.json. The dashboard becomes the single source of truth.
⚠️ Firewall The dashboard listens on the configured port. If you're running Windows Firewall, allow inbound TCP on that port for the agent servers and admin workstations.

10. RMM Integration

DrainCtl is designed to integrate with any RMM platform that can run a script and check an exit code.

How it works

  1. Your RMM runs a monitoring script at its polling interval
  2. The script executes drainctl check
  3. DrainCtl connects to the service via named pipe — instant response
  4. The RMM captures stdout (structured key=value lines) and the exit code
  5. Threshold: exit code 0 = Normal, 1 = Alert, 2 = Error
🎯 Pro tip: Even if your RMM only polls every 15 minutes, the service detected the change in real time. The audit trail has the exact timestamp and attribution. Your RMM is just sampling the service's state.

Minimal monitoring script

drainctl check --quiet
exit $LASTEXITCODE

That's it. --quiet suppresses intermediate log lines — only the final status line is emitted. Works with any RMM that supports PowerShell or CMD scripts with exit code thresholds.

JSON output for advanced integrations

# Parse JSON output for custom dashboards or APIs
$result = drainctl check --format json | ConvertFrom-Json
if ($result.exit_code -ne 0) {
    # Send to your ticketing system, dashboard, etc.
}

11. Service Management

The MSI handles service installation. These commands are for manual management or troubleshooting.

# Check service status (CLI — shows Running / Stopped)
drainctl service status

# Check service status (PowerShell — richer output)
Get-Service DrainCtl

# Stop / Start / Restart
Stop-Service DrainCtl
Start-Service DrainCtl
Restart-Service DrainCtl

# Manual install (if not using MSI)
drainctl service install
drainctl service start

# Manual uninstall
drainctl service stop
drainctl service uninstall

What the service does

Event Log

Check Application log, source DrainCtl:

ID Level What happened
1000 Info Service started
1001 Info Service stopped
1002 Info Check: healthy
1004 Info State transition detected
2000 Warning Drain mode in grace period
3000 Error Drain mode alert (grace exceeded)

12. Troubleshooting

"changed_by" is empty

Run drainctl audit-setup as admin. On domain-joined machines, also configure the GPO (see Audit Setup).

Service won't start

Check the Application Event Log for DrainCtl errors (Event ID 3002). Common causes:

CLI is slow (1-2 seconds instead of instant)

The service isn't running. drainctl check falls back to direct registry read + file-based audit. Start the service: Start-Service DrainCtl.

Notifications not arriving

  1. drainctl notify status — verify URLs are set
  2. drainctl notify test — sends a test message
  3. Check the Application Event Log for "webhook notification failed" or "ntfy notification failed" warnings
  4. Verify network connectivity from the RDSH server to the webhook/ntfy endpoint

Dashboard shows "Offline" for a registered server

The server's DrainCtl service isn't reporting. Check:

Dashboard returns 401 or "Access Denied"

Windows Authentication (Kerberos) issue:

Dashboard returns 403 on agent report

The reporting server isn't registered. Run drainctl register https://dashboard:49470 on that server.

Need help?

Open an issue on GitHub or contact LISS Technologies support.