How to Identify Obsolete Machines in Citrix XenDesktop?

With this post I share a small PowerShell function, Get-ObsoleteBrokerDesktop, that identifies and returns obsolete desktops with in a Citrix XenDesktop Desktop Group.

What are obsolete desktops? Typically, XenDesktop brokers sessions to virtual machines with Microsoft Windows 7 and an according Computer Account in Active Directory. Over time, due to whatever reason, an Administrator may remove VMs and AD Computer Accounts. XenDesktop isn’t aware of this meaning that XenDesktop simply classifies the according machines as powered off, which is only half the story: these machines are good for nothing.

XenDesktop maintains for each virtual machine an appropriate object in its database. A database object representing a virtual machine contains comprehensive information about a the machine such like the VDA’s version, the associated user name(s), if power managed or not, the Desktop Group it belongs to, and – last but not least – Hypervisor- and Active Directory-related details about the brokered machine’s identity on the Hypervisor/Hosting platform (for example the HostedMachineID) as well as in Active Directory (SID).

From a high level perspective the function Get-ObsoleteBrokerMachine performs the following steps:

  • Get the Id of each VM within the specified Hypervisor path. (The VM-Id corresponds to the HostedMachinId property of a desktop object in XenDesktop)
  • Identify all Machines within the specified Desktop Group that have no matching HostedMachineId in the list of VM-Ids

Please keep in mind that this approach will class physical desktops as obsolete too!

Here we go!

function Get-ObsoleteBrokerDesktop
{
    <#
    .SYNOPSIS
        Gets obsolete desktops in this site.

    .DESCRIPTION
        Retrieves desktops within the specified Desktop Group without a matching VM
        within the specified Hypervisor Path.

        Get-ObsoleteBrokerDesktop returns full objects. This is useful for advanced
        searching or retrieval of information about desktops.

    .PARAMETER DesktopGroup
        Specifies the name of a Desktop Group

    .PARAMETER HypervisorPath
        Specifies a valid Hypervisor path

    .EXAMPLE
        PS C:\> Get-ObsoleteBrokerDesktop 'Private Machines' XDHyp:\Connections\XS6\folder1\folder2

        This command will list all obsolete desktops within the Private Machines Desktop Group.

    .EXAMPLE
        PS C:\> Get-ObsoleteBrokerDesktop 'Private Machines' XDHyp:\Connections\XS6\folder1\folder2 | Set-BrokerPrivateDesktop -InMaintenanceMode $true

        This command puts the obsolete desktops into maintenance mode.
    #>

    [cmdletBinding(SupportsShouldProcess=$True)]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String]
        $DesktopGroup
        ,
        [Parameter(Mandatory=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [String]
        $HypervisorPath
    )

    $desktops = @()
    $VmIds = @()

    $VmIds = Get-ChildItem $HypervisorPath\*.vm | ForEach-Object {$_.Id}

    if ($VmIds)
    {
        $desktops = Get-BrokerDesktop -MaxRecordCount 2147483647 -Filter {(DesktopGroupName -eq $DesktopGroup)} |
            Where-Object {$VmIds -notcontains $_.HostedMachineId}
    }

    return $desktops
}

Disclaimer: I hope that the information in this post is valuable to you. Your use of the information contained in this post, however, is at your sole risk. All information on this post is provided “as is”, without any warranty, whether express or implied, of its accuracy, completeness, fitness for a particular purpose, title or non-infringement, and none of the third-party products or information mentioned in the work are authored, recommended, supported or guaranteed by me. Further, I shall not be liable for any damages you may sustain by using this information, whether direct, indirect, special, incidental or consequential, even if it has been advised of the possibility of such damages.

How to Identify and Remove Old Files With PowerShell

Today, I want to share a second PowerShell function, Remove-OldFiles, that shows how to identify and remove old files. Typically, you want such a function in order to keep log and temp folders clean from very old stuff.

Old files, what does that mean? The function investigates the LastWriteTime property from each file found and compares it to a given maximum age. By default, the function will delete all files with a lastwritetime that is over 90 days.

