В процессе разработки очень часто возникает необходимость запустить из powershell скрипта консольное приложение. Что может быть проще?
#test.ps1
& $PSScriptRootConsoleApp.exe
Изучим поведение консольных приложений при запуске их из командной строки, через PowerShell и через PowerShell ISE:
В PowerShell ISE возникла проблема с кодировкой, так как ISE ожидает вывод в кодировке 1251. Воспользуемся гуглом и найдем два решения проблемы: c использованием [Console]::OutputEncoding и через powershell pipeline. Воспользуемся первым решением:
$ErrorActionPreference = "Stop"
function RunConsole($scriptBlock)
{
$encoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
try
{
&$scriptBlock
}
finally
{
[Console]::OutputEncoding = $encoding
}
}
RunConsole {
& $PSScriptRootConsoleApp1.exe
}
В командной строке все хорошо, а вот в ISE ошибка. Exception setting «OutputEncoding»: «The handle is invalid.». Снова берем в руки гугл, и в первом же результате находим решение — надо запустить какое-нибудь консольное приложение для создания консоли. Ну что-же. попробуем.
$ErrorActionPreference = "Stop"
function RunConsole($scriptBlock)
{
# Популярное решение "устранения" ошибки: Exception setting "OutputEncoding": "The handle is invalid."
& cmd /c ver | Out-Null
$encoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
try
{
&$scriptBlock
}
finally
{
[Console]::OutputEncoding = $encoding
}
}
RunConsole {
& $PSScriptRootConsoleApp1.exe
}
Все красиво, все работает. Кто читал мою прошлую заметку, обратил внимание, что WinRM приносит нам много острых впечатлений. Попробуем запустить тест через WinRM. Для запуска воспользуемся вот таким скриптом:
param($script)
$ErrorActionPreference = "Stop"
$s = New-PSSession "."
try
{
$path = "$PSScriptRoot$script"
Invoke-Command -Session $s -ScriptBlock { &$using:path }
}
finally
{
Remove-PSSession -Session $s
}
Что-то пошло не так. Решение с созданием консоли не работает. Ранее мы находили два решения проблемы кодировки. Попробуем второй:
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"
function RunConsole($scriptBlock)
{
function ConvertTo-Encoding ([string]$From, [string]$To)
{
Begin
{
$encFrom = [System.Text.Encoding]::GetEncoding($from)
$encTo = [System.Text.Encoding]::GetEncoding($to)
}
Process
{
$bytes = $encTo.GetBytes($_)
$bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
$encTo.GetString($bytes)
}
}
Write-Verbose "RunConsole: Pipline mode"
&$scriptBlock | ConvertTo-Encoding cp866 windows-1251
}
RunConsole {
& $PSScriptRootConsoleApp1.exe
}
В ISE и через WinRM решение работает, а вот через командную строку и shell — нет.
Надо объединить эти два способа и проблема будет решена!
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"
function RunConsole($scriptBlock)
{
if([Environment]::UserInteractive)
{
# Популярное решение "устранения" ошибки: Exception setting "OutputEncoding": "The handle is invalid."
& cmd /c ver | Out-Null
$encoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
try
{
Write-Verbose "RunConsole: Console.OutputEncoding mode"
&$scriptBlock
return
}
finally
{
[Console]::OutputEncoding = $encoding
}
}
function ConvertTo-Encoding ([string]$From, [string]$To)
{
Begin
{
$encFrom = [System.Text.Encoding]::GetEncoding($from)
$encTo = [System.Text.Encoding]::GetEncoding($to)
}
Process
{
$bytes = $encTo.GetBytes($_)
$bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
$encTo.GetString($bytes)
}
}
Write-Verbose "RunConsole: Pipline mode"
&$scriptBlock | ConvertTo-Encoding cp866 windows-1251
}
RunConsole {
& $PSScriptRootConsoleApp1.exe
}
Кажется, что проблема решена, но продолжим исследование и усложним наше консольное приложение, добавив в него вывод в stdError.
Становится все веселее :) В ISE исполнение скрипта прервалось на середине, а через WinRM мало того, что прервалось, так еще сообщение из stdErr прочитать невозможно. Первым шагом решим проблему с остановкой запускаемого из скрипта приложения, для этого перед запуском приложения изменим значение глобальной переменной $ErrorActionPreference.
$ErrorActionPreference = "Stop"
#$VerbosePreference = "Continue"
function RunConsole($scriptBlock)
{
if([Environment]::UserInteractive)
{
# Популярное решение "устранения" ошибки: Exception setting "OutputEncoding": "The handle is invalid."
& cmd /c ver | Out-Null
$encoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
try
{
Write-Verbose "RunConsole: Console.OutputEncoding mode"
$prevErrAction = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try
{
&$scriptBlock
return
}
finally
{
$ErrorActionPreference = $prevErrAction
}
}
finally
{
[Console]::OutputEncoding = $encoding
}
}
function ConvertTo-Encoding ([string]$From, [string]$To)
{
Begin
{
$encFrom = [System.Text.Encoding]::GetEncoding($from)
$encTo = [System.Text.Encoding]::GetEncoding($to)
}
Process
{
$bytes = $encTo.GetBytes($_)
$bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
$encTo.GetString($bytes)
}
}
Write-Verbose "RunConsole: Pipline mode"
$prevErrAction = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try
{
&$scriptBlock | ConvertTo-Encoding cp866 windows-1251
return
}
finally
{
$ErrorActionPreference = $prevErrAction
}
}
RunConsole {
& $PSScriptRootConsoleApp2.exe
}
Write-Host "ExitCode = $LASTEXITCODE"
echo error message 1>&2
errorActionTest.ps1
#error.cmd
#echo error message 1>&2
#errorActionTest.ps1
$ErrorActionPreference = "Stop"
Write-Host "before"
Invoke-Expression -ErrorAction SilentlyContinue -Command $PSScriptRooterror.cmd
Write-Host "after"
Какой будет результат выполнения такого скрипта?
Вторым шагом доработаем скрипт удаленного запуска через WinRM, чтобы он не падал
param($script)
$ErrorActionPreference = "Stop"
$s = New-PSSession "."
try
{
$path = "$PSScriptRoot$script"
$err = @()
$r = Invoke-Command -Session $s -ErrorAction Continue -ErrorVariable err -ScriptBlock `
{
$ErrorActionPreference = "Stop"
& $using:path | Out-Host
return $true
}
if($r -ne $true)
{
Write-Error "The remote script was completed with an error"
}
if($err.length -ne 0)
{
Write-Warning "Error occurred on remote host"
}
}
finally
{
Remove-PSSession -Session $s
}
И осталось самое сложное — скорректировать сообщение формируемое через stdErr и при этом не изменить его положение в логе. В процессе решения этой задачи коллеги предложили самостоятельно создать консоль, воспользовавшись win api функцией AllocConsole.
$ErrorActionPreference = "Stop"
#$VerbosePreference = "continue"
$consoleAllocated = [Environment]::UserInteractive
function AllocConsole()
{
if($Global:consoleAllocated)
{
return
}
$a = @'
[DllImport("kernel32", SetLastError = true)]
public static extern bool AllocConsole();
'@
$params = New-Object CodeDom.Compiler.CompilerParameters
$params.MainClass = "methods"
$params.GenerateInMemory = $true
$params.CompilerOptions = "/unsafe"
$r = Add-Type -MemberDefinition $a -Name methods -Namespace kernel32 -PassThru -CompilerParameters $params
Write-Verbose "Allocating console"
[kernel32.methods]::AllocConsole() | Out-Null
Write-Verbose "Console allocated"
$Global:consoleAllocated = $true
}
function RunConsole($scriptBlock)
{
AllocConsole
$encoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
$prevErrAction = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try
{
& $scriptBlock
}
finally
{
$ErrorActionPreference = $prevErrAction
[Console]::OutputEncoding = $encoding
}
}
RunConsole {
& $PSScriptRootConsoleApp2.exe
}
Write-Host "ExitCode = $LASTEXITCODE"
Избавится от информации, которую добавляет powershell к stdErr мне так и не удалось.
Надеюсь, что эта информация окажется полезной не только мне! :)
Автор: kuda78