Troubleshooting PsExec Hang After PowerShell Script Execution: Service Startup Case Study


2 views

When automating service management across Windows servers using PsExec and PowerShell, many administrators encounter a peculiar behavior where the remote session hangs despite successful script execution. The core symptom manifests as:

psexec \\target -u domain\user -p pass powershell c:\scripts\start-services.ps1
# Hangs until manual ENTER key press
# Then shows: "powershell exited on target with error code 0"

Standard troubleshooting like adding exit 0 to PowerShell scripts often fails because the issue stems from how PsExec handles I/O streams. The Windows service control commands (Start-Service, Set-Service) create hidden console interactions that PsExec waits to resolve.

For TeamCity and other automation systems, these approaches consistently work:

# Solution 1: Force PowerShell console detachment
psexec \\target -u user -p pass cmd /c "echo . | powershell -NonInteractive -File c:\scripts\start-services.ps1"

# Solution 2: Redirect all streams
psexec \\target -u user -p pass powershell -Command "&{ c:\scripts\start-services.ps1 *>&1 | Out-Null }"

# Solution 3: Service-specific workaround
$services = "Service1","Service2","Service3"
$services | ForEach-Object {
    Start-Service $_ -ErrorAction SilentlyContinue
    [System.Console]::Out.Flush()  # Critical for PsExec
}

Windows services management APIs sometimes maintain open handles to the console window. When combined with:

  • PsExec's strict handle inheritance
  • PowerShell's default interactive mode
  • TeamCity's stream monitoring

This creates the perfect storm for hanging processes. The [System.Console]::Out.Flush() approach forces handle cleanup.

For production environments with hundreds of servers:

function Invoke-RemoteServiceStart {
    param(
        [string]$ComputerName,
        [string[]]$Services,
        [pscredential]$Credential
    )
    
    $scriptBlock = {
        param($services)
        $services | ForEach-Object {
            try {
                $service = Get-Service -Name $_ -ErrorAction Stop
                if ($service.Status -ne 'Running') {
                    $service | Start-Service -ErrorAction Stop
                    [System.Console]::Out.Flush()
                }
            } catch {
                Write-Output "Failed $_ : $($_.Exception.Message)"
            }
        }
    }
    
    $encodedCommand = [Convert]::ToBase64String(
        [Text.Encoding]::Unicode.GetBytes(
            "& {$scriptBlock} -Services @('$($Services -join "','")')"
        )
    )
    
    psexec \\$ComputerName -u $Credential.UserName -p $Credential.GetNetworkCredential().Password 
        powershell -EncodedCommand $encodedCommand *> $null
}

When using PsExec to run PowerShell scripts remotely, many administrators encounter a peculiar issue where the process completes successfully but fails to terminate properly. The console hangs indefinitely until manual intervention (pressing Enter), despite the script having executed completely with exit code 0.

psexec \\\\target -u domain\\username -p password powershell c:\\path\\script.ps1

The core issue stems from how PsExec handles I/O streams and process termination in PowerShell environments. Three primary factors contribute to this behavior:

  • PowerShell's console host (conhost.exe) maintains open handles
  • PsExec's strict waiting for all child processes to terminate
  • Buffered I/O streams not being fully flushed

Here are several working approaches, each suitable for different scenarios:

1. The -NoProfile -NonInteractive Approach

psexec \\\\target -u domain\\user -p pass powershell -NoProfile -NonInteractive -File "c:\\path\\script.ps1"

2. Redirecting All Streams

psexec \\\\target -u domain\\user -p pass cmd /c "powershell -File c:\\path\\script.ps1 > output.txt 2>&1"

3. Using -Command Instead of -File

psexec \\\\target -u domain\\user -p pass powershell -Command "& { . c:\\path\\script.ps1; exit $LASTEXITCODE }"

When dealing with Windows services (as mentioned in the original script purpose), add these PowerShell parameters:

psexec \\\\target -u domain\\user -p pass powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Start-Service servicename1,servicename2; exit 0"

For automated environments like TeamCity, use this robust pattern:

psexec \\\\target -u domain\\user -p pass cmd /c "echo.| powershell -NonInteractive -NoLogo -NoProfile -ExecutionPolicy Bypass -File script.ps1 > nul 2>&1"

This solution combines several techniques:

  • cmd /c wrapper for clean termination
  • Input redirection (echo.) to prevent hangs
  • Full stream redirection
  • PowerShell execution parameters for non-interactive mode

If PsExec isn't mandatory, consider using PowerShell Remoting instead:

Invoke-Command -ComputerName target -ScriptBlock {
    Start-Service servicename1, servicename2
} -Credential $cred