Photo in the public domain, courtesy of Pixabay

Down the Uninstalls Rabbit Hole: Installment 3, Dynamo Uninstalls

In the previous two installments of this series, we looked at “old school” uninstalls, and “new school” Microsoft Store uninstalls. Those installments acted as an introduction to Uninstalls in general, in part because the examples were pretty simple. In this installment we’ll be looking into Dynamo uninstalls, and there simplicity, consistency, even rationality go out the window.
We’ll start with the data gathering process, because much of the problem with automating Dynamo uninstalls is the MANY different bits of information we need to get them all. Then we’ll look at some code that uses that data to uninstall every version of Dynamo installed on a machine, and do some cleanup of detritus Dynamo leaves behind.

Fair warning, this post is a BEAST. Long, detailed, and very likely TL;DR. But for those wanting the deep dive, let’s jump in!

The Data

I have gathered data starting with Dynamo 0.6.1 (the oldest version available at Dynamo Builds), and gathered data on all the stable releases and some of the daily releases (though I will only include stable releases here) through 2.0.3 (the latest version available). I have included my screen grabs of each key for reference.
It was through this data gathering that I finally realized the scope of the problem, Dynamo really isn’t one program with updates, at least from an uninstall perspective. There are multiple permutations, with different ways to identify Dynamo in the registry, different ways to uninstall, and different detritus left behind to clean up. So I’ll walk you through that data and all the pertinent and changing information.
I have yet to grok exactly how the separation of Dynamo Core from the hosted Dynamo part affects everything. So yeah, there will be a followup post at some point.

The early Dynamo Primary Keys are found in
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall,
which makes sense given that the contemporary versions of Revit included 2013 & 2014, before the shift to 64 bit only. Dynamo 0.9.# only installed on Revit 2015-2017, and 2015 marked the beginning of our current x64 only Revit world, but Autodesk waited till Dynamo 1.0.0 to move to
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall.
But, as we shall see, Dynamo is all over the place, with Primary keys in one location and Secondary keys in others, even Tertiary keys sneaking in.

In any case, on to the data.

Dynamo 0.6.#

Dynamo 0.6.# all share a common GUID, but not a standard GUID only key name. The InstallLocation is not in Program Files, with its specialized permissions, so security is reduced simply by having Dynamo installed. The Publisher property is Autodesk, Inc., so we can’t use this alone to identify a Dynamo key, but we do have a URLInfoAbout property that will help. There is a normal UninstallString property, which is what enables the manual uninstall process from Add & Remove Programs, but conveniently there is also a QuietUninstallString property so we don’t have to go chasing down the silent uninstall argument(s). Another oddity with the uninstall; we get an uninstall EXE, much like with Notepad++, but the actual name of this file seems to change. The first Dynamo in the 0.6.# series that you install will have an unins000.exe uninstaller. But installing a new build over the top may, or may not, rename this file to unins001.exe. It doesn’t seem to happen every time, and only 000 and 001 seem to be used, but it still means we need to verify exactly which name is currently applicable, on a machine by machine basis. This is what triggers the need for more extensive PowerShell code to address the early Dynamo uninstalls.
It is also worth noting here that Microsoft best practice is NOT to include version numbers in the DisplayName property, that’s what the DisplayVersion property is for. Early Dynamo ignores this convention.

Pertinent information for our purposes:
Primary Key = HKLM\…\WOW6432Node\…\Uninstall\{12A2BEA3-7641-4AEC-B344-9B49C8DDFF1A}_is1
DisplayName = Dynamo 0.6.#
DisplayVersion = 0.6.#
InstallLocation = C:\Autodesk\Dynamo\Core
Publisher = Autodesk, Inc.
QuietUninstallString = “C:\Autodesk\Dynamo\Core\Uninstall\unins00#.exe”*/SILENT
URLInfoAbout = dynamobim.com
* Uninstall EXE name changes between unins000.exe & unins001.exe.
Reference Registry images

Dynamo 0.7.#

Dynamo 0.7.0 looks much like 0.6.#, with only a change in URLInfoAbout. Dynamo 0.7.1 then changes Publisher and InstallLocation, the latter finally moving to Program Files where it belongs. Uninstall continues to be handled by an uninstaller EXE that changes names as new builds are installed. The 0.7 series shares a GUID, but continues to not use a standard GUID only key name. It’s worth noting that the change to Dynamo as the Publisher means we can use that to identify a Dynamo Primary Key, as the property only exists in the Primary key, not the Secondary key. This means that from here on out (as long as the Publisher doesn’t change back!) we no longer need to be concerned with the URLInfoAbout property.

