summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMatt Wrock <matt@mattwrock.com>2015-12-07 21:17:03 -0800
committerMatt Wrock <matt@mattwrock.com>2015-12-07 21:17:03 -0800
commit3e704d162e3ef5dff9e929eca7c82b48c4d66305 (patch)
tree8b2532bd8208edc62b86ca9ef3ddaf5dc443f9ab /lib
parentdc98ac77aafe4676a45eb16a991f982d20130ed2 (diff)
downloadchef-3e704d162e3ef5dff9e929eca7c82b48c4d66305.tar.gz
adds support to installer types inno, nsis, wise and installshield top the windows_package resource
Diffstat (limited to 'lib')
-rw-r--r--lib/chef/exceptions.rb1
-rw-r--r--lib/chef/provider/package/windows.rb109
-rw-r--r--lib/chef/provider/package/windows/exe.rb129
-rw-r--r--lib/chef/provider/package/windows/msi.rb50
-rw-r--r--lib/chef/provider/package/windows/registry_uninstall_entry.rb89
-rw-r--r--lib/chef/win32/api/file.rb52
-rw-r--r--lib/chef/win32/file.rb5
-rw-r--r--lib/chef/win32/file/version_info.rb93
8 files changed, 500 insertions, 28 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 8172311dd6..0f4e74ad11 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -169,6 +169,7 @@ class Chef
class LCMParser < RuntimeError; end
class CannotDetermineHomebrewOwner < Package; end
+ class CannotDetermineWindowsInstallerType < Package; end
# Can not create staging file during file deployment
class FileContentStagingError < RuntimeError
diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb
index 7ff0b71807..ad2a855f2e 100644
--- a/lib/chef/provider/package/windows.rb
+++ b/lib/chef/provider/package/windows.rb
@@ -32,11 +32,7 @@ class Chef
provides :package, os: "windows"
provides :windows_package, os: "windows"
- # Depending on the installer, we may need to examine installer_type or
- # source attributes, or search for text strings in the installer file
- # binary to determine the installer type for the user. Since the file
- # must be on disk to do so, we have to make this choice in the provider.
- require 'chef/provider/package/windows/msi.rb'
+ require 'chef/provider/package/windows/registry_uninstall_entry.rb'
# load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode?
def load_current_resource
@@ -56,24 +52,64 @@ class Chef
@package_provider ||= begin
case installer_type
when :msi
- Chef::Provider::Package::Windows::MSI.new(resource_for_provider)
+ Chef::Log.debug("#{@new_resource} is MSI")
+ require 'chef/provider/package/windows/msi'
+ Chef::Provider::Package::Windows::MSI.new(resource_for_provider, uninstall_registry_entries)
else
- raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'"
+ Chef::Log.debug("#{@new_resource} is EXE with type '#{installer_type}'")
+ require 'chef/provider/package/windows/exe'
+ Chef::Provider::Package::Windows::Exe.new(resource_for_provider, installer_type, uninstall_registry_entries)
end
end
end
def installer_type
+ # Depending on the installer, we may need to examine installer_type or
+ # source attributes, or search for text strings in the installer file
+ # binary to determine the installer type for the user. Since the file
+ # must be on disk to do so, we have to make this choice in the provider.
@installer_type ||= begin
if @new_resource.installer_type
@new_resource.installer_type
- else
- file_extension = ::File.basename(@new_resource.source).split(".").last.downcase
+ elsif source_location.nil?
+ inferred_registry_type
+ else
+ basename = ::File.basename(source_location)
+ file_extension = basename.split(".").last.downcase
if file_extension == "msi"
:msi
else
- raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'"
+ # search the binary file for installer type
+ ::Kernel.open(::File.expand_path(source_location), 'rb') do |io|
+ filesize = io.size
+ bufsize = 4096 # read 4K buffers
+ overlap = 16 # bytes to overlap between buffer reads
+
+ until io.eof
+ contents = io.read(bufsize)
+
+ case contents
+ when /inno/i # Inno Setup
+ return :inno
+ when /wise/i # Wise InstallMaster
+ return :wise
+ when /nullsoft/i # Nullsoft Scriptable Install System
+ return :nsis
+ end
+
+ if (io.tell() < filesize)
+ io.seek(io.tell() - overlap)
+ end
+ end
+ end
+
+ # if file is named 'setup.exe' assume installshield
+ if basename == 'setup.exe'
+ :installshield
+ else
+ fail Chef::Exceptions::CannotDetermineWindowsInstallerType, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'"
+ end
end
end
end
@@ -93,11 +129,11 @@ class Chef
# Chef::Provider::Package action_install + action_remove call install_package + remove_package
# Pass those calls to the correct sub-provider
def install_package(name, version)
- package_provider.install_package(name, version)
+ package_provider.install_package
end
def remove_package(name, version)
- package_provider.remove_package(name, version)
+ package_provider.remove_package
end
# @return [Array] new_version(s) as an array
@@ -106,15 +142,59 @@ class Chef
[new_resource.version]
end
+ # @return [String] candidate_version
+ def candidate_version
+ @candidate_version ||= (@new_resource.version || 'latest')
+ end
+
+ # @return [Array] current_version(s) as an array
+ # this package provider does not support package arrays
+ # However, There may be multiple versions for a single
+ # package so the first element may be a nested array
+ def current_version_array
+ [ current_resource.version ]
+ end
+
+ # @param current_version<String> one or more versions currently installed
+ # @param new_version<String> version of the new resource
+ #
+ # @return [Boolean] true if new_version is equal to or included in current_version
+ def target_version_already_installed?(current_version, new_version)
+ Chef::Log.debug("Checking if #{@new_resource} version '#{new_version}' is already installed. #{current_version} is currently installed")
+ if current_version.is_a?(Array)
+ current_version.include?(new_version)
+ else
+ new_version == current_version
+ end
+ end
+
+ def have_any_matching_version?
+ target_version_already_installed?(current_resource.version, new_resource.version)
+ end
+
private
+ def uninstall_registry_entries
+ @uninstall_registry_entries ||= Chef::Provider::Package::Windows::RegistryUninstallEntry.find_entries(new_resource.name)
+ end
+
+ def inferred_registry_type
+ uninstall_registry_entries.each do |entry|
+ return :inno if entry.key.end_with?("_is1")
+ return :msi if entry.uninstall_string.downcase.start_with?("msiexec.exe ")
+ return :nsis if entry.uninstall_string.downcase.end_with?("uninst.exe\"")
+ end
+ nil
+ end
+
def downloadable_file_missing?
uri_scheme?(new_resource.source) && !::File.exists?(source_location)
end
def resource_for_provider
@resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r|
- r.source(Chef::Util::PathHelper.validate_path(source_location))
+ r.source(Chef::Util::PathHelper.validate_path(source_location)) unless source_location.nil?
+ r.version(new_resource.version)
r.timeout(new_resource.timeout)
r.returns(new_resource.returns)
r.options(new_resource.options)
@@ -151,7 +231,8 @@ class Chef
if uri_scheme?(new_resource.source)
source_resource.path
else
- Chef::Util::PathHelper.cleanpath(new_resource.source)
+ new_source = Chef::Util::PathHelper.cleanpath(new_resource.source)
+ ::File.exist?(new_source) ? new_source : nil
end
end
diff --git a/lib/chef/provider/package/windows/exe.rb b/lib/chef/provider/package/windows/exe.rb
new file mode 100644
index 0000000000..4495868010
--- /dev/null
+++ b/lib/chef/provider/package/windows/exe.rb
@@ -0,0 +1,129 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+# Author:: Matt Wrock <matt@mattwrock.com>
+# Copyright:: Copyright (c) 2011, 2015 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 'chef/mixin/shell_out'
+
+class Chef
+ class Provider
+ class Package
+ class Windows
+ class Exe
+ include Chef::Mixin::ShellOut
+
+ def initialize(resource, installer_type, uninstall_entries)
+ @new_resource = resource
+ @installer_type = installer_type
+ @uninstall_entries = uninstall_entries
+ end
+
+ attr_reader :new_resource
+ attr_reader :installer_type
+ attr_reader :uninstall_entries
+
+ # From Chef::Provider::Package
+ def expand_options(options)
+ options ? " #{options}" : ""
+ end
+
+ # Returns a version if the package is installed or nil if it is not.
+ def installed_version
+ Chef::Log.debug("#{new_resource} checking package version")
+ current_installed_version
+ end
+
+ def package_version
+ new_resource.version || install_file_version
+ end
+
+ def install_package
+ Chef::Log.debug("#{new_resource} installing #{new_resource.installer_type} package '#{new_resource.source}'")
+ shell_out!(
+ [
+ "start",
+ "\"\"",
+ "/wait",
+ "\"#{new_resource.source}\"",
+ unattended_flags,
+ expand_options(new_resource.options),
+ "& exit %%%%ERRORLEVEL%%%%"
+ ].join(" "), timeout: new_resource.timeout, returns: new_resource.returns
+ )
+ end
+
+ def remove_package
+ uninstall_version = new_resource.version || current_installed_version
+ uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) }
+ .map { |version| version.uninstall_string }.uniq.each do |uninstall_string|
+ Chef::Log.debug("Registry provided uninstall string for #{new_resource} is '#{uninstall_string}'")
+ shell_out!(uninstall_command(uninstall_string), { returns: new_resource.returns })
+ end
+ end
+
+ private
+
+ def uninstall_command(uninstall_string)
+ uninstall_string.delete!('"')
+ uninstall_string = [
+ %q{/d"},
+ ::File.dirname(uninstall_string),
+ %q{" },
+ ::File.basename(uninstall_string),
+ expand_options(new_resource.options),
+ " ",
+ unattended_flags
+ ].join
+ %Q{start "" /wait #{uninstall_string} & exit %%%%ERRORLEVEL%%%%}
+ end
+
+ def current_installed_version
+ @current_installed_version ||= uninstall_entries.count == 0 ? nil : begin
+ uninstall_entries.map { |entry| entry.display_version }.uniq
+ end
+ end
+
+ def install_file_version
+ @install_file_version ||= begin
+ if ::File.exist?(@new_resource.source)
+ version_info = Chef::ReservedNames::Win32::File.version_info(new_resource.source)
+ file_version = version_info.FileVersion || version_info.ProductVersion
+ file_version == '' ? nil : file_version
+ else
+ nil
+ end
+ end
+ end
+
+ # http://unattended.sourceforge.net/installers.php
+ def unattended_flags
+ case installer_type
+ when :installshield
+ '/s /sms'
+ when :nsis
+ '/S /NCRC'
+ when :inno
+ '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART'
+ when :wise
+ '/s'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb
index 7fdbbcff35..1cc636b92e 100644
--- a/lib/chef/provider/package/windows/msi.rb
+++ b/lib/chef/provider/package/windows/msi.rb
@@ -29,10 +29,14 @@ class Chef
include Chef::ReservedNames::Win32::API::Installer if (RUBY_PLATFORM =~ /mswin|mingw32|windows/) && Chef::Platform.supports_msi?
include Chef::Mixin::ShellOut
- def initialize(resource)
+ def initialize(resource, uninstall_entries)
@new_resource = resource
+ @uninstall_entries = uninstall_entries
end
+ attr_reader :new_resource
+ attr_reader :uninstall_entries
+
# From Chef::Provider::Package
def expand_options(options)
options ? " #{options}" : ""
@@ -40,27 +44,47 @@ class Chef
# Returns a version if the package is installed or nil if it is not.
def installed_version
- Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}")
- product_code = get_product_property(@new_resource.source, "ProductCode")
- Chef::Log.debug("#{@new_resource} checking package status and version for #{product_code}")
- get_installed_version(product_code)
+ if ::File.exist?(new_resource.source)
+ Chef::Log.debug("#{new_resource} getting product code for package at #{new_resource.source}")
+ product_code = get_product_property(new_resource.source, "ProductCode")
+ Chef::Log.debug("#{new_resource} checking package status and version for #{product_code}")
+ get_installed_version(product_code)
+ else
+ uninstall_entries.count == 0 ? nil : begin
+ uninstall_entries.map { |entry| entry.display_version }.uniq
+ end
+ end
end
def package_version
- Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}")
- get_product_property(@new_resource.source, "ProductVersion")
+ return new_resource.version if new_resource.version
+ if ::File.exist?(new_resource.source)
+ Chef::Log.debug("#{new_resource} getting product version for package at #{new_resource.source}")
+ get_product_property(new_resource.source, "ProductVersion")
+ end
end
- def install_package(name, version)
+ def install_package
# We could use MsiConfigureProduct here, but we'll start off with msiexec
- Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'")
- shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns})
+ Chef::Log.debug("#{new_resource} installing MSI package '#{new_resource.source}'")
+ shell_out!("msiexec /qn /i \"#{new_resource.source}\" #{expand_options(new_resource.options)}", {:timeout => new_resource.timeout, :returns => new_resource.returns})
end
- def remove_package(name, version)
+ def remove_package
# We could use MsiConfigureProduct here, but we'll start off with msiexec
- Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'")
- shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns})
+ if ::File.exist?(new_resource.source)
+ Chef::Log.debug("#{new_resource} removing MSI package '#{new_resource.source}'")
+ shell_out!("msiexec /qn /x \"#{new_resource.source}\" #{expand_options(new_resource.options)}", {:timeout => new_resource.timeout, :returns => new_resource.returns})
+ else
+ uninstall_version = new_resource.version || installed_version
+ uninstall_entries.select { |entry| [uninstall_version].flatten.include?(entry.display_version) }
+ .map { |version| version.uninstall_string }.uniq.each do |uninstall_string|
+ Chef::Log.debug("#{new_resource} removing MSI package version using '#{uninstall_string}'")
+ uninstall_string += expand_options(new_resource.options)
+ uninstall_string += " /Q" unless uninstall_string =~ / \/Q\b/
+ shell_out!(uninstall_string, {:timeout => new_resource.timeout, :returns => new_resource.returns})
+ end
+ end
end
end
end
diff --git a/lib/chef/provider/package/windows/registry_uninstall_entry.rb b/lib/chef/provider/package/windows/registry_uninstall_entry.rb
new file mode 100644
index 0000000000..98398adf5e
--- /dev/null
+++ b/lib/chef/provider/package/windows/registry_uninstall_entry.rb
@@ -0,0 +1,89 @@
+#
+# Author:: Seth Chisamore (<schisamo@chef.io>)
+# Author:: Matt Wrock <matt@mattwrock.com>
+# Copyright:: Copyright (c) 2011, 2015 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 'win32/registry' if (RUBY_PLATFORM =~ /mswin|mingw32|windows/)
+
+class Chef
+ class Provider
+ class Package
+ class Windows
+ class RegistryUninstallEntry
+
+ def self.find_entries(package_name)
+ Chef::Log.debug("Finding uninstall entries for #{package_name}")
+ entries = []
+ [
+ [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0100)],
+ [::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0200)],
+ [::Win32::Registry::HKEY_CURRENT_USER]
+ ].each do |hkey|
+ desired = hkey.length > 1 ? hkey[1] : ::Win32::Registry::Constants::KEY_READ
+ begin
+ ::Win32::Registry.open(hkey[0], UNINSTALL_SUBKEY, desired) do |reg|
+ reg.each_key do |key, _wtime|
+ begin
+ entry = reg.open(key, desired)
+ display_name = read_registry_property(entry, 'DisplayName')
+ if display_name == package_name
+ entries.push(RegistryUninstallEntry.new(hkey, key, entry))
+ end
+ rescue ::Win32::Registry::Error => ex
+ Chef::Log.debug("Registry error opening key '#{key}' on node #{desired}: #{ex}")
+ end
+ end
+ end
+ rescue ::Win32::Registry::Error
+ Chef::Log.debug("Registry error opening hive '#{hkey[0]}' :: #{desired}: #{ex}")
+ end
+ end
+ entries
+ end
+
+ def self.read_registry_property(data, property)
+ data[property]
+ rescue ::Win32::Registry::Error => ex
+ Chef::Log.debug("Failure to read property '#{property}'")
+ nil
+ end
+
+ def initialize(hive, key, registry_data)
+ Chef::Log.debug("Creating uninstall entry for #{hive}::#{key}")
+ @hive = hive
+ @key = key
+ @data = registry_data
+ @display_name = RegistryUninstallEntry.read_registry_property(registry_data, 'DisplayName')
+ @display_version = RegistryUninstallEntry.read_registry_property(registry_data, 'DisplayVersion')
+ @uninstall_string = RegistryUninstallEntry.read_registry_property(registry_data, 'UninstallString')
+ end
+
+ attr_reader :hive
+ attr_reader :key
+ attr_reader :display_name
+ attr_reader :display_version
+ attr_reader :uninstall_string
+ attr_reader :data
+
+ private
+
+ UNINSTALL_SUBKEY = 'Software\Microsoft\Windows\CurrentVersion\Uninstall'.freeze
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/api/file.rb b/lib/chef/win32/api/file.rb
index 9ff1ad40d6..3618d125a1 100644
--- a/lib/chef/win32/api/file.rb
+++ b/lib/chef/win32/api/file.rb
@@ -182,7 +182,14 @@ class Chef
# Win32 API Bindings
###############################################
- ffi_lib 'kernel32'
+ ffi_lib 'kernel32', 'version'
+
+ # Does not map directly to a win32 struct
+ # see https://msdn.microsoft.com/en-us/library/windows/desktop/ms647464(v=vs.85).aspx
+ class Translation < FFI::Struct
+ layout :w_lang, :WORD,
+ :w_code_page, :WORD
+ end
=begin
typedef struct _FILETIME {
@@ -470,6 +477,34 @@ BOOL WINAPI DeviceIoControl(
#);
safe_attach_function :GetVolumeNameForVolumeMountPointW, [:LPCTSTR, :LPTSTR, :DWORD], :BOOL
+=begin
+BOOL WINAPI GetFileVersionInfo(
+ _In_ LPCTSTR lptstrFilename,
+ _Reserved_ DWORD dwHandle,
+ _In_ DWORD dwLen,
+ _Out_ LPVOID lpData
+);
+=end
+ safe_attach_function :GetFileVersionInfoW, [:LPCTSTR, :DWORD, :DWORD, :LPVOID], :BOOL
+
+=begin
+DWORD WINAPI GetFileVersionInfoSize(
+ _In_ LPCTSTR lptstrFilename,
+ _Out_opt_ LPDWORD lpdwHandle
+);
+=end
+ safe_attach_function :GetFileVersionInfoSizeW, [:LPCTSTR, :LPDWORD], :DWORD
+
+=begin
+BOOL WINAPI VerQueryValue(
+ _In_ LPCVOID pBlock,
+ _In_ LPCTSTR lpSubBlock,
+ _Out_ LPVOID *lplpBuffer,
+ _Out_ PUINT puLen
+);
+=end
+ safe_attach_function :VerQueryValueW, [:LPCVOID, :LPCTSTR, :LPVOID, :PUINT], :BOOL
+
###############################################
# Helpers
###############################################
@@ -565,6 +600,21 @@ BOOL WINAPI DeviceIoControl(
file_information
end
+ def retrieve_file_version_info(file_name)
+ file_name = encode_path(file_name)
+ file_size = GetFileVersionInfoSizeW(file_name, nil)
+ if file_size == 0
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ version_info = FFI::MemoryPointer.new(file_size)
+ unless GetFileVersionInfoW(file_name, 0, file_size, version_info)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ version_info
+ end
+
end
end
end
diff --git a/lib/chef/win32/file.rb b/lib/chef/win32/file.rb
index 700ddb24d3..abfad91fdb 100644
--- a/lib/chef/win32/file.rb
+++ b/lib/chef/win32/file.rb
@@ -150,6 +150,10 @@ class Chef
Info.new(file_name)
end
+ def self.version_info(file_name)
+ VersionInfo.new(file_name)
+ end
+
def self.verify_links_supported!
begin
CreateSymbolicLinkW(nil)
@@ -211,3 +215,4 @@ class Chef
end
require 'chef/win32/file/info'
+require 'chef/win32/file/version_info'
diff --git a/lib/chef/win32/file/version_info.rb b/lib/chef/win32/file/version_info.rb
new file mode 100644
index 0000000000..2974c8a695
--- /dev/null
+++ b/lib/chef/win32/file/version_info.rb
@@ -0,0 +1,93 @@
+#
+# Author:: Matt Wrock (<matt@mattwrock.com>)
+# Copyright:: Copyright 2015 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 'chef/win32/file'
+
+class Chef
+ module ReservedNames::Win32
+ class File
+
+ class VersionInfo
+
+ include Chef::ReservedNames::Win32::API::File
+
+ def initialize(file_name)
+ raise Errno::ENOENT, file_name unless ::File.exist?(file_name)
+ @file_version_info = retrieve_file_version_info(file_name)
+ end
+
+ # defining method for each predefined version resource string
+ # see https://msdn.microsoft.com/en-us/library/windows/desktop/ms647464(v=vs.85).aspx
+ [
+ :Comments,
+ :CompanyName,
+ :FileDescription,
+ :FileVersion,
+ :InternalName,
+ :LegalCopyright,
+ :LegalTrademarks,
+ :OriginalFilename,
+ :ProductName,
+ :ProductVersion,
+ :PrivateBuild,
+ :SpecialBuild
+ ].each do |method|
+ define_method method do
+ begin
+ get_version_info_string(method.to_s)
+ rescue Chef::Exceptions::Win32APIError
+ return nil
+ end
+ end
+ end
+
+ private
+
+ def translation
+ @translation ||= begin
+ info_ptr = FFI::MemoryPointer.new(:pointer)
+ unless VerQueryValueW(@file_version_info, "\\VarFileInfo\\Translation".to_wstring, info_ptr, FFI::MemoryPointer.new(:int))
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ # there can potentially be multiple translations but most installers just have one
+ # we use the first because we use this for the version strings which are language
+ # agnostic. If/when we need other fields, we should we should add logic to find
+ # the "best" translation
+ trans = Translation.new(info_ptr.read_pointer)
+ to_hex(trans[:w_lang]) + to_hex(trans[:w_code_page])
+ end
+ end
+
+ def to_hex(integer)
+ integer.to_s(16).rjust(4,"0")
+ end
+
+ def get_version_info_string(string_key)
+ info_ptr = FFI::MemoryPointer.new(:pointer)
+ size_ptr = FFI::MemoryPointer.new(:int)
+ unless VerQueryValueW(@file_version_info, "\\StringFileInfo\\#{translation}\\#{string_key}".to_wstring, info_ptr, size_ptr)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+
+ info_ptr.read_pointer.read_wstring(size_ptr.read_uint)
+ end
+ end
+ end
+ end
+end