# # Author:: Stuart Preston () # Copyright:: Copyright (c) Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require_relative "./powershell" require_relative "./pwsh" # The Chef-PowerShell gem provides in-process access to the PowerShell engine. # # powershell_exec is initialized with a string that should be set to the script # to run and also takes an optional interpreter argument which must be either # :powershell (Windows PowerShell which is the default) or :pwsh (PowerShell # Core). It will return a Chef_PowerShell::PowerShell object that provides 5 methods: # # .result - returns a hash representing the results returned by executing the # PowerShell script block # .verbose - this is an array of string containing any messages written to the # PowerShell verbose stream during execution # .errors - this is an array of string containing any messages written to the # PowerShell error stream during execution # .error? - returns true if there were error messages written to the PowerShell # error stream during execution # .error! - raise Chef::PowerShell::CommandFailed if there was an error # # Some examples of usage: # # > powershell_exec("(Get-Item c:\\windows\\system32\\w32time.dll).VersionInfo" # ).result["FileVersion"] # => "10.0.14393.0 (rs1_release.160715-1616)" # # > powershell_exec("(get-process ruby).Mainmodule").result["FileName"] # => C:\\opscode\\chef\\embedded\\bin\\ruby.exe" # # > powershell_exec("$a = $true; $a").result # => true # # > powershell_exec("$PSVersionTable", :pwsh).result["PSEdition"] # => "Core" # # > powershell_exec("not-found").errors # => ["ObjectNotFound: (not-found:String) [], CommandNotFoundException: The # term 'not-found' is not recognized as the name of a cmdlet, function, script # file, or operable program. Check the spelling of the name, or if a path was # included, verify that the path is correct and try again. (at , # : line 1)"] # # > powershell_exec("not-found").error? # => true # # > powershell_exec("get-item c:\\notfound -erroraction stop") # WIN32OLERuntimeError: (in OLE method `ExecuteScript': ) # OLE error code:80131501 in System.Management.Automation # The running command stopped because the preference variable # "ErrorActionPreference" or common parameter is set to Stop: Cannot find # path 'C:\notfound' because it does not exist. # # *Why use this and not powershell_out?* Startup time to invoke the PowerShell # engine is much faster (over 7X faster in tests) than writing the PowerShell # to disk, shelling out to powershell.exe and retrieving the .stdout or .stderr # methods afterwards. Additionally we are able to have a higher fidelity # conversation with PowerShell because we are now working with the objects that # are returned by the script, rather than having to parse the stdout of # powershell.exe to get a result. # # *How does this work?* In .NET terms, when you run a PowerShell script block # through the engine, behind the scenes you get a Collection returned # and simply we are serializing this, adding any errors that were generated to # a custom JSON string transferred in memory to Ruby. The easiest way to # develop for this approach is to imagine that the last thing that happens in # your PowerShell script block is "ConvertTo-Json". That's exactly what we are # doing here behind the scenes. # # There are a handful of current limitations with this approach: # - Windows UAC elevation is controlled by the token assigned to the account # that Ruby.exe is running under. # - Terminating errors will result in a WIN32OLERuntimeError and typically are # handled as an exception. # - There are no return/error codes, as we are not shelling out to # powershell.exe but calling a method inline, no errors codes are returned. # - There is no settable timeout on powershell_exec method execution. # - It is not possible to impersonate another user running powershell, the # credentials of the user running Chef Client are used. # class Chef_PowerShell module ChefPowerShell module PowerShellExec # The Chef.PowerShell.Wrapper.dll file looks in the same folder as ruby.exe OR in the folder specified by the environment variable CHEF_POWERSHELL_BIN for other chef powershell dll's # We don't want to move files around so we're setting the variable here to keep everything tidy. file_path = Gem.loaded_specs["chef-powershell"].full_gem_path + "/bin/ruby_bin_folder/#{ENV["PROCESSOR_ARCHITECTURE"]}/" ENV["CHEF_POWERSHELL_BIN"] = file_path # Run a command under PowerShell via a managed (.NET) API. # # Requires: .NET Framework 4.0 or higher on the target machine. # # @param script [String] script to run # @param interpreter [Symbol] the interpreter type, `:powershell` or `:pwsh` # @param timeout [Integer, nil] timeout in seconds. # @return [Chef::PowerShell] output def powershell_exec(script, interpreter = :powershell, timeout: -1) case interpreter when :powershell Chef_PowerShell::PowerShell.new(script, timeout: timeout) when :pwsh Chef_PowerShell::Pwsh.new(script, timeout: timeout) else raise ArgumentError, "Expected interpreter of :powershell or :pwsh" end end # The same as the #powershell_exec method except this will raise # Chef_PowerShell::PowerShellExceptions::PowerShellCommandFailed if the command fails def powershell_exec!(script, interpreter = :powershell, **options) cmd = powershell_exec(script, interpreter, **options) cmd.error! cmd end end end end