windowspowershellrabbitmqerlang

How can I capture the detached child process when uninstalling RabbitMQ in PowerShell


I am doing an un-attended uninstall of RabbitMQ using PowerShell on Windows. I retrieve and run the un-install with:

$RABBITMQ_PRODUCT_NAME = "RabbitMQ Server"
$rabbitMq = Get-ChildItem -Path HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall `
| Get-ItemProperty `
| Where-Object { $_.DisplayName -match $RABBITMQ_PRODUCT_NAME }
$rabbitUninstallProcess = Start-Process -FilePath $rabbitMq.UninstallString -ArgumentList "/S" -PassThru -ErrorAction Stop
# can't use the -Wait parameter or it hangs indefinitely, so we have to get the process, then WaitForExit.
$rabbitUninstallProcess.WaitForExit()

The problem: $rabbitUninstallProcess launches the real uninstall process then exits. So the process I start exits (with a success code) before the uninstall is complete.

I can get the 'real' uninstall process, but now I am in a race condition and the process I find is not guaranteed to be the same one created by $rabbitUninstallProcess. By inspecting the running process I was able to determine how to identify the real uninstall process.

$UNINSTALL_PROCESS_NAME = "Un_A"
$process = Get-Process -Name $UNINSTALL_PROCESS_NAME `
| Where-Object { $_.Product -match $RABBITMQ_PRODUCT_NAME }
$process.WaitForExit()

The $rabbitUninstallProcess writes nothing to StandardOutput or StandardError.

How would I get a handle to THE process created by $rabbitUninstallProcess rather than racing and guessing. Here is my full script with comments and status output (and a bonus Erlang unattended uninstall that works as I would have expected):

$RABBITMQ_PRODUCT_NAME = "RabbitMQ Server"
$ERLANG_PRODUCT_NAME = "Erlang OTP"
$UNINSTALL_PROCESS_NAME = "Un_A"
$FAILURE = 9

$rabbitMq = Get-ChildItem -Path HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall `
| Get-ItemProperty `
| Where-Object { $_.DisplayName -match $RABBITMQ_PRODUCT_NAME }

$erlang = Get-ChildItem -Path HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall `
| Get-ItemProperty `
| Where-Object { $_.DisplayName -match $ERLANG_PRODUCT_NAME }

$rabbitMQDataDir = $env:RABBITMQ_BASE
if ($rabbitMq) {
  $installedRabbitMQVersion = $rabbitMq.DisplayVersion
  Write-Host "Removing $RABBITMQ_PRODUCT_NAME v$installedRabbitMQVersion"

  # can't use the -Wait parameter or it hangs indefinitely, so we have to get the process, then WaitForExit.
  $rabbitUninstallProcess = Start-Process -FilePath $rabbitMq.UninstallString -ArgumentList "/S" -PassThru -ErrorAction Stop
  #This process only kicksoff the actual uninstall process, so wait for it to be done before looking for the uninstall process
  $rabbitUninstallProcess.WaitForExit()
  #The RabbitMQ uninstaller (from Nullsoft) process is called "Un_A".
  #The Product property is what identifies this particular uninstall as RabbitMQ.
  #Race condition, do we find the process before it finishes? Yes, but it is still a race.
  $process = Get-Process -Name $UNINSTALL_PROCESS_NAME `
  | Where-Object { $_.Product -match $RABBITMQ_PRODUCT_NAME }

  if ($process) {
    Write-Host "Waiting for uninstall process to finish"
    $process.WaitForExit()
  } else {
    #If we didn't find the uninstall process, but it was running and the uninstall finishes after the script fails, a second run will pick up where this left off
    Write-Host "Didn't find an uninstall process. Going to sleep then check if the uninstall happened. If RabbitMQ is still installed, it will fail after the re-check."
    Start-Sleep -Seconds 60
  }
  $rabbitMqStillThere = Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall `
  | Get-ItemProperty `
  | Where-Object { $_.DisplayName -match $RABBITMQ_PRODUCT_NAME }

  if ($rabbitMqStillThere) {
    Write-Host "Unable to uninstall RabbitMQ. You may have to uninstall manually."
    exit $FAILURE
  } else {
    Write-Host "$($rabbitMq.DisplayName) has been uninstalled."
  }
} else {
  Write-Host "$RABBITMQ_PRODUCT_NAME not installed moving on."
}
#need to clean the Rabbit MQ data directory.
if([string]::IsNullOrWhiteSpace($rabbitMQDataDir)){
  #if the environment variable wasn't set, use our default.
  $rabbitMQDataDir = "C:\ProgramData\RabbitMQ"
}
if (Test-Path -Path $rabbitMQDataDir -PathType Container) {
  Write-Host "Removing RabbitMQ Data Dir $rabbitMQDataDir"
  Remove-Item -Path $rabbitMQDataDir -Recurse -Force -ErrorAction Stop
}

#now remove erlang.
if ($erlang) {
  $installedErlangVersion = $erlang.DisplayVersion
  Write-Host "Removing $ERLANG_PRODUCT_NAME v$installedErlangVersion"
  #because -Wait (ing) the erlang uninstall works
  $erlangUninstallProcess = Start-Process -FilePath $erlang.UninstallString -ArgumentList "/S" -PassThru -Wait -ErrorAction Stop
  if ($erlangUninstallProcess.ExitCode -ne 0) {
    Write-Host "Error Removing Erlang "
    exit $FAILURE
  } else {
    Write-Host "$($erlang.DisplayName) has been uninstalled."
  }
} else {
  Write-Host "$ERLANG_PRODUCT_NAME not installed moving on"
}

What I expected:

What I found:

I have been unable to deterministically find the child/descendant process started by the Start-Process.


Solution

  • Start-Process ... -Wait behaves differently from (Start-Process ... -PassThru).WaitForExit() and Start-Process ... -PassThru | Wait-Process (see GitHub issue #15555 for details):

    Your symptom - an indefinite hang with Start-Process ... -Wait - suggests that the directly launched uninstaller (bootstrapping) process not only launches additional processes, but that at least one of them seemingly doesn't terminate even after the actual uninstallation process has.


    Workaround:


    The following self-contained, simplified example demonstrates this technique:

    # Launch the uninstaller and obtain and wait for only the immediately
    # launched process, using .WaitForExit()
    ($launcherProcess = 
      Start-Process powershell.exe '-c Start-Process cmd ''/c pause''' -PassThru
    ).WaitForExit()
    
    # Using the launcher process' PID, use CIM  to query all processes whose 
    # .ParentProcess ID property contains this PID, which returns all
    # (still-running) immediate child processes.
    $launcherProcessChildren = 
      Get-CimInstance Win32_Process -Filter "ParentProcessId = $($launcherProcess.Id)"
    
    # Among the child processes, look for the one of interest via its
    # .Product property
    $trueUninstallerProcess = 
      Get-Process -Id $launcherProcessChildren.ProcessId |
      Where-Object Product -eq 'Microsoft® Windows® Operating System'
    
    # Wait for its termination, then report the exit code.
    $null = $trueUninstallerProcess.Handle # !! Needed to ensure .ExitCode can 
                                           # !! be accessed later - see below.
    $trueUninstallerProcess.WaitForExit()
    
    Write-Verbose -Verbose "Uninstaller process exited with code $($trueUninstallerProcess.ExitCode)."
    

    Note:


    [1] See GitHub PR #20749 for background information, in the context of automatically providing caching of the process handle, behind the scenes, for processes launched directly by PowerShell via Start-Process.