6

Inside a powershell script, I'm running a command which starts a new powershell as admin (if I'm not and if needed, depending on $arg) and then runs the script.

I'm trying to redirect stdout and stderr to the first terminal.

Not trying to make things easier, there are arguments too.

param([string]$arg="help")

if($arg -eq "start" -Or $arg -eq "stop")
{
    if(![bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))
    {
        Start-Process powershell -Verb runas -ArgumentList " -file servicemssql.ps1 $arg"
        exit
    }
}

$Services = "MSSQLSERVER", "SQLSERVERAGENT", "MSSQLServerOLAPService", "SSASTELEMETRY", "SQLBrowser", `
"SQLTELEMETRY", "MSSQLLaunchpad", "SQLWriter", "MSSQLFDLauncher"

function startsql {
    "starting SQL services"
    Foreach ($s in $Services) {
        "starting $s"
        Start-Service -Name "$s"
    }
}

function stopsql {
    "stopping SQL services"
    Foreach ($s in $Services) {
        "stopping $s"
        Stop-Service -Force -Name "$s"
    }
}

function statussql {
    "getting SQL services status"
    Foreach ($s in $Services) {
        Get-Service -Name "$s"
    }
}

function help {
    "usage: StartMssql [status|start|stop]"
}

Switch ($arg) {
    "start" { startsql }
    "stop" { stopsql }
    "status" { statussql }
    "help" { help }
    "h" { help }
}

Using the following answers on SO doesn't work:

How to deal with the double quote inside double quote while preserving the variable ($arg) expansion ?

Soleil
  • 6,404
  • 5
  • 41
  • 61
  • 1
    Are you asking how to set up a privileged runspace you can connect to? Or are you asking how to start a privileged powershell process and somehow manipulate the stdin/stdout for that process? You can't redirect the output of a privileged process (`-Verb Runas`) using `Start-Process` – Maximilian Burszley Jun 08 '18 at 19:31
  • I want to redirect the output of a privileged process to a lower privileged indeed. With any strategy. Why can't we with Start-Process ? If then, how to achieve this ? – Soleil Jun 08 '18 at 19:52
  • It *appears* that you can [redirect outputs if you use the `ProcessInfo`](https://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo(v=vs.110).aspx) class alongside `Process.Start` – Maximilian Burszley Jun 08 '18 at 19:54
  • Sounds good, but that's c#. Can't I do this in pure powershell ? – Soleil Jun 08 '18 at 19:58

1 Answers1

14

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
  }
}

# ...
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Microsoft are seriously lacking on their documentation skills then. Runas can't be used with many options it seems, but it isn't documented as such, so we're just led to believe everything works with it. – Studocwho Jun 13 '23 at 02:57
  • @Studocwho, I agree that it would be beneficial to document mutually exclusive parameters more explicitly; such exclusions are, however, _implied_ by the [syntax diagrams](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Command_Syntax) at the start of the topic. I encourage you to ask for the topic to be amended at https://github.com/MicrosoftDocs/PowerShell-Docs/issues/new/choose – mklement0 Jun 13 '23 at 03:04