From 698dd2086a7facd3458eedffa466f5e8fd0508c3 Mon Sep 17 00:00:00 2001 From: buildingvibes Date: Sun, 8 Feb 2026 22:49:28 -0800 Subject: [PATCH] feat(windows-rdp): add RDP keep-alive to extend workspace deadline Add opt-in keepalive feature that monitors port 3389 for established RDP connections and calls the Coder workspace extend API to prevent autostop during active remote desktop sessions. The keepalive script runs as a background coder_script that: - Uses Get-NetTCPConnection to detect active RDP connections - Calls PUT /api/v2/workspaces/{id}/extend with CODER_AGENT_TOKEN - Checks at a configurable interval (default 30s) - Only reports activity when connections are present New variables: keepalive (bool, default false), keepalive_interval (number, default 30). Backward compatible - no changes when disabled. Closes coder/registry#200 --- registry/coder/modules/windows-rdp/README.md | 25 +++- .../coder/modules/windows-rdp/main.test.ts | 62 ++++++++++ registry/coder/modules/windows-rdp/main.tf | 36 ++++++ .../coder/modules/windows-rdp/main.tftest.hcl | 107 ++++++++++++++++++ .../windows-rdp/rdp-keepalive.ps1.tftpl | 78 +++++++++++++ 5 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 registry/coder/modules/windows-rdp/main.tftest.hcl create mode 100644 registry/coder/modules/windows-rdp/rdp-keepalive.ps1.tftpl diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index 111e8d7fd..e9ea45476 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -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 } ``` @@ -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 } ``` @@ -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 } ``` @@ -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. diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index 80c09fd0d..0c4e1b893 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -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 { @@ -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, @@ -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(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(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(import.meta.dir, { + agent_id: "foo", + keepalive: true, + keepalive_interval: customInterval, + }); + + const keepaliveScript = findKeepaliveScript(state); + expect(keepaliveScript).toBeString(); + expect(keepaliveScript).toContain(`$CheckInterval = ${customInterval}`); + }); }); diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index 3c83d195b..f2c50ca12 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -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" @@ -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 +} diff --git a/registry/coder/modules/windows-rdp/main.tftest.hcl b/registry/coder/modules/windows-rdp/main.tftest.hcl new file mode 100644 index 000000000..986cb470b --- /dev/null +++ b/registry/coder/modules/windows-rdp/main.tftest.hcl @@ -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" + } +} diff --git a/registry/coder/modules/windows-rdp/rdp-keepalive.ps1.tftpl b/registry/coder/modules/windows-rdp/rdp-keepalive.ps1.tftpl new file mode 100644 index 000000000..8b7e2b7dc --- /dev/null +++ b/registry/coder/modules/windows-rdp/rdp-keepalive.ps1.tftpl @@ -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 +}