Dramatically Reduce Logon “Time to Desktop” By Moving From Group Policy Preferences to Powershell Logon Script 33


Update 5/16/2017: Thanks to tabularasa for some improvements to this, fixing some missing slashes among other things!
In years past, logon scripts were the bane of many logon experiences due to their synchronous nature.  Beginning with Windows 7, logon scripts can be processed asynchronously.  We glossed over this fact: “Nice feature, but how will it help us? We’re heavily invested into GP Preferences now!”  However, now that we’re 1078 virtual desktops deep, logon time is a serious concern.  Citrix has failed to provide us with a way to extend the time on the automatic “log off if log on takes more than 90 seconds” feature.  In most cases, this isn’t an issue, but this limitation highlighted a serious flaw:  Even under normal circumstances, there are some cases where some users might exceed 90s.

Our organization uses the HP Universal Print driver, Ricoh Universal Print driver and the Lexmark Universal print driver and we map printers with Group Policy Preferences.  This mapping process takes, on an “average” printer, 6-7 seconds.  Unfortunately, this processing does in fact run synchronously (possibly due to GPP Drive mapping – I just found this article but it requires further testing!  Would be interesting if this whole mess could be avoided by moving drive mapping out of GPO!) and therefore can add substantial time to the logon process.  Some users have 5-15 printers available to them, bringing the practical limit to the number of printers to something like 10 (given that portions of the logon process require some time).

As we are heavily invested in preferences, item level targeting and (as everyone is) short on staff / training, we decided to leverage that investment in GPP but still gain the benefits of the logon script.  A colleague of mine, Matt Heckman, created a rather ingenious Powershell script that effectively uses GPP Printer entries from a “dummy” GPO to map printers for our VDI environment.  This means that our level 1 admins can still create printers using GPP with simple filters and the users in VDI will automatically map them.  The results were substantial:  A logon that took 60 seconds to reach a desktop was shortened by 45 seconds!  In K-12, logon time is king as it directly translates into instructional time.

So, how does it work?

Create a new GPO.  It is not required to link it anywhere in the domain.  Be sure to clearly name it so that someone doesn’t think that it is an unused GPO!

Copy your existing printer preferences into the new GPO.

Modify the logon script to reflect the name/GUID of your new GPO.  To obtain the GUID, in GPMC  click the “details” tab, then copy the “Unique ID”. If you use any filters other than computer name or security group name you will need to extend the script.

Without further delay, give it a whirl!

 

#$GPOGuid = "{7B4605FA-3435-4A2B-8A33-A101944C5E47}"
$GPOGuidObject = Get-GPO "VOA - Printer GPP Policy"
$GPOGuid = $GPOGuidObject.Id
$GPOGuidDomain = $GPOGuidObject.DomainName
$LogDir="H:\Windows\PrinterLogs"
$LogPath="$($LogDir)\$($env:USERNAME)_$($env:COMPUTERNAME)_LogonScript.log"
write-host "Writing logs to $($LogPath)"
 
#Check to see if the log directory exists and create it it doesn't yet exist.
    #$LogPathExist=false
    #Is the H drive already mapped
    $LogPathExist=Test-Path H:
    if ($LogPathExist) {
           write-host "H Drive Exists"
    } else {
        write-host "H Drive not Mapped"
        return
    }
    $LogPathExist=Test-Path $LogDir
    if ($logPathExist) {
        write-host "$($LogDir) Exists"
    } else {
        write-host "$($LogDir) Does not exist"
        #create directory for for logfile
        mkdir $LogDir
    }
 
 
    #Is the folder created
        #create the folder if its not already there
 
 
 
 
Start-Transcript -Path $LogPath
 
[xml]$printersXml = Get-Content "\\$GPOGuidDomain\sysvol\$GPOGuidDomain\Policies\{$GPOGuid}\User\Preferences\Printers\Printers.xml"
 
$userGroups = ([Security.Principal.WindowsIdentity]"$($env:USERNAME)").Groups.Translate([System.Security.Principal.NTAccount])
$computerGroups = ([Security.Principal.WindowsIdentity]"$($env:COMPUTERNAME)").Groups.Translate([System.Security.Principal.NTAccount])
 
