2017-11-22

[Powershell] Installing fonts - the hard way

This post is more of a guide than me finding weird bugs and the documention of my adventures trying to figure out what is actually going on. But even then I came across something new that kind of tripped me up.


A number of departments in the company I work for use various special fonts, which are part of the "Corporate Identity". Whenever a machine in one of those departments gets replaced we needed to install these special fonts on the new machine. And since we have been replacing a couple hundred machines over the last half year and will continue to replace a couple hundred machines every year on a regular basis from now on I decided to take a look at how to automate the deployment of fonts.

As I have mentioned in previous posts we are using Microsoft's SCCM (System Center Configuration Manager) for Windows/Office update and general software deployment. So the tool of choice for the task was of course that. All I needed now was a script that could properly install the fonts and hand that script over to the guy in charge of the SCCM server so he could deploy it to the machines in question.


Looking through what we already had in regards to (automatically) installing fonts I quickly found traces of previous attempts. One of them was a crude attempt at a Powershell script that simply copied the font files into the folder "C:\Windows\Fonts" and then proceeded to create and set a multitude of registry keys. I do not think documenting this script here would do anyone any good so I will leave it out.

Not finding much help locally I turned to the internet and quickly found a VBScript that does the trick and was being mentioned in countless articles and forum posts. The script simply invokes the "Install" verb of the font, basically scripting someone right-clicking on the font file and selecting "Install" from the context menu.

Dim objShell, objFSO, wshShell
Dim strFontSourcePath, objFolder, objFont, objNameSpace, objFile
 
Set objShell = CreateObject("Shell.Application")
Set wshShell = CreateObject("WScript.Shell")
Set objFSO = createobject("Scripting.Filesystemobject")
 
Wscript.Echo"--------------------------------------"
Wscript.Echo" Script to install Fonts "
Wscript.Echo"--------------------------------------"
Wscript.Echo" "
 
strFontSourcePath = "\\localhost\Share"
 
If objFSO.FolderExists(strFontSourcePath) Then
  Set objNameSpace = objShell.Namespace(strFontSourcePath)
  Set objFolder = objFSO.getFolder(strFontSourcePath)
 
  For Each objFile In objFolder.files
    If LCase(right(objFile,4)) = ".ttf" OR LCase(right(objFile,4)) = ".otf" Then
      If objFSO.FileExists("C:\Windows\Fonts\"& objFile.Name) Then
        Wscript.Echo "This Font is already installed: " & objFile.Name
      Else
        Set objFont = objNameSpace.ParseName(objFile.Name)
        objFont.InvokeVerb("Install")
        Wscript.Echo "Installed Font: "& objFile.Name
        Set objFont = Nothing
      End If
    End If
  Next
Else
  Wscript.Echo"Check the Source Path or Make sure font islocated inside source folder"
End If

But I do not like VBScript. And while I was able to read the script and understand what it was doing my choice of scripting language, especially for deployment with SCCM, is Powershell. So I took the VBScript and tried to write a script in Powershell that does the exact same thing. Little did I know about what was waiting for me...

The script was supposed to be located in the very same folder as the font files. Coming up with a Powershell version of the VBScript took little time and after some tinkering I got something that worked just the same.

$shell = New-Object -ComObject Shell.Application
foreach ($font in $shell.Namespace($PSScriptRoot).Items()) {
  if ((($font.Name).ToLower() -like "*.ttf" -or ($font.Name).ToLower() -like "*.otf") -and (-not (Test-Path ($env:windir + "\Fonts\" + $font.Name) -ErrorAction SilentlyContinue))) {
    $font.InvokeVerbEx("Install")
  }
}
I was quite pleased about how simple it was and short - and not VBScript. I tested the script on a test machine and everything was fine and working. So I handed the script and font files to the SCCM guy. A few days later he came to me and told me that my script was not working when deployed with SCCM but was working fine when executed manually. Odd. What was going on here?

Since SCCM installs the software using the local System account (SID: S-1-5-18) I logged into a remote machine using PsExec using "/S cmd.exe" and ran the script there. And guess what? It really did not work. I ran the script with my user account from the Powershell ISE and who would have thought - it worked. So I added some debug code inside the foreach loop that would dump the content of the $font variable to a text file and ran the script with my user account again. The output I got looked exactly as I expected it:

Application  : System.__ComObject
Parent       : System.__ComObject
Name         : CorpidC1-Black.otf
Path         : C:\Windows\ccmcache\e1\CorpidC1-Black.otf
GetLink      :
GetFolder    :
IsLink       : False
IsFolder     : False
IsFileSystem : True
IsBrowsable  : False
ModifyDate   : 06.06.2017 16:01:40
Size         : 109496
Type         : OpenType-Schriftartendatei


Then I ran the script again using PsExec and the local system account. And the output was not what I expected:

Application  : System.__ComObject
Parent       : System.__ComObject
Name         : CorpidC1-Black
Path         : C:\Windows\ccmcache\e1\CorpidC1-Black.otf
GetLink      :
GetFolder    :
IsLink       : False
IsFolder     : False
IsFileSystem : True
IsBrowsable  : False
ModifyDate   : 06.06.2017 16:01:40
Size         : 109496
Type         : OpenType-Schriftartendatei


The "Name"-field was suddenly missing the file extension. It only took a few seconds for me to get an idea what was going on here. Like any good admin not only do I have the Windows Explorer option "Hide extensions for known file types" disabled but I have also disabled that option for all users through Group Policies. And apparently the local system account has, like any account, that option enabled by default. And also apparently that option affects the "Name"-value when dealing with ComObjects.

After figuring that out the solution was quite obvious:

$shell = New-Object -ComObject Shell.Application
foreach ($font in $shell.Namespace($PSScriptRoot).Items()) {
  if (($font.Path).ToLower() -like "*.ttf" -or ($font.Path).ToLower() -like "*.otf") {
    if (-not (Test-Path ($env:windir + "\Fonts\" + $font.Path.Split("\")[-1]) -ErrorAction SilentlyContinue)) {
      $font.InvokeVerbEx("Install")
    }
  }
}
So instead of checking whether $font.Name ends with either „*.otf“ or „*.ttf“ I am now checking $font.Path for the same. And then I’m checking if $font.Path.Split("\")[-1] exists in the „C:\Windows\Fonts\“ directory, rather than just $font.Name

Alternatively I could have checked on $font.Type but that is a localized value so on any other system languages besides German the script would have failed again.

And with that the script is now ready for deployment through SCCM. :)


[1] https://twitter.com/BeingSysAdmin/status/933257377356410880

2 comments:

  1. Worked perfectly in SCCM! Thank you

    ReplyDelete
  2. This works great for the most part but unfortunately when re-launched on set schedule the script prompts for confirmation on already installed font. Consequentially the SCCM deployment times out due to hidden dialogue (we want to keep it hidden). Is there any way to force install on already installed font?

    ReplyDelete