<< Back to Script Library

Find FSlogix mounts from share

Searches the given list of shares, or pulls them from the registry if * specified and the script is running on a machine with FSlogix installed, for .metadata files and extracts the name of the machine where the corresponding vhd/vhdx file is mounted along with other useful data about the disk.

The last boot time of the machine where the disk is mounted can be retrieved where if this is empty/missing it likely means that the user running the script does not have WMI/CIM permissions or that machine is not powered on.

Both profile and Office disks will be reported on.
Version: 2.0.12
Created: 2022-10-06
Modified: 2022-12-14
Creator: Guy Leech
Downloads: 30
Tags:
The Script Copy Script Copied to clipboard
#require -version 3

<#
.SYNOPSIS 
    Get date/time and location of where FSlogix disks mounted via the .metadata file

.PARAMETER shares
    Comma separated list of shares to examine. If not specified, the registry will be checked for FSlogix settings
    
.PARAMETER boottime
    Include the last boot time of the machines where the disk is mounted. Could be slow if machines are not booted and/or if WMI/CIM access not available

.PARAMETER filePattern
    The file pattern to search to find the meta data files. It is not recommended to change this

.PARAMETER operationTimeoutSeconds
    How long in seconds to allow the WMI/CIM operation to retrieve the last boot time to run

.NOTES
    Modification History:

    2022/10/06  @guyrleech  Initial Release
    2022/10/31  @guyrleech  Exclude CORRUPT_* files
#>

[CmdletBinding()]

Param
(
    [ValidateSet('yes','no')]
    [string]$bootTime = 'yes' ,
    [string[]]$shares = @( '*' ) ,  # placeholder so CU can pass * to mean get from registry
    [string]$filePattern = '*.metadata' ,
    [int]$operationTimeoutSeconds = 30
)

#region ControlUp_Standards
$VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'

[int]$outputWidth = 400
try
{
    if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
    {
        $WideDimensions.Width = $outputWidth
        $PSWindow.BufferSize = $WideDimensions
    }
}
catch
{
    ## not the ennd of the world but we don't want this to stop the script from completing
}

#endregion ControlUp_Standards

if( $null -eq $shares -or $shares.Count -eq 0 -or $shares[0].Length -eq 1 )
{
    [string]$vhdLocationsValueName = 'VHDLocations'
    $shares = @( ForEach( $key in @( 'HKLM:\Software\FSlogix\Profiles' , 'HKLM:\SOFTWARE\Policies\FSLogix\ODFC' ) )
    {
        if( $fslogixprofileKey = Get-ItemProperty -Path $key -Name $vhdLocationsValueName -ErrorAction SilentlyContinue )
        {
            $fslogixprofileKey.VHDLocations
        }
        else
        {
            Write-Warning "No FSlogix profile root passed and no '$vhdLocationsValueName' value in FSlogix registry key $key"
        }
    })
}
elseif( $shares.Count -eq 1 -and $shares[0].IndexOf( ',' ) -ge 0 ) ## array can be flattened if called outside of PS, eg scheduled task
{
    $shares = @( $shares -split ',' )
}

if( $null -eq $shares -or $shares.Count -eq 0 )
{
    Throw "No shares passed and no FSlogix registry configuration present"
}

[double]$totalVHDSize = 0
[int]$totalVHDs = 0
[string]$baseFileRegex = [regex]::Escape( ( $filePattern -replace '^\*' ) )
[hashtable]$machineOsInfo = @{}

