psuspend.psm1

#requires -version 6
using namespace System.Linq
using namespace System.Reflection
using namespace System.ComponentModel
using namespace System.Reflection.Emit
using namespace System.Runtime.InteropServices

function Get-ProcAddress {
  [OutputType([Hashtable])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNullOrEmpty()]
    [String[]]$Function
  )

  process {
    $kernel32 = @{}

    [Assembly]::LoadFile("$(
      [RuntimeEnvironment]::GetRuntimeDirectory()
    )Microsoft.Win32.SystemEvents.dll"
).GetType('Interop').GetNestedType(
      'Kernel32', [BindingFlags]'NonPublic'
    ).GetMethods([BindingFlags]'NonPublic, Static, Public').Where{
      $_.Name -cmatch '\AGet(Proc|Mod)'
    }.ForEach{$kernel32[$_.Name] = $_}

    if ((
      $mod = $kernel32.GetModuleHandle.Invoke($null, @($Module))
    ) -eq [IntPtr]::Zero) {
      throw [DllNotFoundException]::new()
    }

    $funcs = @{}
    $Function.ForEach{
      if ((
        $$ = $kernel32.GetProcAddress.Invoke($null, @($mod, $_))
      ) -ne [IntPtr]::Zero) { $funcs.$_ = $$ }
    }
    $funcs
  }
}

function Set-Delegate {
  [OutputType([Type])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateScript({$_ -ne [IntPtr]::Zero})]
    [IntPtr]$ProcAddress,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNull()]
    [Type]$Prototype,

    [Parameter(Position=2)]
    [ValidateNotNullOrEmpty()]
    [CallingConvention]$CallingConvention = 'StdCall'
  )

  process {
    $method = $Prototype.GetMethod('Invoke')
    $returntype, $paramtypes = $method.ReturnType, $method.GetParameters().ParameterType
    $paramtypes = ($paramtypes, $null)[!$paramtypes]
    $il, $sz = ($holder = [DynamicMethod]::new(
      'Invoke', $returntype, $paramtypes, $Prototype
    )).GetILGenerator(), [IntPtr]::Size

    if ($paramtypes) {
      (0..($paramtypes.Length - 1)).ForEach{$il.Emit([OpCodes]::ldarg, $_)}
    }

    $il.Emit([OpCodes]::"ldc_i$sz", $ProcAddress."ToInt$((32, 64)[$sz / 4 - 1])"())
    $il.EmitCalli([OpCodes]::calli, $CallingConvention, $returntype, $paramtypes)
    $il.Emit([OpCodes]::ret)

    $holder.CreateDelegate($Prototype)
  }
}

function New-Delegate {
  [OutputType([Hashtable])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNull()]
    [Hashtable]$Signature
  )

  process {
    $funcs, $addr = @{}, (Get-ProcAddress -Module $Module -Function $Signature.Keys)
    $addr.Keys.ForEach{
      $funcs.$_ = Set-Delegate -ProcAddress $addr.$_ -Prototype $Signature.$_
    }
    $funcs
  }
}

Set-Alias -Name psrsm -Value Resume-Process
function Resume-Process {
  [CmdletBinding(DefaultParameterSetName='Name')]
  param(
    [Parameter(Mandatory, ParameterSetName='Name', Position=0)]
    [ValidateScript({!!($script:ps = Get-Process $_ -ErrorAction 0)})]
    [ValidateNotNullOrEmpty()]
    [String]$Name,

    [Parameter(Mandatory, ParameterSetName='Id', Position=0)]
    [ValidateScript({!!($script:ps = Get-Process -Id $_ -ErrorAction 0)})]
    [Int32]$Id
  )

  process {
    $ntdll = New-Delegate ntdll -Signature @{
      NtResumeProcess = [Func[IntPtr, Int32]]
      RtlNtStatusToDosError = [Func[Int32, Int32]]
    }

    $ps.ForEach{
      if ([Enumerable]::Sum([Int32[]](
        Select-Object -InputObject $_.Threads[0] -Property ThreadState, WaitReason
      ).PSObject.Properties.Value.ForEach{$_ -eq 5}) -eq 2) {
        if (($nts = $ntdll.NtResumeProcess.Invoke($_.Handle)) -ne 0) {
          Write-Verbose "$([Win32Exception]::new(
            $ntdll.RtlNtStatusToDosError.Invoke($nts)
          ).Message)"

        }
        else { Write-Verbose "Process $($_.Id) is resumed." }
      }
      else { Write-Verbose "Process $($_.Id) is already active." }
      $_.Dispose()
    }
  }
}

Set-Alias -Name pspnd -Value Suspend-Process
function Suspend-Process {
  [CmdletBinding(DefaultParameterSetName='Name')]
  param(
    [Parameter(Mandatory, ParameterSetName='Name', Position=0)]
    [ValidateScript({!!($script:ps = Get-Process $_ -ErrorAction 0)})]
    [ValidateNotNullOrEmpty()]
    [String]$Name,

    [Parameter(Mandatory, ParameterSetName='Id', Position=0)]
    [ValidateScript({!!($script:ps = Get-Process -Id $_ -ErrorAction 0)})]
    [Int32]$Id
  )

  process {
    $ntdll = New-Delegate ntdll -Signature @{
      NtSuspendProcess = [Func[IntPtr, Int32]]
      RtlNtStatusToDosError = [Func[Int32, Int32]]
    }

    $ps.ForEach{
      if ([Enumerable]::Sum([Int32[]](
        Select-Object -InputObject $_.Threads[0] -Property ThreadState, WaitReason
      ).PSObject.Properties.Value.ForEach{$_ -eq 5}) -ne 2) {
        if (($nts = $ntdll.NtSuspendProcess.Invoke($_.Handle)) -ne 0) {
          Write-Verbose "$([Win32Exception]::new(
            $ntdll.RtlNtStatusToDosError.Invoke($nts)
          ).Message)"

        }
        else { Write-Verbose "Process $($_.Id) is suspended." }
      }
      else { Write-Verbose "Process $($_.Id) is already suspended." }
      $_.Dispose()
    }
  }
}

Export-ModuleMember -Alias psrsm, pspnd -Function Resume-Process, Suspend-Process