
function Load-Win32Bindings {
  Add-Type -TypeDefinition @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Chef
{

[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
  public IntPtr hProcess;
  public IntPtr hThread;
  public uint dwProcessId;
  public uint dwThreadId;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO
{
  public uint cb;
  public string lpReserved;
  public string lpDesktop;
  public string lpTitle;
  public uint dwX;
  public uint dwY;
  public uint dwXSize;
  public uint dwYSize;
  public uint dwXCountChars;
  public uint dwYCountChars;
  public uint dwFillAttribute;
  public STARTF dwFlags;
  public ShowWindow wShowWindow;
  public short cbReserved2;
  public IntPtr lpReserved2;
  public IntPtr hStdInput;
  public IntPtr hStdOutput;
  public IntPtr hStdError;
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
  public int length;
  public IntPtr lpSecurityDescriptor;
  public bool bInheritHandle;
}

[Flags]
public enum CreationFlags : int
{
  NONE = 0,
  DEBUG_PROCESS = 0x00000001,
  DEBUG_ONLY_THIS_PROCESS = 0x00000002,
  CREATE_SUSPENDED = 0x00000004,
  DETACHED_PROCESS = 0x00000008,
  CREATE_NEW_CONSOLE = 0x00000010,
  CREATE_NEW_PROCESS_GROUP = 0x00000200,
  CREATE_UNICODE_ENVIRONMENT = 0x00000400,
  CREATE_SEPARATE_WOW_VDM = 0x00000800,
  CREATE_SHARED_WOW_VDM = 0x00001000,
  CREATE_PROTECTED_PROCESS = 0x00040000,
  EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
  CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
  CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
  CREATE_DEFAULT_ERROR_MODE = 0x04000000,
  CREATE_NO_WINDOW = 0x08000000,
}

[Flags]
public enum STARTF : uint
{
  STARTF_USESHOWWINDOW = 0x00000001,
  STARTF_USESIZE = 0x00000002,
  STARTF_USEPOSITION = 0x00000004,
  STARTF_USECOUNTCHARS = 0x00000008,
  STARTF_USEFILLATTRIBUTE = 0x00000010,
  STARTF_RUNFULLSCREEN = 0x00000020,  // ignored for non-x86 platforms
  STARTF_FORCEONFEEDBACK = 0x00000040,
  STARTF_FORCEOFFFEEDBACK = 0x00000080,
  STARTF_USESTDHANDLES = 0x00000100,
}

public enum ShowWindow : short
{
    SW_HIDE = 0,
    SW_SHOWNORMAL = 1,
    SW_NORMAL = 1,
    SW_SHOWMINIMIZED = 2,
    SW_SHOWMAXIMIZED = 3,
    SW_MAXIMIZE = 3,
    SW_SHOWNOACTIVATE = 4,
    SW_SHOW = 5,
    SW_MINIMIZE = 6,
    SW_SHOWMINNOACTIVE = 7,
    SW_SHOWNA = 8,
    SW_RESTORE = 9,
    SW_SHOWDEFAULT = 10,
    SW_FORCEMINIMIZE = 11,
    SW_MAX = 11
}

public enum StandardHandle : int
{
  Input = -10,
  Output = -11,
  Error = -12
}

public static class Kernel32
{
  [DllImport("kernel32.dll", SetLastError=true)]
  [return: MarshalAs(UnmanagedType.Bool)]
  public static extern bool CreateProcess(
    string lpApplicationName,
    string lpCommandLine,
    ref SECURITY_ATTRIBUTES lpProcessAttributes,
    ref SECURITY_ATTRIBUTES lpThreadAttributes,
    [MarshalAs(UnmanagedType.Bool)] bool bInheritHandles,
    CreationFlags dwCreationFlags,
    IntPtr lpEnvironment,
    string lpCurrentDirectory,
    ref STARTUPINFO lpStartupInfo,
    out PROCESS_INFORMATION lpProcessInformation);

  [DllImport("kernel32.dll", SetLastError=true)]
  public static extern IntPtr GetStdHandle(
    StandardHandle nStdHandle);

  [DllImport("kernel32", SetLastError=true)]
  public static extern int WaitForSingleObject(
    IntPtr hHandle,
    int dwMilliseconds);

  [DllImport("kernel32", SetLastError=true)]
  [return: MarshalAs(UnmanagedType.Bool)]
  public static extern bool CloseHandle(
    IntPtr hObject);

  [DllImport("kernel32", SetLastError=true)]
  [return: MarshalAs(UnmanagedType.Bool)]
  public static extern bool GetExitCodeProcess(
    IntPtr hProcess,
    out int lpExitCode);
}
}
"@
}

function Run-ExecutableAndWait($AppPath, $ArgumentString) {
  # Use the Win32 API to create a new process and wait for it to terminate.
  $null = Load-Win32Bindings

  $si = New-Object Chef.STARTUPINFO
  $pi = New-Object Chef.PROCESS_INFORMATION

  $si.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($si)
  $si.wShowWindow = [Chef.ShowWindow]::SW_SHOW
  $si.dwFlags = [Chef.STARTF]::STARTF_USESTDHANDLES
  $si.hStdError = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Error)
  $si.hStdOutput = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Output)
  $si.hStdInput = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Input)

  $pSec = New-Object Chef.SECURITY_ATTRIBUTES
  $pSec.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($pSec)
  $pSec.bInheritHandle = $true
  $tSec = New-Object Chef.SECURITY_ATTRIBUTES
  $tSec.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($tSec)
  $tSec.bInheritHandle = $true

  $success = [Chef.Kernel32]::CreateProcess($AppPath, $ArgumentString, [ref] $pSec, [ref] $tSec, $true, [Chef.CreationFlags]::NONE, [IntPtr]::Zero, $pwd, [ref] $si, [ref] $pi)
  if (-Not $success) {
    $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
    throw "Unable to create process [$ArgumentString].  Error code $reason."
  }
  $waitReason = [Chef.Kernel32]::WaitForSingleObject($pi.hProcess, -1)
  if ($waitReason -ne 0) {
    if ($waitReason -eq -1) {
      $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
      throw "Could not wait for process to terminate.  Error code $reason."
    } else {
      throw "WaitForSingleObject failed with return code $waitReason - it's impossible!"
    }
  }
  $success = [Chef.Kernel32]::GetExitCodeProcess($pi.hProcess, [ref] $global:LASTEXITCODE)
  if (-Not $success) {
    $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
    throw "Process exit code unavailable.  Error code $reason."
  }
  $success = [Chef.Kernel32]::CloseHandle($pi.hProcess)
  if (-Not $success) {
    $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
    throw "Unable to release process handle.  Error code $reason."
  }
  $success = [Chef.Kernel32]::CloseHandle($pi.hThread)
  if (-Not $success) {
    $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
    throw "Unable to release thread handle.  Error code $reason."
  }
}