function Remove-OldFiles
{
    <#
    .SYNOPSIS
        Deletes files older than a specified amount of days (default 90).

    .DESCRIPTION
        Deletes files older than a specified amount of days (default 90). If not
        specified elsewhere, the function identifies all files within the current
        directory with a lastwritetime that is over the specified amount of days
        and deletes them.

    .PARAMETER Path
        Specifies the folder where to start (default is current directory)

    .PARAMETER Filter
        Specifies a filter (default is *.*)

    .PARAMETER Recurse
        Recursively process the directory structure

    .PARAMETER Age
        Specifies the maximum count of days between current time and the lastwritetime.

    .EXAMPLE
        PS C:\> "C:\Windows\Temp" | Remove-OldFiles -Recurse -WhatIf

        This command shows a way to safely check which files the function would delete

    .EXAMPLE
        PS C:\> Remove-OldFiles C:\Windows\Temp -Recurse

        This command recursively deletes all old files starting from C:\Windows\Temp
    #>

    [cmdletBinding(SupportsShouldProcess=$True)]
    param (
        [Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)]
        [String[]]
        $Path
        ,
        [Parameter(Mandatory=$false)]
        [String]
        $Filter
        ,
        [Switch]
        $Recurse
        ,
        [Parameter(Mandatory=$false)]
        [Int]
        $Age = 90
    )

    # Avoid confusing Get-ChildItem
    $PSBoundParameters.Remove("Age") | Out-Null
    $PSBoundParameters.Remove("WhatIf") | Out-Null
    $PSBoundParameters.Remove("Confirm") | Out-Null

    $OldestLastWriteTime = (Get-Date).AddDays(-$Age)

    Get-ChildItem @PSBoundParameters |
        Where-Object {$_.PSIsContainer -eq $false} |
        Where-Object {$_.LastWriteTime -lt $OldestLastWriteTime} |
        ForEach-Object {Remove-Item -Path $_.FullName -Force}
}

Disclaimer: I hope that the information in this post is valuable to you. Your use of the information contained in this post, however, is at your sole risk. All information on this post is provided “as is”, without any warranty, whether express or implied, of its accuracy, completeness, fitness for a particular purpose, title or non-infringement, and none of the third-party products or information mentioned in the work are authored, recommended, supported or guaranteed by me. Further, I shall not be liable for any damages you may sustain by using this information, whether direct, indirect, special, incidental or consequential, even if it has been advised of the possibility of such damages.

How to Move Machines Within a Citrix XenDesktop Site?

Recently, I used Citrix XenDesktop 5’s PowerShell interface at a mid-size business in order to transfer machines to a new Machine Catalog and Desktop Group. With this article I want to share a generalized version of that script, MoveXDMachines.ps1, in order to help Admins and Consultants who are stuck in a similar situation. (Please note the disclaimer at the end of this article.)

The initial situation was a typical PoC setup meaning that there were XenDesktop Controller, Web Interface, and SQL Express Database installed on a single Windows Server. There was only one Machine Catalog for physical machines configured. Among the members of this catalog I found only 5 to 10 physical machines and round about 300 VMware vSphere based VMs! With that said it seems quite logical that there was no Connection to the Hosting Infrastructure configured. Thus, referred to this case, it was my task to 1) manually configure the connection to VMware vSphere, 2) manually create a new Machine Catalog for the existing VMs, 3) manually create a new Desktop Group for the power managed VMs, and 4) automatically move the VM-hosted machines from the existing Machine Catalog/Desktop Group to the new Machine Catalog/Desktop Group while keeping existing User-to-VM associations.

From a high level perspective the script MoveMachines.ps1 performs the following steps:

  • Preparation:
    • Load Citrix commands – this is straightforward
    • Get the UID of the target Machine Catalog (with Get-BrokerCatalog)
    • Get the UID of the Hypervisor Connection (with Get-BrokerHypervisorConnection)
    • List all VMs within the specified Hypervisor path (with Get-ChildItem and the PSDrive “XDHyp:” created by Get-BrokerHypervisorConnection)
    • List all Machines within the source Desktop Group (with Get-BrokerDesktop)
  • For each Machine:
    • Get the ID from the list of VMs (HostedMachineId)
    • Enable Maintenance Mode (with Set-BrokerPrivateDesktop)
    • Remove the machine from the current Desktop Group (with Remove-BrokerMachine)
    • Remove the machine from the current Machine Catalog (with Remove-BrokerMachine too)
    • Add the machine to the target Machine Catalog (with New-BrokerMachine)
    • Associate the user account to the machine (with Add-BrokerUser)
    • Add the machine to the target Desktop Group (with Add-BrokerMachine)

The script writes to a separate log file for each (known) error or condition making it easier distinguish between the different cases when the script has finished its work.

So, finally here we go:

#
# Script : MoveXDMachines.ps1
# Author : Frank Peter Schultze
# Date   : 17:44 22.03.2012
#