Primary Key = HKLM\…\WOW6432Node\…\Uninstall\{6B5FA6CA-9D69-46CF-B517-1F90C64F7C0B}_is1
DisplayName = Dynamo 0.7.#
DisplayVersion = 0.7.#
InstallLocation = C:\Autodesk\Dynamo07\Core (0.7.0) C:\Program Files\Dynamo 0.7 (0.7.1+)
Publisher = Dynamo (0.7.1+)
QuietUninstallString = “…\unins00#.exe”*/SILENT
URLInfoAbout = dynamobim.org (0.7.0+)
* Uninstall EXE name changes between unins000.exe & unins001.exe.
Dynamo 0.8.#

Dynamo 0.8.# at first looks a lot like 0.7.#, but now we have a new Secondary key to be aware of, and oddly it’s found in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall rather than in WOW6432Node like the Primary key. The 0.8 series again shares a GUID, but continues to not use a standard GUID only key name. Note that the secondary key gives us a Version property that identifies the actual build number. This is the first time we have registry access to that information.
One interesting note with regards to that Secondary key. It includes an UninstallString property, just like the Primary key. And a new UninstallParam property, with all new arguments. This combination works, but since so does the information in the Primary key, we CAN ignore it. I do wonder however, at that /UPDATE flag. Update WHILE uninstalling? Um, OK Autodesk. 😉


Primary Key = HKLM\…\WOW6432Node\…\Uninstall\{24113174-8C13-4FA5-8010-772C9144AC41}_is1
DisplayName = Dynamo 0.8.#
DisplayVersion = 0.8.#
InstallLocation = C:\Program Files\Dynamo 0.8
Publisher = Dynamo
QuietUninstallString = “…\unins00#.exe”*/SILENT
Secondary Key = HKLM\SOFTWARE\…\Uninstall\Dynamo 0.8
* Uninstall EXE name changes between unins000.exe & unins001.exe.
Dynamo 0.9.#

Dynamo 0.9.# continues with the 0.8.# pattern, with a new GUID (but still a non standard key name) and changing the InstallLocation in a predictable manner. The uninstall EXE name continues to be variable.


Primary Key = HKLM\SOFTWARE\…\Uninstall\{86B8C99A-85CE-45e1-8149-0A22AAAB3792}_is1
DisplayName = Dynamo 0.9.#
DisplayVersion = 0.9.#
InstallLocation = C:\Program Files\Dynamo 0.9
Publisher = Dynamo
QuietUninstallString = “…\unins00#.exe”*/SILENT
Secondary Key = HKLM\SOFTWARE\…\Uninstall\Dynamo 0.9
* Uninstall EXE name changes between unins000.exe & unins001.exe.
Dynamo 1.#.#

Dynamo 1.0.0 changes… EVERYTHING! First off, Dynamo Core and Dynamo Revit are now split, with independent uninstalls. The Primary and Secondary keys have moved to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
suggesting Dynamo is a fully 64 bit program as of 1.0.0. But Dynamo Revit now gets a Tertiary key, and it still (for unknown reasons) lives in
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall.
On the bright side, both Primary keys are now normal GUID named keys, which simplifies the uninstall, since all we need to do is use the GUID as an argument for msiexec.exe. On the dark side again, Autodesk has decided that every build, both Dynamo Core and Dynamo Revit need a new GUID. Even daily builds get new GUIDs it seems. This is… madness, and utterly contrary to Microso0ft best practice, or indeed any logic I can come up with. Of course, since it’s the GUID we need for the uninstall, this complicates things, a lot more than moving to GUID based uninstalls simplified things. With regards to the new quiet uninstall parameters, Autodesk has /quiet, but Microsoft’s documentation doesn’t mention this. I suspect it is a deprecated approach that may eventually stop working. We’ll use Microsoft best practice in the code.
Another, lesser, positive is that DisplayName now shows only the main version (e.g. Dynamo 1.0.0), while DisplayVersion now properly provides the full build number (e.g. 1.0.0.1180). For those dealing with daily builds this is super helpful.
Also of interest is that Dynamo Core still has an InstallLocation property in the Primary Key, but it’s empty. You need to look in the Secondary key for a duplicate Property that is actually populated. Dynamo Revit retains the appropriate information in the Primary key.
One last tidbit. Notice in the Primary Keys below, how Dynamo Core has a NoRemove property, with a value of 1, and is missing the UninstallString property completely. Compare this to Dynamo Revit, which has NoModify & NoRepair properties (both with values of 1) and doesn’t have a NoRemove property while having a normal UninstallString property. Those three “No” properties in conjunctions with the associated XXXString properties control what is available in the manual Add & Remove Programs tool. If you have a NoRepair property = 1 or the RepairSting property is missing, you get no Repair button in ARP. NoRepair = 0 or missing, and a valid RepairSting and you get a working Repair button. The same is true for Modify and Remove via their respective properties. So, this difference in the two Primary keys explains this difference in ARP, where Dynamo Revit CAN be uninstalled, and Dynamo Core CAN’T.

