Repository: ansible/ansible
Pull request: win_exec: refactor PS exec runner
##### SUMMARY
This is a bib refactor of the Windows exec runner to add the following features;

* Split out the actual PowerShell to separate files instead of embedding them inside Python, adds more features like static analysis and test mapping
* ~~Added C# modules, still a basic implementation and requires further tweaking to be really useful~~
* Added C# module_utils that can be referenced by ~~both C# and~~ PowerShell modules, these are automatically loaded in the process so no further steps are need by the module to start using it
* Added global functions for use in the exec wrapper to remove duplication of code and make things simpler across the board

I also tried to increase the error reporting for PowerShell errors both in the exec wrappers and module execution. What I've done is;

* Use the same exec_wrapper script when starting a new process (become + async) so those processes are all in the same state as a normal WinRM process from Ansible
* Added a `Write-AnsibleLog` function that can be used to write various exec log states to a file if the user has manually set `ANSIBLE_DEBUG_EXEC` to a path to a log file. This gives greater visibility on what the exec runner is actually doing
* Better error checks if something goes wrong in the wrapper side, will actually return a JSON with the error details instead of outputting it to the stderr stream
* ~~Added a trap to each module execution to catch any uncaught errors and output a JSON with the error details inside, including a Stacktrace~~
* Better error handling when invoking the PowerShell pipeline that runs the module

Fixes https://github.com/ansible/ansible/issues/28402

This is the first step towards achieving https://github.com/ansible/ansible/pull/44705. I wanted to introduce the changes in smaller batches rather one massive PR.

##### ISSUE TYPE
- Feature Pull Request

##### COMPONENT NAME
Windows

##### ANSIBLE VERSION
```paste below
devel
```

##### ADDITIONAL INFO

