代码之家  ›  专栏  ›  技术社区  ›  Soleil

将stdout、stderr从powershell脚本重定向为admin,直至启动进程

  •  3
  • Soleil  · 技术社区  · 6 年前

    在一个powershell脚本中,我正在运行一个命令,该命令以admin的身份启动一个新的powershell(如果不是,并且如果需要,取决于 $arg )然后运行脚本。

    我正试图将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 回复  |  直到 6 年前
        1
  •  4
  •   mklement0    6 年前

    动力壳 Start-Process cmdlet:

    • 确实有 -RedirectStandardOut -RedirectStandardError 参数,
    • 但是 句法上 它们不能与 -Verb Runas ,启动进程所需的参数。 高架 (具有管理权限)。

    此约束也反映在底层的.NET API中,其中设置 .UseShellExecute 上的属性 System.Diagnostics.ProcessStartInfo 实例到 true -能够使用的先决条件 .Verb = "RunAs" 为了运行提升-意味着您不能使用 .RedirectStandardOutput .RedirectStandardError 性质。

    总的来说,这表明你不能 直接地 从非提升的进程捕获提升的进程的输出流。

    纯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 用于调用脚本,因为这允许将重定向附加到命令: *> 重定向所有输出流。

      • @索莱尔自己建议使用 Tee-Object 另一种方法是,在生成提升过程的输出时,不仅捕获该输出,还将其打印到(始终是新窗口的)控制台:
        ..., $arg, '|', 'Tee-Object', '-FilePath', "$PSScriptRoot\out.txt" .

      • 注意:虽然在这个简单的例子中没有什么区别,但是重要的是要知道参数在 -文件 -命令 模式;简而言之 -文件 ,脚本名后面的参数被视为 字面量 ,而下面的论点 -命令 形成一个命令,该命令根据目标会话中的常规powershell规则进行求值,例如,这对转义有影响;值得注意的是,带有嵌入空格的值必须用引号括起来作为值的一部分。

    • 这个 $PSScriptRoot\ 输出捕获文件中的路径组件 $PSScriptRoot\out.txt 确保在调用脚本所在的文件夹中创建文件(提升的进程默认为 $env:SystemRoot\System32 作为工作主管。)

      • 类似地,这意味着脚本文件 servicemssql.ps1 ,如果在没有路径组件的情况下调用它,则必须位于 $env:PATH 为了让提升的powershell实例找到它;否则,还需要完整的路径,例如 $PSScriptRoot\servicemssql.ps1 .
    • -Wait 确保在提升的进程退出之前控件不会返回,此时文件 $psscriptroot\out.txt文件 可以检查。


    关于后续问题:

    更进一步说,我们是否可以有一种方法使管理shell运行不可见,并在使用unix时读取文件 tail -f 从没有特权的外壳?

    可以在不可见的情况下运行提升的进程本身,但请注意,您仍然会得到uac确认提示。(如果要关闭UAC(不推荐),可以使用 Start-Process -NoNewWindow 在同一窗口中运行进程。)

    在生产过程中监控输出, 尾F 风格 ,仅限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
      }
    }
    
    # ...
    
        2
  •  -1
  •   Maximilian Burszley    6 年前

    PultSek引擎在.NET上运行,所以在C++中所做的任何事情,都可以在PuthSek(大多数情况下)进行。

    $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()
    

    Here's a good answer on doing that part asynchronously if needed .

    编辑:内置异步读取和隐藏窗口:

    $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()
    

    文档: