PowerShell 的Start-Process cmdlet:
- 确实有
-RedirectStandardOut 和-RedirectStandardError 参数,
- 但语法它们不能与
-Verb Runas结合使用,这是启动进程所需的参数提升(具有管理权限)。
此约束也反映在底层 .NET API 中,其中将 System.Diagnostics.ProcessStartInfo 实例上的 .UseShellExecute 属性设置为 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 用于调用脚本,因为这允许将重定向附加到命令:*> 重定向所有输出流。
-
@soleil 建议使用Tee-Object 作为替代方案,这样提升的进程产生的输出不仅会被捕获,而且还会在生成时打印到(总是新窗口的)控制台:
..., $arg, '|', 'Tee-Object', '-FilePath', "`"$PSScriptRoot\out.txt`""
-
警告:虽然在这个简单的情况下并没有什么不同,但重要的是要知道-File 和-Command 模式之间的参数解析方式不同;简而言之,使用-File,脚本名称后面的参数被视为文字,而-Command 后面的参数形成一个根据目标会话中的正常PowerShell规则评估的命令,例如,这对逃跑有影响;值得注意的是,嵌入空格的值必须用引号括起来作为值的一部分。
-
输出捕获文件$PSScriptRoot\out.txt 中的$PSScriptRoot\ 路径组件确保文件创建在与调用脚本相同的文件夹中(提升的进程默认使用$env:SystemRoot\System32 作为工作目录。)
- 同样,这意味着脚本文件
servicemssql.ps1,如果在没有路径组件的情况下调用它,则必须位于$env:PATH 中列出的目录之一中,以便提升的PowerShell 实例找到它;否则,还需要完整路径,例如$PSScriptRoot\servicemssql.ps1。
-
-Wait 确保在提升的进程退出之前控制不会返回,此时可以检查文件 $PSScriptRoot\out.txt。
关于后续问题:
更进一步,我们是否有办法让管理 shell 运行不可见,并在使用 Unix 等效的 tail -f 从非特权 shell 中读取文件?
可以不可见地运行提升的进程本身,但请注意,您仍会收到 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
}
}
# ...