Health Check App Volumes End-Point

Perform health check analysis of VMware App Volumes on an end-point, reporting issues that may be or were user impacting. Also shows disk mounts and durations for App Volumes mounted for current user sessions.

Version: 2.7.15
Created: 2020-06-30
Modified: 2020-08-27
Creator: Guy Leech
Downloads: 3
Tags:
The Script Copy Script Copied to clipboard

#required -version 3
<# Health check VMware App Volumes end-point @guyrleech 2020 Modification History: @guyrleech 27/08/2020 Added code for edge case where SQL down when user logs in #>

[CmdletBinding()]

Param
(
[double]$lastDays = 0 ,
[string]$serviceName = ‘svservice’ ,
[string]$serviceProcessName = ‘svservice’ ,
[string]$fullServiceName = ‘App Volumes Service’ ,
[string]$driverName = ‘svdriver’ ,
[string]$productName = ‘App Volumes’ ,
[string]$configKeyName = ‘HKLM:SOFTWAREWOW6432NodeCloudVolumesAgent’ ,
[string]$mountPoint = ‘\SnapVolumesTemp\’ ,
[int]$outputWidth = 400
)

$VerbosePreference = $(if( $PSBoundParameters[ ‘verbose’ ] ) { $VerbosePreference } else { ‘SilentlyContinue’ })
$DebugPreference = $(if( $PSBoundParameters[ ‘debug’ ] ) { $DebugPreference } else { ‘SilentlyContinue’ })
$ErrorActionPreference = $(if( $PSBoundParameters[ ‘erroraction’ ] ) { $ErrorActionPreference } else { ‘Stop’ })
$ProgressPreference = ‘SilentlyContinue’

## https://www.codeproject.com/Articles/18179/Using-the-Local-Security-Authority-to-Enumerate-Us
$LSADefinitions = @’
[DllImport(“secur32.dll”, SetLastError = false)]
public static extern uint LsaFreeReturnBuffer(IntPtr buffer);

[DllImport(“Secur32.dll”, SetLastError = false)]
public static extern uint LsaEnumerateLogonSessions
(out UInt64 LogonSessionCount, out IntPtr LogonSessionList);

[DllImport(“Secur32.dll”, SetLastError = false)]
public static extern uint LsaGetLogonSessionData(IntPtr luid,
out IntPtr ppLogonSessionData);

[StructLayout(LayoutKind.Sequential)]
public struct LSA_UNICODE_STRING
{
public UInt16 Length;
public UInt16 MaximumLength;
public IntPtr buffer;
}

[StructLayout(LayoutKind.Sequential)]
public struct LUID
{
public UInt32 LowPart;
public UInt32 HighPart;
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_LOGON_SESSION_DATA
{
public UInt32 Size;
public LUID LoginID;
public LSA_UNICODE_STRING Username;
public LSA_UNICODE_STRING LoginDomain;
public LSA_UNICODE_STRING AuthenticationPackage;
public UInt32 LogonType;
public UInt32 Session;
public IntPtr PSiD;
public UInt64 LoginTime;
public LSA_UNICODE_STRING LogonServer;
public LSA_UNICODE_STRING DnsDomainName;
public LSA_UNICODE_STRING Upn;
}

public enum SECURITY_LOGON_TYPE : uint
{
Interactive = 2, //The security principal is logging on
//interactively.
Network, //The security principal is logging using a
//network.
Batch, //The logon is for a batch process.
Service, //The logon is for a service account.
Proxy, //Not supported.
Unlock, //The logon is an attempt to unlock a workstation.
NetworkCleartext, //The logon is a network logon with cleartext
//credentials.
NewCredentials, //Allows the caller to clone its current token and
//specify new credentials for outbound connections.
RemoteInteractive, //A terminal server session that is both remote
//and interactive.
CachedInteractive, //Attempt to use the cached credentials without
//going out across the network.
CachedRemoteInteractive,// Same as RemoteInteractive, except used
// internally for auditing purposes.
CachedUnlock // The logon is an attempt to unlock a workstation.
}
‘@

Add-Type -ErrorAction Stop -TypeDefinition @’
using System;
using System.Runtime.InteropServices;
public enum WTS_CONNECTSTATE_CLASS
{
WTSActive,
WTSConnected,
WTSConnectQuery,
WTSShadow,
WTSDisconnected,
WTSIdle,
WTSListen,
WTSReset,
WTSDown,
WTSInit
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct WTSINFOEX_LEVEL1_W {
public Int32 SessionId;
public WTS_CONNECTSTATE_CLASS SessionState;
public Int32 SessionFlags; // 0 = locked, 1 = unlocked , ffffffff = unknown
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
public string WinStationName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)]
public string UserName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 18)]
public string DomainName;
public UInt64 LogonTime;
public UInt64 ConnectTime;
public UInt64 DisconnectTime;
public UInt64 LastInputTime;
public UInt64 CurrentTime;
public Int32 IncomingBytes;
public Int32 OutgoingBytes;
public Int32 IncomingFrames;
public Int32 OutgoingFrames;
public Int32 IncomingCompressedBytes;
public Int32 OutgoingCompressedBytes;
}
[StructLayout(LayoutKind.Sequential)]
public struct WTS_SESSION_INFO
{
public Int32 SessionID;

[MarshalAs(UnmanagedType.LPStr)]
public String pWinStationName;

public WTS_CONNECTSTATE_CLASS State;
}
[StructLayout(LayoutKind.Explicit)]
public struct WTSINFOEX_LEVEL_W
{ //Union
[FieldOffset(0)]
public WTSINFOEX_LEVEL1_W WTSInfoExLevel1;
}
[StructLayout(LayoutKind.Sequential)]
public struct WTSINFOEX
{
public Int32 Level ;
public WTSINFOEX_LEVEL_W Data;
}
public enum WTS_INFO_CLASS
{
WTSInitialProgram,
WTSApplicationName,
WTSWorkingDirectory,
WTSOEMId,
WTSSessionId,
WTSUserName,
WTSWinStationName,
WTSDomainName,
WTSConnectState,
WTSClientBuildNumber,
WTSClientName,
WTSClientDirectory,
WTSClientProductId,
WTSClientHardwareId,
WTSClientAddress,
WTSClientDisplay,
WTSClientProtocolType,
WTSIdleTime,
WTSLogonTime,
WTSIncomingBytes,
WTSOutgoingBytes,
WTSIncomingFrames,
WTSOutgoingFrames,
WTSClientInfo,
WTSSessionInfo,
WTSSessionInfoEx,
WTSConfigInfo,
WTSValidationInfo, // Info Class value used to fetch Validation Information through the WTSQuerySessionInformation
WTSSessionAddressV4,
WTSIsRemoteSession
}
public static class wtsapi
{
[DllImport(“wtsapi32.dll”, SetLastError=true)]
public static extern int WTSQuerySessionInformationW(
System.IntPtr hServer,
int SessionId,
int WTSInfoClass ,
ref System.IntPtr ppSessionInfo,
ref int pBytesReturned );

[DllImport(“wtsapi32.dll”, SetLastError=true)]
public static extern int WTSEnumerateSessions(
System.IntPtr hServer,
int Reserved,
int Version,
ref System.IntPtr ppSessionInfo,
ref int pCount);

[DllImport(“wtsapi32.dll”, SetLastError=true)]
public static extern IntPtr WTSOpenServer(string pServerName);

[DllImport(“wtsapi32.dll”, SetLastError=true)]
public static extern void WTSCloseServer(IntPtr hServer);

[DllImport(“wtsapi32.dll”, SetLastError=true)]
public static extern void WTSFreeMemory(IntPtr pMemory);
}
‘@