Dynamo 1.0.0 manual uninstalls

WHY Autodesk chose to do this I have no clue. It’s extra work to break perfectly normal and expected behavior, and they have maintained this behavior all the way through to the current 2.0.3 install. It’s a bit nuts. That said, it’s also not going to get in our way, because this ONLY affects manual uninstalls. Our PowerShell script is gonna tear that @%#$ing Dynamo Core right out like a weed. OK, maybe that was a bit much. But it will uninstall it. 🙂


Primary Key = HKLM\SOFTWARE\…\Uninstall\{Variable GUID}
DisplayName = Dynamo Core 1.#.#
DisplayVersion = 1.#.#.#
InstallLocation =
Publisher = Dynamo
Secondary Key = HKLM\SOFTWARE\…\Uninstall\Dynamo Core 1.#
InstallLocation = C:\Program Files\Dynamo\Dynamo Core\1.#\
UninstallParam = /X{Variable GUID} /quiet
UninstallString = MsiExec.exe

Primary Key: HKLM\SOFTWARE\…\Uninstall\{Variable GUID}
DisplayName = Dynamo Revit 1.#.#
DisplayVersion = 1.#.#.#
InstallLocation = C:\Program Files\Dynamo\Dynamo Revit\1.#\
Publisher = Dynamo
UninstallString = MsiExec.exe /X{Variable GUID}
Secondary Key: HKLM\SOFTWARE\…\Uninstall\Dynamo Revit 1.#
InstallLocation = C:\Program Files\Dynamo\Dynamo Revit\1.#\
UninstallParam = /X{Variable GUID} /quiet
UninstallString = MsiExec.exe
Tertiary Key: HKLM\…\WOW6432Node\…\Uninstall\Dynamo Revit 1.#.#
, , , ,
, , , ,
, , , ,
, , , ,
, , , ,
, , , ,
, , , ,
, , , ,
, , , ,

Dynamo 2.#.#

Dynamo 2.#.# continues as with 1.#.#, new GUIDs for everything, crippled manual uninstall of Dynamo Core, Primary, Secondary and for Revit misplaced Tertiary keys.

, , , ,
, , , ,
, , , ,
, , , ,

Uninstall Detritus

That gives us the registry data we need, but I should clarify my process a bit, so you understand WHY we need some of that data.

In my testing, I have found that…
Some Dynamo uninstalls leave registry keys behind.
Some Dynamo uninstalls leave empty install folders behind.
Some Dynamo uninstalls delete their install folders AND clean up empty parent folders, other do not and leave parent folders behind.
Some Daily Build Installs delete the Primary Key of the previous major version, but leave the Secondary Key and full Install Location folders. With the Primary Key gone, there isn’t an easy way to track down these orphans other than brute force.

So, our code will need to identify what it can and delete those orphans, and also provide a means of cleaning up any other leftovers.

The Code

In this section, we will build a script to uninstall every Dynamo on a machine, in hopefully easy to follow steps. I am going to annotate the code fairly extensively, for two reasons.
1: I think PowerShell is a super useful tool that all IT/CAD/BIM Managers should be using, and this provides an introduction if you aren’t yet.
2: I want everyone to understand what the code is doing, before they start using it on their production machines.

We will start simple, and add to the code in steps, until we end up with the complete utility. With that, let’s look at the code!

Gathering Uninstall Keys

We’ll start by just gathering all the Uninstall keys and verifying them.

1$uninstall32 = 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
2$uninstall64 = 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall'
3$uninstallKeys = (Get-ChildItem "Registry::$uninstall64") +
4(Get-ChildItem "Registry::$uninstall32")
5
6foreach ($uninstallKey in $uninstallKeys) {
7Write-Host $uninstallKey
8}
For anyone just getting into PowerShell, here’s what’s going on in more detail.

1&2We know we have two registry keys to look in, so assign them to variables. We’ll need to use these variables independently later.

3&4Make an array of all the subkeys by combining the output of Get-ChildItem with each variable. The Registry:: bit defines the “provider” in the string, which is required when using Get-ChildItem.

6-8Loop through the array and use Write-Host to verify we are getting the Keys we expect.

Here I need to call out a feature of PowerShell that is super helpful, but can catch you out at times. Get-ChildItem returns an Object, the actual Key, with all it’s properties and subkeys included. Here our $uninstallKey is therefor an object, not a string. But, when we pass an object to Write-Host, which requires a string, a built in ToString method actually hands back a particular property converted to a string, and what property this is, the developer decides. In this case, it’s the name property, which we would normally access this way $uninstallKey.name. That’s a poor choice of Property name in my opinion, Path would have been better, but it is the information we are looking for, and 90% of the time the developers get it right. But, if you aren’t seeing what you expect to see at some point, you can use Get-Member to explore what the object in your variable actually contains. This may be helpful if you are new to PowerShell and want to explore this in more detail.
In any case, here we could also use Write-Host $uninstallKey.name and get the same results, but with the source of that result made more clear in the code. We will use this approach in the next iteration of the code.
The output of this code will look something like this, and as you can see we have results from both Uninstall keys, and you can even see one of the early Dynamo keys towards the bottom. This list is MUCH larger, so if you are following along at home, scroll around a bit and see what your machine has installed.
Identifying Dynamo Primary Keys

