перенаправить stdout, stderr из сценария powershell в качестве администратора через start-process - PullRequest
0 голосов
/ 08 июня 2018

Внутри сценария powershell я запускаю команду, которая запускает новый powershell от имени администратора (если это не так и при необходимости, в зависимости от $arg), а затем запускает сценарий.

Iя пытаюсь перенаправить stdout и stderr на первый терминал.

Не пытаясь упростить ситуацию, есть и аргументы.

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

Использование следующих ответов на SO не работает:

Как бороться с двойной кавычкой внутри двойной кавычки при сохранении расширения переменной ($arg)?

Ответы [ 2 ]

0 голосов
/ 09 июня 2018

Командлет PowerShell Start-Process:

  • имеет параметры -RedirectStandardOut и -RedirectStandardError,
  • , но синтаксически их нельзя объединить с -Verb Runas аргумент, необходимый для запуска процесса повышенный (с правами администратора).

Это ограничение также отражено в базовом .NET API, где задается свойство .UseShellExecuteдля экземпляров System.Diagnostics.ProcessStartInfo до true - предварительное условие для возможности использования .Verb = "RunAs" для запуска с повышенными правами - означает, что вы не можете использовать свойства .RedirectStandardOutput и .RedirectStandardError.

В целом, это говорит о том, что вы не можете напрямую захватывать выходные потоки процесса с повышенными правами из процесса без прав доступа.

A Чистое решение PowerShell не является тривиальным:

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

# ...
  • Вместо -File, -Command используется для вызова сценария, поскольку это позволяет добавить перенаправление к команде: *> перенаправляет все выходные потоки.

    • @ soleil сам предлагает использовать Tee-Object в качестве альтернативы, чтобы вывод, произведенный повышенным процессом, не только захватывался, но и печатался вконсоль (неизменно нового окна) в том виде, в каком она создается:
      ..., $arg, '|', 'Tee-Object', '-FilePath', "$PSScriptRoot\out.txt".

    • Предупреждение: хотя в этом простом случае это не имеет значения, важнознать, что аргументы по-разному анализируются между режимами -File и -Command;в двух словах, с -File аргументы, следующие за именем скрипта, обрабатываются как литералы , тогда как аргументы после -Command образуют команду, которая оценивается в соответствии с обычными правилами PowerShell в целевом сеансе, которыйнапример, имеет значение для побега;в частности, значения со встроенными пробелами должны быть заключены в кавычки как часть значения.

  • Компонент пути $PSScriptRoot\ в файле захвата вывода $PSScriptRoot\out.txt обеспечиваетчто файл создается в той же папке, что и вызывающий скрипт (повышенные процессы по умолчанию $env:SystemRoot\System32 в качестве рабочего каталога.)

    • Аналогично, это означает, что файл скрипта servicemssql.ps1, если онвызывается без компонента пути, должен находиться в одном из каталогов, перечисленных в $env:PATH, чтобы экземпляр PowerShell с повышенными правами мог его найти;в противном случае также требуется полный путь, такой как $PSScriptRoot\servicemssql.ps1.
  • -Wait гарантирует, что управление не вернется, пока не завершится процесс с повышенными правами, и в этот моментфайл $PSScriptRoot\out.txt можно просмотреть.


Что касается следующего вопроса:

Если пойти еще дальше, могли бы мы найти способ?чтобы оболочка администратора работала невидимой и читала файл по ходу использования Unix, эквивалентного tail -f из непривилегированной оболочки?

Можно самому запускать процесс с повышенными правами незаметно, но учтите, что вы все равно получите запрос подтверждения UAC.(Если бы вы отключили UAC (не рекомендуется), вы могли бы использовать Start-Process -NoNewWindow для запуска процесса в том же окне.)

Чтобы также контролировать вывод в процессе его производства, tail -f -style , решение только для PowerShell является нетривиальным и не самым эффективным;для остроумия:

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

# ...
0 голосов
/ 08 июня 2018

Механизм PowerShell работает в .NET, поэтому все, что вы делаете в C #, вы можете делать в PowerShell (в основном).

$noArgs = @{
    TypeName     = 'System.Diagnostics.ProcessStartInfo'
    ArgumentList = @(
        "$PSHOME\powershell.exe"
        "-File ""$PSScriptRoot\servicemssql.ps1"" $arg"
    )
    Property     = @{
        Verb                   = 'RunAs'
        RedirectStandardOutput = $true
        RedirectStandardError  = $true
        UseShellExecute        = $false
        WindowStyle            = [System.Diagnostics.ProcessWindowStyle]::Hidden
        WorkingDirectory       = $PSScriptRoot
    }
}
$pInfo = New-Object @noArgs
$process = [System.Diagnostics.Process]::Start($pInfo)

Отсюда вам нужно читать потоки вывода и ошибок из $process.

$process.WaitForExit()
$process.StandardOutput.ReadToEnd()
$process.StandardError.ReadToEnd()

Вот хороший ответ на выполнение этой части при необходимости асинхронно .

Редактировать: со встроенным асинхронным чтением и скрытым окном:

$action = {
    if (-not [string]::IsNullOrEmpty($EventArgs.Data)) {
        $Event.MessageData.AppendLine($EventArgs.Data)
    }
}

$stdOutBuilder = [System.Text.StringBuilder]::new()
$stdOutEvent = $pInfo | Register-ObjectEvent -EventName OutputDataReceived -Action $action -MessageData $stdOutBuilder

$stdErrBuilder = [System.Text.StringBuilder]::new()
$stdErrEvent = $pInfo | Register-ObjectEvent -EventName ErrorDataReceived -Action $action -MessageData $stdErrBuilder

$process = [System.Diagnostics.Process]::Start($pInfo)
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()

do { Start-Sleep -Milliseconds 500 } until ($process.HasExited)

Unregister-Event -SourceIdentifier $stdOutEvent.Name
Unregister-Event -SourceIdentifier $stdErrEvent.Name
$stdOutBuilder.ToString().Trim()
$stdErrBuilder.ToString().Trim()

Документация:

...