internal/functions/Invoke-Program.ps1

function Invoke-Program {
    <#
    .SYNOPSIS
        Invokes a remote execution of a file using specific credentials.
 
    .DESCRIPTION
    Based on https://github.com/adbertram/PSSqlUpdater
    Invokes a remote execution of a file passing credentials over the network to avoid a double-hop issue
    and gain privileges necessary to execute any kind of executables.
 
    First it tries to initialize a CredSSP connection by configuring both Client and Server to run CredSSP connections.
 
    If CredSSP connection fails, it falls back to a less secure PSSessionConfiguration workaround, which registers
    a temporary session configuration on a target machine (PS3.0+) and re-creates current PSSession to use remote
    configuration by default.
 
    .PARAMETER ComputerName
        Remote computer name
 
    .PARAMETER Path
        Path to the executable
 
    .PARAMETER Credential
        Credential object that will be used for authentication
 
    .PARAMETER ArgumentList
        List of arguments to pass to the executable
 
    .PARAMETER ExpandStrings
        The strings in ArgumentList and WorkingDirectory will be evaluated remotely on a target machine.
 
    .PARAMETER SuccessReturnCode
        Return codes that will be acknowledged as successful execution. Defaults to 0 (success), 3010 (restart required)
 
    .PARAMETER WorkingDirectory
        Working directory for the process
 
    .PARAMETER UsePSSessionConfiguration
        Skips the CredSSP attempts and proceeds directly to PSSessionConfiguration connections
 
    .EXAMPLE
        PS C:\> Invoke-Program -ComputerName ServerA -Credentials $cred -Path "C:\temp\setup.exe" -ArgumentList '/quiet' -WorkingDirectory 'C:'
 
        Starts "setup.exe /quiet" on ServerA under provided credentials. C:\ will be set as a working directory.
 
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject])]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,
        [DbaInstanceParameter]$ComputerName = $env:COMPUTERNAME,
        [pscredential]$Credential,
        [ValidateNotNullOrEmpty()]
        [string[]]$ArgumentList,
        [bool]$ExpandStrings = $false,
        [ValidateNotNullOrEmpty()]
        [string]$WorkingDirectory,
        [ValidateNotNullOrEmpty()]
        [uint32[]]$SuccessReturnCode = @(0, 3010),
        [switch]$Raw,
        [bool]$UsePSSessionConfiguration = (Get-DbatoolsConfigValue -Name 'psremoting.Sessions.UsePSSessionConfiguration' -Fallback $false),
        [bool]$EnableException = $EnableException
    )
    process {
        $startProcess = {
            Param  (
                $Path,
                $ArgumentList,
                $ExpandStrings,
                $WorkingDirectory,
                $SuccessReturnCode
            )
            $output = [pscustomobject]@{
                ComputerName     = $env:COMPUTERNAME
                Path             = $Path
                ArgumentList     = $ArgumentList
                WorkingDirectory = $WorkingDirectory
                Successful       = $false
                stdout           = $null
                stderr           = $null
                ExitCode         = $null
            }
            $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo
            $processStartInfo.FileName = $Path
            if ($ArgumentList) {
                $processStartInfo.Arguments = $ArgumentList
                if ($ExpandStrings) {
                    $processStartInfo.Arguments = $ExecutionContext.InvokeCommand.ExpandString($ArgumentList)
                }
            }
            if ($WorkingDirectory) {
                $processStartInfo.WorkingDirectory = $WorkingDirectory
                if ($ExpandStrings) {
                    $processStartInfo.WorkingDirectory = $ExecutionContext.InvokeCommand.ExpandString($WorkingDirectory)
                }
            }
            $processStartInfo.UseShellExecute = $false # This is critical for installs to function on core servers
            $processStartInfo.CreateNoWindow = $true
            $processStartInfo.RedirectStandardError = $true
            $processStartInfo.RedirectStandardOutput = $true
            $ps = New-Object System.Diagnostics.Process
            $ps.StartInfo = $processStartInfo
            $started = $ps.Start()
            if ($started) {
                $stdOut = $ps.StandardOutput.ReadToEnd()
                $stdErr = $ps.StandardError.ReadToEnd()
                $ps.WaitForExit()
                # assign output object values
                $output.stdout = $stdOut
                $output.stderr = $stdErr
                $output.ExitCode = $ps.ExitCode
                # Check the exit code of the process to see if it succeeded.
                if ($ps.ExitCode -in $SuccessReturnCode) {
                    $output.Successful = $true
                }
                $output
            }
        }

        $argList = @(
            $Path,
            $ArgumentList,
            $ExpandStrings,
            $WorkingDirectory,
            $SuccessReturnCode
        )

        $params = @{
            ScriptBlock  = $startProcess
            ArgumentList = $argList
            ComputerName = $ComputerName
            Credential   = $Credential
        }

        Write-Message -Level Debug -Message "Acceptable success return codes are [$($SuccessReturnCode -join ',')]"

        if (!$ComputerName.IsLocalHost) {
            if (!$Credential) {
                Stop-Function -Message "Explicit credentials are required when running agains remote hosts. Make sure to define the -Credential parameter"
                return
            }
            # Try to use CredSSP first, otherwise fall back to PSSession configurations with custom user/password
            if (!$UsePSSessionConfiguration) {
                Write-Message -Level Verbose -Message "Attempting to configure CredSSP for remote connections"
                Initialize-CredSSP -ComputerName $ComputerName -Credential $Credential -EnableException $false
                $sspSuccessful = $true
                Write-Message -Level Verbose -Message "Starting process [$Path] with arguments [$ArgumentList] on $ComputerName through CredSSP"
                try {
                    $output = Invoke-Command2 @params -Authentication CredSSP -Raw -ErrorAction Stop
                } catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
                    Write-Message -Level Warning -Message "CredSSP to $ComputerName unsuccessful, falling back to PSSession configurations | $($_.Exception.Message)"
                    $sspSuccessful = $false
                } catch {
                    Stop-Function -Message "Remote CredSSP execution failed" -ErrorRecord $_
                    return
                }
            }
            if ($UsePSSessionConfiguration -or !$sspSuccessful) {
                $configuration = Register-RemoteSessionConfiguration -Computer $ComputerName -Credential $Credential -Name dbatoolsInvokeProgram
                if ($configuration.Successful) {
                    Write-Message -Level Debug -Message "RemoteSessionConfiguration ($($configuration.Name)) was successful, using it."
                    Write-Message -Level Verbose -Message "Starting process [$Path] with arguments [$ArgumentList] on $ComputerName using PS session configuration"
                    try {
                        $output = Invoke-Command2 @params -ConfigurationName $configuration.Name -Raw -ErrorAction Stop
                    } catch {
                        Stop-Function -Message "Remote SessionConfiguration execution failed" -ErrorRecord $_
                        return
                    } finally {
                        # Unregister PSRemote configurations once completed. It's slow, but necessary - otherwise we're gonna leave unnesessary junk on a remote
                        Write-Message -Level Verbose -Message "Unregistering any leftover PSSession Configurations on $ComputerName"
                        $unreg = Unregister-RemoteSessionConfiguration -ComputerName $ComputerName -Credential $Credential -Name dbatoolsInvokeProgram
                        if (!$unreg.Successful) {
                            Stop-Function -Message "Failed to unregister PSSession Configurations on $ComputerName | $($configuration.Status)" -EnableException $false
                        }
                    }
                } else {
                    Stop-Function -Message "RemoteSession configuration unsuccessful, no valid connection options found | $($configuration.Status)"
                    return
                }
            }
        } else {
            Write-Message -Level Verbose -Message "Starting process [$Path] with arguments [$ArgumentList] locally"
            $output = Invoke-Command2 @params -Raw -ErrorAction Stop
        }
        Write-Message -Level Debug -Message "Process [$Path] returned exit code $($output.ExitCode)"
        if ($Raw) {
            if ($output.Successful) {
                return $output.stdout
            } else {
                $message = "Error running [$Path]: exited with errorcode $($output.ExitCode)`:`n$($output.StdErr)`n$($output.StdOut)"
                Stop-Function -Message "Program execution failed | $message"
            }
        } else {
            # Select * to ensure that the object is a generic object and not a de-serialized one from a remote session
            return $output | Select-Object -Property * -ExcludeProperty PSComputerName, RunspaceId | Select-DefaultView -Property ComputerName, Path, Successful, ExitCode, stdout
        }
    }
}