[array]$results = @( ForEach( $share in $shares )
{
    Write-Verbose -Message "Enumerating share $share"
    Get-ChildItem -Path $share -Include $filePattern -Recurse -Force | Where-Object Name -NotMatch '^CORRUPT_' | ForEach-Object `
    {
        $file = $_
        [string]$mountedOn = 'N/A'
        [byte[]]$bytes = @()
        ## can't use Get-Content as -Encoding Byte not available in PS 7 so this way keeps code easier and file should only be small
        $bytes = [System.IO.File]::ReadAllBytes( $file.FullName )

        [int]$lastCharacter = -1

        if( $null -ne $bytes -and $bytes.Count -gt 0 )
        {
            ## get first non-control character (it is actually unicode) and build string from there until 00 00 terminator
            <#
            00000000   01 00 00 00 01 00 00 00 01 00 00 00 47 00 4C 00  ............G.L.
            00000010   57 00 31 00 30 00 43 00 54 00 58 00 4D 00 43 00  W.1.0.C.T.X.M.C.
            00000020   53 00 50 00 30 00 31 00 2E 00 67 00 75 00 79 00  S.P.0.1...g.u.y.
            00000030   72 00 6C 00 65 00 65 00 63 00 68 00 2E 00 6C 00  r.l.e.e.c.h...l.
            00000040   6F 00 63 00 61 00 6C 00 00 00 00 00 00 00 00 00  o.c.a.l.........
            00000050   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
            #>
            [int]$startIndex = -1
            [int]$endIndex = -1
            For( [int]$index = 0 ; $index -lt $bytes.Count ; $index++ )
            {
                if( $startIndex -lt 0 -and $bytes[ $index ] -ge 32 -and $index -gt 0 -and $lastCharacter -eq 0x0 ) ## ascii space with preceding zero byte as Unicode
                {
                    $startIndex = $index - 1
                }
                elseif( $startIndex -ge 0 -and $bytes[ $index ] -eq 0x0 -and $lastCharacter -eq 0x0 )
                {
                    $endIndex = $index
                    break
                }
                $lastCharacter = $bytes[ $index ]
            }

            if( $startIndex -ge 0 )
            {
                if( $endIndex -gt $startIndex )
                {
                    $mountedOn = [System.Text.Encoding]::BigEndianUnicode.GetString( $bytes , $startindex , $endIndex - $startIndex - 1 )
                }
                else
                {
                    Write-Warning -Message "Unable to find end of string in `"$($file.FullName)`""
                }
            }
            else
            {
                Write-Warning -Message "Unable to find start of string in `"$($file.FullName)`""
            }
        }
        elseif( $fileError )
        {
            Write-Warning -Message "Error reading `"$($file.FullName)`" - $fileError"
        }

        [string]$username = 'N/A'

        $parentPath = $null
        if( $parentPath = (Split-Path -Path (Split-Path -path $file -parent) -Leaf) )
        {
            if( $parentPath -match '(S-\d-\d-\d+-\d+-\d+-\d+-\d+)' )
            {
                [string]$sid = $Matches[ 1 ]
                if( $resolvedSid = ([System.Security.Principal.SecurityIdentifier]( $sid )).Translate([System.Security.Principal.NTAccount]).Value )
                {
                    $username = $resolvedSid
                }
                else
                {
                    $username = $sid
                }
            }
            else ## TODO fallback looking for username in file or path
            {
            }
        }

        $baseFile = $null    
        $baseFile = Get-ItemProperty -Path ($file.FullName -replace "$baseFileRegex`$") -ErrorAction SilentlyContinue

        $result = [pscustomobject]@{
            'Disk' = $baseFile | Select-Object -ExpandProperty Name
            'Location' = $baseFile | Select-Object -ExpandProperty DirectoryName
            'Username' = $username
            'Mounted'  = $file.CreationTime
            'Machine'  = $mountedOn
            'Disk Created' = $baseFile | Select-Object -ExpandProperty CreationTime
            'Disk Last Modified' = $baseFile | Select-Object -ExpandProperty LastWriteTime
            'Disk Last Accessed' = $baseFile | Select-Object -ExpandProperty LastAccessTime
            'Disk Size (MB)' = [math]::Round( ( $baseFile | Select-Object -ExpandProperty Length ) / 1MB , 1 )
        }

        if( $bootTime -ieq 'yes' -and $mountedOn -ne 'N/A' )
        {
            if( $null -eq ( $osinfo =  $machineOsInfo[ $mountedOn ] ) )
            {
                Write-Verbose -Message "$(Get-Date -Format G): getting boot time of $mountedOn"
                $osinfo = Get-CimInstance -ClassName Win32_OperatingSystem -OperationTimeoutSec $operationTimeoutSeconds -ComputerName $mountedOn -ErrorAction SilentlyContinue
                if( $osinfo )
                {
                    $machineOsInfo.Add( $mountedOn , $osinfo )
                }
                else
                {
                    ## add even if $null so we don't try a non-booted/bad machine again which will delay script further
                    $machineOsInfo.Add( $mountedOn , $false )
                }              
            }

            Add-Member -InputObject $result -MemberType NoteProperty -Name 'Boot Time' -Value ($osinfo | Select-Object -ExpandProperty LastBootUpTime -ErrorAction SilentlyContinue)
        }

        $result
    }
})

## Remove Format-Table to use outside of ControlUp
$results | Format-Table -AutoSize