Function Process-FilterComputer {
    Param(
        $filter
    )
 
    $result = $false
 
    if ($filter.type -eq "NETBIOS") {
        if ($env:COMPUTERNAME -like $filter.name) {
            $result = $true
        }
    }
 
    if ($filter.not -eq 0) {
        return $result
    } else {
        return !$result
    }
}
 
Function Process-FilterUser {
    Param(
        $filter
    )
 
    $result = $false
 
    if ("$env:USERDOMAIN\$env:USERNAME" -like $filter.name) {
        $result = $true
    }
 
    if ($filter.not -eq 0) {
        return $result
    } else {
        return !$result
    }
}
 
Function Process-FilterGroup {
    Param(
        $filter
    )
 
    $result = $false
 
    if ($filter.userContext -eq 1) {
        if ($userGroups -contains $filter.name) {
            $result = $true
        }
    } else {
        if ($computerGroups -contains $filter.name) {
            $result = $true
        }
    }
 
    if ($filter.not -eq 0) {
        return $result
    } else {
        return !$result
    }
}
 
Function Process-FilterCollection {
    Param(
        $filter
    )
 
    if ($filter.HasChildNodes) {
        $result = $true
        $childFilter = $filter.FirstChild
 
        while ($childFilter -ne $null) {
            if (($childFilter.bool -eq "OR") -or ($childFilter.bool -eq "AND" -and $result -eq $true)) {
                if ($childFilter.LocalName -eq "FilterComputer") {                    
                    $result = Process-FilterComputer $childFilter
                } elseif ($childFilter.LocalName -eq "FilterUser") {
                    $result = Process-FilterUser $childFilter
                } elseif ($childFilter.LocalName -eq "FilterGroup") {
                    $result = Process-FilterGroup $childFilter
                } elseif ($childFilter.LocalName -eq "FilterCollection") {
                    $result = Process-FilterCollection $childFilter
                }
 
                #Write-Host "Process-$($childFilter.LocalName) $($childFilter.name): $($result)"
            } else {
                #Write-Host "Process-$($childFilter.LocalName) $($childFilter.name): skipped"
            }
 
            if (($childFilter.NextSibling.bool -eq "OR") -and ($result -eq $true)) {
                break
            } else {
                $childFilter = $childFilter.NextSibling
            }
        }
    }
 
    if ($filter.not -eq 1) {
        return !$result
    } else {
        return $result
    }
}
 
$com = New-Object -ComObject WScript.Network
 
$installedPrinterDrivers = get-printerdriver
 
#Get-Content "\\$GPOGuidDomain\sysvol\$GPOGuidDomain\Policies\{$GPOGuid}\User\Preferences\Printers\Printers.xml"
 
foreach ($sharedPrinter in $printersXml.Printers.SharedPrinter) {
    $filterResult = Process-FilterCollection $sharedPrinter.Filters
    Write-Host "$($sharedPrinter.name) filters passed: $($filterResult)"
 
    if ($filterResult -eq $true) {
        if ($sharedPrinter.Properties.action -eq 'C') {
            #check to see if the driver is present on the XenAppServer
           
           $printServer = $sharedPrinter.Properties.path
           $printServer = $printServer.Split("\\")[2]
           $driverName = Get-Printer -ComputerName $($PrintServer) -Name $($sharedPrinter.name)
 
            if ($installedPrinterDrivers | where {$_.name -eq $driverName.drivername}) {
            #Create the printer in the session
                $com.AddWindowsPrinterConnection($sharedPrinter.Properties.path)
                "AddWindowsPrinterConnection:$($sharedPrinter.Properties.path)"
                if ($sharedPrinter.Properties.default -eq 1) {
                    #$com.SetDefaultPrinter($sharedPrinter.Properties.path)
                    (Get-WmiObject -ComputerName . -Class Win32_Printer -Filter "Name='$($($sharedPrinter.Properties.path) -replace "\\","\\")'").SetDefaultPrinter()
                    "SetDefaultPrinter:$($sharedPrinter.Properties.path)"
                }
            } else {
            #alert that the driver isn't present
            write-host "`r`n  The driver for $($sharedPrinter.Properties.path) doesn't appear to be present.  The driver that needs to be installed is $($driverName.drivername).  Please install this driver on the XenApp Server.  `n`r"
            }
        } elseif ($sharedPrinter.Properties.action -eq 'D') {
            $com.RemovePrinterConnection($sharedPrinter.Properties.path, $true, $true)
            "RemovePrinterConnection:$($sharedPrinter.Properties.path)"
        }
    }
}
 
Stop-Transcript

Leave a comment

Your email address will not be published.

33 thoughts on “Dramatically Reduce Logon “Time to Desktop” By Moving From Group Policy Preferences to Powershell Logon Script

  • Kim

    Nice script
    it dosen´t comes with any error and in the log file i can see some printers are set to true in pass, but they never get map i have administrators right….?

  • Kim

    Nice script
    it dosen´t comes with any error and in the log file i can see some printers are set to true in pass, but they never get map i have administrators right….?

    • Atum Post author

      Hi Kim,

      Are you able to manually map the printers that are passing the mapping by simply browsing to the print server and opening the printer? Are there any events in the event viewer (Check application and also Applications & services, Microsoft, PrintService, Admin)?

  • Tom

    The theory behind this is awesome. Just wanted to mention a few things the original poster did not mention…

    In this line, you need to replace both contoso.local with your domain name
    [xml]$printersXml = Get-Content “\contoso.localsysvolcontoso.localPolicies$GPOGuidUserPreferencesPrintersPrinters.xml”

    In this line, it assumes you are using the Create action, not update or replace. Change to U or R if you use either of those.
    if ($sharedPrinter.Properties.action -eq ‘C’) {

    Even after modifying these two sections, this script was still not working for me. It would say the printer name, but there would be nothing after filters passed:. Filtering was not working for us at all. Since we only care about the groups that the computer is in, and our computers will only be in 1 printer group, I removed all the filtering and added a line for each one of our printer groups. Fill domainname, groupname, and GPOGuid to your organization.
    if ($computerGroups -eq “DOMAINNAMEGROUPNAME”) {$GPOGuid = “{GUIDofPrinterPolicyForThisGroup}”}

    The printer add, default, and delete lines were not working for us either. I modified the add to the following:
    if ($sharedPrinter.Properties.action -eq “U”) {
    Write-Host Installing Printer: “$($sharedPrinter.name)”
    Write-Host UNC Path: “$PrinterPath”
    (New-Object -ComObject WScript.Network).AddWindowsPrinterConnection($PrinterPath)
    }
    We use ‘Update’ not ‘Create’. I also had to make similar changes to the default and delete sections. Now this script is working like a champ. Thanks!

    • Atum Post author

      We originally found update prefs in general applied in 200ms where create was approximately 4ms, so with large numbers of preferences, it was a big savings. Since this script bypasses all that it is really up to you. I’ll check into the default printer business, there may have been an update to the script after this post but it was working. The result printer business was annoying with upm because the printer wasn’t necessarily mapped at the time of the application of upm, so it would reset to default.

      • Tom

        Thanks for info. Every environment is different, so very well could have been working for you. I’ve seen no other posts on the internet in regards to deploying printers this way. Group Policy Printer Preferences are easy to manage but the slow log-ins were also a big problem for us. This let us use GPP but still have fast log-ins. So thanks for sharing!

  • Tom

    The theory behind this is awesome. Just wanted to mention a few things the original poster did not mention…

    In this line, you need to replace both contoso.local with your domain name
    [xml]$printersXml = Get-Content “\contoso.localsysvolcontoso.localPolicies$GPOGuidUserPreferencesPrintersPrinters.xml”

    In this line, it assumes you are using the Create action, not update or replace. Change to U or R if you use either of those.
    if ($sharedPrinter.Properties.action -eq ‘C’) {

    Even after modifying these two sections, this script was still not working for me. It would say the printer name, but there would be nothing after filters passed:. Filtering was not working for us at all. Since we only care about the groups that the computer is in, and our computers will only be in 1 printer group, I removed all the filtering and added a line for each one of our printer groups. Fill domainname, groupname, and GPOGuid to your organization.
    if ($computerGroups -eq “DOMAINNAMEGROUPNAME”) {$GPOGuid = “{GUIDofPrinterPolicyForThisGroup}”}

    The printer add, default, and delete lines were not working for us either. I modified the add to the following:
    if ($sharedPrinter.Properties.action -eq “U”) {
    Write-Host Installing Printer: “$($sharedPrinter.name)”
    Write-Host UNC Path: “$PrinterPath”
    (New-Object -ComObject WScript.Network).AddWindowsPrinterConnection($PrinterPath)
    }
    We use ‘Update’ not ‘Create’. I also had to make similar changes to the default and delete sections. Now this script is working like a champ. Thanks!

    • Atum Post author

      We originally found update prefs in general applied in 200ms where create was approximately 4ms, so with large numbers of preferences, it was a big savings. Since this script bypasses all that it is really up to you. I’ll check into the default printer business, there may have been an update to the script after this post but it was working. The result printer business was annoying with upm because the printer wasn’t necessarily mapped at the time of the application of upm, so it would reset to default.

      • Tom

        Thanks for info. Every environment is different, so very well could have been working for you. I’ve seen no other posts on the internet in regards to deploying printers this way. Group Policy Printer Preferences are easy to manage but the slow log-ins were also a big problem for us. This let us use GPP but still have fast log-ins. So thanks for sharing!

  • Tom

    Forgot $printerpath variable needs to be declared since that is not in original script. I’m no powershell expert but this worked for us for add, set default, and delete sections. Hope this helps anyone who can’t get the original script to work.

    foreach ($sharedPrinter in $printersXml.Printers.SharedPrinter) {
    #Set printer UNC path as variable
    $PrinterPath=$sharedPrinter.Properties.path

    #Add any printers if the GGP action is set to “U” – which is Update
    if ($sharedPrinter.Properties.action -eq “U”) {
    Write-Host Installing Printer: “$($sharedPrinter.name)”
    Write-Host UNC Path: “$PrinterPath”
    (New-Object -ComObject WScript.Network).AddWindowsPrinterConnection($PrinterPath)
    }

    #Find the default printer, if GGP default value equals 1
    if ($sharedPrinter.Properties.default -eq 1) {
    Write-Host –Setting this printer as default
    (New-Object -ComObject WScript.Network).SetDefaultPrinter($PrinterPath)
    }

    #Remove any printers if GPP action is set to “D” – which is Delete
    if ($sharedPrinter.Properties.action -eq “D”) {
    Write-Host Deleting Printer: “$($sharedPrinter.name)”
    (New-Object -ComObject WScript.Network).RemovePrinterConnection(“$PrinterPath”)
    }
    }

  • Tom

    Forgot $printerpath variable needs to be declared since that is not in original script. I’m no powershell expert but this worked for us for add, set default, and delete sections. Hope this helps anyone who can’t get the original script to work.

    foreach ($sharedPrinter in $printersXml.Printers.SharedPrinter) {
    #Set printer UNC path as variable
    $PrinterPath=$sharedPrinter.Properties.path

    #Add any printers if the GGP action is set to “U” – which is Update
    if ($sharedPrinter.Properties.action -eq “U”) {
    Write-Host Installing Printer: “$($sharedPrinter.name)”
    Write-Host UNC Path: “$PrinterPath”
    (New-Object -ComObject WScript.Network).AddWindowsPrinterConnection($PrinterPath)
    }

    #Find the default printer, if GGP default value equals 1
    if ($sharedPrinter.Properties.default -eq 1) {
    Write-Host –Setting this printer as default
    (New-Object -ComObject WScript.Network).SetDefaultPrinter($PrinterPath)
    }

    #Remove any printers if GPP action is set to “D” – which is Delete
    if ($sharedPrinter.Properties.action -eq “D”) {
    Write-Host Deleting Printer: “$($sharedPrinter.name)”
    (New-Object -ComObject WScript.Network).RemovePrinterConnection(“$PrinterPath”)
    }
    }

  • John

    Hi,

    Excellent script.

    Would this also be applicable to Group Policy Pref drive mappings? If so how would I go about adjusting the script to map drives from GPPrefs rather than printers?

    Thanks in advance,

    John

    • Atum Post author

      Drive mappings are an interesting beast, because they _always_ run synchronously in the login process in prefs. Moving them to the login script should provide a mild improvement, but drive mappings should be pretty fast by default as you’re not dealing with printer mojo. I’ve only seen them behave slowly when using DFS namespaces with invalid folders (non existent or no permissions for example) with the default timeout (120 min). Printers will run asynchronously when Group policy isn’t in first-run mode. You’d need to modify the script to iterate over the drive mapping section, rather than printers. You could map them using something like “New-PSDrive -name X -psprovider FileSystem -root c:scripts

      • John

        Hi,

        Thanks for the fast response!

        The reason I’m asking about drive mappings is not to reduce logon time, more to resolve an issue.

        The issue I have is that we use VPN. When logging in from a cafe for example, the user logs into their machine using a cached AD logon, then connects to the cafe wifi via a landing page. Once wifi is established they connect to the corporate network using VPN. The problem with GPPref drive mappings is that it is a foreground process, meaning it can only apply during startup or logon which in this case has already taken place.
        We do have the option of calling a logon script from the VPN, but I don’t want to have to maintain both GPPrefs and an additional login script. What I would like to do, like your printer script, would be to create a GPO that contains my drive mappings (GPP) and then use a powershell script to process it. That way admins would maintain the GPO and the script would do the work.

        Thanks in advance,

        John

  • John

    Hi,

    Excellent script.

    Would this also be applicable to Group Policy Pref drive mappings? If so how would I go about adjusting the script to map drives from GPPrefs rather than printers?

    Thanks in advance,

    John

    • Atum Post author

      Drive mappings are an interesting beast, because they _always_ run synchronously in the login process in prefs. Moving them to the login script should provide a mild improvement, but drive mappings should be pretty fast by default as you’re not dealing with printer mojo. I’ve only seen them behave slowly when using DFS namespaces with invalid folders (non existent or no permissions for example) with the default timeout (120 min). Printers will run asynchronously when Group policy isn’t in first-run mode. You’d need to modify the script to iterate over the drive mapping section, rather than printers. You could map them using something like “New-PSDrive -name X -psprovider FileSystem -root c:scripts

      • John

        Hi,

        Thanks for the fast response!

        The reason I’m asking about drive mappings is not to reduce logon time, more to resolve an issue.

        The issue I have is that we use VPN. When logging in from a cafe for example, the user logs into their machine using a cached AD logon, then connects to the cafe wifi via a landing page. Once wifi is established they connect to the corporate network using VPN. The problem with GPPref drive mappings is that it is a foreground process, meaning it can only apply during startup or logon which in this case has already taken place.
        We do have the option of calling a logon script from the VPN, but I don’t want to have to maintain both GPPrefs and an additional login script. What I would like to do, like your printer script, would be to create a GPO that contains my drive mappings (GPP) and then use a powershell script to process it. That way admins would maintain the GPO and the script would do the work.

        Thanks in advance,

        John

  • OzThe2

    Hi, Sorry it’s been a long time before replying – I meant to do it sooner and well..you know how it goes in the busy world of I.T. Thanks for dropping by my website and taking the time to comment on my post about our similar issue. Yeah..GPP printer deployment isn’t the bees-knees we all hoped it would be! Ah well – at least we both found a solution!

  • OzThe2

    Hi, Sorry it’s been a long time before replying – I meant to do it sooner and well..you know how it goes in the busy world of I.T. Thanks for dropping by my website and taking the time to comment on my post about our similar issue. Yeah..GPP printer deployment isn’t the bees-knees we all hoped it would be! Ah well – at least we both found a solution!

    • Atum Post author

      Awesome! nice effort. Can you please attribute Matt Heckman directly in the script? He did the bulk of the work on this, I merely worked with him to refine and share it. Cheers!

    • Atum Post author

      Awesome! nice effort. Can you please attribute Matt Heckman directly in the script? He did the bulk of the work on this, I merely worked with him to refine and share it. Cheers!