如何从PowerShell跨用户执行函数获取.NET标准输出与错误输出
问题分析与解决方案
你遇到的核心问题是,通过System.Diagnostics.Process启动的子PowerShell进程,输出和错误没有正确捕获,加上参数传递方式存在漏洞,导致父脚本无法获取子进程的执行结果(成功/失败及原因)。我来一步步帮你解决:
现有代码的关键问题
- 输出读取顺序错误:先调用
$Process.WaitForExit()再读取StandardOutput,如果子进程输出缓冲区满了,会导致子进程挂起,无法正常退出,也读不到完整输出。必须先读取输出和错误,再等待进程退出。 - 参数传递方式不安全:直接把cmdlet字符串传给
powershell.exe容易出现参数解析错误(比如路径含空格时),应该用-Command参数封装命令逻辑。 - 错误捕获不完整:只读取了标准输出,没有处理标准错误,也没有返回进程退出码,父脚本无法判断执行是否成功。
修正后的函数
下面是调整后的函数,解决了上述问题,还能返回结构化的执行结果(包括输出、错误、退出码):
Function Start-PSCMDAsUser { Param ( [Parameter(Mandatory=$true)] [ScriptBlock]$PSCommand, [Parameter(Mandatory=$true)] [System.Management.Automation.PSCredential]$PSCreds ) # 构造PowerShell命令:用-Command执行脚本块,内置错误捕获逻辑 $commandText = "-Command try { & $PSCommand; exit 0 } catch { Write-Error `$_.Exception.Message; exit 1 }" $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo $ProcessInfo.RedirectStandardError = $true $ProcessInfo.RedirectStandardOutput = $true $ProcessInfo.UseShellExecute = $false $ProcessInfo.Arguments = $commandText $ProcessInfo.Username = $PSCreds.GetNetworkCredential().UserName $ProcessInfo.Password = $PSCreds.Password $ProcessInfo.FileName = "powershell.exe" $ProcessInfo.WorkingDirectory = $PWD.Path # 继承父进程工作目录,可选 $Process = New-Object System.Diagnostics.Process $Process.StartInfo = $ProcessInfo # 先读取输出/错误,再等待进程退出(避免缓冲区满导致挂起) $Process.Start() | Out-Null $stdOut = $Process.StandardOutput.ReadToEnd() $stdErr = $Process.StandardError.ReadToEnd() $Process.WaitForExit() # 返回结构化结果,方便父脚本判断 [PSCustomObject]@{ ExitCode = $Process.ExitCode Output = $stdOut.Trim() Error = $stdErr.Trim() Success = ($Process.ExitCode -eq 0) } }
关键改进点说明
- 用ScriptBlock传递命令:避免字符串拼接带来的参数解析问题,比如路径包含空格时的错误。
- 内置错误处理:子进程中加入
try/catch,成功时退出码设为0,失败时输出错误信息并设为1,父脚本可通过退出码快速判断结果。 - 正确的读取顺序:先读取
StandardOutput和StandardError,再调用WaitForExit(),彻底解决缓冲区满导致的进程挂起问题。 - 结构化返回对象:返回包含退出码、输出、错误、成功状态的自定义对象,父脚本可以轻松判断执行结果并做后续逻辑。
调用示例(以你的FSRM测试场景为例)
# 构造测试命令的脚本块(用$using:引用父脚本变量) $testFile = "C:\TestShare\test.txt" $datetime = Get-Date -Format "yyyyMMddHHmmss" $testCommand = { New-Item -Path $using:testFile -ItemType File -Value "Tested_Share_$using:datetime" -Force } # 调用函数 $result = Start-PSCMDAsUser -PSCommand $testCommand -PSCreds $pscreds # 处理结果 if ($result.Success) { Write-Host "测试文件创建成功,输出:`n$result.Output" # 后续逻辑:比如验证文件屏幕是否未拦截(如果是测试允许场景) } else { Write-Error "测试文件创建失败,错误信息:`n$result.Error" # 比如这里可以判断是否是文件屏幕拦截导致的错误,验证配置生效 } # 也可以用try/catch风格处理 try { $result = Start-PSCMDAsUser -PSCommand $testCommand -PSCreds $pscreds if (-not $result.Success) { throw $result.Error } Write-Host "执行成功" } catch { Write-Error "执行失败:$_" # 异常处理逻辑 }
注意事项
- 确保
$PSCreds包含的用户有足够权限执行目标cmdlet(比如创建文件的权限)。 - 如果目标cmdlet需要返回复杂对象,可以在子进程中用
ConvertTo-Json输出,父脚本再用ConvertFrom-Json反序列化。 - 对于长运行的命令,可考虑异步读取输出,但大多数场景下同步读取已经足够。
内容的提问来源于stack exchange,提问作者RSchreib