Adding to the previous code, we can identify just the Dynamo uninstall keys.

1$uninstall32 = 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
2$uninstall64 = 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall'
3$uninstallKeys = (Get-ChildItem "Registry::$uninstall64") +
4(Get-ChildItem "Registry::$uninstall32")
5
6foreach ($uninstallKey in $uninstallKeys) {
7$keyPath = "Registry::$($uninstallKey.name)"
8$publisher = (Get-ItemProperty -path:$keyPath -name:'Publisher' -errorAction:silentlyContinue).Publisher
9$URLInfo = (Get-ItemProperty -path:$keyPath -name:'URLInfoAbout' -errorAction:silentlyContinue).URLInfoAbout
10
11if (($publisher -eq 'Dynamo') -or ($URLInfo -like '*dynamobim*')) {
12$displayname = (Get-ItemProperty -path:$keyPath -name:'DisplayName' -errorAction:silentlyContinue).DisplayName
13Write-Host $displayname
14}
15}
And again the details…

7First we define a keyPath variable, which uses the path we echoed to the console previously, this time prefixed with the provider as needed by Get-ItemProperty, similar to Get-ChildItem.

8&9Then we define two more variables for publisher & URLInfo, which use Get-ItemProperty to get the actual value of the property. Here a couple things to note are the -errorAction:silentlyContinue which ensures that a key that doesn’t contain the given property at all comes up empty rather than throwing an error, and the fact that we have grouped the results of Get-ItemProperty and then referenced the Property by name again. This seems odd, and redundant, but remember that PowerShell likes to return objects. So we are getting an actual Property object from Get-ItemProperty, which contains more information that just the value, and we are accessing the value as we would a property in an object that contains a lot of properties, not just the one. The net result of all this is, on Keys that do have either of these properties with values, we end up with variables containing those values. For keys that don’t, the variables will be empty.

11&14Next we have a conditional, which is true if the Publisher is Dynamo, or the URLInfo contains dynamobim, which accounts for the domain name change in Dynamo 0.7.0. The conditional is really the whole block of code from 11-14, with the first line defining the condition and the last ending the conditional.

12&13Within the conditional we have a variable for the DisplayName property and dump it to the screen for review.

The output is a much smaller list of installed Dynamo stuff, showing only the name. It’s not in order, since Autodesk lets the GUIDs jump around, but I have ever seen an issue with uninstalling out of order, so I haven’t bothered to sort the results.
Gathering Uninstall Data

Now we’ll replace that displayName variable and the Write-Host with some code that gathers all the install data, all the uninstall keys, the uninstall executable and arguments, and finally the install location.

1$uninstall32 = 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
2$uninstall64 = 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall'
3$uninstallKeys = (Get-ChildItem "Registry::$uninstall64") +
4(Get-ChildItem "Registry::$uninstall32")
5
6foreach ($uninstallKey in $uninstallKeys) {
7$keyPath = "Registry::$($uninstallKey.name)"
8$publisher = (Get-ItemProperty -path:$keyPath -name:'Publisher' -errorAction:silentlyContinue).Publisher
9$URLInfo = (Get-ItemProperty -path:$keyPath -name:'URLInfoAbout' -errorAction:silentlyContinue).URLInfoAbout
10
11if (($publisher -eq 'Dynamo') -or ($URLInfo -like '*dynamobim*')) {
12$uData = @{
13pKey = $null
14sKey = $null
15tKey = $null
16uExecutable = $null
17uArguments = $null
18iLocation = $null
19}
20$dName = (Get-ItemProperty -path:$keyPath -name:'DisplayName' -errorAction:silentlyContinue).DisplayName
21$uData.pKey = $keyPath
22if (Test-Path "Registry::$uninstall64\$($dName.subString(0, $dName.length-2))") {
23$uData.skey = "Registry::$uninstall64\$($dName.subString(0, $dName.length-2))"
24}
25if (Test-Path "Registry::$uninstall32\$dName") {
26$uData.tkey = "Registry::$uninstall32\$dName"
27}
28if (-not ($uData.iLocation = (Get-ItemProperty -path:$uData.pKey -name:'InstallLocation' -errorAction:silentlyContinue).InstallLocation)) {
29$uData.iLocation = (Get-ItemProperty -path:$uData.sKey -name:'InstallLocation' -errorAction:silentlyContinue).InstallLocation
30}
31
32$quietUninstallString = (Get-ItemProperty -path:$uData.pKey -name:'QuietUninstallString' -errorAction:silentlyContinue).QuietUninstallString
33if ($uData.pKey -match '(?<guid>\{[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}\})$') {
34$uData.uExecutable = 'MsiExec.exe'
35$uData.uArguments = "/x $($matches['guid']) /q"
36} elseif ($quietUninstallString -match '"(?<executable>[^"]+)"\s+(?<arguments>.+)') {
37$uData.uExecutable = $matches['executable']
38$uData.uArguments = $matches['arguments']
39}
40
41Write-Host $dName
42Write-Host "  p:$($uData.pKey)"
43Write-Host "  s:$($uData.sKey)"
44Write-Host "  t:$($uData.tKey)"
45Write-Host "  l:$($uData.iLocation)"
46Write-Host "  e:$($uData.uExecutable) $($uData.uArguments)"
47}
48}