Function Get-WTSSessionInformation
{
[cmdletbinding()]

Param
(
[string[]]$computers = @( $null )
)

[long]$count = 0
[IntPtr]$ppSessionInfo = 0
[IntPtr]$ppQueryInfo = 0
[long]$ppBytesReturned = 0
$wtsSessionInfo = New-Object -TypeName ‘WTS_SESSION_INFO’
$wtsInfoEx = New-Object -TypeName ‘WTSINFOEX’
[int]$datasize = [system.runtime.interopservices.marshal]::SizeOf( [Type]$wtsSessionInfo.GetType() )

ForEach( $computer in $computers )
{
[string]$machineName = $(if( $computer ) { $computer } else { $env:COMPUTERNAME })
[IntPtr]$serverHandle = [wtsapi]::WTSOpenServer( $computer )

## If the function fails, it returns a handle that is not valid. You can test the validity of the handle by using it in another function call.

[long]$retval = [wtsapi]::WTSEnumerateSessions( $serverHandle , 0 , 1 , [ref]$ppSessionInfo , [ref]$count );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()

if ($retval -ne 0)
{
for ([int]$index = 0; $index -lt $count; $index++)
{
$element = [system.runtime.interopservices.marshal]::PtrToStructure( [long]$ppSessionInfo + ($datasize * $index), [type]$wtsSessionInfo.GetType())
if( $element -and $element.SessionID -ne 0 ) ## session 0 is non-interactive (session zero isolation)
{
$retval = [wtsapi]::WTSQuerySessionInformationW( $serverHandle , $element.SessionID , [WTS_INFO_CLASS]::WTSSessionInfoEx , [ref]$ppQueryInfo , [ref]$ppBytesReturned );$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
if( $retval -and $ppQueryInfo )
{
$value = [system.runtime.interopservices.marshal]::PtrToStructure( $ppQueryInfo , [Type]$wtsInfoEx.GetType())
if( $value -and $value.Data -and $value.Data.WTSInfoExLevel1.SessionState -ne [WTS_CONNECTSTATE_CLASS]::WTSListen -and $value.Data.WTSInfoExLevel1.SessionState -ne [WTS_CONNECTSTATE_CLASS]::WTSConnected )
{
$wtsinfo = $value.Data.WTSInfoExLevel1
$idleTime = New-TimeSpan -End ([datetime]::FromFileTimeUtc($wtsinfo.CurrentTime)) -Start ([datetime]::FromFileTimeUtc($wtsinfo.LastInputTime))
Add-Member -InputObject $wtsinfo -Force -NotePropertyMembers @{
‘IdleTimeInSeconds’ = $idleTime | Select -ExpandProperty TotalSeconds
‘IdleTimeInMinutes’ = $idleTime | Select -ExpandProperty TotalMinutes
‘Computer’ = $machineName
‘LogonTime’ = [datetime]::FromFileTime( $wtsinfo.LogonTime )
‘DisconnectTime’ = [datetime]::FromFileTime( $wtsinfo.DisconnectTime )
‘LastInputTime’ = [datetime]::FromFileTime( $wtsinfo.LastInputTime )
‘ConnectTime’ = [datetime]::FromFileTime( $wtsinfo.ConnectTime )
‘CurrentTime’ = [datetime]::FromFileTime( $wtsinfo.CurrentTime )
}
$wtsinfo
}
[wtsapi]::WTSFreeMemory( $ppQueryInfo )
$ppQueryInfo = [IntPtr]::Zero
}
else
{
Write-Error “$($machineName): $LastError”
}
}
}
}
else
{
Write-Error “$($machineName): $LastError”
}

if( $ppSessionInfo -ne [IntPtr]::Zero )
{
[wtsapi]::WTSFreeMemory( $ppSessionInfo )
$ppSessionInfo = [IntPtr]::Zero
}
[wtsapi]::WTSCloseServer( $serverHandle )
$serverHandle = [IntPtr]::Zero
}
}

[datetime]$startDate = Get-Date -Date “01/01/2000”

if( $PSBoundParameters[ ‘lastDays’ ] )
{
$startDate = (Get-Date).AddDays( -$lastDays )
Write-Output “Checking from $(Get-Date -Date $startDate -Format G)”
}

