Controlling the Starting of the Citrix Desktop Service (BrokerAgent)

by Jeremy Saunders on March 4, 2019

UPDATED 18th July 2019

  • The creation of the Scheduled Task needed for this script can be found in the Citrix Virtual Delivery Agent (VDA) Post Install Script. It’s important that the priority of the Scheduled Task is set to normal to prevent it from being queued.
  • Enhanced the Get-LogonLogoffEvent function for backward support of Windows 7/2008R2

UPDATED 16th May 2019

  • Added much more logging with timestamps to help debug issues and correlate events. You’ll see from the screen shot of the log file below that it’s now quite comprehensive.
  • Ensured that the format of the logging timestamp and LastBootUpTime were aligned so that its accuracy can be easily verified as I documented here.
  • Added an XDPing function I wrote to health check the Delivery Controllers.

This is a process I’ve been working on perfecting for a couple of years now. I’ve got it to a point where it works perfectly for my needs, and has been very reliable over the last few months, so I decided it was ready to release to the community.

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!

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 autologon process similar to what George Spiers documented in his article “Reduce Citrix logon times by up to 75%“. 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.

An ex-Citrix employee created a “Citrix Desktop Helper” 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.

I have a post VDA install script that…

  • Disables the Citrix Desktop Service (BrokerAgent) service
  • Creates a Scheduled Task called “Start the Citrix Desktop Service”, which runs at computer startup under the local System account

Pretty easy so far.

The Scheduled Task starts the StartCitrixDesktopService.ps1 script, which is where all the smarts are. I wanted it to do 3 things:

  1. Wait until there is a valid server listed under the ListofDDCs registry value. This ensures Group Policy has applied, and you’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 /MASTERIMAGE switch.
  2. Set the delay based on a specified time, which is what the “Citrix Desktop Helper” tool does.
  3. 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 I have another Scheduled Task called “Session Host Autologon Logoff Task”, 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).

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 the “HKEY_LOCAL_MACHINE\SOFTWARE\Citrix\VDAHelper” key to determine how long to wait before starting the Citrix Desktop Service.

The values I added are:

  • The VDAHelperSettingsEnabled tells the script if it should action or ignore all other settings under this registry key
    • Type: REG_DWORD
    • Value: VDAHelperSettingsEnabled
    • Data: 0=False; 1=True
  • The DelayDesktopServiceTime value tells this script, in seconds, how long to wait before starting the Citrix Desktop Service
    • Type: REG_DWORD
    • Value: DelayDesktopServiceTime
  • The TriggerOnTaskEndEvent value tells this script to wait for a task to end before starting the Citrix Desktop Service
    • Type: REG_DWORD
    • Value: TriggerOnTaskEndEvent
    • Data: 0=False; 1=True
  • The TaskNameFullPath value tells this script to which task to base the trigger event off before starting the Citrix Desktop Service
    • Type: REG_SZ
    • Value: TaskNameFullPath
  • The MaximumTaskWaitTime 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.
    • Type: REG_DWORD
    • Value: MaximumTaskWaitTime
  • The LogonEventUserName value tells this script which username will be in the logon event 4624 so that it can collect the “Logon ID” value to match it with a logoff event.
    • Type: REG_SZ
    • Value: LogonEventUserName

VDA Helper

Notes:

  • 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.
  • During the initial implementation I found that the logoff event for the autologon account may occur a few seconds after the Scheduled 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. Typically when you correlate logon and logoff events you can “tie” events 4624 (logon) and 4634 (logoff) together using the “Logon ID” value, which is a unique hexadecimal code that identifies that particular logon session. In this case we are unable tie the events together as a standard 4634 event is not produced because the logoff process is not initiated by a user as I prefer to use the “shutdown.exe /l /f” command line to force the logoff. We can, however, tie it 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 actual termination of the logoff. This allows us to start the Citrix Desktop Service without the risk of a reboot loop occurring.

A comprehensive log file is created that logs all steps of the process.

Start Citrix Desktop Service Log

Future Improvements:

Some of what I’ve documented may seem a little over complicated and overwhelming. I will be writing further articles to explain how I implemented post VDA install tasks and the autologon and logoff process. You don’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.

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:

Here is the StartCitrixDesktopService.ps1 (241 downloads) script:

