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 enum HandleFlags : int { HANDLE_FLAG_INHERIT = 0x00000001, HANDLE_FLAG_PROTECT_FROM_CLOSE = 0x00000002 } 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.dll")] public static extern bool SetHandleInformation( IntPtr hObject, int dwMask, uint dwFlags); [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); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CreatePipe( out IntPtr phReadPipe, out IntPtr phWritePipe, IntPtr lpPipeAttributes, uint nSize); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ReadFile( IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead, ref int lpNumberOfBytesRead, IntPtr lpOverlapped); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool PeekNamedPipe( IntPtr handle, byte[] buffer, uint nBufferSize, ref uint bytesRead, ref uint bytesAvail, ref uint BytesLeftThisMessage); public const int STILL_ACTIVE = 259; } } "@ } 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 $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 # Create pipe for process stdout $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.Runtime.InteropServices.Marshal]::SizeOf($si)) [System.Runtime.InteropServices.Marshal]::StructureToPtr($pSec, $ptr, $true) $hReadOut = [IntPtr]::Zero $hWriteOut = [IntPtr]::Zero $success = [Chef.Kernel32]::CreatePipe([ref] $hReadOut, [ref] $hWriteOut, $ptr, 0) if (-Not $success) { $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Unable to create output pipe. Error code $reason." } $success = [Chef.Kernel32]::SetHandleInformation($hReadOut, [Chef.HandleFlags]::HANDLE_FLAG_INHERIT, 0) if (-Not $success) { $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Unable to set output pipe handle information. Error code $reason." } $si.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($si) $si.wShowWindow = [Chef.ShowWindow]::SW_SHOW $si.dwFlags = [Chef.STARTF]::STARTF_USESTDHANDLES $si.hStdOutput = $hWriteOut $si.hStdError = $hWriteOut $si.hStdInput = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Input) $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." } $buffer = New-Object byte[] 1024 # Initialize reference variables $bytesRead = 0 $bytesAvailable = 0 $bytesLeftThisMsg = 0 $global:LASTEXITCODE = [Chef.Kernel32]::STILL_ACTIVE $isActive = $true while ($isActive) { $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]::PeekNamedPipe( $hReadOut, $null, $buffer.Length, [ref] $bytesRead, [ref] $bytesAvailable, [ref] $bytesLeftThisMsg ) if (-Not $success) { $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Output pipe unavailable for peeking. Error code $reason." } if ($bytesRead -gt 0) { while ([Chef.Kernel32]::ReadFile($hReadOut, $buffer, $buffer.Length, [ref] $bytesRead, 0)) { $output = [Text.Encoding]::UTF8.GetString($buffer, 0, $bytesRead) if ($output) { $output } if ($bytesRead -lt $buffer.Length) { # Partial buffer indicating the end of stream, break out of ReadFile loop # ReadFile will block until: # The number of bytes requested is read. # A write operation completes on the write end of the pipe. # An asynchronous handle is being used and the read is occurring asynchronously. # An error occurs. break } } } else { # For some reason, you can't read from the read-end of the read-pipe before the write end has started # to write. Otherwise the process just blocks forever and never returns from the read. So we peek # at the pipe until there is something. But don't peek too eagerly. This is stupid stupid stupid. # There must be a way to do this without having to peek at a pipe first but I have not found it. # # Note to the future intrepid soul who wants to fix this: # 0) This is related to unreasonable CPU usage by the wrapper PS script on a 1 VCPU VM (either Hyper-V # or VirtualBox) running a consumer Windows SKU (Windows 10 for example...). Test it there. # 1) Maybe this entire script is unnecessary and the bugs mentioned below have been fixed or don't need # to be supported. # 2) The server and consumer windows schedulers have different defaults. I had a hard time reproducing # any issue on a win 2008 on win 2012 server default setup. See the "foreground application scheduler # priority" setting to see if it's relevant. # 3) This entire endeavor is silly anyway - why are we reimplementing process forking all over? Maybe try # to get the folks above to accept patches instead of extending this crazy script. Start-Sleep -s 1 # Start-Sleep -m 100 } if ($global:LASTEXITCODE -ne [Chef.Kernel32]::STILL_ACTIVE) { $isActive = $false } } # Cleanup handles $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." } $success = [Chef.Kernel32]::CloseHandle($hWriteOut) if (-Not $success) { $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Unable to release output write handle. Error code $reason." } $success = [Chef.Kernel32]::CloseHandle($hReadOut) if (-Not $success) { $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Unable to release output read handle. Error code $reason." } [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) } 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 # Export-ModuleMember -function Run-RubyCommand # Then run the following to reload the module. Use puts_argv as a helpful debug executable. # Remove-Module chef # Import-Module chef # "puts ARGV" | Out-File C:\opscode\chef\bin\puts_args -Encoding ASCII # Copy-Item C:\opscode\chef\bin\ohai.bat C:\opscode\chef\bin\puts_args.bat # Run-RubyCommand puts_args 'Here' "are" some '"very interesting"' 'arguments[to]' "`"try out`""