As an example here is the output of `win_ping` with `data: crash` in devel (the error stays the same even when running with `-vvv`

```
SERVER2016.domain.local | FAILED! => {
    "changed": false,
    "module_stderr": "boom\r\nAt line:14 char:5\r\n+     throw \"boom\"\r\n+     ~~~~~~~~~~~~\r\n    + CategoryInfo          : OperationStopped: (boom:String) [], RuntimeException\r\n    + FullyQualifiedErrorId : boom\r\n \r\n\r\n",                                                  
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 1
}
```

With the changes in this PR, it now turns into this

```
(ansible-py36) jborean:~/dev/module-tester$ ansible '2016' -m win_ping -a data=crash
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: at <ScriptBlock>, <No file>: line 14
SERVER2016.domain.local | FAILED! => {
    "changed": false,
    "msg": "Unhandled exception while executing module: boom"
}

(ansible-py36) jborean:~/dev/module-tester$ ansible '2016' -m win_ping -a data=crash -vvv
ansible 2.8.0.dev0 (win-exec-wrapper df1a5ab49c) last updated 2018/09/08 11:37:06 (GMT +1000)
  config file = /Users/jborean/dev/module-tester/ansible.cfg
  configured module search path = ['/Users/jborean/dev/module-tester/library']
  ansible python module location = /Users/jborean/dev/ansible/lib/ansible
  executable location = /Users/jborean/dev/ansible/bin/ansible
  python version = 3.6.4 (default, Jan 18 2018, 12:35:18) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]
Using /Users/jborean/dev/module-tester/ansible.cfg as config file
Parsed /Users/jborean/dev/module-tester/inventory.ini inventory source with ini plugin
META: ran handlers
Using module file /Users/jborean/dev/ansible/lib/ansible/modules/windows/win_ping.ps1
<SERVER2016.domain.local> ESTABLISH WINRM CONNECTION FOR USER: vagrant on PORT 5986 TO SERVER2016.domain.local
EXEC (via pipeline wrapper)
The full traceback is:
boom
At line:14 char:5
+     throw "boom"
+     ~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (boom:String) [], RuntimeException
    + FullyQualifiedErrorId : boom

ScriptStackTrace:
at <ScriptBlock>, <No file>: line 14
SERVER2016.domain.local | FAILED! => {
    "changed": false,
    "msg": "Unhandled exception while executing module: boom"
}
```

Without `-vvv` we get a hint saying the traceback can be access with `-vvv`, with this level we get the actual error not JSON encoded but also a stacktrace to identify the line it failed on.

Another example is when a module calls a PowerShell cmdlet and doesn't catch any exceptions, here is the output from devel Ansible

```
SERVER2016.domain.local | FAILED! => {
    "changed": false,
    "module_stderr": "Exception calling \"Run\" with \"1\" argument(s): \"Exception calling \"Invoke\" with \"0\" argument(s): \"The running command \r\nstopped because the preference variable \"ErrorActionPreference\" or common parameter is set to Stop: Requested registry \r\naccess is not allowed.\"\"\r\nAt line:65 char:5\r\n+     $output = $entrypoint.Run($payload)\r\n+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n    + CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordException\r\n    + FullyQualifiedErrorId : ScriptMethodRuntimeException\r\n \r\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 1
}
```

With these changes we now get

```
(ansible-py36) jborean:~/dev/module-tester$ ansible '2016' -m win_acl -a "path=HKLM:/Security user=SYSTEM rights=rights type=allow"
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: at <ScriptBlock>, <No file>: line 104
SERVER2016.domain.local | FAILED! => {
    "changed": false,
    "msg": "Unhandled exception while executing module: Requested registry access is not allowed."
}

(ansible-py36) jborean:~/dev/module-tester$ ansible '2016' -m win_acl -a "path=HKLM:/Security user=SYSTEM rights=rights type=allow" -vvv
ansible 2.8.0.dev0 (win-exec-wrapper df1a5ab49c) last updated 2018/09/08 11:37:06 (GMT +1000)
  config file = /Users/jborean/dev/module-tester/ansible.cfg
  configured module search path = ['/Users/jborean/dev/module-tester/library']
  ansible python module location = /Users/jborean/dev/ansible/lib/ansible
  executable location = /Users/jborean/dev/ansible/bin/ansible
  python version = 3.6.4 (default, Jan 18 2018, 12:35:18) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]
Using /Users/jborean/dev/module-tester/ansible.cfg as config file
Parsed /Users/jborean/dev/module-tester/inventory.ini inventory source with ini plugin
META: ran handlers
Using module file /Users/jborean/dev/ansible/lib/ansible/modules/windows/win_acl.ps1
<SERVER2016.domain.local> ESTABLISH WINRM CONNECTION FOR USER: vagrant on PORT 5986 TO SERVER2016.domain.local
EXEC (via pipeline wrapper)
The full traceback is:
Requested registry access is not allowed.
At line:104 char:5
+ If (Test-Path -Path $path -PathType Leaf) {
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : PermissionDenied: (HKEY_LOCAL_MACHINE\Security:String) [Test-Path], SecurityException
    + FullyQualifiedErrorId : System.Security.SecurityException,Microsoft.PowerShell.Commands.TestPathCommand

ScriptStackTrace:
at <ScriptBlock>, <No file>: line 104
SERVER2016.domain.local | FAILED! => {
    "changed": false,
    "msg": "Unhandled exception while executing module: Requested registry access is not allowed."
}
```

We no longer get the extra info around Run, Invoke and ErrorActionPreference andjust get the error at hand with a hint that `-vvv` gives us more info. With `-vvv` we once again get the location of the error as well as the stacktrace.

Problem statement:
Improve Windows module exception output
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Feature Idea

##### COMPONENT NAME
Windows modules

##### ANSIBLE VERSION
v2.4

##### SUMMARY
So if a Windows module raises an exception, the output is hard to read because it is encapsulated inside of a weird message most users wouldn't know or care about:

```
fatal: [some-system]: FAILED! => {
    "changed": false,
    "failed": true,
    "module_stderr": "Exception calling \"Run\" with \"1\" argument(s): \"Exception calling \"Invoke\" with \r\n\"0\" argument(s): \"The running command stopped because the preference variable \r\n\"ErrorActionPreference\" or common parameter is set to Stop: Computer 'some-system' \r\nfailed to join domain 'some.domain.lab' from its current workgroup \r\n'WORKGROUP' with following error message: Unable to update the password. The \r\nvalue provided as the current password is incorrect.\"\"\r\nAt line:47 char:5\r\n+     $output = $entrypoint.Run($payload)\r\n+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n    + CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordE \r\n   xception\r\n    + FullyQualifiedErrorId : ScriptMethodRuntimeException\r\n \r\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE",
    "rc": 1
}
```
It may look easier in your browser as you can horizontally scroll, but in a Unix terminal it's one large blob from hell :-)

It would be nice if we could get this translated into:
```
fatal: [some-system]: FAILED! => {
    "changed": false,
    "failed": true,
    "module_stderr": "Computer 'some-system' failed to join domain 'some.domain.lab' from its current workgroup 'WORKGROUP' with following error message: Unable to update the password. The value provided as the current password is incorrect.",
    "module_stdout": "",
    "msg": "MODULE FAILURE",
    "rc": 1
}
```

Hints from issue discussion:
I think things would be improved if the \r\n could be interpretted as new-lines.  However skippy seems to be wanting to output something that mostly looks like json.
I use a tweaked version of the minimal callback plugin which improves things (its tweaked because minimal doesn't display task names, without which I am completely lost!).  I would contribute but I am not sure it works fully.
@jhawkesworth I really think we should fix this at the core, so not the representation level.

The `"Exception calling \"Run\" with \"1\" argument(s): \"Exception calling \"Invoke\" with \r\n\"0\" argument(s): \"The running command stopped because the preference variable \r\n\"ErrorActionPreference\" or common parameter is set to Stop:"` is part of our implementation (our wrapping of things) and lost on most of our users. We should get rid of it altogether.
@dagwieers - you make a good point.  I have kind of stopped seeing that part of the message now - I'm too familiar with how things are at the moment.
cc @dharmabumstead
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I guess this is just a matter of catching potential places that can throw exceptions like this:
```powershell
    Try {
        Some-Statement
    } Catch {
        Fail-Json -obj $result -message $_.Exception.Message
    }
```

However if we could clean up the exception message from the wrapper, blank statements like the above would no longer be necessary. Only very specific messages adding info would still be useful.

Commit message: removed C# module support to focus on just error msg improvement

Patch to review:
diff --git a/MANIFEST.in b/MANIFEST.in
index ffc7cf0a14afed..27a901b6db3758 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,6 +6,8 @@ include requirements.txt
 include .coveragerc
 include examples/hosts
 include examples/ansible.cfg
+recursive-include lib/ansible/executor/powershell *
+recursive-include lib/ansible/module_utils/csharp *
 recursive-include lib/ansible/module_utils/powershell *
 recursive-include lib/ansible/modules *
 recursive-include lib/ansible/galaxy/data *
diff --git a/changelogs/fragments/windows-exec-changes.yaml b/changelogs/fragments/windows-exec-changes.yaml
new file mode 100644
index 00000000000000..bc3966f448fd9e
--- /dev/null
+++ b/changelogs/fragments/windows-exec-changes.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+- include better error handling for Windows errors to help with debugging module errors
diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py
index ed0a231643b5b8..f384b639c882e7 100644
--- a/lib/ansible/executor/module_common.py
+++ b/lib/ansible/executor/module_common.py
@@ -28,17 +28,15 @@
 import os
 import shlex
 import zipfile
-import random
 import re
-from distutils.version import LooseVersion
 from io import BytesIO
 
 from ansible.release import __version__, __author__
 from ansible import constants as C
 from ansible.errors import AnsibleError
+from ansible.executor.powershell import module_manifest as ps_manifest
 from ansible.module_utils._text import to_bytes, to_text, to_native
-from ansible.plugins.loader import module_utils_loader, ps_module_utils_loader
-from ansible.plugins.shell.powershell import async_watchdog, async_wrapper, become_wrapper, leaf_exec, exec_wrapper
+from ansible.plugins.loader import module_utils_loader
 # Must import strategy and use write_locks from there
 # If we import write_locks directly then we end up binding a
 # variable to the object and then it never gets updated.
@@ -430,74 +428,6 @@ def visit_ImportFrom(self, node):
         self.generic_visit(node)
 
 
-class PSModuleDepFinder():
-
-    def __init__(self):
-        self.modules = dict()
-        self.ps_version = None
-        self.os_version = None
-        self.become = False
-
-        self._re_module = re.compile(to_bytes(r'(?i)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)'))
-        self._re_ps_version = re.compile(to_bytes(r'(?i)^#requires\s+\-version\s+([0-9]+(\.[0-9]+){0,3})$'))
-        self._re_os_version = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-osversion\s+([0-9]+(\.[0-9]+){0,3})$'))
-        self._re_become = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-become$'))
-
-    def scan_module(self, module_data):
-        lines = module_data.split(b'\n')
-        module_utils = set()
-
-        for line in lines:
-            module_util_match = self._re_module.match(line)
-            if module_util_match:
-                # tolerate windows line endings by stripping any remaining newline chars
-                module_util_name = to_text(module_util_match.group(1).rstrip())
-                if module_util_name not in self.modules.keys():
-                    module_utils.add(module_util_name)
-
-            ps_version_match = self._re_ps_version.match(line)
-            if ps_version_match:
-                self._parse_version_match(ps_version_match, "ps_version")
-
-            os_version_match = self._re_os_version.match(line)
-            if os_version_match:
-                self._parse_version_match(os_version_match, "os_version")
-
-            # once become is set, no need to keep on checking recursively
-            if not self.become:
-                become_match = self._re_become.match(line)
-                if become_match:
-                    self.become = True
-
-        # recursively drill into each Requires to see if there are any more
-        # requirements
-        for m in set(module_utils):
-            m = to_text(m)
-            mu_path = ps_module_utils_loader.find_plugin(m, ".psm1")
-            if not mu_path:
-                raise AnsibleError('Could not find imported module support code for \'%s\'.' % m)
-
-            module_util_data = to_bytes(_slurp(mu_path))
-            self.modules[m] = module_util_data
-            self.scan_module(module_util_data)
-
-    def _parse_version_match(self, match, attribute):
-        new_version = to_text(match.group(1)).rstrip()
-
-        # PowerShell cannot cast a string of "1" to Version, it must have at
-        # least the major.minor for it to be valid so we append 0
-        if match.group(2) is None:
-            new_version = "%s.0" % new_version
-
-        existing_version = getattr(self, attribute, None)
-        if existing_version is None:
-            setattr(self, attribute, new_version)
-        else:
-            # determine which is the latest version and set that
-            if LooseVersion(new_version) > LooseVersion(existing_version):
-                setattr(self, attribute, new_version)
-
-
 def _slurp(path):
     if not os.path.exists(path):
         raise AnsibleError("imported module support code does not exist at %s" % os.path.abspath(path))
@@ -688,69 +618,6 @@ def _is_binary(b_module_data):
     return bool(start.translate(None, textchars))
 
 
-def _create_powershell_wrapper(b_module_data, module_args, environment,
-                               async_timeout, become, become_method,
-                               become_user, become_password, become_flags,
-                               scan_dependencies=True):
-    # creates the manifest/wrapper used in PowerShell modules to enable things
-    # like become and async - this is also called in action/script.py
-    exec_manifest = dict(
-        module_entry=to_text(base64.b64encode(b_module_data)),
-        powershell_modules=dict(),
-        module_args=module_args,
-        actions=['exec'],
-        environment=environment
-    )
-
-    exec_manifest['exec'] = to_text(base64.b64encode(to_bytes(leaf_exec)))
-
-    if async_timeout > 0:
-        exec_manifest["actions"].insert(0, 'async_watchdog')
-        exec_manifest["async_watchdog"] = to_text(
-            base64.b64encode(to_bytes(async_watchdog)))
-        exec_manifest["actions"].insert(0, 'async_wrapper')
-        exec_manifest["async_wrapper"] = to_text(
-            base64.b64encode(to_bytes(async_wrapper)))
-        exec_manifest["async_jid"] = str(random.randint(0, 999999999999))
-        exec_manifest["async_timeout_sec"] = async_timeout
-
-    if become and become_method == 'runas':
-        exec_manifest["actions"].insert(0, 'become')
-        exec_manifest["become_user"] = become_user
-        exec_manifest["become_password"] = become_password
-        exec_manifest['become_flags'] = become_flags
-        exec_manifest["become"] = to_text(
-            base64.b64encode(to_bytes(become_wrapper)))
-
-    finder = PSModuleDepFinder()
-
-    # we don't want to scan for any module_utils or other module related flags
-    # if scan_dependencies=False - action/script sets to False
-    if scan_dependencies:
-        finder.scan_module(b_module_data)
-
-    for name, data in finder.modules.items():
-        b64_data = to_text(base64.b64encode(data))
-        exec_manifest['powershell_modules'][name] = b64_data
-
-    exec_manifest['min_ps_version'] = finder.ps_version
-    exec_manifest['min_os_version'] = finder.os_version
-    if finder.become and 'become' not in exec_manifest['actions']:
-        exec_manifest['actions'].insert(0, 'become')
-        exec_manifest['become_user'] = 'SYSTEM'
-        exec_manifest['become_password'] = None
-        exec_manifest['become_flags'] = None
-        exec_manifest['become'] = to_text(
-            base64.b64encode(to_bytes(become_wrapper)))
-
-    # FUTURE: smuggle this back as a dict instead of serializing here;
-    # the connection plugin may need to modify it
-    b_json = to_bytes(json.dumps(exec_manifest))
-    b_data = exec_wrapper.replace(b"$json_raw = ''",
-                                  b"$json_raw = @'\r\n%s\r\n'@" % b_json)
-    return b_data
-
-
 def _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression, async_timeout, become,
                        become_method, become_user, become_password, become_flags, environment):
     """
@@ -932,10 +799,10 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
         shebang = u'#!powershell'
         # create the common exec wrapper payload and set that as the module_data
         # bytes
-        b_module_data = _create_powershell_wrapper(
+        b_module_data = ps_manifest._create_powershell_wrapper(
             b_module_data, module_args, environment, async_timeout, become,
             become_method, become_user, become_password, become_flags,
-            scan_dependencies=True
+            module_substyle
         )
 
     elif module_substyle == 'jsonargs':
diff --git a/lib/ansible/executor/powershell/__init__.py b/lib/ansible/executor/powershell/__init__.py
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/lib/ansible/executor/powershell/async_watchdog.ps1 b/lib/ansible/executor/powershell/async_watchdog.ps1
new file mode 100644
index 00000000000000..cd3de81b81b615
--- /dev/null
+++ b/lib/ansible/executor/powershell/async_watchdog.ps1
@@ -0,0 +1,110 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+param(
+    [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
+)
+
+# help with debugging errors as we don't have visibility of this running process
+trap {
+    $watchdog_path = "$($env:TEMP)\ansible-async-watchdog-error-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ").txt"
+    $error_msg = "Error while running the async exec wrapper`r`n$(Format-AnsibleException -ErrorRecord $_)"
+    Set-Content -Path $watchdog_path -Value $error_msg
+    break
+}
+
+$ErrorActionPreference = "Stop"
+
+Write-AnsibleLog "INFO - starting async_watchdog" "async_watchdog"
+
+# pop 0th action as entrypoint
+$payload.actions = $payload.actions[1..99]
+
+$actions = $Payload.actions
+$entrypoint = $payload.($actions[0])
+$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint))
+
+$resultfile_path = $payload.async_results_path
+$max_exec_time_sec = $payload.async_timeout_sec
+
+Write-AnsibleLog "INFO - deserializing existing result file args at: '$resultfile_path'" "async_watchdog"
+if (-not (Test-Path -Path $resultfile_path)) {
+    $msg = "result file at '$resultfile_path' does not exist"
+    Write-AnsibleLog "ERROR - $msg" "async_watchdog"
+    throw $msg
+}
+$result_json = Get-Content -Path $resultfile_path -Raw
+Write-AnsibleLog "INFO - result file json is: $result_json" "async_watchdog"
+$result = ConvertFrom-AnsibleJson -InputObject $result_json
+
+Write-AnsibleLog "INFO - creating async runspace" "async_watchdog"
+$rs = [RunspaceFactory]::CreateRunspace()
+$rs.Open()
+
+Write-AnsibleLog "INFO - creating async PowerShell pipeline" "async_watchdog"
+$ps = [PowerShell]::Create()
+$ps.Runspace = $rs
+
+# these functions are set in exec_wrapper
+Write-AnsibleLog "INFO - adding global functions to PowerShell pipeline script" "async_watchdog"
+$ps.AddScript($script:common_functions).AddStatement() > $null
+$ps.AddScript($script:wrapper_functions).AddStatement() > $null
+$ps.AddCommand("Set-Variable").AddParameters(@{Name="common_functions"; Value=$script:common_functions; Scope="script"}).AddStatement() > $null
+
+Write-AnsibleLog "INFO - adding $($actions[0]) to PowerShell pipeline script" "async_watchdog"
+$ps.AddScript($entrypoint).AddArgument($payload) > $null
+
+Write-AnsibleLog "INFO - async job start, calling BeginInvoke()" "async_watchdog"
+$job_async_result = $ps.BeginInvoke()
+
+Write-AnsibleLog "INFO - waiting '$max_exec_time_sec' seconds for async job to complete" "async_watchdog"
+$job_async_result.AsyncWaitHandle.WaitOne($max_exec_time_sec * 1000) > $null
+$result.finished = 1
+
+if ($job_async_result.IsCompleted) {
+    Write-AnsibleLog "INFO - async job completed, calling EndInvoke()" "async_watchdog"
+
+    $job_output = $ps.EndInvoke($job_async_result)
+    $job_error = $ps.Streams.Error
+
+    Write-AnsibleLog "INFO - raw module stdout:`r`n$($job_output | Out-String)" "async_watchdog"
+    if ($job_error) {
+        Write-AnsibleLog "WARN - raw module stderr:`r`n$($job_error | Out-String)" "async_watchdog"
+    }
+
+    # write success/output/error to result object
+    # TODO: cleanse leading/trailing junk
+    try {
+        Write-AnsibleLog "INFO - deserializing Ansible stdout" "async_watchdog"
+        $module_result = ConvertFrom-AnsibleJson -InputObject $job_output
+        # TODO: check for conflicting keys
+        $result = $result + $module_result
+    } catch {
+        $result.failed = $true
+        $result.msg = "failed to parse module output: $($_.Exception.Message)"
+        # return output back to Ansible to help with debugging errors
+        $result.stdout = $job_output | Out-String
+        $result.stderr = $job_error | Out-String
+    }
+
+    $result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
+    Set-Content -Path $resultfile_path -Value $result_json
+
+    Write-AnsibleLog "INFO - wrote output to $resultfile_path" "async_watchdog"
+} else {
+    Write-AnsibleLog "ERROR - reached timeout on async job, stopping job" "async_watchdog"
+    $ps.BeginStop($null, $null)  > $null # best effort stop
+
+    # write timeout to result object
+    $result.failed = $true
+    $result.msg = "timed out waiting for module completion"
+    $result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
+    Set-Content -Path $resultfile_path -Value $result_json
+
+    Write-AnsibleLog "INFO - wrote timeout to '$resultfile_path'" "async_watchdog"
+}
+
+# in the case of a hung pipeline, this will cause the process to stay alive until it's un-hung...
+#$rs.Close() | Out-Null
+
+Write-AnsibleLog "INFO - ending async_watchdog" "async_watchdog"
diff --git a/lib/ansible/executor/powershell/async_wrapper.ps1 b/lib/ansible/executor/powershell/async_wrapper.ps1
new file mode 100644
index 00000000000000..f91216c15ff4d9
--- /dev/null
+++ b/lib/ansible/executor/powershell/async_wrapper.ps1
@@ -0,0 +1,163 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+param(
+    [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
+)
+
+$ErrorActionPreference = "Stop"
+
+Write-AnsibleLog "INFO - starting async_wrapper" "async_wrapper"
+
+if (-not $Payload.environment.ContainsKey("ANSIBLE_ASYNC_DIR")) {
+    Write-AnsibleError -Message "internal error: the environment variable ANSIBLE_ASYNC_DIR is not set and is required for an async task"
+    $host.SetShouldExit(1)
+    return
+}
+$async_dir = [System.Environment]::ExpandEnvironmentVariables($Payload.environment.ANSIBLE_ASYNC_DIR)
+
+# 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($async_dir, $local_jid)
+
+Write-AnsibleLog "INFO - creating async results path at '$results_path'" "async_wrapper"
+
+$Payload.async_results_path = $results_path
+[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) > $null
+
+# we use Win32_Process to escape the current process job, CreateProcess with a
+# breakaway flag won't work for psrp as the psrp process does not have breakaway
+# rights. Unfortunately we can't read/write to the spawned process as we can't
+# inherit the handles. We use a locked down named pipe to send the exec_wrapper
+# payload. Anonymous pipes won't work as the spawned process will not be a child
+# of the current one and will not be able to inherit the handles
+
+# pop the async_wrapper action so we don't get stuck in a loop and create new
+# exec_wrapper for our async process
+$Payload.actions = $Payload.actions[1..99]
+$payload_json = ConvertTo-Json -InputObject $Payload -Depth 99 -Compress
+
+$exec_wrapper = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.exec_wrapper))
+$exec_wrapper = $exec_wrapper.Replace("`$json_raw = ''", "`$json_raw = @'`r`n$payload_json`r`n'@")
+$payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($exec_wrapper)
+$pipe_name = "ansible-async-$jid-$([guid]::NewGuid())"
+
+# template the async process command line with the payload details
+$bootstrap_wrapper = {
+    # help with debugging errors as we loose visibility of the process output
+    # from here on
+    trap {
+        $wrapper_path = "$($env:TEMP)\ansible-async-wrapper-error-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ").txt"
+        $error_msg = "Error while running the async exec wrapper`r`n$($_ | Out-String)`r`n$($_.ScriptStackTrace)"
+        Set-Content -Path $wrapper_path -Value $error_msg
+        break
+    }
+
+    &chcp.com 65001 > $null
+
+    # store the pipe name and no. of bytes to read, these are populated before
+    # before the process is created - do not remove or changed
+    $pipe_name = ""
+    $bytes_length = 0
+
+    $input_bytes = New-Object -TypeName byte[] -ArgumentList $bytes_length
+    $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @(
+        ".",  # localhost
+        $pipe_name,
+        [System.IO.Pipes.PipeDirection]::In,
+        [System.IO.Pipes.PipeOptions]::None,
+        [System.Security.Principal.TokenImpersonationLevel]::Anonymous
+    )
+    try {
+        $pipe.Connect()
+        $pipe.Read($input_bytes, 0, $bytes_length) > $null
+    } finally {
+        $pipe.Close()
+    }
+    $exec = [System.Text.Encoding]::UTF8.GetString($input_bytes)
+    $exec = [ScriptBlock]::Create($exec)
+    &$exec
+}
+
+$bootstrap_wrapper = $bootstrap_wrapper.ToString().Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"")
+$bootstrap_wrapper = $bootstrap_wrapper.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)")
+$encoded_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper))
+$exec_args = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command"
+
+# create a named pipe that is set to allow only the current user read access
+$current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User
+$pipe_sec = New-Object -TypeName System.IO.Pipes.PipeSecurity
+$pipe_ar = New-Object -TypeName System.IO.Pipes.PipeAccessRule -ArgumentList @(
+    $current_user,
+    [System.IO.Pipes.PipeAccessRights]::Read,
+    [System.Security.AccessControl.AccessControlType]::Allow
+)
+$pipe_sec.AddAccessRule($pipe_ar)
+
+Write-AnsibleLog "INFO - creating named pipe '$pipe_name'" "async_wrapper"
+$pipe = New-Object -TypeName System.IO.Pipes.NamedPipeServerStream -ArgumentList @(
+    $pipe_name,
+    [System.IO.Pipes.PipeDirection]::Out,
+    1,
+    [System.IO.Pipes.PipeTransmissionMode]::Byte,
+    [System.IO.Pipes.PipeOptions]::Asynchronous,
+    0,
+    0,
+    $pipe_sec
+)
+
+try {
+    Write-AnsibleLog "INFO - creating async process '$exec_args'" "async_wrapper"
+    $process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine=$exec_args}
+    $rc = $process.ReturnValue
+
+    Write-AnsibleLog "INFO - return value from async process exec: $rc" "async_wrapper"
+    if ($rc -ne 0) {
+        $error_msg = switch($rc) {
+            2 { "Access denied" }
+            3 { "Insufficient privilege" }
+            8 { "Unknown failure" }
+            9 { "Path not found" }
+            21 { "Invalid parameter" }
+            default { "Other" }
+        }
+        throw "Failed to start async process: $rc ($error_msg)"
+    }
+    $watchdog_pid = $process.ProcessId
+    Write-AnsibleLog "INFO - created async process PID: $watchdog_pid" "async_wrapper"
+
+    # populate initial results before we send the async data 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
+    }
+
+    Write-AnsibleLog "INFO - writing initial async results to '$results_path'" "async_wrapper"
+    $result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
+    Set-Content $results_path -Value $result_json
+
+    Write-AnsibleLog "INFO - waiting for async process to connect to named pipe for 5 seconds" "async_wrapper"
+    $wait_async = $pipe.BeginWaitForConnection($null, $null)
+    $wait_async.AsyncWaitHandle.WaitOne(5000) > $null
+    if (-not $wait_async.IsCompleted) {
+        throw "timeout while waiting for child process to connect to named pipe"
+    }
+    $pipe.EndWaitForConnection($wait_async)
+
+    Write-AnsibleLog "INFO - writing exec_wrapper and payload to async process" "async_wrapper"
+    $pipe.Write($payload_bytes, 0, $payload_bytes.Count)
+    $pipe.Flush()
+    $pipe.WaitForPipeDrain()
+} finally {
+    $pipe.Close()
+}
+
+Write-AnsibleLog "INFO - outputting initial async result: $result_json" "async_wrapper"
+Write-Output -InputObject $result_json
+Write-AnsibleLog "INFO - ending async_wrapper" "async_wrapper"
diff --git a/lib/ansible/executor/powershell/become_wrapper.ps1 b/lib/ansible/executor/powershell/become_wrapper.ps1
new file mode 100644
index 00000000000000..a27ea908a6ee32
--- /dev/null
+++ b/lib/ansible/executor/powershell/become_wrapper.ps1
@@ -0,0 +1,142 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+param(
+    [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
+)
+
+#AnsibleRequires -CSharpUtil Ansible.Become
+
+$ErrorActionPreference = "Stop"
+
+Write-AnsibleLog "INFO - starting become_wrapper" "become_wrapper"
+
+Function Get-EnumValue($enum, $flag_type, $value, $prefix) {
+    $raw_enum_value = "$prefix$($value.ToUpper())"
+    try {
+        $enum_value = [Enum]::Parse($enum, $raw_enum_value)
+    } catch [System.ArgumentException] {
+        $valid_options = [Enum]::GetNames($enum) | ForEach-Object { $_.Substring($prefix.Length).ToLower() }
+        throw "become_flags $flag_type value '$value' is not valid, valid values are: $($valid_options -join ", ")"
+    }
+    return $enum_value
+}
+
+Function Get-BecomeFlags($flags) {
+    $logon_type = [Ansible.Become.LogonType]::LOGON32_LOGON_INTERACTIVE
+    $logon_flags = [Ansible.Become.LogonFlags]::LOGON_WITH_PROFILE
+
+    if ($flags -eq $null -or $flags -eq "") {
+        $flag_split = @()
+    } elseif ($flags -is [string]) {
+        $flag_split = $flags.Split(" ")
+    } else {
+        throw "become_flags must be a string, was $($flags.GetType())"
+    }
+
+    foreach ($flag in $flag_split) {
+        $split = $flag.Split("=")
+        if ($split.Count -ne 2) {
+            throw "become_flags entry '$flag' is in an invalid format, must be a key=value pair"
+        }
+        $flag_key = $split[0]
+        $flag_value = $split[1]
+        if ($flag_key -eq "logon_type") {
+            $enum_details = @{
+                enum = [Ansible.Become.LogonType]
+                flag_type = $flag_key
+                value = $flag_value
+                prefix = "LOGON32_LOGON_"
+            }
+            $logon_type = Get-EnumValue @enum_details
+        } elseif ($flag_key -eq "logon_flags") {
+            $logon_flag_values = $flag_value.Split(",")
+            $logon_flags = 0 -as [Ansible.Become.LogonFlags]
+            foreach ($logon_flag_value in $logon_flag_values) {
+                if ($logon_flag_value -eq "") {
+                    continue
+                }
+                $enum_details = @{
+                    enum = [Ansible.Become.LogonFlags]
+                    flag_type = $flag_key
+                    value = $logon_flag_value
+                    prefix = "LOGON_"
+                }
+                $logon_flag = Get-EnumValue @enum_details
+                $logon_flags = $logon_flags -bor $logon_flag
+            }
+        } else {
+            throw "become_flags key '$flag_key' is not a valid runas flag, must be 'logon_type' or 'logon_flags'"
+        }
+    }
+
+    return $logon_type, [Ansible.Become.LogonFlags]$logon_flags
+}
+
+Write-AnsibleLog "INFO - loading C# become code" "become_wrapper"
+$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"]))
+
+# set the TMP env var to _ansible_remote_tmp to ensure the tmp binaries are
+# compiled to that location
+$new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"])
+$old_tmp = $env:TMP
+$env:TMP = $new_tmp
+Add-Type -TypeDefinition $become_def -Debug:$false
+$env:TMP = $old_tmp
+
+$username = $Payload.become_user
+$password = $Payload.become_password
+try {
+    $logon_type, $logon_flags = Get-BecomeFlags -flags $Payload.become_flags
+} catch {
+    Write-AnsibleError -Message "internal error: failed to parse become_flags '$($Payload.become_flags)'" -ErrorRecord $_
+    $host.SetShouldExit(1)
+    return
+}
+Write-AnsibleLog "INFO - parsed become input, user: '$username', type: '$logon_type', flags: '$logon_flags'" "become_wrapper"
+
+# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must
+# bootstrap via small wrapper which contains the exec_wrapper passed through the
+# stdin pipe. Cannot use 'powershell -' as the $ErrorActionPreference is always
+# set to Stop and cannot be changed
+$bootstrap_wrapper = {
+    &chcp.com 65001 > $null
+    $exec_wrapper_str = [System.Console]::In.ReadToEnd()
+    $exec_wrapper = [ScriptBlock]::Create($exec_wrapper_str)
+    &$exec_wrapper
+}
+$exec_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper.ToString()))
+$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command")
+$lp_current_directory = $env:SystemRoot  # TODO: should this be set to the become user's profile dir?
+
+# pop the become_wrapper action so we don't get stuck in a loop
+$Payload.actions = $Payload.actions[1..99]
+# we want the output from the exec_wrapper to be base64 encoded to preserve unicode chars
+$Payload.encoded_output = $true
+
+$payload_json = ConvertTo-Json -InputObject $Payload -Depth 99 -Compress
+$exec_wrapper = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.exec_wrapper))
+$exec_wrapper = $exec_wrapper.Replace("`$json_raw = ''", "`$json_raw = @'`r`n$payload_json`r`n'@")
+
+try {
+    Write-AnsibleLog "INFO - starting become process '$lp_command_line'" "become_wrapper"
+    $result = [Ansible.Become.BecomeUtil]::RunAsUser($username, $password, $lp_command_line,
+        $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type)
+    Write-AnsibleLog "INFO - become process complete with rc: $($result.ExitCode)" "become_wrapper"
+    $stdout = $result.StandardOut
+    try {
+        $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout))
+    } catch [FormatException] {
+        # output wasn't Base64, ignore as it may contain an error message we want to pass to Ansible
+        Write-AnsibleLog "WARN - become process stdout was not base64 encoded as expected: $stdout"
+    }
+
+    $host.UI.WriteLine($stdout)
+    $host.UI.WriteErrorLine($result.StandardError.Trim())
+    $host.SetShouldExit($result.ExitCode)
+} catch {
+    Write-AnsibleError -Message "internal error: failed to become user '$username'" -ErrorRecord $_
+    $host.SetShouldExit(1)
+}
+
+Write-AnsibleLog "INFO - ending become_wrapper" "become_wrapper"
diff --git a/lib/ansible/executor/powershell/exec_wrapper.ps1 b/lib/ansible/executor/powershell/exec_wrapper.ps1
new file mode 100644
index 00000000000000..d860264ef1f523
--- /dev/null
+++ b/lib/ansible/executor/powershell/exec_wrapper.ps1
@@ -0,0 +1,228 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+begin {
+    $DebugPreference = "Continue"
+    $ProgressPreference = "SilentlyContinue"
+    $ErrorActionPreference = "Stop"
+    Set-StrictMode -Version 2
+
+    # common functions that are loaded in exec and module context, this is set
+    # as a script scoped variable so async_watchdog and module_wrapper can
+    # access the functions when creating their Runspaces
+    $script:common_functions = {
+        Function ConvertFrom-AnsibleJson {
+            <#
+            .SYNOPSIS
+            Converts a JSON string to a Hashtable/Array in the fastest way
+            possible. Unfortunately ConvertFrom-Json is still faster but outputs
+            a PSCustomObject which is combersone for module consumption.
+
+            .PARAMETER InputObject
+            [String] The JSON string to deserialize.
+            #>
+            param(
+                [Parameter(Mandatory=$true, Position=0)][String]$InputObject
+            )
+
+            # we can use -AsHashtable to get PowerShell to convert the JSON to
+            # a Hashtable and not a PSCustomObject. This was added in PowerShell
+            # 6.0, fall back to a manual conversion for older versions
+            $cmdlet = Get-Command -Name ConvertFrom-Json -CommandType Cmdlet
+            if ("AsHashtable" -in $cmdlet.Parameters.Keys) {
+                return ,(ConvertFrom-Json -InputObject $InputObject -AsHashtable)
+            } else {
+                # get the PSCustomObject and then manually convert from there
+                $raw_obj = ConvertFrom-Json -InputObject $InputObject
+
+                Function ConvertTo-Hashtable {
+                    param($InputObject)
+
+                    if ($null -eq $InputObject) {
+                        return $null
+                    }
+
+                    if ($InputObject -is [PSCustomObject]) {
+                        $new_value = @{}
+                        foreach ($prop in $InputObject.PSObject.Properties.GetEnumerator()) {
+                            $new_value.($prop.Name) = (ConvertTo-Hashtable -InputObject $prop.Value)
+                        }
+                        return ,$new_value
+                    } elseif ($InputObject -is [Array]) {
+                        $new_value = [System.Collections.ArrayList]@()
+                        foreach ($val in $InputObject) {
+                            $new_value.Add((ConvertTo-Hashtable -InputObject $val)) > $null
+                        }
+                        return ,$new_value.ToArray()
+                    } else {
+                        return ,$InputObject
+                    }
+                }
+                return ,(ConvertTo-Hashtable -InputObject $raw_obj)
+            }
+        }
+
+        Function Format-AnsibleException {
+            <#
+            .SYNOPSIS
+            Formats a PowerShell ErrorRecord to a string that's fit for human
+            consumption.
+
+            .NOTES
+            Using Out-String can give us the first part of the exception but it
+            also wraps the messages at 80 chars which is not ideal. We also
+            append the ScriptStackTrace and the .NET StackTrace if present.
+            #>
+            param([System.Management.Automation.ErrorRecord]$ErrorRecord)
+
+            $exception = @"
+$($ErrorRecord.ToString())
+$($ErrorRecord.InvocationInfo.PositionMessage)
+    + CategoryInfo          : $($ErrorRecord.CategoryInfo.ToString())
+    + FullyQualifiedErrorId : $($ErrorRecord.FullyQualifiedErrorId.ToString())
+"@
+            # module_common strip comments and empty newlines, need to manually
+            # add a preceding newline using `r`n
+            $exception += "`r`n`r`nScriptStackTrace:`r`n$($ErrorRecord.ScriptStackTrace)`r`n"
+
+            # exceptions from C# will also have a StackTrace which we
+            # append if found
+            if ($null -ne $ErrorRecord.Exception.StackTrace) {
+                $exception += "`r`n$($ErrorRecord.Exception.ToString())"
+            }
+
+            return $exception
+        }
+    }
+    .$common_functions
+
+    # common wrapper functions used in the exec wrappers, this is defined in a
+    # script scoped variable so async_watchdog can pass them into the async job
+    $script:wrapper_functions = {
+        Function Write-AnsibleError {
+            <#
+            .SYNOPSIS
+            Writes an error message to a JSON string in the format that Ansible
+            understands. Also optionally adds an exception record if the
+            ErrorRecord is passed through.
+            #>
+            param(
+                [Parameter(Mandatory=$true)][String]$Message,
+                [System.Management.Automation.ErrorRecord]$ErrorRecord = $null
+            )
+            $result = @{
+                msg = $Message
+                failed = $true
+            }
+            if ($null -ne $ErrorRecord) {
+                $result.msg += ": $($ErrorRecord.Exception.Message)"
+                $result.exception = (Format-AnsibleException -ErrorRecord $ErrorRecord)
+            }
+            Write-Output -InputObject (ConvertTo-Json -InputObject $result -Depth 99 -Compress)
+        }
+
+        Function Write-AnsibleLog {
+            <#
+            .SYNOPSIS
+            Used as a debugging tool to log events to a file as they run in the
+            exec wrappers. By default this is a noop function but the $log_path
+            can be manually set to enable it. Manually set ANSIBLE_EXEC_DEBUG as
+            an env value on the Windows host that this is run on to enable.
+            #>
+            param(
+                [Parameter(Mandatory=$true, Position=0)][String]$Message,
+                [Parameter(Position=1)][String]$Wrapper
+            )
+
+            $log_path = $env:ANSIBLE_EXEC_DEBUG
+            if ($log_path) {
+                $log_path = [System.Environment]::ExpandEnvironmentVariables($log_path)
+                $parent_path = [System.IO.Path]::GetDirectoryName($log_path)
+                if (Test-Path -LiteralPath $parent_path -PathType Container) {
+                    $msg = "{0:u} - {1} - {2} - " -f (Get-Date), $pid, ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
+                    if ($null -ne $Wrapper) {
+                        $msg += "$Wrapper - "
+                    }
+                    $msg += $Message + "`r`n"
+                    $msg_bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
+
+                    $fs = [System.IO.File]::Open($log_path, [System.IO.FileMode]::Append,
+                        [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
+                    try {
+                        $fs.Write($msg_bytes, 0, $msg_bytes.Length)
+                    } finally {
+                        $fs.Close()
+                    }
+                }
+            }
+        }
+    }
+    .$wrapper_functions
+
+    # NB: do not adjust the following line - it is replaced when doing
+    # non-streamed input
+    $json_raw = ''
+} process {
+    $json_raw += [String]$input
+} end {
+    Write-AnsibleLog "INFO - starting exec_wrapper" "exec_wrapper"
+    if (-not $json_raw) {
+        Write-AnsibleError -Message "internal error: no input given to PowerShell exec wrapper"
+        exit 1
+    }
+
+    Write-AnsibleLog "INFO - converting json raw to a payload" "exec_wrapper"
+    $payload = ConvertFrom-AnsibleJson -InputObject $json_raw
+
+    # TODO: handle binary modules
+    # TODO: handle persistence
+
+    if ($payload.min_os_version) {
+        $min_os_version = [Version]$payload.min_os_version
+        # Environment.OSVersion.Version is deprecated and may not return the
+        # right version
+        $actual_os_version = [Version](Get-Item -Path $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion
+
+        Write-AnsibleLog "INFO - checking if actual os version '$actual_os_version' is less than the min os version '$min_os_version'" "exec_wrapper"
+        if ($actual_os_version -lt $min_os_version) {
+            Write-AnsibleError -Message "internal error: This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version"
+            exit 1
+        }
+    }
+    if ($payload.min_ps_version) {
+        $min_ps_version = [Version]$payload.min_ps_version
+        $actual_ps_version = $PSVersionTable.PSVersion
+
+        Write-AnsibleLog "INFO - checking if actual PS version '$actual_ps_version' is less than the min PS version '$min_ps_version'" "exec_wrapper"
+        if ($actual_ps_version -lt $min_ps_version) {
+            Write-AnsibleError -Message "internal error: This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version"
+            exit 1
+        }
+    }
+
+    # pop 0th action as entrypoint
+    $action = $payload.actions[0]
+    Write-AnsibleLog "INFO - running action $action" "exec_wrapper"
+
+    $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.($action)))
+    $entrypoint = [ScriptBlock]::Create($entrypoint)
+    # so we preserve the formatting and don't fall prey to locale issues, some
+    # wrappers want the output to be in base64 form, we store the value here in
+    # case the wrapper changes the value when they create a payload for their
+    # own exec_wrapper
+    $encoded_output = $payload.encoded_output
+
+    try {
+        $output = &$entrypoint -Payload $payload
+        if ($encoded_output -and $null -ne $output) {
+            $b64_output = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($output))
+            Write-Output -InputObject $b64_output
+        } else {
+            Write-Output -InputObject $output
+        }
+    } catch {
+        Write-AnsibleError -Message "internal error: failed to run exec_wrapper action $action" -ErrorRecord $_
+        exit 1
+    }
+    Write-AnsibleLog "INFO - ending exec_wrapper" "exec_wrapper"
+}
diff --git a/lib/ansible/executor/powershell/module_manifest.py b/lib/ansible/executor/powershell/module_manifest.py
new file mode 100644
index 00000000000000..9ad8261cf745e9
--- /dev/null
+++ b/lib/ansible/executor/powershell/module_manifest.py
@@ -0,0 +1,288 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import base64
+import json
+import os
+import pkgutil
+import random
+import re
+
+from distutils.version import LooseVersion
+
+from ansible import constants as C
+from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_bytes, to_text
+from ansible.plugins.loader import ps_module_utils_loader
+
+
+class PSModuleDepFinder(object):
+
+    def __init__(self):
+        self.ps_modules = dict()
+        self.exec_scripts = dict()
+
+        # by defining an explicit dict of cs utils and where they are used, we
+        # can potentially save time by not adding the type multiple times if it
+        # isn't needed
+        self.cs_utils_wrapper = dict()
+        self.cs_utils_module = dict()
+
+        self.ps_version = None
+        self.os_version = None
+        self.become = False
+
+        self._re_cs_module = re.compile(to_bytes(r'(?i)^using\s(Ansible\..+);$'))
+        self._re_cs_in_ps_module = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)'))
+        self._re_module = re.compile(to_bytes(r'(?i)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)'))
+        self._re_wrapper = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-wrapper\s+(\w*)'))
+        self._re_ps_version = re.compile(to_bytes(r'(?i)^#requires\s+\-version\s+([0-9]+(\.[0-9]+){0,3})$'))
+        self._re_os_version = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-osversion\s+([0-9]+(\.[0-9]+){0,3})$'))
+        self._re_become = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-become$'))
+
+    def scan_module(self, module_data, wrapper=False, powershell=True):
+        lines = module_data.split(b'\n')
+        module_utils = set()
+        if wrapper:
+            cs_utils = self.cs_utils_wrapper
+        else:
+            cs_utils = self.cs_utils_module
+
+        if powershell:
+            checks = [
+                # PS module contains '#Requires -Module Ansible.ModuleUtils.*'
+                (self._re_module, self.ps_modules, ".psm1"),
+                # PS module contains '#AnsibleRequires -CSharpUtil Ansible.*'
+                (self._re_cs_in_ps_module, cs_utils, ".cs"),
+            ]
+        else:
+            checks = [
+                # CS module contains 'using Ansible.*;'
+                (self._re_cs_module, cs_utils, ".cs"),
+            ]
+
+        for line in lines:
+            for check in checks:
+                match = check[0].match(line)
+                if match:
+                    # tolerate windows line endings by stripping any remaining
+                    # newline chars
+                    module_util_name = to_text(match.group(1).rstrip())
+                    if module_util_name not in check[1].keys():
+                        module_utils.add((module_util_name, check[2]))
+
+            if powershell:
+                ps_version_match = self._re_ps_version.match(line)
+                if ps_version_match:
+                    self._parse_version_match(ps_version_match, "ps_version")
+
+                os_version_match = self._re_os_version.match(line)
+                if os_version_match:
+                    self._parse_version_match(os_version_match, "os_version")
+
+                # once become is set, no need to keep on checking recursively
+                if not self.become:
+                    become_match = self._re_become.match(line)
+                    if become_match:
+                        self.become = True
+
+            if wrapper:
+                wrapper_match = self._re_wrapper.match(line)
+                if wrapper_match:
+                    self.scan_exec_script(wrapper_match.group(1).rstrip())
+
+        # recursively drill into each Requires to see if there are any more
+        # requirements
+        for m in set(module_utils):
+            self._add_module(m, wrapper=wrapper)
+
+    def scan_exec_script(self, name):
+        # scans lib/ansible/executor/powershell for scripts used in the module
+        # exec side. It also scans these scripts for any dependencies
+        name = to_text(name)
+        if name in self.exec_scripts.keys():
+            return
+
+        data = pkgutil.get_data("ansible.executor.powershell", name + ".ps1")
+        if data is None:
+            raise AnsibleError("Could not find executor powershell script "
+                               "for '%s'" % name)
+
+        b_data = to_bytes(data)
+
+        # remove comments to reduce the payload size in the exec wrappers
+        if C.DEFAULT_DEBUG:
+            exec_script = b_data
+        else:
+            exec_script = _strip_comments(b_data)
+        self.exec_scripts[name] = to_bytes(exec_script)
+        self.scan_module(b_data, wrapper=True, powershell=True)
+
+    def _add_module(self, name, wrapper=False):
+        m, ext = name
+        m = to_text(m)
+        mu_path = ps_module_utils_loader.find_plugin(m, ext)
+        if not mu_path:
+            raise AnsibleError('Could not find imported module support code '
+                               'for \'%s\'' % m)
+
+        module_util_data = to_bytes(_slurp(mu_path))
+        if ext == ".psm1":
+            self.ps_modules[m] = module_util_data
+        else:
+            if wrapper:
+                self.cs_utils_wrapper[m] = module_util_data
+            else:
+                self.cs_utils_module[m] = module_util_data
+        self.scan_module(module_util_data, wrapper=wrapper,
+                         powershell=(ext == ".psm1"))
+
+    def _parse_version_match(self, match, attribute):
+        new_version = to_text(match.group(1)).rstrip()
+
+        # PowerShell cannot cast a string of "1" to Version, it must have at
+        # least the major.minor for it to be valid so we append 0
+        if match.group(2) is None:
+            new_version = "%s.0" % new_version
+
+        existing_version = getattr(self, attribute, None)
+        if existing_version is None:
+            setattr(self, attribute, new_version)
+        else:
+            # determine which is the latest version and set that
+            if LooseVersion(new_version) > LooseVersion(existing_version):
+                setattr(self, attribute, new_version)
+
+
+def _slurp(path):
+    if not os.path.exists(path):
+        raise AnsibleError("imported module support code does not exist at %s"
+                           % os.path.abspath(path))
+    fd = open(path, 'rb')
+    data = fd.read()
+    fd.close()
+    return data
+
+
+def _strip_comments(source):
+    # Strip comments and blank lines from the wrapper
+    buf = []
+    start_block = False
+    for line in source.splitlines():
+        l = line.strip()
+
+        if start_block and l.endswith(b'#>'):
+            start_block = False
+            continue
+        elif start_block:
+            continue
+        elif l.startswith(b'<#'):
+            start_block = True
+            continue
+        elif not l or l.startswith(b'#'):
+            continue
+
+        buf.append(line)
+    return b'\n'.join(buf)
+
+
+def _create_powershell_wrapper(b_module_data, module_args, environment,
+                               async_timeout, become, become_method,
+                               become_user, become_password, become_flags,
+                               substyle):
+    # creates the manifest/wrapper used in PowerShell/C# modules to enable
+    # things like become and async - this is also called in action/script.py
+
+    # FUTURE: add process_wrapper.ps1 to run module_wrapper in a new process
+    # if running under a persistent connection and substyle is C# so we
+    # don't have type conflicts
+    finder = PSModuleDepFinder()
+    if substyle != 'script':
+        # don't scan the module for util dependencies and other Ansible related
+        # flags if the substyle is 'script' which is set by action/script
+        finder.scan_module(b_module_data, powershell=(substyle == "powershell"))
+
+    module_wrapper = "module_%s_wrapper" % substyle
+    exec_manifest = dict(
+        module_entry=to_text(base64.b64encode(b_module_data)),
+        powershell_modules=dict(),
+        csharp_utils=dict(),
+        csharp_utils_module=list(),  # csharp_utils only required by a module
+        module_args=module_args,
+        actions=[module_wrapper],
+        environment=environment,
+        encoded_output=False
+    )
+    finder.scan_exec_script(module_wrapper)
+
+    if async_timeout > 0:
+        finder.scan_exec_script('exec_wrapper')
+        finder.scan_exec_script('async_watchdog')
+        finder.scan_exec_script('async_wrapper')
+
+        exec_manifest["actions"].insert(0, 'async_watchdog')
+        exec_manifest["actions"].insert(0, 'async_wrapper')
+        exec_manifest["async_jid"] = str(random.randint(0, 999999999999))
+        exec_manifest["async_timeout_sec"] = async_timeout
+
+    if become and become_method == 'runas':
+        finder.scan_exec_script('exec_wrapper')
+        finder.scan_exec_script('become_wrapper')
+
+        exec_manifest["actions"].insert(0, 'become_wrapper')
+        exec_manifest["become_user"] = become_user
+        exec_manifest["become_password"] = become_password
+        exec_manifest['become_flags'] = become_flags
+
+    exec_manifest['min_ps_version'] = finder.ps_version
+    exec_manifest['min_os_version'] = finder.os_version
+    if finder.become and 'become_wrapper' not in exec_manifest['actions']:
+        finder.scan_exec_script('exec_wrapper')
+        finder.scan_exec_script('become_wrapper')
+
+        exec_manifest['actions'].insert(0, 'become_wrapper')
+        exec_manifest['become_user'] = 'SYSTEM'
+        exec_manifest['become_password'] = None
+        exec_manifest['become_flags'] = None
+
+    # make sure Ansible.ModuleUtils.AddType is added if any C# utils are used
+    if len(finder.cs_utils_wrapper) > 0 or len(finder.cs_utils_module) > 0:
+        finder._add_module((b"Ansible.ModuleUtils.AddType", ".psm1"),
+                           wrapper=False)
+
+    # exec_wrapper is only required to be part of the payload if using
+    # become or async, to save on payload space we check if exec_wrapper has
+    # already been added, and remove it manually if it hasn't later
+    exec_required = "exec_wrapper" in finder.exec_scripts.keys()
+    finder.scan_exec_script("exec_wrapper")
+    # must contain an empty newline so it runs the begin/process/end block
+    finder.exec_scripts["exec_wrapper"] += b"\n\n"
+
+    exec_wrapper = finder.exec_scripts["exec_wrapper"]
+    if not exec_required:
+        finder.exec_scripts.pop("exec_wrapper")
+
+    for name, data in finder.exec_scripts.items():
+        b64_data = to_text(base64.b64encode(data))
+        exec_manifest[name] = b64_data
+
+    for name, data in finder.ps_modules.items():
+        b64_data = to_text(base64.b64encode(data))
+        exec_manifest['powershell_modules'][name] = b64_data
+
+    cs_utils = finder.cs_utils_wrapper
+    cs_utils.update(finder.cs_utils_module)
+    for name, data in cs_utils.items():
+        b64_data = to_text(base64.b64encode(data))
+        exec_manifest['csharp_utils'][name] = b64_data
+    exec_manifest['csharp_utils_module'] = list(finder.cs_utils_module.keys())
+
+    # FUTURE: smuggle this back as a dict instead of serializing here;
+    # the connection plugin may need to modify it
+    b_json = to_bytes(json.dumps(exec_manifest))
+    b_data = exec_wrapper.replace(b"$json_raw = ''",
+                                  b"$json_raw = @'\r\n%s\r\n'@" % b_json)
+    return b_data
diff --git a/lib/ansible/executor/powershell/module_powershell_wrapper.ps1 b/lib/ansible/executor/powershell/module_powershell_wrapper.ps1
new file mode 100644
index 00000000000000..c092f5eb007a4b
--- /dev/null
+++ b/lib/ansible/executor/powershell/module_powershell_wrapper.ps1
@@ -0,0 +1,57 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+param(
+    [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
+)
+
+#AnsibleRequires -Wrapper module_wrapper
+
+$ErrorActionPreference = "Stop"
+
+Write-AnsibleLog "INFO - starting module_powershell_wrapper" "module_powershell_wrapper"
+
+$module_name = $Payload.module_args["_ansible_module_name"]
+Write-AnsibleLog "INFO - building module payload for '$module_name'" "module_powershell_wrapper"
+
+# compile any C# module utils passed in from the controller, Add-CSharpType is
+# automatically added to the payload manifest if any csharp util is set
+$csharp_utils = [System.Collections.ArrayList]@()
+foreach ($csharp_util in $Payload.csharp_utils_module) {
+    Write-AnsibleLog "INFO - adding $csharp_util to list of C# references to compile" "module_powershell_wrapper"
+    $util_code = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils[$csharp_util]))
+    $csharp_utils.Add($util_code) > $null
+}
+if ($csharp_utils.Count -gt 0) {
+    $add_type_b64 = $Payload.powershell_modules["Ansible.ModuleUtils.AddType"]
+    $add_type = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($add_type_b64))
+    New-Module -Name Ansible.ModuleUtils.AddType -ScriptBlock ([ScriptBlock]::Create($add_type)) | Import-Module > $null
+
+    # add any C# references so the module does not have to do so
+    $new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"])
+    Add-CSharpType -References $csharp_utils -TempPath $new_tmp -IncludeDebugInfo
+}
+
+# get the common module_wrapper code and invoke that to run the module
+$variables = [System.Collections.ArrayList]@(@{ Name = "complex_args"; Value = $Payload.module_args; Scope = "Global" })
+$module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.module_entry))
+$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.module_wrapper))
+$entrypoint = [ScriptBlock]::Create($entrypoint)
+
+try {
+    &$entrypoint -Scripts $script:common_functions, $module -Variables $variables `
+        -Environment $Payload.environment -Modules $Payload.powershell_modules `
+        -ModuleName $module_name
+} catch {
+    # failed to invoke the PowerShell module, capture the exception and
+    # output a pretty error for Ansible to parse
+    $result = @{
+        msg = "Failed to invoke PowerShell module: $($_.Exception.Message)"
+        failed = $true
+        exception = (Format-AnsibleException -ErrorRecord $_)
+    }
+    Write-Output -InputObject (ConvertTo-Json -InputObject $result -Depth 99 -Compress)
+    $host.SetShouldExit(1)
+}
+
+Write-AnsibleLog "INFO - ending module_powershell_wrapper" "module_powershell_wrapper"
diff --git a/lib/ansible/executor/powershell/module_script_wrapper.ps1 b/lib/ansible/executor/powershell/module_script_wrapper.ps1
new file mode 100644
index 00000000000000..7a2e4ba418ca2e
--- /dev/null
+++ b/lib/ansible/executor/powershell/module_script_wrapper.ps1
@@ -0,0 +1,22 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+param(
+    [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
+)
+
+#AnsibleRequires -Wrapper module_wrapper
+
+$ErrorActionPreference = "Stop"
+
+Write-AnsibleLog "INFO - starting module_script_wrapper" "module_script_wrapper"
+
+$script = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.module_entry))
+
+# get the common module_wrapper code and invoke that to run the module
+$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.module_wrapper))
+$entrypoint = [ScriptBlock]::Create($entrypoint)
+
+&$entrypoint -Scripts $script -Environment $Payload.environment -ModuleName "script"
+
+Write-AnsibleLog "INFO - ending module_script_wrapper" "module_script_wrapper"
diff --git a/lib/ansible/executor/powershell/module_wrapper.ps1 b/lib/ansible/executor/powershell/module_wrapper.ps1
new file mode 100644
index 00000000000000..84f30d8b186d58
--- /dev/null
+++ b/lib/ansible/executor/powershell/module_wrapper.ps1
@@ -0,0 +1,165 @@
+# (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+<#
+.SYNOPSIS
+Invokes an Ansible module in a new Runspace. This cmdlet will output the
+module's output and write any errors to the error stream of the current
+host.
+
+.PARAMETER Scripts
+[Object[]] String or ScriptBlocks to execute.
+
+.PARAMETER Variables
+[System.Collections.ArrayList] The variables to set in the new Pipeline.
+Each value is a hashtable that contains the parameters to use with
+Set-Variable;
+    Name: the name of the variable to set
+    Value: the value of the variable to set
+    Scope: the scope of the variable
+
+.PARAMETER Environment
+[System.Collections.IDictionary] A Dictionary of environment key/values to
+set in the new Pipeline.
+
+.PARAMETER Modules
+[System.Collections.IDictionary] A Dictionary of PowerShell modules to
+import into the new Pipeline. The key is the name of the module and the
+value is a base64 string of the module util code.
+
+.PARAMETER ModuleName
+[String] The name of the module that is being executed.
+#>
+param(
+    [Object[]]$Scripts,
+    [System.Collections.ArrayList][AllowEmptyCollection()]$Variables,
+    [System.Collections.IDictionary]$Environment,
+    [System.Collections.IDictionary]$Modules,
+    [String]$ModuleName
+)
+
+Write-AnsibleLog "INFO - creating new PowerShell pipeline for $ModuleName" "module_wrapper"
+$ps = [PowerShell]::Create()
+
+# do not set ErrorActionPreference for script
+if ($ModuleName -ne "script") {
+    $ps.Runspace.SessionStateProxy.SetVariable("ErrorActionPreference", "Stop")
+}
+
+# force input encoding to preamble-free UTF8 so PS sub-processes (eg,
+# Start-Job) don't blow up. This is only required for WinRM, a PSRP
+# runspace doesn't have a host console and this will bomb out
+if ($host.Name -eq "ConsoleHost") {
+    Write-AnsibleLog "INFO - setting console input encoding to UTF8 for $ModuleName" "module_wrapper"
+    $ps.AddScript('[Console]::InputEncoding = New-Object Text.UTF8Encoding $false').AddStatement() > $null
+}
+
+# set the variables
+foreach ($variable in $Variables) {
+    Write-AnsibleLog "INFO - setting variable '$($variable.Name)' for $ModuleName" "module_wrapper"
+    $ps.AddCommand("Set-Variable").AddParameters($variable).AddStatement() > $null
+}
+
+# set the environment vars
+if ($Environment) {
+    foreach ($env_kv in $Environment.GetEnumerator()) {
+        Write-AnsibleLog "INFO - setting environment '$($env_kv.Key)' for $ModuleName" "module_wrapper"
+        $env_key = $env_kv.Key.Replace("'", "''")
+        $env_value = $env_kv.Value.ToString().Replace("'", "''")
+        $escaped_env_set = "[System.Environment]::SetEnvironmentVariable('$env_key', '$env_value')"
+        $ps.AddScript($escaped_env_set).AddStatement() > $null
+    }
+}
+
+# import the PS modules
+if ($Modules) {
+    foreach ($module in $Modules.GetEnumerator()) {
+        Write-AnsibleLog "INFO - create module util '$($module.Key)' for $ModuleName" "module_wrapper"
+        $module_name = $module.Key
+        $module_code = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($module.Value))
+        $ps.AddCommand("New-Module").AddParameters(@{Name=$module_name; ScriptBlock=[ScriptBlock]::Create($module_code)}) > $null
+        $ps.AddCommand("Import-Module").AddParameter("WarningAction", "SilentlyContinue") > $null
+        $ps.AddCommand("Out-Null").AddStatement() > $null
+    }
+}
+
+# redefine Write-Host to dump to output instead of failing
+# lots of scripts still use it
+$ps.AddScript('Function Write-Host($msg) { Write-Output -InputObject $msg }').AddStatement() > $null
+
+# add the scripts and run
+foreach ($script in $Scripts) {
+    $ps.AddScript($script).AddStatement() > $null
+}
+
+Write-AnsibleLog "INFO - start module exec with Invoke() - $ModuleName" "module_wrapper"
+try {
+    $module_output = $ps.Invoke()
+} catch {
+    # uncaught exception while executing module, present a prettier error for
+    # Ansible to parse
+    Write-AnsibleError -Message "Unhandled exception while executing module" `
+        -ErrorRecord $_.Exception.InnerException.ErrorRecord
+    $host.SetShouldExit(1)
+    return
+}
+
+# other types of errors may not throw an exception in Invoke but rather just
+# set the pipeline state to failed
+if ($ps.InvocationStateInfo.State -eq "Failed" -and $ModuleName -ne "script") {
+    Write-AnsibleError -Message "Unhandled exception while executing module" `
+        -ErrorRecord $ps.InvocationStateInfo.Reason.ErrorRecord
+    $host.SetShouldExit(1)
+    return
+}
+
+Write-AnsibleLog "INFO - module exec ended $ModuleName" "module_wrapper"
+$ansible_output = $ps.Runspace.SessionStateProxy.GetVariable("_ansible_output")
+
+# _ansible_output is a special var used by new modules to store the
+# output JSON. If set, we consider the ExitJson and FailJson methods
+# called and assume it contains the JSON we want and the pipeline
+# output won't contain anything of note
+# TODO: should we validate it or use a random variable name?
+# TODO: should we use this behaviour for all new modules and not just
+# ones running under psrp
+if ($null -ne $ansible_output) {
+    Write-AnsibleLog "INFO - using the _ansible_output variable for module output - $ModuleName" "module_wrapper"
+    Write-Output -InputObject $ansible_output.ToString()
+} elseif ($module_output.Count -gt 0) {
+    # do not output if empty collection
+    Write-AnsibleLog "INFO - using the output stream for module output - $ModuleName" "module_wrapper"
+    Write-Output -InputObject ($module_output -join "`r`n")
+}
+
+# we attempt to get the return code from the LASTEXITCODE variable
+# this is set explicitly in newer style variables when calling
+# ExitJson and FailJson. If set we set the current hosts' exit code
+# to that same value
+$rc = $ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE")
+if ($null -ne $rc) {
+    Write-AnsibleLog "INFO - got an rc of $rc from $ModuleName exec" "module_wrapper"
+    $host.SetShouldExit($rc)
+}
+
+# PS3 doesn't properly set HadErrors in many cases, inspect the error stream as a fallback
+# with the trap handler that's now in place, this should only write to the output if
+# $ErrorActionPreference != "Stop", that's ok because this is sent to the stderr output
+# for a user to manually debug if something went horribly wrong
+if ($ps.HadErrors -or ($PSVersionTable.PSVersion.Major -lt 4 -and $ps.Streams.Error.Count -gt 0)) {
+    Write-AnsibleLog "WARN - module had errors, outputting error info $ModuleName" "module_wrapper"
+    # if the rc wasn't explicitly set, we return an exit code of 1
+    if ($null -eq $rc) {
+        $host.SetShouldExit(1)
+    }
+
+    # output each error to the error stream of the current pipeline
+    foreach ($err in $ps.Streams.Error) {
+        $error_msg = Format-AnsibleException -ErrorRecord $err
+
+        # need to use the current hosts's UI class as we may not have
+        # a console to write the stderr to, e.g. psrp
+        Write-AnsibleLog "WARN - error msg for for $($ModuleName):`r`n$error_msg" "module_wrapper"
+        $host.UI.WriteErrorLine($error_msg)
+    }
+}
diff --git a/lib/ansible/module_utils/csharp/Ansible.Become.cs b/lib/ansible/module_utils/csharp/Ansible.Become.cs
new file mode 100644
index 00000000000000..3a8a8527680146
--- /dev/null
+++ b/lib/ansible/module_utils/csharp/Ansible.Become.cs
@@ -0,0 +1,721 @@
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.AccessControl;
+using System.Security.Principal;
+using System.Text;
+using System.Threading;
+
+// TODO: make some classes/structs private/internal before the next release
+
+namespace Ansible.Become
+{
+    [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;
+        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
+        public byte[] _data1;
+        public Int32 dwFlags;
+        public Int16 wShowWindow;
+        public Int16 cbReserved2;
+        public IntPtr lpReserved2;
+        public SafeFileHandle hStdInput;
+        public SafeFileHandle hStdOutput;
+        public SafeFileHandle hStdError;
+        public STARTUPINFO()
+        {
+            cb = Marshal.SizeOf(this);
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public class STARTUPINFOEX
+    {
+        public STARTUPINFO startupInfo;
+        public IntPtr lpAttributeList;
+        public STARTUPINFOEX()
+        {
+            startupInfo = new STARTUPINFO();
+            startupInfo.cb = Marshal.SizeOf(this);
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct PROCESS_INFORMATION
+    {
+        public IntPtr hProcess;
+        public IntPtr hThread;
+        public int dwProcessId;
+        public int dwThreadId;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct SID_AND_ATTRIBUTES
+    {
+        public IntPtr Sid;
+        public int Attributes;
+    }
+
+    public struct TOKEN_USER
+    {
+        public SID_AND_ATTRIBUTES User;
+    }
+
+    [Flags]
+    public enum StartupInfoFlags : uint
+    {
+        USESTDHANDLES = 0x00000100
+    }
+
+    [Flags]
+    public enum CreationFlags : uint
+    {
+        CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
+        CREATE_DEFAULT_ERROR_MODE = 0x04000000,
+        CREATE_NEW_CONSOLE = 0x00000010,
+        CREATE_SUSPENDED = 0x00000004,
+        CREATE_UNICODE_ENVIRONMENT = 0x00000400,
+        EXTENDED_STARTUPINFO_PRESENT = 0x00080000
+    }
+
+    public enum HandleFlags : uint
+    {
+        None = 0,
+        INHERIT = 1
+    }
+
+    [Flags]
+    public enum LogonFlags
+    {
+        LOGON_WITH_PROFILE = 0x00000001,
+        LOGON_NETCREDENTIALS_ONLY = 0x00000002
+    }
+
+    public enum LogonType
+    {
+        LOGON32_LOGON_INTERACTIVE = 2,
+        LOGON32_LOGON_NETWORK = 3,
+        LOGON32_LOGON_BATCH = 4,
+        LOGON32_LOGON_SERVICE = 5,
+        LOGON32_LOGON_UNLOCK = 7,
+        LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
+        LOGON32_LOGON_NEW_CREDENTIALS = 9
+    }
+
+    public enum LogonProvider
+    {
+        LOGON32_PROVIDER_DEFAULT = 0,
+    }
+
+    public enum TokenInformationClass
+    {
+        TokenUser = 1,
+        TokenType = 8,
+        TokenImpersonationLevel = 9,
+        TokenElevationType = 18,
+        TokenLinkedToken = 19,
+    }
+
+    public enum TokenElevationType
+    {
+        TokenElevationTypeDefault = 1,
+        TokenElevationTypeFull,
+        TokenElevationTypeLimited
+    }
+
+    [Flags]
+    public enum ProcessAccessFlags : uint
+    {
+        PROCESS_QUERY_INFORMATION = 0x00000400,
+    }
+
+    public enum SECURITY_IMPERSONATION_LEVEL
+    {
+        SecurityImpersonation,
+    }
+
+    public enum TOKEN_TYPE
+    {
+        TokenPrimary = 1,
+        TokenImpersonation
+    }
+
+    class NativeWaitHandle : WaitHandle
+    {
+        public NativeWaitHandle(IntPtr handle)
+        {
+            this.SafeWaitHandle = new SafeWaitHandle(handle, false);
+        }
+    }
+
+    public class Win32Exception : System.ComponentModel.Win32Exception
+    {
+        private string _msg;
+        public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+        public Win32Exception(int errorCode, string message) : base(errorCode)
+        {
+            _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
+        }
+        public override string Message { get { return _msg; } }
+        public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+    }
+
+    public class CommandResult
+    {
+        public string StandardOut { get; internal set; }
+        public string StandardError { get; internal set; }
+        public uint ExitCode { get; internal set; }
+    }
+
+    public class BecomeUtil
+    {
+        [DllImport("advapi32.dll", SetLastError = true)]
+        private static extern bool LogonUser(
+            string lpszUsername,
+            string lpszDomain,
+            string lpszPassword,
+            LogonType dwLogonType,
+            LogonProvider dwLogonProvider,
+            out IntPtr phToken);
+
+        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+        private static extern bool CreateProcessWithTokenW(
+            IntPtr hToken,
+            LogonFlags dwLogonFlags,
+            [MarshalAs(UnmanagedType.LPTStr)]
+            string lpApplicationName,
+            StringBuilder lpCommandLine,
+            CreationFlags dwCreationFlags,
+            IntPtr lpEnvironment,
+            [MarshalAs(UnmanagedType.LPTStr)]
+            string lpCurrentDirectory,
+            STARTUPINFOEX lpStartupInfo,
+            out PROCESS_INFORMATION lpProcessInformation);
+
+        [DllImport("kernel32.dll")]
+        private static extern bool CreatePipe(
+            out SafeFileHandle hReadPipe,
+            out SafeFileHandle hWritePipe,
+            SECURITY_ATTRIBUTES lpPipeAttributes,
+            uint nSize);
+
+        [DllImport("kernel32.dll", SetLastError = true)]
+        private static extern bool SetHandleInformation(
+            SafeFileHandle hObject,
+            HandleFlags dwMask,
+            int dwFlags);
+
+        [DllImport("kernel32.dll", SetLastError = true)]
+        private static extern bool GetExitCodeProcess(
+            IntPtr hProcess,
+            out uint lpExitCode);
+
+        [DllImport("kernel32.dll", SetLastError = true)]
+        private static extern bool CloseHandle(
+            IntPtr hObject);
+
+        [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();
+
+        [DllImport("advapi32.dll", SetLastError = true)]
+        private static extern bool GetTokenInformation(
+            IntPtr TokenHandle,
+            TokenInformationClass TokenInformationClass,
+            IntPtr TokenInformation,
+            uint TokenInformationLength,
+            out uint ReturnLength);
+
+        [DllImport("psapi.dll", SetLastError = true)]
+        private static extern bool EnumProcesses(
+            [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)]
+                [In][Out] IntPtr[] processIds,
+            uint cb,
+            [MarshalAs(UnmanagedType.U4)]
+                out uint pBytesReturned);
+
+        [DllImport("kernel32.dll", SetLastError = true)]
+        private static extern IntPtr OpenProcess(
+            ProcessAccessFlags processAccess,
+            bool bInheritHandle,
+            IntPtr processId);
+
+        [DllImport("advapi32.dll", SetLastError = true)]
+        private static extern bool OpenProcessToken(
+            IntPtr ProcessHandle,
+            TokenAccessLevels DesiredAccess,
+            out IntPtr TokenHandle);
+
+        [DllImport("advapi32.dll", SetLastError = true)]
+        private static extern bool ConvertSidToStringSidW(
+            IntPtr pSID,
+            [MarshalAs(UnmanagedType.LPTStr)]
+            out string StringSid);
+
+        [DllImport("advapi32", SetLastError = true)]
+        private static extern bool DuplicateTokenEx(
+            IntPtr hExistingToken,
+            TokenAccessLevels dwDesiredAccess,
+            IntPtr lpTokenAttributes,
+            SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
+            TOKEN_TYPE TokenType,
+            out IntPtr phNewToken);
+
+        [DllImport("advapi32.dll", SetLastError = true)]
+        private static extern bool ImpersonateLoggedOnUser(
+            IntPtr hToken);
+
+        [DllImport("advapi32.dll", SetLastError = true)]
+        private static extern bool RevertToSelf();
+
+        public static CommandResult RunAsUser(string username, string password, string lpCommandLine,
+            string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType)
+        {
+            SecurityIdentifier account = null;
+            if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
+            {
+                account = GetBecomeSid(username);
+            }
+
+            STARTUPINFOEX si = new STARTUPINFOEX();
+            si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
+
+            SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES();
+            pipesec.bInheritHandle = true;
+
+            // Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
+            SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write;
+            if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
+                throw new Win32Exception("STDOUT pipe setup failed");
+            if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
+                throw new Win32Exception("STDOUT pipe handle setup failed");
+
+            if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0))
+                throw new Win32Exception("STDERR pipe setup failed");
+            if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0))
+                throw new Win32Exception("STDERR pipe handle setup failed");
+
+            if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0))
+                throw new Win32Exception("STDIN pipe setup failed");
+            if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0))
+                throw new Win32Exception("STDIN pipe handle setup failed");
+
+            si.startupInfo.hStdOutput = stdout_write;
+            si.startupInfo.hStdError = stderr_write;
+            si.startupInfo.hStdInput = stdin_read;
+
+            // Setup the stdin buffer
+            UTF8Encoding utf8_encoding = new UTF8Encoding(false);
+            FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768);
+            StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
+
+            // Create the environment block if set
+            IntPtr lpEnvironment = IntPtr.Zero;
+
+            CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
+
+            PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
+
+            // Get the user tokens to try running processes with
+            List<IntPtr> tokens = GetUserTokens(account, username, password, logonType);
+
+            bool launch_success = false;
+            foreach (IntPtr token in tokens)
+            {
+                if (CreateProcessWithTokenW(
+                    token,
+                    logonFlags,
+                    null,
+                    new StringBuilder(lpCommandLine),
+                    startup_flags,
+                    lpEnvironment,
+                    lpCurrentDirectory,
+                    si,
+                    out pi))
+                {
+                    launch_success = true;
+                    break;
+                }
+            }
+
+            if (!launch_success)
+                throw new Win32Exception("Failed to start become process");
+
+            CommandResult result = new CommandResult();
+            // Setup the output buffers and get stdout/stderr
+            FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096);
+            StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
+            stdout_write.Close();
+
+            FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096);
+            StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
+            stderr_write.Close();
+
+            stdin.WriteLine(stdinInput);
+            stdin.Close();
+
+            string stdout_str, stderr_str = null;
+            GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
+            UInt32 rc = GetProcessExitCode(pi.hProcess);
+
+            result.StandardOut = stdout_str;
+            result.StandardError = stderr_str;
+            result.ExitCode = rc;
+
+            return result;
+        }
+
+        private static SecurityIdentifier GetBecomeSid(string username)
+        {
+            NTAccount account = new NTAccount(username);
+            try
+            {
+                SecurityIdentifier security_identifier = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
+                return security_identifier;
+            }
+            catch (IdentityNotMappedException ex)
+            {
+                throw new Exception(String.Format("Unable to find become user {0}: {1}", username, ex.Message));
+            }
+        }
+
+        private static List<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType)
+        {
+            List<IntPtr> tokens = new List<IntPtr>();
+            List<String> service_sids = new List<String>()
+            {
+                "S-1-5-18", // NT AUTHORITY\SYSTEM
+                "S-1-5-19", // NT AUTHORITY\LocalService
+                "S-1-5-20"  // NT AUTHORITY\NetworkService
+            };
+
+            IntPtr hSystemToken = IntPtr.Zero;
+            string account_sid = "";
+            if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
+            {
+                GrantAccessToWindowStationAndDesktop(account);
+                // Try to get SYSTEM token handle so we can impersonate to get full admin token
+                hSystemToken = GetSystemUserHandle();
+                account_sid = account.ToString();
+            }
+            bool impersonated = false;
+
+            try
+            {
+                IntPtr hSystemTokenDup = IntPtr.Zero;
+                if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid))
+                {
+                    // We need the SYSTEM token if we want to become one of those accounts, fail here
+                    throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
+                }
+                else if (hSystemToken != IntPtr.Zero)
+                {
+                    // We have the token, need to duplicate and impersonate
+                    bool dupResult = DuplicateTokenEx(
+                        hSystemToken,
+                        TokenAccessLevels.MaximumAllowed,
+                        IntPtr.Zero,
+                        SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
+                        TOKEN_TYPE.TokenPrimary,
+                        out hSystemTokenDup);
+                    int lastError = Marshal.GetLastWin32Error();
+                    CloseHandle(hSystemToken);
+
+                    if (!dupResult && service_sids.Contains(account_sid))
+                        throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM");
+                    else if (dupResult && account_sid != "S-1-5-18")
+                    {
+                        if (ImpersonateLoggedOnUser(hSystemTokenDup))
+                            impersonated = true;
+                        else if (service_sids.Contains(account_sid))
+                            throw new Win32Exception("Failed to impersonate as SYSTEM account");
+                    }
+                    // If SYSTEM impersonation failed but we're trying to become a regular user, just proceed;
+                    // might get a limited token in UAC-enabled cases, but better than nothing...
+                }
+
+                string domain = null;
+
+                if (service_sids.Contains(account_sid))
+                {
+                    // We're using a well-known service account, do a service logon instead of the actual flag set
+                    logonType = LogonType.LOGON32_LOGON_SERVICE;
+                    domain = "NT AUTHORITY";
+                    password = null;
+                    switch (account_sid)
+                    {
+                        case "S-1-5-18":
+                            tokens.Add(hSystemTokenDup);
+                            return tokens;
+                        case "S-1-5-19":
+                            username = "LocalService";
+                            break;
+                        case "S-1-5-20":
+                            username = "NetworkService";
+                            break;
+                    }
+                }
+                else
+                {
+                    // We are trying to become a local or domain account
+                    if (username.Contains(@"\"))
+                    {
+                        var user_split = username.Split(Convert.ToChar(@"\"));
+                        domain = user_split[0];
+                        username = user_split[1];
+                    }
+                    else if (username.Contains("@"))
+                        domain = null;
+                    else
+                        domain = ".";
+                }
+
+                IntPtr hToken = IntPtr.Zero;
+                if (!LogonUser(
+                    username,
+                    domain,
+                    password,
+                    logonType,
+                    LogonProvider.LOGON32_PROVIDER_DEFAULT,
+                    out hToken))
+                {
+                    throw new Win32Exception("LogonUser failed");
+                }
+
+                if (!service_sids.Contains(account_sid))
+                {
+                    // Try and get the elevated token for local/domain account
+                    IntPtr hTokenElevated = GetElevatedToken(hToken);
+                    tokens.Add(hTokenElevated);
+                }
+
+                // add the original token as a fallback
+                tokens.Add(hToken);
+            }
+            finally
+            {
+                if (impersonated)
+                    RevertToSelf();
+            }
+
+            return tokens;
+        }
+
+        private static IntPtr GetSystemUserHandle()
+        {
+            uint array_byte_size = 1024 * sizeof(uint);
+            IntPtr[] pids = new IntPtr[1024];
+            uint bytes_copied;
+
+            if (!EnumProcesses(pids, array_byte_size, out bytes_copied))
+            {
+                throw new Win32Exception("Failed to enumerate processes");
+            }
+            // TODO: Handle if bytes_copied is larger than the array size and rerun EnumProcesses with larger array
+            uint num_processes = bytes_copied / sizeof(uint);
+
+            for (uint i = 0; i < num_processes; i++)
+            {
+                IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, pids[i]);
+                if (hProcess != IntPtr.Zero)
+                {
+                    IntPtr hToken = IntPtr.Zero;
+                    // According to CreateProcessWithTokenW we require a token with
+                    //  TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
+                    // Also add in TOKEN_IMPERSONATE so we can get an impersontated token
+                    TokenAccessLevels desired_access = TokenAccessLevels.Query |
+                        TokenAccessLevels.Duplicate |
+                        TokenAccessLevels.AssignPrimary |
+                        TokenAccessLevels.Impersonate;
+
+                    if (OpenProcessToken(hProcess, desired_access, out hToken))
+                    {
+                        string sid = GetTokenUserSID(hToken);
+                        if (sid == "S-1-5-18")
+                        {
+                            CloseHandle(hProcess);
+                            return hToken;
+                        }
+                    }
+
+                    CloseHandle(hToken);
+                }
+                CloseHandle(hProcess);
+            }
+
+            return IntPtr.Zero;
+        }
+
+        private static string GetTokenUserSID(IntPtr hToken)
+        {
+            uint token_length;
+            string sid;
+
+            if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length))
+            {
+                int last_err = Marshal.GetLastWin32Error();
+                if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER
+                    throw new Win32Exception(last_err, "Failed to get TokenUser length");
+            }
+
+            IntPtr token_information = Marshal.AllocHGlobal((int)token_length);
+            try
+            {
+                if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, token_information, token_length, out token_length))
+                    throw new Win32Exception("Failed to get TokenUser information");
+
+                TOKEN_USER token_user = (TOKEN_USER)Marshal.PtrToStructure(token_information, typeof(TOKEN_USER));
+
+                if (!ConvertSidToStringSidW(token_user.User.Sid, out sid))
+                    throw new Win32Exception("Failed to get user SID");
+            }
+            finally
+            {
+                Marshal.FreeHGlobal(token_information);
+            }
+
+            return sid;
+        }
+
+        private 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;
+        }
+
+        private static uint GetProcessExitCode(IntPtr processHandle)
+        {
+            new NativeWaitHandle(processHandle).WaitOne();
+            uint exitCode;
+            if (!GetExitCodeProcess(processHandle, out exitCode))
+                throw new Win32Exception("Error getting process exit code");
+            return exitCode;
+        }
+
+        private static IntPtr GetElevatedToken(IntPtr hToken)
+        {
+            uint requestedLength;
+
+            IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int));
+
+            try
+            {
+                if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength))
+                    throw new Win32Exception("Unable to get TokenElevationType");
+
+                var tet = (TokenElevationType)Marshal.ReadInt32(pTokenInfo);
+
+                // we already have the best token we can get, just use it
+                if (tet != TokenElevationType.TokenElevationTypeLimited)
+                    return hToken;
+
+                GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength);
+
+                IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength);
+
+                if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength))
+                    throw new Win32Exception("Unable to get linked token");
+
+                IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken);
+
+                Marshal.FreeHGlobal(pLinkedToken);
+
+                return linkedToken;
+            }
+            finally
+            {
+                Marshal.FreeHGlobal(pTokenInfo);
+            }
+        }
+
+        private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account)
+        {
+            const int WindowStationAllAccess = 0x000f037f;
+            GrantAccess(account, GetProcessWindowStation(), WindowStationAllAccess);
+            const int DesktopRightsAllAccess = 0x000f01ff;
+            GrantAccess(account, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess);
+        }
+
+        private static void GrantAccess(SecurityIdentifier account, IntPtr handle, int accessMask)
+        {
+            SafeHandle safeHandle = new NoopSafeHandle(handle);
+            GenericSecurity security =
+                new GenericSecurity(false, ResourceType.WindowObject, safeHandle, AccessControlSections.Access);
+            security.AddAccessRule(
+                new GenericAccessRule(account, accessMask, AccessControlType.Allow));
+            security.Persist(safeHandle, AccessControlSections.Access);
+        }
+
+        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; }
+        }
+
+        private class GenericAccessRule : AccessRule
+        {
+            public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
+                base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type)
+            { }
+        }
+    }
+}
diff --git a/lib/ansible/module_utils/csharp/__init__.py b/lib/ansible/module_utils/csharp/__init__.py
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1
new file mode 100644
index 00000000000000..09591583c354e7
--- /dev/null
+++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1
@@ -0,0 +1,261 @@
+# Copyright (c) 2018 Ansible Project
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+Function Add-CSharpType {
+    <#
+    .SYNOPSIS
+    Compiles one or more C# scripts similar to Add-Type. This exposes
+    more configuration options that are useable within Ansible and it
+    also allows multiple C# sources to be compiled together.
+
+    .PARAMETER References
+    [String[]] A collection of C# scripts to compile together.
+
+    .PARAMETER IgnoreWarnings
+    [Switch] Whether to compile code that contains compiler warnings, by
+    default warnings will cause a compiler error.
+
+    .PARAMETER PassThru
+    [Switch] Whether to return the loaded Assembly
+
+    .PARAMETER AnsibleModule
+    TODO - This is an AnsibleModule object that is used to derive the
+    TempPath and Debug values.
+        TempPath is set to the TmpDir property of the class
+        IncludeDebugInfo is set when the Ansible verbosity is >= 3
+
+    .PARAMETER TempPath
+    [String] The temporary directory in which the dynamic assembly is
+    compiled to. This file is deleted once compilation is complete.
+    Cannot be used when AnsibleModule is set. This is a no-op when
+    running on PSCore.
+
+    .PARAMETER IncludeDebugInfo
+    [Switch] Whether to include debug information in the compiled
+    assembly. Cannot be used when AnsibleModule is set. This is a no-op
+    when running on PSCore.
+    #>
+    param(
+        [Parameter(Mandatory=$true)][AllowEmptyCollection()][String[]]$References,
+        [Switch]$IgnoreWarnings,
+        [Switch]$PassThru,
+        [Parameter(Mandatory=$true, ParameterSetName="Module")][Object]$AnsibleModule,
+        [Parameter(ParameterSetName="Manual")][String]$TempPath = $env:TMP,
+        [Parameter(ParameterSetName="Manual")][Switch]$IncludeDebugInfo
+    )
+    if ($null -eq $References -or $References.Length -eq 0) {
+        return
+    }
+
+    # define special symbols CORECLR, WINDOWS, UNIX if required
+    # the Is* variables are defined on PSCore, if absent we assume an
+    # older version of PowerShell under .NET Framework and Windows
+    $defined_symbols = [System.Collections.ArrayList]@()
+    $is_coreclr = Get-Variable -Name IsCoreCLR -ErrorAction SilentlyContinue
+    if ($null -ne $is_coreclr) {
+        if ($is_coreclr.Value) {
+            $defined_symbols.Add("CORECLR") > $null
+        }
+    }
+    $is_windows = Get-Variable -Name IsWindows -ErrorAction SilentlyContinue
+    if ($null -ne $is_windows) {
+        if ($is_windows.Value) {
+            $defined_symbols.Add("WINDOWS") > $null
+        } else {
+            $defined_symbols.Add("UNIX") > $null
+        }
+    } else {
+        $defined_symbols.Add("WINDOWS") > $null
+    }
+
+    # pattern used to find referenced assemblies in the code
+    $assembly_pattern = "^//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
+
+    # PSCore vs PSDesktop use different methods to compile the code,
+    # PSCore uses Roslyn and can compile the code purely in memory
+    # without touching the disk while PSDesktop uses CodeDom and csc.exe
+    # to compile the code. We branch out here and run each
+    # distribution's method to add our C# code.
+    if ($is_coreclr) {
+        # compile the code using Roslyn on PSCore
+
+        # Include the default assemblies using the logic in Add-Type
+        # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs
+        $assemblies = [System.Collections.Generic.HashSet`1[Microsoft.CodeAnalysis.MetadataReference]]@(
+            [Microsoft.CodeAnalysis.CompilationReference]::CreateFromFile(([System.Reflection.Assembly]::GetAssembly([PSObject])).Location)
+        )
+        $netcore_app_ref_folder = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName([PSObject].Assembly.Location), "ref")
+        foreach ($file in [System.IO.Directory]::EnumerateFiles($netcore_app_ref_folder, "*.dll", [System.IO.SearchOption]::TopDirectoryOnly)) {
+            $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($file)) > $null
+        }
+
+        # loop through the references, parse as a SyntaxTree and get
+        # referenced assemblies
+        $parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols)
+        $syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@()
+        foreach ($reference in $References) {
+            # scan through code and add any assemblies that match
+            # //AssemblyReference -Name ... [-CLR Core]
+            $sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
+            try {
+                while ($null -ne ($line = $sr.ReadLine())) {
+                    if ($line -imatch $assembly_pattern) {
+                        # verify the reference is not for .NET Framework
+                        if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
+                            continue
+                        }
+                        $assemblies.Add($Matches.Name) > $null
+                    }
+                }
+            } finally {
+                $sr.Close()
+            }
+            $syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null
+        }
+
+        # Release seems to contain the correct line numbers compared to
+        # debug,may need to keep a closer eye on this in the future
+        $compiler_options = (New-Object -TypeName Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions -ArgumentList @(
+            [Microsoft.CodeAnalysis.OutputKind]::DynamicallyLinkedLibrary
+        )).WithOptimizationLevel([Microsoft.CodeAnalysis.OptimizationLevel]::Release)
+
+        # set warnings to error out if IgnoreWarnings is not set
+        if (-not $IgnoreWarnings.IsPresent) {
+            $compiler_options = $compiler_options.WithGeneralDiagnosticOption([Microsoft.CodeAnalysis.ReportDiagnostic]::Error)
+        }
+
+        # create compilation object
+        $compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create(
+            [System.Guid]::NewGuid().ToString(),
+            $syntax_trees,
+            $assemblies,
+            $compiler_options
+        )
+
+        # Load the compiled code and pdb info, we do this so we can
+        # include line number in a stracktrace
+        $code_ms = New-Object -TypeName System.IO.MemoryStream
+        $pdb_ms = New-Object -TypeName System.IO.MemoryStream
+        try {
+            $emit_result = $compilation.Emit($code_ms, $pdb_ms)
+            if (-not $emit_result.Success) {
+                $errors = [System.Collections.ArrayList]@()
+
+                foreach ($e in $emit_result.Diagnostics) {
+                    # builds the error msg, based on logic in Add-Type
+                    # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#L1239
+                    if ($null -eq $e.Location.SourceTree) {
+                        $errors.Add($e.ToString()) > $null
+                        continue
+                    }
+
+                    $cancel_token = New-Object -TypeName System.Threading.CancellationToken -ArgumentList $false
+                    $text_lines = $e.Location.SourceTree.GetText($cancel_token).Lines
+                    $line_span = $e.Location.GetLineSpan()
+
+                    $diagnostic_message = $e.ToString()
+                    $error_line_string = $text_lines[$line_span.StartLinePosition.Line].ToString()
+                    $error_position = $line_span.StartLinePosition.Character
+
+                    $sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($diagnostic_message.Length + $error_line_string.Length * 2 + 4)
+                    $sb.AppendLine($diagnostic_message)
+                    $sb.AppendLine($error_line_string)
+
+                    for ($i = 0; $i -lt $error_line_string.Length; $i++) {
+                        if ([System.Char]::IsWhiteSpace($error_line_string[$i])) {
+                            continue
+                        }
+                        $sb.Append($error_line_string, 0, $i)
+                        $sb.Append(' ', [Math]::Max(0, $error_position - $i))
+                        $sb.Append("^")
+                        break
+                    }
+
+                    $errors.Add($sb.ToString()) > $null
+                }
+
+                throw [InvalidOperationException]"Failed to compile C# code:`r`n$($errors -join "`r`n")"
+            }
+
+            $code_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
+            $pdb_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
+            $compiled_assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($code_ms, $pdb_ms)
+        } finally {
+            $code_ms.Close()
+            $pdb_ms.Close()
+        }
+    } else {
+        # compile the code using CodeDom on PSDesktop
+
+        # configure compile options based on input
+        if ($PSCmdlet.ParameterSetName -eq "Module") {
+            $temp_path = $AnsibleModule.TmpDir
+            $include_debug = $AnsibleModule.Verbosity -ge 3
+        } else {
+            $temp_path = $TempPath
+            $include_debug = $IncludeDebugInfo.IsPresent
+        }
+        $compiler_options = [System.Collections.ArrayList]@("/optimize")
+        if ($defined_symbols.Count -gt 0) {
+            $compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null
+        }
+
+        $compile_parameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters
+        $compile_parameters.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray())
+        $compile_parameters.GenerateExecutable = $false
+        $compile_parameters.GenerateInMemory = $true
+        $compile_parameters.TreatWarningsAsErrors = (-not $IgnoreWarnings.IsPresent)
+        $compile_parameters.IncludeDebugInformation = $include_debug
+        $compile_parameters.TempFiles = (New-Object -TypeName System.CodeDom.Compiler.TempFileCollection -ArgumentList $temp_path, $false)
+
+        # Add-Type automatically references System.dll, System.Core.dll,
+        # and System.Management.Automation.dll which we replicate here
+        $assemblies = [System.Collections.Generic.HashSet`1[String]]@(
+            "System.dll",
+            "System.Core.dll",
+            ([System.Reflection.Assembly]::GetAssembly([PSObject])).Location
+        )
+
+        # create a code snippet for each reference and check if we need
+        # to reference any extra assemblies
+        # //AssemblyReference -Name ... [-CLR Framework]
+        $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
+        foreach ($reference in $References) {
+            $sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
+            try {
+                while ($null -ne ($line = $sr.ReadLine())) {
+                    if ($line -imatch $assembly_pattern) {
+                        # verify the reference is not for .NET Core
+                        if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
+                            continue
+                        }
+                        $assemblies.Add($Matches.Name) > $null
+                    }
+                }
+            } finally {
+                $sr.Close()
+            }
+            $compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
+        }
+        $compile_parameters.ReferencedAssemblies.AddRange($assemblies)
+
+        # compile the code together and check for errors
+        $provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider
+        $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units.ToArray())
+        if ($compile.Errors.HasErrors) {
+            $msg = "Failed to compile C# code: "
+            foreach ($e in $compile.Errors) {
+                $msg += "`r`n" + $e.ToString()
+            }
+            throw [InvalidOperationException]$msg
+        }
+        $compiled_assembly = $compile.CompiledAssembly
+    }
+
+    # return the compiled assembly if PassThru is set.
+    if ($PassThru) {
+        return $compiled_assembly
+    }
+}
+
+Export-ModuleMember -Function Add-CSharpType
diff --git a/lib/ansible/modules/windows/win_dsc.ps1 b/lib/ansible/modules/windows/win_dsc.ps1
index d80fe640a45a69..4903ebcf2182b4 100644
--- a/lib/ansible/modules/windows/win_dsc.ps1
+++ b/lib/ansible/modules/windows/win_dsc.ps1
@@ -14,29 +14,9 @@ $result = @{
     changed = $false
 }
 
-Function ConvertTo-HashtableFromPsCustomObject($psObject)
-{
-    $hashtable = @{}
-    $psObject | Get-Member -MemberType *Property | ForEach-Object {
-        $value = $psObject.($_.Name)
-        if ($value -is [PSObject])
-        {
-            $value = ConvertTo-HashtableFromPsCustomObject -myPsObject $value
-        }
-        $hashtable.($_.Name) = $value
-    }
-
-    return ,$hashtable
-}
-
 Function Cast-ToCimInstance($name, $value, $className)
 {
     # this converts a hashtable to a CimInstance
-    if ($value -is [PSObject])
-    {
-        # convert to hashtable
-        $value = ConvertTo-HashtableFromPsCustomObject -psObject $value
-    }
 
     $valueType = $value.GetType()
     if ($valueType -ne [hashtable])
diff --git a/lib/ansible/modules/windows/win_scheduled_task.ps1 b/lib/ansible/modules/windows/win_scheduled_task.ps1
index ba5d36a419669d..2fda4955268610 100644
--- a/lib/ansible/modules/windows/win_scheduled_task.ps1
+++ b/lib/ansible/modules/windows/win_scheduled_task.ps1
@@ -10,49 +10,7 @@
 
 $ErrorActionPreference = "Stop"
 
-Function ConvertTo-Hashtable {
-    param([Object]$Value)
-
-    if ($null -eq $Value) {
-        return $null
-    }
-    $value_type = $Value.GetType()
-    if ($value_type.IsGenericType) {
-        $value_type = $value_type.GetGenericTypeDefinition()
-    }
-    if ($value_type -eq [System.Collections.Generic.Dictionary`2]) {
-        $new_value = @{}
-        foreach ($kv in $Value.GetEnumerator()) {
-            $new_value.Add($kv.Key, (ConvertTo-Hashtable -Value $kv.Value))
-        }
-        return ,$new_value
-    } elseif ($value_type -eq [System.Collections.ArrayList]) {
-        for ($i = 0; $i -lt $Value.Count; $i++) {
-            $Value[$i] = ConvertTo-Hashtable -Value $Value[$i]
-        }
-        return ,$Value.ToArray()
-    } else {
-        return ,$Value
-    }
-}
-
 $params = Parse-Args -arguments $args -supports_check_mode $true
-
-# FUTURE: remove this once exec_wrapper has this behaviour inbuilt with the new
-# json changes in the exec_wrapper.
-# Currently ConvertFrom-Json creates a PSObject for the deserialized JSON and the
-# exec_wrapper converts all dicts as Hashtable. Unfortunately it doesn't
-# convert any dict in lists leaving to some confusing behaviour. We manually
-# use JavaScriptSerializer to ensure we have the type of objects to simply the
-# code in the module when it comes to type checking
-$params_json = ConvertTo-Json -InputObject $params -Depth 99 -Compress
-
-Add-Type -AssemblyName System.Web.Extensions
-$json = New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer
-$json.MaxJsonLength = [Int32]::MaxValue
-$json.RecursionLimit = [Int32]::MaxValue
-$params = ConvertTo-Hashtable -Value ($json.Deserialize($params_json, [System.Collections.Generic.Dictionary`2[[String], [Object]]]))
-
 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
 $diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
 $_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
diff --git a/lib/ansible/modules/windows/win_wait_for_process.ps1 b/lib/ansible/modules/windows/win_wait_for_process.ps1
index 5ddd1837bb9fcb..5b18e30eaaf040 100644
--- a/lib/ansible/modules/windows/win_wait_for_process.ps1
+++ b/lib/ansible/modules/windows/win_wait_for_process.ps1
@@ -9,13 +9,6 @@
 
 $ErrorActionPreference = "Stop"
 
-# NOTE: Ensure we get proper debug information when things fall over
-trap {
-    if ($null -eq $result) { $result = @{} }
-    $result.exception = "$($_ | Out-String)`r`n$($_.ScriptStackTrace)"
-    Fail-Json -obj $result -message "Uncaught exception: $($_.Exception.Message)"
-}
-
 $params = Parse-Args -arguments $args -supports_check_mode $true
 
 $process_name_exact = Get-AnsibleParam -obj $params -name "process_name_exact" -type "list"
diff --git a/lib/ansible/plugins/action/script.py b/lib/ansible/plugins/action/script.py
index dc8655f958779e..a7a338e5b290c6 100644
--- a/lib/ansible/plugins/action/script.py
+++ b/lib/ansible/plugins/action/script.py
@@ -22,7 +22,7 @@
 import shlex
 
 from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail, AnsibleActionSkip
-from ansible.executor.module_common import _create_powershell_wrapper
+from ansible.executor.powershell import module_manifest as ps_manifest
 from ansible.module_utils._text import to_bytes, to_native, to_text
 from ansible.plugins.action import ActionBase
 
@@ -129,10 +129,10 @@ def run(self, tmp=None, task_vars=None):
             if self._connection._shell.SHELL_FAMILY == "powershell":
                 # FIXME: use a more public method to get the exec payload
                 pc = self._play_context
-                exec_data = _create_powershell_wrapper(
+                exec_data = ps_manifest._create_powershell_wrapper(
                     to_bytes(script_cmd), {}, env_dict, self._task.async_val,
                     pc.become, pc.become_method, pc.become_user,
-                    pc.become_pass, pc.become_flags, scan_dependencies=False
+                    pc.become_pass, pc.become_flags, substyle="script"
                 )
                 script_cmd = "-"
 
diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py
index 0aa703db78f90e..c017278386bb58 100644
--- a/lib/ansible/plugins/shell/powershell.py
+++ b/lib/ansible/plugins/shell/powershell.py
@@ -69,1397 +69,6 @@
 if _powershell_version:
     _common_args = ['PowerShell', '-Version', _powershell_version] + _common_args[1:]
 
-exec_wrapper = br'''
-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
-
-    # NB: do not adjust the following line- it is replaced when doing non-streamed 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
-
-    $min_os_version = [version]$payload.min_os_version
-    if ($min_os_version -ne $null) {
-        $actual_os_version = [System.Environment]::OSVersion.Version
-        if ($actual_os_version -lt $min_os_version) {
-            $msg = "This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version"
-            Write-Output (ConvertTo-Json @{failed=$true;msg=$msg})
-            exit 1
-        }
-    }
-
-    $min_ps_version = [version]$payload.min_ps_version
-    if ($min_ps_version -ne $null) {
-        $actual_ps_version = $PSVersionTable.PSVersion
-        if ($actual_ps_version -lt $min_ps_version) {
-            $msg = "This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version"
-            Write-Output (ConvertTo-Json @{failed=$true;msg=$msg})
-            exit 1
-        }
-    }
-
-    $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 -WarningAction SilentlyContinue | 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
-
-    ForEach ($env_kv in $payload.environment.GetEnumerator()) {
-        # need to escape ' in both the key and value
-        $env_key = $env_kv.Key.ToString().Replace("'", "''")
-        $env_value = $env_kv.Value.ToString().Replace("'", "''")
-        $escaped_env_set = "[System.Environment]::SetEnvironmentVariable('{0}', '{1}')" -f $env_key, $env_value
-        $ps.AddStatement().AddScript($escaped_env_set) | 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").AddParameters(@{WarningAction="SilentlyContinue"}) | Out-Null
-        $ps.AddCommand("Out-Null") | Out-Null
-    }
-
-    # force input encoding to preamble-free UTF8 so PS sub-processes (eg,
-    # Start-Job) don't blow up. This is only required for WinRM, a PSRP
-    # runspace doesn't have a host console and this will bomb out
-    if ($host.Name -eq "ConsoleHost") {
-        $ps.AddStatement().AddScript("[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false") | 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)) {
-        $host.UI.WriteErrorLine($($ps.Streams.Error | Out-String))
-        $exit_code = $ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE")
-        If(-not $exit_code) {
-            $exit_code = 1
-        }
-        # need to use this instead of Exit keyword to prevent runspace from crashing with dynamic modules
-        $host.SetShouldExit($exit_code)
-    }
-}
-'''  # end leaf_exec
-
-become_wrapper = br'''
-Set-StrictMode -Version 2
-$ErrorActionPreference = "Stop"
-
-$helper_def = @"
-using Microsoft.Win32.SafeHandles;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Security.AccessControl;
-using System.Security.Principal;
-using System.Text;
-using System.Threading;
-
-namespace AnsibleBecome
-{
-    [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;
-        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
-        public byte[] _data1;
-        public Int32 dwFlags;
-        public Int16 wShowWindow;
-        public Int16 cbReserved2;
-        public IntPtr lpReserved2;
-        public SafeFileHandle hStdInput;
-        public SafeFileHandle hStdOutput;
-        public SafeFileHandle hStdError;
-        public STARTUPINFO()
-        {
-            cb = Marshal.SizeOf(this);
-        }
-    }
-
-    [StructLayout(LayoutKind.Sequential)]
-    public class STARTUPINFOEX
-    {
-        public STARTUPINFO startupInfo;
-        public IntPtr lpAttributeList;
-        public STARTUPINFOEX()
-        {
-            startupInfo = new STARTUPINFO();
-            startupInfo.cb = Marshal.SizeOf(this);
-        }
-    }
-
-    [StructLayout(LayoutKind.Sequential)]
-    public struct PROCESS_INFORMATION
-    {
-        public IntPtr hProcess;
-        public IntPtr hThread;
-        public int dwProcessId;
-        public int dwThreadId;
-    }
-
-    [StructLayout(LayoutKind.Sequential)]
-    public struct SID_AND_ATTRIBUTES
-    {
-        public IntPtr Sid;
-        public int Attributes;
-    }
-
-    public struct TOKEN_USER
-    {
-        public SID_AND_ATTRIBUTES User;
-    }
-
-    [Flags]
-    public enum StartupInfoFlags : uint
-    {
-        USESTDHANDLES = 0x00000100
-    }
-
-    [Flags]
-    public enum CreationFlags : uint
-    {
-        CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
-        CREATE_DEFAULT_ERROR_MODE = 0x04000000,
-        CREATE_NEW_CONSOLE = 0x00000010,
-        CREATE_SUSPENDED = 0x00000004,
-        CREATE_UNICODE_ENVIRONMENT = 0x00000400,
-        EXTENDED_STARTUPINFO_PRESENT = 0x00080000
-    }
-
-    public enum HandleFlags : uint
-    {
-        None = 0,
-        INHERIT = 1
-    }
-
-    [Flags]
-    public enum LogonFlags
-    {
-        LOGON_WITH_PROFILE = 0x00000001,
-        LOGON_NETCREDENTIALS_ONLY = 0x00000002
-    }
-
-    public enum LogonType
-    {
-        LOGON32_LOGON_INTERACTIVE = 2,
-        LOGON32_LOGON_NETWORK = 3,
-        LOGON32_LOGON_BATCH = 4,
-        LOGON32_LOGON_SERVICE = 5,
-        LOGON32_LOGON_UNLOCK = 7,
-        LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
-        LOGON32_LOGON_NEW_CREDENTIALS = 9
-    }
-
-    public enum LogonProvider
-    {
-        LOGON32_PROVIDER_DEFAULT = 0,
-    }
-
-    public enum TokenInformationClass
-    {
-        TokenUser = 1,
-        TokenType = 8,
-        TokenImpersonationLevel = 9,
-        TokenElevationType = 18,
-        TokenLinkedToken = 19,
-    }
-
-    public enum TokenElevationType
-    {
-        TokenElevationTypeDefault = 1,
-        TokenElevationTypeFull,
-        TokenElevationTypeLimited
-    }
-
-    [Flags]
-    public enum ProcessAccessFlags : uint
-    {
-        PROCESS_QUERY_INFORMATION = 0x00000400,
-    }
-
-    public enum SECURITY_IMPERSONATION_LEVEL
-    {
-        SecurityImpersonation,
-    }
-
-    public enum TOKEN_TYPE
-    {
-        TokenPrimary = 1,
-        TokenImpersonation
-    }
-
-    class NativeWaitHandle : WaitHandle
-    {
-        public NativeWaitHandle(IntPtr handle)
-        {
-            this.SafeWaitHandle = new SafeWaitHandle(handle, false);
-        }
-    }
-
-    public class Win32Exception : System.ComponentModel.Win32Exception
-    {
-        private string _msg;
-        public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
-        public Win32Exception(int errorCode, string message) : base(errorCode)
-        {
-            _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
-        }
-        public override string Message { get { return _msg; } }
-        public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
-    }
-
-    public class CommandResult
-    {
-        public string StandardOut { get; internal set; }
-        public string StandardError { get; internal set; }
-        public uint ExitCode { get; internal set; }
-    }
-
-    public class BecomeUtil
-    {
-        [DllImport("advapi32.dll", SetLastError = true)]
-        private static extern bool LogonUser(
-            string lpszUsername,
-            string lpszDomain,
-            string lpszPassword,
-            LogonType dwLogonType,
-            LogonProvider dwLogonProvider,
-            out IntPtr phToken);
-
-        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
-        private static extern bool CreateProcessWithTokenW(
-            IntPtr hToken,
-            LogonFlags dwLogonFlags,
-            [MarshalAs(UnmanagedType.LPTStr)]
-            string lpApplicationName,
-            StringBuilder lpCommandLine,
-            CreationFlags dwCreationFlags,
-            IntPtr lpEnvironment,
-            [MarshalAs(UnmanagedType.LPTStr)]
-            string lpCurrentDirectory,
-            STARTUPINFOEX lpStartupInfo,
-            out PROCESS_INFORMATION lpProcessInformation);
-
-        [DllImport("kernel32.dll")]
-        private static extern bool CreatePipe(
-            out SafeFileHandle hReadPipe,
-            out SafeFileHandle hWritePipe,
-            SECURITY_ATTRIBUTES lpPipeAttributes,
-            uint nSize);
-
-        [DllImport("kernel32.dll", SetLastError = true)]
-        private static extern bool SetHandleInformation(
-            SafeFileHandle hObject,
-            HandleFlags dwMask,
-            int dwFlags);
-
-        [DllImport("kernel32.dll", SetLastError = true)]
-        private static extern bool GetExitCodeProcess(
-            IntPtr hProcess,
-            out uint lpExitCode);
-
-        [DllImport("kernel32.dll", SetLastError = true)]
-        private static extern bool CloseHandle(
-            IntPtr hObject);
-
-        [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();
-
-        [DllImport("advapi32.dll", SetLastError = true)]
-        private static extern bool GetTokenInformation(
-            IntPtr TokenHandle,
-            TokenInformationClass TokenInformationClass,
-            IntPtr TokenInformation,
-            uint TokenInformationLength,
-            out uint ReturnLength);
-
-        [DllImport("psapi.dll", SetLastError = true)]
-        private static extern bool EnumProcesses(
-            [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)]
-                [In][Out] IntPtr[] processIds,
-            uint cb,
-            [MarshalAs(UnmanagedType.U4)]
-                out uint pBytesReturned);
-
-        [DllImport("kernel32.dll", SetLastError = true)]
-        private static extern IntPtr OpenProcess(
-            ProcessAccessFlags processAccess,
-            bool bInheritHandle,
-            IntPtr processId);
-
-        [DllImport("advapi32.dll", SetLastError = true)]
-        private static extern bool OpenProcessToken(
-            IntPtr ProcessHandle,
-            TokenAccessLevels DesiredAccess,
-            out IntPtr TokenHandle);
-
-        [DllImport("advapi32.dll", SetLastError = true)]
-        private static extern bool ConvertSidToStringSidW(
-            IntPtr pSID,
-            [MarshalAs(UnmanagedType.LPTStr)]
-            out string StringSid);
-
-        [DllImport("advapi32", SetLastError = true)]
-        private static extern bool DuplicateTokenEx(
-            IntPtr hExistingToken,
-            TokenAccessLevels dwDesiredAccess,
-            IntPtr lpTokenAttributes,
-            SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
-            TOKEN_TYPE TokenType,
-            out IntPtr phNewToken);
-
-        [DllImport("advapi32.dll", SetLastError = true)]
-        private static extern bool ImpersonateLoggedOnUser(
-            IntPtr hToken);
-
-        [DllImport("advapi32.dll", SetLastError = true)]
-        private static extern bool RevertToSelf();
-
-        public static CommandResult RunAsUser(string username, string password, string lpCommandLine,
-            string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType)
-        {
-            SecurityIdentifier account = null;
-            if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
-            {
-                account = GetBecomeSid(username);
-            }
-
-            STARTUPINFOEX si = new STARTUPINFOEX();
-            si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
-
-            SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES();
-            pipesec.bInheritHandle = true;
-
-            // Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
-            SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write;
-            if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
-                throw new Win32Exception("STDOUT pipe setup failed");
-            if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
-                throw new Win32Exception("STDOUT pipe handle setup failed");
-
-            if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0))
-                throw new Win32Exception("STDERR pipe setup failed");
-            if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0))
-                throw new Win32Exception("STDERR pipe handle setup failed");
-
-            if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0))
-                throw new Win32Exception("STDIN pipe setup failed");
-            if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0))
-                throw new Win32Exception("STDIN pipe handle setup failed");
-
-            si.startupInfo.hStdOutput = stdout_write;
-            si.startupInfo.hStdError = stderr_write;
-            si.startupInfo.hStdInput = stdin_read;
-
-            // Setup the stdin buffer
-            UTF8Encoding utf8_encoding = new UTF8Encoding(false);
-            FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768);
-            StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
-
-            // Create the environment block if set
-            IntPtr lpEnvironment = IntPtr.Zero;
-
-            CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
-
-            PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
-
-            // Get the user tokens to try running processes with
-            List<IntPtr> tokens = GetUserTokens(account, username, password, logonType);
-
-            bool launch_success = false;
-            foreach (IntPtr token in tokens)
-            {
-                if (CreateProcessWithTokenW(
-                    token,
-                    logonFlags,
-                    null,
-                    new StringBuilder(lpCommandLine),
-                    startup_flags,
-                    lpEnvironment,
-                    lpCurrentDirectory,
-                    si,
-                    out pi))
-                {
-                    launch_success = true;
-                    break;
-                }
-            }
-
-            if (!launch_success)
-                throw new Win32Exception("Failed to start become process");
-
-            CommandResult result = new CommandResult();
-            // Setup the output buffers and get stdout/stderr
-            FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096);
-            StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
-            stdout_write.Close();
-
-            FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096);
-            StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
-            stderr_write.Close();
-
-            stdin.WriteLine(stdinInput);
-            stdin.Close();
-
-            string stdout_str, stderr_str = null;
-            GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
-            UInt32 rc = GetProcessExitCode(pi.hProcess);
-
-            result.StandardOut = stdout_str;
-            result.StandardError = stderr_str;
-            result.ExitCode = rc;
-
-            return result;
-        }
-
-        private static SecurityIdentifier GetBecomeSid(string username)
-        {
-            NTAccount account = new NTAccount(username);
-            try
-            {
-                SecurityIdentifier security_identifier = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
-                return security_identifier;
-            }
-            catch (IdentityNotMappedException ex)
-            {
-                throw new Exception(String.Format("Unable to find become user {0}: {1}", username, ex.Message));
-            }
-        }
-
-        private static List<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType)
-        {
-            List<IntPtr> tokens = new List<IntPtr>();
-            List<String> service_sids = new List<String>()
-            {
-                "S-1-5-18", // NT AUTHORITY\SYSTEM
-                "S-1-5-19", // NT AUTHORITY\LocalService
-                "S-1-5-20"  // NT AUTHORITY\NetworkService
-            };
-
-            IntPtr hSystemToken = IntPtr.Zero;
-            string account_sid = "";
-            if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
-            {
-                GrantAccessToWindowStationAndDesktop(account);
-                // Try to get SYSTEM token handle so we can impersonate to get full admin token
-                hSystemToken = GetSystemUserHandle();
-                account_sid = account.ToString();
-            }
-            bool impersonated = false;
-
-            try
-            {
-                IntPtr hSystemTokenDup = IntPtr.Zero;
-                if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid))
-                {
-                    // We need the SYSTEM token if we want to become one of those accounts, fail here
-                    throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
-                }
-                else if (hSystemToken != IntPtr.Zero)
-                {
-                    // We have the token, need to duplicate and impersonate
-                    bool dupResult = DuplicateTokenEx(
-                        hSystemToken,
-                        TokenAccessLevels.MaximumAllowed,
-                        IntPtr.Zero,
-                        SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
-                        TOKEN_TYPE.TokenPrimary,
-                        out hSystemTokenDup);
-                    int lastError = Marshal.GetLastWin32Error();
-                    CloseHandle(hSystemToken);
-
-                    if (!dupResult && service_sids.Contains(account_sid))
-                        throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM");
-                    else if (dupResult && account_sid != "S-1-5-18")
-                    {
-                        if (ImpersonateLoggedOnUser(hSystemTokenDup))
-                            impersonated = true;
-                        else if (service_sids.Contains(account_sid))
-                            throw new Win32Exception("Failed to impersonate as SYSTEM account");
-                    }
-                    // If SYSTEM impersonation failed but we're trying to become a regular user, just proceed;
-                    // might get a limited token in UAC-enabled cases, but better than nothing...
-                }
-
-                string domain = null;
-
-                if (service_sids.Contains(account_sid))
-                {
-                    // We're using a well-known service account, do a service logon instead of the actual flag set
-                    logonType = LogonType.LOGON32_LOGON_SERVICE;
-                    domain = "NT AUTHORITY";
-                    password = null;
-                    switch (account_sid)
-                    {
-                        case "S-1-5-18":
-                            tokens.Add(hSystemTokenDup);
-                            return tokens;
-                        case "S-1-5-19":
-                            username = "LocalService";
-                            break;
-                        case "S-1-5-20":
-                            username = "NetworkService";
-                            break;
-                    }
-                }
-                else
-                {
-                    // We are trying to become a local or domain account
-                    if (username.Contains(@"\"))
-                    {
-                        var user_split = username.Split(Convert.ToChar(@"\"));
-                        domain = user_split[0];
-                        username = user_split[1];
-                    }
-                    else if (username.Contains("@"))
-                        domain = null;
-                    else
-                        domain = ".";
-                }
-
-                IntPtr hToken = IntPtr.Zero;
-                if (!LogonUser(
-                    username,
-                    domain,
-                    password,
-                    logonType,
-                    LogonProvider.LOGON32_PROVIDER_DEFAULT,
-                    out hToken))
-                {
-                    throw new Win32Exception("LogonUser failed");
-                }
-
-                if (!service_sids.Contains(account_sid))
-                {
-                    // Try and get the elevated token for local/domain account
-                    IntPtr hTokenElevated = GetElevatedToken(hToken);
-                    tokens.Add(hTokenElevated);
-                }
-
-                // add the original token as a fallback
-                tokens.Add(hToken);
-            }
-            finally
-            {
-                if (impersonated)
-                    RevertToSelf();
-            }
-
-            return tokens;
-        }
-
-        private static IntPtr GetSystemUserHandle()
-        {
-            uint array_byte_size = 1024 * sizeof(uint);
-            IntPtr[] pids = new IntPtr[1024];
-            uint bytes_copied;
-
-            if (!EnumProcesses(pids, array_byte_size, out bytes_copied))
-            {
-                throw new Win32Exception("Failed to enumerate processes");
-            }
-            // TODO: Handle if bytes_copied is larger than the array size and rerun EnumProcesses with larger array
-            uint num_processes = bytes_copied / sizeof(uint);
-
-            for (uint i = 0; i < num_processes; i++)
-            {
-                IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, pids[i]);
-                if (hProcess != IntPtr.Zero)
-                {
-                    IntPtr hToken = IntPtr.Zero;
-                    // According to CreateProcessWithTokenW we require a token with
-                    //  TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
-                    // Also add in TOKEN_IMPERSONATE so we can get an impersontated token
-                    TokenAccessLevels desired_access = TokenAccessLevels.Query |
-                        TokenAccessLevels.Duplicate |
-                        TokenAccessLevels.AssignPrimary |
-                        TokenAccessLevels.Impersonate;
-
-                    if (OpenProcessToken(hProcess, desired_access, out hToken))
-                    {
-                        string sid = GetTokenUserSID(hToken);
-                        if (sid == "S-1-5-18")
-                        {
-                            CloseHandle(hProcess);
-                            return hToken;
-                        }
-                    }
-
-                    CloseHandle(hToken);
-                }
-                CloseHandle(hProcess);
-            }
-
-            return IntPtr.Zero;
-        }
-
-        private static string GetTokenUserSID(IntPtr hToken)
-        {
-            uint token_length;
-            string sid;
-
-            if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length))
-            {
-                int last_err = Marshal.GetLastWin32Error();
-                if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER
-                    throw new Win32Exception(last_err, "Failed to get TokenUser length");
-            }
-
-            IntPtr token_information = Marshal.AllocHGlobal((int)token_length);
-            try
-            {
-                if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, token_information, token_length, out token_length))
-                    throw new Win32Exception("Failed to get TokenUser information");
-
-                TOKEN_USER token_user = (TOKEN_USER)Marshal.PtrToStructure(token_information, typeof(TOKEN_USER));
-
-                if (!ConvertSidToStringSidW(token_user.User.Sid, out sid))
-                    throw new Win32Exception("Failed to get user SID");
-            }
-            finally
-            {
-                Marshal.FreeHGlobal(token_information);
-            }
-
-            return sid;
-        }
-
-        private 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;
-        }
-
-        private static uint GetProcessExitCode(IntPtr processHandle)
-        {
-            new NativeWaitHandle(processHandle).WaitOne();
-            uint exitCode;
-            if (!GetExitCodeProcess(processHandle, out exitCode))
-                throw new Win32Exception("Error getting process exit code");
-            return exitCode;
-        }
-
-        private static IntPtr GetElevatedToken(IntPtr hToken)
-        {
-            uint requestedLength;
-
-            IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int));
-
-            try
-            {
-                if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength))
-                    throw new Win32Exception("Unable to get TokenElevationType");
-
-                var tet = (TokenElevationType)Marshal.ReadInt32(pTokenInfo);
-
-                // we already have the best token we can get, just use it
-                if (tet != TokenElevationType.TokenElevationTypeLimited)
-                    return hToken;
-
-                GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength);
-
-                IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength);
-
-                if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength))
-                    throw new Win32Exception("Unable to get linked token");
-
-                IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken);
-
-                Marshal.FreeHGlobal(pLinkedToken);
-
-                return linkedToken;
-            }
-            finally
-            {
-                Marshal.FreeHGlobal(pTokenInfo);
-            }
-        }
-
-        private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account)
-        {
-            const int WindowStationAllAccess = 0x000f037f;
-            GrantAccess(account, GetProcessWindowStation(), WindowStationAllAccess);
-            const int DesktopRightsAllAccess = 0x000f01ff;
-            GrantAccess(account, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess);
-        }
-
-        private static void GrantAccess(SecurityIdentifier account, IntPtr handle, int accessMask)
-        {
-            SafeHandle safeHandle = new NoopSafeHandle(handle);
-            GenericSecurity security =
-                new GenericSecurity(false, ResourceType.WindowObject, safeHandle, AccessControlSections.Access);
-            security.AddAccessRule(
-                new GenericAccessRule(account, accessMask, AccessControlType.Allow));
-            security.Persist(safeHandle, AccessControlSections.Access);
-        }
-
-        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; }
-        }
-
-        private class GenericAccessRule : AccessRule
-        {
-            public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
-                base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type)
-            { }
-        }
-    }
-}
-"@
-
-# due to the command line size limitations of CreateProcessWithTokenW, we
-# execute a simple PS script that executes our full exec_wrapper so no files
-# touch the disk
-$become_exec_wrapper = {
-    chcp.com 65001 > $null
-    $ProgressPreference = "SilentlyContinue"
-    $exec_wrapper_str = [System.Console]::In.ReadToEnd()
-    $exec_wrapper = [ScriptBlock]::Create($exec_wrapper_str)
-    &$exec_wrapper
-}
-
-$exec_wrapper = {
-    &chcp.com 65001 > $null
-    Set-StrictMode -Version 2
-    $DebugPreference = "Continue"
-    $ErrorActionPreference = "Stop"
-
-    Function ConvertTo-HashtableFromPsCustomObject($myPsObject) {
-        $output = @{}
-        $myPsObject | Get-Member -MemberType *Property | % {
-            $val = $myPsObject.($_.name)
-            if ($val -is [psobject]) {
-                $val = ConvertTo-HashtableFromPsCustomObject -myPsObject $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. Do not change this as it is set become before being passed to the
-    # become process.
-    $json_raw = ""
-
-    If (-not $json_raw) {
-        Write-Error "no input given" -Category InvalidArgument
-    }
-
-    $payload = ConvertTo-HashtableFromPsCustomObject -myPsObject (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 -WarningAction SilentlyContinue | Out-Null
-    }
-
-    $output = $entrypoint.Run($payload)
-    # base64 encode the output so the non-ascii characters are preserved
-    Write-Output ([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Write-Output ($output | Out-String)))))
-} # end exec_wrapper
-
-Function Dump-Error ($excep, $msg=$null) {
-    $eo = @{failed=$true}
-
-    $exception_message = $excep.Exception.Message
-    if ($null -ne $msg) {
-        $exception_message = "$($msg): $exception_message"
-    }
-    $eo.msg = $exception_message
-    $eo.exception = $excep | Out-String
-    $host.SetShouldExit(1)
-
-    $eo | ConvertTo-Json -Depth 10 -Compress
-}
-
-Function Parse-EnumValue($enum, $flag_type, $value, $prefix) {
-    $raw_enum_value = "$prefix$($value.ToUpper())"
-    try {
-        $enum_value = [Enum]::Parse($enum, $raw_enum_value)
-    } catch [System.ArgumentException] {
-        $valid_options = [Enum]::GetNames($enum) | ForEach-Object { $_.Substring($prefix.Length).ToLower() }
-        throw "become_flags $flag_type value '$value' is not valid, valid values are: $($valid_options -join ", ")"
-    }
-    return $enum_value
-}
-
-Function Parse-BecomeFlags($flags) {
-    $logon_type = [AnsibleBecome.LogonType]::LOGON32_LOGON_INTERACTIVE
-    $logon_flags = [AnsibleBecome.LogonFlags]::LOGON_WITH_PROFILE
-
-    if ($flags -eq $null -or $flags -eq "") {
-        $flag_split = @()
-    } elseif ($flags -is [string]) {
-        $flag_split = $flags.Split(" ")
-    } else {
-        throw "become_flags must be a string, was $($flags.GetType())"
-    }
-
-    foreach ($flag in $flag_split) {
-        $split = $flag.Split("=")
-        if ($split.Count -ne 2) {
-            throw "become_flags entry '$flag' is in an invalid format, must be a key=value pair"
-        }
-        $flag_key = $split[0]
-        $flag_value = $split[1]
-        if ($flag_key -eq "logon_type") {
-            $enum_details = @{
-                enum = [AnsibleBecome.LogonType]
-                flag_type = $flag_key
-                value = $flag_value
-                prefix = "LOGON32_LOGON_"
-            }
-            $logon_type = Parse-EnumValue @enum_details
-        } elseif ($flag_key -eq "logon_flags") {
-            $logon_flag_values = $flag_value.Split(",")
-            $logon_flags = 0 -as [AnsibleBecome.LogonFlags]
-            foreach ($logon_flag_value in $logon_flag_values) {
-                if ($logon_flag_value -eq "") {
-                    continue
-                }
-                $enum_details = @{
-                    enum = [AnsibleBecome.LogonFlags]
-                    flag_type = $flag_key
-                    value = $logon_flag_value
-                    prefix = "LOGON_"
-                }
-                $logon_flag = Parse-EnumValue @enum_details
-                $logon_flags = $logon_flags -bor $logon_flag
-            }
-        } else {
-            throw "become_flags key '$flag_key' is not a valid runas flag, must be 'logon_type' or 'logon_flags'"
-        }
-    }
-
-    return $logon_type, [AnsibleBecome.LogonFlags]$logon_flags
-}
-
-Function Run($payload) {
-    # NB: action popping handled inside subprocess wrapper
-
-    $original_tmp = $env:TMP
-    $remote_tmp = $payload["module_args"]["_ansible_remote_tmp"]
-    $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
-    if ($null -eq $remote_tmp) {
-        $remote_tmp = $original_tmp
-    }
-
-    # become process is run under a different console to the WinRM one so we
-    # need to set the UTF-8 codepage again
-    $env:TMP = $remote_tmp
-    Add-Type -TypeDefinition $helper_def -Debug:$false
-    $env:TMP = $original_tmp
-
-    $username = $payload.become_user
-    $password = $payload.become_password
-    try {
-        $logon_type, $logon_flags = Parse-BecomeFlags -flags $payload.become_flags
-    } catch {
-        Dump-Error -excep $_ -msg "Failed to parse become_flags '$($payload.become_flags)'"
-        return $null
-    }
-
-    # NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via small
-    # wrapper which calls our read wrapper passed through stdin. Cannot use 'powershell -' as
-    # the $ErrorActionPreference is always set to Stop and cannot be changed
-    $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
-    $exec_wrapper = $exec_wrapper.ToString().Replace('$json_raw = ""', "`$json_raw = '$payload_string'")
-    $rc = 0
-
-    $exec_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($become_exec_wrapper.ToString()))
-    $lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command")
-    $lp_current_directory = "$env:SystemRoot"
-
-    Try {
-        $result = [AnsibleBecome.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type)
-        $stdout = $result.StandardOut
-        $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim()))
-        $stderr = $result.StandardError
-        $rc = $result.ExitCode
-
-        $host.UI.WriteLine($stdout)
-        $host.UI.WriteErrorLine($stderr.Trim())
-    } Catch {
-        $excep = $_
-        Dump-Error -excep $excep -msg "Failed to become user $username"
-    }
-    $host.SetShouldExit($rc)
-}
-'''
-
-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 = {
-    # help to debug any errors in the exec_wrapper or async_watchdog by generating
-    # an error log in case of a terminating error
-    trap {
-        $log_path = "$($env:TEMP)\async-exec-wrapper-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ")-error.txt"
-        $error_msg = "Error while running the async exec wrapper`r`n$_`r`n$($_.ScriptStackTrace)"
-        Set-Content -Path $log_path -Value $error_msg
-        throw $_
-    }
-
-    &chcp.com 65001 > $null
-    $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;
-    }
-
-    # store the pipe name and no. of bytes to read, these are populated by the
-    # Run function before being run - do not remove or change
-    $pipe_name = ""
-    $bytes_length = 0
-
-    # 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
-    $input_bytes = New-Object -TypeName byte[] -ArgumentList $bytes_length
-    $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @(
-        ".",  # localhost
-        $pipe_name,
-        [System.IO.Pipes.PipeDirection]::In,
-        [System.IO.Pipes.PipeOptions]::None,
-        [System.Security.Principal.TokenImpersonationLevel]::Anonymous
-    )
-    try {
-        $pipe.Connect()
-        $pipe.Read($input_bytes, 0, $bytes_length) > $null
-    } finally {
-        $pipe.Close()
-    }
-    $json_raw = [System.Text.Encoding]::UTF8.GetString($input_bytes)
-
-    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 -WarningAction SilentlyContinue | Out-Null
-    }
-
-    $output = $entrypoint.Run($payload)
-
-    Write-Output $output
-} # end exec_wrapper
-
-
-Function Run($payload) {
-    if ($payload.environment.ContainsKey("ANSIBLE_ASYNC_DIR")) {
-        $async_dir = $payload.environment.ANSIBLE_ASYNC_DIR
-    } else {
-        $async_dir = "%USERPROFILE%\.ansible_async"
-    }
-    $async_dir = [System.Environment]::ExpandEnvironmentVariables($async_dir)
-
-    # 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($async_dir, $local_jid)
-
-    $payload.async_results_path = $results_path
-
-    [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null
-
-    # can't use anonymous pipes as the spawned process will not be a child due to
-    # the way WMI works, use a named pipe with a random name instead and set to
-    # only allow current user to read from the pipe
-    $pipe_name = "ansible-async-$jid-$([guid]::NewGuid())"
-    $current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User
-    $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
-    $payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($payload_string)
-
-    $pipe_sec = New-Object -TypeName System.IO.Pipes.PipeSecurity
-    $pipe_ar = New-Object -TypeName System.IO.Pipes.PipeAccessRule -ArgumentList @(
-        $current_user,
-        [System.IO.Pipes.PipeAccessRights]::Read,
-        [System.Security.AccessControl.AccessControlType]::Allow
-    )
-    $pipe_sec.AddAccessRule($pipe_ar)
-    $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeServerStream -ArgumentList @(
-        $pipe_name,
-        [System.IO.Pipes.PipeDirection]::Out,
-        1,
-        [System.IO.Pipes.PipeTransmissionMode]::Byte,
-        [System.IO.Pipes.PipeOptions]::Asynchronous,
-        0,
-        0,
-        $pipe_sec
-    )
-
-    try {
-        $exec_wrapper_str = $exec_wrapper.ToString()
-        $exec_wrapper_str = $exec_wrapper_str.Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"")
-        $exec_wrapper_str = $exec_wrapper_str.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)")
-
-        $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper_str))
-        $exec_args = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command"
-
-        # not all connection plugins support breakaway from job that is required
-        # for async, Win32_Process.Create() is still able to escape so we use
-        # that here
-        $process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine=$exec_args}
-        $rc = $process.ReturnValue
-        if ($rc -ne 0) {
-            $error_msg = switch($rc) {
-                2 { "Access denied" }
-                3 { "Insufficient privilege" }
-                8 { "Unknown failure" }
-                9 { "Path not found" }
-                21 { "Invalid parameter" }
-                default { "Other" }
-            }
-            throw "Failed to start async process: $rc ($error_msg)"
-        }
-        $watchdog_pid = $process.ProcessId
-
-        # populate initial results before we send the async data 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
-
-        # wait until the client connects, throw an error if the timeout is reached
-        $wait_async = $pipe.BeginWaitForConnection($null, $null)
-        $wait_async.AsyncWaitHandle.WaitOne(5000) > $null
-        if (-not $wait_async.IsCompleted) {
-            throw "timeout while waiting for child process to connect to named pipe"
-        }
-        $pipe.EndWaitForConnection($wait_async)
-
-        # write the exec manifest to the child process
-        $pipe.Write($payload_bytes, 0, $payload_bytes.Count)
-        $pipe.Flush()
-        $pipe.WaitForPipeDrain()
-    } finally {
-        $pipe.Close()
-    }
-
-    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"
-            # return the output back to Ansible to help with debugging errors
-            $result.stdout = $job_output | Out-String
-            $result.stderr = $job_error | Out-String
-        }
-
-        # 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
-
-from ansible.plugins import AnsiblePlugin
-
 
 class ShellModule(ShellBase):
 
@@ -1691,7 +300,7 @@ def _encode_script(self, script, as_list=False, strict_mode=True, preserve_rc=Tr
         script = to_text(script)
 
         if script == u'-':
-            cmd_parts = _common_args + ['-']
+            cmd_parts = _common_args + ['-Command', '-']
 
         else:
             if strict_mode:
diff --git a/setup.py b/setup.py
index d8b2017368f771..8f3d268b974131 100644
--- a/setup.py
+++ b/setup.py
@@ -249,6 +249,9 @@ def get_dynamic_setup_params():
     packages=find_packages('lib'),
     package_data={
         '': [
+            'executor/powershell/*.ps1',
+            'module_utils/csharp/*.cs',
+            'module_utils/csharp/*/*.cs',
             'module_utils/powershell/*.psm1',
             'module_utils/powershell/*/*.psm1',
             'modules/windows/*.ps1',
diff --git a/test/integration/targets/win_async_wrapper/tasks/main.yml b/test/integration/targets/win_async_wrapper/tasks/main.yml
index 756e3ee78037fa..91b45846edd8e0 100644
--- a/test/integration/targets/win_async_wrapper/tasks/main.yml
+++ b/test/integration/targets/win_async_wrapper/tasks/main.yml
@@ -148,8 +148,7 @@
     - asyncresult is finished
     - asyncresult is not changed
     - asyncresult is failed
-# TODO: re-enable after catastrophic failure behavior is cleaned up
-#    - asyncresult.msg is search('failing via exception')
+    - 'asyncresult.msg == "Unhandled exception while executing module: failing via exception"'
 
 - name: echo some non ascii characters
   win_command: cmd.exe /c echo über den Fußgängerübergang gehen
diff --git a/test/integration/targets/win_become/tasks/main.yml b/test/integration/targets/win_become/tasks/main.yml
index 1f2f241884a832..0aab437471061c 100644
--- a/test/integration/targets/win_become/tasks/main.yml
+++ b/test/integration/targets/win_become/tasks/main.yml
@@ -136,7 +136,8 @@
     register: become_invalid_pass
     failed_when:
     - '"Failed to become user " + become_test_username not in become_invalid_pass.msg'
-    - '"LogonUser failed (The user name or password is incorrect, Win32ErrorCode 1326)" not in become_invalid_pass.msg'
+    - '"LogonUser failed" not in become_invalid_pass.msg'
+    - '"Win32ErrorCode 1326)" not in become_invalid_pass.msg'
     
   - name: test become with SYSTEM account
     win_whoami:
@@ -206,21 +207,21 @@
     become_flags: logon_type=batch invalid_flags=a
     become_method: runas
     register: failed_flags_invalid_key
-    failed_when: "failed_flags_invalid_key.msg != \"Failed to parse become_flags 'logon_type=batch invalid_flags=a': become_flags key 'invalid_flags' is not a valid runas flag, must be 'logon_type' or 'logon_flags'\""
+    failed_when: "failed_flags_invalid_key.msg != \"internal error: failed to parse become_flags 'logon_type=batch invalid_flags=a': become_flags key 'invalid_flags' is not a valid runas flag, must be 'logon_type' or 'logon_flags'\""
 
   - name: test failure with invalid logon_type
     vars: *become_vars
     win_whoami:
     become_flags: logon_type=invalid
     register: failed_flags_invalid_type
-    failed_when: "failed_flags_invalid_type.msg != \"Failed to parse become_flags 'logon_type=invalid': become_flags logon_type value 'invalid' is not valid, valid values are: interactive, network, batch, service, unlock, network_cleartext, new_credentials\""
+    failed_when: "failed_flags_invalid_type.msg != \"internal error: failed to parse become_flags 'logon_type=invalid': become_flags logon_type value 'invalid' is not valid, valid values are: interactive, network, batch, service, unlock, network_cleartext, new_credentials\""
 
   - name: test failure with invalid logon_flag
     vars: *become_vars
     win_whoami:
     become_flags: logon_flags=with_profile,invalid
     register: failed_flags_invalid_flag
-    failed_when: "failed_flags_invalid_flag.msg != \"Failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
+    failed_when: "failed_flags_invalid_flag.msg != \"internal error: failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
 
   # Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway
   - name: check if we are running on a dinosaur, neanderthal or an OS of the modern age
diff --git a/test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1 b/test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1
new file mode 100644
index 00000000000000..9a5918f943f3c8
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1
@@ -0,0 +1,40 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+
+Function Assert-Equals($actual, $expected) {
+    if ($actual -cne $expected) {
+        $call_stack = (Get-PSCallStack)[1]
+        $error_msg = "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: $($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+        Fail-Json -obj $result -message $error_msg
+    }
+}
+
+$result = @{
+    changed = $false
+}
+
+#ConvertFrom-AnsibleJso
+$input_json  = '{"string":"string","float":3.1415926,"dict":{"string":"string","int":1},"list":["entry 1","entry 2"],"null":null,"int":1}'
+$actual = ConvertFrom-AnsibleJson -InputObject $input_json
+Assert-Equals -actual $actual.GetType() -expected ([Hashtable])
+Assert-Equals -actual $actual.string.GetType() -expected ([String])
+Assert-Equals -actual $actual.string -expected "string"
+Assert-Equals -actual $actual.int.GetType() -expected ([Int32])
+Assert-Equals -actual $actual.int -expected 1
+Assert-Equals -actual $actual.null -expected $null
+Assert-Equals -actual $actual.float.GetType() -expected ([Decimal])
+Assert-Equals -actual $actual.float -expected 3.1415926
+Assert-Equals -actual $actual.list.GetType() -expected ([Object[]])
+Assert-Equals -actual $actual.list.Count -expected 2
+Assert-Equals -actual $actual.list[0] -expected "entry 1"
+Assert-Equals -actual $actual.list[1] -expected "entry 2"
+Assert-Equals -actual $actual.GetType() -expected ([Hashtable])
+Assert-Equals -actual $actual.dict.string -expected "string"
+Assert-Equals -actual $actual.dict.int -expected 1
+
+$result.msg = "good"
+Exit-Json -obj $result
+
diff --git a/test/integration/targets/win_exec_wrapper/library/test_fail.ps1 b/test/integration/targets/win_exec_wrapper/library/test_fail.ps1
new file mode 100644
index 00000000000000..06c63f72c4e27b
--- /dev/null
+++ b/test/integration/targets/win_exec_wrapper/library/test_fail.ps1
@@ -0,0 +1,58 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$params = Parse-Args $args -supports_check_mode $true
+
+$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "normal"
+$result = @{
+    changed = $false
+}
+
+<#
+This module tests various error events in PowerShell to verify our hidden trap
+catches them all and outputs a pretty error message with a traceback to help
+users debug the actual issue
+
+normal - normal execution, no errors
+fail - Calls Fail-Json like normal
+throw - throws an exception
+error - Write-Error with ErrorActionPreferenceStop
+cmdlet_error - Calls a Cmdlet with an invalid error
+dotnet_exception - Calls a .NET function that will throw an error
+function_throw - Throws an exception in a function
+proc_exit_fine - calls an executable with a non-zero exit code with Exit-Json
+proc_exit_fail - calls an executable with a non-zero exit code with Fail-Json
+#>
+
+Function Test-ThrowException {
+    throw "exception in function"
+}
+
+if ($data -eq "normal") {
+    Exit-Json -obj $result
+} elseif ($data -eq "fail") {
+    Fail-Json -obj $result -message "fail message"
+} elseif ($data -eq "throw") {
+    throw [ArgumentException]"module is thrown"
+} elseif ($data -eq "error") {
+    Write-Error -Message $data
+} elseif ($data -eq "cmdlet_error") {
+    Get-Item -Path "fake:\path"
+} elseif ($data -eq "dotnet_exception") {
+    [System.IO.Path]::GetFullPath($null)
+} elseif ($data -eq "function_throw") {
+    Test-ThrowException
+} elseif ($data -eq "proc_exit_fine") {
+    # verifies that if no error was actually fired and we have an output, we
+    # don't use the RC to validate if the module failed
+    &cmd.exe /c exit 2
+    Exit-Json -obj $result
+} elseif ($data -eq "proc_exit_fail") {
+    &cmd.exe /c exit 2
+    Fail-Json -obj $result -message "proc_exit_fail"
+}
+
+# verify no exception were silently caught during our tests
+Fail-Json -obj $result -message "end of module"
+
diff --git a/test/integration/targets/win_exec_wrapper/tasks/main.yml b/test/integration/targets/win_exec_wrapper/tasks/main.yml
index 0ab4fe1d5568fd..b067168b80f18d 100644
--- a/test/integration/targets/win_exec_wrapper/tasks/main.yml
+++ b/test/integration/targets/win_exec_wrapper/tasks/main.yml
@@ -1,4 +1,115 @@
 ---
+- name: test normal module execution
+  test_fail:
+  register: normal
+
+- name: assert test normal module execution
+  assert:
+    that:
+    - not normal is failed
+
+- name: test fail module execution
+  test_fail:
+    data: fail
+  register: fail_module
+  ignore_errors: yes
+
+- name: assert test fail module execution
+  assert:
+    that:
+    - fail_module is failed
+    - fail_module.msg == "fail message"
+    - not fail_module.exception is defined
+
+- name: test module with exception thrown
+  test_fail:
+    data: throw
+  register: throw_module
+  ignore_errors: yes
+
+- name: assert test module with exception thrown
+  assert:
+    that:
+    - throw_module is failed
+    - 'throw_module.msg == "Unhandled exception while executing module: module is thrown"'
+    - '"throw [ArgumentException]\"module is thrown\"" in throw_module.exception'
+
+- name: test module with error msg
+  test_fail:
+    data: error
+  register: error_module
+  ignore_errors: yes
+
+- name: assert test module with error msg
+  assert:
+    that:
+    - error_module is failed
+    - 'error_module.msg == "Unhandled exception while executing module: error"'
+    - '"Write-Error -Message $data" in error_module.exception'
+
+- name: test module with cmdlet error
+  test_fail:
+    data: cmdlet_error
+  register: cmdlet_error
+  ignore_errors: yes
+
+- name: assert test module with cmdlet error
+  assert:
+    that:
+    - cmdlet_error is failed
+    - 'cmdlet_error.msg == "Unhandled exception while executing module: Cannot find drive. A drive with the name ''fake'' does not exist."'
+    - '"Get-Item -Path \"fake:\\path\"" in cmdlet_error.exception'
+
+- name: test module with .NET exception
+  test_fail:
+    data: dotnet_exception
+  register: dotnet_exception
+  ignore_errors: yes
+
+- name: assert test module with .NET exception
+  assert:
+    that:
+    - dotnet_exception is failed
+    - 'dotnet_exception.msg == "Unhandled exception while executing module: Exception calling \"GetFullPath\" with \"1\" argument(s): \"The path is not of a legal form.\""'
+    - '"[System.IO.Path]::GetFullPath($null)" in dotnet_exception.exception'
+
+- name: test module with function exception
+  test_fail:
+    data: function_throw
+  register: function_exception
+  ignore_errors: yes
+
+- name: assert test module with function exception
+  assert:
+    that:
+    - function_exception is failed
+    - 'function_exception.msg == "Unhandled exception while executing module: exception in function"'
+    - '"throw \"exception in function\"" in function_exception.exception'
+    - '"at Test-ThrowException, <No file>: line" in function_exception.exception'
+
+- name: test module with fail process but Exit-Json
+  test_fail:
+    data: proc_exit_fine
+  register: proc_exit_fine
+
+- name: assert test module with fail process but Exit-Json
+  assert:
+    that:
+    - not proc_exit_fine is failed
+
+- name: test module with fail process but Fail-Json
+  test_fail:
+    data: proc_exit_fail
+  register: proc_exit_fail
+  ignore_errors: yes
+
+- name: assert test module with fail process but Fail-Json
+  assert:
+    that:
+    - proc_exit_fail is failed
+    - proc_exit_fail.msg == "proc_exit_fail"
+    - not proc_exit_fail.exception is defined
+
 - name: test out invalid options
   test_invalid_requires:
   register: invalid_options
@@ -127,3 +238,13 @@
     args:
       executable: cmd.exe
     when: become_test_username in profile_dir_out.stdout_lines[0]
+
+- name: test common functions in exec
+  test_common_functions:
+  register: common_functions_res
+
+- name: assert test common functions in exec
+  assert:
+    that:
+    - not common_functions_res is failed
+    - common_functions_res.msg == "good"
diff --git a/test/integration/targets/win_module_utils/library/add_type_test.ps1 b/test/integration/targets/win_module_utils/library/add_type_test.ps1
new file mode 100644
index 00000000000000..f27a1d9c58e3e9
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/add_type_test.ps1
@@ -0,0 +1,187 @@
+#!powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.AddType
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+    changed = $false
+}
+
+Function Assert-Equals($actual, $expected) {
+    if ($actual -cne $expected) {
+        $call_stack = (Get-PSCallStack)[1]
+        $error_msg = "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: $($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
+        Fail-Json -obj $result -message $error_msg
+    }
+}
+
+$code = @'
+using System;
+
+namespace Namespace1
+{
+    public class Class1
+    {
+        public static string GetString(bool error)
+        {
+            if (error)
+                throw new Exception("error");
+            return "Hello World";
+        }
+    }
+}
+'@
+$res = Add-CSharpType -References $code
+Assert-Equals -actual $res -expected $null
+
+$actual = [Namespace1.Class1]::GetString($false)
+Assert-Equals $actual -expected "Hello World"
+
+try {
+    [Namespace1.Class1]::GetString($true)
+} catch {
+    Assert-Equals ($_.Exception.ToString().Contains("at Namespace1.Class1.GetString(Boolean error)`r`n")) -expected $true
+}
+
+$code_debug = @'
+using System;
+
+namespace Namespace2
+{
+    public class Class2
+    {
+        public static string GetString(bool error)
+        {
+            if (error)
+                throw new Exception("error");
+            return "Hello World";
+        }
+    }
+}
+'@
+$res = Add-CSharpType -References $code_debug -IncludeDebugInfo
+Assert-Equals -actual $res -expected $null
+
+$actual = [Namespace2.Class2]::GetString($false)
+Assert-Equals $actual -expected "Hello World"
+
+try {
+    [Namespace2.Class2]::GetString($true)
+} catch {
+    $tmp_path = [System.IO.Path]::GetFullPath($env:TMP).ToLower()
+    Assert-Equals ($_.Exception.ToString().ToLower().Contains("at namespace2.class2.getstring(boolean error) in $tmp_path")) -expected $true
+    Assert-Equals ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
+}
+
+$code_tmp = @'
+using System;
+
+namespace Namespace3
+{
+    public class Class3
+    {
+        public static string GetString(bool error)
+        {
+            if (error)
+                throw new Exception("error");
+            return "Hello World";
+        }
+    }
+}
+'@
+$tmp_path = $env:USERPROFILE
+$res = Add-CSharpType -References $code_tmp -IncludeDebugInfo -TempPath $tmp_path -PassThru
+Assert-Equals -actual $res.GetType().Name -expected "RuntimeAssembly"
+Assert-Equals -actual $res.Location -expected ""
+Assert-Equals -actual $res.GetTypes().Length -expected 1
+Assert-Equals -actual $res.GetTypes()[0].Name -expected "Class3"
+
+$actual = [Namespace3.Class3]::GetString($false)
+Assert-Equals $actual -expected "Hello World"
+
+try {
+    [Namespace3.Class3]::GetString($true)
+} catch {
+    Assert-Equals ($_.Exception.ToString().ToLower().Contains("at namespace3.class3.getstring(boolean error) in $($tmp_path.ToLower())")) -expected $true
+    Assert-Equals ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
+}
+
+$warning_code = @'
+using System;
+
+namespace Namespace4
+{
+    public class Class4
+    {
+        public static string GetString(bool test)
+        {
+            if (test)
+            {
+                string a = "";
+            }
+
+            return "Hello World";
+        }
+    }
+}
+'@
+$failed = $false
+try {
+    Add-CSharpType -References $warning_code
+} catch {
+    $failed = $true
+    Assert-Equals -actual ($_.Exception.Message.Contains("error CS0219: Warning as Error: The variable 'a' is assigned but its value is never used")) -expected $true
+}
+Assert-Equals -actual $failed -expected $true
+
+Add-CSharpType -References $warning_code -IgnoreWarnings
+$actual = [Namespace4.Class4]::GetString($true)
+Assert-Equals -actual $actual -expected "Hello World"
+
+$reference_1 = @'
+using System;
+using System.Web.Script.Serialization;
+
+//AssemblyReference -Name System.Web.Extensions.dll
+
+namespace Namespace5
+{
+    public class Class5
+    {
+        public static string GetString()
+        {
+            return "Hello World";
+        }
+    }
+}
+'@
+
+$reference_2 = @'
+using System;
+using Namespace5;
+using System.Management.Automation;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Namespace6
+{
+    public class Class6
+    {
+        public static string GetString()
+        {
+            Hashtable hash = new Hashtable();
+            hash["test"] = "abc";
+            return Class5.GetString();
+        }
+    }
+}
+'@
+
+Add-CSharpType -References $reference_1, $reference_2
+$actual = [Namespace6.Class6]::GetString()
+Assert-Equals -actual $actual -expected "Hello World"
+
+$result.res = "success"
+Exit-Json -obj $result
diff --git a/test/integration/targets/win_module_utils/library/csharp_util.ps1 b/test/integration/targets/win_module_utils/library/csharp_util.ps1
new file mode 100644
index 00000000000000..cf2dc45202a24c
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/csharp_util.ps1
@@ -0,0 +1,12 @@
+#1powershell
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#AnsibleRequires -CSharpUtil Ansible.Test
+
+$result = @{
+    res = [Ansible.Test.OutputTest]::GetString()
+    changed = $false
+}
+
+Exit-Json -obj $result
+
diff --git a/test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs b/test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs
new file mode 100644
index 00000000000000..9556d9af49c8fe
--- /dev/null
+++ b/test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs
@@ -0,0 +1,26 @@
+//AssemblyReference -Name System.Web.Extensions.dll
+
+using System;
+using System.Collections.Generic;
+using System.Web.Script.Serialization;
+
+namespace Ansible.Test
+{
+    public class OutputTest
+    {
+        public static string GetString()
+        {
+            Dictionary<string, object> obj = new Dictionary<string, object>();
+            obj["a"] = "a";
+            obj["b"] = 1;
+            return ToJson(obj);
+        }
+
+        private static string ToJson(object obj)
+        {
+            JavaScriptSerializer jss = new JavaScriptSerializer();
+            return jss.Serialize(obj);
+        }
+    }
+}
+
diff --git a/test/integration/targets/win_module_utils/tasks/main.yml b/test/integration/targets/win_module_utils/tasks/main.yml
index 05f2dffab377e2..18fca76ccdf78a 100644
--- a/test/integration/targets/win_module_utils/tasks/main.yml
+++ b/test/integration/targets/win_module_utils/tasks/main.yml
@@ -143,3 +143,23 @@
 - assert:
     that:
     - privilege_util_test.data == 'success'
+
+- name: call module with C# reference
+  csharp_util:
+  register: csharp_res
+
+- name: assert call module with C# reference
+  assert:
+    that:
+    - not csharp_res is failed
+    - csharp_res.res == '{"a":"a","b":1}'
+
+- name: call module with AddType tests
+  add_type_test:
+  register: add_type_test
+
+- name: assert call module with AddType tests
+  assert:
+    that:
+    - not add_type_test is failed
+    - add_type_test.res == 'success'
diff --git a/test/integration/targets/win_ping/tasks/main.yml b/test/integration/targets/win_ping/tasks/main.yml
index a1b4c1a15ed47e..4ed8cd9af7abf4 100644
--- a/test/integration/targets/win_ping/tasks/main.yml
+++ b/test/integration/targets/win_ping/tasks/main.yml
@@ -51,6 +51,7 @@
       - win_ping_ps1_result is not changed
       - win_ping_ps1_result.ping == 'bleep'
 
+# TODO: this will have to be removed once PS basic is implemented
 - name: test win_ping with extra args to verify that v2 module replacer escaping works as expected
   win_ping:
     data: bloop
@@ -92,71 +93,5 @@
     that:
       - win_ping_crash_result is failed
       - win_ping_crash_result is not changed
-      - "'FullyQualifiedErrorId : boom' in win_ping_crash_result.module_stderr"
-
-# TODO: fix code or tests? discrete error returns from PS are strange...
-
-#- name: test modified win_ping that throws an exception
-#  action: win_ping_throw
-#  register: win_ping_throw_result
-#  ignore_errors: true
-#
-#- name: check win_ping_throw result
-#  assert:
-#    that:
-#      - win_ping_throw_result is failed
-#      - win_ping_throw_result is not changed
-#      - win_ping_throw_result.msg == 'MODULE FAILURE'
-#      - win_ping_throw_result.exception
-#      - win_ping_throw_result.error_record
-#
-#- name: test modified win_ping that throws a string exception
-#  action: win_ping_throw_string
-#  register: win_ping_throw_string_result
-#  ignore_errors: true
-#
-#- name: check win_ping_throw_string result
-#  assert:
-#    that:
-#      - win_ping_throw_string_result is failed
-#      - win_ping_throw_string_result is not changed
-#      - win_ping_throw_string_result.msg == 'no ping for you'
-#      - win_ping_throw_string_result.exception
-#      - win_ping_throw_string_result.error_record
-#
-#- name: test modified win_ping that has a syntax error
-#  action: win_ping_syntax_error
-#  register: win_ping_syntax_error_result
-#  ignore_errors: true
-#
-#- name: check win_ping_syntax_error result
-#  assert:
-#    that:
-#      - win_ping_syntax_error_result is failed
-#      - win_ping_syntax_error_result is not changed
-#      - win_ping_syntax_error_result.msg
-#      - win_ping_syntax_error_result.exception
-#
-#- name: test modified win_ping that has an error that only surfaces when strict mode is on
-#  action: win_ping_strict_mode_error
-#  register: win_ping_strict_mode_error_result
-#  ignore_errors: true
-#
-#- name: check win_ping_strict_mode_error result
-#  assert:
-#    that:
-#      - win_ping_strict_mode_error_result is failed
-#      - win_ping_strict_mode_error_result is not changed
-#      - win_ping_strict_mode_error_result.msg
-#      - win_ping_strict_mode_error_result.exception
-#
-#- name: test modified win_ping to verify a Set-Attr fix
-#  action: win_ping_set_attr data="fixed"
-#  register: win_ping_set_attr_result
-#
-#- name: check win_ping_set_attr_result result
-#  assert:
-#    that:
-#      - win_ping_set_attr_result is not failed
-#      - win_ping_set_attr_result is not changed
-#      - win_ping_set_attr_result.ping == 'fixed'
+      - 'win_ping_crash_result.msg == "Unhandled exception while executing module: boom"'
+      - '"throw \"boom\"" in win_ping_crash_result.exception'
diff --git a/test/runner/lib/classification.py b/test/runner/lib/classification.py
index 37ec6f1eb20671..77fc06f673efee 100644
--- a/test/runner/lib/classification.py
+++ b/test/runner/lib/classification.py
@@ -25,6 +25,10 @@
     get_python_module_utils_imports,
 )
 
+from lib.csharp_import_analysis import (
+    get_csharp_module_utils_imports,
+)
+
 from lib.powershell_import_analysis import (
     get_powershell_module_utils_imports,
 )
@@ -168,6 +172,7 @@ def __init__(self, args):
         self.units_targets = list(walk_units_targets())
         self.sanity_targets = list(walk_sanity_targets())
         self.powershell_targets = [t for t in self.sanity_targets if os.path.splitext(t.path)[1] == '.ps1']
+        self.csharp_targets = [t for t in self.sanity_targets if os.path.splitext(t.path)[1] == '.cs']
 
         self.units_modules = set(t.module for t in self.units_targets if t.module)
         self.units_paths = set(a for t in self.units_targets for a in t.aliases)
@@ -189,6 +194,7 @@ def __init__(self, args):
 
         self.python_module_utils_imports = {}  # populated on first use to reduce overhead when not needed
         self.powershell_module_utils_imports = {}  # populated on first use to reduce overhead when not needed
+        self.csharp_module_utils_imports = {}  # populated on first use to reduce overhead when not needed
 
     def get_dependent_paths(self, path):
         """
@@ -204,6 +210,9 @@ def get_dependent_paths(self, path):
             if ext == '.psm1':
                 return self.get_powershell_module_utils_usage(path)
 
+            if ext == '.cs':
+                return self.get_csharp_module_utils_usage(path)
+
         if path.startswith('test/integration/targets/'):
             return self.get_integration_target_usage(path)
 
@@ -247,6 +256,22 @@ def get_powershell_module_utils_usage(self, path):
 
         return sorted(self.powershell_module_utils_imports[name])
 
+    def get_csharp_module_utils_usage(self, path):
+        """
+        :type path: str
+        :rtype: list[str]
+        """
+        if not self.csharp_module_utils_imports:
+            display.info('Analyzing C# module_utils imports...')
+            before = time.time()
+            self.csharp_module_utils_imports = get_csharp_module_utils_imports(self.powershell_targets, self.csharp_targets)
+            after = time.time()
+            display.info('Processed %d C# module_utils in %d second(s).' % (len(self.csharp_module_utils_imports), after - before))
+
+        name = os.path.splitext(os.path.basename(path))[0]
+
+        return sorted(self.csharp_module_utils_imports[name])
+
     def get_integration_target_usage(self, path):
         """
         :type path: str
@@ -320,7 +345,7 @@ def _classify(self, path):
                 return {
                     'units': module_name if module_name in self.units_modules else None,
                     'integration': self.posix_integration_by_module.get(module_name) if ext == '.py' else None,
-                    'windows-integration': self.windows_integration_by_module.get(module_name) if ext == '.ps1' else None,
+                    'windows-integration': self.windows_integration_by_module.get(module_name) if ext in ['.cs', '.ps1'] else None,
                     'network-integration': self.network_integration_by_module.get(module_name),
                     FOCUSED_TARGET: True,
                 }
@@ -328,6 +353,9 @@ def _classify(self, path):
             return minimal
 
         if path.startswith('lib/ansible/module_utils/'):
+            if ext == '.cs':
+                return minimal  # already expanded using get_dependent_paths
+
             if ext == '.psm1':
                 return minimal  # already expanded using get_dependent_paths
 
diff --git a/test/runner/lib/csharp_import_analysis.py b/test/runner/lib/csharp_import_analysis.py
new file mode 100644
index 00000000000000..5bb2af81308ae7
--- /dev/null
+++ b/test/runner/lib/csharp_import_analysis.py
@@ -0,0 +1,82 @@
+"""Analyze C# import statements."""
+
+from __future__ import absolute_import, print_function
+
+import os
+import re
+
+from lib.util import (
+    display,
+)
+
+
+def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
+    """Return a dictionary of module_utils names mapped to sets of powershell file paths.
+    :type powershell_targets: list[TestTarget] - C# files
+    :type csharp_targets: list[TestTarget] - PS files
+    :rtype: dict[str, set[str]]
+    """
+
+    module_utils = enumerate_module_utils()
+
+    imports_by_target_path = {}
+
+    for target in powershell_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
+
+    for target in csharp_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
+
+    imports = dict([(module_util, set()) for module_util in module_utils])
+
+    for target_path in imports_by_target_path:
+        for module_util in imports_by_target_path[target_path]:
+            imports[module_util].add(target_path)
+
+    for module_util in sorted(imports):
+        if not imports[module_util]:
+            display.warning('No imports found which use the "%s" module_util.' % module_util)
+
+    return imports
+
+
+def enumerate_module_utils():
+    """Return a list of available module_utils imports.
+    :rtype: set[str]
+    """
+    return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
+
+
+def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
+    """Return a list of module_utils imports found in the specified source file.
+    :type path: str
+    :type module_utils: set[str]
+    :rtype: set[str]
+    """
+    imports = set()
+    if is_pure_csharp:
+        pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
+    else:
+        pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
+
+    with open(path, 'r') as module_fd:
+        code = module_fd.read()
+
+        lines = code.splitlines()
+        line_number = 0
+
+        for line in lines:
+            line_number += 1
+            match = re.search(pattern, line)
+
+            if not match:
+                continue
+
+            import_name = match.group(1)
+
+            if import_name in module_utils:
+                imports.add(import_name)
+            else:
+                display.warning('%s:%d Invalid module_utils import: %s' % (path, line_number, import_name))
+
+    return imports
diff --git a/test/runner/lib/util.py b/test/runner/lib/util.py
index 5780ad9642573f..72f2da39770153 100644
--- a/test/runner/lib/util.py
+++ b/test/runner/lib/util.py
@@ -489,6 +489,7 @@ def is_binary_file(path):
         '.cfg',
         '.conf',
         '.crt',
+        '.cs',
         '.css',
         '.html',
         '.ini',


Code review comments:
diff_hunk: @@ -0,0 +1,82 @@
+"""Analyze C# import statements."""
+
+from __future__ import absolute_import, print_function
+
+import os
+import re
+
+from lib.util import (
+    display,
+)
+
+
+def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
+    """Return a dictionary of module_utils names mapped to sets of powershell file paths.
+    :type powershell_targets: list[TestTarget] - C# files
+    :type csharp_targets: list[TestTarget] - PS files
+    :rtype: dict[str, set[str]]
+    """
+
+    module_utils = enumerate_module_utils()
+
+    imports_by_target_path = {}
+
+    for target in powershell_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
+
+    for target in csharp_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
+
+    imports = dict([(module_util, set()) for module_util in module_utils])
+
+    for target_path in imports_by_target_path:
+        for module_util in imports_by_target_path[target_path]:
+            imports[module_util].add(target_path)
+
+    for module_util in sorted(imports):
+        if not imports[module_util]:
+            display.warning('No imports found which use the "%s" module_util.' % module_util)
+
+    return imports
+
+
+def enumerate_module_utils():
+    """Return a list of available module_utils imports.
+    :rtype: set[str]
+    """
+    return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
+
+
+def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
+    """Return a list of module_utils imports found in the specified source file.
+    :type path: str
+    :type module_utils: set[str]
+    :rtype: set[str]
+    """
+    imports = set()
+    if is_pure_csharp:
+        pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
+    else:
+        pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
+
+    with open(path, 'r') as module_fd:
+        code = module_fd.read()
path: test/runner/lib/csharp_import_analysis.py
text: @user1:
Rather than copying the whole file in-memory you could iterate over it right away:
```python
for line_number, line in enumerate(module_file, 1):
```

diff_hunk: @@ -0,0 +1,82 @@
+"""Analyze C# import statements."""
+
+from __future__ import absolute_import, print_function
+
+import os
+import re
+
+from lib.util import (
+    display,
+)
+
+
+def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
+    """Return a dictionary of module_utils names mapped to sets of powershell file paths.
+    :type powershell_targets: list[TestTarget] - C# files
+    :type csharp_targets: list[TestTarget] - PS files
+    :rtype: dict[str, set[str]]
+    """
+
+    module_utils = enumerate_module_utils()
+
+    imports_by_target_path = {}
+
+    for target in powershell_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
+
+    for target in csharp_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
+
+    imports = dict([(module_util, set()) for module_util in module_utils])
+
+    for target_path in imports_by_target_path:
+        for module_util in imports_by_target_path[target_path]:
+            imports[module_util].add(target_path)
+
+    for module_util in sorted(imports):
+        if not imports[module_util]:
+            display.warning('No imports found which use the "%s" module_util.' % module_util)
+
+    return imports
+
+
+def enumerate_module_utils():
+    """Return a list of available module_utils imports.
+    :rtype: set[str]
+    """
+    return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
+
+
+def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
+    """Return a list of module_utils imports found in the specified source file.
+    :type path: str
+    :type module_utils: set[str]
+    :rtype: set[str]
+    """
+    imports = set()
+    if is_pure_csharp:
+        pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
+    else:
+        pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
+
+    with open(path, 'r') as module_fd:
+        code = module_fd.read()
+
+        lines = code.splitlines()
+        line_number = 0
+
+        for line in lines:
+            line_number += 1
+            match = re.search(pattern, line)
+
+            if not match:
+                continue
+
+            import_name = match.group(1)
+
+            if import_name in module_utils:
path: test/runner/lib/csharp_import_analysis.py
text: @user1:
I think, guard-expression style would fit better here:
```python
if not import_name in module_utils:
    display.warning('%s:%d Invalid module_utils import: %s' % (path, line_number, import_name))
    continue

imports.add(import_name)
```

@author:
Any reason you say that, just curious?

@user1:
It's more about readability. When you see two if-branches it's different than just one and then following scenario.
Plus it's less indentation, which is less important in this simple case, but in general helps to have such habit :)

diff_hunk: @@ -0,0 +1,82 @@
+"""Analyze C# import statements."""
+
+from __future__ import absolute_import, print_function
+
+import os
+import re
+
+from lib.util import (
+    display,
+)
+
+
+def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
+    """Return a dictionary of module_utils names mapped to sets of powershell file paths.
+    :type powershell_targets: list[TestTarget] - C# files
+    :type csharp_targets: list[TestTarget] - PS files
+    :rtype: dict[str, set[str]]
+    """
+
+    module_utils = enumerate_module_utils()
+
+    imports_by_target_path = {}
+
+    for target in powershell_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
+
+    for target in csharp_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
+
+    imports = dict([(module_util, set()) for module_util in module_utils])
+
+    for target_path in imports_by_target_path:
+        for module_util in imports_by_target_path[target_path]:
+            imports[module_util].add(target_path)
+
+    for module_util in sorted(imports):
+        if not imports[module_util]:
+            display.warning('No imports found which use the "%s" module_util.' % module_util)
+
+    return imports
+
+
+def enumerate_module_utils():
+    """Return a list of available module_utils imports.
+    :rtype: set[str]
+    """
+    return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
+
+
+def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
+    """Return a list of module_utils imports found in the specified source file.
+    :type path: str
+    :type module_utils: set[str]
+    :rtype: set[str]
+    """
+    imports = set()
+    if is_pure_csharp:
+        pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
+    else:
+        pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
+
+    with open(path, 'r') as module_fd:
+        code = module_fd.read()
+
+        lines = code.splitlines()
+        line_number = 0
+
+        for line in lines:
path: test/runner/lib/csharp_import_analysis.py
text: @user1:
```python
for line_number, line in enumerate(lines, 1):
```

@user1:
(and there's no need to have this var outside or this loop or increment it manually, then)

diff_hunk: @@ -0,0 +1,82 @@
+"""Analyze C# import statements."""
+
+from __future__ import absolute_import, print_function
+
+import os
+import re
+
+from lib.util import (
+    display,
+)
+
+
+def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
+    """Return a dictionary of module_utils names mapped to sets of powershell file paths.
+    :type powershell_targets: list[TestTarget] - C# files
+    :type csharp_targets: list[TestTarget] - PS files
+    :rtype: dict[str, set[str]]
+    """
+
+    module_utils = enumerate_module_utils()
+
+    imports_by_target_path = {}
+
+    for target in powershell_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
+
+    for target in csharp_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
+
+    imports = dict([(module_util, set()) for module_util in module_utils])
+
+    for target_path in imports_by_target_path:
+        for module_util in imports_by_target_path[target_path]:
+            imports[module_util].add(target_path)
+
+    for module_util in sorted(imports):
+        if not imports[module_util]:
+            display.warning('No imports found which use the "%s" module_util.' % module_util)
+
+    return imports
+
+
+def enumerate_module_utils():
+    """Return a list of available module_utils imports.
+    :rtype: set[str]
+    """
+    return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
+
+
+def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
+    """Return a list of module_utils imports found in the specified source file.
+    :type path: str
+    :type module_utils: set[str]
+    :rtype: set[str]
+    """
+    imports = set()
+    if is_pure_csharp:
+        pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
+    else:
+        pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
+
+    with open(path, 'r') as module_fd:
+        code = module_fd.read()
+
+        lines = code.splitlines()
path: test/runner/lib/csharp_import_analysis.py
text: @user1:
Could you please dedent everything below including this line? There's no point in keeping it inside of context manager block, which _may_ swallow some types of exceptions.

diff_hunk: @@ -0,0 +1,82 @@
+"""Analyze C# import statements."""
+
+from __future__ import absolute_import, print_function
+
+import os
+import re
+
+from lib.util import (
+    display,
+)
+
+
+def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
+    """Return a dictionary of module_utils names mapped to sets of powershell file paths.
+    :type powershell_targets: list[TestTarget] - C# files
+    :type csharp_targets: list[TestTarget] - PS files
+    :rtype: dict[str, set[str]]
+    """
+
+    module_utils = enumerate_module_utils()
+
+    imports_by_target_path = {}
+
+    for target in powershell_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
+
+    for target in csharp_targets:
+        imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
+
+    imports = dict([(module_util, set()) for module_util in module_utils])
+
+    for target_path in imports_by_target_path:
+        for module_util in imports_by_target_path[target_path]:
+            imports[module_util].add(target_path)
+
+    for module_util in sorted(imports):
+        if not imports[module_util]:
+            display.warning('No imports found which use the "%s" module_util.' % module_util)
+
+    return imports
+
+
+def enumerate_module_utils():
+    """Return a list of available module_utils imports.
+    :rtype: set[str]
+    """
+    return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
+
+
+def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
+    """Return a list of module_utils imports found in the specified source file.
+    :type path: str
+    :type module_utils: set[str]
+    :rtype: set[str]
+    """
+    imports = set()
+    if is_pure_csharp:
+        pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
+    else:
+        pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
+
+    with open(path, 'r') as module_fd:
path: test/runner/lib/csharp_import_analysis.py
text: @user1:
`fd` in name implies "file descriptor", but file-object is more than that. I'd do `module` or `module_file`