PowerShell: за гранью
Какие бы хвалебные оды не пелись в адрес PowerShell, всегда найдется тот, кто подбросит дегтя в боченок с медом. Нет, я не имею в виду себя, так как в виду своей природы мне непонятны все эти словесные перепалки между людьми, культивирующими те или иные операционные системы, командные шеллы и прочее, — еще ничто из того, что было бы создано человеком, не было совершенным, да и вряд ли таковое когда-либо случится, так как нельзя удержать в поле зрения абсолютно все аспекты предмета, не говоря уже о том, что в некоторых из них человек может оказаться не сведущ вовсе. Предмет — всего лишь средство в достижении поставленой цели, насколько эффективно он используется — это уже вопрос рационального подхода к его характеристикам.Предел — это более психологический барьер, нежели факт. Когда кто-то говорит, что достиг предела в некотором из своих начинаний, можно с уверенностью утверждать, что человек не добился ровным счетом ничего, а некоторый результат — всего лишь промежуточное состояние предмета. Возможно, кто-то припомнит избитую поговорку «нет предела совершенству», на что можно парировать «ничто не совершенно»; совершенство по сути — недостижимая цель, которую человек себе ставит очевидно лишь для того, чтобы наполнить жизнь смыслом. Впрочем, это все риторика, имеющая к делу лишь посредственное отношение.Развитие PowerShell планомерно, в смысле разработчики изначально заложили в него прочный фундамент, возводя с каждой последующей версией не менее твердую конструкцию. И все же лично мне кажется, что некоторые вещи в PowerShell развиваются несколько не в том направлении. Например, если с отсутствием возможности создавать перечисления мириться можно (в виду наличия хэштаблиц), то как быть со структурами? Разумеется и перечисления и структуры могут быть созданы посредством командлета Add-Type, но лично мне этот способ кажется топорным из-за его расхода времени на компиляцию кода. Найдется еще с десяток прочих аргументов не в пользу использования данного командлета, но соль не в этом. Создатели PowerShell весьма дальновидно предусмотрели расширяемость последнего за счет модулей (если мне не изменяет память, эта возможность появилась во второй версии), тем самым стимулируя разработчиков на различного рода эксперименты как с функциональностью, так и синтаксисом.
Мои эксперименты с PowerShell начинались в пору первой версии последнего. Тогда мной было предпринято несколько попыток расширения возможностей PowerShell за счет компиляции C#-кода, так как командлета Add-Type еще не было; чуть позже возникло желание расширить синтаксис самого PowerShell, но эта затея стала принимать вполне осязаемые черты лишь с переходом на вторую версию, — ключевую роль здесь сыграли именно модули. Впрочем, на все имеющиеся на данный момент наработки повляла одна специфическая черта PowerShell — диски.
Согласно официальной документации диск в PowerShell представляет собой хранилище данных, доступ к которому аналогичен тому, как если бы мы обращались к объекту файловой системы. Я не стану подробно останавливаться на описании каждого диска в отдельности — детали в документации, поясню концепцию легшей в основу идеи расширения синтаксических возможностей PowerShell.
В языках программирования под словом функция разумеется блок инструкций, в то время как в PowerShell функция — это диск, хранящий определение функции в виде пары имя-скрипт-блок. Это проще продемонстрировать на примере.
PS C:\> function add ($a, $b) { $a + $b } PS C:\> dir function: PS C:\> #или чтобы отсеять ненужное PS C:\> dir function: add Чтобы посмотреть содержимое функции, используем командлет Get-Content. PS C:\> gc function: add param ($a, $b) $a + $b Убеждаемся, что содержимое является скрипт-блоком. PS C:\> (gc function: add).GetType ()
IsPublic IsSerial Name BaseType -------- -------- ---- -------- True False ScriptBlock System.Object Иными словами объявление функции в PowerShell является своего рода иллюзией функции в привычной для последней трактовке, а смысловую нагрузку на себя принимает именно скрипт-блок. PS C:\> add 10 20 30 PS C:\> (gc function: add).Invoke (10, 20) 30 Так как функция — это диск, следовательно объявление функции в PowerShell в сущности является записью данных на этот диск. PS C:\> sc function: add { param ($a, $b) $a + $b } Такая запись избыточна, так как все накладные расходы при традиционном объявлении функции хост берет на себя, здесь эта запись приводится для понимания сути (лично я использую подобную запись, чтобы отделить функции с составным именем от простых). PS C:\> function Add-Something { … } #составное имя PS C:\> sc function: done { … } #простое имя Все это занимательно, но какое отношение это имеет к расширению синтаксиса PowerShell? Как я уже говорил, за основу была взята концепция, о которой только что было рассказано, реализация же строится относительно понятия динамической сборки в текущем домене приложений. Давайте посмотрим на следующий код. Set-Content function: dynmod { $name = -join (0…7 | % {$rnd = New-Object Random}{ [Char]$rnd.Next (97, 122) }) if (!($asm = ($cd = [AppDomain]:: CurrentDomain).GetAssemblies () | ? { $_.ManifestModule.ScopeName.Equals (($mem = 'RefEmit_InMemoryManifestModule')) })) { ($cd.DefineDynamicAssembly ( (New-Object Reflection.AssemblyName ($name)), 'Run' )).DefineDynamicModule ($name, $false) } else { $asm.GetModules () | ? {$_.FullyQualifiedName -ne $mem} } } Функция (мы то знаем, что на самом деле скрипт-блок) создает или обращается к уже созданному модулю в динамической сборке в текщем домене приложений. Сама по себе она мало что значит и в сущности является связующим звеном между хостом и прочими функциями, которые мы в дальнейшем определим. Например, давайте упростим себе вызов API’шных функций за счет подобия C#-делегатов. #обертка над инкапсулированными функциями GetModuleHandle и GetProcAddress function Get-ProcAddress { [OutputType ([IntPtr])] param ( [Parameter (Mandatory=$true, Position=0)] [String]$Dll, [Parameter (Mandatory=$true, Position=1)] [String]$Function ) $href = New-Object Runtime.InteropServices.HandleRef ( (New-Object IntPtr), [IntPtr]($$ = [Regex].Assembly.GetType ( 'Microsoft.Win32.UnsafeNativeMethods' ).GetMethods () | ? { $_.Name -match '\AGet (ModuleH|ProcA).*\Z' })[0].Invoke ( $null, @($Dll) )) if (($ptr = [IntPtr]$$[1].Invoke ($null, @([Runtime.InteropServices.HandleRef]$href, $Function) )) -eq [IntPtr]:: Zero) { throw (New-Object Exception («Could not find $Function entry point in $Dll library.»)) } return $ptr }
#какбы новое ключевое слово — delegate Set-Content function: delegate { [OutputType ([Type])] param ( [Parameter (Mandatory=$true, Position=0)] [String]$Dll, [Parameter (Mandatory=$true, Position=1)] [String]$Function, [Parameter (Mandatory=$true, Position=2)] [Type]$ReturnType, [Parameter (Mandatory=$true, Position=3)] [Type[]]$Parameters ) $ptr = Get-ProcAddress $Dll $Function $Delegate = $Function + 'Delegate' if (!(($mb = dynmod).GetTypes () | ? {$_.Name -eq $Delegate})) { $type = $mb.DefineType ( $Delegate, 'AnsiClass, Class, Public, Sealed', [MulticastDelegate] ) $ctor = $type.DefineConstructor ( 'HideBySig, Public, RTSpecialName', 'Standard', $Parameters ) $ctor.SetImplementationFlags ('Managed, Runtime') $meth = $type.DefineMethod ( 'Invoke', 'HideBySig, NewSlot, Public, Virtual', $ReturnType, $Parameters ) $Parameters | % {$i = 1}{ if ($_.IsByRef) { [void]$meth.DefineParameter ($i, 'Out', $null) } $i++ } $meth.SetImplementationFlags ('Managed, Runtime') [Runtime.InteropServices.Marshal]:: GetDelegateForFunctionPointer ( $ptr, ($type.CreateType ()) ) } else { [Runtime.InteropServices.Marshal]:: GetDelegateForFunctionPointer ( $ptr, $mb.GetType ($Delegate) ) } } Теперь вызвать некоторую API-функцию стало проще (эдакий clockres). [Int32]$max = $min = $cur = 0
if ((delegate ntdll NtQueryTimerResolution Int32 @( [Int32].MakeByRefType (), [Int32].MakeByRefType (), [Int32].MakeByRefType () )).Invoke ([ref]$max, [ref]$min. [ref]$cur) -eq 0) { 'Maximum timer resolution: {0:3f}' -f ($max / 10000) 'Minimum timer resolution: {0:3f}' -f {$min / 10000) 'Current timer resolution: {0:3f}' -f ($cur / 10000) } Понятно, что есть свои ограничения и подводные камни, но повторюсь, что это лишь идея.Как я уже говорил, мне хотелось иметь возможноть создавать структуры прямо в PowerShell, без кода на C#. #какбы новое ключевое слово — struct Set-Content function: struct { [OutputType ([Type])] param ( [Parameter (Mandatory=$true, Position=0)] [String]$StructName, [Parameter (Mandatory=$true, Position=1)] [ScriptBlock]$Definition, [Parameter (Position=2)] [Reflection.Emit.PackingSize]$PackingSize = 'Unspecified', [Parameter (Position=3)] [Switch]$Explicit ) if (!(($mb = dynmod).GetTypes () | ? {$_.Name -eq $StructName})) { [Reflection.TypeAttributes]$attr = 'AnsiClass, BeforeFieldInit, Class, Public, Sealed' $attr = switch ($Explicit) { $true { $attr -bor [Reflection.TypeAttributes]:: ExplicitLayout } $false { $attr -bor [Reflection.TypeAttributes]:: SequentialLayout } } $type = $mb.DefineType ($StructName, $attr, [ValueType], $PackingSize) $ctor = [Runtime.InteropServices.MarshalAsAttribute].GetConstructor ( [Reflection.BindingFlags]20, $null, [Type[]]@([Runtime.InteropServices.UnmanagedType]), $null ) $cnst = @([Runtime.InteropServices.MarshalAsAttribute].GetField ('SizeConst')) $ret = $null [Management.Automation.PSParser]:: Tokenize ($Definition, [ref]$ret) | ? { $_.Type -match '\A (Command|String)\Z' } | % { if ($_.Type -eq 'Command') { $token = $_.Content #тип поля $ft = switch (($def = $mb.GetType ($token)) -eq $null) { $true { [Type]$token } $false { $def } #поиск типа в динамической сборке } #switch } else { $token = @($_.Content -split '\s') #имя поля, смещение, атрибуты и размер switch ($token.Length) { 1 { [void]$type.DefineField ($token[0], $ft, 'Public') } #пример: UInt32 'e_lfanew'; 2 { #структура помечена как Explicit: Int64 'QuadPart 0'; иначе String 'Buffer LPWStr'; switch ($Explicit) { $true { [void]$type.DefineField ($token[0], $ft, 'Public').SetOffset ([Int32]($token[1])) } $false { $unm = [Runtime.InteropServices.UnmanagedType]($token[1]) [void]$type.DefineField ($token[0], $ft, 'Public, HasFieldMarshal').SetCustomAttribute ( (New-Object Reflection.Emit.CustomAttributeBuilder ($ctor, [Object[]]@($unm))) ) } } #switch } 3 { #пример: UInt16[] 'e_res ByValArray 10'; $unm = [Runtime.InteropServices.UnmanagedType]$token[1] [void]$type.DefineField ($token[0], $ft, 'Public, HasFieldMarshal').SetCustomAttribute ( (New-Object Reflection.Emit.CustomAttributeBuilder ($ctor, $unm, $cnst, @([Int32]$token[2]))) ) } } #switch } } #foreach #пара полезных методов для создаваемой структуры $OpCodes = [Reflection.Emit.OpCodes] $Marshal = [Runtime.InteropServices.Marshal] $GetSize = $type.DefineMethod ('GetSize', 'Public, Static', [Int32], [Type[]]@()) $IL = $GetSize.GetILGenerator () $IL.Emit ($OpCodes: Ldtoken, $type) $IL.Emit ($OpCodes: Call, [Type].GetMethod ('GetTypeFromHandle')) $IL.Emit ($OpCodes: Call, $Marshal.GetMethod ('SizeOf', [Type[]]@([Type]))) $IL.Emit ($OpCodes: Ret) $Implicit = $type.DefineMethod ( 'op_Implicit', 'PrivateScope, Public, Static, HideBySig, SpecialName', $type, [Type[]]@([IntPtr]) ) $IL = $Implicit.GetILGenerator () $IL.Emit ($OpCodes: Ldarg_0) $IL.Emit ($OpCodes: Ldtoken, $type) $IL.Emit ($OpCodes: Call, [Type].GetMethod ('GetTypeFromHandle')) $IL.Emit ($OpCodes: Call, $Marshal.GetMethod ('PtrToStructure', [Type[]]@([IntPtr], [Type]))) $IL.Emit ($OpCodes: Unbox_Any, $type) $IL.Emit ($OpCodes: Ret) $type.CreateType () } else { $mb.GetType ($StructName) } } Пример (uptime). $sti = struct SYSTEM_TIMEOFDAY_INFORMATION { Int64 'BootTime'; Int64 'CurrentTime'; Int64 'TimeZoneBias'; UInt32 'TimeZoneId'; UInt32 'Reserved'; UInt64 'BootTimeBias'; UInt64 'SleepTimeBias'; }
$sti = NtQuerySystemInformation $sti SystemTimeOfDayInformation
'{0: D2}:{1: D2}:{2: D2} up {3} day{4}' -f ( $u = (Get-Date) — [DateTime]:: FromFileTime ($sti.BootTime) ).Hours, $u.Minutes, $u.Seconds, $u.Days, $(if ($u.Days -gt 1){'s'}else{''}) Где NtQuerySystemInformation: $SYSTEM_INFORMATION_CLASS = @{ … SystemTimeOfDayInformation = 3 … }
Set-Content function: NtQuerySystemInformation { param ( [Parameter (Mandatory=$true, Position=0)] [Type]$Struct, [Parameter (Mandatory=$true, Position=1)] [String]$Class ) $len = $Struct: GetSize () $ptr = [Runtime.InteropServices.Marshal]:: AllocHGlobal ($len) $cls = $SYSTEM_INFORMATION_CLASS[$Class] if ([Regex].Assembly.GetType ('Microsoft.Win32.NativeMethods').GetMethod ( 'NtQuerySystemInformation' ).Invoke ($null, @($cls, $ptr, $len, $ref)) -eq 0) { $str = $ptr -as $Struct } [Runtime.InteropServices.Marshal]:: FreeHGlobal ($ptr) return $str } Вроде бы начиналось что-то о функциях, а заканчивается кучей кода, — позвольте кое-что прояснить. Будь то делегат или структура, которую мы объявляем, все это заносится в одну единственную сборку (принцип диска); прочий код — попытка автоматизировать\упростить создание\вызов структур\API-функций. При этом структуры дополняются парой полезных методов (получения размера структуры и конвертации указателя в структуру с помощью оператора as, например, $ptr -as $struc).Все эти изыскания не появились в одночасье, а являются результатом многих экспериментов и простым желанием упростить написание модулей. Возможно кто-то найдет для себя все это полезным и интересным, а этот пост станет своего рода отправной точкой в дальнейших исследованиях или даже поможет сократить количество набираемого кода.