@ -34,6 +34,845 @@ _powershell_version = os.environ.get('POWERSHELL_VERSION', None)
if _powershell_version :
if _powershell_version :
_common_args = [ ' PowerShell ' , ' -Version ' , _powershell_version ] + _common_args [ 1 : ]
_common_args = [ ' PowerShell ' , ' -Version ' , _powershell_version ] + _common_args [ 1 : ]
exec_wrapper = br '''
#Requires -Version 3.0
begin {
$ DebugPreference = " Continue "
$ ErrorActionPreference = " Stop "
Set - StrictMode - Version 2
function ConvertTo - HashtableFromPsCustomObject ( $ myPsObject ) {
$ output = @ { } ;
$ myPsObject | Get - Member - MemberType * Property | % {
$ val = $ myPsObject . ( $ _ . name ) ;
If ( $ val - is [ psobject ] ) {
$ val = ConvertTo - HashtableFromPsCustomObject $ val
}
$ output . ( $ _ . name ) = $ val
}
return $ output ;
}
# stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
# exec runspace, capture output, cleanup, return module output
$ json_raw = " "
}
process {
$ input_as_string = [ string ] $ input
$ json_raw + = $ input_as_string
}
end {
If ( - not $ json_raw ) {
Write - Error " no input given " - Category InvalidArgument
}
$ payload = ConvertTo - HashtableFromPsCustomObject ( ConvertFrom - Json $ json_raw )
# TODO: handle binary modules
# TODO: handle persistence
$ actions = $ payload . actions
# pop 0th action as entrypoint
$ entrypoint = $ payload . ( $ actions [ 0 ] )
$ payload . actions = $ payload . actions [ 1. .99 ]
$ entrypoint = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ entrypoint ) )
# load the current action entrypoint as a module custom object with a Run method
$ entrypoint = New - Module - ScriptBlock ( [ scriptblock ] : : Create ( $ entrypoint ) ) - AsCustomObject
Set - Variable - Scope global - Name complex_args - Value $ payload [ " module_args " ] | Out - Null
# dynamically create/load modules
ForEach ( $ mod in $ payload . powershell_modules . GetEnumerator ( ) ) {
$ decoded_module = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ mod . Value ) )
New - Module - ScriptBlock ( [ scriptblock ] : : Create ( $ decoded_module ) ) - Name $ mod . Key | Import - Module | Out - Null
}
$ output = $ entrypoint . Run ( $ payload )
Write - Output $ output
}
''' # end exec_wrapper
leaf_exec = br '''
Function Run ( $ payload ) {
$ entrypoint = $ payload . module_entry
$ entrypoint = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ entrypoint ) )
$ ps = [ powershell ] : : Create ( )
$ ps . AddStatement ( ) . AddCommand ( " Set-Variable " ) . AddParameters ( @ { Scope = " global " ; Name = " complex_args " ; Value = $ payload . module_args } ) | Out - Null
$ ps . AddCommand ( " Out-Null " ) | Out - Null
# redefine Write-Host to dump to output instead of failing- lots of scripts use it
$ ps . AddStatement ( ) . AddScript ( " Function Write-Host(`$msg) { Write-Output `$msg } " ) | Out - Null
# dynamically create/load modules
ForEach ( $ mod in $ payload . powershell_modules . GetEnumerator ( ) ) {
$ decoded_module = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ mod . Value ) )
$ ps . AddStatement ( ) . AddCommand ( " New-Module " ) . AddParameters ( @ { ScriptBlock = ( [ scriptblock ] : : Create ( $ decoded_module ) ) ; Name = $ mod . Key } ) | Out - Null
$ ps . AddCommand ( " Import-Module " ) | Out - Null
$ ps . AddCommand ( " Out-Null " ) | Out - Null
}
$ ps . AddStatement ( ) . AddScript ( $ entrypoint ) | Out - Null
$ output = $ ps . Invoke ( )
$ output
# PS3 doesn't properly set HadErrors in many cases, inspect the error stream as a fallback
If ( $ ps . HadErrors - or ( $ PSVersionTable . PSVersion . Major - lt 4 - and $ ps . Streams . Error . Count - gt 0 ) ) {
[ System . Console ] : : Error . WriteLine ( $ ( $ ps . Streams . Error | Out - String ) )
$ exit_code = $ ps . Runspace . SessionStateProxy . GetVariable ( " LASTEXITCODE " )
If ( - not $ exit_code ) {
$ exit_code = 1
}
$ host . SetShouldExit ( $ exit_code )
}
}
''' # end leaf_exec
become_wrapper = br '''
Set - StrictMode - Version 2
$ ErrorActionPreference = " Stop "
$ helper_def = @ "
using System ;
using System . Diagnostics ;
using System . IO ;
using System . Threading ;
using System . Security ;
using System . Security . AccessControl ;
using System . Security . Principal ;
using System . Runtime . InteropServices ;
namespace Ansible . Shell
{
public class ProcessUtil
{
public static void GetProcessOutput ( StreamReader stdoutStream , StreamReader stderrStream , out string stdout , out string stderr )
{
var sowait = new EventWaitHandle ( false , EventResetMode . ManualReset ) ;
var sewait = new EventWaitHandle ( false , EventResetMode . ManualReset ) ;
string so = null , se = null ;
ThreadPool . QueueUserWorkItem ( ( s ) = >
{
so = stdoutStream . ReadToEnd ( ) ;
sowait . Set ( ) ;
} ) ;
ThreadPool . QueueUserWorkItem ( ( s ) = >
{
se = stderrStream . ReadToEnd ( ) ;
sewait . Set ( ) ;
} ) ;
foreach ( var wh in new WaitHandle [ ] { sowait , sewait } )
wh . WaitOne ( ) ;
stdout = so ;
stderr = se ;
}
/ / http : / / stackoverflow . com / a / 30687230 / 139652
public static void GrantAccessToWindowStationAndDesktop ( string username )
{
const int WindowStationAllAccess = 0x000f037f ;
GrantAccess ( username , GetProcessWindowStation ( ) , WindowStationAllAccess ) ;
const int DesktopRightsAllAccess = 0x000f01ff ;
GrantAccess ( username , GetThreadDesktop ( GetCurrentThreadId ( ) ) , DesktopRightsAllAccess ) ;
}
private static void GrantAccess ( string username , IntPtr handle , int accessMask )
{
SafeHandle safeHandle = new NoopSafeHandle ( handle ) ;
GenericSecurity security =
new GenericSecurity ( false , ResourceType . WindowObject , safeHandle , AccessControlSections . Access ) ;
security . AddAccessRule (
new GenericAccessRule ( new NTAccount ( username ) , accessMask , AccessControlType . Allow ) ) ;
security . Persist ( safeHandle , AccessControlSections . Access ) ;
}
[ DllImport ( " user32.dll " , SetLastError = true ) ]
private static extern IntPtr GetProcessWindowStation ( ) ;
[ DllImport ( " user32.dll " , SetLastError = true ) ]
private static extern IntPtr GetThreadDesktop ( int dwThreadId ) ;
[ DllImport ( " kernel32.dll " , SetLastError = true ) ]
private static extern int GetCurrentThreadId ( ) ;
private class GenericAccessRule : AccessRule
{
public GenericAccessRule ( IdentityReference identity , int accessMask , AccessControlType type ) :
base ( identity , accessMask , false , InheritanceFlags . None , PropagationFlags . None , type ) { }
}
private class GenericSecurity : NativeObjectSecurity
{
public GenericSecurity ( bool isContainer , ResourceType resType , SafeHandle objectHandle , AccessControlSections sectionsRequested )
: base ( isContainer , resType , objectHandle , sectionsRequested ) { }
public new void Persist ( SafeHandle handle , AccessControlSections includeSections ) { base . Persist ( handle , includeSections ) ; }
public new void AddAccessRule ( AccessRule rule ) { base . AddAccessRule ( rule ) ; }
public override Type AccessRightType { get { throw new NotImplementedException ( ) ; } }
public override AccessRule AccessRuleFactory ( System . Security . Principal . IdentityReference identityReference , int accessMask , bool isInherited ,
InheritanceFlags inheritanceFlags , PropagationFlags propagationFlags , AccessControlType type ) { throw new NotImplementedException ( ) ; }
public override Type AccessRuleType { get { return typeof ( AccessRule ) ; } }
public override AuditRule AuditRuleFactory ( System . Security . Principal . IdentityReference identityReference , int accessMask , bool isInherited ,
InheritanceFlags inheritanceFlags , PropagationFlags propagationFlags , AuditFlags flags ) { throw new NotImplementedException ( ) ; }
public override Type AuditRuleType { get { return typeof ( AuditRule ) ; } }
}
private class NoopSafeHandle : SafeHandle
{
public NoopSafeHandle ( IntPtr handle ) : base ( handle , false ) { }
public override bool IsInvalid { get { return false ; } }
protected override bool ReleaseHandle ( ) { return true ; }
}
}
}
" @
$ exec_wrapper = {
#Requires -Version 3.0
$ DebugPreference = " Continue "
$ ErrorActionPreference = " Stop "
Set - StrictMode - Version 2
function ConvertTo - HashtableFromPsCustomObject ( $ myPsObject ) {
$ output = @ { } ;
$ myPsObject | Get - Member - MemberType * Property | % {
$ val = $ myPsObject . ( $ _ . name ) ;
If ( $ val - is [ psobject ] ) {
$ val = ConvertTo - HashtableFromPsCustomObject $ val
}
$ output . ( $ _ . name ) = $ val
}
return $ output ;
}
# stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
# exec runspace, capture output, cleanup, return module output
$ json_raw = [ System . Console ] : : In . ReadToEnd ( )
If ( - not $ json_raw ) {
Write - Error " no input given " - Category InvalidArgument
}
$ payload = ConvertTo - HashtableFromPsCustomObject ( ConvertFrom - Json $ json_raw )
# TODO: handle binary modules
# TODO: handle persistence
$ actions = $ payload . actions
# pop 0th action as entrypoint
$ entrypoint = $ payload . ( $ actions [ 0 ] )
$ payload . actions = $ payload . actions [ 1. .99 ]
$ entrypoint = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ entrypoint ) )
# load the current action entrypoint as a module custom object with a Run method
$ entrypoint = New - Module - ScriptBlock ( [ scriptblock ] : : Create ( $ entrypoint ) ) - AsCustomObject
Set - Variable - Scope global - Name complex_args - Value $ payload [ " module_args " ] | Out - Null
# dynamically create/load modules
ForEach ( $ mod in $ payload . powershell_modules . GetEnumerator ( ) ) {
$ decoded_module = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ mod . Value ) )
New - Module - ScriptBlock ( [ scriptblock ] : : Create ( $ decoded_module ) ) - Name $ mod . Key | Import - Module | Out - Null
}
$ output = $ entrypoint . Run ( $ payload )
Write - Output $ output
} # end exec_wrapper
Function Run ( $ payload ) {
# NB: action popping handled inside subprocess wrapper
$ username = $ payload . become_user
$ password = $ payload . become_password
Add - Type - TypeDefinition $ helper_def
$ exec_args = $ null
$ exec_application = " powershell "
# NB: CreateProcessWithLogonW commandline maxes out at 1024 chars, must bootstrap via filesystem
$ temp = [ System . IO . Path ] : : Combine ( [ System . IO . Path ] : : GetTempPath ( ) , [ System . IO . Path ] : : GetRandomFileName ( ) + " .ps1 " )
$ exec_wrapper . ToString ( ) | Set - Content - Path $ temp
# TODO: grant target user permissions on tempfile/tempdir
Try {
# Base64 encode the command so we don't have to worry about the various levels of escaping
# $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper.ToString()))
# force the input encoding to preamble-free UTF8 before we create the new process
[ System . Console ] : : InputEncoding = $ ( New - Object System . Text . UTF8Encoding @ ( $ false ) )
$ exec_args = @ ( " -noninteractive " , $ temp )
$ proc = New - Object System . Diagnostics . Process
$ psi = $ proc . StartInfo
$ psi . FileName = $ exec_application
$ psi . Arguments = $ exec_args
$ psi . RedirectStandardInput = $ true
$ psi . RedirectStandardOutput = $ true
$ psi . RedirectStandardError = $ true
$ psi . UseShellExecute = $ false
If ( $ username . Contains ( " \" )) {
$ sp = $ username . Split ( @ ( [ char ] " \" ), 2)
$ domain = $ sp [ 0 ]
$ username = $ sp [ 1 ]
}
ElseIf ( $ username . Contains ( " @ " ) ) {
$ domain = $ null
}
Else {
$ domain = " . "
}
$ psi . Domain = $ domain
$ psi . Username = $ username
$ psi . Password = $ ( $ password | ConvertTo - SecureString - AsPlainText - Force )
[ Ansible . Shell . ProcessUtil ] : : GrantAccessToWindowStationAndDesktop ( $ username )
$ proc . Start ( ) | Out - Null # will always return $true for non shell-exec cases
$ payload_string = $ payload | ConvertTo - Json - Depth 99 - Compress
# push the execution payload over stdin
$ proc . StandardInput . WriteLine ( $ payload_string )
$ proc . StandardInput . Close ( )
$ stdout = $ stderr = [ string ] $ null
[ Ansible . Shell . ProcessUtil ] : : GetProcessOutput ( $ proc . StandardOutput , $ proc . StandardError , [ ref ] $ stdout , [ ref ] $ stderr ) | Out - Null
# TODO: decode CLIXML stderr output (and other streams?)
$ proc . WaitForExit ( ) | Out - Null
$ rc = $ proc . ExitCode
If ( $ rc - eq 0 ) {
$ stdout
$ stderr
}
Else {
Throw " failed, rc was $rc, stderr was $stderr, stdout was $stdout "
}
}
Finally {
Remove - Item $ temp - ErrorAction SilentlyContinue
}
}
''' # end become_wrapper
async_wrapper = br '''
Set - StrictMode - Version 2
$ ErrorActionPreference = " Stop "
# build exec_wrapper encoded command
# start powershell with breakaway running exec_wrapper encodedcommand
# stream payload to powershell with normal exec, but normal exec writes results to resultfile instead of stdout/stderr
# return asyncresult to controller
$ exec_wrapper = {
#Requires -Version 3.0
$ DebugPreference = " Continue "
$ ErrorActionPreference = " Stop "
Set - StrictMode - Version 2
function ConvertTo - HashtableFromPsCustomObject ( $ myPsObject ) {
$ output = @ { } ;
$ myPsObject | Get - Member - MemberType * Property | % {
$ val = $ myPsObject . ( $ _ . name ) ;
If ( $ val - is [ psobject ] ) {
$ val = ConvertTo - HashtableFromPsCustomObject $ val
}
$ output . ( $ _ . name ) = $ val
}
return $ output ;
}
# stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
# exec runspace, capture output, cleanup, return module output
$ json_raw = [ System . Console ] : : In . ReadToEnd ( )
If ( - not $ json_raw ) {
Write - Error " no input given " - Category InvalidArgument
}
$ payload = ConvertTo - HashtableFromPsCustomObject ( ConvertFrom - Json $ json_raw )
# TODO: handle binary modules
# TODO: handle persistence
$ actions = $ payload . actions
# pop 0th action as entrypoint
$ entrypoint = $ payload . ( $ actions [ 0 ] )
$ payload . actions = $ payload . actions [ 1. .99 ]
$ entrypoint = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ entrypoint ) )
# load the current action entrypoint as a module custom object with a Run method
$ entrypoint = New - Module - ScriptBlock ( [ scriptblock ] : : Create ( $ entrypoint ) ) - AsCustomObject
Set - Variable - Scope global - Name complex_args - Value $ payload [ " module_args " ] | Out - Null
# dynamically create/load modules
ForEach ( $ mod in $ payload . powershell_modules . GetEnumerator ( ) ) {
$ decoded_module = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ mod . Value ) )
New - Module - ScriptBlock ( [ scriptblock ] : : Create ( $ decoded_module ) ) - Name $ mod . Key | Import - Module | Out - Null
}
$ output = $ entrypoint . Run ( $ payload )
Write - Output $ output
} # end exec_wrapper
Function Run ( $ payload ) {
# BEGIN Ansible.Async native type definition
$ native_process_util = @ "
using Microsoft . Win32 . SafeHandles ;
using System ;
using System . ComponentModel ;
using System . Diagnostics ;
using System . IO ;
using System . Linq ;
using System . Runtime . InteropServices ;
using System . Text ;
using System . Threading ;
namespace Ansible . Async {
public static class NativeProcessUtil
{
[ DllImport ( " kernel32.dll " , SetLastError = true , CharSet = CharSet . Unicode , BestFitMapping = false ) ]
public static extern bool CreateProcess (
[ MarshalAs ( UnmanagedType . LPTStr ) ]
string lpApplicationName ,
StringBuilder lpCommandLine ,
IntPtr lpProcessAttributes ,
IntPtr lpThreadAttributes ,
bool bInheritHandles ,
uint dwCreationFlags ,
IntPtr lpEnvironment ,
[ MarshalAs ( UnmanagedType . LPTStr ) ]
string lpCurrentDirectory ,
STARTUPINFO lpStartupInfo ,
out PROCESS_INFORMATION lpProcessInformation ) ;
[ DllImport ( " kernel32.dll " , SetLastError = true , CharSet = CharSet . Unicode ) ]
public static extern uint SearchPath (
string lpPath ,
string lpFileName ,
string lpExtension ,
int nBufferLength ,
[ MarshalAs ( UnmanagedType . LPTStr ) ]
StringBuilder lpBuffer ,
out IntPtr lpFilePart ) ;
[ DllImport ( " kernel32.dll " ) ]
public static extern bool CreatePipe ( out IntPtr hReadPipe , out IntPtr hWritePipe , SECURITY_ATTRIBUTES lpPipeAttributes , uint nSize ) ;
[ DllImport ( " kernel32.dll " , SetLastError = true ) ]
public static extern IntPtr GetStdHandle ( StandardHandleValues nStdHandle ) ;
[ DllImport ( " kernel32.dll " , SetLastError = true ) ]
public static extern bool SetHandleInformation ( IntPtr hObject , HandleFlags dwMask , int dwFlags ) ;
public static string SearchPath ( string findThis )
{
StringBuilder sbOut = new StringBuilder ( 1024 ) ;
IntPtr filePartOut ;
if ( SearchPath ( null , findThis , null , sbOut . Capacity , sbOut , out filePartOut ) == 0 )
throw new FileNotFoundException ( " Couldn ' t locate " + findThis + " on path " ) ;
return sbOut . ToString ( ) ;
}
[ DllImport ( " kernel32.dll " , SetLastError = true ) ]
static extern SafeFileHandle OpenThread (
ThreadAccessRights dwDesiredAccess ,
bool bInheritHandle ,
int dwThreadId ) ;
[ DllImport ( " kernel32.dll " , SetLastError = true ) ]
static extern int ResumeThread ( SafeHandle hThread ) ;
public static void ResumeThreadById ( int threadId )
{
var threadHandle = OpenThread ( ThreadAccessRights . SUSPEND_RESUME , false , threadId ) ;
if ( threadHandle . IsInvalid )
throw new Exception ( String . Format ( " Thread ID {0} is invalid ( {1} ) " , threadId ,
new Win32Exception ( Marshal . GetLastWin32Error ( ) ) . Message ) ) ;
try
{
if ( ResumeThread ( threadHandle ) == - 1 )
throw new Exception ( String . Format ( " Thread ID {0} cannot be resumed ( {1} ) " , threadId ,
new Win32Exception ( Marshal . GetLastWin32Error ( ) ) . Message ) ) ;
}
finally
{
threadHandle . Dispose ( ) ;
}
}
public static void ResumeProcessById ( int pid )
{
var proc = Process . GetProcessById ( pid ) ;
/ / wait for at least one suspended thread in the process ( this handles possible slow startup race where
/ / primary thread of created - suspended process has not yet become runnable )
var retryCount = 0 ;
while ( ! proc . Threads . OfType < ProcessThread > ( ) . Any ( t = > t . ThreadState == System . Diagnostics . ThreadState . Wait & &
t . WaitReason == ThreadWaitReason . Suspended ) )
{
proc . Refresh ( ) ;
Thread . Sleep ( 50 ) ;
if ( retryCount > 100 )
throw new InvalidOperationException ( String . Format ( " No threads were suspended in target PID {0} after 5s " , pid ) ) ;
}
foreach ( var thread in proc . Threads . OfType < ProcessThread > ( ) . Where ( t = > t . ThreadState == System . Diagnostics . ThreadState . Wait & &
t . WaitReason == ThreadWaitReason . Suspended ) )
ResumeThreadById ( thread . Id ) ;
}
}
[ StructLayout ( LayoutKind . Sequential ) ]
public class SECURITY_ATTRIBUTES
{
public int nLength ;
public IntPtr lpSecurityDescriptor ;
public bool bInheritHandle = false ;
public SECURITY_ATTRIBUTES ( ) {
nLength = Marshal . SizeOf ( this ) ;
}
}
[ StructLayout ( LayoutKind . Sequential ) ]
public class STARTUPINFO
{
public Int32 cb ;
public IntPtr lpReserved ;
public IntPtr lpDesktop ;
public IntPtr lpTitle ;
public Int32 dwX ;
public Int32 dwY ;
public Int32 dwXSize ;
public Int32 dwYSize ;
public Int32 dwXCountChars ;
public Int32 dwYCountChars ;
public Int32 dwFillAttribute ;
public Int32 dwFlags ;
public Int16 wShowWindow ;
public Int16 cbReserved2 ;
public IntPtr lpReserved2 ;
public IntPtr hStdInput ;
public IntPtr hStdOutput ;
public IntPtr hStdError ;
public STARTUPINFO ( ) {
cb = Marshal . SizeOf ( this ) ;
}
}
[ StructLayout ( LayoutKind . Sequential ) ]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess ;
public IntPtr hThread ;
public int dwProcessId ;
public int dwThreadId ;
}
[ Flags ]
enum ThreadAccessRights : uint
{
SUSPEND_RESUME = 0x0002
}
[ Flags ]
public enum StartupInfoFlags : uint
{
USESTDHANDLES = 0x00000100
}
public enum StandardHandleValues : int
{
STD_INPUT_HANDLE = - 10 ,
STD_OUTPUT_HANDLE = - 11 ,
STD_ERROR_HANDLE = - 12
}
[ Flags ]
public enum HandleFlags : uint
{
None = 0 ,
INHERIT = 1
}
}
" @ # END Ansible.Async native type definition
# calculate the result path so we can include it in the worker payload
$ jid = $ payload . async_jid
$ local_jid = $ jid + " . " + $ pid
$ results_path = [ System . IO . Path ] : : Combine ( $ env : LOCALAPPDATA , " .ansible_async " , $ local_jid )
$ payload . async_results_path = $ results_path
[ System . IO . Directory ] : : CreateDirectory ( [ System . IO . Path ] : : GetDirectoryName ( $ results_path ) ) | Out - Null
Add - Type - TypeDefinition $ native_process_util
# FUTURE: create under new job to ensure all children die on exit?
# FUTURE: move these flags into C# enum
# start process suspended + breakaway so we can record the watchdog pid without worrying about a completion race
Set - Variable CREATE_BREAKAWAY_FROM_JOB - Value ( [ uint32 ] 0x01000000 ) - Option Constant
Set - Variable CREATE_SUSPENDED - Value ( [ uint32 ] 0x00000004 ) - Option Constant
Set - Variable CREATE_UNICODE_ENVIRONMENT - Value ( [ uint32 ] 0x000000400 ) - Option Constant
Set - Variable CREATE_NEW_CONSOLE - Value ( [ uint32 ] 0x00000010 ) - Option Constant
$ pstartup_flags = $ CREATE_BREAKAWAY_FROM_JOB - bor $ CREATE_UNICODE_ENVIRONMENT - bor $ CREATE_NEW_CONSOLE - bor $ CREATE_SUSPENDED
# execute the dynamic watchdog as a breakway process, which will in turn exec the module
$ si = New - Object Ansible . Async . STARTUPINFO
# setup stdin redirection, we'll leave stdout/stderr as normal
$ si . dwFlags = [ Ansible . Async . StartupInfoFlags ] : : USESTDHANDLES
$ si . hStdOutput = [ Ansible . Async . NativeProcessUtil ] : : GetStdHandle ( [ Ansible . Async . StandardHandleValues ] : : STD_OUTPUT_HANDLE )
$ si . hStdError = [ Ansible . Async . NativeProcessUtil ] : : GetStdHandle ( [ Ansible . Async . StandardHandleValues ] : : STD_ERROR_HANDLE )
$ stdin_read = $ stdin_write = 0
$ pipesec = New - Object Ansible . Async . SECURITY_ATTRIBUTES
$ pipesec . bInheritHandle = $ true
If ( - not [ Ansible . Async . NativeProcessUtil ] : : CreatePipe ( [ ref ] $ stdin_read , [ ref ] $ stdin_write , $ pipesec , 0 ) ) {
throw " Stdin pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()) "
}
If ( - not [ Ansible . Async . NativeProcessUtil ] : : SetHandleInformation ( $ stdin_write , [ Ansible . Async . HandleFlags ] : : INHERIT , 0 ) ) {
throw " Stdin handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()) "
}
$ si . hStdInput = $ stdin_read
# need to use a preamble-free version of UTF8Encoding
$ utf8_encoding = New - Object System . Text . UTF8Encoding @ ( $ false )
$ stdin_fs = New - Object System . IO . FileStream @ ( $ stdin_write , [ System . IO . FileAccess ] : : Write , $ true , 32768 )
$ stdin = New - Object System . IO . StreamWriter @ ( $ stdin_fs , $ utf8_encoding , 32768 )
$ pi = New - Object Ansible . Async . PROCESS_INFORMATION
$ encoded_command = [ Convert ] : : ToBase64String ( [ System . Text . Encoding ] : : Unicode . GetBytes ( $ exec_wrapper . ToString ( ) ) )
# FUTURE: direct cmdline CreateProcess path lookup fails- this works but is sub-optimal
$ exec_cmd = [ Ansible . Async . NativeProcessUtil ] : : SearchPath ( " powershell.exe " )
$ exec_args = New - Object System . Text . StringBuilder @ ( " ` " $ exec_cmd ` " -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command " )
# TODO: use proper Win32Exception + error
If ( - not [ Ansible . Async . NativeProcessUtil ] : : CreateProcess ( $ exec_cmd , $ exec_args ,
[ IntPtr ] : : Zero , [ IntPtr ] : : Zero , $ true , $ pstartup_flags , [ IntPtr ] : : Zero , $ env : windir , $ si , [ ref ] $ pi ) ) {
#throw New-Object System.ComponentModel.Win32Exception
throw " Worker creation failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()) "
}
# FUTURE: watch process for quick exit, capture stdout/stderr and return failure
$ watchdog_pid = $ pi . dwProcessId
[ Ansible . Async . NativeProcessUtil ] : : ResumeProcessById ( $ watchdog_pid )
# once process is resumed, we can send payload over stdin
$ payload_string = $ payload | ConvertTo - Json - Depth 99 - Compress
$ stdin . WriteLine ( $ payload_string )
$ stdin . Close ( )
# populate initial results before we resume the process to avoid result race
$ result = @ {
started = 1 ;
finished = 0 ;
results_file = $ results_path ;
ansible_job_id = $ local_jid ;
_ansible_suppress_tmpdir_delete = $ true ;
ansible_async_watchdog_pid = $ watchdog_pid
}
$ result_json = ConvertTo - Json $ result
Set - Content $ results_path - Value $ result_json
return $ result_json
}
''' # end async_wrapper
async_watchdog = br '''
Set - StrictMode - Version 2
$ ErrorActionPreference = " Stop "
Add - Type - AssemblyName System . Web . Extensions
Function Log {
Param (
[ string ] $ msg
)
If ( Get - Variable - Name log_path - ErrorAction SilentlyContinue ) {
Add - Content $ log_path $ msg
}
}
Function Deserialize - Json {
Param (
[ Parameter ( ValueFromPipeline = $ true ) ]
[ string ] $ json
)
# FUTURE: move this into module_utils/powershell.ps1 and use for everything (sidestep PSCustomObject issues)
# FUTURE: won't work w/ Nano Server/.NET Core- fallback to DataContractJsonSerializer (which can't handle dicts on .NET 4.0)
Log " Deserializing:`n$json "
$ jss = New - Object System . Web . Script . Serialization . JavaScriptSerializer
return $ jss . DeserializeObject ( $ json )
}
Function Write - Result {
Param (
[ hashtable ] $ result ,
[ string ] $ resultfile_path
)
$ result | ConvertTo - Json | Set - Content - Path $ resultfile_path
}
Function Run ( $ payload ) {
$ actions = $ payload . actions
# pop 0th action as entrypoint
$ entrypoint = $ payload . ( $ actions [ 0 ] )
$ entrypoint = [ System . Text . Encoding ] : : UTF8 . GetString ( [ System . Convert ] : : FromBase64String ( $ entrypoint ) )
$ payload . actions = $ payload . actions [ 1. .99 ]
$ resultfile_path = $ payload . async_results_path
$ max_exec_time_sec = $ payload . async_timeout_sec
Log " deserializing existing resultfile args "
# read in existing resultsfile to merge w/ module output (it should be written by the time we're unsuspended and running)
$ result = Get - Content $ resultfile_path - Raw | Deserialize - Json
Log " deserialized result is $($result | Out-String) "
Log " creating runspace "
$ rs = [ runspacefactory ] : : CreateRunspace ( )
$ rs . Open ( )
Log " creating Powershell object "
$ job = [ powershell ] : : Create ( )
$ job . Runspace = $ rs
$ job . AddScript ( $ entrypoint ) | Out - Null
$ job . AddStatement ( ) . AddCommand ( " Run " ) . AddArgument ( $ payload ) | Out - Null
Log " job BeginInvoke() "
$ job_asyncresult = $ job . BeginInvoke ( )
Log " waiting $max_exec_time_sec seconds for job to complete "
$ signaled = $ job_asyncresult . AsyncWaitHandle . WaitOne ( $ max_exec_time_sec * 1000 )
$ result [ " finished " ] = 1
If ( $ job_asyncresult . IsCompleted ) {
Log " job completed, calling EndInvoke() "
$ job_output = $ job . EndInvoke ( $ job_asyncresult )
$ job_error = $ job . Streams . Error
Log " raw module stdout: \r \n $job_output "
If ( $ job_error ) {
Log " raw module stderr: \r \n $job_error "
}
# write success/output/error to result object
# TODO: cleanse leading/trailing junk
Try {
$ module_result = Deserialize - Json $ job_output
# TODO: check for conflicting keys
$ result = $ result + $ module_result
}
Catch {
$ excep = $ _
$ result . failed = $ true
$ result . msg = " failed to parse module output: $excep "
}
# TODO: determine success/fail, or always include stderr if nonempty?
Write - Result $ result $ resultfile_path
Log " wrote output to $resultfile_path "
}
Else {
$ job . BeginStop ( $ null , $ null ) | Out - Null # best effort stop
# write timeout to result object
$ result . failed = $ true
$ result . msg = " timed out waiting for module completion "
Write - Result $ result $ resultfile_path
Log " wrote timeout to $resultfile_path "
}
# in the case of a hung pipeline, this will cause the process to stay alive until it's un-hung...
#$rs.Close() | Out-Null
}
''' # end async_watchdog
class ShellModule ( object ) :
class ShellModule ( object ) :
@ -51,6 +890,15 @@ class ShellModule(object):
# env provider's limitations don't appear to be documented.
# env provider's limitations don't appear to be documented.
safe_envkey = re . compile ( r ' ^[ \ d \ w_] { 1,255}$ ' )
safe_envkey = re . compile ( r ' ^[ \ d \ w_] { 1,255}$ ' )
# TODO: implement module transfer
# TODO: implement #Requires -Modules parser/locator
# TODO: add raw failure + errcode preservation (all success right now)
# TODO: add KEEP_REMOTE_FILES support + debug wrapper dump
# TODO: add become support
# TODO: add binary module support
# TODO: figure out non-pipelined path (or force pipelining)
def assert_safe_env_key ( self , key ) :
def assert_safe_env_key ( self , key ) :
if not self . safe_envkey . match ( key ) :
if not self . safe_envkey . match ( key ) :
raise AnsibleError ( " Invalid PowerShell environment key: %s " % key )
raise AnsibleError ( " Invalid PowerShell environment key: %s " % key )
@ -164,6 +1012,12 @@ class ShellModule(object):
return self . _encode_script ( script )
return self . _encode_script ( script )
def build_module_command ( self , env_string , shebang , cmd , arg_path = None , rm_tmp = None ) :
def build_module_command ( self , env_string , shebang , cmd , arg_path = None , rm_tmp = None ) :
# pipelining bypass
if cmd == ' ' :
return ' '
# non-pipelining
cmd_parts = shlex . split ( to_bytes ( cmd ) , posix = False )
cmd_parts = shlex . split ( to_bytes ( cmd ) , posix = False )
cmd_parts = map ( to_text , cmd_parts )
cmd_parts = map ( to_text , cmd_parts )
if shebang and shebang . lower ( ) == ' #!powershell ' :
if shebang and shebang . lower ( ) == ' #!powershell ' :
@ -218,6 +1072,9 @@ class ShellModule(object):
script = ' %s \n Finally { %s } ' % ( script , rm_cmd )
script = ' %s \n Finally { %s } ' % ( script , rm_cmd )
return self . _encode_script ( script , preserve_rc = False )
return self . _encode_script ( script , preserve_rc = False )
def wrap_for_exec ( self , cmd ) :
return ' & %s ' % cmd
def _unquote ( self , value ) :
def _unquote ( self , value ) :
''' Remove any matching quotes that wrap the given value. '''
''' Remove any matching quotes that wrap the given value. '''
value = to_text ( value or ' ' )
value = to_text ( value or ' ' )