summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Rakefile9
-rw-r--r--distro/templates/powershell/chef/chef.psm1.erb459
3 files changed, 469 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index a72dd9f33b..52705a76ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,6 +80,7 @@ docs_site
# Rendered Templates
ext/win32-eventlog/chef-log.man
+distro/powershell/chef/chef.psm1
# tool logs
ext/win32-eventlog/mkmf.log
diff --git a/Rakefile b/Rakefile
index 643d70015b..b54411af4c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -22,6 +22,7 @@ begin
require_relative "tasks/dependencies"
require_relative "tasks/announce"
require_relative "tasks/docs"
+ require_relative "lib/chef/dist"
rescue LoadError => e
puts "Skipping missing rake dep: #{e}"
end
@@ -35,6 +36,14 @@ task :super_install do
Dir.chdir(path)
sh("rake install")
end
+
+# Templating the powershell extensions so we can inject distro constants
+ template_file = ::File.join(::File.dirname(__FILE__), "distro", "templates", "powershell", "chef", "chef.psm1.erb")
+ psm1_path = ::File.join(::File.dirname(__FILE__), "distro", "powershell", "chef")
+ FileUtils.mkdir_p psm1_path
+ template = ERB.new(IO.read(template_file))
+ chef_psm1 = template.result
+ File.open(::File.join(psm1_path, "chef.psm1"), "w") { |f| f.write(chef_psm1) }
end
task install: :super_install
diff --git a/distro/templates/powershell/chef/chef.psm1.erb b/distro/templates/powershell/chef/chef.psm1.erb
new file mode 100644
index 0000000000..652dec04f6
--- /dev/null
+++ b/distro/templates/powershell/chef/chef.psm1.erb
@@ -0,0 +1,459 @@
+
+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::Dist::APPLY %> {
+ Run-RubyCommand '<%= Chef::Dist::APPLY %>' $args
+}
+
+function <%= Chef::Dist::CLIENT %> {
+ Run-RubyCommand '<%= Chef::Dist::CLIENT %>' $args
+}
+
+function <%= Chef::Dist::EXEC %>-service-manager {
+ Run-RubyCommand '<%= Chef::Dist::EXEC %>-service-manager' $args
+}
+
+function <%= Chef::Dist::SHELL %> {
+ Run-RubyCommand '<%= Chef::Dist::SHELL %>' $args
+}
+
+function <%= Chef::Dist::SOLOEXEC %> {
+ Run-RubyCommand '<%= Chef::Dist::SOLOEXEC %>' $args
+}
+
+function <%= Chef::Dist::EXEC %>-windows-service {
+ Run-RubyCommand '<%= Chef::Dist::EXEC %>-windows-service' $args
+}
+
+function knife {
+ Run-RubyCommand 'knife' $args
+}
+
+Export-ModuleMember -function <%= Chef::Dist::APPLY %>
+Export-ModuleMember -function <%= Chef::Dist::CLIENT %>
+Export-ModuleMember -function <%= Chef::Dist::EXEC %>-service-manager
+Export-ModuleMember -function <%= Chef::Dist::SHELL %>
+Export-ModuleMember -function <%= Chef::Dist::SOLOEXEC %>
+Export-ModuleMember -function <%= Chef::Dist::EXEC %>-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`""