# Altering the size of the PS Buffer
if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ($WideDimensions = $PSWindow.BufferSize) )
{
$WideDimensions.Width = $outputWidth
$PSWindow.BufferSize = $WideDimensions
}

$warnings = New-Object -TypeName System.Collections.Generic.List[string]
$information = New-Object -TypeName System.Collections.Generic.List[psobject]

[string]$appVolumesVersion = $null
[datetime]$installDate = New-Object -TypeName DateTime

if( ( $appvolumesKey = Get-ItemProperty -Path ‘HKLM:SOFTWAREWow6432NodeMicrosoftWindowsCurrentVersionUninstall*’ -Name DisplayName -ErrorAction SilentlyContinue | Where-Object DisplayName -match $productName | Select-Object -ExpandProperty PSPath )

    -or ( $appvolumesKey = Get-ItemProperty -Path 'HKLM:SOFTWAREMicrosoftWindowsCurrentVersionUninstall*' -Name DisplayName -ErrorAction SilentlyContinue | Where-Object DisplayName -match $productName | Select-Object -ExpandProperty PSPath ) )
{
    $appVolumesVersion = Get-ItemProperty -Path $appvolumesKey -Name DisplayVersion | Sort-Object -Descending -Property DisplayVersion | Select-object -ExpandProperty DisplayVersion -First 1
    if( ( [string]$installedOn =  Get-ItemProperty -Path $appvolumesKey -Name InstallDate -ErrorAction SilentlyContinue | Select-Object -ExpandProperty InstallDate )

-and [datetime]::TryParseExact( $installedOn , ‘yyyyMMdd’ , [System.Globalization.CultureInfo]::InvariantCulture , [System.Globalization.DateTimeStyles]::None , [ref]$installdate ) )
{
$information.Add( ([pscustomobject]@{ ‘Item’ = “$productName Installed On” ; ‘Description’ = “$(Get-Date -Date $installDate -Format d)” } ) )
}
else
{
$warnings.Add( “Unable to decode installation date of $installedOn” )
}
}
else
{
$warnings.Add( “Unable to find installation of $productName in registry” )
}

if( ! [string]::IsNullOrEmpty( $appVolumesVersion ) )
{
$information.Add( ([pscustomobject]@{ ‘Item’ = “$productName Installed Version” ; ‘Description’ = $appVolumesVersion } ) )
}
else
{
$warnings.Add( “Unable to find installation details for $productName in the registry” )
}

