Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions registry/coder/modules/windows-rdp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.4.0"
agent_id = coder_agent.main.id
}
```
Expand All @@ -32,7 +32,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.4.0"
agent_id = coder_agent.main.id
}
```
Expand All @@ -43,7 +43,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.4.0"
agent_id = coder_agent.main.id
}
```
Expand All @@ -54,8 +54,25 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.3.0"
version = "1.4.0"
agent_id = coder_agent.main.id
devolutions_gateway_version = "2025.2.2" # Specify a specific version
}
```

### With RDP Keep-Alive

Automatically extend the workspace deadline while an RDP connection is active. This prevents workspace autostop during active remote desktop sessions:

```tf
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.4.0"
agent_id = coder_agent.main.id
keepalive = true
keepalive_interval = 30 # Check every 30 seconds (default)
}
```

When `keepalive` is enabled, a background script monitors port 3389 for established RDP connections. On detection, it calls the [Coder workspace extend API](https://coder.com/docs/reference/api/workspaces#extend-workspace-deadline-by-id) to bump the workspace deadline. When the RDP session disconnects, the normal autostop countdown resumes.
62 changes: 62 additions & 0 deletions registry/coder/modules/windows-rdp/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type TestVariables = Readonly<{
share?: string;
admin_username?: string;
admin_password?: string;
keepalive?: boolean;
keepalive_interval?: number;
}>;

function findWindowsRdpScript(state: TerraformState): string | null {
Expand All @@ -35,6 +37,29 @@ function findWindowsRdpScript(state: TerraformState): string | null {
return null;
}

function findKeepaliveScript(state: TerraformState): string | null {
for (const resource of state.resources) {
const isKeepaliveScriptResource =
resource.type === "coder_script" &&
resource.name === "windows-rdp-keepalive";

if (!isKeepaliveScriptResource) {
continue;
}

for (const instance of resource.instances) {
if (
instance.attributes.display_name === "windows-rdp-keepalive" &&
typeof instance.attributes.script === "string"
) {
return instance.attributes.script;
}
}
}

return null;
}

/**
* @todo It would be nice if we had a way to verify that the Devolutions root
* HTML file is modified to include the import for the patched Coder script,
Expand Down Expand Up @@ -128,4 +153,41 @@ describe("Web RDP", async () => {
expect(customResultsGroup.username).toBe(customAdminUsername);
expect(customResultsGroup.password).toBe(customAdminPassword);
});

it("Does not create keepalive script when keepalive is disabled (default)", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
});

const keepaliveScript = findKeepaliveScript(state);
expect(keepaliveScript).toBeNull();
});

it("Creates keepalive script when keepalive is enabled", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
keepalive: true,
});

const keepaliveScript = findKeepaliveScript(state);
expect(keepaliveScript).toBeString();
expect(keepaliveScript).toContain("Get-NetTCPConnection");
expect(keepaliveScript).toContain("-LocalPort 3389");
expect(keepaliveScript).toContain("/api/v2/workspaces/");
expect(keepaliveScript).toContain("/extend");
expect(keepaliveScript).toContain("CODER_AGENT_TOKEN");
});

it("Uses custom keepalive interval", async () => {
const customInterval = 120;
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
keepalive: true,
keepalive_interval: customInterval,
});

const keepaliveScript = findKeepaliveScript(state);
expect(keepaliveScript).toBeString();
expect(keepaliveScript).toContain(`$CheckInterval = ${customInterval}`);
});
});
36 changes: 36 additions & 0 deletions registry/coder/modules/windows-rdp/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ variable "devolutions_gateway_version" {
description = "Version of Devolutions Gateway to install. Use 'latest' for the most recent version, or specify a version like '2025.3.2'."
}

variable "keepalive" {
type = bool
default = false
description = "Enable automatic workspace activity reporting while an RDP connection is active. When enabled, the module monitors port 3389 for established connections and uses the Coder API to extend the workspace deadline, preventing autostop during active RDP sessions."
}

variable "keepalive_interval" {
type = number
default = 30
description = "Interval in seconds between RDP connection checks when keepalive is enabled."
validation {
condition = var.keepalive_interval >= 10 && var.keepalive_interval <= 3600
error_message = "keepalive_interval must be between 10 and 3600 seconds."
}
}

data "coder_workspace" "me" {
count = var.keepalive ? 1 : 0
}

resource "coder_script" "windows-rdp" {
agent_id = var.agent_id
display_name = "windows-rdp"
Expand Down Expand Up @@ -118,3 +138,19 @@ resource "coder_app" "rdp-docs" {
url = "https://coder.com/docs/user-guides/workspace-access/remote-desktops#rdp"
external = true
}

resource "coder_script" "windows-rdp-keepalive" {
count = var.keepalive ? 1 : 0
agent_id = var.agent_id
display_name = "windows-rdp-keepalive"
icon = "/icon/rdp.svg"

script = templatefile("${path.module}/rdp-keepalive.ps1.tftpl", {
keepalive_interval = var.keepalive_interval
coder_url = data.coder_workspace.me[0].access_url
workspace_id = data.coder_workspace.me[0].id
})

run_on_start = true
start_blocks_login = false
}
107 changes: 107 additions & 0 deletions registry/coder/modules/windows-rdp/main.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
run "basic_defaults" {
command = plan

variables {
agent_id = "test-agent-id"
}

assert {
condition = coder_script.windows-rdp.agent_id == "test-agent-id"
error_message = "Windows RDP script should use the provided agent_id"
}

assert {
condition = coder_app.windows-rdp.agent_id == "test-agent-id"
error_message = "Windows RDP app should use the provided agent_id"
}

assert {
condition = length(coder_script.windows-rdp-keepalive) == 0
error_message = "Keepalive script should not be created when keepalive is disabled (default)"
}
}

run "keepalive_disabled_by_default" {
command = plan

variables {
agent_id = "test-agent-id"
}

assert {
condition = var.keepalive == false
error_message = "keepalive should default to false"
}
}

run "keepalive_enabled" {
command = plan

variables {
agent_id = "test-agent-id"
keepalive = true
}

assert {
condition = length(coder_script.windows-rdp-keepalive) == 1
error_message = "Keepalive script should be created when keepalive is enabled"
}

assert {
condition = coder_script.windows-rdp-keepalive[0].display_name == "windows-rdp-keepalive"
error_message = "Keepalive script should have correct display name"
}

assert {
condition = coder_script.windows-rdp-keepalive[0].run_on_start == true
error_message = "Keepalive script should run on start"
}

assert {
condition = coder_script.windows-rdp-keepalive[0].start_blocks_login == false
error_message = "Keepalive script should not block login"
}
}

run "keepalive_default_interval" {
command = plan

variables {
agent_id = "test-agent-id"
keepalive = true
}

assert {
condition = var.keepalive_interval == 30
error_message = "Default keepalive interval should be 30 seconds"
}
}

run "keepalive_custom_interval" {
command = plan

variables {
agent_id = "test-agent-id"
keepalive = true
keepalive_interval = 120
}

assert {
condition = var.keepalive_interval == 120
error_message = "Custom keepalive interval should be accepted"
}
}

run "custom_devolutions_version" {
command = plan

variables {
agent_id = "test-agent-id"
devolutions_gateway_version = "2025.2.2"
}

assert {
condition = var.devolutions_gateway_version == "2025.2.2"
error_message = "Custom Devolutions Gateway version should be accepted"
}
}
78 changes: 78 additions & 0 deletions registry/coder/modules/windows-rdp/rdp-keepalive.ps1.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# RDP Keep-Alive Monitor for Coder Workspaces
# Detects active RDP connections on port 3389 and extends the workspace
# deadline via the Coder API to prevent autostop during active sessions.

$CheckInterval = ${keepalive_interval}
$CoderURL = "${coder_url}"
$WorkspaceID = "${workspace_id}"

# The Coder agent sets CODER_AGENT_TOKEN in the environment automatically.
$AgentToken = $env:CODER_AGENT_TOKEN

if (-not $AgentToken) {
Write-Host "[RDP Keep-Alive] WARNING: CODER_AGENT_TOKEN not found. Activity reporting will not work."
}

function Test-RDPConnection {
try {
$connections = Get-NetTCPConnection -LocalPort 3389 -State Established -ErrorAction SilentlyContinue
if ($connections) {
return ($connections | Measure-Object).Count
}
return 0
}
catch {
return 0
}
}

function Send-ActivityBump {
# Extend the workspace deadline by the template's activity bump duration.
# We set the deadline far enough into the future that the server will clamp
# it to the template-configured activity bump value.
$newDeadline = (Get-Date).AddHours(12).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$body = @{ deadline = $newDeadline } | ConvertTo-Json
$url = "$CoderURL/api/v2/workspaces/$WorkspaceID/extend"

try {
$headers = @{
"Content-Type" = "application/json"
"Coder-Session-Token" = $AgentToken
}
$response = Invoke-WebRequest -Uri $url -Method PUT -Headers $headers -Body $body -UseBasicParsing -ErrorAction Stop
return $true
}
catch {
Write-Host "[RDP Keep-Alive] Failed to extend deadline: $_"
return $false
}
}

Write-Host "[RDP Keep-Alive] Started monitoring RDP connections (interval: $($CheckInterval)s)"
$wasActive = $false

while ($true) {
$connectionCount = Test-RDPConnection

if ($connectionCount -gt 0) {
if (-not $wasActive) {
Write-Host "[RDP Keep-Alive] RDP connection(s) detected ($connectionCount active)"
}
$wasActive = $true

if ($AgentToken) {
$success = Send-ActivityBump
if ($success) {
Write-Host "[RDP Keep-Alive] Workspace deadline extended ($connectionCount active connection(s))"
}
}
}
else {
if ($wasActive) {
Write-Host "[RDP Keep-Alive] RDP connection(s) ended"
}
$wasActive = $false
}

Start-Sleep -Seconds $CheckInterval
}