#!powershell # This file is part of Ansible # # Copyright (c)2016, Matt Davis # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . Param( [string]$jid, [int]$max_exec_time_sec, [string]$module_path, [string]$argfile_path, [switch]$preserve_tmp ) # WANT_JSON # POWERSHELL_COMMON Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" Function Start-Watchdog { Param( [string]$module_tempdir, [string]$module_path, [int]$max_exec_time_sec, [string]$resultfile_path, [string]$argfile_path, [switch]$preserve_tmp, [switch]$start_suspended ) $native_process_util = @" using Microsoft.Win32.SafeHandles; using System; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; namespace Ansible.Async { public static class NativeProcessUtil { [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); [Flags] enum ThreadAccessRights : uint { SUSPEND_RESUME = 0x0002 } 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().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().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait && t.WaitReason == ThreadWaitReason.Suspended)) ResumeThreadById(thread.Id); } } } "@ Add-Type -TypeDefinition $native_process_util $watchdog_script = { Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" Function Log { Param( [string]$msg ) If(Get-Variable -Name log_path -ErrorAction SilentlyContinue) { Add-Content $log_path $msg } } Add-Type -AssemblyName System.Web.Extensions # -EncodedCommand won't allow us to pass args, so they have to be templated into the script $jsonargs = @" <> "@ 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 { [hashtable]$result, [string]$resultfile_path $result | ConvertTo-Json | Set-Content -Path $resultfile_path } Function Exec-Module { Param( [string]$module_tempdir, [string]$module_path, [int]$max_exec_time_sec, [string]$resultfile_path, [string]$argfile_path, [switch]$preserve_tmp ) Log "in watchdog exec" Try { 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() $rs.SessionStateProxy.Path.SetLocation($module_tempdir) | Out-Null Log "creating Powershell object" $job = [powershell]::Create() $job.Runspace = $rs Log "adding scripts" if($module_path.EndsWith(".ps1")) { $job.AddScript($module_path) | Out-Null } else { $job.AddCommand($module_path) | Out-Null $job.AddArgument($argfile_path) | 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.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" } $rs.Close() | Out-Null } Catch { $excep = $_ $result = @{failed=$true; msg="module execution failed: $($excep.ToString())`n$($excep.InvocationInfo.PositionMessage)"} Write-Result $result $resultfile_path } Finally { If(-not $preserve_tmp -and $module_tempdir -imatch "-tmp-") { Try { Log "deleting tempdir, cwd is $(Get-Location)" Set-Location $env:USERPROFILE $res = Remove-Item $module_tempdir -recurse 2>&1 Log "delete output was $res" } Catch { $excep = $_ Log "error deleting tempdir: $excep" } } Else { Log "skipping tempdir deletion" } } } Try { Log "deserializing args" # deserialize the JSON args that should've been templated in before execution $ext_args = Deserialize-Json $jsonargs Log "exec module" Exec-Module @ext_args Log "exec done" } Catch { $excep = $_ Log $excep } } $bp = [hashtable] $MyInvocation.BoundParameters # convert switch types to bool so they'll serialize as simple bools $bp["preserve_tmp"] = [bool]$bp["preserve_tmp"] $bp["start_suspended"] = [bool]$bp["start_suspended"] # serialize this function's args to JSON so we can template them verbatim into the script(block) $jsonargs = $bp | ConvertTo-Json $raw_script = $watchdog_script.ToString() $raw_script = $raw_script.Replace("<>", $jsonargs) $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_script)) $exec_path = "powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" # FUTURE: create under new job to ensure we kill all children on exit? # 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 $pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB If($start_suspended) { $pstartup_flags = $pstartup_flags -bor $CREATE_SUSPENDED } $pstartup = ([wmiclass]"Win32_ProcessStartup") $pstartup.Properties['CreateFlags'].Value = $pstartup_flags # execute the dynamic watchdog as a breakway process, which will in turn exec the module # FUTURE: use CreateProcess + stream redirection to watch for/return quick watchdog failures? $result = $([wmiclass]"Win32_Process").Create($exec_path, $null, $pstartup) $watchdog_pid = $result.ProcessId return $watchdog_pid } $local_jid = $jid + "." + $pid $results_path = [System.IO.Path]::Combine($env:LOCALAPPDATA, ".ansible_async", $local_jid) [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null $watchdog_args = @{ module_tempdir=$([System.IO.Path]::GetDirectoryName($module_path)); module_path=$module_path; max_exec_time_sec=$max_exec_time_sec; resultfile_path=$results_path; argfile_path=$argfile_path; start_suspended=$true; } If($preserve_tmp) { $watchdog_args["preserve_tmp"] = $true } # start watchdog/module-exec $watchdog_pid = Start-Watchdog @watchdog_args # 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; _suppress_tmpdir_delete=$true; ansible_async_watchdog_pid=$watchdog_pid } $result_json = ConvertTo-Json $result Set-Content $results_path -Value $result_json [Ansible.Async.NativeProcessUtil]::ResumeProcessById($watchdog_pid) return $result_json