Export and Import user assignments from one Citrix RemotePC to another

$srcRemotePC = 'DomainName\65TELW19'
$destRemotePC = 'DomainName\65TELW20'
Get-BrokerUser -PrivateDesktopUid (get-BrokerPrivateDesktop -MachineName $srcRemotePC ).Uid | | %{ if($_.Upn -ne "") {
Add-BrokerUser -Machine $destRemotePC -Name $_.Upn
}}

Identify VDI workers that blue screened in vSphere

Typical recommendation in VDI is to turn off OS Memory dump collection and auto reboot on the bluescreen. Optimization tools from citrix/vmware take care of this setting automatically. But this masks the events if there a real issue with master image. Following PowerShell snippet will provide information if any of the worker vm’s have crashed.

Import-Module vmware.powercli
Connect-VIServer vcenterserver
Get-ViEvent -Username User | Where {$_.FullFormattedMessage -like "*Guest operating system has crashed." -AND $_.CreatedTime -gt (Get-Date).AddDays(-1)} | Select CreatedTime,FullFormattedMessage

Good places to find installer silent switches and post optimization/cleanup tasks

https://www.powershellgallery.com/packages/Evergreen
refer to JSON manifest files

https://silentinstallhq.com/silent-install-knowledge-base/

https://community.chocolatey.org/packages

https://github.com/ScoopInstaller/Main/tree/master/bucket

How does browser know what Certificate to provide for Client Cert Authentication

Ever wonder how the browser provided the client cert when multiple are available for the user?

The answer is the server that is requesting the client cert for authentication has an option to ask for the cert that is signed by a specific Distinguished Name CA, which is part of the server communication. You can see this in wireshark capture, No need for SSL decryption, as the DN CA information is part of server hello messages which happens before the encryption beings between the server and client.

As long as the browser can find the matching cert with a private key signed by DN CA, it has the ability to silently provide to the server based on browser settings. If there is more than one issued by the DN CA, it will prompt the user to make the selection. If the browser finds a matching cert but it doesn’t have the private key, it will skip that cert.

If the server just asks for client cert without providing DN CA, then the browser displays all available client cert that have the private key and prompts the user to make the selection.

Powershell Get Citrix Current Session Logon Time

$code = @'
using System;
using System.Runtime.InteropServices;


namespace WFAPI
{
    
   public  enum WF_INFO_CLASS
    {
            WFVersion,             // OSVERSIONINFO
            WFInitialProgram,
            WFWorkingDirectory,
            WFOEMId,
            WFSessionId,
            WFUserName,
            WFWinStationName,
            WFDomainName,
            WFConnectState,
            WFClientBuildNumber,
            WFClientName,
            WFClientDirectory,
            WFClientProductId,
            WFClientHardwareId,
            WFClientAddress,
            WFClientDisplay,
            WFClientCache,
            WFClientDrives,
            WFICABufferLength,
            WFLicenseEnabler,
            RESERVED2,
            WFApplicationName,
            WFVersionEx,
            WFClientInfo,
            WFUserInfo,
            WFAppInfo,
            WFClientLatency,
            WFSessionTime,
            WFLicensingModel
    } 
    [StructLayout(LayoutKind.Sequential)]
    public struct WF_SESSION_TIME
    {
        public long ConnectTime;
        public long DisconnectTime;
        public long LastInputTime;
        public long LogonTime;
        public long CurrentTime;
    }
    
    public class Program
    {
        [DllImport("wfapi64.dll", CharSet=CharSet.Unicode,SetLastError=true)]
        public static extern bool WFQuerySessionInformation(System.IntPtr hServer, int sessionId, WF_INFO_CLASS WFInfoClass, out System.IntPtr ppBuffer, out uint pBytesReturned);              

        [DllImport("wfapi64.dll", ExactSpelling = true, SetLastError = false)]
        public static extern void WFFreeMemory(IntPtr memory);

        public const int WF_CURRENT_SESSION = -1;

