vb.netvisual-studio-2022microsoft365

VB.NET Can't Uninstall M365 via Registry UninstallString


Morning all I am trying to uninstall Microsoft 365 products using VB.NET with .NET Frameworks 9.0.

I have a list of M365 products that need to be unisntalled saved as a array

Dim officeProductIds As String() = {
    "O365ProPlusEEANoTeamsRetail",
    "O365ProPlusRetail",
    "O365BusinessEEANoTeamsRetail",
    "O365BusinessRetail"
}

I then find the uninstall string by looking in the following registry keys

        Dim uninstallPaths As String() = {
            "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
            "SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
        }

I then ass this down and call PowerShell to uninstall the application with the following code

Try
    Dim baseKey As RegistryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)

    For Each regPath In uninstallPaths
        Using uninstallKey As RegistryKey = baseKey.OpenSubKey(regPath)
            If uninstallKey Is Nothing Then Continue For

            For Each subKeyName As String In uninstallKey.GetSubKeyNames()
                If officeProductIds.Any(Function(id) subKeyName.Contains(id)) Then
                    Dim uninstallString As String = ReadRegistry.GetOfficeUninstallString(subKeyName)
                    If Not String.IsNullOrEmpty(uninstallString) Then
                        Console.WriteLine($"[FOUND] Uninstall string for {subKeyName}: {uninstallString}")


                        Dim installerPath As String = ""
                        Dim arguments As String = ""

                        If uninstallString.StartsWith(ChrW(34)) Then
                            Dim endQuoteIndex As Integer = uninstallString.IndexOf(ChrW(34), 1)
                            installerPath = uninstallString.Substring(1, endQuoteIndex - 1)
                            arguments = uninstallString.Substring(endQuoteIndex + 1).Trim()
                        Else
                            Dim firstSpace = uninstallString.IndexOf(" "c)
                            If firstSpace > 0 Then
                                installerPath = uninstallString.Substring(0, firstSpace)
                                arguments = uninstallString.Substring(firstSpace + 1).Trim()
                            Else
                                installerPath = uninstallString
                            End If
                        End If
                        Dim installerFolder As String = IO.Path.GetDirectoryName(installerPath)
                        Console.WriteLine()
                        Console.WriteLine($"Installer: {IO.Path.GetFileName(installerPath)}")
                        Console.WriteLine($"Install Folder: {installerFolder}")
                        Console.WriteLine($"Arguments: {arguments}")
                        Console.WriteLine()

                        ApplicationInstallerHelper.UninstallApplication(IO.Path.GetFileName(installerPath), installerFolder, True, arguments)
                    Else
                        Console.WriteLine($"[WARNING] No UninstallString for {subKeyName}")
                    End If
                End If
            Next
        End Using
    Next
  Catch ex As Exception
     Console.WriteLine("[EXCEPTION] " & ex.Message)
  End Try

It then shows it as completed but when I look in Windows Installed Applications 365 products are still listed.

Just FWI the code below

ApplicationInstallerHelper.UninstallApplication(IO.Path.GetFileName(installerPath), installerFolder, True, arguments)
Public Sub UninstallApplication(installerFileName As String, installerFolderPath As String, Optional silent As Boolean = False, Optional installerArguments As String = "")
    Dim fullPath As String = IO.Path.Combine(installerFolderPath, installerFileName)

    If Not IO.File.Exists(fullPath) Then
        StrBuilder.WrapMessage($"Installer not found at: {fullPath}", "ERR")
        Return
    End If

    If silent AndAlso Not installerArguments.Contains("/quiet") Then
        installerArguments &= " /quiet"
    End If
    If silent AndAlso Not installerArguments.Contains("/norestart") Then
        installerArguments &= " /norestart"
    End If

    Dim psCommand As String =
        $"Start-Process -FilePath '{fullPath}' -ArgumentList '{installerArguments}' -Wait -NoNewWindow; exit $LASTEXITCODE"

    Dim psi As New ProcessStartInfo("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command ""{psCommand}""")
    psi.UseShellExecute = False
    psi.RedirectStandardOutput = True
    psi.RedirectStandardError = True
    psi.CreateNoWindow = True

    Dim proc As Process = Process.Start(psi)
    proc.WaitForExit()

    Dim output As String = proc.StandardOutput.ReadToEnd()
    Dim errors As String = proc.StandardError.ReadToEnd()
    Dim code As Integer = proc.ExitCode

    If code = 0 Then
        StrBuilder.WrapMessage("Uninstall completed successfully.", "OK")
    Else
        StrBuilder.WrapMessage($"Uninstall failed with exit code {code}", "ERR")
        If Not String.IsNullOrWhiteSpace(errors) Then
            Console.WriteLine("[STDERR] " & errors)
        End If
    End If
