{"id":1894,"date":"2019-03-04T02:22:06","date_gmt":"2019-03-03T18:22:06","guid":{"rendered":"http:\/\/www.jhouseconsulting.com\/?p=1894"},"modified":"2026-03-29T20:14:02","modified_gmt":"2026-03-29T12:14:02","slug":"controlling-the-starting-of-the-citrix-desktop-service-brokeragent","status":"publish","type":"post","link":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/2019\/03\/04\/controlling-the-starting-of-the-citrix-desktop-service-brokeragent-1894","title":{"rendered":"Controlling the Starting of the Citrix Desktop Service (BrokerAgent)"},"content":{"rendered":"\n<p>This is a process I&#8217;ve been working on since early 2017. I got it to a point where it works perfectly for my needs in my lab and several customer environments, and has been very reliable over the last few months, so I decided it was ready to release to the community in March 2019. I have continued to update and tweak this over the years so that it works under all PVS and MCS deployments and Windows Operating Systems.<\/p>\n\n\n\n<p>UPDATED 27th March 2026<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Tweaked the updates completed under 3.9 as I experienced a timing issue. Added the Create-RegValue function to assist with this.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 25th March 2026<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The updates added to 3.7 that starts the Broker service immediately if it is a Citrix MCS Master Image caused an issue because the Broker service was left in a Manual startup type. So the fix is to set it back to Disabled startup type after starting it. This allows this script to control how it starts once the MCS Master Image is deployed.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 28th January 2026<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Added the Delete-BrokerAgentCachedData function, which clears the SavedListOfDDCsSids.xml as per <a href=\"https:\/\/support.citrix.com\/support-home\/kbsearch\/article?articleNumber=CTX216883\" target=\"_blank\" rel=\"noreferrer noopener\">CTX216883<\/a> to prevent registration issues caused by old or decommissioned Delivery Controllers or Cloud Connectors.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 17th November 2025<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Improved the check for the Personality.ini and MCSPersonality.ini files based on the history of VDA changes.<\/li>\n\n\n\n<li>Replaced the Get-LastBootTime function with the Get-UpTime function. This starts to phase out the reliance on the Get-WmiObject cmdlet, with preference to use the Get-CimInstance cmdlet.<\/li>\n\n\n\n<li>Added a group policy update (gpupdate) to be invoked before it reads the VDAHelper registry values. I&#8217;ve<br>found this hit and miss with MCS images. So a gpupdate helps to ensures that the registry values are in<br>place.<\/li>\n\n\n\n<li>Added a check to see if it is a Citrix MCS Master Image. The script will check the following value:<br>Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Citrix\\Configuration<br>Type: DWORD<br>Value: MasterImage<br>Data: 1<br>This is important so that it starts the Broker service immediately so that it does not cause any issues<br>with the image preparation process.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 21st July 2025<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Added extra error checking and further improved the coding<\/li>\n\n\n\n<li>When we set the Winlogon DefaultDomainName value we also need to set AutoAdminLogon value to 0 and clear the DefaultUserName value.<\/li>\n\n\n\n<li>If the Winlogon AutoAdminLogon value is 0 (disabled), and the TriggerOnTaskEndEvent is set to 1 (enabled), we change the TriggerOnTaskEndEvent to 0 (disabled). This reduces unnecessary further delay waiting for an event that will never run.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 14th July 2025<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Replaced the Get-DeliveryControllers function with the Get-ListOfDDCs function. The name of the function may have been misleading given that it&#8217;s for both Delivery Controller or Cloud Connector addresses. It now allows for the C:\\Personality.ini and C:\\MCSPersonality.ini for MCS deployments.<\/li>\n\n\n\n<li>Added the UsePersonalityini value to the VDAHelper registry values, which is used to call the updated Get-ListOfDDCs function.<\/li>\n\n\n\n<li>Updated the XDPing function<\/li>\n\n\n\n<li>Changed the flow of some of the code<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 31st January 2023<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Added&nbsp;the DefaultDomainName value to the registry, which tells this script to set the Winlogon DefaultDomainName value in the registry once the autologon process has started. This allows us to use a local account for the Autologon process instead of a Domain service account, which won&#8217;t work if the Winlogon DefaultDomainName value has already been set to your preferred domain.&nbsp;This reduces the security footprint when using service accounts by allowing us to easily rotate passwords using a local account for the autologon process for each image build. Refer to my article <a href=\"https:\/\/www.jhouseconsulting.com\/2025\/07\/26\/priming-a-non-persistent-windows-image-using-an-autologon-process-with-an-auto-logoff-timer-3343\" target=\"_blank\" rel=\"noopener\" title=\"\">Priming a Non-persistent Windows Image using an Autologon Process with an Auto Logoff Timer<\/a> to understand how this works.<\/li>\n\n\n\n<li>You can now use the&nbsp;policies registry key for the registry settings:\n<ul class=\"wp-block-list\">\n<li>Policies Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Citrix\\VDAHelper<\/li>\n\n\n\n<li>Preferences Key:&nbsp;HKEY_LOCAL_MACHINE\\SOFTWARE\\Citrix\\VDAHelper<\/li>\n\n\n\n<li>Values set under the Policies key have a higher priority.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 30th April 2022<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Improved the code so that the logon\/logoff events are detected more efficiently across all Windows Operating Systems.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 17th March 2020<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Enhanced the checking for the ListofDDCs registry value by also checking the Policies key structure.<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 18th July 2019<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The creation of the Scheduled Task needed for this script can be found in the&nbsp;<a href=\"https:\/\/www.jhouseconsulting.com\/2019\/07\/18\/citrix-virtual-delivery-agent-vda-post-install-script-2015\" target=\"_blank\" rel=\"noopener\">Citrix Virtual Delivery Agent (VDA) Post Install Script<\/a>. It&#8217;s important that the priority of the Scheduled Task is set to normal to prevent it from being queued.<\/li>\n\n\n\n<li>Enhanced the Get-LogonLogoffEvent function for backward support of Windows 7\/2008R2<\/li>\n<\/ul>\n\n\n\n<p>UPDATED 16th May 2019<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Added much more logging with timestamps to help debug issues and correlate events. You&#8217;ll see from the screen shot of the log file below that it&#8217;s now quite comprehensive.<\/li>\n\n\n\n<li>Ensured that the format of the logging timestamp and LastBootUpTime were aligned so that its accuracy can be easily verified as I documented <a href=\"https:\/\/www.jhouseconsulting.com\/2019\/05\/16\/best-practice-for-the-windows-time-w32time-service-for-rdsh-and-vdi-workloads-1948\" target=\"_blank\" rel=\"noopener\">here<\/a>.<\/li>\n\n\n\n<li>Added an <a href=\"https:\/\/www.jhouseconsulting.com\/2019\/05\/16\/xdping-powershell-function-1931\" target=\"_blank\" rel=\"noopener\">XDPing function<\/a>&nbsp;I wrote to health check the Delivery Controllers.<\/li>\n<\/ul>\n\n\n\n<p>The challenge has always been that as a Session Host boots up the Citrix Desktop Service (BrokerAgent) starts and registers with the Delivery Controllers before the boot process is complete. Therefore, a user can potentially launch an application\/desktop during the tail end of the boot process. When this happens it may fail the session launch, which can leave the Session Host in an unhealthy state, such as a stuck prelaunch state, with the user potentially needing to involve the Service Desk to clear the issue. So managing the timing of the start of the Citrix Desktop Service (BrokerAgent) is extremely important to ensure that your Session Hosts have completed their startup process before registering with the Delivery Controllers. This can be easier said than done!<\/p>\n\n\n\n<p>To add to this complexity, power managed Workstation OS pools can get into a reboot loop if you use an autologon process. I use an <a href=\"https:\/\/www.jhouseconsulting.com\/2025\/07\/26\/priming-a-non-persistent-windows-image-using-an-autologon-process-with-an-auto-logoff-timer-3343\" target=\"_blank\" rel=\"noopener\" title=\"\">autologon<\/a> process similar to what <a href=\"https:\/\/www.jgspiers.com\/\" target=\"_blank\" rel=\"noopener\">George Spiers<\/a> documented in his article &#8220;<a href=\"https:\/\/www.jgspiers.com\/citrix-director-reduce-logon-times\/\" target=\"_blank\" rel=\"noopener\">Reduce Citrix logon times by up to 75%<\/a>&#8220;. So if the Session Host has registered with the Delivery Controllers before the autologon process has logged off, the logoff process will trigger another reboot. This can become a real problem to manage.<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>An ex-Citrix employee created a &#8220;<a href=\"https:\/\/www.citrix.com\/blogs\/2017\/06\/26\/augment-your-xendesktop-deployment-with-the-desktop-helper-service\/\" target=\"_blank\" rel=\"noopener\">Citrix Desktop Helper<\/a>&#8221; tool. I had varying success with the tool. He left Citrix and no one took over the development and upkeep of this tool. As it was an unsupported Citrix tool with no ownership, I decided to stop using it and write my own, which included the logic I was after. Plus, my goal was to write it in PowerShell.<\/p>\n\n\n\n<p>I have a <a href=\"https:\/\/www.jhouseconsulting.com\/2019\/07\/18\/citrix-virtual-delivery-agent-vda-post-install-script-2015\" target=\"_blank\" rel=\"noopener\">post VDA install script<\/a> that&#8230;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Disables the Citrix Desktop Service (BrokerAgent) service<\/li>\n\n\n\n<li>Creates a Scheduled Task called &#8220;Start the Citrix Desktop Service&#8221;, which runs at computer startup under the local System account<\/li>\n<\/ul>\n\n\n\n<p>Pretty easy so far.<\/p>\n\n\n\n<p>The Scheduled Task starts the StartCitrixDesktopService.ps1 script, which is where all the smarts are. I wanted it to do&nbsp;3 things:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Wait until there is a valid server listed under the ListofDDCs registry value. This ensures Group Policy has applied, and you&#8217;re not starting the service with an empty list, or an invalid server. This is important for Gold Images where the VDA is installed with the \/MASTERMCSIMAGE, \/MASTERIMAGE or \/MASTERPVSIMAGE switch.<\/li>\n\n\n\n<li>Set the delay based on a specified time, which is what the &#8220;Citrix Desktop Helper&#8221; tool does.<\/li>\n\n\n\n<li>Set the delay based on an event. This is the cool bit where I can make it an exact science. As mentioned, I use an autologon process, so&nbsp;I have another Scheduled Task called &#8220;Session Host Autologon Logoff Task&#8221;, which logs off this autologon session. This of course writes the logon and logoff events to the Windows Security Event Log, so the script simply triggers based on those events. This means that as soon as the autologon account logs off, the service will start and register with the Delivery Controller(s).<\/li>\n<\/ol>\n\n\n\n<p>I decided to use registry values to manage most of these settings, which can be set via Group Policy Preferences. The script will read the values under either of the following registry keys to determine how long to wait before starting the Citrix Desktop Service:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Policies Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Citrix\\VDAHelper<\/li>\n\n\n\n<li>Preferences Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Citrix\\VDAHelper<\/li>\n<\/ul>\n\n\n\n<p>Values set under the Policies key have a higher priority.<\/p>\n\n\n\n<p>The values I added are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <strong>VDAHelperSettingsEnabled<\/strong> tells the script if it should action or ignore all other settings under this registry key\n<ul class=\"wp-block-list\">\n<li>Type: REG_DWORD<\/li>\n\n\n\n<li>Value: VDAHelperSettingsEnabled<\/li>\n\n\n\n<li>Data: 0=False; 1=True<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>DelayDesktopServiceTime<\/strong> value tells this script, in seconds, how long to wait before starting the Citrix Desktop Service\n<ul class=\"wp-block-list\">\n<li>Type: REG_DWORD<\/li>\n\n\n\n<li>Value: DelayDesktopServiceTime<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>TriggerOnTaskEndEvent<\/strong> value tells this script to wait for a task to end before starting the Citrix Desktop Service\n<ul class=\"wp-block-list\">\n<li>Type: REG_DWORD<\/li>\n\n\n\n<li>Value: TriggerOnTaskEndEvent<\/li>\n\n\n\n<li>Data: 0=False; 1=True<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>TaskNameFullPath<\/strong> value tells this script to which task to base the trigger event off before starting the Citrix Desktop Service\n<ul class=\"wp-block-list\">\n<li>Type: REG_SZ<\/li>\n\n\n\n<li>Value: TaskNameFullPath<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>MaximumTaskWaitTime<\/strong> value tells this script, in seconds, how long to wait for the task to start\/end before starting the Citrix Desktop Service. This ensures that any failure in the task trigger events does not ultimately prevent the Citrix Desktop Service from starting.\n<ul class=\"wp-block-list\">\n<li>Type: REG_DWORD<\/li>\n\n\n\n<li>Value: MaximumTaskWaitTime<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>LogonEventUserName<\/strong> value tells this script which username will be in the logon event 4624 so that it can collect the &#8220;Logon ID&#8221; value to match it with a logoff event.\n<ul class=\"wp-block-list\">\n<li>Type: REG_SZ<\/li>\n\n\n\n<li>Value: LogonEventUserName<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>DefaultDomainName<\/strong> value tells this script to set the Winlogon DefaultDomainName value in the registry once the autologon process has started. This allows us to use a local account for the Autologon process instead of a Domain service account, which won&#8217;t work if the Winlogon DefaultDomainName value has already been set to your preferred domain. This reduces the security footprint when using service accounts by allowing us to easily rotate passwords using a local account for the autologon process for each image build. Adding this value is optional. If this value is present, the script will set the DefaultDomainName value under the &#8220;HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon&#8221; registry key.\n<ul class=\"wp-block-list\">\n<li>Type: REG_SZ<\/li>\n\n\n\n<li>Value: DefaultDomainName<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The <strong>UsePersonalityini<\/strong> value tells this script to get the list of DDCs from the MCSPersonality.ini or Personality.ini, whichever is present.\n<ul class=\"wp-block-list\">\n<li>Type: REG_DWORD<\/li>\n\n\n\n<li>Value: UsePersonalityini<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>The following screen shot shows the registry values set under the Policies key.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e9afd20b3cb&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e9afd20b3cb\" class=\"aligncenter size-large wp-lightbox-container\"><img fetchpriority=\"high\" decoding=\"async\" width=\"680\" height=\"285\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2025\/07\/VDAHelper-680x285.png\" alt=\"VDA Helper Registry Settings\" class=\"wp-image-3340\" srcset=\"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2025\/07\/VDAHelper-680x285.png 680w, https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2025\/07\/VDAHelper-768x322.png 768w, https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2025\/07\/VDAHelper-300x126.png 300w, https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2025\/07\/VDAHelper.png 803w\" sizes=\"(max-width: 680px) 100vw, 680px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Enlarge\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n<\/div>\n\n\n<p>Notes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The TriggerOnTaskEndEvent value (0 or 1) will determine if the script waits for the specified task to start and end or uses the DelayDesktopServiceTime value instead.<\/li>\n\n\n\n<li>During the initial implementation I found that the logoff event for the autologon account may occur a few seconds after the task ends. When this happens, and depending on how quickly the the VDA registers with the Delivery Controller, it may cause a power managed VDI machine to reboot again; creating a reboot loop. This is simply a timing issue. So to work around this, we also wait for the logoff event that tells us that the session has been destroyed\/terminated. This presented me with a challenge as I was unable tie events 4624 (logon) and 4634 (logoff) together. We can, however, tie the 4624 (logon) event to the 4647 (logoff) event. However, this event just tells us that the logoff was initiated and not complete. A 4634 (logoff) event will follow, but there is no information in that event which allows us to formally tie them together. Therefore, we make the assumption that the 4634 event that follows the 4647 event is the<br>actual complete termination of the logoff, which is when the logon session is actually destroyed. At this point it&#8217;s then safe to start the Citrix Desktop Service without the risk of a reboot loop occurring.<\/li>\n\n\n\n<li>I have found that it may take some time for the 4634 (logoff) event to come through that tells you that<br>the session has been destroyed\/terminated. This is typically due to antivirus software. My experience was with Symantec Endpoint Protection (SEP).<\/li>\n<\/ul>\n\n\n\n<p>A comprehensive log file is created that logs all steps of the process. This log is from the 16th May 2019 release. Each release since then has further improved the logging.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e9afd20b80c&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e9afd20b80c\" class=\"aligncenter wp-lightbox-container\"><img decoding=\"async\" width=\"762\" height=\"786\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2019\/05\/StartCitrixDesktopServiceLog-e1558013813813.png\" alt=\"Start Citrix Desktop Service Log\" class=\"wp-image-1928\" srcset=\"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2019\/05\/StartCitrixDesktopServiceLog-e1558013813813.png 762w, https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2019\/05\/StartCitrixDesktopServiceLog-e1558013813813-291x300.png 291w, https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-content\/uploads\/2019\/05\/StartCitrixDesktopServiceLog-e1558013813813-300x309.png 300w\" sizes=\"(max-width: 762px) 100vw, 762px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Enlarge\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n<\/div>\n\n\n<p>Future Improvements:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The Validate-ListOfDDCs function needs to be updated to look in the C:\\Personality.ini for MCS deployments<\/li>\n\n\n\n<li>Can take different actions based on image type or manual deployment:&nbsp;<a href=\"https:\/\/github.com\/megamorf\/CitrixImagingTools\/issues\/13\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/megamorf\/CitrixImagingTools\/issues\/13<\/a><\/li>\n<\/ul>\n\n\n\n<p>Some of what I&#8217;ve documented may seem a little over complicated and overwhelming. To help understand how I implemented the post VDA install tasks and the autologon and logoff process, refer to my articles:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.jhouseconsulting.com\/2019\/07\/18\/citrix-virtual-delivery-agent-vda-post-install-script-2015\" target=\"_blank\" rel=\"noopener\">Citrix Virtual Delivery Agent (VDA) Post Install Script<\/a><\/li>\n<li><a title=\"\" href=\"https:\/\/www.jhouseconsulting.com\/2025\/07\/26\/priming-a-non-persistent-windows-image-using-an-autologon-process-with-an-auto-logoff-timer-3343\" target=\"_blank\" rel=\"noopener\">Priming a Non-persistent Windows Image using an Autologon Process with an Auto Logoff Timer<\/a><\/li>\n<\/ul>\n<p>You don&#8217;t need to worry about this as part of your initial implementation. Simply start by using the DelayDesktopServiceTime value and you can enhance this at a later date.<\/p>\n\n\n\n<p>I also want to acknowledge other scripts and methods available, neither of which provided the exact outcome I was after, but they may to others:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/smulpuru.wordpress.com\" target=\"_blank\" rel=\"noopener\">Siva Mulpuru<\/a> released a blog to &#8220;<a href=\"https:\/\/smulpuru.wordpress.com\/2017\/04\/30\/delay-vda-registration-for-xendestopxenapp\/\" target=\"_blank\" rel=\"noopener\">Delay VDA registration for XenDestop\/Xenapp<\/a>&#8220;<\/li>\n\n\n\n<li>The awesome&nbsp;<a href=\"https:\/\/www.loginconsultants.com\/en\/news\/tech-update\/item\/base-image-script-framework-bis-f\" target=\"_blank\" rel=\"noopener\">Base Image Script Framework (BIS-F)<\/a>&nbsp;has a setting to &#8220;Delay Citrix Desktop Service&#8221;<\/li>\n<\/ul>\n\n\n\n<p>Here is the <a  data-e-Disable-Page-Transition=\"true\" class=\"download-link\" title=\"\" href=\"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/download\/1904\/?tmstv=1776922578\" rel=\"nofollow\" id=\"download-link-1904\" data-redirect=\"false\" >\n\tStartCitrixDesktopService.ps1\t(2308 downloads\t)\n<\/a>\n script:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: powershell; auto-links: false; title: ; quick-code: false; notranslate\" title=\"\">\n&lt;#\n  This script will manage the start of the Citrix Desktop Service.\n\n  This script will read the values under the following registry key to determine how long to wait before\n  starting the Citrix Desktop Service.\n  - Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Citrix\\VDAHelper\n  - Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Citrix\\VDAHelper\n  - Note that the values set under the Policies key have a higher priority.\n  The VDAHelperSettingsEnabled tells the script if it should action or ignore all other settings under this\n  registry key\n  - Type: REG_DWORD\n  - Value: VDAHelperSettingsEnabled\n  - Data: 0=False; 1=True\n  The DelayDesktopServiceTime value tells this script, in seconds, how long to wait before starting the\n  Citrix Desktop Service\n  - Type: REG_DWORD\n  - Value: DelayDesktopServiceTime\n  The TriggerOnTaskEndEvent value tells this script to wait for a task to end before starting the Citrix\n  Desktop Service\n  - Type: REG_DWORD\n  - Value: TriggerOnTaskEndEvent\n  - Data: 0=False; 1=True\n  The TaskNameFullPath value tells this script to which task to base the trigger event off before starting\n  the Citrix Desktop Service\n  - Type: REG_SZ\n  - Value: TaskNameFullPath\n  The MaximumTaskWaitTime value tells this script, in seconds, how long to wait for the task to start\/end\n  before starting the Citrix Desktop Service. This ensures that any failure in the task trigger events\n  does not ultimately prevent the Citrix Desktop Service from starting.\n  - Type: REG_DWORD\n  - Value: MaximumTaskWaitTime\n  The LogonEventUserName value tells this script which username will be in the logon event 4624 so that it\n  can collect the &quot;Logon ID&quot; value to match it with a logoff event.\n  - Type: REG_SZ\n  - Value: LogonEventUserName\n  The DefaultDomainName value tells this script what to set the Winlogon DefaultDomainName value to once\n  the autologon process has started. This allows us to use a local account for the Autologon process.\n  - Type: REG_SZ\n  - Value: DefaultDomainName\n  The UsePersonalityini value tells this script to get the list of DDCs from the MCSPersonality.ini or\n  Personality.ini, whichever is present.\n  - Type: REG_DWORD\n  - Value: UsePersonalityini\n  - Data: 0=False; 1=True\n\n  IMPORTANT NOTES:\n  - The TriggerOnTaskEndEvent value (0 or 1) will determine if the script waits for the specified task to\n    start and end or uses the DelayDesktopServiceTime value instead.\n  - During the initial implementation I found that the logoff event for the autologon account may occur a few\n    seconds after the task ends. When this happens, and depending on how quickly the the VDA registers with\n    the Delivery Controller or Cloud Connector, it may cause a power managed VDI machine to reboot again;\n    creating a reboot loop. This is simply a timing issue. So to work around this we also wait for the logoff\n    event that tells us that the session has been destroyed\/terminated. This presented me with a challenge as\n    I was unable tie events 4624 (logon) and 4634 (logoff) together. We can, however, tie the 4624 (logon)\n    event to the 4647 (logoff) event. However, this event just tells us that the logoff was initiated and not\n    complete. A 4634 (logoff) event will follow, but there is no information in that event which allows us to\n    formally tie them together. Therefore, we make the assumption that the 4634 event that follows the 4647\n    event is the actual complete termination of the logoff, which is when the logon session is actually\n    destroyed. At this point it's then safe to start the Citrix Desktop Service without the risk of a reboot\n    loop occuring.\n  - I have found that it may take some time for the 4634 (logoff) event to come through that tells you that\n    the session has been destroyed\/terminated. This is typically due to antivirus software.\n\n  Version 3.3 Updates\n  - Replaced the Get-DeliveryControllers function with the Get-ListOfDDCs function. The name of the function\n    may have been misleading given that it's for both Delivery Controller or Cloud Connector addresses. It\n    now allows for the C:\\Personality.ini and C:\\MCSPersonality.ini for MCS deployments.\n  - Added the UsePersonalityini value to the VDAHelper registry values, which is used to call the updated\n    Get-ListOfDDCs function.\n  - Updated the XDPing function\n  - Changed the flow of some of the code\n\n  Version 3.4 Updates\n  - Added extra error checking and further improved the coding\n  - When we set the Winlogon DefaultDomainName value we also need to set AutoAdminLogon value to 0 and clear\n    the DefaultUserName value.\n  - If the Winlogon AutoAdminLogon value is 0 (disabled), and the TriggerOnTaskEndEvent is set to 1 (enabled),\n    we change the TriggerOnTaskEndEvent to 0 (disabled). This reduces unnecessary further delay waiting for an\n    event that will never run.\n\n  Version 3.5 Updates\n  - Improved the check for the Personality.ini and MCSPersonality.ini files based on the history of VDA changes.\n\n  Version 3.6 Updates\n  - Replaced the Get-LastBootTime function with the Get-UpTime function. This starts to phase out the reliance\n    on the Get-WmiObject cmdlet, with preference to use the Get-CimInstance cmdlet.\n\n  Version 3.7 Updates\n  - Added a group policy update (gpupdate) to be invoked before it reads the VDAHelper registry values. I've\n    found this hit and miss with MCS images. So a gpupdate helps to ensures that the registry values are in\n    place.\n  - Added a check to see if it is a Citrix MCS Master Image. The script will check the following value:\n      Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Citrix\\Configuration\n      Type: DWORD\n      Value: MasterImage\n      Data: 1\n    This is important so that it starts the Broker service immediately so that it does not cause any issues\n    with the image preparation process.\n\n  Version 3.8 Updates\n  - Added the Delete-BrokerAgentCachedData function, which clears the SavedListOfDDCsSids.xml as per CTX216883\n    to prevent registration issues caused by old or decommissioned Delivery Controllers or Cloud Connectors.\n\n  Version 3.9 Updates\n  - The updates added to 3.7 that starts the Broker service immediately if it is a Citrix MCS Master Image\n    caused an issue because the Broker service was left in a Manual startup type. So the fix is to set it back\n    to Disabled startup type after starting it. This allows this script to control how it starts once the MCS\n    Master Image is deployed.\n\n  Version 3.10 Updates\n  - Tweaked the updates completed under 3.9 as I experienced a timing issue. Added the Create-RegValue function\n    to assist with this.\n\n  Future Improvements:\n  - Add an option so it only starts the Citrix Desktop Service after the session host has been joined to Azure AD.\n    https:\/\/github.com\/EUCweb\/BIS-F\/issues\/387\n  - Can take different actions based on image type or manual deployment.\n    https:\/\/github.com\/megamorf\/CitrixImagingTools\/issues\/13\n\n  Script name: StartCitrixDesktopService.ps1\n  Release 3.10\n  Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 16th October 2017\n  Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 27th March 2026\n\n#&gt;\n\n#-------------------------------------------------------------\n\n# Set Powershell Compatibility Mode\nSet-StrictMode -Version 2.0\n\n# Enable verbose, warning and error mode\n$VerbosePreference = 'Continue'\n$WarningPreference = 'Continue'\n$ErrorPreference = 'Continue'\n\n$StartDTM = (Get-Date)\n\n#-------------------------------------------------------------\n\n$invalidChars = &#x5B;io.path]::GetInvalidFileNamechars() \n$datestampforfilename = ((Get-Date -format s).ToString() -replace &quot;&#x5B;$invalidChars]&quot;,&quot;-&quot;)\n\n# Get the TEMP path\n$logPath = &#x5B;System.IO.Path]::GetTempPath()\n$logPath = $logPath.Substring(0,$logPath.Length-1)\n# Note that the GetTempPath changes in Windows from February 2025 means that the SYSTEM identity returns\n# %WINDIR%\\SystemTemp by default. Note the missing slash between System and Temp. Use $env:TEMP if you\n# want it to return %WINDIR%\\System\\Temp instead.\n# Reference:\n# - https:\/\/support.microsoft.com\/en-au\/topic\/gettemppath-changes-in-windows-february-cumulative-update-preview-4cc631fb-9d97-4118-ab6d-f643cd0a7259\n\n$ScriptName = &#x5B;System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString())\n$logFile = &quot;$logPath\\$ScriptName-$($datestampforfilename).log&quot;\n\ntry {\n  Start-Transcript &quot;$logFile&quot;\n}\ncatch {\n  write-verbose &quot;$(Get-Date): This host does not support transcription&quot;\n}\n\n#-------------------------------------------------------------\n\n# Set to true to invoke a gpupdate before it reads the VDAHelper registry values.\n$InvokeGPUpdate = $True\n\n# Set to true if you want to check if there are valid Delivery Controller(s) or\n# Cloud Connectors set under the following registry values:\n# - HKLM\\SOFTWARE\\Policies\\Citrix\\VirtualDesktopAgent\\ListOfDDCs\n# - HKLM\\SOFTWARE\\Citrix\\VirtualDesktopAgent\\ListOfDDCs\n# Note that the Policies value is checked first.\n$CheckForValidity = $True\n\n# Set the number of seconds to wait between validity checks of the ListOfDDCs\n# registry value.\n$interval = 10\n\n# Set the number of times to repeat the validity before continuing.\n$RepeatValidityCheck = 25\n\n# Set to true if you want to restart the service(s) if already started.\n$StopServiceIfAlreadyStarted = $True\n\n# Create the hashtable for services to be started\n$ServicesToStart = @{}\n\n# Create an object for each service that needs to be started\n$objService = New-Object -TypeName PSObject -Property @{\n  &quot;Name&quot;\t=\t&quot;BrokerAgent&quot;\n  &quot;DisplayName&quot;\t=\t&quot;Citrix Desktop Service&quot;\n}\n# Add the object to the hashtable\n$ServicesToStart.Add($objService.Name,$objService)\n\n#-------------------------------------------------------------\n\nFunction Get-VDAHelperSettings {\n  #  This function read the values under the following registry key to determine how long to wait\n  #  before starting the Citrix Desktop Service.\n  #  - Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Citrix\\VDAHelper\n  #  - Key: HKEY_LOCAL_MACHINE\\SOFTWARE\\Citrix\\VDAHelper\n  #  Note that the values set under the Policies key have a higher priority.\n  $regKey1 = &quot;SOFTWARE\\Citrix\\VDAHelper&quot;\n  $regKey2 = &quot;SOFTWARE\\Policies\\Citrix\\VDAHelper&quot;\n  $ResultProps = @{\n    VDAHelperSettingsEnabled = 0\n    DelayDesktopServiceTime = 0\n    TriggerOnTaskEndEvent = 0\n    TaskNameFullPath = &quot;&quot;\n    MaximumTaskWaitTime = 0\n    LogonEventUserName = &quot;&quot;\n    DefaultDomainName = &quot;&quot;\n    UsePersonalityini = 0\n  }\n  $results = @()\n  # Create an instance of the Registry Object and open the HKLM base key\n  $reg = &#x5B;microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$env:computername)  \n  Try {\n    $thisRegKey = $reg.OpenSubKey($regKey1)\n    ForEach ($Key in $($ResultProps.Keys)) {\n      Try {\n        If ($null -ne $thisRegKey.GetValue($Key)) {\n          If ($thisRegKey.GetValueKind($Key) -eq &quot;String&quot;) {\n            $ResultProps.$Key = $thisRegKey.GetValue($Key).Trim()\n          } Else {\n            $ResultProps.$Key = $thisRegKey.GetValue($Key)\n          }\n        }\n      }\n      Catch &#x5B;System.Exception] {\n        #$($_.Exception.Message)\n      }\n    }\n  }\n  Catch &#x5B;System.Exception] {\n    #$($_.Exception.Message)\n  }\n  Try {\n    $thisRegKey = $reg.OpenSubKey($regKey2)\n    ForEach ($Key in $($ResultProps.Keys)) {\n      Try {\n        If ($null -ne $thisRegKey.GetValue($Key)) {\n          If ($thisRegKey.GetValueKind($Key) -eq &quot;String&quot;) {\n            $ResultProps.$Key = $thisRegKey.GetValue($Key).Trim()\n          } Else {\n            $ResultProps.$Key = $thisRegKey.GetValue($Key)\n          }\n        }\n      }\n      Catch &#x5B;System.Exception] {\n        #$($_.Exception.Message)\n      }\n    }\n  }\n  Catch &#x5B;System.Exception] {\n    #$($_.Exception.Message)\n  }\n  $results += New-Object PsObject -Property $ResultProps\n  return $results\n}\n\nFunction IsTaskValid {\n  param(&#x5B;string]$TaskNameFullPath)\n  If ($TaskNameFullPath -Match &quot;\\\\&quot;) {\n    $TaskName = $TaskNameFullPath.Split('\\')&#x5B;1]\n    $TaskRootFolder = $TaskNameFullPath.Split('\\')&#x5B;0] + &quot;\\&quot;\n  } Else {\n    $TaskName = $TaskNameFullPath\n    $TaskRootFolder = &quot;\\&quot;\n  }\n  $TaskExists = $False\n  # Create the TaskService object.\n  Try {\n    &#x5B;Object] $service = new-object -com(&quot;Schedule.Service&quot;)\n    If (!($service.Connected)){\n      Try {\n        $service.Connect()\n        $rootFolder = $service.GetFolder(&quot;$TaskRootFolder&quot;)\n        $rootFolder.GetTasks(0) | ForEach-Object {\n          If ($_.Name -eq $TaskName) {\n            $TaskExists = $True\n          } #If\n        } #ForEach\n      } #Try\n      Catch &#x5B;System.Exception]{\n        &quot;Scheduled Task Connection Failed&quot;\n      }\n    } #If\n  } #Try\n  Catch &#x5B;System.Exception]{\n    &quot;Scheduled Task Object Creation Failed&quot;\n  } #Catch\n  return $TaskExists\n}\n\nFunction IsEventLogValid {\n  param(&#x5B;string]$EventLog)\n  $EventLogExists = $False\n  Try {\n    Get-WinEvent -ListLog &quot;$EventLog&quot; -ErrorAction Stop | out-null\n    $EventLogExists = $True\n  }\n  Catch &#x5B;System.Exception]{\n    #$($_.Exception.Message)\n  }\n  return $EventLogExists\n}\n\nfunction Get-TaskEventRunTime {\n  param (\n         &#x5B;switch]$start,\n         &#x5B;switch]$end,\n         &#x5B;string]$taskPath\n        )\n  $results = @()\n  $ResultProps = @{\n    EventFound = $False\n    TimeCreated = &#x5B;datetime]&quot;1\/1\/1600&quot;\n    ErrorMessage = &quot;&quot;\n  }\n  # in case taskpath contains quotes, have to escape (double) them\n  $taskPath = $taskPath.Replace(&quot;'&quot;,&quot;''&quot;)\n  If ($Start) {\n    # fetch the most recent start event (100) event\n    $XPath = &quot;*&#x5B;System&#x5B;(EventID=100)]] and *&#x5B;EventData&#x5B;Data&#x5B;1]='&quot; + $taskPath + &quot;']]&quot;\n  }\n  If ($End) {\n    # fetch the most recent success event (102) or failed (111) event\n    $XPath = &quot;*&#x5B;System&#x5B;((EventID=102) or (EventID=111))]] and *&#x5B;EventData&#x5B;Data&#x5B;1]='&quot; + $taskPath + &quot;']]&quot;\n  }\n  Try {\n    get-winevent -LogName 'Microsoft-Windows-TaskScheduler\/Operational' -FilterXPath $XPath -MaxEvents 1 -ErrorAction Stop  | ForEach-Object{\n      $ResultProps.EventFound = $True\n      $ResultProps.TimeCreated = $_.TimeCreated\n    }\n  }\n  Catch {\n    $ResultProps.ErrorMessage = $($_.Exception.Message)\n    #$($_.Exception.Message)\n  }\n  $results += New-Object PsObject -Property $ResultProps\n  return $results\n}\n\nFunction Get-UpTime {\n  # This function will get the uptime of the machine, returning both the LastBootUpTime in Date\/Time\n  # format, and also as a TimeSpan.\n  param (\n         &#x5B;switch]$UseWinRM,\n         &#x5B;int]$WinRMTimeoutSec=30\n        )\n  $ResultProps = @{ \n    LBTime = $null\n    TimeSpan = $null\n    Success = $False\n  }\n  Try {\n    If ($UseWinRM) {\n      # LastBootUpTime from Get-CimInstance is already been converted to Date\/Time\n      $LBTime = (Get-CimInstance -ClassName Win32_OperatingSystem -OperationTimeoutSec $WinRMTimeoutSec -ErrorAction Stop).LastBootUpTime\n    } Else {\n      $LBTime = &#x5B;Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime)\n    }\n    If ($LBTime -ne $null) {\n      &#x5B;TimeSpan]$uptime = New-TimeSpan $LBTime $(get-date)\n      $ResultProps.LBTime = $LBTime\n      $ResultProps.TimeSpan = $uptime\n      $ResultProps.Success = $True\n    }\n  }\n  Catch {\n    $($_.Exception.Message)\n  }\n  return $ResultProps\n}\n\nfunction Get-LogonLogoffEvent {\n  param (\n         &#x5B;switch]$Logon,\n         &#x5B;switch]$Logoff,\n         &#x5B;string]$UserName,\n         &#x5B;string]$Event,\n         &#x5B;string]$LogonID=&quot;&quot;\n        )\n  $results = @()\n  $ResultProps = @{\n    EventFound = $False\n    TimeCreated = &#x5B;datetime]&quot;1\/1\/1600&quot;\n    LogonID = &quot;&quot;\n  }\n  If ($Logon) {\n  # Query to check that the session has logged on.\n$xmlquery=@&quot;\n&lt;QueryList&gt;\n  &lt;Query Id=&quot;0&quot; Path=&quot;Security&quot;&gt;\n    &lt;Select Path=&quot;Security&quot;&gt;\n\t\t*&#x5B;System&#x5B;(EventID='4624')]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='TargetUserName'] and (Data='$UserName')]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='LogonType'] and ((Data='2') or (Data='3'))]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='ProcessName'] and ((Data='C:\\Windows\\System32\\winlogon.exe') or (Data='C:\\Windows\\System32\\svchost.exe'))]]\n\t&lt;\/Select&gt;\n  &lt;\/Query&gt;\n&lt;\/QueryList&gt;\n&quot;@\n  }\n  If ($Logoff -AND $Event -eq &quot;4647&quot;) {\n  # Query to check that the logoff has been initiated\n$xmlquery=@&quot;\n&lt;QueryList&gt;\n  &lt;Query Id=&quot;0&quot; Path=&quot;Security&quot;&gt;\n    &lt;Select Path=&quot;Security&quot;&gt;\n\t\t*&#x5B;System&#x5B;(EventID='4647')]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='TargetUserName'] and (Data='$UserName')]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='TargetLogonId'] and (Data='$LogonID')]]\n\t&lt;\/Select&gt;\n  &lt;\/Query&gt;\n&lt;\/QueryList&gt;\n&quot;@\n  }\n  If ($Logoff -AND $Event -eq &quot;4634&quot;) {\n  # Query to check that the session has been terminated\/destroyed\n$xmlquery=@&quot;\n&lt;QueryList&gt;\n  &lt;Query Id=&quot;0&quot; Path=&quot;Security&quot;&gt;\n    &lt;Select Path=&quot;Security&quot;&gt;\n\t\t*&#x5B;System&#x5B;(EventID='4634')]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='TargetUserName'] and (Data='$UserName')]]\n\t\tand\n\t\t*&#x5B;EventData&#x5B;Data&#x5B;@Name='LogonType'] and ((Data='2') or (Data='3'))]]\n\t&lt;\/Select&gt;\n  &lt;\/Query&gt;\n&lt;\/QueryList&gt;\n&quot;@\n  }\n  Try {\n    Get-WinEvent -MaxEvents 1 -FilterXml $xmlquery -ErrorAction Stop | ForEach-Object{\n      $ResultProps.EventFound = $True\n      $ResultProps.TimeCreated = $_.TimeCreated\n      if ($_.ID -eq &quot;4624&quot;) {\n        $ResultProps.LogonID = $_.Properties&#x5B;7].Value\n      }\n      if ($_.ID -eq &quot;4647&quot;) {\n        $ResultProps.LogonID = $_.Properties&#x5B;3].Value\n      }\n      if ($_.ID -eq &quot;4634&quot;) {\n        #\n      }\n    }\n  }\n  Catch {\n    #$($_.Exception.Message)\n  }\n  $results += New-Object PsObject -Property $ResultProps\n  return $results\n}\n\nFunction Test-ScheduledTaskCompletionAfterRegistration {\n  # This function retrieves the most recent &quot;Task Registered&quot; event (Event ID 106) and then\n  # checks whether a &quot;Task Completed&quot; event (Event ID 102) occurred after that registration.\n  # It returns:\n  # - The registration time.\n  # - The most recent completion time (if any).\n  # - A Boolean indicating whether the task completed after registration.\n  # We can then use the CompletedAfterRegistration boolean value to determine if the task\n  # has run once since registration.\n  # If you delete the Scheduled Task, the Events will still be found in the\n  # &quot;Microsoft-Windows-TaskScheduler\/Operational&quot; Event Log. This can be misleading. Always\n  # pair this function with the IsTaskValid and IsEventLogValid functions to ensure you get\n  # accurate results.\n  &#x5B;CmdletBinding()]\n  param (\n         &#x5B;Parameter(Mandatory)]\n         &#x5B;string]$TaskName\n        )\n  $logName = &quot;Microsoft-Windows-TaskScheduler\/Operational&quot;\n  try {\n    # Get the latest 'Task Registered' event (Event ID 106)\n    $registeredEvent = Get-WinEvent -LogName $logName -FilterXPath &quot;*&#x5B;System&#x5B;(EventID=106)] and EventData&#x5B;Data&#x5B;@Name='TaskName']='$TaskName']]&quot; -MaxEvents 1 -ErrorAction Stop\n    if (-not $registeredEvent) {\n      Write-Warning &quot;No 'Task Registered' event found for task '$TaskName'.&quot;\n      return\n    }\n    $registeredTime = $registeredEvent.TimeCreated\n    $lastCompletedTime = $null\n    $completedAfterRegistration = $false\n    # Get recent 'Task Completed' events (Event ID 102)\n    $completedEvents = Get-WinEvent -LogName $logName -FilterXPath &quot;*&#x5B;System&#x5B;(EventID=102)] and EventData&#x5B;Data&#x5B;@Name='TaskName']='$TaskName']]&quot; -MaxEvents 100 -ErrorAction Stop\n    foreach ($event in $completedEvents) {\n      if ($event.TimeCreated -gt $registeredTime) {\n        $lastCompletedTime = $event.TimeCreated\n        $completedAfterRegistration = $true\n        break\n      }\n    }\n    &#x5B;PSCustomObject]@{\n      TaskName                   = $TaskName\n      LastRegisteredTime         = $registeredTime\n      LastCompletedTime          = $lastCompletedTime\n      CompletedAfterRegistration = $completedAfterRegistration\n    }\n  }\n  catch {\n    #&quot;Error occurred: $_&quot;\n  }\n}\n\nFunction XDPing {\n  # This function performs an XDPing to make sure the Delivery Controller or Cloud Connector is in a healthy state.\n  # It tests whether the Broker service is reachable, listening and processing requests on its configured port.\n  # We do this by issuing a blank HTTP POST requests to the Broker's Registrar service. Including &quot;Expect: 100-continue&quot;\n  # in the body will ensure we receive a respose of &quot;HTTP\/1.1 100 Continue&quot;, which is what we use to verify that it's in\n  # a healthy state.\n  # You will notice that you can also pass proxy parameters to the function. This is for test and development ONLY. I\n  # added this as I was using Fiddler to test the functionality and make sure the raw data sent was correctly formatted.\n  # I decided to leave these parameters in the function so that others can learn and understand how this works.\n  # To work out the best way to write this function I decompiled the VDAAssistant.Backend.dll from the Citrix Health\n  # Assistant tool using JetBrains decompiler.\n  # Written by Jeremy Saunders\n  param(\n    &#x5B;Parameter(Mandatory=$True)]&#x5B;String]$ComputerName, \n    &#x5B;Parameter(Mandatory=$True)]&#x5B;Int32]$Port,\n    &#x5B;String]$ProxyServer=&quot;&quot;, \n    &#x5B;Int32]$ProxyPort,\n    &#x5B;Switch]$ConsoleOutput\n  )\n  $service = &quot;http:\/\/${ComputerName}:${Port}\/Citrix\/CdsController\/IRegistrar&quot;\n  $s = &quot;POST $service HTTP\/1.1`r`nContent-Type: application\/soap+xml; charset=utf-8`r`nHost: ${ComputerName}:${Port}`r`nContent-Length: 1`r`nExpect: 100-continue`r`nConnection: Close`r`n`r`n&quot;\n  $log = New-Object System.Text.StringBuilder\n  $log.AppendLine(&quot;Attempting an XDPing against $ComputerName on TCP port number $port&quot;) | Out-Null\n  $listening = $false\n  If (&#x5B;string]::IsNullOrEmpty($ProxyServer)) {\n    $ConnectToHost = $ComputerName\n    &#x5B;int]$ConnectOnPort = $Port\n  } Else {\n    $ConnectToHost = $ProxyServer\n    &#x5B;int]$ConnectOnPort = $ProxyPort\n    $log.AppendLine(&quot;- Connecting via a proxy: ${ProxyServer}:${ProxyPort}&quot;) | Out-Null\n  }\n  try {\n    $socket = New-Object System.Net.Sockets.Socket (&#x5B;System.Net.Sockets.AddressFamily]::InterNetwork, &#x5B;System.Net.Sockets.SocketType]::Stream, &#x5B;System.Net.Sockets.ProtocolType]::Tcp)\n    try {\n      $socket.Connect($ConnectToHost,$ConnectOnPort)\n      if ($socket.Connected) {\n        $log.AppendLine(&quot;- Socket connected&quot;) | Out-Null\n        $bytes = &#x5B;System.Text.Encoding]::ASCII.GetBytes($s)\n        $socket.Send($bytes) | Out-Null\n        $log.AppendLine(&quot;- Sent the data&quot;) | Out-Null\n        $numArray = New-Object byte&#x5B;] 21\n        $socket.ReceiveTimeout = 5000\n        $socket.Receive($numArray) | Out-Null\n        $log.AppendLine(&quot;- Received the following 21 byte array: &quot; + &#x5B;BitConverter]::ToString($numArray)) | Out-Null\n        $strASCII = &#x5B;System.Text.Encoding]::ASCII.GetString($numArray)\n        $strUTF8 = &#x5B;System.Text.Encoding]::UTF8.GetString($numArray)\n        $log.AppendLine(&quot;- Converting to ASCII: `&quot;$strASCII`&quot;&quot;) | Out-Null\n        $log.AppendLine(&quot;- Converting to UTF8: `&quot;$strUTF8`&quot;&quot;) | Out-Null\n        $socket.Send(&#x5B;byte&#x5B;]](32)) | Out-Null\n        $log.AppendLine(&quot;- Sent a single byte with the value 32 (which represents the ASCII space character) to the connected socket.&quot;) | Out-Null\n        $log.AppendLine(&quot;- This is done to gracefully signal the end of the communication.&quot;) | Out-Null\n        $log.AppendLine(&quot;- This ensures it does not block\/consume unnecessary requests needed by VDAs.&quot;) | Out-Null\n        if ($strASCII.Trim().StartsWith(&quot;HTTP\/1.1 100 Continue&quot;, &#x5B;System.StringComparison]::CurrentCultureIgnoreCase)) {\n          $listening = $true\n          $log.AppendLine(&quot;- The service is listening and healthy&quot;) | Out-Null\n        } else {\n          $log.AppendLine(&quot;- The service is not listening&quot;) | Out-Null\n        }\n        try {\n          $socket.Close()\n          $log.AppendLine(&quot;- Socket closed&quot;) | Out-Null\n        } catch {\n          $log.AppendLine(&quot;- Failed to close socket&quot;) | Out-Null\n          $log.AppendLine(&quot;- ERROR: $_&quot;) | Out-Null\n        }\n        $socket.Dispose()\n      } else {\n        $log.AppendLine(&quot;- Socket failed to connect&quot;) | Out-Null\n      }\n    } catch {\n      $log.AppendLine(&quot;- Failed to connect to service&quot;) | Out-Null\n      $log.AppendLine(&quot;- ERROR: $_&quot;) | Out-Null\n    }\n  } catch {\n    $log.AppendLine(&quot;- Failed to create socket&quot;) | Out-Null\n    $log.AppendLine(&quot;- ERROR: $_&quot;) | Out-Null\n  }\n  If ($ConsoleOutput) {\n    Write-Host $log.ToString().TrimEnd()\n  }\n  return $listening\n}\n\nFunction Delete-BrokerAgentCachedData {\n  # This function will delete the SavedListOfDDCsSids.xml\n  # The SavedListOfDDCsSids.xml file can onlt be accessed and deleted by the SYSTEM account. Therefore,\n  # this function will only work as expected when this script is run as a scheduled task.\n  param (\n         &#x5B;int]$WinRMTimeoutSec=30\n        )\n  Try {\n    $PersistentDataLocation = (Get-CimInstance -Namespace 'root\\citrix\\desktopinformation' -ClassName 'Citrix_VirtualDesktopInfo' -OperationTimeoutSec $WinRMTimeoutSec -ErrorAction Stop).PersistentDataLocation\n    If (!&#x5B;string]::IsNullOrEmpty($PersistentDataLocation)) {\n      If (Test-Path -path &quot;$PersistentDataLocation&quot;) {\n        $BrokerAgentInfoFolder = Join-Path $PersistentDataLocation 'BrokerAgentInfo'\n        $targets = @('SaveListOfDdcsSids.xml','ListOfSIDs.xml') | ForEach-Object { Join-Path $BrokerAgentInfoFolder $_ }\n        ForEach ($target in $targets) {\n          If (Test-Path -path &quot;$target&quot;) {\n            write-verbose &quot;$(Get-Date): Removing the `&quot;$target`&quot; file&quot; -verbose\n            Remove-Item $target -Force -ErrorAction SilentlyContinue\n          }\n        }\n      }\n    }\n  }\n  Catch {\n    #$($_.Exception.Message)\n  }\n}\n\nFunction Get-ListOfDDCs {\n  # This function will get the list of DDCs from either the Registry or the MCSPersonality.ini\/Personality.ini,\n  # depending on the value of the UsePersonalityini parameter.\n  # You can get the list from either the Registry or ini file. I was initially considering prioritising it, but\n  # felt that this would be too confusing and lead to potential issues.\n  # If it gets the list from the registry, it will prioritise getting it from the Policy-based (LGPO or GPO) key\n  # over the Registry-based (manual, GPP, specified during VDA installation) key.\n  # Even though Citrix changed MCS images from Personality.ini to MCSPersonality.ini from VDA 2303, this function\n  # will check for both for legacy reasons.\n  # At least 1 controller in the list of DDCs must return True from the XDPing function for the IsServiceListening\n  # property to be set to True.\n  # References:\n  # - https:\/\/docs.citrix.com\/en-us\/citrix-virtual-apps-desktops\/manage-deployment\/vda-registration.html\n  # - https:\/\/portal.nutanix.com\/page\/documents\/solutions\/details?targetId=RA-2018-Citrix-Virtual-Apps-and-Desktops-Service-NCA-Disaster-Recovery:citrix-virtual-delivery-agent-registration.html\n  param (\n         &#x5B;switch]$UsePersonalityini\n        )\n  #----------------- Set Variables ------------------\n  $UseRegistry = $False\n  If ($UsePersonalityini -eq $False) {\n    $UseRegistry = $True\n  }\n  $DeliveryControllers = &quot;&quot;\n  $Port = 80\n  $IsServiceListening = $False\n  $ListOfDDCsValue = &quot;ListOfDDCs&quot;\n  $ListOfDDCsPropertyExist = $False\n  $ListOfDDCsValueExist = $False\n  $ControllerRegistrarPortValue = &quot;ControllerRegistrarPort&quot;\n  $ControllerRegistrarPortValueExists = $False\n  $RegPath = &quot;&quot;\n  $RegPath1 = &quot;HKLM:\\SOFTWARE\\Policies\\Citrix\\VirtualDesktopAgent&quot;\n  $RegPath2 = &quot;HKLM:\\SOFTWARE\\Citrix\\VirtualDesktopAgent&quot;\n  $FilePath = &quot;&quot;\n  #---------------- Process Registry ----------------\n  If ($UseRegistry) {\n    $ErrorActionPreference = &quot;stop&quot;\n    try {\n      If ((Get-ItemProperty -Path &quot;$RegPath1&quot; | Select-Object -ExpandProperty &quot;$ListOfDDCsValue&quot;) -ne $null) {\n        $ListOfDDCsPropertyExist = $True\n        $ListOfDDCsValueExist = $True\n        $RegPath = $RegPath1\n      }\n      If ((Get-ItemProperty -Path &quot;$RegPath1&quot; | Select-Object -ExpandProperty &quot;$ControllerRegistrarPortValue&quot;) -ne $null) {\n        $ControllerRegistrarPortValueExists = $True\n      }\n    }\n    catch {\n      #\n    }\n    $ErrorActionPreference = &quot;Continue&quot;\n    If (&#x5B;String]::IsNullOrEmpty($RegPath)) {\n      $ErrorActionPreference = &quot;stop&quot;\n      try {\n        If ((Get-ItemProperty -Path &quot;$RegPath2&quot; | Select-Object -ExpandProperty &quot;$ListOfDDCsValue&quot;) -ne $null) {\n          $ListOfDDCsPropertyExist = $True\n          $ListOfDDCsValueExist = $True\n          $RegPath = $RegPath2\n        }\n        If ((Get-ItemProperty -Path &quot;$RegPath2&quot; | Select-Object -ExpandProperty &quot;$ControllerRegistrarPortValue&quot;) -ne $null) {\n          $ControllerRegistrarPortValueExists = $True\n        }\n      }\n      catch {\n        #\n      }\n      $ErrorActionPreference = &quot;Continue&quot;\n    }\n  }\n  #--------------- Process ini Files ----------------\n  If ($UsePersonalityini) {\n    # Due to VDA upgrades and the change from Personality.ini to MCSPersonality.ini from VDA 2303, both ini files can potentially exist in the root of the C drive.\n    # The assumption is that the one that was last written to is the live ini file.\n    $FilePath = Get-ChildItem -Path &quot;${env:SYSTEMDRIVE}\\&quot; -Filter '*Personality.ini' | Where-Object {$_.Extension -eq &quot;.ini&quot;} | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1\n    If ($null -ne $FilePath) {\n      $Personalityini = (Get-Content -Path $FilePath.FullName) -match 'ListOfDDCs='\n      If ($Personalityini -ne $null) {\n        $ListOfDDCsPropertyExist = $True\n        $DeliveryControllers = ($Personalityini | Select-String &quot;$ListOfDDCsValue&quot; | ForEach-Object {$_.Line}).split('=')&#x5B;1]\n        If (!&#x5B;string]::IsNullOrEmpty($DeliveryControllers)) {\n          $ListOfDDCsValueExist = $True\n        }\n      }\n    }\n  }\n  #----------------- Process output -----------------\n  If ($ListOfDDCsValueExist) {\n    If ($UseRegistry) {\n      $DeliveryControllers = Get-ItemProperty -Path &quot;$RegPath&quot; | Select-Object -ExpandProperty &quot;$ListOfDDCsValue&quot;\n      If ($ControllerRegistrarPortValueExists) {\n        $Port = Get-ItemProperty -Path &quot;$RegPath&quot; | Select-Object -ExpandProperty &quot;$ControllerRegistrarPortValue&quot;\n      }\n    }\n    If (!&#x5B;string]::IsNullOrEmpty($DeliveryControllers)) {\n      $delimiters = &quot;&#x5B;, ]+&quot; # Splits by comma or space, handling multiple delimiters\n      $DeliveryControllers -split $delimiters | ForEach{\n        # Check to to make sure it is an FQDN, so contains at least 1 decimal point\n        $charCount = ($_.ToCharArray() | Where-Object {$_ -eq '.'} | Measure-Object).Count\n        If ($charCount -gt 0) {\n          # Check that at least 1 Delivery Controller is valid\n          $TestConnection = (XDPing -ComputerName &quot;$_&quot; -Port $Port)\n          If ($TestConnection) { $IsServiceListening = $True }\n        }\n      }\n    } Else {\n      $DeliveryControllers = &quot;Not found&quot;\n    }\n  } Else {\n    $DeliveryControllers = &quot;Not found&quot;\n  }\n  $results = @()\n  $ResultProps = @{\n    UseRegistry = $UseRegistry\n    RegPath = $RegPath.replace(&quot;HKLM:\\&quot;,&quot;HKLM\\&quot;)\n    UsePersonalityini = $UsePersonalityini\n    FilePath = $FilePath\n    ListOfDDCsPropertyExist = $ListOfDDCsPropertyExist\n    ListOfDDCsValueExist = $ListOfDDCsValueExist\n    DeliveryControllers = $DeliveryControllers\n    Port = $Port\n    IsServiceListening = $IsServiceListening\n  }\n  $results += New-Object PsObject -Property $ResultProps\n  return $results\n}\n\nFunction Create-RegValue {\n  param (\n         &#x5B;String]$Path,\n         &#x5B;String]$Value,\n         &#x5B;String]$Data,\n         &#x5B;String]$Type\n        )\n  $ValueExists = $False\n  $ErrorActionPreference = &quot;stop&quot;\n  try {\n    If ((Get-ItemProperty -Path &quot;$Path&quot; | Select-Object -ExpandProperty &quot;$Value&quot;) -ne $null) {\n      $ValueExists = $True\n    }\n  }\n  catch {\n    #\n  }\n  $ErrorActionPreference = &quot;Continue&quot;\n  If ($ValueExists) {\n    write-verbose &quot;- Setting the $Value registry value to $Data&quot; -verbose\n    Set-ItemProperty -Path &quot;$Path&quot; -Name &quot;$Value&quot; -Type $Type -Value $Data -Force\n  } Else {\n    write-verbose &quot;- Creating the $Value registry value and setting it to $Data&quot; -verbose\n    New-ItemProperty -Path &quot;$Path&quot; -Name &quot;$Value&quot; -PropertyType $Type -Value $Data | Out-Null\n  }\n}\n\n#-------------------------------------------------------------\n\n$ServiceExists = $False\nif (Get-Service -Name &quot;BrokerAgent&quot; -ErrorAction SilentlyContinue) {\n  $ServiceExists = $True\n  write-verbose &quot;$(Get-Date): The `&quot;Citrix Desktop Service`&quot; service was found&quot; -verbose\n} else {\n  write-warning &quot;$(Get-Date): The `&quot;Citrix Desktop Service`&quot; service does not exist&quot; -verbose\n}\n\n$IsMCSMasterImage = $False\n$ErrorActionPreference = &quot;stop&quot;\ntry {\n  If ((Get-ItemProperty -Path &quot;HKLM:\\SOFTWARE\\Citrix\\Configuration&quot; | Select-Object -ExpandProperty &quot;MasterImage&quot;) -ne $null) {\n    &#x5B;int]$MasterImageValue = (Get-ItemProperty -Path &quot;HKLM:\\SOFTWARE\\Citrix\\Configuration&quot; -Name &quot;MasterImage&quot;).MasterImage\n    If ($MasterImageValue -eq 1) {\n      $IsMCSMasterImage = $True\n    }\n  }\n}\ncatch {\n  #\n}\n$ErrorActionPreference = &quot;Continue&quot;\n\nIf ($IsMCSMasterImage -eq $False) {\n\n  # Get the Operating System Major, Minor, Build and Revision Version Numbers\n  $OSVersion = &#x5B;System.Environment]::OSVersion.Version\n  &#x5B;int]$OSMajorVer = $OSVersion.Major\n  &#x5B;int]$OSMinorVer = $OSVersion.Minor\n  &#x5B;int]$OSBuildVer = $OSVersion.Build\n  # Get the UBR (Update Build Revision) from the registry so that the full build number\n  # is the same as the output of of the ver command line.\n  &#x5B;int]$OSRevisionVer = 0\n  $Path = &quot;HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion&quot;\n  $ValueExists = $False\n  $ErrorActionPreference = &quot;stop&quot;\n  try {\n    If ((Get-ItemProperty -Path &quot;$Path&quot; | Select-Object -ExpandProperty &quot;UBR&quot;) -ne $null) {\n      $ValueExists = $True\n    }\n  }\n  catch {\n    #\n  }\n  $ErrorActionPreference = &quot;Continue&quot;\n  If ($ValueExists) {\n    &#x5B;int]$OSRevisionVer = (Get-ItemProperty -Path &quot;$Path&quot; -Name UBR).UBR\n  }\n\n  If ($InvokeGPUpdate) {\n    # Update group policy.\n    write-verbose &quot;$(Get-Date): Invoking a Group Policy Update...&quot; -verbose\n    $gpupdate = Invoke-Command { gpupdate.exe \/force }\n    # Split up the results between Computer Policy and User Policy\n    $computerResult = $gpupdate | Select-String &quot;Computer Policy&quot;\n    If ($computerResult | Select-String &quot;errors&quot;) {\n      write-warning &quot;$(Get-Date): - Group Policy Update failed!&quot; -verbose\n    } Else {\n      write-verbose &quot;$(Get-Date): - Group Policy Update was successful.&quot; -verbose\n    }\n  }\n\n  #-------------------------------------------------------------\n\n  $VDAHelperSettings = Get-VDAHelperSettings\n  write-verbose &quot;$(Get-Date): The VDA Helper settings are:&quot; -verbose\n  $VDAHelperSettingsEnabled = $VDAHelperSettings.VDAHelperSettingsEnabled\n  write-verbose &quot;$(Get-Date): - VDAHelperSettingsEnabled: $VDAHelperSettingsEnabled (0=False,1=True)&quot; -verbose\n  $TriggerOnTaskEndEvent = $VDAHelperSettings.TriggerOnTaskEndEvent\n  write-verbose &quot;$(Get-Date): - TriggerOnTaskEndEvent: $TriggerOnTaskEndEvent (0=False,1=True)&quot; -verbose\n  $TaskNameFullPath = $VDAHelperSettings.TaskNameFullPath\n  write-verbose &quot;$(Get-Date): - TaskNameFullPath: $TaskNameFullPath&quot; -verbose\n  $DelayDesktopServiceTime = $VDAHelperSettings.DelayDesktopServiceTime\n  write-verbose &quot;$(Get-Date): - DelayDesktopServiceTime: $DelayDesktopServiceTime seconds&quot; -verbose\n  $MaximumTaskWaitTime = $VDAHelperSettings.MaximumTaskWaitTime\n  write-verbose &quot;$(Get-Date): - MaximumTaskWaitTime: $MaximumTaskWaitTime seconds&quot; -verbose\n  $LogonEventUserName = $VDAHelperSettings.LogonEventUserName\n  write-verbose &quot;$(Get-Date): - LogonEventUserName: $LogonEventUserName&quot; -verbose\n  $DefaultDomainName = $VDAHelperSettings.DefaultDomainName\n  write-verbose &quot;$(Get-Date): - DefaultDomainName: $DefaultDomainName&quot; -verbose\n  $UsePersonalityini = $VDAHelperSettings.UsePersonalityini\n  write-verbose &quot;$(Get-Date): - UsePersonalityini: $UsePersonalityini (0=False,1=True)&quot; -verbose\n  # To avoid any issues using passing UsePersonalityini as a parameter for the Get-ListOfDDCs function, we cast it to &#x5B;bool].\n  # - &#x5B;bool]0 evaluates to $false\n  # - &#x5B;bool]1 (or any non-zero value) evaluates to $true\n  $UsePersonalityini = &#x5B;bool]$UsePersonalityini\n\n  If ($TriggerOnTaskEndEvent) {\n    $AutoAdminLogon = &quot;0&quot;\n    $ErrorActionPreference = &quot;stop&quot;\n    try {\n      If ((Get-ItemProperty -Path &quot;HKLM:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon&quot; | Select-Object -ExpandProperty &quot;AutoAdminLogon&quot;) -ne $null) {\n        &#x5B;string]$AutoAdminLogon = (Get-ItemProperty -Path &quot;HKLM:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon&quot; -Name &quot;AutoAdminLogon&quot;).AutoAdminLogon\n      }\n    }\n    catch {\n      #\n    }\n    $ErrorActionPreference = &quot;Continue&quot;\n    If ($AutoAdminLogon -eq &quot;0&quot;) {\n      write-verbose &quot;$(Get-Date): - Disabled the TriggerOnTaskEndEvent because AutoAdminLogon is disabled.&quot; -verbose\n      $TriggerOnTaskEndEvent = 0\n    }\n  }\n\n  # There may be circumstances when WinRM fails if is being reconfigured and or restarted at computer startup.\n  # This may happen if there is a conflicting startup script running in your environment. So we just loop until\n  # it's successful.\n  $LastBootTime = $null\n  Do {\n    $GetUpTime = Get-UpTime -UseWinRM:$True\n    If ($GetUpTime.Success) {\n      $LastBootTime = $GetUpTime.LBTime\n    } Else {\n      write-warning &quot;$(Get-Date): Could not retrieve the LastBootTime. Will try again in 1 second!&quot; -verbose\n      Start-Sleep -Milliseconds 1000\n    }\n  } Until ($GetUpTime.Success)\n  write-verbose &quot;$(Get-Date): This host was last booted on $LastBootTime&quot; -verbose\n\n  If ($CheckForValidity -AND $ServiceExists) {\n    $SkippingValidationTest = $False\n    $DeliveryControllers = &quot;&quot;\n    $i = 0\n    Do {\n      If ($UsePersonalityini -eq $False) {\n        write-verbose &quot;$(Get-Date): Checking to see if there is a valid DDCs set under the following registry values:&quot; -verbose\n        write-verbose &quot;$(Get-Date): - `&quot;HKLM\\SOFTWARE\\Policies\\Citrix\\VirtualDesktopAgent\\ListOfDDCs`&quot;&quot; -verbose\n        write-verbose &quot;$(Get-Date): - `&quot;HKLM\\SOFTWARE\\Citrix\\VirtualDesktopAgent\\ListOfDDCs`&quot;&quot; -verbose\n      } Else {\n        write-verbose &quot;$(Get-Date): Checking to see if there is a valid DDCs set in the following ini files:&quot; -verbose\n        write-verbose &quot;$(Get-Date): - `&quot;${env:SYSTEMDRIVE}\\Personality.ini`&quot;&quot; -verbose\n        write-verbose &quot;$(Get-Date): - `&quot;${env:SYSTEMDRIVE}\\MCSPersonality.ini`&quot;&quot; -verbose\n      }\n      $IsValid = Get-ListOfDDCs -UsePersonalityini:$UsePersonalityini\n\n      # If the Personality.ini does not contain a ListOfDDCs Property, it's not an MCS image\n      # that leverages the Personality.ini, so we skip the validation tests and break out of\n      # the Do Until loop.\n      If ($UsePersonalityini -AND $IsValid.ListOfDDCsPropertyExist -eq $False) {\n       write-verbose &quot;$(Get-Date): - The host does not contain the ListOfDDCs Property in the Personality.ini&quot; -verbose\n       $SkippingValidationTest = $True\n       break\n      }\n\n      $DeliveryControllers = $IsValid.DeliveryControllers\n      $Port = $IsValid.Port\n      If (&#x5B;String]::IsNullOrEmpty($DeliveryControllers) -OR $DeliveryControllers -eq &quot;Not found&quot;) { $DeliveryControllers = &quot;none set&quot; }\n      If ($UsePersonalityini -eq $False) {\n        write-verbose &quot;$(Get-Date): - Values found under `&quot;$($IsValid.RegPath)`&quot;&quot; -verbose\n      } Else {\n        write-verbose &quot;$(Get-Date): - Values found under `&quot;$($IsValid.FilePath)`&quot;&quot; -verbose\n      }\n      write-verbose &quot;$(Get-Date): - Attempting an XDPing to Delivery Controller(s) or Cloud Connector(s): $DeliveryControllers&quot; -verbose\n      write-verbose &quot;$(Get-Date): - Using TCP port: $Port&quot; -verbose\n      write-verbose &quot;$(Get-Date): - Note that at least one Delivery Controller or Cloud Connector must test successfully.&quot; -verbose\n      If (!$IsValid.IsServiceListening) {\n        write-verbose &quot;$(Get-Date): - XDPing failed&quot; -verbose\n        write-verbose &quot;$(Get-Date): - Will check again in $interval seconds&quot; -verbose\n        $i++\n        If ($i -eq $RepeatValidityCheck) { break }\n        Start-Sleep -Seconds $interval\n      }\n    } Until ($IsValid.IsServiceListening -eq $True)\n    If ($IsValid.IsServiceListening) {\n      write-verbose &quot;$(Get-Date): - XDPing successful&quot; -verbose\n    } Else {\n      If ($SkippingValidationTest) {\n        write-verbose &quot;$(Get-Date): - Continuing without validating Delivery Controller(s) or Cloud Connector(s): $DeliveryControllers&quot; -verbose\n      } Else {\n        write-verbose &quot;$(Get-Date): - Continuing with invalid Delivery Controller(s) or Cloud Connector(s): $DeliveryControllers&quot; -verbose\n      }\n    }\n  }\n\n  $SetDefaultDomainName = $False\n  $TaskExists = $False\n  $EventLogExists = $False\n  If (!&#x5B;string]::IsNullOrEmpty($TaskNameFullPath)) {\n    $TaskExists = IsTaskValid -TaskName:&quot;$TaskNameFullPath&quot;\n    $EventLog = &quot;Microsoft-Windows-TaskScheduler\/Operational&quot;\n    $EventLogExists = IsEventLogValid -EventLog:&quot;$EventLog&quot;\n    If ($EventLogExists -eq $False) {\n      write-warning &quot;The `&quot;$EventLog`&quot; Event Log does not&quot; -verbose\n      write-warning &quot;exist. This may be because there's a new disk attached to the virtual&quot; -verbose\n      write-warning &quot;machine and the Event Logs haven't finished being moved to the new&quot; -verbose\n      write-warning &quot;location. If this is the case, restarting the Virtual Machine again&quot; -verbose\n      write-warning &quot;should address this issue.&quot; -verbose\n    }\n  }\n\n  If ($VDAHelperSettingsEnabled -AND $ServiceExists) {\n\n    If ($LastBootTime -gt (Get-Date)) {\n      If ($TriggerOnTaskEndEvent) {\n        $TriggerOnTaskEndEvent = $False\n        write-warning &quot;$(Get-Date -format &quot;dd\/MM\/yyyy HH:mm:ss&quot;): The last boot up time is greater than the current time. This means that we are unable to trigger on a task event because we cannot accurately correlate the Event logs due to not having a valid starting point.&quot; -verbose\n      }\n    }\n\n    If ($TriggerOnTaskEndEvent -AND $TaskExists -AND $EventLogExists) {\n      write-verbose &quot;$(Get-Date): The `&quot;$TaskNameFullPath`&quot; task is valid&quot; -verbose\n      $StartPhase = (Get-Date)\n      $ValidStart = $False\n      $ValidEnd = $False\n      $ValidLogoffInitiated = $False\n      $ValidLogoffTerminated = $False\n      Do {\n        $TaskLastStartTime = (Get-TaskEventRunTime -Start -Taskpath &quot;$TaskNameFullPath&quot;).TimeCreated\n        write-verbose &quot;$(Get-Date): The task last started on $TaskLastStartTime&quot; -verbose\n        If ($LastBootTime -lt $TaskLastStartTime) {\n          write-verbose &quot;$(Get-Date): - The task has started.&quot; -verbose\n          $ValidStart = $True\n        } Else {\n          write-verbose &quot;$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the task to start after the reboot...&quot; -verbose\n        }\n        If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {\n          $ValidStart = $True\n          $ValidEnd = $True\n        }\n        Start-Sleep -Seconds 1\n      } Until ($ValidStart -eq $True)\n      Do {\n        $TaskLastEndTime = (Get-TaskEventRunTime -End -Taskpath &quot;$TaskNameFullPath&quot;).TimeCreated\n        write-verbose &quot;$(Get-Date): The task last finished on $TaskLastEndTime&quot; -verbose\n        If ($LastBootTime -lt $TaskLastEndTime -AND $TaskLastStartTime -lt $TaskLastEndTime){\n          write-verbose &quot;$(Get-Date): - The task has ended.&quot; -verbose\n          $ValidEnd = $True\n        } Else {\n          write-verbose &quot;$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the task to end...&quot; -verbose\n        }\n        If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {\n          $ValidEnd = $True\n        }\n        Start-Sleep -Seconds 1\n      } Until ($ValidEnd -eq $True)\n\n      # Confirming that the session has logged on.\n      $LogonEvent = Get-LogonLogoffEvent -Logon -UserName:&quot;$LogonEventUserName&quot;\n      If ($LogonEvent.EventFound) {\n        write-verbose &quot;$(Get-Date): A valid logon event was found for the `&quot;$LogonEventUserName`&quot; user account on $($LogonEvent.TimeCreated)&quot; -verbose\n        Do {\n          # Confirming that the logoff has been initiated.\n          $LogoffEvent4647 = Get-LogonLogoffEvent -Logoff -Event:&quot;4647&quot; -UserName:&quot;$LogonEventUserName&quot; -LogonID:&quot;$($LogonEvent.LogonID)&quot;\n          If ($LogonEvent.TimeCreated -lt $LogoffEvent4647.TimeCreated){\n            write-verbose &quot;$(Get-Date): - The logoff was initiated on $($LogoffEvent4647.TimeCreated)&quot; -verbose\n            $ValidLogoffInitiated = $True\n          } Else {\n            write-verbose &quot;$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the account to logoff...&quot; -verbose\n          }\n          If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {\n            $ValidLogoffInitiated = $True\n            $ValidLogoffTerminated = $True\n          }\n          Start-Sleep -Seconds 1\n        } Until ($ValidLogoffInitiated -eq $True)\n        Do {\n          # Confirming that the session has been terminated\/destroyed.\n          For ($i=1; $i -le 10) {\n            If ($OSMajorVer -eq 6 -AND $OSMinorVer -eq 1) {\n              $LogoffEvent4634 = Get-LogonLogoffEvent -Logoff -Event:&quot;4634&quot; -UserName:&quot;${env:computername}$&quot;\n              $i = 10\n            } Else {\n              $LogoffEvent4634 = Get-LogonLogoffEvent -Logoff -Event:&quot;4634&quot; -UserName:&quot;DWM-$i&quot;\n            }\n            If ($LogoffEvent4647.TimeCreated -le $LogoffEvent4634.TimeCreated){\n              write-verbose &quot;$(Get-Date): - The logoff for user DWM-$i was terminated on $($LogoffEvent4634.TimeCreated)&quot; -verbose\n              $ValidLogoffTerminated = $True\n            } Else {\n              write-verbose &quot;$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the account to logoff...&quot; -verbose\n            }\n            If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {\n              $ValidLogoffTerminated = $True\n            }\n            If ($ValidLogoffTerminated) {\n              Break\n            }\n            $i++\n          }\n          Start-Sleep -Seconds 1\n        } Until ($ValidLogoffTerminated -eq $True)\n        # Disable the TriggerOnTaskEndEvent to reduce further delay waiting for an event that will never run.\n        $TriggerOnTaskEndEvent = 0\n      } Else {\n        write-verbose &quot;$(Get-Date): No valid logon event was found for the `&quot;$LogonEventUserName`&quot; user account.&quot; -verbose\n      }\n    } Else {\n      If ($TriggerOnTaskEndEvent) {\n        If ($TaskExists -eq $False) {\n          write-warning &quot;The `&quot;$TaskNameFullPath`&quot; task does not exist&quot; -verbose\n        }\n      }\n      write-verbose &quot;$(Get-Date): Delaying the starting of the Citrix Desktop Service for $DelayDesktopServiceTime seconds&quot; -verbose\n      Start-Sleep -s $DelayDesktopServiceTime\n    }\n  }\n\n  $SetDefaultDomainName = $True\n  If ($TriggerOnTaskEndEvent) {\n    If (!&#x5B;string]::IsNullOrEmpty($TaskNameFullPath)) {\n      If ($TaskExists -AND $EventLogExists) {\n        $HasTaskCompleteOnce = Test-ScheduledTaskCompletionAfterRegistration -TaskName:&quot;$TaskNameFullPath&quot;\n        If ($VDAHelperSettingsEnabled -AND $HasTaskCompleteOnce.CompletedAfterRegistration -eq $False) {\n          $SetDefaultDomainName = $False\n          Write-Verbose &quot;$(Get-Date): The `&quot;$TaskNameFullPath`&quot; Scheduled Task has not completed.&quot; -verbose\n          If (!(&#x5B;String]::IsNullOrEmpty($DefaultDomainName))) {\n            $WaitTimeInMinutes = 10\n            Write-Verbose &quot;$(Get-Date): - waiting up to $($WaitTimeInMinutes) minutes for the Scheduled Task to run and complete.&quot; -verbose\n            $i = 0\n            Do {\n              $HasTaskCompleted = Test-ScheduledTaskCompletionAfterRegistration -TaskName:&quot;$TaskNameFullPath&quot;\n              Start-Sleep -s 60\n              $i = $i + 1\n              If ($i -eq $WaitTimeInMinutes) {\n                $HasTaskCompleted = $True\n              }\n            } Until ($HasTaskCompleted -eq $True)\n          }\n        }\n      }\n    }\n  }\n  $WinlogonPath = &quot;HKLM:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon&quot;\n  If (!(&#x5B;String]::IsNullOrEmpty($DefaultDomainName))) {\n    If ($SetDefaultDomainName) {\n      Write-Verbose &quot;$(Get-Date): Setting the DefaultDomainName Winlogon value to `&quot;$DefaultDomainName`&quot;&quot; -verbose\n      Set-ItemProperty -Path $WinlogonPath -Name &quot;DefaultDomainName&quot; -Value &quot;$DefaultDomainName&quot; -Type STRING -Force\n      Write-Verbose &quot;$(Get-Date): Setting the AutoAdminLogon registry value to 0.&quot; -verbose\n      Set-ItemProperty -Path $WinlogonPath -Name AutoAdminLogon -Value 0 -Type STRING -Force\n      Write-Verbose &quot;$(Get-Date): Clearing the DefaultUserName registry value.&quot; -verbose\n      Set-ItemProperty -Path $WinlogonPath -Name DefaultUserName -Value &quot;&quot; -Type STRING -Force\n    } Else {\n      Write-Verbose &quot;$(Get-Date): The DefaultDomainName Winlogon registry value will not be set.&quot; -verbose\n    }\n  } Else {\n    Write-Verbose &quot;$(Get-Date): DefaultDomainName does not have a value set under the VDAHelper registry key.&quot; -verbose\n  }\n\n} Else {\n  Write-Warning &quot;$(Get-Date): This Citrix VDA is the MCS Master Iamge.&quot; -verbose\n  Write-Warning &quot;$(Get-Date): The Broker service will be started immediately to avoid issues during the image preparation process.&quot; -verbose\n}\n\nIf ($ServiceExists) {\n  Delete-BrokerAgentCachedData\n  ForEach ($key in $ServicesToStart.keys) {\n    $ServiceName = $ServicesToStart.$key.Name\n    $ServiceDisplayName = $ServicesToStart.$key.DisplayName\n\n    write-verbose &quot;$(Get-Date): Setting the `&quot;$ServiceDisplayName`&quot; service to Manual start type...&quot; -verbose\n\n    # Possible results using the sc.exe command line tool:\n    # &#x5B;SC] ChangeServiceConfig SUCCESS\n    # &#x5B;SC] OpenSCManager FAILED 5:  Access is denied.\n    # &#x5B;SC] OpenSCManager FAILED 1722:  The RPC server is unavailable.&quot; --&gt; Computer shutdown\n    # &#x5B;SC] OpenService FAILED 1060:  The specified service does not exist as an installed service.&quot; --&gt; Service not installed\n\n    Invoke-Command {cmd \/c sc.exe config &quot;$ServiceName&quot; start= demand} | out-null\n\n    If ($StopServiceIfAlreadyStarted) {\n      # Stop the service\n      $objservice = Get-Service &quot;$ServiceName&quot; -ErrorAction SilentlyContinue\n      If ($objService) {\n        If ($objService.Status -eq &quot;Running&quot;) {\n          write-verbose &quot;$(Get-Date): Stopping the service...&quot; -verbose\n          stop-service -name &quot;$ServiceName&quot; -verbose\n          do { \n              Start-sleep -s 5\n              write-verbose &quot;$(Get-Date): - waiting for the service to stop&quot; -verbose\n             }  \n          until ((get-service &quot;$ServiceName&quot;).Status -eq &quot;Stopped&quot;)\n          write-verbose &quot;$(Get-Date): - the service has stopped&quot; -verbose\n        } Else {\n          If ($objService.Status -eq &quot;Stopped&quot;) {\n            write-verbose &quot;$(Get-Date): The service is not running&quot; -verbose\n          }\n        }\n      } Else {\n        write-verbose &quot;$(Get-Date): The `&quot;$ServiceDisplayName`&quot; service does not exist&quot; -verbose\n      }\n    }\n\n    # Start the Service\n    $objservice = Get-Service &quot;$ServiceName&quot; -ErrorAction SilentlyContinue\n    If ($objService) {\n      If ($objService.Status -eq &quot;Stopped&quot;) {\n        write-verbose &quot;$(Get-Date): Starting the service...&quot; -verbose\n        start-service -name &quot;$ServiceName&quot; -verbose\n        If ($IsMCSMasterImage) {\n          If ($ServiceName -eq &quot;BrokerAgent&quot;) {\n            # We cannot set the Citrix Desktop Service (BrokerAgent) service back to Disabled until it's been started. However, if\n            # the MCS image preparation process completes and starts to shutdown the machine whilst the service is still starting, the\n            # command to disable the service will not run and therefore leave the service set to Manual start type. This is a timing\n            # issue. So the trick here is to immediately set the service to disabled via the registry, which won't interfere with the\n            # service starting, and will ensure that it is disabled in the image.\n            write-verbose &quot;$(Get-Date): Setting the `&quot;$ServiceDisplayName`&quot; service to Disabled start type so that this script will control how it starts once the MCS Master Image is deployed...&quot; -verbose\n            Create-RegValue -Path:&quot;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\$ServiceName&quot; -Value:&quot;Start&quot; -Data:4 -Type:DWORD\n          }\n        }\n        do { \n            Start-sleep -s 5 \n            write-verbose &quot;$(Get-Date): - waiting for the service to start&quot; -verbose\n           }  \n        until ((get-service &quot;$ServiceName&quot;).Status -eq &quot;Running&quot;) \n        write-verbose &quot;$(Get-Date): - the service has started&quot; -verbose\n      } Else {\n        If ($objService.Status -eq &quot;Running&quot;) {\n          write-verbose &quot;$(Get-Date): The service is already running&quot; -verbose\n        }\n      }\n    } Else {\n      write-verbose &quot;$(Get-Date): The `&quot;$ServiceDisplayName`&quot; service does not exist&quot; -verbose\n    }\n  }\n}\n\n#-------------------------------------------------------------\n\n$EndDTM = (Get-Date)\nwrite-verbose &quot;$(Get-Date): Elapsed Time: $(($EndDTM-$StartDTM).TotalSeconds) Seconds&quot; -Verbose\nwrite-verbose &quot;$(Get-Date): Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes&quot; -Verbose\n\ntry {\n  Stop-Transcript\n}\ncatch {\n  write-verbose &quot;$(Get-Date): This host does not support transcription&quot;\n}\n<\/pre><\/div>\n\n\n<p>Enjoy!<\/p>\n\n\n\n\n\n\n","protected":false},"excerpt":{"rendered":"<p>This is a process I&#8217;ve been working on since early 2017. I got it to a point where it works perfectly for my needs in my lab and several customer environments, and has been very reliable over the last few months, so I decided it was ready to release to the community in March 2019. &#8230; <a title=\"Controlling the Starting of the Citrix Desktop Service (BrokerAgent)\" class=\"read-more\" href=\"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/2019\/03\/04\/controlling-the-starting-of-the-citrix-desktop-service-brokeragent-1894\" aria-label=\"Read more about Controlling the Starting of the Citrix Desktop Service (BrokerAgent)\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[14,5,91,38,242],"tags":[864,534,535,862,858,863,865,538,861,847,845,842,851,852,853,843,844,540,541,542,539,537,536],"class_list":["post-1894","post","type-post","status-publish","format-standard","hentry","category-citrix","category-scripting","category-vdi","category-xenapp","category-xendesktop","tag-autoadminlogon","tag-brokeragent","tag-citrix-desktop-service","tag-cloud-connector","tag-cloud-connectors","tag-defaultdomainname","tag-defaultusername","tag-delay","tag-delivery-controller","tag-hkey_local_machinesoftwarecitrixvirtualdesktopagent","tag-hkey_local_machinesoftwarepoliciescitrixvirtualdesktopagent","tag-listofddcs","tag-masterimage","tag-mastermcsimage","tag-masterpvsimage","tag-mcspersonality-ini","tag-personality-ini","tag-pooled","tag-power-managed","tag-reboot-loop","tag-register","tag-startup","tag-vda"],"aioseo_notices":[],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/posts\/1894","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/comments?post=1894"}],"version-history":[{"count":5,"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/posts\/1894\/revisions"}],"predecessor-version":[{"id":3565,"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/posts\/1894\/revisions\/3565"}],"wp:attachment":[{"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/media?parent=1894"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/categories?post=1894"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.jhouseconsulting.com\/jhouseconsulting\/wp-json\/wp\/v2\/tags?post=1894"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}