        public static string GetClientName()
        {
            System.IntPtr buffer = IntPtr.Zero;
            uint bytesReturned;
            try
            {
                bool sessionInfo = WFQuerySessionInformation(System.IntPtr.Zero, WF_CURRENT_SESSION, WF_INFO_CLASS.WFClientName, out buffer, out bytesReturned);
                return Marshal.PtrToStringUni(buffer);
            }
            catch
            {
                return string.Empty;
            }
            finally
            {
                WFFreeMemory(buffer);
                buffer = IntPtr.Zero;
            }
        }       
        public static string getSessionLogonTime()
        {
            System.IntPtr buffer = IntPtr.Zero;
            string sessionLogonTime = "";
            uint bytesReturned = 0;            
            try
            {
                bool sessionInfo = WFQuerySessionInformation(System.IntPtr.Zero, WF_CURRENT_SESSION, WF_INFO_CLASS.WFSessionTime, out buffer, out bytesReturned);
                if(sessionInfo)
                {
                    WF_SESSION_TIME WFSessionTime = (WF_SESSION_TIME)Marshal.PtrToStructure(buffer, typeof(WF_SESSION_TIME));
                    sessionLogonTime = Convert.ToString(DateTime.FromFileTime(WFSessionTime.LogonTime));     
                }
            }
            catch
            {
                return string.Empty;
            }
            finally
            {
                WFFreeMemory(buffer);
                buffer = IntPtr.Zero;
            }
            return sessionLogonTime;
        }  

    }
}

'@
Add-Type -TypeDefinition $code -Language CSharp   
[WFAPI.Program]::getSessionLogonTime()

WFAPI disconnect sessions that exceed certain idle time [override policy]

/*
Author - Siva Mulpuru [sivamulpuru.com]
Usage - WFAPI "maxIdleTimeInMin" "GatewaySNIP"
Comment - Workaround for Citrix Virtal Apps [FKA XenApp] to disconnect AGW connections when exceed passed value. Designed to run as scheduled job. 
          Allows non AGW connections to use serverIdleTimeout policy while providing override for AGW connections.
 
 */

using System;
using System.Runtime.InteropServices;

