Send-VMKeystrokes.psm1
using namespace System.Collections.Generic using namespace System.Management.Automation using namespace VMware.VimAutomation.ViCore.Types.V1.Inventory #Requires -Module VMware.VimAutomation.Core $Script:HIDMap = Import-PowerShellDataFile -Path "$PSScriptRoot\data\HIDMap.psd1" $Script:ShiftedChars = [string[]][char[]]( Get-Content -Path "$PSScriptRoot\data\ShiftedChars.txt" ) class VirtualMachineTransformAttribute : ArgumentTransformationAttribute { # NOTE: output type MUST be [object] [object] Transform( [EngineIntrinsics]$EngineIntrinsics, [object]$InputObject ) { if ( $InputObject -is [VirtualMachine] ) { return $InputObject } if ( $InputObject -is [string] ) { $VirtualMachine = Get-VM -Name $InputObject -ErrorAction Stop if ( $VirtualMachine.Count -gt 1 ) { throw 'Function requires that a single virtual machine matches.' } return $VirtualMachine } throw 'Failed to translate to Virtual Machine. Input must be a Virtual Machine or string.' } } function Split-VMKeystrokeString { <# .SYNOPSIS Split a command string into chunks for processing .PARAMETER String The string to split .EXAMPLE 'Test{BACKSPACE}String^!xExample' | Split-VMKeystrokeString T e s t {BACKSPACE} S t r i n g ^!x E x a m p l e #> [CmdletBinding()] [OutputType( [string[]] )] param( [Parameter( Mandatory, ValueFromPipeline, Position = 0 )] [AllowEmptyString()] [AllowNull()] [string[]] $String, [switch] $Verbatim ) process { # always process input strings as a single string $JoinedString = $String -join '' # if the input string is null or empty we don't want to put an empty array on the pipeline if ( [string]::IsNullOrEmpty($JoinedString) ) { return } # if -Verbatim then we split the string up into single characters and return it if ( $Verbatim ) { return [string[]][char[]]$JoinedString } # otherwise process with our magic regex $JoinedString -split '(?=(?<=[^!+#^])[!+#^]+(?:[^!+#^{}]|\{\w+\}|\{[!+#^{}]\}))|(?<=(?<=^|[^!+#^])[!+#^]+(?:[^!+#^{}]|\{\w+\}|\{[!+#^{}]\}))|(?=(?<![!+#^])(?:\{\w+\}|\{[!+#^{}]\}))|(?<=\{\w+\}|\{[!+#^{}]\})' | ForEach-Object { # matches any special character, single character with a modifier, or single character # ex: {ENTER} or ^!{DELETE} or +a or a if ( $_ -match '^[!+#^]*(?:.|\{(?:[!+#^{}]|\w+)\})$' ) { return $_ } # everything else should be single characters or escaped characters # the pattern below un-escapes any escaped characters, i.e. {{} should be { return [string[]][char[]]$_ } } } function Convert-VMKeystrokeStringToHIDEvent { <# .SYNOPSIS Converts a string into a UsbScanCodeSpecKeyEvent object .PARAMETER Strings String(s) to convert into events #> [CmdletBinding()] [OutputType([VMware.Vim.UsbScanCodeSpecKeyEvent])] param( [Parameter( Mandatory, ValueFromPipeline, Position = 0 )] [ValidateNotNullOrEmpty()] [string[]] $String ) process { $String | ForEach-Object { Write-Debug ( 'Pre-Processed Character: {0}' -f $_ ) if ( $_.Length -eq 1 ) { [string]$Character = $_ [string[]]$Modifiers = @() } else { if ( $_ -notmatch '^(?:.|[!+#^]*(?:\{(?:[!+#^{}]|\w+)\}|[^!+#^{}]))$' ) { throw ( 'Invalid character definition detected: {0}' -f $_ ) } # This regex will always put at least a character into $Character since it # matches any modifiers and the beginning of the string. This means it can # handle: +x, ^!{DELETE}, and {ENTER} [string[]][char[]] $Modifiers, [string]$Character = $_ -split '(?<=^[!+#^]*)(?=[^!+#^])' # remove the curly braces around the special characters if ( $Character -match '^\{(?<Character>\w+|[!+#^{}])\}$') { $Character = $Matches.Character } } Write-Debug ( 'Post-Processed Character: {0}' -f $Character ) Write-Debug ( 'Post-Porcessed Modifiers: {0}' -f [string]$Modifiers ) # Check to see if we've mapped the character to HID code if ( -not $Script:HIDMap.ContainsKey($Character) ) { throw ( 'The character ''{0}'' has not been mapped, you will need to remove or manually process this character.' -f $Character ) } $UsbScanCodeSpecKeyEvent = New-Object VMware.Vim.UsbScanCodeSpecKeyEvent $UsbScanCodeSpecKeyEvent.UsbHidCode = ( [Int64]$Script:HIDMap[$Character] -shl 16 ) -bor 7 $UsbScanCodeSpecKeyEvent.Modifiers = New-Object Vmware.Vim.UsbScanCodeSpecModifierType # use a shift modifier if idicated or # the character is in the ShiftedChars array or # the character is an upper case letter if ( $Modifiers -contains '+' ) { Write-Debug 'Character has the + prefix, adding SHIFT modifier' $UsbScanCodeSpecKeyEvent.Modifiers.LeftShift = $true } elseif ( $Character -cmatch '^[A-Z]$' ) { Write-Debug 'Character is a capital letter, adding SHIFT modifier' $UsbScanCodeSpecKeyEvent.Modifiers.LeftShift = $true } elseif ( $Script:ShiftedChars -contains $Character ) { Write-Debug ( 'Characters is one of {0}, adding SHIFT modifier' -f $Script:ShiftedChars ) $UsbScanCodeSpecKeyEvent.Modifiers.LeftShift = $true } # use an alt modifier if indicated if ( $Modifiers -contains '!' ) { Write-Debug 'Character has the ! prefix, adding ALT modifier' $UsbScanCodeSpecKeyEvent.Modifiers.LeftAlt = $true } # use a ctrl modifier if indicated if ( $Modifiers -contains '^' ) { Write-Debug 'Character has the ^ prefix, adding CTRL modifier' $UsbScanCodeSpecKeyEvent.Modifiers.LeftControl = $true } # use a OS key modifier if indicated if ( $Modifiers -contains '#' ) { Write-Debug 'Character has the # prefix, adding META modifier' $UsbScanCodeSpecKeyEvent.Modifiers.LeftGui = $true } Write-Debug ( 'Character: {0} -> HIDCode: 0x{1:x2} -> HIDCodeValue: 0x{2:x8} -> Modifiers: {3}' -f $Character, $Script:HIDMap[$Character], $UsbScanCodeSpecKeyEvent.UsbHidCode, ( $UsbScanCodeSpecKeyEvent.Modifiers.PSObject.Properties.Where({ $_.Value -eq $true }).Name -join '+' ) ) $UsbScanCodeSpecKeyEvent } } } function Send-VMKeystrokeHIDEvent { <# .SYNOPSIS Sends USB keyboard HID events to a VMware virtual machine .DESCRIPTION Sends USB keyboard HID events to a VMware virtual machine .PARAMETER HIDEvents A list of HID events to send .PARAMETER VM The VMware virtual machine to send events to .PARAMETER KeyPressDelay The delay between key presses in milliseconds, the default is send all events together .PARAMETER PassThru PassThru the VMware virtual machine object on the pipeline #> [CmdletBinding( DefaultParameterSetName='Default' )] [OutputType( [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine], ParameterSetName='PassThru' )] param( [Parameter( Mandatory, Position = 0 )] [AllowEmptyString()] [VMware.Vim.UsbScanCodeSpecKeyEvent[]] $HIDEvents, [Parameter( Mandatory, ValueFromPipeline )] [Alias( 'VirtualMachine', 'Name' )] [VirtualMachineTransformAttribute()] [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $VM, [ValidateRange(100,[uint]::MaxValue)] [System.Nullable[uint32]] $KeyPressDelay, [Parameter( Mandatory, ParameterSetName='PassThru' )] [switch] $PassThru ) process { $VMView = Get-View -ViewType VirtualMachine -Filter @{ Name = "^${VM}$" } -ErrorAction Stop $UsbScanCodeSpec = New-Object Vmware.Vim.UsbScanCodeSpec Write-Debug ( 'Sending {0} HID events to {1}' -f $HIDEvents.Count, $VM ) if ( $KeyPressDelay ) { for ( $i = 0; $i -lt $HIDEvents.Count; $i ++ ) { $UsbScanCodeSpec.KeyEvents = $HIDEvents[$i] [void] $VMView.PutUsbScanCodes($UsbScanCodeSpec) if ( ( $i + 1 ) -lt $HIDEvents.Count ) { Write-Debug ( 'Sending USB HID code 0x{0:x2} with {1} ms delay' -f ( ( $HIDEvents[$i].UsbHidCode -bor 7 ) -shr 16 ), $KeyPressDelay ) Start-Sleep -Milliseconds $KeyPressDelay } else { Write-Debug ( 'Sending USB HID code 0x{0:x2}' -f ( ( $HIDEvents[$i].UsbHidCode -bor 7 ) -shr 16 ) ) } } } else { $UsbScanCodeSpec.KeyEvents = $HIDEvents [void] $VMView.PutUsbScanCodes($UsbScanCodeSpec) } if ( $PassThru ) { $VM } } } function Send-VMKeystrokes { <# .SYNOPSIS Sends a formatted string of keystrokes to a VMware virtual machine .DESCRIPTION Sends a formatted string of keystrokes to a VMware virtual machine. This function uses mostly the same format as AutoIt's Send function. See the HIDMap.psd1 file for a full list of supported keys. .PARAMETER String Formatted string(s) to send to the VMware virtual machine .PARAMETER VM The VMware virtual machine to send events to .PARAMETER KeyPressDelay The delay between key presses in milliseconds, the default is send all events together .PARAMETER PauseAfterSeconds Seconds to pause after sending keypresses, usefull to pipe multiple commands together for automation. .PARAMETER PauseAfterMilliseconds Milliseconds to pause after sending keypresses, usefull to pipe multiple commands together for automation. .PARAMETER PauseAfterDuration Duration specified as a timespan to pause after sending keypresses, usefull to pipe multiple commands together for automation. .PARAMETER SendCarriageReturn Send a trailing {ENTER} key .PARAMETER Verbatim Send the string without parsing form command keys, useful for sending passwords .PARAMETER PassThru PassThru the VMware virtual machine object on the pipeline #> [CmdletBinding( DefaultParameterSetName='Default' )] [OutputType( [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] )] param( [Parameter( Mandatory, Position = 0 )] [AllowEmptyString()] [string[]] $String, [Parameter( Mandatory, ValueFromPipeline )] [Alias( 'VirtualMachine', 'Name' )] [VirtualMachineTransformAttribute()] [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $VM, [ValidateRange(100,[uint]::MaxValue)] [System.Nullable[uint32]] $KeyPressDelay, [Parameter( Mandatory, ParameterSetName='PauseAfterSeconds' )] [System.Nullable[uint]] $PauseAfterSeconds, [Parameter( Mandatory, ParameterSetName='PauseAfterMilliseconds' )] [System.Nullable[uint]] $PauseAfterMilliseconds, [Parameter( Mandatory, ParameterSetName='PauseAfterDuration' )] [timespan] $PauseAfterDuration, [switch] $SendCarriageReturn, [switch] $Verbatim, [switch] $PassThru ) # convert input string in to list of HID events [List[object]]$HIDEvents = $String | Split-VMKeystrokeString -Verbatim:$Verbatim.IsPresent | Convert-VMKeystrokeStringToHIDEvent # Add return carriage to the end of the string input (useful for logins or executing commands) if ( $SendCarriageReturn ) { $HIDEvents.Add((Convert-VMKeystrokeStringToHIDEvent '{ENTER}')) } Write-Verbose ( 'Sending keystrokes to {0}' -f $VM ) $KeyPressDelaySplat = @{} if ( $KeyPressDelay ) { $KeyPressDelaySplat.KeyPressDelay = $KeyPressDelay } $VM | Send-VMKeystrokeHIDEvent -HIDEvents $HIDEvents @KeyPressDelaySplat if ( $PauseAfterSeconds ) { $PauseAfterDuration = [timespan]::FromSeconds($PauseAfterSeconds) } if ( $PauseAfterMilliseconds ) { $PauseAfterDuration = [timespan]::FromMilliseconds($PauseAfterMilliseconds) } if ( $PauseAfterDuration ) { Start-Sleep -Duration $PauseAfterDuration } if ( $PassThru ) { $VM } } function ConvertTo-VMKeystrokeEscapedString { <# .SYNOPSIS Escapes control characters in a string, useful for sending passwords .PARAMETER String The string to escape #> [CmdletBinding()] [OutputType([string])] param( [Parameter( Mandatory, Position = 0, ValueFromPipeline )] [AllowEmptyString()] [string] $String ) $String -replace '([!+#^{}])', '{$1}' } |