Tag Archives: PowerShell

Programmatically Uninstalling a ClickOnce Application

ClickOnce, in general, is a great way to deploy a program. I could set it to check for updates before starting, easily publish a new version of a program, etc. But uninstalling ClickOnce programs cannot be done automatically as they pop up a “Maintenance” window when you try to uninstall that needs someone to click on it:

Well I was in a situation where I needed to uninstall two different ClickOnce programs on 200+ machines and manually uninstalling on all of them wasn’t really a option. So I searched and found a 13 year old post about the same issue with a neat PowerShell answer here: https://stackoverflow.com/a/24440119/5061596 It’s slightly hacky, using SendKeys to acknowledge the uninstall screen, but it worked. I then customized it more to look for the original executable and wait until it was removed before moving on as ClickOnce wouldn’t let me try uninstalling another program before the first one was done. Here is the PowerShell function:

Function UninstallClickOnceProgram {
    Param(
        [Parameter(Mandatory=$true)][string]$ProgramName,
        [Parameter(Mandatory=$true)][string]$Image
    )
    <#
    .SYNOPSIS
        Checks for the existence of a ClickOnce program and if found uninstalls it using the uninstall string in the registry.  While it's uninstalling it will
        look for the existence of the executable for up to 1 minute
    .PARAMETER ProgramName
        Name of the program being checked.  This is the name as it exists in the standard Add/Remove programs dialog.
    .PARAMETER Image
        Name of the executable itself. This is used to kill the process if it's open.
    .OUTPUTS
        None
    #>
    $InstalledApplicationNotMSI = Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall | ForEach-Object {Get-ItemProperty $_.PsPath}
    $UninstallString = $InstalledApplicationNotMSI | ? { $_.displayname -match $ProgramName } | select UninstallString
    If (-not [String]::IsNullOrEmpty($UninstallString)){
        $MyPID = (Get-Process $Image -ea SilentlyContinue | select -ExpandProperty id)
        IF ($MyPID -ne $null) {
            Write-Host "$ProgramName is running.  Attempting to kill process...."
            Stop-Process -id $MyPID -Force
            Start-Sleep -s 5
        }
        Write-Host "Attempting to uninstall the ClickOnce application $ProgramName"
        $wshell = new-object -com wscript.shell
        $selectedUninstallString = $UninstallString.UninstallString
        $wshell.run("cmd /c $selectedUninstallString")
        Start-Sleep 5
        $wshell.sendkeys("`"OK`"~")
        Write-Host "Checking status...."
        For ($i=1; $i -le 10; $i++) {
            $MyExecutable = Get-ChildItem "$env:LOCALAPPDATA\Apps\2.0" -Filter "$Image.exe" -Recurse | % { $_.FullName } | Select-Object -First 1
                If ($MyExecutable -eq $null) {
                    Write-Host "$ProgramName was not found."
                    break
                } Else {
                    If (Test-Path $MyExecutable) {
                        Write-Host "$ProgramName still found.  Waiting....  ($i/10)."
                    } Else {
                        Write-Host "$ProgramName was uninstalled."
                        break
                    }
                }
            Start-Sleep -s 6
        }
        Start-Sleep -s 5
    } else {
        Write-Host "Did not find the ClickOnce application $ProgramName"
    }
}

And here is how I call it:

UninstallClickOnceProgram -ProgramName "My Super Database" -Image "MSD"

I still sent out a notification email letting people know this was happening and not to click anything and give the login scripts a extra 60 seconds. But the end result was a relatively automated uninstall of our ClickOnce programs that we needed to uninstall.

Removing Orphaned GPO Settings Listed as Extra Registry Settings

Our domain has gone through multiple upgrades over the years from Server 2003 to the current Server 2019 domain controllers with a mix of 2012 and 2019 member servers. Each time there is a group policy admin template update I download it and copy it over to the central store. Today I went to edit a GPO object and noticed when checking the settings being applied under the “settings” tab there was a “Extra Registry Settings” section for a old version of Office under the administrative templates section:

After some research a couple things said to download the old templates, install them, remove the settings, then remove the templates. But that got me thinking on how old some of the templates probably are at this point and how I probably also had a bunch of no longer needed settings in GPO.

So I decided to start “fresh”. I first reviewed Microsoft’s document on creating and managing the central store for group policy admin templates and also downloaded the latest templates which as of this post were the Windows 10 November 2021. I also grabbed the latest Microsoft Office templates and made sure to check where the OneDrive ones were located as I would need those. I installed the Windows 10 ADMX to C:\Temp\Win10 and the Office ADMX to C:\Temp\Office on my own computer.

I then made a backup of my current templates by browsing to \\domain.name\SYSVOL\domain.name\Policies and creating a directory called “Old PolicyDefinitions”. I went into the PolicyDefinitions folder, selected everything, cut it, and moved it to my old folder. I then copied all the new templates from my computer, Windows 10, Office, and OneDrive, to the now empty PolicyDefinitions folder.

Now that everything was shiny and new I went through every GPO and checked for that Extra Registry Settings section and there were a bunch. References to Office 14.0 and 15.0 that are no longer used, SkyDrive, misc Windows XP settings, depreciated settings that are just not in the latest templates, etc. Now there are two choices here. Either I had to screen shot each one of these screens, switch back to the old templates, remove things, and switch back or thankfully there is a PowerShell command you can use on the domain controller:

Remove-GPRegistryValue -Name "Default Domain Policy" -key "HKLM\Software\Policies\Microsoft\OneDrive" -ValueName KFMSilentOptOut

Name is your GPO name. Key is going to be whatever is in the extra settings list. HKLM for anything computer config related, HKCU for anything user config related. You can also delete entire keys by omitting the -ValueName part but you will have to delete subkeys first. I.e. Office\14.0\Word\Common needs to be deleted before Office\14.0\Word can be deleted. I deleted a bunch, hit F5 in Group Policy Management so the settings screen would refresh, then delete what was left.

After a hour or so I had all my GPOs cleaned up with all the obsolete junk removed and also went through a bunch of settings that don’t need applied any more. Between the two I probably removed 100 entries which should speed up group policy processing if even slightly.

Check Uptime of Domain Computers

Recently we pushed some updates through GPO which ran at a users login to the domain. Weeks went by and I kept getting calls about people with old software that didn’t update. After some quick investigating these users were simply not rebooting or shutting down their computers and some were going on two months. On one hand that’s pretty good for Windows 10 machines but on the other they were missing important updates. After looking around I found that PsInfo.exe, part of the PSTools suite, would let me poll a computer for uptime but I wanted to poll all the computers and see how widespread this problem was.

First I started with a list of all computers taken from Active Directory using this PowerShell command to export them to a text file. Technically this command exports to a csv but I’m only taking one column so I skipped a step:

Get-ADComputer -Filter * -Properties Name | Select-Object Name | Export-CSV "C:\temp\ComputerNames.txt" -NoTypeInformation

Opening the file you should have a header of Name with all your workstations. I deleted the header and did a global find and replace to remove the quotes so I had a file with just the workstation names. Next I made a batch file with this single line:

For /f "tokens=*" %%i in (ComputerNames.txt) do psinfo uptime -nobanner \\%%i >> uptime.txt

I placed the batch file (CheckUptime.bat for me) in the same directory as PsInfo.exe and my ComputerNames.txt file. Run the batch file and it will step through each computer name in the file and check the uptime giving you something like this:

System information for \WSComputer2:
Uptime: 0 days 5 hours 24 minutes 57 seconds
System information for \WSComputer6:
Uptime: 2 days 17 hours 45 minutes 18 seconds
System information for \WSComputer23:
Uptime: 0 days 0 hours 42 minutes 41 seconds

I’m sure there is also a way to scrap the file and clean this up but it works for my needs.