End Sub

Just translate the string into something my code will understand and excite via PowerShell.

I have been at this for a month so any support would be welcome


Solution

  • You'll need to run your app with administrative privileges. It's not necessary to use PowerShell with System.Diagnostics.Process. Also, some of the arguments you've specified are for msiexec not for Click-To-Run. Try the following:

    Add Application Manifest File:

    Open Solution Explorer:

    Modify requestedExecutionLevel:

    Change From:

    <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
    

    Change To:

    <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
    

    Add a class to your project (name: UninstallInfo.vb)

    Public Class UninstallInfo
        Public Property Arguments As String = String.Empty
        Public Property Filename As String = String.Empty
        Public Property InstallFolder As String = String.Empty
        Public Property Installer As String = String.Empty
    End Class
    

    Add a module to your project (name: Module1.vb)

    Imports System.Text
    Imports Microsoft.Win32
    
    Module Module1
    
        Private officeProductIds As String() = {
        "O365ProPlusEEANoTeamsRetail",
        "O365ProPlusRetail",
        "O365BusinessEEANoTeamsRetail",
        "O365BusinessRetail"
        }
    
        Private Function GetOffice365UninstallInfo() As List(Of UninstallInfo)
    
            Dim uninstallInfos As List(Of UninstallInfo) = New List(Of UninstallInfo)()
    
            Dim uninstallPaths As String() = {
                "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
                "SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
            }
    
            Try
                Dim baseKey As RegistryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)
    
                For Each regPath In uninstallPaths
                    Using uninstallKey As RegistryKey = baseKey.OpenSubKey(regPath)
                        If uninstallKey Is Nothing Then Continue For
    
                        For Each subKeyName As String In uninstallKey.GetSubKeyNames()
                            'Debug.WriteLine($"subkeyName: {subKeyName}")
    
                            If officeProductIds.Any(Function(id) subKeyName.Contains(id)) Then
                                Using skey As RegistryKey = uninstallKey.OpenSubKey(subKeyName, False)
    
                                    If skey IsNot Nothing Then
                                        Dim uninstallString As String = skey.GetValue("UninstallString", String.Empty)?.ToString()
    
                                        If Not String.IsNullOrEmpty(uninstallString) Then
                                            Debug.WriteLine($"[FOUND] Uninstall string for {subKeyName}: {uninstallString}")
    
                                            Dim installerPath As String = ""
                                            Dim arguments As String = ""
    
                                            If uninstallString.StartsWith(ChrW(34)) Then
                                                Dim endQuoteIndex As Integer = uninstallString.IndexOf(ChrW(34), 1)
                                                installerPath = uninstallString.Substring(1, endQuoteIndex - 1)
                                                arguments = uninstallString.Substring(endQuoteIndex + 1).Trim()
                                            Else
                                                Dim firstSpace = uninstallString.IndexOf(" "c)
                                                If firstSpace > 0 Then
                                                    installerPath = uninstallString.Substring(0, firstSpace)
                                                    arguments = uninstallString.Substring(firstSpace + 1).Trim()
                                                Else
                                                    installerPath = uninstallString
                                                End If
                                            End If
                                            Dim installerFolder As String = IO.Path.GetDirectoryName(installerPath)
                                            Debug.WriteLine("")
                                            Debug.WriteLine($"Installer: {System.IO.Path.GetFileName(installerPath)}")
                                            Debug.WriteLine($"Install Folder: {installerFolder}")
                                            Debug.WriteLine($"Arguments: {arguments}")
                                            Debug.WriteLine("")
    
                                            'add
                                            uninstallInfos.Add(New UninstallInfo() With {.Arguments = arguments, .Filename = installerPath, .Installer = System.IO.Path.GetFileName(installerPath), .InstallFolder = installerFolder})
                                        Else
                                            Debug.WriteLine($"[WARNING] No UninstallString for {subKeyName}")
                                        End If
                                    End If
                                End Using
                            End If
                        Next
                    End Using
                Next
            Catch ex As Exception
                Debug.WriteLine("[EXCEPTION] " & ex.Message)
            End Try
    
            Return uninstallInfos
        End Function
    
        Public Function UninstallOffice365() As List(Of String)
            Dim results As List(Of String) = New List(Of String)()
    
            'get Office 365 uinstall info
            Dim installations As List(Of UninstallInfo) = GetOffice365UninstallInfo()
    
            If installations.Count = 0 Then
                'add
                results.Add("Info: No matching Office 365 installations found.")
            Else
                For Each installation In installations
                    Dim result As String = RunProcess(installation.Filename, installation.Arguments, installation.InstallFolder, True)
                    Debug.WriteLine($"result: {Environment.NewLine}{result}{Environment.NewLine}")
    
                    'add
                    results.Add(result)
                Next
            End If
    
            Return results
        End Function
    
        Private Function RunProcess(filename As String, arguments As String, workingDirectory As String, Optional silent As Boolean = False) As String
            'create new instance and set properties
            Dim startInfo As ProcessStartInfo = New ProcessStartInfo(filename) With {.CreateNoWindow = True, .RedirectStandardError = True, .RedirectStandardOutput = True, .UseShellExecute = False, .Verb = "runas", .WindowStyle = ProcessWindowStyle.Hidden, .WorkingDirectory = workingDirectory}
    
            If silent AndAlso Not arguments.Contains("DisplayLevel=False") Then
                arguments &= " DisplayLevel=False"
            End If
    
            Debug.WriteLine($"arguments: {arguments}")
    
            If Not String.IsNullOrEmpty(arguments) Then
                startInfo.Arguments = arguments
            End If
    
            Dim outputSB As StringBuilder = New StringBuilder()
    
            Using p As Process = New Process() With {.EnableRaisingEvents = True, .StartInfo = startInfo}
    
                AddHandler p.ErrorDataReceived, Sub(sender As Object, e As DataReceivedEventArgs)
                                                    If Not String.IsNullOrWhiteSpace(e.Data) Then
                                                        'ToDo: add desired code
                                                        Debug.WriteLine("error: " & e.Data)
                                                        outputSB.AppendFormat($"Error: {e.Data}{Environment.NewLine}")
                                                    End If
                                                End Sub
    
                AddHandler p.OutputDataReceived, Sub(sender As Object, e As DataReceivedEventArgs)
                                                     If Not String.IsNullOrWhiteSpace(e.Data) Then
                                                         'ToDo: add desired code
                                                         Debug.WriteLine("output: " & e.Data)
                                                         outputSB.AppendFormat($"{e.Data}{Environment.NewLine}")
                                                     End If
                                                 End Sub
    
                p.Start()
    
                p.BeginErrorReadLine()
                p.BeginOutputReadLine()
    
                'wait for exit
                p.WaitForExit()
    
            End Using
    
            Return outputSB.ToString()
        End Function
    End Module
    

    Note: While it's good practice to capture StandardError and StandardOutput, since the uninstaller is graphical, and DisplayLevel=False is specified it's unlikely there will be any StandardError or StandardOutput data. You may consider executing GetOffice365UninstallInfo() again after the uinstall to ensure that the Office 365 versions that you specified to uninstall have been uninstalled.

    Usage:

    Dim results As List(Of String) = UninstallOffice365()
    
    For Each result In results
        Debug.WriteLine($"{result}{Environment.NewLine}")
    Next
    

    Also see Script To Silently Uninstall Built-In Office 365 ClickToRun and this post.