在powershell脚本中,我正在运行一个命令,以管理员身份启动一个新的PowerShell(如果我不是,如果需要,取决于$arg
),然后运行脚本。
我正在尝试将stdout和stderr重定向到第一个终端。
不试图让事情变得更容易,也有争论。
param([string]$arg="help")
if($arg -eq "start" -Or $arg -eq "stop")
{
if().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
)扩展的同时处理双引号内的双引号?
答案 0 :(得分:4)
PowerShell的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
,因为它允许向命令添加重定向:*>
重定向所有输出流。
@soleil本人建议使用Tee-Object
作为替代方案,以便提升过程产生的输出不仅被捕获,而且还会在生成时打印到(总是新窗口)控制台:
..., $arg, '|', 'Tee-Object', '-FilePath', "$PSScriptRoot\out.txt"
。
警告:虽然在这个简单的案例中没有什么区别,但重要的是要知道在-File
和-Command
模式之间对参数的解析是不同的。简而言之,使用-File
,脚本名称后面的参数被视为 literals ,而-Command
后面的参数形成一个根据正常PowerShell规则评估的命令。例如,目标会话对逃避有影响;值得注意的是,嵌入空格的值必须用引号括起来作为值的一部分。
输出捕获文件$PSScriptRoot\
中的$PSScriptRoot\out.txt
路径组件可确保在与调用脚本相同的文件夹中创建文件(提升的进程默认为$env:SystemRoot\System32
作为工作目录。)
servicemssql.ps1
(如果在没有路径组件的情况下调用)必须位于$env:PATH
中列出的某个目录中,以便提升的PowerShell实例可以找到它;否则,还需要完整路径,例如$PSScriptRoot\servicemssql.ps1
。 -Wait
确保在提升的流程退出之前不会返回控制权,此时可以检查文件$PSScriptRoot\out.txt
。
关于后续问题:
为了更进一步,我们是否可以让管理shell运行不可见,并在我们使用来自非特权shell的等效于
tail -f
的Unix时读取文件?
可以无形地运行提升的进程本身,但请注意,您仍然会收到UAC确认提示。 (如果您要关闭UAC(不推荐),您可以使用Start-Process -NoNewWindow
在同一窗口中运行该过程。)
要监视正在生成的输出tail -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
}
}
# ...
答案 1 :(得分:-1)
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()
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()
文档: