redirect stdout, stderr from powershell script as admin through start-process redirect stdout, stderr from powershell script as admin through start-process powershell powershell

redirect stdout, stderr from powershell script as admin through start-process


PowerShell's Start-Process cmdlet:

  • does have -RedirectStandardOut and -RedirectStandardError parameters,
  • but syntactically they cannot be combined with -Verb Runas, the argument required to start a process elevated (with administrative privileges).

This constraint is also reflected in the underlying .NET API, where setting the .UseShellExecute property on a System.Diagnostics.ProcessStartInfo instance to true - the prerequisite for being able to use .Verb = "RunAs" in order to run elevated - means that you cannot use the .RedirectStandardOutput and .RedirectStandardError properties.

Overall, this suggests that you cannot directly capture an elevated process' output streams from a non-elevated process.

A pure PowerShell workaround is not trivial:

param([string] $arg='help')if ($arg -in 'start', 'stop') {  if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {    # Invoke the script via -Command rather than -File, so that     # a redirection can be specified.    $passThruArgs = '-command', '&', 'servicemssql.ps1', $arg, '*>', "`"$PSScriptRoot\out.txt`""    Start-Process powershell -Wait -Verb RunAs -ArgumentList $passThruArgs    # Retrieve the captured output streams here:    Get-Content "$PSScriptRoot\out.txt"    exit  }}# ...
  • Instead of -File, -Command is used to invoke the script, because that allows appending a redirection to the command: *> redirects all output streams.

    • @soleil suggests using Tee-Object as an alternative so that the output produced by the elevated process is not only captured, but also printed to the (invariably new window's) console as it is being produced:
      ..., $arg, '|', 'Tee-Object', '-FilePath', "`"$PSScriptRoot\out.txt`""

    • Caveat: While it doesn't make a difference in this simple case, it's important to know that arguments are parsed differently between -File and -Command modes; in a nutshell, with -File, the arguments following the script name are treated as literals, whereas the arguments following -Command form a command that is evaluated according to normal PowerShell rules in the target session, which has implications for escaping, for instance; notably, values with embedded spaces must be surrounded with quotes as part of the value.

  • The $PSScriptRoot\ path component in output-capture file $PSScriptRoot\out.txt ensures that the file is created in the same folder as the calling script (elevated processes default to $env:SystemRoot\System32 as the working dir.)

    • Similarly, this means that script file servicemssql.ps1, if it is invoked without a path component, must be in one of the directories listed in $env:PATH in order for the elevated PowerShell instance to find it; otherwise, a full path is also required, such as $PSScriptRoot\servicemssql.ps1.
  • -Wait ensures that control doesn't return until the elevated process has exited, at which point file $PSScriptRoot\out.txt can be examined.


As for the follow-up question:

To go even further, could we have a way to have the admin shell running non visible, and read the file as we go with the Unix equivalent of tail -f from the non -privileged shell ?

It is possible to run the elevated process itself invisibly, but note that you'll still get the UAC confirmation prompt. (If you were to turn UAC off (not recommended), you could use Start-Process -NoNewWindow to run the process in the same window.)

To also monitor output as it is being produced, tail -f-style, a PowerShell-only solution is both nontrivial and not the most efficient; to wit:

param([string]$arg='help')if ($arg -in 'start', 'stop') {  if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {    # Delete any old capture file.    $captureFile = "$PSScriptRoot\out.txt"    Remove-Item -ErrorAction Ignore $captureFile    # Start the elevated process *hidden and asynchronously*, passing    # a [System.Diagnostics.Process] instance representing the new process out, which can be used    # to monitor the process    $passThruArgs = '-noprofile', '-command', '&',  "servicemssql.ps1", $arg, '*>', $captureFile    $ps = Start-Process powershell -WindowStyle Hidden -PassThru  -Verb RunAs -ArgumentList $passThruArgs    # Wait for the capture file to appear, so we can start    # "tailing" it.    While (-not $ps.HasExited -and -not (Test-Path -LiteralPath $captureFile)) {      Start-Sleep -Milliseconds 100      }    # Start an aux. background that removes the capture file when the elevated    # process exits. This will make Get-Content -Wait below stop waiting.    $jb = Start-Job {       # Wait for the process to exit.      # Note: $using:ps cannot be used directly, because, due to      #       serialization/deserialization, it is not a live object.      $ps = (Get-Process -Id $using:ps.Id)      while (-not $ps.HasExited) { Start-Sleep -Milliseconds 100 }      # Get-Content -Wait only checks once every second, so we must make      # sure that it has seen the latest content before we delete the file.      Start-Sleep -Milliseconds 1100       # Delete the file, which will make Get-Content -Wait exit (with an error).      Remove-Item -LiteralPath $using:captureFile     }    # Output the content of $captureFile and wait for new content to appear    # (-Wait), similar to tail -f.    # `-OutVariable capturedLines` collects all output in    # variable $capturedLines for later inspection.    Get-Content -ErrorAction SilentlyContinue -Wait -OutVariable capturedLines -LiteralPath $captureFile    Remove-Job -Force $jb  # Remove the aux. job    Write-Verbose -Verbose "$($capturedLines.Count) line(s) captured."    exit  }}# ...