<#
  This script will manage the start of the Citrix Desktop Service.

  This script will read the values under the following registry key to determine how long to wait before
  starting the Citrix Desktop Service.
  - Key: HKEY_LOCAL_MACHINE\SOFTWARE\Citrix\VDAHelper
  The VDAHelperSettingsEnabled tells the script if it should action or ignore all other settings under this
  registry key
  - Type: REG_DWORD
  - Value: VDAHelperSettingsEnabled
  - Data: 0=False; 1=True
  The DelayDesktopServiceTime value tells this script, in seconds, how long to wait before starting the
  Citrix Desktop Service
  - Type: REG_DWORD
  - Value: DelayDesktopServiceTime
  The TriggerOnTaskEndEvent value tells this script to wait for a task to end before starting the Citrix
  Desktop Service
  - Type: REG_DWORD
  - Value: TriggerOnTaskEndEvent
  - Data: 0=False; 1=True
  The TaskNameFullPath value tells this script to which task to base the trigger event off before starting
  the Citrix Desktop Service
  - Type: REG_SZ
  - Value: TaskNameFullPath
  The MaximumTaskWaitTime 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.
  - Type: REG_DWORD
  - Value: MaximumTaskWaitTime
  The LogonEventUserName value tells this script which username will be in the logon event 4624 so that it
  can collect the "Logon ID" value to match it with a logoff event.
  - Type: REG_SZ
  - Value: LogonEventUserName

  IMPORTANT NOTES:
  - 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.
  - 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. Typically when you correlate logon and logoff events you can "tie"
    events 4624 (logon) and 4634 (logoff) together using the "Logon ID" value, which is a unique hexadecimal
    code that identifies that particular logon session. However, in this case the logoff process is not
    initiated by a user as I prefer to use the "shutdown.exe /l /f" command line to force the logoff. This
    presented me with a challenge as I was unable tie the events together as a standard 4634 event is not
    created in the same way. We can, however, tie it 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 actual termination of the logoff, which
    is when the logon session is actually destroyed. At this point it's then safe to start the Citrix Desktop
    Service without the risk of a reboot loop occuring.

  Future Improvements:
  - The Get-DeliveryControllers function needs to be updated to look in the C:\Personality.ini for MCS deployments
  - Can take different actions based on image type or manual deployment.
    https://github.com/megamorf/CitrixImagingTools/issues/13

  Script name: StartCitrixDesktopService.ps1
  Release 2.8
  Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 16th October 2017
  Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 29th May 2019

#>

#-------------------------------------------------------------

# Set Powershell Compatibility Mode
Set-StrictMode -Version 2.0

# Enable verbose, warning and error mode
$VerbosePreference = 'Continue'
$WarningPreference = 'Continue'
$ErrorPreference = 'Continue'

$StartDTM = (Get-Date)

#-------------------------------------------------------------

$invalidChars = [io.path]::GetInvalidFileNamechars() 
$datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-")

# Get the TEMP path
$logPath = [System.IO.Path]::GetTempPath()
$logPath = $logPath.Substring(0,$logPath.Length-1)

$ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString())
$logFile = "$logPath\$ScriptName-$($datestampforfilename).log"

try {
  Start-Transcript "$logFile"
}
catch {
  write-verbose "$(Get-Date): This host does not support transcription"
}

#-------------------------------------------------------------

# Set to true if you want to check if there are valid Delivery Controller(s) set
# under the HKLM\SOFTWARE\Citrix\VirtualDesktopAgent\ListOfDDCs registry value.
$CheckForValidity = $True

# Set the number of seconds to wait between validity checks of the ListOfDDCs
# registry value.
$interval = 10

# Set the number of times to repeat the validity before continuing.
$RepeatValidityCheck = 25

# Set to true if you want to restart the service(s) if already started.
$StopServiceIfAlreadyStarted = $True

# Create the hashtable for services to be started
$ServicesToStart = @{}

# Create an object for each service that needs to be started
$objService = New-Object -TypeName PSObject -Property @{
  "Name"	=	"BrokerAgent"
  "DisplayName"	=	"Citrix Desktop Service"
}
# Add the object to the hashtable
$ServicesToStart.Add($objService.Name,$objService)

#-------------------------------------------------------------