12-19First we define the uData variable as a hash table, with keys for each of the pieces of information we will need to manage, and set the value to null so we have no leftover data from the previous key. The exception is the DisplayName, which we will handle as a separate variable, for reasons that will become apparent in the next iteration.

20Then we set the dName variable, using the same code we used for the displayName variable in the previous iteration. The change in variable naming convention is just due to some upcoming width constraints related to the web site. You can use whatever naming convention makes sense.

21The pKey (Primary Key) is just the keyPath variable we previously defined and used to get the Publisher and URLInfo data we needed to isolate Dynamo.

22-24The Secondary Key requires a little string manipulation to build, then we test with Test-Path to see if the registry key actually exists, and set the hash table key only if it does. The substring stuff is just removing the final 2 characters, since the Secondary key name consistently gets a name based on major & minor version only. And the Secondary Key is always in the native architecture Uninstall key, even with older versions of Dynamo where the Primary Key is in Wow6432Node, so we can use the uninstall64 variable we assigned earlier for the first part of the path.

25-27The Tertiary Key is similar to the Secondary Key, with no need to do any string manipulation of the DisplayName and a consistent location in Wow6432Node.

28-30For the install location we first check in the Primary Key, and if we don’t find it there we check in the Secondary Key, to account for the change in Dynamo Core 1.0.0. We don’t bother with a Test-Path since the only way we won’t find it is after we actually uninstall, so the test happens later.

32-39Next we get the SilentInstallString and assign it to a dedicated variable, then use a Regular Expression to see if we have a GUID based Primary Key, in which case we assign standard Uninstall Executable and Arguments based on that GUID. If we don’t have a true GUID based Primary Key, then we check for a Dynamo uninstall executable and arguments in the SilentUninstallString, again with a RegEx. This could be a more complex Regex, looking for unins###.exe specifically, but RegEx can be hard, so I kept things somewhat simple. If neither condition is found, then both the uExecutable and uArguments keys of the uData hash table variable will remain blank.

41-46Finally we use Write-Host to present the gathered data for verification.

The output here shouldn’t be much of a surprise at this point.
Dynamo Uninstalls

Now that we have our data, we can do the actual uninstalls.

1$uninstall32 = 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
2$uninstall64 = 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall'
3$uninstallKeys = (Get-ChildItem "Registry::$uninstall64") +
4(Get-ChildItem "Registry::$uninstall32")
5
6foreach ($uninstallKey in $uninstallKeys) {
7$keyPath = "Registry::$($uninstallKey.name)"
8$publisher = (Get-ItemProperty -path:$keyPath -name:'Publisher' -errorAction:silentlyContinue).Publisher
9$URLInfo = (Get-ItemProperty -path:$keyPath -name:'URLInfoAbout' -errorAction:silentlyContinue).URLInfoAbout
10
11if (($publisher -eq 'Dynamo') -or ($URLInfo -like '*dynamobim*')) {
12$uData = @{
13pKey = $null
14sKey = $null
15tKey = $null
16uExecutable = $null
17uArguments = $null
18iLocation = $null
19}
20$dName = (Get-ItemProperty -path:$keyPath -name:'DisplayName' -errorAction:silentlyContinue).DisplayName
21$uData.pKey = $keyPath
22if (Test-Path "Registry::$uninstall64\$($dName.subString(0, $dName.length-2))") {
23$uData.skey = "Registry::$uninstall64\$($dName.subString(0, $dName.length-2))"
24}
25if (Test-Path "Registry::$uninstall32\$dName") {
26$uData.tkey = "Registry::$uninstall32\$dName"
27}
28if (-not ($uData.iLocation = (Get-ItemProperty -path:$uData.pKey -name:'InstallLocation' -errorAction:silentlyContinue).InstallLocation)) {
29$uData.iLocation = (Get-ItemProperty -path:$uData.sKey -name:'InstallLocation' -errorAction:silentlyContinue).InstallLocation
30}
31
32$quietUninstallString = (Get-ItemProperty -path:$uData.pKey -name:'QuietUninstallString' -errorAction:silentlyContinue).QuietUninstallString
33if ($uData.pKey -match '(?<guid>\{[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}\})$') {
34$uData.uExecutable = 'MsiExec.exe'
35$uData.uArguments = "/x $($matches['guid']) /q"
36} elseif ($quietUninstallString -match '"(?<executable>[^"]+)"\s+(?<arguments>.+)') {
37$uData.uExecutable = $matches['executable']
38$uData.uArguments = $matches['arguments']
39}
40
41if ($uData.uExecutable -and $uData.uArguments) {
42$process = (Start-Process -filePath:$uData.uExecutable -argumentList:$uData.uArguments -passThru -wait)
43if ($process.exitCode -eq 0) {
44Write-Host "Uninstalled $dName"
45} else {
46Write-Host "Failed to uninstall $dName with exit code $($process.exitCode)"
47}
48}
49}
50}