# Check service running and when started wrt boot
if( ! ( $svservice = Get-CimInstance -ClassName win32_service -filter “name = ‘$serviceName'” -ErrorAction SilentlyContinue ) )
{
$warnings.Add( “Unable to find $productName service $svservice” )
}
else
{
if( $svservice.State -ne ‘Running’ )
{
$warnings.Add( “svservice is not running, it is $($svservice.State)” )
}

if( $svservice.StartMode -ne ‘Auto’ )
{
$warnings.Add( “svservice is not set to auto start, it is set to $($svservice.StartMode)” )
}

## get service restart options which can’t do natively in PowerShell/CIM
[bool]$seenFailureActionsLabel = $false
$restartAfterFailures = New-Object -TypeName System.Collections.Generic.List[int]
[int]$failureNumber = 1

<# [SC] QueryServiceConfig2 SUCCESS SERVICE_NAME: svservice RESET_PERIOD (in seconds) : 43200 REBOOT_MESSAGE : COMMAND_LINE : FAILURE_ACTIONS : RESTART -- Delay = 30000 milliseconds. RESTART -- Delay = 30000 milliseconds. RESTART -- Delay = 60000 milliseconds. #>
## technically only need to skip any lines that don’t have a : in them like “[SC] QueryServiceConfig2 SUCCESS”
sc.exe qfailure $serviceName | Select-Object -Skip 6 | ForEach-Object

    {
        if( $_ -match 'FAILURE_ACTIONSs*:s*' )
        {
            if( $_ -cmatch 'RESTART' )
            {
                $restartAfterFailures.Add( $failureNumber )
            }
            $seenFailureActionsLabel = $true
        }
        elseif( $seenFailureActionsLabel -or $_ -match '^[^:]*$' ) ## if first failure action not set then the FAILURE_ACTIONS tag is not present
        {
            $failureNumber++
            $seenFailureActionsLabel = $true
            if( $_ -cmatch 'RESTART' )
            {
                $restartAfterFailures.Add( $failureNumber )
            }
        }
    }

    if( $restartAfterFailures -and $restartAfterFailures.Count )
    {
        Write-Output -InputObject "Service is set to restart after failures $(($restartAfterFailures|Sort-Object) -join ', ')"
        if( ! $restartAfterFailures.Contains( [int]1 ) )
        {
            $warnings.Add( "Restart service recovery option not set for first failure" )
        }
           
    }
    else
    {
        $warnings.Add( "Restart service recovery options are not set" )
    }

    ## can be more than one process, eg. if a dialogue box is being shown
    if( ! ( $svserviceProcess = Get-Process -Name ( $serviceProcessName -replace '.exe$') -ErrorAction SilentlyContinue | Select-Object -First 1 ))
    {
        $warnings.Add( "Unable to find running svservice process" )
    }
    else
    {
        if( $exeVersion = Get-ItemProperty -ErrorAction SilentlyContinue -Path $svserviceProcess.Path )
        {
            if( $exeVersion.PSObject.Properties[ 'VersionInfo' ] -and $exeVersion.VersionInfo.PSObject.Properties[ 'ProductVersion' ] )
            {
                $information.Add( ([pscustomobject]@{ 'Item' = "$productName running service executable version" ; 'Description' = $exeVersion.VersionInfo.ProductVersion } ) )
            }
            else
            {
                $warnings.Add( "Running service executable $($svServiceProcess.Path) does not contain version information" )
            }
        }
        $os = Get-CimInstance -ClassName Win32_OperatingSystem
        ## TODO figure out how we get the real boot time as this comes back as the parent VM not when we were cloned
        <#
            Event Id 1 source Kernel-General

            The system time has changed to ‎2020‎-‎06‎-‎24T15:57:30.511000000Z from ‎2020‎-‎06‎-‎24T12:38:09.648303500Z.

            Change Reason: An application or system component changed the time.
            Process: 'DeviceHarddiskVolume4Program FilesVMwareVMware Toolsvmtoolsd.exe' (PID 4432).
        #>

        ##Write-Output "svservice process pid $($svserviceProcess.Id) started at $(Get-Date -Format G -Date $svserviceProcess.StartTime), $(($svserviceProcess.StartTime - $os.LastBootUpTime).TotalMinutes) minutes after boot at $(Get-Date -Date $os.LastBootUpTime -Format G)"
    }

    ## check event log set correctly because if not event messages display "The description for Event ID 218 from source svservice cannot be found"
    if( $messageFileValue = Get-ItemProperty -Path "HKLM:SYSTEMCurrentControlSetServicesEventLogApplication$serviceName" -Name 'EventMessageFile' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'EventMessageFile' -ErrorAction SilentlyContinue )
    {
        [string]$messageFile = [System.Environment]::ExpandEnvironmentVariables( ( $messageFileValue -replace '"' ) )
        if( $messageFile -ne ( $svservice.PathName -replace '"') )
        {
            # simple size check to see if someone has applied a workaround by copying the real svservice.exe to the bad location
            if( ! ( $messageFileProperties = (Get-ItemProperty -Path $messageFile -ErrorAction SilentlyContinue) ) -or ! ( $goodFileProperties = (Get-ItemProperty -Path ($svservice.PathName -replace '"') -ErrorAction SilentlyContinue) ) -or $messageFileProperties.Length -ne $goodFileProperties.Length )
            {
                $warnings.Add( "Event log message file set to

“$messageFileValue

" not $($svservice.PathName) - event log messages will not display correctly" )
            }
        }
    }
    else
    {
        $warnings.Add( "Failed to find Application event log setting for $serviceName - event log messages will not display correctly" )
    }
}

# Get server details and check connectivity
if( $configKey = Get-ItemProperty -Path $configKeyName -ErrorAction SilentlyContinue )
{
    if( ! $configKey.PSObject.Properties[ 'Manager_Address' ] )
    {
        $warnings.Add( "Registry value Manager_Address does not exist in $configKeyName" )
    }
    elseif( ! $configKey.PSObject.Properties[ 'Manager_Port' ] )
    {
        $warnings.Add( "Registry value Manager_Port does not exist in $configKeyName" )
    }
    else
    {
        $information.Add( ([pscustomobject]@{ 'Item' = "$productName server" ; 'Description' =  "$($configKey.Manager_Address):$($configKey.Manager_Port)" } ) )
        if( ! ( $networkTestResult = Test-NetConnection -ComputerName $configKey.Manager_Address -Port $configKey.Manager_Port -InformationLevel Detailed ) -or ! $networkTestResult.TcpTestSucceeded )
        {
           [string]$message = "Failed to connect to $($configKey.Manager_Address) on port $($configKey.Manager_Port)"
            if( ! $networkTestResult.NameResolutionSucceeded )
            {
                $message += ' (name resolution failed)'
            }
            $warnings.Add( $message )
        }
        else
        {
            $information.Add( ([pscustomobject]@{ 'Item' = "Test Connection to $productName Server" ; 'Description' = 'OK' } ) )
        }
    }
}
else
{
    $warnings.Add( "Failed to read configuration key $configKeyName" )
}

if( ! [string]::IsNullOrEmpty( $driverName ) )
{
    if( ! ($driver = Get-CimInstance -ClassName Win32_SystemDriver -Filter "Name = '$driverName'" -ErrorAction SilentlyContinue ) )
    {
        $warnings.Add( "Unable to find instance of driver $drivername" )
    }
    elseif( $driver.State -ne 'Running' )
    {
        $warnings.Add( "Driver $drivername is $($driver.State) but should be running" )
    }
    elseif( $driver.Started -ne 'True' )
    {
        $warnings.Add( "Driver $drivername is not started" )
    }
    elseif( $driver.Status -ne 'OK' )
    {
        $warnings.Add( "Driver $drivername  status is $($driver.Status) but should be OK" )
    }
    else
    {
        $information.Add( ([pscustomobject]@{ 'Item' = "Driver $driverName" ; 'Description' = 'Running ok' } ) )
    }
}

# Check event logs for service failures & app crashes
if( ( [array]$crashes = @( Get-WinEvent -FilterHashtable @{ ProviderName = 'Service Control Manager' ; id = 7034 ; Data = $fullServiceName ; StartTime = $startDate } -ErrorAction SilentlyContinue) ) -and $crashes.Count )
{
    [string]$message = "There have been $($crashes.Count) service crashes. Latest @ $(Get-Date -Date $crashes[0].TimeCreated -Format G)"
    if( $crashes.Count -gt 1 )
    {
        $message += ". Oldest @ $(Get-Date -Date $crashes[-1].TimeCreated -Format G)"
    }
    if( $firstEvent = Get-WinEvent -LogName $crashes[0].ContainerLog -Oldest -MaxEvents 1 )
    {
        $message += ". First event in $($crashes[0].ContainerLog) event log @ $(Get-Date -Date $firstEvent.TimeCreated -Format G) ($([math]::Round(([datetime]::Now - $firstEvent.TimeCreated).TotalDays / 7 , 1 )) weeks ago)"
    }
    $warnings.Add(  $message )
}

$oldestEvent = $null

## find oldest event in log containing events so we can report this
if( ! ( $provider = Get-WinEvent -ListProvider $serviceName ) )
{
    $warnings.Add(  "No event provider $serviceName found" )
}
else
{
    ## Find when the first event log entry in this log is
    ForEach( $eventLog in $provider.LogLinks )
    {
        if( $thisOldestEvent = Get-WinEvent -LogName $eventLog.LogName -MaxEvents 1 -Oldest -ErrorAction SilentlyContinue )
        {
            if( ! $oldestEvent -or $oldestEvent -lt $thisOldestEvent )
            {
                $oldestEvent = $thisOldestEvent ## slightly simplistic as event logs could've been cleared at different times but unlikely that writes to more than one event log anyway
            }
        }
        else
        {
            $warnings.Add(  "There are no events at all in the $($eventLog.DisplayName) event log which $serviceName writes to" )
        }
    }
}

$badEvents = New-Object -TypeName System.Collections.Generic.List[psobject]

# Check for svservice specific issues in event log - get relevant events into an array, oldest first so we can grab the first event and not have to work back when searching
if( ! ( [array]$svserviceEventLogEntries = @( Get-WinEvent -FilterHashtable @{ ProviderName = $serviceName ; StartTime = $startDate } -Oldest -ErrorAction SilentlyContinue ) ) -or ! $svserviceEventLogEntries.Count )
{
    $warnings.Add(  "No eventlog entries found for the event log provider $serviceName - this is unusual" )
    if( $oldestEvent )
    {
        $warnings.Add(  "Oldest event in the $($eventLog.DisplayName) event log which $serviceName writes to is $(Get-Date -Date $oldestEvent.TimeCreated -Format G) ($([math]::Round(([datetime]::Now - $oldestEvent.TimeCreated).TotalDays / 7 , 1 )) weeks ago)" )
    }
}
else ## look for bad event log entries
{
    ## TODO what errors are there? Should we just check for "error"?
    [int[]]$badLevels = @( 1 , 2, 3 )
    [array]$avErrors = @( ($svserviceEventLogEntries).Where( { ( $_.id -eq '240' -and $_.Properties[1].Value -match 'Connection Error' -and $_.Properties[1].Value -notmatch 'Sending user popup' ) -or $_.Level -in $badLevels } )|select TimeCreated,@{n='Text';e={$_.Properties[1].value}},Id ,Message)
    if( $avErrors -and $avErrors.Count )
    {
        #$warnings.Add( $message )
        ForEach( $avError in $avErrors )
        {
            if( ( [string]::IsNullOrEmpty( ( [string]$errorText = ($avError.Text -replace 'r?n' , ' ' -replace 's+' , ' ').Trim() ) ) )

-and ! [string]::IsNullOrEmpty( $avError.Message ) )
{
$errorText = $avError.Message -replace ‘r?n’ , ‘ ‘ -replace ‘Details:’ -replace ‘s+’ , ‘ ‘
}
$badEvents.Add( ( [pscustomobject][ordered]@{
‘Time’ = $avError.TimeCreated
‘Id’ = $avError.Id
‘Error’ = $errorText
} ) )
}
}
}

# Check mount points
## we use LSA to get the definitive logon time

if( ! ( ([System.Management.Automation.PSTypeName]’Win32.Secure32′).Type ) )
{
Add-Type -MemberDefinition $LSADefinitions -Name ‘Secure32’ -Namespace ‘Win32’ -UsingNamespace System.Text -Debug:$false
}

$count = [UInt64]0
$luidPtr = [IntPtr]::Zero

[uint64]$ntStatus = [Win32.Secure32]::LsaEnumerateLogonSessions( [ref]$count , [ref]$luidPtr )

if( $ntStatus )
{
Write-Error “LsaEnumerateLogonSessions failed with error $ntStatus”
}
elseif( ! $count )
{
Write-Error “No sessions returned by LsaEnumerateLogonSessions”
}
elseif( $luidPtr -eq [IntPtr]::Zero )
{
Write-Error “No buffer returned by LsaEnumerateLogonSessions”
}
else
{
Write-Debug “$count sessions retrieved from LSASS”
[IntPtr] $iter = $luidPtr
$earliestSession = $null
[array]$lsaSessions = @( For ([uint64]$i = 0; $i -lt $count; $i++)
{
$sessionData = [IntPtr]::Zero
$ntStatus = [Win32.Secure32]::LsaGetLogonSessionData( $iter , [ref]$sessionData )

if( ! $ntStatus -and $sessionData -ne [IntPtr]::Zero

            -and ($data = [System.Runtime.InteropServices.Marshal]::PtrToStructure( $sessionData , [type][Win32.Secure32+SECURITY_LOGON_SESSION_DATA] ) )

-and $data.PSiD -ne [IntPtr]::Zero

                    -and ( $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $Data.PSiD ) )
        {
            #extract some useful information from the session data struct
            [datetime]$loginTime = [datetime]::FromFileTime( $data.LoginTime )
            $thisUser = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.Username.buffer) #get the account name
            $thisDomain = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.LoginDomain.buffer) #get the domain name
            try
            {
                $secType = [Win32.Secure32+SECURITY_LOGON_TYPE]$data.LogonType
            }
            catch
            {
                $secType = 'Unknown'
            }

            if( ! $earliestSession -or $loginTime -lt $earliestSession )
            {
                $earliestSession = $loginTime
            }
            if( $secType -match 'Interactive' -and $thisDomain -ne 'Window Manager' -and $thisDomain -ne 'Font Driver Host' )
            {
                $authPackage = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.AuthenticationPackage.buffer) #get the authentication package
                $session = $data.Session # get the session number
                $logonServer = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.LogonServer.buffer) #get the logon server
                $DnsDomainName = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.DnsDomainName.buffer) #get the DNS Domain Name
                $upn = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($data.upn.buffer) #get the User Principal Name

                [pscustomobject]@{
                    'Sid' = $sid
                    'Username' = $thisUser
                    'Domain' = $thisDomain
                    'SessionId' = $session
                    'LoginId' = [uint64]( $loginID = [Int64]("0x{0:x8}{1:x8}" -f $data.LoginID.HighPart , $data.LoginID.LowPart) )
                    'LogonServer' = $logonServer
                    'DnsDomainName' = $DnsDomainName
                    'UPN' = $upn
                    'AuthPackage' = $authPackage
                    'SecurityType' = $secType
                    'Type' = $data.LogonType
                    'LoginTime' = [datetime]$loginTime
                }
            }
            [void][Win32.Secure32]::LsaFreeReturnBuffer( $sessionData )
            $sessionData = [IntPtr]::Zero
        }
        $iter = $iter.ToInt64() + [System.Runtime.InteropServices.Marshal]::SizeOf([type][Win32.Secure32+LUID])  # move to next pointer
    }) | Sort-Object -Descending -Property 'LoginTime'

    [void]([Win32.Secure32]::LsaFreeReturnBuffer( $luidPtr ))
    $luidPtr = [IntPtr]::Zero

    Write-Debug "Found $(if( $lsaSessions ) { $lsaSessions.Count } else { 0 }) LSA sessions, earliest session $(if( $earliestSession ) { Get-Date $earliestSession -Format G } else { 'never' })"
}

