When deploying a package that installs fonts (a simple MSI installation in our case) via SCCM, the user that is logged on to the computer when the MSI is installed will not be able to use these new fonts until they log out and log back on. Read on for my explanation and solution…
Note: I have only been working with Windows 7 machines (x86 & x64) so I have no idea if my solution works with XP or even if the same problem happens on XP in the first place, though I suspect it does.
Let’s go through the exact symptoms of this problem first:
You have an MSI installation (or any other kind of installer) that installs an application and also installs some fonts. You run the silent installation manually to test it and find that the application gets installed correctly and when you launch the application you can see that the fonts have been correctly installed. However, when SCCM deploys this package and runs the exact same silent install you just tested but from the SCCM agent service, the application gets installed correctly but the fonts appear to be missing.
When the user launches the application that was just installed they can see that the fonts do not work as the application looks wrong and if the user looks in C:\Windows\Fonts the fonts are not there, despite the fact that the MSI installation log reported no errors for font installation and the fact that you can see the font files are there if you look via command prompt (or connect to the C$ share of the machine from another computer). All of the correct registry entries get created for the font just like they do when the installation is run manually, but the fonts just do not appear to be installed to the currently logged on user.
If the user logs off and logs back on though, the fonts are there and the application displays correctly. The fonts also work correctly straight away if no user was logged on during the SCCM deployment.
So what is happening here? Well I have my own theory, which I have now pretty much proved, but have not been able to find any official confirmation anywhere that this is actually how it all works. My theory is as follows:
When a user logs on, Windows looks at each of the values in the following registry key:
For each of the font files in this key, it calls the AddFontResource function. We can see this in action here in a Process Monitor capture of a user logging on:
At the start of the Process Monitor screenshot we can see winlogon is querying each value in that registry key mentioned above, then it starts to access font files corresponding to these values. Towards the bottom of the screenshot you can see it doing this, starting with C:\Windows\Fonts\arial.ttf, then ariali.ttf, then arialbd.ttf (and obviously it goes on and on for each font that was in the registry key).
Now if we open up one of the events in Process Monitor where winlogon is actually accessing one of the font files, we can view the call stack and see that it is indeed calling the AddFontResource function (ignore the Gdi prefix, that must just be the name of the internal function within gdi32.dll that AddFontResource actually calls)
So at logon all existing fonts are available to the user’s session, then when they install a font (either manually or by running an installation that installs the fonts) the font gets added to that registry key so that it is available at next logon but AddFontResource also gets called there and then to make the font available straight away. One thing to note about the AddFontResource function is that the fonts it has added to the system font table get lost at logoff (due to something which I will cover in the next paragraph), which is presumably why it does not require administrator permissions and why that registry key is used to keep a list of all fonts that should be added back to the system font table when a user logs on.
So this is all well and good, but you might be thinking it is not getting us any closer to explaining why the fonts are not available to the current user when SCCM installs them. Well… we can see from the MSDN documentation that AddFontResource only makes a font available for applications in the same session that AddFontResource is being called from – this is the key point. During logon this is fine because it is the winlogon.exe process that is calling AddFontResource and winlogon.exe is running from the user’s session. When a user installs a font themselves (manually or by running an installation program) it obviously is being run from their session. However, when SCCM tries to run the MSI installation that installs the fonts, it is being run from a service (the SCCM agent service) and therefore is in another session (in Vista/7 services all run in session 0, user sessions are session 1 or above).
So the fonts are getting installed in the exact same way when installed by SCCM (or indeed any service) – they get copied to C:\Windows\Fonts and added to the registry key mentioned earlier, and then AddFontResource gets called, its just that as this is being called from a different session to the user, the user does not get the fonts made available to their session. Of course when they log off and back on winlogon.exe will see the fonts in that registry key and call AddFontResource for them, so that is why the fonts work after logging off and back on (or logging on for the first time if SCCM installed the app while no one was logged on).
Oh and as for why the user cannot see the fonts in C:\Windows\Fonts when they are actually there, this is down to the hidden system file named desktop.ini that is used to make certain folders display files in a special way. If we use command prompt to go in and delete that desktop.ini file from C:\Windows\Fonts, and then go back into there in explorer we can see the new font files.
So we have our goal then: we need to get AddFontResource to be called in the currently logged on user’s session instead of in the services session once the MSI installation is complete.
Obviously we cannot modify the SCCM agent service itself to tell it to do this, but we can make it run whatever program we want after the MSI installation is complete just by changing our advertised program in the SCCM package to be a batch file that first of all runs msiexec to install whatever app it is we are installing and then runs our chosen program once that has finished.
So I wrote a VB.NET program named StartInConsoleSession.exe that uses various Windows API functions to launch a specified process in the currently logged on user’s session (aka the console session) when run from the services session. Obviously this wouldn’t be any use in a Terminal Services environment where users don’t get logged on to the console session, but we would never be deploying apps to a TS server via SCCM so that doesn’t matter. The application I wrote is command line and has the following syntax:
StartInConsoleSession.exe [/W] cmdpath arguments
You can see an explanation of the arguments and examples in the screenshot below
Note that this operation of launching a process in the user’s session from another session requires permissions that only the Local System account has, so you will not be able to successfully run this unless it is being called from a service that is running as Local System – of course the SCCM agent service that will be launching our program meets these requirements so that is not a problem.
Anyway, back to our problem. So we now have a program that we can tell the SCCM agent to launch that will in turn launch a specified program in the user’s session. All we need now is a program that calls AddFontResource for specified font files so that we can make StartInConsoleSession.exe launch this in the user’s session.
So I wrote another VB.NET application: CurrentSessionFonts.exe (yeah I’m not very creative when I name my applications am I…). This is another command line program and it has the following syntax:
CurrentSessionFonts.exe [add | remove] fontfiles
So we just specify whether or not we want to add or remove a font, then specify the path to the font file. You can specify multiple font files if you separate them with a comma (no space between the font file names and the comma’s). e.g:
CurrentSessionFonts.exe add C:\Font1.fon,C:\Font2.ttf
NB: If your font file name has a comma in it… well… sorry, you better rename it (who puts commas in their file names anyway!).
Here is a call stack screenshot from my CurrentSessionFonts.exe program as it adds a font, as you can see the same function in gdi32.dll that winlogon.exe calls at logon is called.
OK so we now have all the tools for the job, we just need to put them together – which we do by using the following batch file as our advertised program in SCCM (instead of just calling msiexec directly to install the MSI and do nothing more):
msiexec /i “%~dp0SomeInstall.msi” /qb!
“%~dp0StartInConsoleSession” /W “%~dp0CurrentSessionFonts” add %SystemRoot%\Fonts\Font1.TTF,%SystemRoot%\Fonts\Font2.FON
Ignore the word wrapping – that second part should be all on one line (you might want to copy and paste it into notepad to view it more clearly).
So obviously the first line just installs the MSI silently as normal, but what are we doing in the second line? Well firstly the %~dp0 variable is an environmental variable that expands to the location that the batch file is being run from. We can’t use %cd% because this package could be run from a UNC path and command prompt does not support the current directory (which is what %cd% is) being a UNC path. Oh and %systemroot% just expands to C:\Windows on the vast majority of PCs (the only time it wouldn’t is if you had not installed Windows on the C drive or something like that).
As you can see, we call StartInConsoleSession.exe and tell it to launch CurrentSessionFonts.exe in the user’s session. We pass in the “add” argument and the paths to the two font files we want to call AddFontResource for as arguments to the CurrentSessionFonts.exe program. We also use the /W argument of StartInConsoleSession.exe to tell it to wait for the process it is launching (in this case CurrentSessionFonts.exe) to exit before it returns control back to the batch file that it is being run from. In this example we don’t need to because we have nothing else after that line, but I figured other people might find this useful as you might not want your batch file to move on to the next line until the new process is finished doing its stuff and has terminated. Note that if you use /W then it must be the first argument passed to StartInConsoleSession.exe (otherwise it would just get passed in as an argument to whatever program you asked StartInConsoleSession to launch).
A more basic, and perhaps easier to read/understand, example would be:
msiexec.exe /i C:\SomeInstall.msi /qb!
C:\StartInConsoleSession.exe C:\CurrentSessionFonts.exe add C:\Windows\Fonts\Font1.FON
(again the second part should all be on one line)
but of course this would only work if your batch file copied those files to the root of C first (not advisable obviously) and is only provided here as an example of how to use the programs for anyone that didn’t understand that first example due to all the variables etc.
You can download both of the programs I have written for this solution (see the section above titled “The Solution” for more information on how to use each one) from here:
Note that both programs require .NET Framework v2.0, but as this is intended to be run on Windows 7 systems and they have .NET 2.0 and 3.5 installed straight out of the box, that shouldn’t be an issue.
Also note that they both require the file Cjwdev.WindowsApi.dll to be in the folder that they are being run from. Each zip folder contains a copy of this file but they are both identical so if you keep both EXEs in the same folder it doesn’t matter which version of the DLL you keep in that folder as well.
For any developers that would like to see exactly how all of this code works, send me an email (firstname.lastname@example.org) and I will gladly send you the source code for both applications. Also, feel free to use my Windows API library in your own .NET projects – just download one of the ZIP files linked to above and pinch the Cjwdev.WindowsApi.dll file from it. Add a reference to this DLL from your own .NET project and you then have access to roughly 80 Windows API definitions and almost as many managed .NET methods that wrap these APIs into easy to use methods that you can use with absolutely no Windows API knowledge (all of the classes are in the Cjwdev.WindowsApi namespace). If you would like the intellisense descriptions/documentation to be displayed for the methods and classes in this DLL then see here.
Never spend 3 full days (2 of those being weekend days!) on something as unimportant as users needing to log out and back on before they can use an application! 🙂
Just kidding… it has been quite an interesting experience, I’ve certainly learnt a lot from it. Plus its nice to know that I didn’t just settle for the old “it can’t be done” answer and actually ended up with a fully functional solution. Coming up with that solution was not as easy as it may have looked in this blog post (see my thread here for example) but it was still interesting.
As always, let me know if this helped you out or if you have any questions.