41-48To handle the actual uninstalls we have an if to ensure we only attempt an uninstall if we have both an uninstall executable and arguments to work with. If we do, we set the Process variable using Start-Process and our executable and arguments. The -passThru parameter allows the exit code from the executable to be passed through the cmdlet and available in the variable, and the -wait parameter ensured that the next uninstall isn’t started until the previous one completes, since Windows only allows one at a time and will throw an error otherwise. The exit code we are looking for is 0, the standard “success” code, and we have a conditional to either show us the uninstall succeeded or what exit code we got back on a failure. Thus far I have yet to see a failure in all my testing, but we want that possibility accounted for.

This gives us the basics to uninstall every Dynamo on a machine, but we still have to deal with some issues in the final version.
The whole Shebang

There are a couple of issues to be addressed in the final version. Uninstalls sometimes orphan things, registry keys and file system folders, so we need to deal with that. And it would be good to more gracefully address machines that don’t have any Dynamo installed. And when we delete the InstallLocation, when it’s orphaned, we want to also walk back up the file system tree deleting parent folders until we reach Program Files. This avoids having C:
Lastly, the uninstalls happen out of order, due to the random nature of Autodesk’s GUIDs for the Primary Keys. I haven’t had any issues with uninstalls being out of order, but I find it… inelegant. So we will add sorting as well.