## Borrowed from Analyze Logon Durations - so we can map the various GUIDs to app names

$AppList = New-Object -TypeName System.Collections.Generic.List[psobject]
[string]$username = $null
[string]$domainname = $null

$svserviceEventLogEntries.Where( { $_.Id -eq 218 } ) | . { Process { $username = $domainname = $null ; $event = $_ ; $_.properties[1].Value -split  "

r

n" } } | . { Process {
    ## multiple lines of MOUNTED-READ;External SSDappvolumespackagesPuTTY.vmdk;{b2a70e3f-90ef-45b5-87a0-b7d07402a977}
    if( $_  -match 'bLOGINs*(.*)\(.*)$' ) {
        $username   = $matches[2]
        $domainname = $Matches[1]
    }
    elseif( $_ -match '(MOUNTED.*);(.+);({.+})' ) {
        if( ! $username -or ! $domainname )
        {
            Write-Debug -Message "Failed to get username or domain for $_"
        }
        $AppList.Add( [pscustomobject]@{
            'Time'       = $event.TimeCreated
            'UserName'   = $username
            'DomainName' = $domainname
            'MountType'  = $matches[1]
            'AppPath'    = $matches[2]
            'AppGUID'    = $matches[3]
            'AppName'    = $matches[2].Split( '' )[-1].Replace( '.vmdk' , '' ).Replace( '!20!' , ' ').Replace( '!2B!' , '+' )
            'AppId'      = $null } )
    }
    elseif( $_ -match 'ENABLE-APP;({.+});({.+})' ) ## ENABLE-APP;{65950e61-0304-45a6-b354-f3b26ced3f64};{b2a70e3f-90ef-45b5-87a0-b7d07402a977}
    {
        if( $appObject = $AppList | Where-Object -Property AppGuid -eq $Matches[2] | Select-Object -First 1 )
        {
            $appObject.AppId = $Matches[1]
        }
        else
        {
            Write-Debug "Couldn't find appguid $($matches[2]) in AppList"
        }
    }
    elseif( $_ -match 'bResponseb.*!FAILURE!' )
    {
        $badEvents.Add( ([pscustomobject]@{ 'Time' = $event.TimeCreated ; 'Id' = $event.Id ; Error = "Found FAILURE response event" }) )
    }
    elseif( $_ -match 'bManager:.*bStatus:s+([45]dd)' ) ## Manager: grl-appvolv2.guyrleech.local, Status: 500
    {
        [int]$errorCode = $Matches[1]
        [string]$errorText = $null
        if( ( $event.Properties[1].Value  -replace  "

r

n|<[a-z]+/>", ' ' -replace 's+' , ' ' ) -match 'bResponsebs+(d+ bytes):s+(.*)' ) ## Response (88 bytes): This request is taking too long.<br/>...
        {
            ## we split the string and are reading in chunks so get all lines here, join and isolate the response text
            if( $Matches[1] -like '<html>*' )
            {
                ## it's html and probably not complete so unlikely we can pull anything useful out of it
                $errorText = 'html response from server'
            }
            else
            {
                $errorText = $Matches[1]
            }
        }
        $badEvents.Add( ([pscustomobject]@{ 'Time' = $event.TimeCreated ; 'Id' = $event.Id ; Error = "Error code $errorCode

“$($errorText.Trim( ‘ .’))

"" }) )
    }
}}

[array]$userSessions = @( Get-WTSSessionInformation | Sort-Object -Property LogonTime )

[string]$appVolumesLogFile = "${env:ProgramFiles(x86)}CloudVolumesAgentLogssvservice.log"
$AppVolumesLogonEvents = $null
$parsedLogFile = $false

$information.Add( ([pscustomobject]@{ 'Item' = "Logged On or Disconnected Sessions" ; 'Description' = $userSessions.Count } ) )

# Get disk mounts - may not be any if no users
if( ( [array]$AVmounts = @( Get-Partition | Where-Object { $_.AccessPaths.Where( { $_ -match $mountPoint } , 1 ) } ) ) -and $AVmounts.Count -gt 0 )
{
    $information.Add( ([pscustomobject]@{ 'Item' = "Mounted $productName" ; 'Description' = $AVmounts.Count } ) )
}
## labels are CVApps or CVWritables
elseif( ( [array]$AVVolumes = @( Get-CimInstance -ClassName win32_volume -Filter "Label like 'CV%' and BootVolume = 'false' and systemvolume = 'false'" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DeviceID )) -and $AVVolumes.Count -gt 0 )
{
    $information.Add( ([pscustomobject]@{ 'Item' = "Mounted $productName" ; 'Description' = $AVVolumes.Count } ) )
}
elseif( $userSessions.Count )
{
   $warnings.Add( "No disk partititons mounted on $mountPoint - may be correct depending on assignments" )
}

if( $userSessions -and $userSessions.Count )
{
    [int]$userSessionCounter = 0
    ForEach( $userSession in $userSessions )
    {
        $userSessionCounter++
        ## WTS API logon time is not accurate so we need to find the LSASS one
        $lsaSession = $lsaSessions.Where( { $_.SessionId -eq $userSession.SessionId -and $_.Domain -eq $userSession.DomainName -and $_.Username -eq $userSession.UserName } , 1 )
        [datetime]$logonTime = $(if( $lsaSession ) { $lsaSession.LoginTime } else { $userSession.LogonTime })

        if( $logonTime -ge $startDate -and ( $logonRecord = $svserviceEventLogEntries.Where( { $_.Id -eq '210' -and $_.TimeCreated -ge $logonTime -and $_.Properties[1].Value -match "Session ID:s*$($userSession.SessionId)$" } , 1 ) ) )
        {
            ## if we haven't parsed the service log file yet we'll do it now so that we only get events after the logon of the first session chronologically
            ## need to parse the log file for App Volumes v2 to get GUIDS so we can find disk mount times
            ## we also need the log file for 4.x to see if it has completely failed because of SQL issues which don't go into the event log
            if( ! $parsedLogFile -and (Test-Path -Path $appVolumesLogFile -ErrorAction SilentlyContinue) )
            {
                $timeZone = Get-TimeZone
               
                $AppVolumesLogonEvents = @( Get-Content -Path $appVolumesLogFile | . { Process {
                    ## [2020-01-10 13:43:23.383 UTC] [svservice:P7776:T1108] Service path: C:Program Files (x86)CloudVolumesAgentsvservice.exe
                    ## Split into 3 - the first two [ ] delimited sections and then the rest
                    if( $_ -match '^[([^]]+)] [([^]]+)] (.+)$' -and ( $time = [datetime]::ParseExact( $Matches[1] , 'yyyy-MM-dd HH:mm:ss.fff UTC' , $null ) )

-and ( $adjustedForTimeZone = [System.TimeZoneInfo]::ConvertTimeFromUtc( $time, $timeZone ) )

                            -and $adjustedForTimeZone -ge $logonTime )
                    {
                        [pscustomobject]@{
                            'Time' = $adjustedForTimeZone
                            'ProcessInfo' = $Matches[2]
                            'Message' = $Matches[3]
                        }
                    }
                   
                }})
                $parsedLogFile = $true
            }

            if( $appVolumesVersion -match '^2.' )
            {
                ## now try and marry up GUIDs between log file and event log so we can map mount points to apps
                ##[array]$AppVolGUIDMappings = $( @(
                Foreach ($App in $AppList) {
                    $result = $null
                    Write-Verbose "AppGUID   : $($App.AppGUID)"
                    $AppVolGUIDEvents = $AppVolumesLogonEvents.Where( { $_.Message -like "*$($App.AppGUID)*" } )
                    Write-Debug "AppVolGUIDEvents : $($AppVolGuidEvents | Out-String)"
                    $AppVolumesLogonEvents.Where( { $_.Message -like "*$($App.AppGUID)*" } ) | . { Process {
                        $GUIDEvent = $_.Message
                        if( ! $result -and $GUIDEvent -match '(\Device\w+)' ) {
                            [string]$appDevice = $Matches[1]
                            Write-Verbose "AppDevice : $appDevice"
                            $AppVolumesLogonEvents.Where( {$_.Message -like "*$appDevice*"} ) | . { Process {
                            ## Do we assume VMWare will always keep the messages to a specific format? Or filter out the AppGUID
                            ## and assume what's left is the device GUID?  I'm leaning towards the latter... Let me know if this fails future Trentent
                                $AppVolDeviceEvent = $_.Message
                                if (($AppVolDeviceEvent -like "*$appDevice*") -and ($AppVolDeviceEvent -notlike "*$($App.AppGUID)*")) {
                                    Write-Debug "AppVolDeviceEvent: $AppVolDeviceEvent"
                                    if( $appDevice -and $App.AppGUID -and ( $GUIDs = [regex]::Match( $AppVolDeviceEvent , "({[0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}})" ) ) -and $GUIDS.Success )
                                    {
                                        $DiskGUID =  $(if( ! ( $GUIDS -is [array] ) -or $GUIDS.count -eq 1 ) {
                                            $GUIDS.Value
                                        } elseif ($GUIDS.count -eq 2) {
                                            $GUIDS.Value.Where( { $_ -ne $App.AppGUID } )
                                        } else {
                                            Write-Debug "AppVolumes: ERROR -- Unable to determine DiskGUD from

“$AppVolDeviceEvent

""
                                        })
                                        ## only create object if all fields are present
                                        if( $DiskGUID -and ($appName = $AppList.Where( { $_.AppGUID -eq $App.AppGUID } , 1 ) | Select-Object -ExpandProperty AppName ) )
                                        {
                                            Write-Verbose "$appName DiskGUID : $DiskGUID"
                                            ## add to existing app object if not already present
                                            if( $app.PSObject.Properties[ 'DiskGUID' ] )
                                            {
                                                if( $app.DiskGUID -ne $DiskGUID )
                                                {
                                                    $warnings.Add( "Different disk GUIDs $($app.DiskGUID) and $DiskGUID found for app $appName" )
                                                }
                                            }
                                            else
                                            {
                                                Add-Member -InputObject $app -MemberType NoteProperty -Name DiskGUID -Value $DiskGUID
                                            }
                                            if( $app.PSObject.Properties[ 'AppName' ] )
                                            {
                                                if( $app.AppName -ne $AppName )
                                                {
                                                    $warnings.Add( "Different app names $($app.AppName) and $appName for same app via GUID $($app.AppGUID)" )
                                                }
                                            }
                                            else
                                            {
                                                Add-Member -InputObject $app -MemberType NoteProperty -Name AppName -Value $AppName
                                            }
                                            if( $app.PSObject.Properties[ 'AppDevice' ] )
                                            {
                                                if( $app.AppDevice -ne $AppDevice )
                                                {
                                                    $warnings.Add( "Different app devices $($app.Device) and $AppDevice for app $appName" )
                                                }
                                            }
                                            else
                                            {
                                                Add-Member -InputObject $app -MemberType NoteProperty -Name AppDevice -Value $AppDevice
                                            }
                                        }
                                    }
                                }
                            }}
                        }
                    } }
                }
            }

            ## There will be a pair of 226 (Detected new volume, processing...) and 227 (New volume finished processing) events for each disk mounted for the user so time each
            [array]$thisUsersApps = @( $AppList.Where( { $_.username -eq $userSession.UserName -and $_.domainname -eq $userSession.DomainName -and $_.Time -ge $logonTime } ) )
            Write-Verbose -Message "Looking for $($thisUsersApps.Count) apps for user $($userSession.username)"
            ForEach( $userApp in $thisUsersApps )
            {
                [string]$message = "disk mount for $(if( $userApp.MountType -eq 'MOUNTED-WRITE' )
                    {
                        "writable volume"
                    }
                    else
                    {
                        "app $($userapp.AppName)"
                    })"
                $message += " from $($userapp.AppPath) for user $($userSession.UserName) in session $($userSession.SessionId), logged on at $(Get-Date -Date $logonTime -Format G)"

                if( ! ( $startMount = $svserviceEventLogEntries.Where( { $_.id -eq '226' -and $_.TimeCreated -ge $logonRecord.TimeCreated -and ( $_.Properties[1].value -imatch "bGUID: $($userapp.DiskGUID)" -or $_.Properties[1].value -imatch "VolumeId: $($userapp.AppGUID)" ) } , 1 ) ))
                {
                    $message = "Failed to find start event for " + $message
                    $warnings.Add( $message )
                }
                elseif( ! ( $endMount = $svserviceEventLogEntries.Where( { $_.id -eq '227' -and $_.TimeCreated -ge $logonRecord.TimeCreated -and ( $_.Properties[1].value -imatch "bGUID: $($userapp.DiskGUID)" -or $_.Properties[1].value -imatch "VolumeId: $($userapp.AppGUID)" ) } , 1 ) ))
                {
                    $message = "Failed to find end event for " + $message
                    $warnings.Add( $message )
                }
                else
                {
                    ## first character needs to be made upper case
                    $information.Add( ([pscustomobject]@{ 'Item' = ( '{0}{1}' -f $message.Substring(0,1).ToUpper() , $message.Substring( 1 ) ) ; 'Description' = "$([math]::Round( ($endMount.TimeCreated - $startMount.TimeCreated).TotalSeconds , 2 )) seconds" } ) )

                    ## check we can find the mounted partition
                    if( ! ( [string]$GUID = $(if( $endMount.properties[1].value -match 'GUID:s*({.*})' ) { $Matches[1 ] }) )

-and ! ( [string]$GUID = $(if( $startMount.properties[1].value -match ‘GUID:s*({.*})’ ) { $Matches[1 ] }) ) )
{
$warnings.Add( “Unable to find GUID for partition access path for $($userApp.AppName) for $($userSession.UserName)” )
}
elseif( ! ( $thisMount = $AVmounts.Where( { $_.AccessPaths.Where( { $_ -match $GUID } , 1 ) } ) )

                        -and ! $AVVolumes.Where( { $_ -match $GUID } , 1 ) )
                    {
                        $warnings.Add( "Unable to find a mounted volume for GUID $GUID for user $($userSession.UserName) $($userApp.AppName)" )
                    }
                }
            }

            ## Check for catastrophic failures via log file
            if( $AppVolumesLogonEvents -and $AppVolumesLogonEvents.Count )
            {
                ## get logon time of next user session to ensure we only look before that
                $nextlogontime = $null
                if( $userSessionCounter -lt $userSessions.Count )
                {
                    $nextlsaSession = $lsaSessions.Where( { $_.SessionId -eq $userSessions[$userSessionCounter].SessionId -and $_.Domain -eq $userSessions[$userSessionCounter].DomainName -and $_.Username -eq $userSessions[$userSessionCounter].UserName } , 1 )
                    $nextlogonTime = $(if( $nextlsaSession ) { $nextlsaSession.LoginTime } else { $userSessions[$userSessionCounter].LogonTime })
                }
                if( ( $theEvent = $AppVolumesLogonEvents.Where( { $_.Time -ge $logonTime -and ( ! $nextlogontime -or $_.Time -lt $nextlogontime ) -and $_.Message -match 'HttpUserLogin: (succeeded|failed) (user login)' } , 1 ) ) )
                {
                    if( $Matches[1] -eq 'failed' )
                    {
                        $badEvents.Add( ([pscustomobject]@{ 'Time' = $theEvent.Time ; 'Id' = $null ; Error = "Found failed logon to $productName server in log file, probably for user $($userSession.UserName) $($userApp.AppName)" }) )
                    }
                }
                else
                {
                    $warnings.Add( "Unable to find successful logon to $productName server for user $($userSession.UserName) $($userApp.AppName)" )
                }
            }
        }
        else
        {
            $warnings.Add( "Unable to find $productName logon event for $($userSession.username) in session $($userSession.SessionId)" )
        }
    }
}

$information | Format-Table -AutoSize

if( $warnings -and $warnings.Count )
{
    Write-Output -InputObject "$($warnings.Count) warnings:

n”
$warnings | Format-Table -AutoSize
}
else
{
Write-Output -InputObject “No warnings to report”
}

if( $badEvents -and $badEvents.Count )
{
[string]$message = “Found $($badEvents.Count) events indicating $productName issues”
if( $oldestEvent )
{
$message += “, oldest event in event log is from $(Get-Date -Date ($oldestEvent.TimeCreated) -Format G)”
}

Write-Output -InputObject “`n$message”

$badEvents | Sort-Object -Property Time | Format-Table -AutoSize
}

START YOUR TRIAL

Get Your Download Link

Gain access to ControlUp from your PC. Register and get a link to start your Free Trial.