namespace WFAPI
{
    class Program
    {
        [DllImport("wfapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool WFQuerySessionInformation(System.IntPtr hServer, int sessionId, WF_INFO_CLASS WFInfoClass, out System.IntPtr ppBuffer, out uint pBytesReturned);

        [DllImport("wfapi.dll", ExactSpelling = true, SetLastError = false)]
        public static extern void WFFreeMemory(IntPtr memory);

        [DllImport("wfapi.dll", ExactSpelling = true, SetLastError = false)]
        public static extern bool WFDisconnectSession(System.IntPtr hServer, int sessionId, bool bWait);

        [DllImport("wfapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool WFEnumerateSessions(IntPtr hServer, 
            [MarshalAs(UnmanagedType.U4)] Int32 Reserved,  
            [MarshalAs(UnmanagedType.U4)] Int32 Version,  
            ref IntPtr pSessionInfo,  
            [MarshalAs(UnmanagedType.U4)] 
            ref Int32 pCount);   

        public enum WF_INFO_CLASS    
        {
            WFVersion,             
            WFInitialProgram,
            WFWorkingDirectory,
            WFOEMId,
            WFSessionId,
            WFUserName,
            WFWinStationName,
            WFDomainName,
            WFConnectState,
            WFClientBuildNumber,
            WFClientName,
            WFClientDirectory,
            WFClientProductId,
            WFClientHardwareId,
            WFClientAddress,
            WFClientDisplay,
            WFClientCache,
            WFClientDrives,
            WFICABufferLength,
            WFLicenseEnabler,
            RESERVED2,
            WFApplicationName,
            WFVersionEx,
            WFClientInfo,
            WFUserInfo,
            WFAppInfo,
            WFClientLatency,
            WFSessionTime,
            WFLicensingModel
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct WF_SESSION_TIME
        {
            public long ConnectTime;
            public long DisconnectTime;
            public long LastInputTime;
            public long LogonTime;
            public long CurrentTime;
        }

        public enum  WF_CONNECTSTATE_CLASS
        {
            WFActive,              // User logged on to WinStation
            WFConnected,           // WinStation connected to client
            WFConnectQuery,        // In the process of connecting to client
            WFShadow,              // Shadowing another WinStation
            WFDisconnected,        // WinStation logged on without client
            WFIdle,                // Waiting for client to connect
            WFListen,              // WinStation is listening for connection
            WFReset,               // WinStation is being reset
            WFDown,                // WinStation is down due to error
            WFInit                 // WinStation in initialization
        }
        
        [StructLayout(LayoutKind.Sequential)]
        public struct WF_CLIENT_ADDRESS
        {
            public int AddressFamily;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
            public byte[] Address;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct WF_SESSION_INFO
        {
            public Int32 SessionID;
            [MarshalAs(UnmanagedType.LPStr)]
            public String pWinStationName;
            public WF_CONNECTSTATE_CLASS State;
        }
        static DateTime getSessionLastInteractionTime(int sessionid)
        {
            IntPtr buffer = IntPtr.Zero;
            DateTime lastInteractionTime = DateTime.Now;
            uint bytesReturned = 0;
            try
            {
                if (WFQuerySessionInformation(IntPtr.Zero, sessionid, WF_INFO_CLASS.WFSessionTime, out buffer, out bytesReturned))
                {
                    WF_SESSION_TIME WFSessionTime = (WF_SESSION_TIME)Marshal.PtrToStructure(buffer, typeof(WF_SESSION_TIME));
                    lastInteractionTime = DateTime.FromFileTime(WFSessionTime.LastInputTime);
                }
            }
            finally
            {
                WFFreeMemory(buffer);                
            }
            return lastInteractionTime;
        }
        
        static string getClientIP(int sessionid)
        {
            IntPtr buffer = IntPtr.Zero;
            uint bytesReturned;
            string clientIPAddress = "0.0.0.0";
            try
            {
                if (WFQuerySessionInformation(IntPtr.Zero, sessionid, WF_INFO_CLASS.WFClientAddress, out buffer, out bytesReturned))
                {

                    WF_CLIENT_ADDRESS si = (WF_CLIENT_ADDRESS)Marshal.PtrToStructure((System.IntPtr)buffer, typeof(WF_CLIENT_ADDRESS));
                    clientIPAddress = si.Address[2] + "." + si.Address[3] + "." + si.Address[4] + "." + si.Address[5];
                }
            }
            finally
            {
                WFFreeMemory(buffer);
            }
            return clientIPAddress;
        }
        
        static void Main(string[] args)
        {
            int maxIdleTimeInMin = Convert.ToInt32(args[0]);
            string gatewaySNIP = args[1];
            //Console.WriteLine("{0} {1}", maxIdleTimeInMin, gatewaySNIP);
            IntPtr pSessionInfo = IntPtr.Zero;
            int Count = 0;
            if (WFEnumerateSessions(IntPtr.Zero,0,1,ref pSessionInfo,ref Count))
            {
                try
                {
                    Int32 dataSize = Marshal.SizeOf(typeof(WF_SESSION_INFO));
                    Int64 current = (int)pSessionInfo;
                    for (int i = 0; i < Count; i++)
                    {
                        WF_SESSION_INFO si = (WF_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WF_SESSION_INFO));
                        current += dataSize;                        
                        if (si.State == WF_CONNECTSTATE_CLASS.WFActive)
                        {
                            //Console.WriteLine("{0} {1} {2}", si.SessionID, getSessionLastInteractionTime(si.SessionID), getClientIP(si.SessionID));                            
                            if(gatewaySNIP.CompareTo(getClientIP(si.SessionID)) == 0 && (DateTime.Now - getSessionLastInteractionTime(si.SessionID)).TotalMinutes > maxIdleTimeInMin)
                            {
                               bool result = WFDisconnectSession(IntPtr.Zero, si.SessionID, false);
                               Console.WriteLine("{0} {1} is {2}", "Disconnect result for SessionID", si.SessionID, result);
                            }
                        }
                    }
                }
                finally
                {
                    WFFreeMemory(pSessionInfo);
                }
            } 
        }
    }
}

Automate Citrix Session Launch for Load tests or health probing

Requirements

  • Citrix Workspace app Version 1808 for Windows or later.
  • Storefront Store needs to have http auth module enabled.

Step 1: Retrieve Resource Name of the app/desktop you would like to automate

“C:\Program Files (x86)\Citrix\ICA Client\AuthManager\storebrowse.exe” -U <username> -P <password> -D <domain> -M 0x2000 -E “https://<storefrontserver>/Citrix/<storename>/discovery”

First column of the output would have the resource name. In My case it’s a windows 10 VDI >
Controller.W10-QA $S2-4‘ ‘W10-QA’ ‘\’ ” https://storefront/Citrix/XDOnly-Internal/resources/v2/Q29udHJvbGxlci5XMTAtUUEgJFMyLTQ-/launch/ica

Step 2: Get the ICA file for the desired Resource Name

“C:\Program Files (x86)\Citrix\ICA Client\AuthManager\storebrowse.exe” -U <username> -P <password> -D <domain> -L “Controller.W10-QA $S2-4” “https://<storefrontserver>/Citrix/<storename>/discovery”

-L with resource name will retrieve the ICA file and save it to %AppData%\Local\Citrix\storebrowse\cache\launch.ica

Step 3: Launch the ICA file

%AppData%\Local\Citrix\storebrowse\cache\launch.ica

Reference > https://docs.citrix.com/en-us/citrix-workspace-app-for-windows/store-browse.html

How to prevent creation of System and Recovery partitions for VDI Master

During the initial windows install screen, press Shift-F10 to launch the command window, and using diskpart create primary partition.

select disk 0
create partition primary
exit

ref > https://www.howtogeek.com/192772/what-is-the-system-reserved-partition-and-can-you-delete-it/