Function Get-VDAHelperSettings {
  #  This function read the values under the following registry key to determine how long to wait
  #  before starting the Citrix Desktop Service.
  #  - Key: HKEY_LOCAL_MACHINE\SOFTWARE\Citrix\VDAHelper
  $regKey = "SOFTWARE\Citrix\VDAHelper"
  $ResultProps = @{
    VDAHelperSettingsEnabled = 0
    DelayDesktopServiceTime = 0
    TriggerOnTaskEndEvent = 0
    TaskNameFullPath = ""
    MaximumTaskWaitTime = 0
    LogonEventUserName = ""
  }
  $results = @()
  # Create an instance of the Registry Object and open the HKLM base key
  $reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$env:computername)  
  $thisRegKey = $reg.OpenSubKey($regKey)
  ForEach ($Key in $($ResultProps.Keys)) {
    Try {
      $ResultProps.$Key = $thisRegKey.GetValue($Key)
    }
    Catch {
      #
    }
  }
  $results += New-Object PsObject -Property $ResultProps
  return $results
}

Function IsTaskValid {
  param([string]$TaskNameFullPath)
  If ($TaskNameFullPath -Match "\\") {
    $TaskName = $TaskNameFullPath.Split('\')[1]
    $TaskRootFolder = $TaskNameFullPath.Split('\')[0] + "\"
  } Else {
    $TaskName = $TaskNameFullPath
    $TaskRootFolder = "\"
  }
  $TaskExists = $False
  # Create the TaskService object.
  Try {
    [Object] $service = new-object -com("Schedule.Service")
    If (!($service.Connected)){
      Try {
        $service.Connect()
        $rootFolder = $service.GetFolder("$TaskRootFolder")
        $rootFolder.GetTasks(0) | ForEach-Object {
          If ($_.Name -eq $TaskName) {
            $TaskExists = $True
          } #If
        } #ForEach
      } #Try
      Catch [System.Exception]{
        "Scheduled Task Connection Failed"
      }
    } #If
  } #Try
  Catch [System.Exception]{
    "Scheduled Task Object Creation Failed"
  } #Catch
  return $TaskExists
}

Function IsEventLogValid {
  param([string]$EventLog)
  $EventLogExists = $False
  Try {
    Get-WinEvent -ListLog "$EventLog" -ErrorAction Stop | out-null
    $EventLogExists = $True
  }
  Catch [System.Exception]{
    #$($_.Exception.Message)
  }
  return $EventLogExists
}

function Get-TaskEventRunTime {
  param (
         [switch]$start,
         [switch]$end,
         [string]$taskPath
        )
  $results = @()
  $ResultProps = @{
    EventFound = $False
    TimeCreated = [datetime]"1/1/1600"
    ErrorMessage = ""
  }
  # in case taskpath contains quotes, have to escape (double) them
  $taskPath = $taskPath.Replace("'","''")
  If ($Start) {
    # fetch the most recent start event (100) event
    $XPath = "*[System[(EventID=100)]] and *[EventData[Data[1]='" + $taskPath + "']]"
  }
  If ($End) {
    # fetch the most recent success event (102) or failed (111) event
    $XPath = "*[System[((EventID=102) or (EventID=111))]] and *[EventData[Data[1]='" + $taskPath + "']]"
  }
  Try {
    get-winevent -LogName 'Microsoft-Windows-TaskScheduler/Operational' -FilterXPath $XPath -MaxEvents 1 -ErrorAction Stop  | ForEach-Object{
      $ResultProps.EventFound = $True
      $ResultProps.TimeCreated = $_.TimeCreated
    }
  }
  Catch {
    $ResultProps.ErrorMessage = $($_.Exception.Message)
    #$($_.Exception.Message)
  }
  $results += New-Object PsObject -Property $ResultProps
  return $results
}

function Get-LastBootTime {
  return [Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject Win32_OperatingSystem).LastBootUpTime)
}

function Get-LogonLogoffEvent {
  param (
         [switch]$Logon,
         [switch]$Logoff,
         [string]$UserName,
         [string]$Event,
         [string]$LogonID="",
         [string]$LogonType=""
        )
  # The XML Query can be written in a couple of different formats. I've left both formats in this function to make
  # it easier to enhance in the future.
  $results = @()
  $ResultProps = @{
    EventFound = $False
    TimeCreated = [datetime]"1/1/1600"
    LogonID = ""
  }
  If ($Logon) {
  # Query to check that the session has logged on.
$xmlquery=@"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
		*/System[EventID=4624]
		and
		*/EventData/Data[@Name='TargetUserName']='$UserName'
		and
		*/EventData/Data[@Name='LogonType']='$LogonID'
		and
		*/EventData/Data[@Name='ProcessName']='C:\Windows\System32\winlogon.exe'
	</Select>
  </Query>
</QueryList>
"@
$xmlquery=@"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
		*[System[(EventID='4624')]]
		and
		*[EventData[Data[@Name='TargetUserName'] and (Data='$UserName')]]
		and
		*[EventData[Data[@Name='LogonType'] and (Data='$LogonID')]]
		and
		*[EventData[Data[@Name='ProcessName'] and (Data='C:\Windows\System32\winlogon.exe')]]
	</Select>
  </Query>
</QueryList>
"@
  }
  If ($Logoff -AND $Event -eq "4647") {
  # Query to check that the logoff has been initiated
$xmlquery=@"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
		*/System[EventID=4647]
		and
		*/EventData/Data[@Name='TargetUserName']='$UserName'
		and
		*/EventData/Data[@Name='TargetLogonId']='$LogonID'
	</Select>
  </Query>
</QueryList>
"@
$xmlquery=@"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
		*[System[(EventID='4647')]]
		and
		*[EventData[Data[@Name='TargetUserName'] and (Data='$UserName')]]
		and
		*[EventData[Data[@Name='TargetLogonId'] and (Data='$LogonID')]]
	</Select>
  </Query>
</QueryList>
"@
  }
  If ($Logoff -AND $Event -eq "4634") {
  # Query to check that the session has been destroyed
$xmlquery=@"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
		*/System[EventID=4634]
		and
		*/EventData/Data[@Name='TargetUserName']='$UserName'
		and
		*/EventData/Data[@Name='LogonType']='$LogonType'
	</Select>
  </Query>
</QueryList>
"@
$xmlquery=@"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
		*[System[(EventID='4634')]]
		and
		*[EventData[Data[@Name='TargetUserName'] and (Data='$UserName')]]
		and
		*[EventData[Data[@Name='LogonType'] and (Data='$LogonType')]]
	</Select>
  </Query>
</QueryList>
"@
  }
  Try {
    Get-WinEvent -MaxEvents 1 -FilterXml $xmlquery -ErrorAction Stop | ForEach-Object{
      $ResultProps.EventFound = $True
      $ResultProps.TimeCreated = $_.TimeCreated
      if ($_.ID -eq "4624") {
        $ResultProps.LogonID = $_.Properties[7].Value
      }
      if ($_.ID -eq "4647") {
        $ResultProps.LogonID = $_.Properties[3].Value
      }
      if ($_.ID -eq "4634") {
        #
      }
    }
  }
  Catch {
    #$($_.Exception.Message)
  }
  $results += New-Object PsObject -Property $ResultProps
  return $results
}

Function XDPing {
  # To test if the Broker service is reachable, listening and processing requests on its configured port,
  # you can issue blank HTTP POST requests at the Broker's Registrar service, which is located at
  # /Citrix/CdsController/IRegistrar. If the first line displayed is "HTTP/1.1 100 Continue", then the
  # Broker service responded and deemed to be healthy.
  param(
    [Parameter(Mandatory=$True)][String]$ComputerName, 
    [Parameter(Mandatory=$True)][Int32]$Port,
    [String]$ProxyServer="", 
    [Int32]$ProxyPort
  )
  $URI = "http://$ComputerName/Citrix/CdsController/IRegistrar"
  $InitialDataToSend = "POST $URI 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"
  $FinalDataToSend = "X"
  $IsServiceListening = $False
  If ($ProxyServer -eq "" -OR $ProxyServer -eq $NULL) {
    $ConnectToHost = $ComputerName
    [int]$ConnectOnPort = $Port
  } Else {
    $ConnectToHost = $ProxyServer
    [int]$ConnectOnPort = $ProxyPort
    #write-verbose "Connecting via a proxy" -verbose
  }
  $Saddrf   = [System.Net.Sockets.AddressFamily]::InterNetwork 
  $Stype    = [System.Net.Sockets.SocketType]::Stream 
  $Ptype    = [System.Net.Sockets.ProtocolType]::TCP
  $socket    = New-Object System.Net.Sockets.Socket $saddrf, $stype, $ptype 
  Try {
    $socket.Connect($ConnectToHost,$ConnectOnPort)
    #$socket.Connected
    $initialbytes = [System.Text.Encoding]::ASCII.GetBytes($InitialDataToSend)
    #write-verbose "Sending initial data..." -verbose
    #$InitialDataToSend
    $NumBytesSent = $socket.Send($initialbytes, $initialbytes.length,[net.sockets.socketflags]::None)
    #write-verbose "Sent $NumBytesSent bytes" -verbose
    $numArray = new-object byte[] 21
    $socket.ReceiveTimeout = 5000
    $NumBytesReceived = $socket.Receive($numArray)
    #write-verbose "Received $NumBytesReceived bytes" -verbose
    $output = [System.Text.Encoding]::ASCII.GetString($numArray)
    $finalBytes = [System.Text.Encoding]::ASCII.GetBytes($FinalDataToSend)
    #write-verbose "Sending final data..." -verbose
    #$FinalDataToSend
    $NumBytesSent = $socket.Send($finalBytes, $finalBytes.length,[net.sockets.socketflags]::None)
    #write-verbose "Sent $NumBytesSent bytes" -verbose
    $socket.Close() | out-null
    #write-verbose "Received data..." -verbose
    #$output
    if ($output -eq "HTTP/1.1 100 Continue") {
      $IsServiceListening = $True
    }
  }
  Catch {
    #
  }
  return $IsServiceListening
}

Function Get-DeliveryControllers {
  param (
         [switch]$Registry
        )
  $DeliveryControllers = ""
  $Port = 80
  $IsServiceListening = $False
  $RegPath = "HKLM:\SOFTWARE\Citrix\VirtualDesktopAgent"
  $ListOfDDCsValue = "ListOfDDCs"
  $ListOfDDCsValueExist = $False
  $ControllerRegistrarPortValue = "ControllerRegistrarPort"
  $ControllerRegistrarPortValueExists = $False
  If ($Registry) {
    $ErrorActionPreference = "stop"
    try {
      If ((Get-ItemProperty -Path "$RegPath" | Select-Object -ExpandProperty "$ListOfDDCsValue") -ne $null) {
        $ListOfDDCsValueExist = $True
      }
      If ((Get-ItemProperty -Path "$RegPath" | Select-Object -ExpandProperty "$ControllerRegistrarPortValue") -ne $null) {
        $ControllerRegistrarPortValueExists = $True
      }
    }
    catch {
      #
    }
    finally { $ErrorActionPreference = "Continue" }
  }
  If ($ListOfDDCsValueExist -AND $ControllerRegistrarPortValueExists) {
    $DeliveryControllers = Get-ItemProperty -Path "$RegPath" | Select-Object -ExpandProperty "$ListOfDDCsValue"
    $Port = Get-ItemProperty -Path "$RegPath" | Select-Object -ExpandProperty "$ControllerRegistrarPortValue"
    If ($DeliveryControllers -ne "") {
      $DeliveryControllers.Split(" ") | ForEach{
        $charCount = ($_.ToCharArray() | Where-Object {$_ -eq '.'} | Measure-Object).Count
        If ($charCount -gt 0) {
          If (XDPing -ComputerName "$_" -Port $Port) { $IsServiceListening = $True }
        }
      }
    }
  }
  $results = @()
  $ResultProps = @{ 
    DeliveryControllers = $DeliveryControllers
    Port = $Port
    IsServiceListening = $IsServiceListening
  }
  $results += New-Object PsObject -Property $ResultProps
  return $results
}

# Use PowerShell to Find Operating System Version
# You can use "[System.Environment]::OSVersion.Version", or the PInvoke method as per the following Scripting Guy article:
# http://blogs.technet.com/b/heyscriptingguy/archive/2014/04/25/use-powershell-to-find-operating-system-version.aspx
Function Get-OSVersion
{
 $signature = @"
 [DllImport("kernel32.dll")]
 public static extern uint GetVersion();
"@
Add-Type -MemberDefinition $signature -Name "Win32OSVersion" -Namespace Win32Functions -PassThru
}

$os = [System.BitConverter]::GetBytes((Get-OSVersion)::GetVersion())
$majorOSVersion = $os[0]
$minorOSVersion = $os[1]
$buildOSNumber = [byte]$os[2],[byte]$os[3]
$buildOSNumberNumber = [System.BitConverter]::ToInt16($buildOSNumber,0)
[float]$OSVersion = $majorOSVersion.ToString() + "." + $minorOSVersion.ToString()

#-------------------------------------------------------------

If ($CheckForValidity) {
  $DeliveryControllers = ""
  $i = 0
  Do {
    write-verbose "$(Get-Date): Checking to see if there is a valid Deliver Controller set under the" -verbose
    write-verbose "$(Get-Date): `"HKLM\SOFTWARE\Citrix\VirtualDesktopAgent\ListOfDDCs`" registry value..." -verbose
    $IsValid = Get-DeliveryControllers -Registry
    $DeliveryControllers = $IsValid.DeliveryControllers
    $Port = $IsValid.Port
    If ($DeliveryControllers -eq "" -OR $DeliveryControllers -eq $NULL) { $DeliveryControllers = "none set" }
    write-verbose "$(Get-Date): - Attempting an XDPing to Delivery Controller(s): $DeliveryControllers" -verbose
    write-verbose "$(Get-Date): - Using TCP port: $Port" -verbose
    If (!$IsValid.IsServiceListening) {
      write-verbose "$(Get-Date): - XDPing failed" -verbose
      write-verbose "$(Get-Date): - Will check again in $interval seconds" -verbose
      $i++
      If ($i -eq $RepeatValidityCheck) { break }
      Start-Sleep -Seconds $interval
    }
  } Until ($IsValid.IsServiceListening -eq $True)
  If ($IsValid.IsServiceListening) {
    write-verbose "$(Get-Date): - XDPing successful" -verbose
  } Else {
    write-verbose "$(Get-Date): - Continuing with invalid Delivery Controller(s): $DeliveryControllers" -verbose
  }
}

$VDAHelperSettings = Get-VDAHelperSettings
write-verbose "$(Get-Date): The VDA Helper settings are:" -verbose
$VDAHelperSettingsEnabled = $VDAHelperSettings.VDAHelperSettingsEnabled
write-verbose "$(Get-Date): - VDAHelperSettingsEnabled: $VDAHelperSettingsEnabled (0=False,1=True)" -verbose
$TriggerOnTaskEndEvent = $VDAHelperSettings.TriggerOnTaskEndEvent
write-verbose "$(Get-Date): - TriggerOnTaskEndEvent: $TriggerOnTaskEndEvent (0=False,1=True)" -verbose
$TaskNameFullPath = $VDAHelperSettings.TaskNameFullPath
write-verbose "$(Get-Date): - TaskNameFullPath: $TaskNameFullPath" -verbose
$DelayDesktopServiceTime = $VDAHelperSettings.DelayDesktopServiceTime
write-verbose "$(Get-Date): - DelayDesktopServiceTime: $DelayDesktopServiceTime seconds" -verbose
$MaximumTaskWaitTime = $VDAHelperSettings.MaximumTaskWaitTime
write-verbose "$(Get-Date): - MaximumTaskWaitTime: $MaximumTaskWaitTime seconds" -verbose
$LogonEventUserName = $VDAHelperSettings.LogonEventUserName
write-verbose "$(Get-Date): - LogonEventUserName: $LogonEventUserName" -verbose

$LastBootTime = Get-LastBootTime
write-verbose "$(Get-Date): This host was last booted on $LastBootTime" -verbose

If ($VDAHelperSettingsEnabled) {

  $TaskExists = IsTaskValid -TaskName:"$TaskNameFullPath"
  $EventLog = "Microsoft-Windows-TaskScheduler/Operational"
  $EventLogExists = IsEventLogValid -EventLog:"$EventLog"

  If ($LastBootTime -gt (Get-Date)) {
    If ($TriggerOnTaskEndEvent) {
      $TriggerOnTaskEndEvent = $False
      write-warning "$(Get-Date -format "dd/MM/yyyy HH:mm:ss"): 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." -verbose
    }
  }

  If ($TriggerOnTaskEndEvent -AND $TaskExists -AND $EventLogExists) {
    write-verbose "$(Get-Date): The `"$TaskNameFullPath`" task is valid" -verbose
    $StartPhase = (Get-Date)
    $ValidStart = $False
    $ValidEnd = $False
    $ValidLogoffInitiated = $False
    $ValidLogoffTerminated = $False
    Do {
      $TaskLastStartTime = (Get-TaskEventRunTime -Start -Taskpath "$TaskNameFullPath").TimeCreated
      write-verbose "$(Get-Date): The task last started on $TaskLastStartTime" -verbose
      If ($LastBootTime -lt $TaskLastStartTime) {
        write-verbose "$(Get-Date): - The task has started." -verbose
        $ValidStart = $True
      } Else {
        write-verbose "$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the task to start after the reboot..." -verbose
      }
      If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {
        $ValidStart = $True
        $ValidEnd = $True
      }
      Start-Sleep -Seconds 1
    } Until ($ValidStart -eq $True)
    Do {
      $TaskLastEndTime = (Get-TaskEventRunTime -End -Taskpath "$TaskNameFullPath").TimeCreated
      write-verbose "$(Get-Date): The task last finished on $TaskLastEndTime" -verbose
      If ($LastBootTime -lt $TaskLastEndTime -AND $TaskLastStartTime -lt $TaskLastEndTime){
        write-verbose "$(Get-Date): - The task has ended." -verbose
        $ValidEnd = $True
      } Else {
        write-verbose "$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the task to end..." -verbose
      }
      If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {
        $ValidEnd = $True
      }
      Start-Sleep -Seconds 1
    } Until ($ValidEnd -eq $True)

    # Confirming that the session has logged on.
    $LogonEvent = Get-LogonLogoffEvent -Logon -UserName:"$LogonEventUserName" -LogonType:"3"
    If ($LogonEvent.EventFound) {
      write-verbose "$(Get-Date): A valid logon event was found for the `"$LogonEventUserName`" user account on $($LogonEvent.TimeCreated)" -verbose
      Do {
        # Confirming that the logoff has been initiated.
        $LogoffEvent4647 = Get-LogonLogoffEvent -Logoff -Event:"4647" -UserName:"$LogonEventUserName" -LogonID:"$($LogonEvent.LogonID)"
        If ($LogonEvent.TimeCreated -lt $LogoffEvent4647.TimeCreated){
          write-verbose "$(Get-Date): - The logoff was initiated on $($LogoffEvent4647.TimeCreated)" -verbose
          $ValidLogoffInitiated = $True
        } Else {
          write-verbose "$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the account to logoff..." -verbose
        }
        If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {
          $ValidLogoffInitiated = $True
          $ValidLogoffTerminated = $True
        }
        Start-Sleep -Seconds 1
      } Until ($ValidLogoffInitiated -eq $True)
      Do {
        # Confirming that the session has been destroyed.
        If ($OSVersion -gt 6.1) {
          $LogoffEvent4634 = Get-LogonLogoffEvent -Logoff -Event:"4634" -UserName:"DWM-1" -LogonType:"2"
        } Else {
          $LogoffEvent4634 = Get-LogonLogoffEvent -Logoff -Event:"4634" -UserName:"${env:computername}$" -LogonType:"3"
        }
        If ($LogoffEvent4647.TimeCreated -le $LogoffEvent4634.TimeCreated){
          write-verbose "$(Get-Date): - The logoff was terminated on $($LogoffEvent4634.TimeCreated)" -verbose
          $ValidLogoffTerminated = $True
        } Else {
         write-verbose "$(Get-Date):  - Waiting $(((Get-Date)-$StartPhase).TotalSeconds) seconds for the account to logoff..." -verbose
        }
        If ($(((Get-Date)-$StartPhase).TotalSeconds) -ge $MaximumTaskWaitTime) {
          $ValidLogoffTerminated = $True
        }
        Start-Sleep -Seconds 1
      } Until ($ValidLogoffTerminated -eq $True)
    } Else {
      write-verbose "$(Get-Date): No valid logon event was found for the `"$LogonEventUserName`" user account." -verbose
    }
  } Else {
    If ($TriggerOnTaskEndEvent) {
      If ($TaskExists -eq $False) {
        write-warning "The `"$TaskNameFullPath`" task does not exist" -verbose
      }
      If ($EventLogExists -eq $False) {
        write-warning "The `"$EventLog`" Event Log does not" -verbose
        write-warning "exist. This may be because there's a new disk attached to the virtual" -verbose
        write-warning "machine and the Event Logs haven't finished being moved to the new" -verbose
        write-warning "location. If this is the case, restarting the Virtual Machine again" -verbose
        write-warning "should address this issue." -verbose
      }
    }
    write-verbose "$(Get-Date): Delaying the starting of the Citrix Desktop Service for $DelayDesktopServiceTime seconds" -verbose
    Start-Sleep -s $DelayDesktopServiceTime
  }
}

ForEach ($key in $ServicesToStart.keys) {
  $ServiceName = $ServicesToStart.$key.Name
  $ServiceDisplayName = $ServicesToStart.$key.DisplayName

  write-verbose "$(Get-Date): Setting the `"$ServiceDisplayName`" service to Manual start type..." -verbose

  # Possible results using the sc.exe command line tool:
  # [SC] ChangeServiceConfig SUCCESS
  # [SC] OpenSCManager FAILED 5:  Access is denied.
  # [SC] OpenSCManager FAILED 1722:  The RPC server is unavailable." --> Computer shutdown
  # [SC] OpenService FAILED 1060:  The specified service does not exist as an installed service." --> Service not installed

  Invoke-Command {cmd /c sc.exe config "$ServiceName" start= demand} | out-null

  If ($StopServiceIfAlreadyStarted) {
    # Stop the service
    $objservice = Get-Service "$ServiceName" -ErrorAction SilentlyContinue
    If ($objService) {
      If ($objService.Status -eq "Running") {
        write-verbose "$(Get-Date): Stopping the service..." -verbose
        stop-service -name "$ServiceName" -verbose
        do { 
            Start-sleep -s 5
            write-verbose "$(Get-Date): - waiting for the service to stop" -verbose
           }  
        until ((get-service "$ServiceName").Status -eq "Stopped")
        write-verbose "$(Get-Date): - the service has stopped" -verbose
      } Else {
        If ($objService.Status -eq "Stopped") {
          write-verbose "$(Get-Date): The service is not running" -verbose
        }
      }
    } Else {
      write-verbose "$(Get-Date): The `"$ServiceDisplayName`" service does not exist" -verbose
    }
  }

  # Start the Service
  $objservice = Get-Service "$ServiceName" -ErrorAction SilentlyContinue
  If ($objService) {
    If ($objService.Status -eq "Stopped") {
      write-verbose "$(Get-Date): Starting the service..." -verbose
      start-service -name "$ServiceName" -verbose
      do { 
          Start-sleep -s 5 
          write-verbose "$(Get-Date): - waiting for the service to start" -verbose
         }  
      until ((get-service "$ServiceName").Status -eq "Running") 
      write-verbose "$(Get-Date): - the service has started" -verbose
    } Else {
      If ($objService.Status -eq "Running") {
        write-verbose "$(Get-Date): The service is already running" -verbose
      }
    }
  } Else {
    write-verbose "$(Get-Date): The `"$ServiceDisplayName`" service does not exist" -verbose
  }
}

#-------------------------------------------------------------

$EndDTM = (Get-Date)
write-verbose "$(Get-Date): Elapsed Time: $(($EndDTM-$StartDTM).TotalSeconds) Seconds" -Verbose
write-verbose "$(Get-Date): Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes" -Verbose

try {
  Stop-Transcript
}
catch {
  write-verbose "$(Get-Date): This host does not support transcription"
}

Enjoy!

Jeremy Saunders

Jeremy Saunders

Independent Consultant | Contractor | Microsoft & Citrix Specialist | Desktop Virtualization Specialist at J House Consulting
Jeremy is a highly respected, IT Professional, with over 30 years’ experience in the industry. He is an independent IT consultant providing expertise to enterprise, corporate, higher education and government clients. His skill set, high ethical standards, integrity, morals and attention to detail, coupled with his friendly nature and exceptional design and problem solving skills, makes him one of the most highly respected and sought after Microsoft and Citrix technical resources in Australia. His alignment with industry and vendor best practices puts him amongst the leaders of his field.
Jeremy Saunders
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: