Manual Folder Redirection with Symbolic Links

This is a guest post by Bryan Chriscoli, who implemented an innovative alternative to folder redirection with the help of symbolic links, AppSense products and PowerShell scripting. All credit goes to him.

Motivation

Whilst I was still working for AppSense as a Solutions Architect assigned to implement Environment Manager at UnitedHealth Group, it became apparent that UHG’s use of Folder Redirection was causing issues that included Folder Redirection failing, extremely long logons, applications breaking and, when combined with Offline Files, the added risk of unreliable file synchronization.

When I left AppSense, I joined UHG and began to look at the overall picture. By the time AppSense DataNow was introduced in a very early pilot phase UHG were still using Folder Redirection to the local DataNow folder. Multiple issues with the DataNow client and the unreliability of Folder Redirection were causing delays to the project.

There were three scenarios where Folder Redirection was in play:

  • Physical device: Folder Redirection to local DataNow folder
  • Virtual device: Folder Redirection to network home share
  • Citrix device: Folder Redirection to network home share

Initially I focused on physical devices (where DataNow was going to be deployed) and tried to reduce the login times caused by the initial Folder Redirection copy. Anything after that would be considered a bonus.

When Folder Redirection is applied for the first time for a user, existing data is moved to the new redirected location of the folder. Historically, this was very slow. In Windows 7 Microsoft made improvements to speed up the process (TechNet):

Windows 7 optimizes the first-time logon process with Folder Redirection. Windows 7 presents the user’s desktop as soon as the files are moved to the Offline Files cache. The user is allowed to log on, and Offline Files in Windows 7 synchronizes the data between the local computer and the server in the background. Background synchronization decreases the time that the user waits for the desktop and reduces the amount of network utilization.

Now that is all well and good if a new user without much data logs on for the first time. But what if you are implementing this with someone who has 50GB of data in their My Documents folder dating back years? Well… in short, welcome back, slow logon.

Better Moves

We initially came up with the idea of utilizing PowerShell to move the data to the new redirected target during logon with the following (simplified) script:

#Folders to be moved
$SourceFolders = "$Env:UserProfile\Documents","$Env:UserProfile\Desktop","$Env:UserProfile\Favorites"
 
#Get the new path, move the folders
foreach ($Folder in $SourceFolders){
   $Destination = $Folder -creplace "(?s)^.*\\", "$Env:UserProfile\DataNow\Data\"
   Move-Item $Folder $Destination -force
}

Now I know people may say: “but that’s single-threaded”. Yes, you’re right, it is. But the key difference here is that we are moving the data to its new location. As this happens on the same disk the files stay where they are, only the MFT entries are updated. Windows 7 Offline Files, on the other hand, has a whole lot of cataloguing to do when files are added to its cache. Replacing the Folder Redirection mechanism of moving files to the Offline Files cache with a simple scripted file move resulted in an incredibly quick move.

At the same time, we utilize a secondary Powershell script to alert the user that we are moving their data – communication is key. Keeping people in the dark is a sure way to generate tickets:

[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[System.Windows.Forms.MessageBox]::Show("Please Note: This logon may take longer as the Follow Me Desktop is being configured. `nThis may require an automatic restart" , "AppSense EM Enablement" , 0)

These two scripts are executed through AppSense Environment Manager. The relevant configuration looks like this:

AppSense-EM-01a

We continued to use Folder Redirection, just without the initial copy, and we found that our logon times had improved. However, we then started to get complaints that applications were no longer functioning, were requesting licenses, etc…

Moving up

After investigation, we found that the applications in question are using an “unusual” way to determine the path to their settings and license folders: they look up the location of the Documents folder from the User Shell Folders registry key and then navigate UP one level. In other words, before redirection an application would:

  1. Retrieve the path to the Documents folder: %UserProfile%\Documents
  2. Go up one level: %UserProfile%
  3. Append the name of its settings directory: %UserProfile%\Application1

With redirection in place the same process would yield a different end result:

  1. Retrieve the path to the Documents folder: %UserProfile%\DataNow\Data\Documents
  2. Go up one level: %UserProfile%\DataNow\Data
  3. Append the name of its settings directory: %UserProfile%\DataNow\Data\Application1

With redirection enabled the applications could not find their previously stored files any more, including licenses – thus prompting the user and generating helpdesk tickets.

Moving on

With the new technology on the scene and essentially us breaking it, we had to come up with a way of ensuring the applications were able to function as normal, yet redirect the data. This is where the idea of using Junction points was born. Junction points allowed us the flexibility to redirect Documents/Desktop/Favorites to the DataNow folder, but also maintain the standard directory structure of the user profile:

  • C:\Users\Username
  • C:\Users\Username\Desktop
  • C:\Users\Username\Documents
  • C:\Users\Username\Favorites
  • etc.

Symbolic Links, Scripted

Our first iteration of the junction points went smoothly enough and it fixed the issue we had with other applications. We then moved over to our VDI implementation, where DataNow was not in use. This would simply be a redirection to the network home share.

Our users’ network home share is gathered from AD using multiple attributes and built together using a PowerShell script. To save re-querying AD at every logon, we set this homeshare path as a registry setting and capture it in Environment Manager Personalization. At each logon after it has been restored by EM, we have a PowerShell script that sets the environment variable from the registry setting. If it does not exist, we rebuild it again.

We found that junction points cannot be used to point to network locations, however symbolic links can. It was agreed that for all implementations on any platform where redirection was required, symbolic links was the way forward, to allow the flexibility of local or network paths. As such we re-developed our script to use symbolic links and deployed that to our physical workstations and VDI.

Our symbolic link creation script runs under the System Account because Microsoft, in their infinite wisdom, decided that only Administrators can use the mklink.exe command. As it runs under the system account, we have to gather what session the script is running in, get the user associated with that session and then gather their SID from their username. From there we can extract the environment variables we need to make the script function and to apply the symbolic links.

The script essentially renames the existing folder to FolderName_Old and marks it as a hidden system folder. If the existing folder was already a symbolic link, we remove it instead. We then run the mklink /D command via cmd.exe as PowerShell does not recognise the command. After some initial failures we integrated looping to retry each symbolic link before failing.

You can find the script’s source code below. The resulting user profile directory structure looks like this:

Profile-physical

For our VDI setup, we run the same script, but we replace the $DataNow variable with an $AppSenseHome variable, which is the environment variable for the user’s home directory mentioned above. A VDI user’s profile looks like this:

Profile-VDI

In our Environment Manager configuration we have flag files that prevent the scripts from running over and over at each logon. They only run if the data move has completed:

EM-script-config

Result

The end result is that our workstations keep their data local. DataNow synchronizes the local data with a user’s home share. In our VDI and Citrix environments, the symbolic links point to the home share instead, which allows the user to directly access their synchronized data.

We are constantly reviewing the scripts to ensure we get maximum performance and as little failure as possible. Although they work well today, there are additional things we are looking at implementing to further enhance the success.

Symbolic Link Folder Redirection Script

This is the symbolic link creation script in all its glory:

# Get session info and the user associated with the session
$sessionId = Get-Process -id $pid | select-object -expand SessionId
$quOutput = query.exe user $sessionId
$userId = $quOutput[1] -replace '^>([^\s]+)+.*$','$1'
 
#Get ther user's SID
$objUser = New-Object System.Security.Principal.NTAccount("$userid")
$UserSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
 
#Get the environment variables needed.
$UserProfile = (Get-ItemProperty "Registry::\HKEY_USERS\$userSID\Volatile Environment").UserProfile
$DataNow = (Get-ItemProperty "Registry::\HKEY_USERS\$userSID\Environment").DataNow
 
#Function for creating symbolic links
#Loop of 5 tries (10 seconds total) to rename folders
Function SymLink($Folder) {
   $Stoploop = $false
   [int]$Retrycount = "0"
   $FolderOld = $Folder + "_Old"
 
   do {
      try {
         if (Test-Path $UserProfile\$Folder) {
            $file = Get-Item $UserProfile\$Folder -Force -ea 0
            #if the $Folder is NOT a Symbolic Link
            if ([bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne "True") {
               #Rename it to _Old and set it as a hidden system folder (in case of file move issues)
               Rename-Item "$UserProfile\$Folder" "$UserProfile\$FolderOld" -Force
               $(Get-Item $UserProfile\$FolderOld).Attributes = "Hidden","System"
            } ELSE {
               #Else Use RMDIR to remove the Symbolic Link - Prevents removal of data from the redirected folder
               Start-Process -file "cmd.exe" -arg "/c rmdir `"$UserProfile\$Folder`"" -Wait -WindowStyle Hidden
            }
         }
         #Create the Symbolic Link
         Start-Process -file "cmd.exe" -arg "/c mklink /D `"$UserProfile\$Folder`" `"$DataNow\$Folder`"" -Wait -NoNewWindow
         $Stoploop = $true
         Return $Null
      }
      catch {
         #If the retries reach 5, exit the loop
         if ($Retrycount -gt 5){
            $Stoploop = $true
            Return $Null
         }
         else {
            #Sleep for 1 second then loop again
            Start-Sleep -Seconds 1
            $Retrycount = $Retrycount + 1
         }
      }
   }
   While ($Stoploop -eq $false)
   Return $Null
}
 
#Perform the functions and sleep in betweenm
 
Symlink "Documents"
Start-Sleep 2
Symlink "Desktop"
Start-Sleep 2
Symlink "Favorites"

, , ,

3 Responses to Manual Folder Redirection with Symbolic Links

  1. Jeremy Saunders February 25, 2015 at 00:58 #

    Nice write-up Bryan and well explained. Really interesting solution.

  2. Carlos June 30, 2016 at 14:44 #

    You nailed the issue were facing right on the head. We’re experiencing the delayed first time post folder redirection at a rate of 1GB/min.

    Your solution may just do the trick for us!

  3. Doug Munford July 6, 2016 at 10:35 #

    Thanks for this, I’ve actually used this method to solve a different problem. For fixed physical workstations and terminal services users I map OneDrive for business to their O: drive and wanted to redirect some folders into it. For laptop users I don’t bother with a mapping and can just redirect to the local sync location instead. For whatever reason I couldn’t get explorer to respect the GPO redirection to O: drive locations, but doing it this way works perfectly.

Leave a Reply