From 8fff96ca7f97c2a5531876f7aafc88a5d69a60c2 Mon Sep 17 00:00:00 2001 From: Rhys Williams Date: Thu, 23 Oct 2025 14:41:39 +0100 Subject: [PATCH 01/14] Update devolutions patch --- .../modules/windows-rdp/devolutions-patch.js | 770 +++++++++--------- 1 file changed, 380 insertions(+), 390 deletions(-) diff --git a/registry/coder/modules/windows-rdp/devolutions-patch.js b/registry/coder/modules/windows-rdp/devolutions-patch.js index ef7364523..14ec7e8de 100644 --- a/registry/coder/modules/windows-rdp/devolutions-patch.js +++ b/registry/coder/modules/windows-rdp/devolutions-patch.js @@ -1,425 +1,415 @@ -// @ts-check -/** - * @file Defines the custom logic for patching in UI changes/behavior into the - * base Devolutions Gateway Angular app. - * - * Defined as a JS file to remove the need to have a separate compilation step. - * It is highly recommended that you work on this file from within VS Code so - * that you can take advantage of the @ts-check directive and get some type- - * checking still. - * - * Other notes about the weird ways this file is set up: - * - A lot of the HTML selectors in this file will look nonstandard. This is - * because they are actually custom Angular components. - * - It is strongly advised that you avoid template literals that use the - * placeholder syntax via the dollar sign. The Terraform file is treating this - * as a template file, and because it also uses a similar syntax, there's a - * risk that some values will trigger false positives. If a template literal - * must be used, be sure to use a double dollar sign to escape things. - * - All the CSS should be written via custom style tags and the !important - * directive (as much as that is a bad idea most of the time). We do not - * control the Angular app, so we have to modify things from afar to ensure - * that as Angular's internal state changes, it doesn't modify its HTML nodes - * in a way that causes our custom styles to get wiped away. - * - * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry - * @typedef {Readonly>} FormFieldEntries - */ - -/** - * The communication protocol to set Devolutions to. - */ -const PROTOCOL = "RDP"; - -/** - * The hostname to use with Devolutions. - */ -const HOSTNAME = "localhost"; - -/** - * How often to poll the screen for the main Devolutions form. - */ -const SCREEN_POLL_INTERVAL_MS = 500; - -/** - * The fields in the Devolutions sign-in form that should be populated with - * values from the Coder workspace. - * - * All properties should be defined as placeholder templates in the form - * VALUE_NAME. The Coder module, when spun up, should then run some logic to - * replace the template slots with actual values. These values should never - * change from within JavaScript itself. - * - * @satisfies {FormFieldEntries} - */ -const formFieldEntries = { - /** @readonly */ - username: { +(function () { + /** + * The communication protocol to set Devolutions to. + */ + const PROTOCOL = "RDP"; + + /** + * The hostname to use with Devolutions. + */ + const HOSTNAME = "localhost"; + + /** + * How often to poll the screen for the main Devolutions form. + */ + const POLL_INTERVAL_MS = 500; + + /** + * The fields in the Devolutions sign-in form that should be populated with + * values from the Coder workspace. + * + * All properties should be defined as placeholder templates in the form + * VALUE_NAME. The Coder module, when spun up, should then run some logic to + * replace the template slots with actual values. These values should never + * change from within JavaScript itself. + * + * @satisfies {FormFieldEntries} + */ + const formFieldEntries = { /** @readonly */ - querySelector: "web-client-username-control input", - - /** @readonly */ - value: "${CODER_USERNAME}", - }, - - /** @readonly */ - password: { - /** @readonly */ - querySelector: "web-client-password-control input", - - /** @readonly */ - value: "${CODER_PASSWORD}", - }, -}; - -/** - * Handles typing in the values for the input form. All values are written - * immediately, even though that would be physically impossible with a real - * keyboard. - * - * Note: this code will never break, but you might get warnings in the console - * from Angular about unexpected value changes. Angular patches over a lot of - * the built-in browser APIs to support its component change detection system. - * As part of that, it has validations for checking whether an input it - * previously had control over changed without it doing anything. - * - * But the only way to simulate a keyboard input is by setting the input's - * .value property, and then firing an input event. So basically, the inner - * value will change, which Angular won't be happy about, but then the input - * event will fire and sync everything back together. - * - * @param {HTMLInputElement} inputField - * @param {string} inputText - * @returns {Promise} - */ -function setInputValue(inputField, inputText) { - return new Promise((resolve, reject) => { - // Adding timeout for input event, even though we'll be dispatching it - // immediately, just in the off chance that something in the Angular app - // intercepts it or stops it from propagating properly - const timeoutId = window.setTimeout(() => { - reject(new Error("Input event did not get processed correctly in time.")); - }, 3_000); - - const handleSuccessfulDispatch = () => { - window.clearTimeout(timeoutId); - inputField.removeEventListener("input", handleSuccessfulDispatch); - resolve(); - }; + username: { + /** @readonly */ + querySelector: "web-client-username-control input", + /** @readonly */ + value: "${CODER_USERNAME}", + }, + /** @readonly */ + password: { + /** @readonly */ + querySelector: "web-client-password-control input", + /** @readonly */ + value: "${CODER_PASSWORD}", + }, + }; - inputField.addEventListener("input", handleSuccessfulDispatch); + /** + * This ensures that the Devolutions login form (which by default, always shows + * up on screen when the app first launches) stays visually hidden from the user + * when they open Devolutions via the Coder module. + * + * The form will still be filled out automatically and submitted in the + * background via the rest of the logic in this file, so this function is mainly + * to help avoid screen flickering and make the overall experience feel a little + * more polished (even though it's just one giant hack). + * + * @returns {void} + */ + function hideFormForInitialSubmission() { + const styleId = "coder-patch--styles-initial-submission"; + const cssOpacityVariableName = "--coder-opacity-multiplier"; + + /** @type {HTMLStyleElement | null} */ + // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation + let styleContainer = document.querySelector("#" + styleId); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` + /* + Have to use opacity instead of visibility, because the element still + needs to be interactive via the script so that it can be auto-filled. + */ + :root { + /* + Can be 0 or 1. Start off invisible to avoid risks of UI flickering, + but the rest of the function should be in charge of making the form + container visible again if something goes wrong during setup. - // Code assumes that Angular will have an event handler in place to handle - // the new event - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); + Double dollar sign needed to avoid Terraform script false positives + */ + $${cssOpacityVariableName}: 0; + } - inputField.value = inputText; - inputField.dispatchEvent(inputEvent); - }); -} - -/** - * Takes a Devolutions remote session form, auto-fills it with data, and then - * submits it. - * - * The logic here is more convoluted than it should be for two main reasons: - * 1. Devolutions' HTML markup has errors. There are labels, but they aren't - * bound to the inputs they're supposed to describe. This means no easy hooks - * for selecting the elements, unfortunately. - * 2. Trying to modify the .value properties on some of the inputs doesn't - * work. Probably some combo of Angular data-binding and some inputs having - * the readonly attribute. Have to simulate user input to get around this. - * - * @param {HTMLFormElement} myForm - * @returns {Promise} - */ -async function autoSubmitForm(myForm) { - const setProtocolValue = () => { - /** @type {HTMLDivElement | null} */ - const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); - if (protocolDropdownTrigger === null) { - throw new Error("No clickable trigger for setting protocol value"); + /* + web-client-form is the container for the main session form, while + the div is for the dropdown that is used for selecting the protocol. + The dropdown is not inside of the form for CSS styling reasons, so we + need to select both. + */ + web-client-form, + body > div.p-overlay { + /* + Double dollar sign needed to avoid Terraform script false positives + */ + opacity: calc(100% * var($${cssOpacityVariableName})) !important; + } + `; + + document.head.appendChild(styleContainer); } - protocolDropdownTrigger.click(); - - // Can't use form as container for querying the list of dropdown options, - // because the elements don't actually exist inside the form. They're placed - // in the top level of the HTML doc, and repositioned to make it look like - // they're part of the form. Avoids CSS stacking context issues, maybe? - /** @type {HTMLLIElement | null} */ - const protocolOption = document.querySelector( - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li', - ); - - if (protocolOption === null) { - throw new Error( - "Unable to find protocol option on screen that matches desired protocol", - ); + // The root node being undefined should be physically impossible (if it's + // undefined, the browser itself is busted), but we need to do a type check + // here so that the rest of the function doesn't need to do type checks over + // and over. + const rootNode = document.querySelector(":root"); + if (!(rootNode instanceof HTMLHtmlElement)) { + // Remove the container entirely because if the browser is busted, who knows + // if the CSS variables can be applied correctly. Better to have something + // be a bit more ugly/painful to use, than have it be impossible to use + styleContainer.remove(); + return; } - protocolOption.click(); - }; + // It's safe to make the form visible preemptively because Devolutions + // outputs the Windows view through an HTML canvas that it overlays on top + // of the rest of the app. Even if the form isn't hidden at the style level, + // it will still be covered up. + const restoreOpacity = () => { + rootNode.style.setProperty(cssOpacityVariableName, "1"); + }; - const setHostname = () => { - /** @type {HTMLInputElement | null} */ - const hostnameInput = myForm.querySelector("p-autocomplete#hostname input"); + // If this file gets more complicated, it might make sense to set up the + // timeout and event listener so that if one triggers, it cancels the other, + // but having restoreOpacity run more than once is a no-op for right now. + // Not a big deal if these don't get cleaned up. - if (hostnameInput === null) { - throw new Error("Unable to find field for adding hostname"); - } + // Have the form automatically reappear no matter what, so that if something + // does break, the user isn't left out to dry + window.setTimeout(restoreOpacity, 5_000); - return setInputValue(hostnameInput, HOSTNAME); - }; + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + form?.addEventListener( + "submit", + () => { + // Not restoring opacity right away just to give the HTML canvas a little + // bit of time to get spun up and cover up the main form + window.setTimeout(restoreOpacity, 1_000); + }, + { once: true }, + ); + } - const setCoderFormFieldValues = async () => { - // The RDP form will not appear on screen unless the dropdown is set to use - // the RDP protocol - const rdpSubsection = myForm.querySelector("rdp-form"); - if (rdpSubsection === null) { - throw new Error( - "Unable to find RDP subsection. Is the value of the protocol set to RDP?", - ); + /** + * Sets up custom styles for hiding default Devolutions elements that Coder + * users shouldn't need to care about. + * + * @returns {void} + */ + function setupAlwaysOnStyles() { + const styleId = "coder-patch--styles-always-on"; + // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation + const existingContainer = document.querySelector("#" + styleId); + if (existingContainer) { + return; } - for (const { value, querySelector } of Object.values(formFieldEntries)) { - /** @type {HTMLInputElement | null} */ - const input = document.querySelector(querySelector); + const styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` + /* app-menu corresponds to the sidebar of the default view. */ + app-menu { + display: none !important; + } - if (input === null) { - throw new Error( - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - 'Unable to element that matches query "' + querySelector + '"', - ); + /* app-net-scan corresponds to the auto-discovery feature. */ + app-net-scan { + display: none !important; } + `; - await setInputValue(input, value); - } - }; + document.head.appendChild(styleContainer); + } - const triggerSubmission = () => { - /** @type {HTMLButtonElement | null} */ - const submitButton = myForm.querySelector( - 'p-button[ng-reflect-type="submit"] button', - ); + /** + * Handles typing in the values for the input form. All values are written + * immediately, even though that would be physically impossible with a real + * keyboard. + * + * Note: this code will never break, but you might get warnings in the console + * from Angular about unexpected value changes. Angular patches over a lot of + * the built-in browser APIs to support its component change detection system. + * As part of that, it has validations for checking whether an input it + * previously had control over changed without it doing anything. + * + * But the only way to simulate a keyboard input is by setting the input's + * .value property, and then firing an input event. So basically, the inner + * value will change, which Angular won't be happy about, but then the input + * event will fire and sync everything back together. + * + * @param {HTMLInputElement} inputField + * @param {string} inputText + * @returns {Promise} + */ + function setInputValue(inputField, inputText) { + return new Promise((resolve, reject) => { + // Adding timeout for input event, even though we'll be dispatching it + // immediately, just in the off chance that something in the Angular app + // intercepts it or stops it from propagating properly + const timeoutId = window.setTimeout(() => { + reject(new Error("Input event did not get processed correctly in time.")); + }, 3_000); + + const handleSuccessfulDispatch = () => { + window.clearTimeout(timeoutId); + inputField.removeEventListener("input", handleSuccessfulDispatch); + resolve(); + }; + + inputField.addEventListener("input", handleSuccessfulDispatch); + + // Code assumes that Angular will have an event handler in place to handle + // the new event + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); + + inputField.value = inputText; + inputField.dispatchEvent(inputEvent); + }); + } - if (submitButton === null) { - throw new Error("Unable to find submission button"); - } + /** + * Takes a Devolutions remote session form, auto-fills it with data, and then + * submits it. + * + * The logic here is more convoluted than it should be for two main reasons: + * 1. Devolutions' HTML markup has errors. There are labels, but they aren't + * bound to the inputs they're supposed to describe. This means no easy hooks + * for selecting the elements, unfortunately. + * 2. Trying to modify the .value properties on some of the inputs doesn't + * work. Probably some combo of Angular data-binding and some inputs having + * the readonly attribute. Have to simulate user input to get around this. + * + * @param {HTMLFormElement} form + */ + async function fillForm(form) { + try { + log("Form detected. Starting auto-fill..."); + + // By default, RDP is selected. Leaving this here if needed + // in the future. + const protocolTrigger = form.querySelector('p-dropdown[id="protocol"]'); + if (protocolTrigger) { + protocolTrigger.click(); + const protocolOption = document.querySelector( + `li[aria-label="$${PROTOCOL}"]` + ); + if (protocolOption) { + protocolOption.click(); + log(`Protocol set to $${PROTOCOL}`); + } else { + log("Protocol option not found."); + } + } else { + log("Protocol dropdown trigger not found."); + } - if (submitButton.disabled) { - throw new Error( - "Unable to submit form because submit button is disabled. Are all fields filled out correctly?", - ); - } + const hostnameInput = form.querySelector("p-autocomplete#hostname input"); + if (hostnameInput) { + await setInputValue(hostnameInput, HOSTNAME); + log(`Hostname set to $${HOSTNAME}`); + } else { + log("Hostname input not found."); + } - submitButton.click(); - }; + for (const [key, { querySelector, value }] of Object.entries(formFieldEntries)) { + const input = document.querySelector(querySelector); + if (input) { + await setInputValue(input, value); + log(`Set $${key} to $${value}`); + } else { + log(`Input for $${key} not found with selector: $${querySelector}`); + } + } - setProtocolValue(); - await setHostname(); - await setCoderFormFieldValues(); - triggerSubmission(); -} - -/** - * Sets up logic for auto-populating the form data when the form appears on - * screen. - * - * @returns {void} - */ -function setupFormDetection() { - /** @type {HTMLFormElement | null} */ - let formValueFromLastMutation = null; - - /** @returns {void} */ - const onDynamicTabMutation = () => { - /** @type {HTMLFormElement | null} */ - const latestForm = document.querySelector("web-client-form > form"); - - // Only try to auto-fill if we went from having no form on screen to - // having a form on screen. That way, we don't accidentally override the - // form if the user is trying to customize values, and this essentially - // makes the script values function as default values - const mounted = formValueFromLastMutation === null && latestForm !== null; - if (mounted) { - autoSubmitForm(latestForm); + const submitButton = form.querySelector('p-button[class="p-element"] button'); + if (submitButton && !submitButton.disabled) { + submitButton.click(); + log("Form submitted."); + } else { + log("Submit button not found or disabled."); + } + } catch (err) { + console.error("[Devolutions Patch] Error during form fill:", err); } + } - formValueFromLastMutation = latestForm; - }; - - /** @type {number | undefined} */ - let pollingId = undefined; - - /** @returns {void} */ - const checkScreenForDynamicTab = () => { - const dynamicTab = document.querySelector("web-client-dynamic-tab"); + /** + * Attaches a click event listener to the "Close Session" button within the provided top bar element. + * When clicked, the listener triggers the window to close. + * Logs a message indicating whether the listener was successfully attached or if the button was not found. + * + * @param {HTMLElement} topBar - The container element that includes the "Close Session" button. + * @returns {void} + */ + function attachCloseListener(topBar) { + const buttons = topBar.querySelectorAll('button'); + + const closeButton = Array.from(buttons).find(button => { + const labelSpan = button.querySelector('.p-button-label'); + return labelSpan && labelSpan.textContent.trim() === 'Close Session'; + }); - // Keep polling until the main content container is on screen - if (dynamicTab === null) { - return; + if (closeButton) { + closeButton.parentElement.addEventListener('click', () => { + window.close(); + }); + log("Close listener attached."); + } else { + log("Close button not found in top bar."); } + } - window.clearInterval(pollingId); - - // Call the mutation callback manually, to ensure it runs at least once - onDynamicTabMutation(); - - // Having the mutation observer is kind of an extra safety net that isn't - // really expected to run that often. Most of the content in the dynamic - // tab is being rendered through Canvas, which won't trigger any mutations - // that the observer can detect - const dynamicTabObserver = new MutationObserver(onDynamicTabMutation); - dynamicTabObserver.observe(dynamicTab, { - subtree: true, - childList: true, + /** + * Sets the checked state of a checkbox based on its label text. + * Searches all components in the document and identifies the one + * whose label matches the provided `filterText`. Once found, it sets the checkbox + * to the specified `checked` state (true or false) and dispatches a change event + * to ensure any bound listeners (e.g., Angular change detection) are triggered. + * Logs the outcome of the operation for debugging or audit purposes. + * + * @param {string} filterText - The exact label text of the checkbox to target. + * @param {boolean} checked - The desired checked state (true to check, false to uncheck). + * @returns {void} + */ + function setCheckbox(filterText, checked) { + const checkboxes = document.querySelectorAll('p-checkbox'); + + const targetCheckbox = Array.from(checkboxes).find(checkbox => { + const label = checkbox.querySelector('.p-checkbox-label'); + return label && label.textContent.trim() === filterText; }); - }; - pollingId = window.setInterval( - checkScreenForDynamicTab, - SCREEN_POLL_INTERVAL_MS, - ); -} - -/** - * Sets up custom styles for hiding default Devolutions elements that Coder - * users shouldn't need to care about. - * - * @returns {void} - */ -function setupAlwaysOnStyles() { - const styleId = "coder-patch--styles-always-on"; - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - const existingContainer = document.querySelector("#" + styleId); - if (existingContainer) { - return; + if (targetCheckbox) { + const input = targetCheckbox.querySelector('input[type="checkbox"]'); + if (input) { + input.checked = checked; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + log(`$${filterText} set to $${checked}.`); + } else { + log(`$${filterText} checkbox not found in top bar.`); + } } - - const styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` - /* app-menu corresponds to the sidebar of the default view. */ - app-menu { - display: none !important; + + /** + * Continuously polls the DOM for a specific form element. + * - Searches for a
inside a element. + * - If found, calls `fillForm(form)` to process it. + * - If not found, logs a retry message and schedules another check after a delay. + * + * @returns {void} + */ + function pollForForm() { + const form = document.querySelector("web-client-form form"); + if (form) { + fillForm(form); + + // Start polling for top bar after form is filled + pollForSessionToolBar(); + } else { + log("Form not yet available. Retrying..."); + setTimeout(pollForForm, POLL_INTERVAL_MS); } - `; - - document.head.appendChild(styleContainer); -} - -/** - * This ensures that the Devolutions login form (which by default, always shows - * up on screen when the app first launches) stays visually hidden from the user - * when they open Devolutions via the Coder module. - * - * The form will still be filled out automatically and submitted in the - * background via the rest of the logic in this file, so this function is mainly - * to help avoid screen flickering and make the overall experience feel a little - * more polished (even though it's just one giant hack). - * - * @returns {void} - */ -function hideFormForInitialSubmission() { - const styleId = "coder-patch--styles-initial-submission"; - const cssOpacityVariableName = "--coder-opacity-multiplier"; - - /** @type {HTMLStyleElement | null} */ - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - let styleContainer = document.querySelector("#" + styleId); - if (!styleContainer) { - styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` - /* - Have to use opacity instead of visibility, because the element still - needs to be interactive via the script so that it can be auto-filled. - */ - :root { - /* - Can be 0 or 1. Start off invisible to avoid risks of UI flickering, - but the rest of the function should be in charge of making the form - container visible again if something goes wrong during setup. - - Double dollar sign needed to avoid Terraform script false positives - */ - $${cssOpacityVariableName}: 0; - } - - /* - web-client-form is the container for the main session form, while - the div is for the dropdown that is used for selecting the protocol. - The dropdown is not inside of the form for CSS styling reasons, so we - need to select both. - */ - web-client-form, - body > div.p-overlay { - /* - Double dollar sign needed to avoid Terraform script false positives - */ - opacity: calc(100% * var($${cssOpacityVariableName})) !important; - } - `; - - document.head.appendChild(styleContainer); } - // The root node being undefined should be physically impossible (if it's - // undefined, the browser itself is busted), but we need to do a type check - // here so that the rest of the function doesn't need to do type checks over - // and over. - const rootNode = document.querySelector(":root"); - if (!(rootNode instanceof HTMLHtmlElement)) { - // Remove the container entirely because if the browser is busted, who knows - // if the CSS variables can be applied correctly. Better to have something - // be a bit more ugly/painful to use, than have it be impossible to use - styleContainer.remove(); - return; + /** + * Continuously polls the DOM for a specific form element. + * - Searches for a element. + * - If found, adds another listener to session toolbar + * - If not found, logs a retry message and schedules another check after a delay. + * + * @returns {void} + */ + function pollForSessionToolBar() { + const sessionToolBar = document.querySelector("session-toolbar"); + if (sessionToolBar) { + log("Top bar detected. Proceeding with next steps..."); + attachCloseListener(sessionToolBar); + + // Automatically set checkboxes to improve user experience + setCheckbox("Unicode Keyboard Mode", true) + setCheckbox("Dynamic Resize", true) + } else { + log("Top bar not yet available. Retrying..."); + setTimeout(pollForSessionToolBar, POLL_INTERVAL_MS); + } + } + + /** + * Logs a message to the console with a standardized prefix. + * Format: [Devolutions Patch] $ + * + * @param {string} msg - The message to log. + * @returns {void} + */ + function log(msg) { + console.log(`[Devolutions Patch] $${msg}`); } - // It's safe to make the form visible preemptively because Devolutions - // outputs the Windows view through an HTML canvas that it overlays on top - // of the rest of the app. Even if the form isn't hidden at the style level, - // it will still be covered up. - const restoreOpacity = () => { - rootNode.style.setProperty(cssOpacityVariableName, "1"); - }; - - // If this file gets more complicated, it might make sense to set up the - // timeout and event listener so that if one triggers, it cancels the other, - // but having restoreOpacity run more than once is a no-op for right now. - // Not a big deal if these don't get cleaned up. - - // Have the form automatically reappear no matter what, so that if something - // does break, the user isn't left out to dry - window.setTimeout(restoreOpacity, 5_000); - - /** @type {HTMLFormElement | null} */ - const form = document.querySelector("web-client-form > form"); - form?.addEventListener( - "submit", - () => { - // Not restoring opacity right away just to give the HTML canvas a little - // bit of time to get spun up and cover up the main form - window.setTimeout(restoreOpacity, 1_000); - }, - { once: true }, - ); -} - -// Always safe to call these immediately because even if the Angular app isn't -// loaded by the time the function gets called, the CSS will always be globally -// available for when Angular is finally ready -setupAlwaysOnStyles(); -hideFormForInitialSubmission(); - -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormDetection); -} else { - setupFormDetection(); -} + // Always safe to call these immediately because even if the Angular app isn't + // loaded by the time the function gets called, the CSS will always be globally + // available for when Angular is finally ready + setupAlwaysOnStyles(); + hideFormForInitialSubmission(); + + log("Script loaded. Starting form detection..."); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", pollForForm); + } else { + pollForForm(); + } +})(); \ No newline at end of file From 3468a9fc7f9daba70665faa2fa27a7e920c4637f Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:15:28 +0100 Subject: [PATCH 02/14] Fix auto-complete --- .../modules/windows-rdp/devolutions-patch.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/registry/coder/modules/windows-rdp/devolutions-patch.js b/registry/coder/modules/windows-rdp/devolutions-patch.js index 14ec7e8de..5f3471486 100644 --- a/registry/coder/modules/windows-rdp/devolutions-patch.js +++ b/registry/coder/modules/windows-rdp/devolutions-patch.js @@ -1,3 +1,30 @@ +// @ts-check +/** + * @file Defines the custom logic for patching in UI changes/behavior into the + * base Devolutions Gateway Angular app. + * + * Defined as a JS file to remove the need to have a separate compilation step. + * It is highly recommended that you work on this file from within VS Code so + * that you can take advantage of the @ts-check directive and get some type- + * checking still. + * + * Other notes about the weird ways this file is set up: + * - A lot of the HTML selectors in this file will look nonstandard. This is + * because they are actually custom Angular components. + * - It is strongly advised that you avoid template literals that use the + * placeholder syntax via the dollar sign. The Terraform file is treating this + * as a template file, and because it also uses a similar syntax, there's a + * risk that some values will trigger false positives. If a template literal + * must be used, be sure to use a double dollar sign to escape things. + * - All the CSS should be written via custom style tags and the !important + * directive (as much as that is a bad idea most of the time). We do not + * control the Angular app, so we have to modify things from afar to ensure + * that as Angular's internal state changes, it doesn't modify its HTML nodes + * in a way that causes our custom styles to get wiped away. + * + * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry + * @typedef {Readonly>} FormFieldEntries + */ (function () { /** * The communication protocol to set Devolutions to. @@ -30,6 +57,7 @@ username: { /** @readonly */ querySelector: "web-client-username-control input", + /** @readonly */ value: "${CODER_USERNAME}", }, @@ -37,6 +65,7 @@ password: { /** @readonly */ querySelector: "web-client-password-control input", + /** @readonly */ value: "${CODER_PASSWORD}", }, From f09a2ac33f861f8fe5860d64f771ed0d6d1358db Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:16:16 +0100 Subject: [PATCH 03/14] Modify regex expression to account for modified devolutions-patch.js --- registry/coder/modules/windows-rdp/main.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index 125b3b3b6..76bf0dc36 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -86,7 +86,7 @@ describe("Web RDP", async () => { * @see {@link https://regex101.com/r/UMgQpv/2} */ const formEntryValuesRe = - /^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?.+?)",$.*?^};$/ms; + /username:\s*\{[\s\S]*?value:\s*"(?[^"]+)"[\s\S]*?password:\s*\{[\s\S]*?value:\s*"(?[^"]+)"/; // Test that things work with the default username/password const defaultState = await runTerraformApply( @@ -99,9 +99,10 @@ describe("Web RDP", async () => { const defaultRdpScript = findWindowsRdpScript(defaultState); expect(defaultRdpScript).toBeString(); + const defaultResultsGroup = formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {}; - + expect(defaultResultsGroup.username).toBe("Administrator"); expect(defaultResultsGroup.password).toBe("coderRDP!"); From 24f699b2c2b80b0805e468d27d41c76d0e372937 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:16:46 +0100 Subject: [PATCH 04/14] Make module path dynamic --- .../powershell-installation-script.tftpl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 27c45b45b..cf32cb8c6 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -43,8 +43,21 @@ catch { } # Construct the module path for system-wide installation -$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" -$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1" +$modulePath = $null # Declare outside the loop +$paths = $env:PSModulePath -split ';' + +foreach ($path in $paths) { + $candidatePath = Join-Path -Path $path -ChildPath $moduleName + if ($moduleVersion) { + $candidatePath = Join-Path -Path $candidatePath -ChildPath $moduleVersion + } + + $psd1Path = Join-Path -Path $candidatePath -ChildPath "$moduleName.psd1" + if (Test-Path $psd1Path) { + $modulePath = $psd1Path + break + } +} # Import the module using the full path Import-Module $modulePath From 5f349dff2ba4a11634053459ddedcb11b672ca02 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:17:12 +0100 Subject: [PATCH 05/14] Change to variable input --- registry/coder/modules/windows-rdp/main.tf | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index c1b996dd6..f79d222af 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -9,6 +9,18 @@ terraform { } } +variable "display_name" { + type = string + description = "The display name for the Web RDP application." + default = "Web RDP" +} + +variable "slug" { + type = string + description = "The slug for the Web RDP application." + default = "web-rdp" +} + variable "order" { type = number description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." @@ -77,8 +89,8 @@ resource "coder_script" "windows-rdp" { resource "coder_app" "windows-rdp" { agent_id = var.agent_id share = var.share - slug = "web-rdp" - display_name = "Web RDP" + slug = var.slug + display_name = var.display_name url = "http://localhost:7171" icon = "/icon/desktop.svg" subdomain = true From 0267779b7406c5506b50a3c71b1f06e4f1e46d5d Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:46:05 +0100 Subject: [PATCH 06/14] Run bun fmt --- .../modules/windows-rdp/devolutions-patch.js | 46 +++++++++++-------- .../coder/modules/windows-rdp/main.test.ts | 3 +- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/registry/coder/modules/windows-rdp/devolutions-patch.js b/registry/coder/modules/windows-rdp/devolutions-patch.js index 5f3471486..1f231ca3f 100644 --- a/registry/coder/modules/windows-rdp/devolutions-patch.js +++ b/registry/coder/modules/windows-rdp/devolutions-patch.js @@ -57,11 +57,11 @@ username: { /** @readonly */ querySelector: "web-client-username-control input", - + /** @readonly */ value: "${CODER_USERNAME}", }, - /** @readonly */ + /** @readonly */ password: { /** @readonly */ querySelector: "web-client-password-control input", @@ -227,7 +227,9 @@ // immediately, just in the off chance that something in the Angular app // intercepts it or stops it from propagating properly const timeoutId = window.setTimeout(() => { - reject(new Error("Input event did not get processed correctly in time.")); + reject( + new Error("Input event did not get processed correctly in time."), + ); }, 3_000); const handleSuccessfulDispatch = () => { @@ -274,7 +276,7 @@ if (protocolTrigger) { protocolTrigger.click(); const protocolOption = document.querySelector( - `li[aria-label="$${PROTOCOL}"]` + `li[aria-label="$${PROTOCOL}"]`, ); if (protocolOption) { protocolOption.click(); @@ -294,7 +296,9 @@ log("Hostname input not found."); } - for (const [key, { querySelector, value }] of Object.entries(formFieldEntries)) { + for (const [key, { querySelector, value }] of Object.entries( + formFieldEntries, + )) { const input = document.querySelector(querySelector); if (input) { await setInputValue(input, value); @@ -304,7 +308,9 @@ } } - const submitButton = form.querySelector('p-button[class="p-element"] button'); + const submitButton = form.querySelector( + 'p-button[class="p-element"] button', + ); if (submitButton && !submitButton.disabled) { submitButton.click(); log("Form submitted."); @@ -325,15 +331,15 @@ * @returns {void} */ function attachCloseListener(topBar) { - const buttons = topBar.querySelectorAll('button'); + const buttons = topBar.querySelectorAll("button"); - const closeButton = Array.from(buttons).find(button => { - const labelSpan = button.querySelector('.p-button-label'); - return labelSpan && labelSpan.textContent.trim() === 'Close Session'; + const closeButton = Array.from(buttons).find((button) => { + const labelSpan = button.querySelector(".p-button-label"); + return labelSpan && labelSpan.textContent.trim() === "Close Session"; }); if (closeButton) { - closeButton.parentElement.addEventListener('click', () => { + closeButton.parentElement.addEventListener("click", () => { window.close(); }); log("Close listener attached."); @@ -355,10 +361,10 @@ * @returns {void} */ function setCheckbox(filterText, checked) { - const checkboxes = document.querySelectorAll('p-checkbox'); + const checkboxes = document.querySelectorAll("p-checkbox"); - const targetCheckbox = Array.from(checkboxes).find(checkbox => { - const label = checkbox.querySelector('.p-checkbox-label'); + const targetCheckbox = Array.from(checkboxes).find((checkbox) => { + const label = checkbox.querySelector(".p-checkbox-label"); return label && label.textContent.trim() === filterText; }); @@ -366,14 +372,14 @@ const input = targetCheckbox.querySelector('input[type="checkbox"]'); if (input) { input.checked = checked; - input.dispatchEvent(new Event('change', { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); } log(`$${filterText} set to $${checked}.`); } else { log(`$${filterText} checkbox not found in top bar.`); } } - + /** * Continuously polls the DOM for a specific form element. * - Searches for a inside a element. @@ -410,14 +416,14 @@ attachCloseListener(sessionToolBar); // Automatically set checkboxes to improve user experience - setCheckbox("Unicode Keyboard Mode", true) - setCheckbox("Dynamic Resize", true) + setCheckbox("Unicode Keyboard Mode", true); + setCheckbox("Dynamic Resize", true); } else { log("Top bar not yet available. Retrying..."); setTimeout(pollForSessionToolBar, POLL_INTERVAL_MS); } } - + /** * Logs a message to the console with a standardized prefix. * Format: [Devolutions Patch] $ @@ -441,4 +447,4 @@ } else { pollForForm(); } -})(); \ No newline at end of file +})(); diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index 76bf0dc36..a319fbaac 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -99,10 +99,9 @@ describe("Web RDP", async () => { const defaultRdpScript = findWindowsRdpScript(defaultState); expect(defaultRdpScript).toBeString(); - const defaultResultsGroup = formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {}; - + expect(defaultResultsGroup.username).toBe("Administrator"); expect(defaultResultsGroup.password).toBe("coderRDP!"); From b950d8243eeec1dd769806794e3cef41a7c514b6 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:46:28 +0100 Subject: [PATCH 07/14] Update version ref --- registry/coder/modules/windows-rdp/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index f19afc475..92c5ac17e 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.2.3" + version = "1.3.0" agent_id = resource.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.2.3" + version = "1.3.0" agent_id = resource.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.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -54,7 +54,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id devolutions_gateway_version = "2025.2.2" # Specify a specific version } From 057bebde7499c9bb97d64c0cb48f050ad22aadc5 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:49:20 +0000 Subject: [PATCH 08/14] Extract icon as a variable --- registry/coder/modules/windows-rdp/main.tf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index f79d222af..ae728368e 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -21,6 +21,12 @@ variable "slug" { default = "web-rdp" } +variable "icon" { + type = string + description = "The icon for the Web RDP application." + default = "/icon/desktop.svg" +} + variable "order" { type = number description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." @@ -92,7 +98,7 @@ resource "coder_app" "windows-rdp" { slug = var.slug display_name = var.display_name url = "http://localhost:7171" - icon = "/icon/desktop.svg" + icon = var.icon subdomain = true order = var.order group = var.group From 95a71e983f14bfdada3f9222abbf09bcd4515c7b Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 27 Oct 2025 14:54:20 -0500 Subject: [PATCH 09/14] fix: change devolutions version default to latest, and enhance install logic --- registry/coder/modules/windows-rdp/main.test.ts | 6 +++--- registry/coder/modules/windows-rdp/main.tf | 4 ++-- .../powershell-installation-script.tftpl | 13 +++++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index a319fbaac..ae503a19f 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -59,9 +59,9 @@ describe("Web RDP", async () => { expect(lines).toEqual( expect.arrayContaining([ '$moduleName = "DevolutionsGateway"', - // Devolutions does versioning in the format year.minor.patch - expect.stringMatching(/^\$moduleVersion = "\d{4}\.\d+\.\d+"$/), - "Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force", + // Default is "latest" to automatically get the newest version + '$moduleVersion = "latest"', + "Install-Module -Name $moduleName -Force", ]), ); }); diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index ae728368e..3c83d195b 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -66,8 +66,8 @@ variable "admin_password" { variable "devolutions_gateway_version" { type = string - default = "2025.2.2" - description = "Version of Devolutions Gateway to install. Defaults to the latest available version." + default = "latest" + description = "Version of Devolutions Gateway to install. Use 'latest' for the most recent version, or specify a version like '2025.3.2'." } resource "coder_script" "windows-rdp" { diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index cf32cb8c6..e58659f1c 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -34,12 +34,21 @@ try { # Install-PackageProvider is required for AWS. Need to set command to # terminate on failure so that try/catch actually triggers Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + Install-Module -Name $moduleName -Force + } else { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } } catch { # If the first command failed, assume that we're on GCP and run # Install-Module only - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + Install-Module -Name $moduleName -Force + } else { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } } # Construct the module path for system-wide installation From 42dbdd77b1e8e7c2c3d136522174378055e7a375 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 27 Oct 2025 15:16:20 -0500 Subject: [PATCH 10/14] chore: ensure TLS 1.2 is enabled and PSGallery is trusted for automated installation --- registry/coder/modules/windows-rdp/main.test.ts | 2 ++ .../modules/windows-rdp/powershell-installation-script.tftpl | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index ae503a19f..80c09fd0d 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -61,6 +61,8 @@ describe("Web RDP", async () => { '$moduleName = "DevolutionsGateway"', // Default is "latest" to automatically get the newest version '$moduleVersion = "latest"', + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", + "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted", "Install-Module -Name $moduleName -Force", ]), ); diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index e58659f1c..1b68c2832 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -28,6 +28,10 @@ function Install-DevolutionsGateway { $moduleName = "DevolutionsGateway" $moduleVersion = "${devolutions_gateway_version}" +# Ensure TLS 1.2 is enabled and PSGallery is trusted for automated installation +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + # Install the module with the specified version for all users # This requires administrator privileges try { From 1d9874ebcf4a867b680602db510108053315d136 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 27 Oct 2025 15:34:51 -0500 Subject: [PATCH 11/14] fix: adjust PowerShell installation script to set PSGallery as trusted after NuGet installation --- .../windows-rdp/powershell-installation-script.tftpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 1b68c2832..97b4ec1af 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -28,9 +28,8 @@ function Install-DevolutionsGateway { $moduleName = "DevolutionsGateway" $moduleVersion = "${devolutions_gateway_version}" -# Ensure TLS 1.2 is enabled and PSGallery is trusted for automated installation +# Ensure TLS 1.2 is enabled for PSGallery [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -Set-PSRepository -Name PSGallery -InstallationPolicy Trusted # Install the module with the specified version for all users # This requires administrator privileges @@ -39,6 +38,9 @@ try { # terminate on failure so that try/catch actually triggers Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop + # Set PSGallery as trusted after NuGet is installed + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { Install-Module -Name $moduleName -Force } else { From 213d110190ec9789630d5c933bf192765f38a479 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 27 Oct 2025 16:23:00 -0500 Subject: [PATCH 12/14] fix: change installation script to correctly handle module version retrieval --- .../powershell-installation-script.tftpl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 97b4ec1af..a32ebb452 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -59,12 +59,22 @@ catch { # Construct the module path for system-wide installation $modulePath = $null # Declare outside the loop + +if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + $installedModule = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue + if ($installedModule) { + $installedVersion = $installedModule.Version.ToString() + } +} else { + $installedVersion = $moduleVersion +} + $paths = $env:PSModulePath -split ';' foreach ($path in $paths) { $candidatePath = Join-Path -Path $path -ChildPath $moduleName - if ($moduleVersion) { - $candidatePath = Join-Path -Path $candidatePath -ChildPath $moduleVersion + if ($installedVersion) { + $candidatePath = Join-Path -Path $candidatePath -ChildPath $installedVersion } $psd1Path = Join-Path -Path $candidatePath -ChildPath "$moduleName.psd1" From aa3fbe921734f9b1059d8e94c54a4c4d58e91d44 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Mon, 27 Oct 2025 22:09:07 +0000 Subject: [PATCH 13/14] fix: explicitly import LocalAccounts module in PowerShell Set-AdminPassword --- .../modules/windows-rdp/powershell-installation-script.tftpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index a32ebb452..2366a7a8a 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -2,6 +2,9 @@ function Set-AdminPassword { param ( [string]$adminPassword ) + # Explicitly import LocalAccounts module + Import-Module Microsoft.PowerShell.LocalAccounts -ErrorAction SilentlyContinue + # Set admin password Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user From c8895dbf559bb7beb8ce758ee3cb262aaa48b374 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:53:32 +0000 Subject: [PATCH 14/14] fix: Remove excessive whitespace Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../modules/windows-rdp/powershell-installation-script.tftpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 2366a7a8a..1657b878d 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -61,7 +61,7 @@ catch { } # Construct the module path for system-wide installation -$modulePath = $null # Declare outside the loop +$modulePath = $null # Declare outside the loop if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { $installedModule = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue