Windows async module support (#4710)

Powershell impls of async_wrapper, async_status- associated tests and async action changes are in https://github.com/ansible/ansible/pull/17400.
This commit is contained in:
Matt Davis 2016-09-06 13:26:40 -07:00 committed by Matt Clay
parent 909e62b25b
commit 6099e6d19e
2 changed files with 427 additions and 0 deletions

View file

@ -0,0 +1,69 @@
#!powershell
# This file is part of Ansible
#
# 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/>.
# WANT_JSON
# POWERSHELL_COMMON
$results = @{changed=$false}
$parsed_args = Parse-Args $args
$jid = Get-AnsibleParam $parsed_args "jid" -failifempty $true -resultobj $results
$mode = Get-AnsibleParam $parsed_args "mode" -Default "status" -ValidateSet "status","cleanup"
# setup logging directory
$log_path = [System.IO.Path]::Combine($env:LOCALAPPDATA, ".ansible_async", $jid)
If(-not $(Test-Path $log_path))
{
Fail-Json @{ansible_job_id=$jid; started=1; finished=1} "could not find job"
}
If($mode -eq "cleanup") {
Remove-Item $log_path -Recurse
Exit-Json @{ansible_job_id=$jid; erased=$log_path}
}
# NOT in cleanup mode, assume regular status mode
# no remote kill mode currently exists, but probably should
# consider log_path + ".pid" file and also unlink that above
$data = $null
Try {
$data_raw = Get-Content $log_path
# TODO: move this into module_utils/powershell.ps1?
$jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer
$data = $jss.DeserializeObject($data_raw)
}
Catch {
If(-not $data_raw) {
# file not written yet? That means it is running
Exit-Json @{results_file=$log_path; ansible_job_id=$jid; started=1; finished=0}
}
Else {
Fail-Json @{ansible_job_id=$jid; results_file=$log_path; started=1; finished=1} "Could not parse job output: $data"
}
}
If (-not $data.ContainsKey("started")) {
$data['finished'] = 1
$data['ansible_job_id'] = $jid
}
ElseIf (-not $data.ContainsKey("finished")) {
$data['finished'] = 0
}
Exit-Json $data

View file

@ -0,0 +1,358 @@
#!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
)
$native_process_util = @"
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
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);
foreach(var thread in proc.Threads.OfType<ProcessThread>().Where(t => 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 = @"
<<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))
$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