PowerShell: за гранью

в 9:51, , рубрики: powershell, powershell2, Программирование, метки:

Какие бы хвалебные оды не пелись в адрес 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).

Все эти изыскания не появились в одночасье, а являются результатом многих экспериментов и простым желанием упростить написание модулей. Возможно кто-то найдет для себя все это полезным и интересным, а этот пост станет своего рода отправной точкой в дальнейших исследованиях или даже поможет сократить количество набираемого кода.

Автор: gregzakharov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js