2016-09-06 22:26:40 +02:00
|
|
|
#!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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2016-10-24 08:16:35 +02:00
|
|
|
# BEGIN Ansible.Async native type definition
|
2016-09-06 22:26:40 +02:00
|
|
|
$native_process_util = @"
|
|
|
|
using Microsoft.Win32.SafeHandles;
|
|
|
|
using System;
|
|
|
|
using System.ComponentModel;
|
|
|
|
using System.Diagnostics;
|
2016-10-24 08:16:35 +02:00
|
|
|
using System.IO;
|
2016-09-06 22:26:40 +02:00
|
|
|
using System.Linq;
|
|
|
|
using System.Runtime.InteropServices;
|
2016-10-24 08:16:35 +02:00
|
|
|
using System.Text;
|
2016-09-07 01:51:17 +02:00
|
|
|
using System.Threading;
|
2016-09-06 22:26:40 +02:00
|
|
|
|
|
|
|
namespace Ansible.Async {
|
|
|
|
|
|
|
|
public static class NativeProcessUtil
|
|
|
|
{
|
2016-10-24 08:16:35 +02:00
|
|
|
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
|
|
public static extern bool CreateProcess(
|
|
|
|
string lpApplicationName,
|
|
|
|
string lpCommandLine,
|
|
|
|
IntPtr lpProcessAttributes,
|
|
|
|
IntPtr lpThreadAttributes,
|
|
|
|
bool bInheritHandles,
|
|
|
|
uint dwCreationFlags,
|
|
|
|
IntPtr lpEnvironment,
|
|
|
|
string lpCurrentDirectory,
|
|
|
|
[In] ref 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);
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2016-09-06 22:26:40 +02:00
|
|
|
[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);
|
|
|
|
|
2016-09-07 01:51:17 +02:00
|
|
|
// 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))
|
2016-09-06 22:26:40 +02:00
|
|
|
ResumeThreadById(thread.Id);
|
|
|
|
}
|
|
|
|
}
|
2016-10-24 08:16:35 +02:00
|
|
|
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
|
|
public struct STARTUPINFO
|
|
|
|
{
|
|
|
|
public Int32 cb;
|
|
|
|
public string lpReserved;
|
|
|
|
public string lpDesktop;
|
|
|
|
public string 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
[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
|
|
|
|
}
|
2016-09-06 22:26:40 +02:00
|
|
|
}
|
2016-10-24 08:16:35 +02:00
|
|
|
"@ # END Ansible.Async native type definition
|
2016-09-06 22:26:40 +02:00
|
|
|
|
|
|
|
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 = @"
|
|
|
|
<<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>>", $jsonargs)
|
|
|
|
|
|
|
|
$encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_script))
|
|
|
|
|
2016-10-24 08:16:35 +02:00
|
|
|
# FUTURE: create under new job to ensure all children die on exit?
|
2016-09-06 22:26:40 +02:00
|
|
|
|
2016-10-24 08:16:35 +02:00
|
|
|
# FUTURE: move these flags into C# enum
|
2016-09-06 22:26:40 +02:00
|
|
|
# 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
|
2016-10-24 08:16:35 +02:00
|
|
|
Set-Variable CREATE_UNICODE_ENVIRONMENT -Value ([uint32]0x000000400) -Option Constant
|
|
|
|
Set-Variable CREATE_NEW_CONSOLE -Value ([uint32]0x00000010) -Option Constant
|
2016-09-06 22:26:40 +02:00
|
|
|
|
2016-10-24 08:16:35 +02:00
|
|
|
$pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB -bor $CREATE_UNICODE_ENVIRONMENT -bor $CREATE_NEW_CONSOLE
|
2016-09-06 22:26:40 +02:00
|
|
|
If($start_suspended) {
|
|
|
|
$pstartup_flags = $pstartup_flags -bor $CREATE_SUSPENDED
|
|
|
|
}
|
|
|
|
|
|
|
|
# execute the dynamic watchdog as a breakway process, which will in turn exec the module
|
2016-10-24 08:16:35 +02:00
|
|
|
$si = New-Object Ansible.Async.STARTUPINFO
|
|
|
|
$si.cb = [System.Runtime.InteropServices.Marshal]::SizeOf([type][Ansible.Async.STARTUPINFO])
|
|
|
|
|
|
|
|
$pi = New-Object Ansible.Async.PROCESS_INFORMATION
|
2016-09-06 22:26:40 +02:00
|
|
|
|
2016-10-24 08:16:35 +02:00
|
|
|
# FUTURE: direct cmdline CreateProcess path lookup fails- this works but is sub-optimal
|
|
|
|
$exec_cmd = [Ansible.Async.NativeProcessUtil]::SearchPath("powershell.exe")
|
|
|
|
$exec_args = "`"$exec_cmd`" -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command"
|
|
|
|
|
2016-11-18 18:57:51 +01:00
|
|
|
If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, [IntPtr]::Zero, [IntPtr]::Zero, $false, $pstartup_flags, [IntPtr]::Zero, $env:windir, [ref]$si, [ref]$pi)) {
|
2016-10-24 08:16:35 +02:00
|
|
|
#throw New-Object System.ComponentModel.Win32Exception
|
|
|
|
throw "create bang $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
|
|
|
}
|
2016-10-02 17:54:20 +02:00
|
|
|
|
2016-10-24 08:16:35 +02:00
|
|
|
$watchdog_pid = $pi.dwProcessId
|
2016-09-06 22:26:40 +02:00
|
|
|
|
|
|
|
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;
|
2016-12-14 18:52:18 +01:00
|
|
|
_ansible_suppress_tmpdir_delete=$true;
|
2016-09-06 22:26:40 +02:00
|
|
|
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
|