1$uninstall32 = 'HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
2$uninstall64 = 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall'
3$uninstallKeys = (Get-ChildItem "Registry::$uninstall64") +
4(Get-ChildItem "Registry::$uninstall32")
5
6$uninstalls = @{}
7foreach ($uninstallKey in $uninstallKeys) {
8$keyPath = "Registry::$($uninstallKey.name)"
9$publisher = (Get-ItemProperty -path:$keyPath -name:'Publisher' -errorAction:silentlyContinue).Publisher
10$URLInfo = (Get-ItemProperty -path:$keyPath -name:'URLInfoAbout' -errorAction:silentlyContinue).URLInfoAbout
11
12if (($publisher -eq 'Dynamo') -or ($URLInfo -like '*dynamobim*')) {
13$uData = @{
14pKey = $null
15sKey = $null
16tKey = $null
17uExecutable = $null
18uArguments = $null
19iLocation = $null
20}
21$dName = (Get-ItemProperty -path:$keyPath -name:'DisplayName' -errorAction:silentlyContinue).DisplayName
22$uData.pKey = $keyPath
23if (Test-Path "Registry::$uninstall64\$($dName.subString(0, $dName.length-2))") {
24$uData.skey = "Registry::$uninstall64\$($dName.subString(0, $dName.length-2))"
25}
26if (Test-Path "Registry::$uninstall32\$dName") {
27$uData.tkey = "Registry::$uninstall32\$dName"
28}
29if (-not ($uData.iLocation = (Get-ItemProperty -path:$uData.pKey -name:'InstallLocation' -errorAction:silentlyContinue).InstallLocation)) {
30$uData.iLocation = (Get-ItemProperty -path:$uData.sKey -name:'InstallLocation' -errorAction:silentlyContinue).InstallLocation
31}
32
33$quietUninstallString = (Get-ItemProperty -path:$uData.pKey -name:'QuietUninstallString' -errorAction:silentlyContinue).QuietUninstallString
34if ($uData.pKey -match '(?<guid>\{[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}\})$') {
35$uData.uExecutable = 'MsiExec.exe'
36$uData.uArguments = "/x $($matches['guid']) /q"
37} elseif ($quietUninstallString -match '"(?<executable>[^"]+)"\s+(?<arguments>.+)') {
38$uData.uExecutable = $matches['executable']
39$uData.uArguments = $matches['arguments']
40}
41
42if ($uData.uExecutable -and $uData.uArguments) {
43$uninstalls.Add($dName, $uData)
44} else {
45Write-Host "Error: Unable to define uninstall for $dName"
46}
47}
48}
49
50if ($uninstalls.count -gt 0) {
51Write-Host "`nUninstalling Dynamo...`n"
52
53# Build Special Folders array
54$specialFolders = New-Object collections.arrayList
55foreach ($specialFolderName in [enum]::GetNames([Environment+SpecialFolder])) {
56if ((($specialFolderPath = [environment]::getfolderpath($specialFolderName)) -ne '') -and ($specialFolders -NotContains $specialFolderPath)) {
57[void]$specialFolders.Add($specialFolderPath)
58}
59}
60
61# Sort Uninstalls
62$sortedUninstalls = New-Object collections.specialized.orderedDictionary
63foreach ($key in $uninstalls.keys | Sort-Object)) {
64$sortedUninstalls[$key] = $uninstalls[$key]
65}
66
67# Process Uninstalls
68foreach ($uninstall in $sortedUninstalls.keys)) {
69
70# Refresh Uninstall Data
71$uData = @{}
72$uData.Add('dName', $uninstall)
73foreach ($key in $sortedUninstalls.$uninstall.keys)) {
74$uData.Add($key, $sortedUninstalls.$uninstall[$key])
75}
76
77# Process Uninstall
78$process = (Start-Process -filePath:$uData.uExecutable -argumentList:$uData.uArguments -passThru -wait)
79if ($process.exitCode -eq 0) {
80$uninstallSucceeded = $true
81Write-Host "Uninstalled $($uData.dName)"
82Start-Sleep -seconds:10
83
84if ($uData.pKey -and (Test-Path $uData.pKey)) {
85try {
86Remove-Item $uData.pKey -force -errorAction:stop
87Write-Host "  Deleted Primary Key: $($uData.pKey)"
88} catch {
89Write-Host "  Delete key failed: $($_.Exception.Message)"
90}
91}
92if ($uData.sKey -and (Test-Path $uData.sKey)) {
93try {
94Remove-Item $uData.sKey -force -errorAction:stop
95Write-Host "  Deleted Secondary Key: $($uData.sKey)"
96} catch {
97Write-Host "  Delete key failed: $($_.Exception.Message)"
98}
99}
100if ($uData.tKey -and (Test-Path $uData.tKey)) {
101try {
102Remove-Item $uData.tKey -force -errorAction:stop
103Write-Host "  Deleted Tertiary Key: $($uData.tKey)"
104} catch {
105Write-Host "  Delete key failed: $($_.Exception.Message)"
106}
107}
108if ($uData.iLocation -and (Test-Path $uData.iLocation)) {
109try {
110Remove-Item "$($uData.iLocation)\*" -recurse -force -errorAction:stop
111Write-Host "  Deleted Install Location contents: $($uData.iLocation)"
112} catch {
113Write-Host "  Delete folder contents failed: $($_.Exception.Message)"
114}
115$candidateFolderToDelete = $uData.iLocation
116:deleteEmptyFolders while ((Get-ChildItem -path:$candidateFolderToDelete -force -errorAction:silentlyContinue | Measure-Object).count -eq 0) {
117try {
118Remove-Item $candidateFolderToDelete -recurse -force -errorAction:stop
119Write-Host "  Deleted: $candidateFolderToDelete"
120$candidateFolderToDelete = Split-Path $candidateFolderToDelete -parent
121if ($SpecialFolders -contains $candidateFolderToDelete) {
122break :deleteEmptyFolders
123}
124} catch {
125Write-Host "  Delete folder failed: $($_.Exception.Message)"
126break :deleteEmptyFolders
127}
128}
129}
130} else {
131Write-Host "Failed to uninstall $($uData.dName) with exit code $($process.exitCode)"
132$uninstallFailed = $true
133}
134}
135
136# Orphans
137if ($uninstallSucceeded -and (-not $uninstallFailed)) {
138$orphans = @('C:\Program Files\Dynamo', 'C:\Program Files\Dynamo 0.9', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Dynamo 0.9')
139$orphanHeader = "`n Deleted orphan(s)"
140foreach ($orphan in $orphans) {
141if (Test-Path $orphan) {
142try {
143Remove-Item $orphan -force -recurse -errorAction:stop
144if ($orphanHeader) {
145Write-Host $orphanHeader
146$orphanHeader = $null
147}
148Write-Host "    Deleted $orphan"
149} catch {
150if ($orphanHeader) {
151Write-Host $orphanHeader
152$orphanHeader = $null
153}
154Write-Host "  Delete folder failed: $($_.Exception.Message)"
155}
156}
157}
158}
159} else {
160Write-Host "`nNo Dynamo installed on this machine."
161}
162Write-Host "`nCOMPLETE!"

