summaryrefslogtreecommitdiff
path: root/windows
diff options
context:
space:
mode:
authorMatt Davis <nitzmahone@users.noreply.github.com>2016-09-06 13:26:40 -0700
committerGitHub <noreply@github.com>2016-09-06 13:26:40 -0700
commita746eff954e0fb2985784ce4f12a9017301480a2 (patch)
tree272ee4d3ef3ee50bff148423239baef894b453f9 /windows
parent8e0cc9d370134b1b6989cde478d3710dc2c4cfba (diff)
downloadansible-modules-core-a746eff954e0fb2985784ce4f12a9017301480a2.tar.gz
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.
Diffstat (limited to 'windows')
-rw-r--r--windows/async_status.ps169
-rw-r--r--windows/async_wrapper.ps1358
2 files changed, 427 insertions, 0 deletions
diff --git a/windows/async_status.ps1 b/windows/async_status.ps1
new file mode 100644
index 00000000..efde748f
--- /dev/null
+++ b/windows/async_status.ps1
@@ -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
diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1
new file mode 100644
index 00000000..03ba5d72
--- /dev/null
+++ b/windows/async_wrapper.ps1
@@ -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