function Get-ScriptDirectory {
  if (!$PSScriptRoot) {
    $Invocation = (Get-Variable MyInvocation -Scope 1).Value
    $PSScriptRoot = Split-Path $Invocation.MyCommand.Path
  }
  $PSScriptRoot
}

function Run-RubyCommand($command, $argList) {
  # This method exists to take the given list of arguments and get it past ruby's command-line
  # interpreter unscathed and untampered.  See https://github.com/ruby/ruby/blob/trunk/win32/win32.c#L1582
  # for a list of transformations that ruby attempts to perform with your command-line arguments
  # before passing it onto a script.  The most important task is to defeat the globbing
  # and wild-card expansion that ruby performs.  Note that ruby does not use MSVCRT's argc/argv
  # and deliberately reparses the raw command-line instead.
  #
  # To stop ruby from interpreting command-line arguments as globs, they need to be enclosed in '
  # Ruby doesn't allow any escape characters inside '.  This unfortunately prevents us from sending
  # any strings which themselves contain '.  Ruby does allow multi-fragment arguments though.
  # "foo bar"'baz qux'123"foo" is interpreted as 1 argument because there are no un-escaped
  # whitespace there.  The argument would be interpreted as the string "foo barbaz qux123foo".
  # This lets us escape ' characters by exiting the ' quoted string, injecting a "'" fragment and
  # then resuming the ' quoted string again.
  #
  # In the process of defeating ruby, one must also defeat the helpfulness of powershell.
  # When arguments come into this method, the standard PS rules for interpreting cmdlet arguments
  # apply.  When using & (call operator) and providing an array of arguments, powershell (verified
  # on PS 4.0 on Windows Server 2012R2) will not evaluate them but (contrary to documentation),
  # it will still marginally interpret them.  The behaviour of PS 5.0 seems to be different but
  # ignore that for now.  If any of the provided arguments has a space in it, powershell checks
  # the first and last character to ensure that they are " characters (and that's all it checks).
  # If they are not, it will blindly surround that argument with " characters.  It won't do this
  # operation if no space is present, even if other special characters are present. If it notices
  # leading and trailing " characters, it won't actually check to see if there are other "
  # characters in the string.  Since PS 5.0 changes this behavior, we could consider using the --%
  # "stop screwing up my arguments" operator, which is available since PS 3.0.  When encountered
  # --% indicates that the rest of line is to be sent literally...  except if the parser encounters
  # %FOO% cmd style environment variables.  Because reasons.  And there is no way to escape the
  # % character in *any* waym shape or form.
  # https://connect.microsoft.com/PowerShell/feedback/details/376207/executing-commands-which-require-quotes-and-variables-is-practically-impossible
  #
  # In case you think that you're either reading this incorrectly or that I'm full of shit, here
  # are some examples.  These use EchoArgs.exe from the PowerShell Community Extensions package.
  # I have not included the argument parsing output from EchoArgs.exe to prevent confusing you with
  # more details about MSVCRT's parsing algorithm.
  #
  # $x = "foo '' bar `"baz`""
  # & EchoArgs @($x, $x)
  # Command line:
  # "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe"  "foo '' bar "baz"" "foo '' bar "baz""
  #
  # $x = "abc'123'nospace`"lulz`"!!!"
  # & EchoArgs @($x, $x)
  # Command line:
  # "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe"  abc'123'nospace"lulz"!!! abc'123'nospace"lulz"!!!
  #
  # $x = "`"`"Look ma! Tonnes of spaces! 'foo' 'bar'`"`""
  # & EchoArgs @($x, $x)
  # Command line:
  # "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe"  ""Look ma! Tonnes of spaces! 'foo' 'bar'"" ""Look ma! Tonnes of spaces! 'foo' 'bar'""
  #
  # Given all this, we can now device a strategy to work around all these immensely helpful, well
  # documented and useful tools by looking at each incoming argument, escaping any ' characters
  # with a '"'"' sequence, surrounding each argument with ' & joining them with a space separating
  # them.
  # There is another bug (https://bugs.ruby-lang.org/issues/11142) that causes ruby to mangle any
  # "" two-character double quote sequence but since we always emit our strings inside ' except for
  # ' characters, this should be ok.  Just remember that an argument '' should get translated to
  # ''"'"''"'"'' on the command line.  If those intervening empty ''s are not present, the presence
  # of "" will cause ruby to mangle that argument.
  $transformedList = $argList | foreach { "'" + ( $_ -replace "'","'`"'`"'" ) + "'" }
  $fortifiedArgString = $transformedList -join ' '

  # Use the correct embedded ruby path.  We'll be deployed at a path that looks like
  # [C:\opscode or some other prefix]\chef\modules\chef
  $ruby = Join-Path (Get-ScriptDirectory)  "..\..\embedded\bin\ruby.exe"
  $commandPath = Join-Path (Get-ScriptDirectory) "..\..\bin\$command"

  Run-ExecutableAndWait $ruby """$ruby"" '$commandPath' $fortifiedArgString"
}


function chef-apply {
  Run-RubyCommand 'chef-apply' $args
}

function chef-client {
  Run-RubyCommand 'chef-client' $args
}

function chef-service-manager {
  Run-RubyCommand 'chef-service-manager' $args
}

function chef-shell {
  Run-RubyCommand 'chef-shell' $args
}

function chef-solo {
  Run-RubyCommand 'chef-solo' $args
}

function chef-windows-service {
  Run-RubyCommand 'chef-windows-service' $args
}

function knife {
  Run-RubyCommand 'knife' $args
}

Export-ModuleMember -function chef-apply
Export-ModuleMember -function chef-client
Export-ModuleMember -function chef-service-manager
Export-ModuleMember -function chef-shell
Export-ModuleMember -function chef-solo
Export-ModuleMember -function chef-windows-service
Export-ModuleMember -function knife

# To debug this module, uncomment the line below and then run the following.
# Export-ModuleMember -function Run-RubyCommand
# Remove-Module chef
# Import-Module chef
# "puts ARGV" | Out-File C:\opscode\chef\bin\puts_args
# Run-RubyCommand puts_args 'Here' "are" some '"very interesting"' 'arguments[to]' "`"try out`""