6We start by initializing $uninstalls as a hash table, to facilitate the uninstalls sorting.

43-45Instead of simply uninstalling when $uData.uExecutable & $uData.uArguments are present, we instead add the current $uData hash table to $uninstalls as a nested hash table, with $dName as the key name. If the conditional fails, we provide a message.

50&51Here we check for uninstalls to do by checking the count property of $uninstalls and provide a nice message.

53-59Next we build an array with the paths to all the Special Folders, which are locations like My Documents, All User Desktop, and most important for us, Program Files. Some of these are blank, or duplicates, so we have some code to validate a bit and keep the array light weight. It’s worth noting here that the Special Folders list we build here is complete in Windows 10/PowerShell 3.0 and higher, but if you are using Windows 7 and haven’t upgraded PowerShell from the 2.0 version that comes default, then your list will be incomplete. For our purposes we are fine, because Program Files is there, but if you use this technique more broadly, there’s the potential for an error on those older machines. One of the MANY reasons why Px Tools 4.0 will require PowerShell 5.0, even on Windows 7.

61-65Next we address the sorting issue. We have , which is the unsorted hash table of hash tables. So we create a new $sortedUninstalls orderedDictionary, which is just a hash table that stays in creation order, whereas a regular hash table does not maintain order. Then we loop through the keys of our $uninstalls hash table that have been sorted by piping to Sort-Object. We can then build our sorted hash table in the correct order by referencing the specific key from the unsorted hash table.

68Then we loop through the keys of that sorted hash table.

70-75With the key in sorted order in $uninstall, we reinitialize the $uData hash table on each iteration of the loop, then use $uninstall to populate the dName key of the fresh hash table. Then a loop through the other keys of nested hash table so we end up with all our required data in a single, simple unsorted hash table for the current uninstall iteration.

78&79Finally we process the uninstall, and test for success.

80-82And if we are successful, we set $uninstallSucceeded to true, we provide a message showing that success, and we pause for a few seconds with Start-Sleep. This last is because I have seen some of these uninstalls take a few seconds to complete some folder deletes, despite control being handed back to PowerShell from the uninstaller. It is as if the uninstaller is actually spawning a separate thread for folder delete, which is really odd. But a short pause here ensures that by the time we continue Autodesk really is done with whatever they are doing, and we can start cleaning up what they didn’t do.

84-91Now we can start the orphan cleanup process, beginning with the Primary Key. We test to see if a Primary Key is defined (it always will be, but good to be consistent) and present, and if so delete with Remove-Item and provide a message. Note that here we are using a try { } catch { } to deal with any potential permissions issues.

92-99The Secondary Key is handled the same way.

100-107As is the Tertiary Key.

108-129Install Location is a bit more complex. We start with a similar construct, but we only delete the CONTENTS of the folder. Then we seed $candidateFolderToDelete with the InstallLocation path. Next we enter a labeled while loop that only deletes the folder if it’s empty. On first pass it is, because we just emptied it. Having deleted a target folder we then revise the variable to the parent folder. If that parent folder is in $SpecialFolders we break out of the loop and don’t delete. But if it’s not a Special Folder we continue the loop, walking back up the file system tree deleting empty folders until we find one that either isn’t empty or is a Special Folder. We also have a try { } catch { } for errors again.

131&132Here we have the other end of the conditional from line 79, providing a message if the uninstall failed. We also flag failed uninstalls here with $uninstallFailed

136-158If we both DID complete at least one uninstall, and we did NOT have any failures, then we consider the process a success, and cleaning up orphans is appropriate. This will loop through the array of possible orphans and delete any found, with a nifty trick for providing a header message only if an orphan is handled (deleted or found but failed to delete). Note that the registry path has a different format, because Test-Path and Remove-Item don’t use the Registry provider exclusively, so they take a different format from . This code also doesn’t walk back up the tree since orphans are somewhat unusal. Additional orphans can be added if someone discovers a daily build that orphans something other than Dynamo 0.9.

160-162And finally, we have the other end of the conditional from line 50, where we provide a message when no Dynamo is installed, and a final message at completion.

So that about wraps us up. Hopefully you have a good understanding of the implications of a Dynamo uninstall, and what kind of process you might go through to gather data on a different program you need to uninstall. And you now have some understanding of PowerShell and how it can be used to automate this kind of thing. In the next installment I will be talking a bit about Px Tools 4.0, and how Dynamo has informed some new features and workflows that I think will greatly improve the process.

Finally, we get to the bit that actually matters. A DOWNLOAD!

Uninstall_Dynamo.zip

And lastly, a tangent and request. A HUGE part of this post has been getting the CSS right, and while I have tested on a few browsers, I can’t be sure yet if this is adequately “cross browser/platform/device”. So, if you have any issues with the formatting, please contact me and let me know what issues you are having, and on what browser/platform.

Comments