param (
    $SourceDG = $(Read-Host 'Source Desktop Group'),
    $TargetDG = $(Read-Host 'Target Desktop Group'),
    $TargetMC = $(Read-Host 'Target Machine Catalog',
    $VmPath = $(Read-Host 'Path of the VMs on the Hypervisor, for ex. XDHyp:\Connections\MyESX\folder1\folder2\windowsvm'
)

function Write-Log
{
    [CmdletBinding(SupportsShouldProcess=$false)]
    param (
        [Parameter(Position=0)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Message
        ,
        [Parameter(Position=1)]
        [String]
        $FilePath = $(if ($LogFilePreference) {$LogFilePreference} else {Read-Host 'Value for the FilePath parameter'})
    )

    Write-Output $Message | Out-File -FilePath $FilePath -Append
}

function Get-XDBrokerCatalogUid
{
    param (
        $Name = $(Read-Host 'Supply a value for the Name parameter')
    )

    $bc = Get-BrokerCatalog -ErrorAction SilentlyContinue | ? {$_.Name -eq $Name}
    if ($?) {$ReturnValue = $bc.Uid} else {$ReturnValue = $false}

    return $ReturnValue
}

function Get-XDHypervisorConnectionUid
{
    param (
        $Name = $(Read-Host 'Supply a value for the Name parameter')
    )

    $hc = Get-BrokerHypervisorConnection -ErrorAction SilentlyContinue | ? {$_.Name -eq $Name}
    if ($?) {$ReturnValue = $hc.Uid} else {$ReturnValue = $false}

    return $ReturnValue
}

function Get-VMIDs
{
    param (
        $Path = $(Read-Host 'Supply a value for the Path parameter')
    )

    $vmlist = Get-ChildItem $Path\*.vm | Select-Object Name, Id
    if ($?) {$ReturnValue = $vmlist} else {$ReturnValue = $false}

    return $ReturnValue
}

function Get-XDMachinesFromDesktopGroup
{
    param (
        $DesktopGroupName = $(Read-Host 'Supply a value for the DesktopGroupName parameter')
    )

    $desktops = Get-BrokerDesktop -MaxRecordCount 2147483647 -Filter {(DesktopGroupName -eq $DesktopGroupName)} -ErrorAction SilentlyContinue | Select-Object AssociatedUserNames, MachineName, SID
    if ($?) {$ReturnValue = $desktops} else {$ReturnValue = $false}

    return $ReturnValue
}

function Get-VMHostedMachineId
{
    param (
        $VmList = $(Read-Host 'Supply a value for the VmList parameter'),
        $Name = $(Read-Host 'Supply a value for the Name parameter')
    )

    $HostedMachine = $VmList | ? {$_.Name -eq ($Name.Split('\')[-1])}
    if ($HostedMachine) {$ReturnValue = $HostedMachine.Id} else {$ReturnValue = $false}

    return $ReturnValue
}

function Set-XDMaintenanceMode
{
    param (
        $MachineName = $(Read-Host 'Supply a value for the MachineName parameter')
    )

    Set-BrokerPrivateDesktop -MachineName $MachineName -InMaintenanceMode $True -ErrorAction SilentlyContinue

    return $?
}

function Remove-XDMachine
{
    param (
        $MachineName = $(Read-Host 'Supply a value for the MachineName parameter'),
        $DesktopGroup
    )

    if ($DesktopGroup) {
        Remove-BrokerMachine -MachineName $MachineName -Force -DesktopGroup $DesktopGroup -ErrorAction SilentlyContinue
    } else {
        Remove-BrokerMachine -MachineName $MachineName -Force -ErrorAction SilentlyContinue
    }

    return $?
}

function New-XDMachine
{
    param (
        $SID = $(Read-Host 'Supply a value for the SID parameter'),
        $CatalogUid = $(Read-Host 'Supply a value for the CatalogUid parameter'),
        $HostedMachineId = $(Read-Host 'Supply a value for the HostedMachineId parameter'),
        $HypervisorConnectionUid = $(Read-Host 'Supply a value for the HypervisorConnectionUid parameter')
    )

    New-BrokerMachine -MachineName $SID -CatalogUid $CatalogUid -HostedMachineId $HostedMachineId -HypervisorConnectionUid $HypervisorConnectionUid -ErrorAction SilentlyContinue

    return $?
}

function Add-XDMachineUser
{
    param (
        $Name = $(Read-Host 'Supply a value for the Name parameter'),
        $Machine = $(Read-Host 'Supply a value for the Machine parameter')
    )

    Add-BrokerUser -Name $Name -Machine $Machine -ErrorAction SilentlyContinue

    return $?
}

function Add-XDMachine
{
    param (
        $Machine = $(Read-Host 'Supply a value for the Machine parameter'),
        $DesktopGroup = $(Read-Host 'Supply a value for the DesktopGroup parameter')
    )

    Add-BrokerMachine -Machine $Machine -DesktopGroup $DesktopGroup -ErrorAction SilentlyContinue

    return $?
}

$ScriptPath = Split-Path $MyInvocation.MyCommand.Path

$Today = Get-Date
$Year  = $Today.Year
$Month = '{0:00}' -f $Today.Month
$Day   = '{0:00}' -f $Today.Day

$LogDir = Join-Path -Path $ScriptPath -ChildPath ('Logs-{0}.{1}.{2}' -f $Year, $Month, $Day)

if (!(Test-Path -Path $LogDir)) {mkdir $LogDir}

$LogFilePreference = Join-Path -Path $LogDir -ChildPath 'ScriptOutput.log'

$ErrorLog0  = Join-Path -Path $LogDir -ChildPath 'Error-GetTargetCatalogUid.log'
$ErrorLog1  = Join-Path -Path $LogDir -ChildPath 'Error-GetBrokerHypervisorConnectionUid.log'
$ErrorLog2  = Join-Path -Path $LogDir -ChildPath 'Error-GetVMsThroughHypervisorConnection.log'
$ErrorLog3  = Join-Path -Path $LogDir -ChildPath 'Error-GetDesktopsInSourceDesktopGroup.log'
$ErrorLog4  = Join-Path -Path $LogDir -ChildPath 'Error-GetHostedMachineId.log'
$ErrorLog5  = Join-Path -Path $LogDir -ChildPath 'Error-EnableMaintenanceMode.log'
$ErrorLog6  = Join-Path -Path $LogDir -ChildPath 'Error-RemoveMachineFromDesktopGroup.log'
$ErrorLog7  = Join-Path -Path $LogDir -ChildPath 'Error-RemoveMachineFromMachineCatalog.log'
$ErrorLog8  = Join-Path -Path $LogDir -ChildPath 'Error-JoinMachineCatalog.log'
$ErrorLog9  = Join-Path -Path $LogDir -ChildPath 'Error-AssociateUserNames.log'
$ErrorLog10 = Join-Path -Path $LogDir -ChildPath 'Error-JoinDesktopGroup.log'

$SuccessLog = Join-Path -Path $LogDir -ChildPath 'Success.log'

Write-Host 'Preparation tasks:'

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

Write-Host '- Load Citrix commands'

Add-PSSnapin Citrix* -ErrorAction SilentlyContinue
Get-PSSnapin Citrix* -ErrorAction SilentlyContinue | Out-Null
if (!$?) {
    Write-Host $Error[0].Exception -ForegroundColor Red
    return
}

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

Write-Host '- Get target catalog uid'

$CatalogUid = Get-XDBrokerCatalogUid -Name $TargetMC

if ($CatalogUid -eq $false) {
    Write-Host $Error[0].Exception -ForegroundColor Red
    Write-Log $TargetMC $ErrorLog0
    return
}

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

Write-Host '- Get broker hypervisor connection uid'

$HypervisorConnectionUid = Get-XDHypervisorConnectionUid -Name ($VmPath.Split('\')[2])

if ($HypervisorConnectionUid -eq $false) {
    Write-Host $Error[0].Exception -ForegroundColor Red
    Write-Log ($VmPath.Split('\')[2]) $ErrorLog1
    return
}

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

Write-Host '- Get VMs through hypervisor connection'

$VMs = Get-VMIDs -Path $VmPath

if ($VMs -eq $false) {
    Write-Host $Error[0].Exception -ForegroundColor Red
    Write-Log $VmPath $ErrorLog2
    return
}

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

Write-Host '- Get desktops in source desktop group'

$Desktops = Get-XDMachinesFromDesktopGroup -DesktopGroupName $SourceDG

if ($Desktops -eq $false) {
    Write-Host $Error[0].Exception -ForegroundColor Red
    Write-Log $SourceDG $ErrorLog3
    return
}

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

Write-Host "Preparation tasks finished.`n"

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

foreach ($Desktop in $Desktops)
{
    $AssociatedUserNames = $Desktop.AssociatedUserNames
    $MachineName = $Desktop.MachineName
    $SID = $Desktop.SID
    $HostedMachineId = ''

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

    Write-Host ("Processing desktop {0}:" -f $MachineName)

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

    Write-Host '- Get HostedMachineId:' -NoNewline

    $HostedMachineId = Get-VMHostedMachineId -VmList $VMs -Name $MachineName

    if ($HostedMachineId) {
        Write-Host ' OK' -ForegroundColor Green
    } else {
        Write-Host ' Unknown machine' -ForegroundColor Red
        Write-Log $MachineName $ErrorLog4
        continue
    }

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

    Write-Host '- Enable maintenance mode:' -NoNewline

    if (Set-XDMaintenanceMode -MachineName $MachineName) {
        Write-Host ' OK' -ForegroundColor Green
    } else {
        Write-Host ("`n{0}" -f $Error[0].Exception) -ForegroundColor Red
        Write-Log $MachineName $ErrorLog5
        continue
    }

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

    Write-Host '- Remove machine from (1) desktop group and (2) machine catalog:' -NoNewline

    if (Remove-XDMachine -MachineName $MachineName -DesktopGroup $SourceDG) {
        Write-Host ' 1=OK' -ForegroundColor Green
    } else {
        Write-Host ("`n{0}" -f $Error[0].Exception) -ForegroundColor Red
        Write-Log $MachineName $ErrorLog6
        continue
    }

    if (Remove-XDMachine -MachineName $MachineName) {
        Write-Host ' 2=OK' -ForegroundColor Green
    } else {
        Write-Host ("`n{0}" -f $Error[0].Exception) -ForegroundColor Red
        Write-Log $MachineName $ErrorLog7
        continue
    }

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

    Write-Host "- Join machine catalog $TargetMC:" -NoNewline

    if (New-XDMachine -SID $SID -CatalogUid $CatalogUid -HostedMachineId $HostedMachineId -HypervisorConnectionUid $HypervisorConnectionUid) {
        Write-Host ' OK' -ForegroundColor Green
    } else {
        Write-Host ("`n{0}" -f $Error[0].Exception) -ForegroundColor Red
        Write-Log $MachineName $ErrorLog8
        continue
    }

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

    Write-Host ("- Associate user names {0}:" -f $AssociatedUserNames) -NoNewline

    $AssociatedUserNames | % {
        if (Add-XDMachineUser -Name $_ -Machine $MachineName) {
            Write-Host " $_=OK" -ForegroundColor Green
        } else {
            Write-Host ("`n{0}" -f $Error[0].Exception) -ForegroundColor Red
            Write-Log $MachineName $ErrorLog9
            continue
        }
    }

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

    Write-Host "- Join desktop group $TargetDG:" -NoNewline

    if (Add-XDMachine -Machine $MachineName -DesktopGroup $TargetDG) {
        Write-Host ' OK' -ForegroundColor Green
    } else {
        Write-Host ("`n{0}" -f $Error[0].Exception) -ForegroundColor Red
        Write-Log $MachineName $ErrorLog10
        continue
    }

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

    Write-Host ("Processing desktop finished successfully - {0}`n" -f $MachineName)

    Write-Log $MachineName $SuccessLog
}

Disclaimer: I hope that the information in this post is valuable to you. Your use of the information contained in this post, however, is at your sole risk. All information on this post is provided “as is”, without any warranty, whether express or implied, of its accuracy, completeness, fitness for a particular purpose, title or non-infringement, and none of the third-party products or information mentioned in the work are authored, recommended, supported or guaranteed by me. Further, I shall not be liable for any damages you may sustain by using this information, whether direct, indirect, special, incidental or consequential, even if it has been advised of the possibility of such damages.

Open Explorer Window Here

From time to time, while working within a PowerShell command window, I want to open Windows Explorer with the current directory.

In order to achieve this “reverse command prompt here” functionality you just need to use Invoke-Item as follows:

ii .

(Btw, in the days of the legacy command prompt, I used to type "START ." to do the same.)

How To Listen To The File System With PowerShell?

Currently I am building the prototype of a PowerShell-based automation solution. From the high level perspective it will work like a Print Spooler meaning that some kind of watchdog listens in the background to a particular directory for incoming files in order to process them or rather pass them to a processing engine.

With PowerShell 2.0 it is an easy task to leverage the .NET Framework’s FileSystemWatcher Class to establish such a file system watchdog:

PS C:\Scripts> $fsw = New-Object System.IO.FileSystemWatcher
PS C:\Scripts> $fsw

NotifyFilter          : FileName, DirectoryName, LastWrite
EnableRaisingEvents   : False
Filter                : *.*
IncludeSubdirectories : False
InternalBufferSize    : 8192
Path                  :
Site                  :
SynchronizingObject   :
Container             :

PS C:\Scripts>

Obviously, a freshly initialized instance of System.IO.FileSystemWatcher is very roughly prepared to watch for file system changes. Five properties are preset, namely NotifyFilter (specifies the changes to watch for in a file system object), EnableRaisingEvents (indicates whether events are raised or not – yes, we want!), Filter (specifies what files are monitored), IncludeSubdirectories (indicates whether subdirectories are monitored or not), and InternalBufferSize. (Please look in the MSDN for more info on the FileSystemWatcher class.)

So, how about a PowerShell function to initialize a .NET FileSystemWatcher object with proper values for Patch, Filter, NotifyFilter, IncludeSubdirectories, and EnableRaisingEvents? Here we go!

function New-FSWatcher
{
    <#
    .SYNOPSIS
        Initializes a .NET file system watcher

    .DESCRIPTION
        Initializes a .NET file system watcher, given the specified directory and optionally the type of files to monitor.

    .PARAMETER Path
        Specifies the path of an existing directory to watch.

    .PARAMETER Filter
        Determines what files are monitored in Path. (Defaults to *.*)

    .PARAMETER NotifyFilter
        Specifies the types of changes to watch for. Valid types are FileName (default), DirectoryName (default), Attributes, Size, LastWrite (default), LastAccess (default), CreationTime, and Security.

    .PARAMETER Recurse
        Indicates whether subdirectories within the Path should be monitored.

    .OUTPUTS
        Returns a .NET FileSystemWatcher object

    .EXAMPLE
        PS C:\> $fsw = New-FSWatcher -Path 'C:\Temp'

        This command initializes a file system watcher for *.* in C:\Temp.

    .EXAMPLE
        PS C:\> $fswxml = New-FSWatcher C:\Temp *.xml -Recurse

        This command initializes a file system watcher for *.xml in C:\Temp including its subdirectories.

    .LINK
        Start-FSWatcher
        Register-FSWatcherEventHandler
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path
        ,
        [Parameter(Mandatory=$false, Position=1)]
        [String]
        $Filter = '*.*'
        ,
        [Parameter(Mandatory=$false)]
        [System.IO.NotifyFilters]
        $NotifyFilter = ('FileName','LastWrite','LastAccess')
        ,
        [Switch]
        $Recurse
    )

    if ($PSCmdlet.ShouldProcess("$Path\$Filter", "Initialize FileSystemWatcher"))
    {
        $FileSystemWatcher = New-Object System.IO.FileSystemWatcher
        $FileSystemWatcher.Path = $Path
        $FileSystemWatcher.Filter = $Filter
        $FileSystemWatcher.NotifyFilter = $NotifyFilter
        if ($Recurse)
        {
            $FileSystemWatcher.IncludeSubdirectories = $true
        }
        $FileSystemWatcher.EnableRaisingEvents = $true
        $FileSystemWatcher
    }
}

That’s only half the battle. With New-FSWatcher you are able to initialize a FileSystemWatcher. But how to start monitoring?

If you look at the methods of System.IO.FileSystemWatcher you will sooner or later discover WaitForChanged(), "a synchronous method that returns a structure that contains specific information on the change that occurred, given the type of change you want to monitor and the time (in milliseconds) to wait before timing out".

So, with WaitForChanged() it is perfectly possible to stop script processing for a given amount of maximum time in order to wait for a file. Apart from the fact that I have a watchdog in mind that should wait for file system events in the background, I will provide a function to leverage a synchronous (foreground) wait for files anyways:

function Start-FSWatcher
{
    <#
    .SYNOPSIS
        Listens the file system changes for a given amount of time.

    .DESCRIPTION
        Uses a previously configured FileSystemWatcher to listen for changes in a directory using a synchronous method that returns specific information on each change that occurred, given the type of change you want to monitor and the time to wait before timing out.

    .PARAMETER FileSystemWatcher
        Specifies a FileSystemWatcher object.

    .PARAMETER Type
        Specifies the type of change you want to monitor. Valid type are All (default), Changed, Created, Deleted, Disposed, Error, and Renamed.

    .PARAMETER TimeOut
        Specifies the timeout in ms. (Defaults to 10000)

    .PARAMETER Infinite
        Specifies to watch infinitely. (Not recommended. Consider registering events instead.)

    .OUTPUTS
        A structure containing specific information on each change that occurred.

    .NOTES
        Hidden files are NOT ignored.

    .EXAMPLE
        PS C:\> $fsw = New-FSWatcher -Path 'C:\Temp' -Filter 'jobfinished.txt'
        PS C:\> Start-FSWatcher -FSW $fsw

        These two commands initialize and use a FileSystemWather for C:\Temp. The watcher listens 10 seconds (default) for any changes in the directory.

    .EXAMPLE
        PS C:\> New-FSWatcher C:\Temp *.xml | Start-FSWatcher -Infinite

        This pipeline shows how to start an infinite FileSystemWatcher for any change on any XML file in C:\Temp.

    .LINK
        New-FSWatcher
        Register-FSWatcherEventHandler
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [Alias("FSW")]
        [ValidateNotNullOrEmpty()]
        [System.IO.FileSystemWatcher]
        $FileSystemWatcher
        ,
        [Parameter(Mandatory=$false)]
        [ValidateSet('All','Changed', 'Created', 'Deleted', 'Disposed', 'Error', 'Renamed')]
        [String]
        $Type = 'All'
        ,
        [Parameter(Mandatory=$false)]
        [Int]
        $TimeOut = 10000
        ,
        [Switch]
        $Infinite
    )

    if ($PSCmdlet.ShouldProcess("$($FileSystemWatcher.Path)", "WaitForChanged($Type, $TimeOut)"))
    {
        do
        {
            $FileSystemChange = $FileSystemWatcher.WaitforChanged($Type, $TimeOut)
            if (!$FileSystemChange.TimedOut)
            {
                $FileSystemChange
            }
        }
        while ($Infinite)
    }
}

Please note that Start-FSWatcher will stop monitoring the file system as soon as the first change occurs that matches the FileSystemWatcher object’s configuration (unless the Infinite switch has been specified which causes the function to invoke the WaitForChanges() method again and again till the end of time). If you want to wait for a particular file you can define a FileSystemWatcher object and setup its Filter property to match the exact file name. Therefore, concerning a specific file name to be monitored, there’s no need to change the function.

To return to square one finally: how to define the file system watcher event handlers that will be fired on change, creation, deletion, or renaming of a file or directory? This is really straightforward. Basically you need to use the Register-ObjectEvent Cmdlet with a previously initialized FileSystemWatcher object, the event to act on, and the action to be executed on that event. Although it seems to be overkill, I’ll provide a third function, Register-FSWatcherEventHandler, that helps to define a proper event action for a file system watcher:

function Register-FSWatcherEventHandler
{
    <#
    .SYNOPSIS
        Registers a FileSystemWatcher event handler.

    .DESCRIPTION
        Specifies what is done when a file is changed, created, deleted, or renamed.

    .PARAMETER FileSystemWatcher
        Specifies a FileSystemWatcher object

    .PARAMETER EventName
        Specifies the type of event. Valid events are Changed, Created, Deleted, Disposed, Error, and Renamed.

    .PARAMETER EventAction
        Specifies what is done.

    .EXAMPLE
        PS C:\Scripts> $fsw = New-FSWatcher -Path 'C:\Temp'
        PS C:\Scripts> Register-FSWatcherEventHandler $fsw 'Created' -Action {
        >> Remove-Item $($eventArgs.FullPath) -Confirm
        }

        These two commands initialize a FileSystemWather for C:\Temp and registers an event handler which will offer to delete any created file.

    .LINK
        New-FSWatcher
        Start-FSWatcher
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [Alias("FSW")]
        [ValidateNotNullOrEmpty()]
        [System.IO.FileSystemWatcher]
        $FileSystemWatcher
        ,
        [Parameter(Mandatory=$true, Position=1)]
        [ValidateSet('Changed', 'Created', 'Deleted', 'Disposed', 'Error', 'Renamed')]
        [String]
        $EventName
        ,
        [Parameter(Mandatory=$true, Position=2)]
        [Alias('Action')]
        [Scriptblock]
        $EventAction
    )

    if ($PSCmdlet.ShouldProcess("$($FileSystemWatcher.Path)", "Register Event Handler (for File $EventName)"))
    {
        Register-ObjectEvent -InputObject $FileSystemWatcher -EventName $EventName -Action $EventAction
    }
}

Have fun! :-)

Pit

PowerShell Proxy Extensions (Beta)

A few moments ago, at the PowerShell Deep Dive in Frankfurt, Kirk "Poshoholic" Munro and Shay Levy published the Beta of PSPXPowerShell Proxy Extensions Module (Project at CodePlex).

PSPX leverages so-called Proxy Functions in order to extend the functionality of native PowerShell Cmdlets. What is a proxy function? Basically, a proxy function is a wrapper around a Cmdlet. A proxy function has access to the Cmdlet’s parameters and has control over the command steppable pipeline. Implementing a proxy function is not a piece of cake. Earlier this year, Shay wrote about proxy functions as a guest blogger in the Hey, Scripting Guy! blog. It’s a great introduction.

With the PSPX you don’t need to care about that stuff behind the scenes. PSPX encapsulates complexity and therefore makes it very easy to extend PowerShell cmdlets.

Example: in PowerShell 1.0 and 2.0 the Get-ChildItem doesn’t come with a -Container switch meaning that if you want to get only Container objects (for example Directories/Folders in a file system) you need to pipe Get-ChildItem’s results to Where-Object {$_.PSIsContainer}. Wouldn’t it be nice if the Get-ChildItem had a -Container switch? With, PSPX you can fix Get-ChildItem on your own – and it’s very easy:

Fix-It Get-ChildItem `
    -Parameter @{
        Name = 'Container'
        PostProcess = {
            Where-Object {$_.PSIsContainer}
        }
    } `
    -DefineNow

This is just a simple example. Download PSPX, it’s an MSI file, from the CodePlex, install it, load the pspx module, and play with it… ;-)

PowerShell Function to Set Page File Initial/Maximum Size

The PowerShell 2.0 function below, Set-PageFile, sets a page file to the given initial and maximum size.

At first sight this seems to be an easy task – just set the properties InitialSize and MaximumSize accordingly (Win32_PageFileSetting class). The tricky part is that the system won’t let you change any page file setting as long as the system automatically manages these settings. My function takes care of this fact and disables the Win32_ComputerSystem‘s AutomaticManagedPagefile property if necessary.

The function supports PowerShell’s -WhatIf switch. Therefore you can test it safely without any impact.

function Set-PageFile
{
    <#
    .SYNOPSIS
        Sets Page File to custom size

    .DESCRIPTION
        Applies the given values for initial and maximum page file size.

    .PARAMETER Path
        The page file's fully qualified file name (such as C:\pagefile.sys)

    .PARAMETER InitialSize
        The page file's initial size [MB]

    .PARAMETER MaximumSize
        The page file's maximum size [MB]

    .EXAMPLE
        C:\PS> Set-PageFile "C:\pagefile.sys" 4096 6144
    #>

    [CmdletBinding(SupportsShouldProcess=$True)]
    param (
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path,
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateNotNullOrEmpty()]
        [Int]
        $InitialSize,
        [Parameter(Mandatory=$true,Position=2)]
        [ValidateNotNullOrEmpty()]
        [Int]
        $MaximumSize
    )

    Set-PSDebug -Strict

    $ComputerSystem = $null
    $CurrentPageFile = $null
    $Modified = $false

    # Disables automatically managed page file setting first
    $ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem -EnableAllPrivileges
    if ($ComputerSystem.AutomaticManagedPagefile)
    {
        $ComputerSystem.AutomaticManagedPagefile = $false
        if ($PSCmdlet.ShouldProcess("$($ComputerSystem.Path.Server)", "Disable automatic managed page file"))
        {
            $ComputerSystem.Put()
        }
    }

    $CurrentPageFile = Get-WmiObject -Class Win32_PageFileSetting
    if ($CurrentPageFile.Name -eq $Path)
    {
        # Keeps the existing page file
        if ($CurrentPageFile.InitialSize -ne $InitialSize)
        {
            $CurrentPageFile.InitialSize = $InitialSize
            $Modified = $true
        }
        if ($CurrentPageFile.MaximumSize -ne $MaximumSize)
        {
            $CurrentPageFile.MaximumSize = $MaximumSize
            $Modified = $true
        }
        if ($Modified)
        {
            if ($PSCmdlet.ShouldProcess("Page file $Path", "Set initial size to $InitialSize and maximum size to $MaximumSize"))
            {
                $CurrentPageFile.Put()
            }
        }
    }
    else
    {
        # Creates a new page file
        if ($PSCmdlet.ShouldProcess("Page file $($CurrentPageFile.Name)", "Delete old page file"))
        {
            $CurrentPageFile.Delete()
        }
        if ($PSCmdlet.ShouldProcess("Page file $Path", "Set initial size to $InitialSize and maximum size to $MaximumSize"))
        {
            Set-WmiInstance -Class Win32_PageFileSetting -Arguments @{Name=$Path; InitialSize = $InitialSize; MaximumSize = $MaximumSize}
        }
    }
}

Function to Bulk-Register PowerShell Snap-Ins

I wrote the PowerShell function below, Register-PSSnapin, to facilitate the usage of the InstallUtil.exe program in order to register a series of PowerShell snap-ins. Since this utility isn’t located in the  normal  command path, you have to find it in the .NET Framework’s directory. Or, with my function you just don’t care ;-)

Side note: in PowerShell 2.0 the concept of snap-ins is substantially replaced by binary modules. Apart from the fact that the core of PowerShell 2.0 is delivered as snap-ins, many software vendors still provide a PowerShell snap-in to allow for command-line based administration and automation. (For example the latest release of Citrix Provisioning Services 6.0 ships a PowerShell Snap-in.)

<#
.SYNOPSIS
Registers PowerShell Snap-ins.

.DESCRIPTION
Uses the InstallUtil tool in the .NET Framework to register one ore more snap-ins

.PARAMETER Path
Specifies the path to the file name or "module name" of the snap-in

.EXAMPLE
C:\PS> Register-PSSnapin -Path C:\Dev\Management\ManagementCmdlets.dll
#>
function Register-PSSnapin
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Path
    )

    $CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
    if (!($CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)))
    {
        Write-Host -ForegroundColor Red 'This script requires elevated permission.'
        Exit
    }
    $InstallUtil = Join-Path (Split-path ([Object].Assembly.Location) -Parent) InstallUtil.exe
    foreach ($PathElement in $Path)
    {
        if (Test-Path -Path $PathElement)
        {
            if ($PSCmdlet.ShouldProcess("$PathElement", 'Register PowerShell Snap-in'))
            {
                Start-Process -FilePath $InstallUtil -ArgumentList $PathElement -Wait
            }
        }
        else
        {
            Write-Host "File not found - $PathElement"
        }
    }
}

