# (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 += "`0`0`0`0" + $payload_json
$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_parts = $exec.Split(@("`0`0`0`0"), 2, [StringSplitOptions]::RemoveEmptyEntries)
    Set-Variable -Name json_raw -Value $exec_parts[1]
    $exec = [ScriptBlock]::Create($exec_parts[0])
    &$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))
$pwsh_path = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$exec_args = "`"$pwsh_path`" -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

    $np_timeout = $Payload.async_startup_timeout * 1000
    Write-AnsibleLog "INFO - waiting for async process to connect to named pipe for $np_timeout milliseconds" "async_wrapper"
    $wait_async = $pipe.BeginWaitForConnection($null, $null)
    $wait_async.AsyncWaitHandle.WaitOne($np_timeout) > $null
    if (-not $wait_async.IsCompleted) {
        $msg = "Ansible encountered a timeout while waiting for the async task to start and connect to the named"
        $msg += "pipe. This can be affected by the performance of the target - you can increase this timeout using"
        $msg += "WIN_ASYNC_STARTUP_TIMEOUT or just for this host using the win_async_startup_timeout hostvar if "
        $msg += "this keeps happening."
        throw $msg
    }
    $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"