Can I use the XenApp 6 Migration Tool on XenApp 6.5?

The XenApp 6 Migration Tool is a PowerShell 2.0 Module that pulls data from a MFCOM-based XenApp farm and adds it to a XenApp 6 server farm. As XenApp 6.5 is around now, including almost the same set of Cmdlets like its predecessor, it seems to be perfectly possible to use that Tool to migrate from XenApp 5 to XenApp 6.5 for example. So what’s the catch?

The answer is pretty simple: Citrix XenApp 6.5 has built-in migration capabilities which includes the former Migration Tool cmdlets. Therefore, don’t care about the past and take a closer look at the XenApp 6.5 Migration Center ;-)

The Migration Center is a PowerShell module and you can use it through a GUI or a command-line.

Citrix XenApp 6.0 to 6.5 Upgrade Script/Utility

Let’s say you have to upgrade Citrix XenApp 6.0 to 6.5. Since the XenApp 6.5 installer doesn’t include an upgrade path from 6.0 you would end up uninstalling all 6.0 components in correct order and finally installing XenApp 6.5 – manually. No better way? Yes, there seems to be a better way with a neat PowerShell script from Citrix.

The script XenAppUpgrade.ps1 (download) performs what you normally would do manually in order to upgrade a XenApp server from 6.0 to 6.5

The Citrix Support document CTX130614 outlines how to install, to prepare, and to use the script.