summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSalim Alam <salam@chef.io>2015-12-08 10:17:11 -0800
committerSalim Alam <salam@chef.io>2015-12-08 10:17:11 -0800
commit6a9cec48804f097ce55d2934f97ea4409890506a (patch)
treefeb19eedf7151c47fb5db7b831a07cba8b155670
parent5982661df7c09bbdadd5d2b8431f2a7bbba05c0d (diff)
parent3e704d162e3ef5dff9e929eca7c82b48c4d66305 (diff)
downloadchef-6a9cec48804f097ce55d2934f97ea4409890506a.tar.gz
Merge pull request #4193 from chef/mwrock/package
adds support for installer types inno, nsis, wise and installshield to windows_package resource
-rw-r--r--CHANGELOG.md1
-rw-r--r--DOC_CHANGES.md39
-rw-r--r--RELEASE_NOTES.md4
-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
-rw-r--r--spec/functional/resource/windows_package_spec.rb177
-rw-r--r--spec/functional/win32/version_info_spec.rb50
-rw-r--r--spec/unit/provider/package/windows/exe_spec.rb251
-rw-r--r--spec/unit/provider/package/windows/msi_spec.rb104
-rw-r--r--spec/unit/provider/package/windows_spec.rb253
16 files changed, 1343 insertions, 64 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 899fa979e8..2ccb511aea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,6 +54,7 @@
* [**Nolan Davidson**](https://github.com/nsdavidson)
[pr#4014](https://github.com/chef/chef/pull/4014) Adding ksh resource
+* [pr#4193](https://github.com/chef/chef/pull/4196) support for inno, nsis, wise and installshield installer types in windows_package resource
* [pr#4196](https://github.com/chef/chef/pull/4196) multipackage dpkg_package and bonus fixes
* [pr#4185](https://github.com/chef/chef/pull/4185) dpkg provider cleanup
* [pr#4165](https://github.com/chef/chef/pull/4165) Multipackage internal API improvements
diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md
index 58de140ec0..363efd444e 100644
--- a/DOC_CHANGES.md
+++ b/DOC_CHANGES.md
@@ -78,3 +78,42 @@ either reboot immediately (:reboot_now) or queue a reboot (:request_reboot). Th
The --identity-file option to `knife bootstrap` has been deprecated in favor of `knife bootstrap --ssh-identity-file`
to better align with other ssh related options.
+
+### `windows_package` resource
+
+`windows_package` now supports more than just `MSI`. Most common windows installer types are supported including Inno Setup, Nullsoft, Wise and InstallShield. The new allowed `installer_type` values are: `inno`, `nsis`, `wise`, `installshield`, `custom`, and `msi`.
+
+Also, while being able to download remote installers from a `HTTP` resource is not new, it looks as though the top of the docs page is incorrect stating that only local installers can be used as a source.
+
+An unspecified source now only defaults to the name if the name is either a URL or a valid file path.
+
+Example Nullsoft (`nsis`) package resource:
+```
+windows_package 'Mercurial 3.6.1 (64-bit)' do
+ source 'http://mercurial.selenic.com/release/windows/Mercurial-3.6.1-x64.exe'
+ checksum 'febd29578cb6736163d232708b834a2ddd119aa40abc536b2c313fc5e1b5831d'
+end
+```
+
+Example Custom `windows_package` resource:
+```
+windows_package 'Microsoft Visual C++ 2005 Redistributable' do
+ source 'https://download.microsoft.com/download/6/B/B/6BB661D6-A8AE-4819-B79F-236472F6070C/vcredist_x86.exe'
+ installer_type :custom
+ options '/Q'
+end
+```
+Using a `:custom` package is one way to install a non `.msi` file that embeds an `msi` based installer.
+
+Packages can now be removed without the need to include the package `source`. The relevent uninstall metadata will now be discovered from the registry.
+```
+windows_package 'Mercurial 3.6.1 (64-bit)' do
+ action :remove
+end
+```
+It is important that the package name used when not including the `source` is EXACTLY the same as the display name found in "Add/Remove programs" or the `DisplayName` property in the appropriate registry key:
+* HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall
+* HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall
+* HKEY_LOCAL_MACHINE\Software\Wow6464Node\Microsoft\Windows\CurrentVersion\Uninstall
+
+Note that if there are multiple versions of a package installed with the same display name, all packages will be removed unless a version is provided in the `version` attribute or can be discovered in the `source` installer file.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index ff7ab9c54c..5a06af0dbb 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -57,6 +57,10 @@ Please see the following for more details : https://docs.chef.io/release/12-6/re
This is the first release where we are rolling out a MSI package for Windows that significantly improves the installation time. In a nutshell, the new approach is to deploy and extract a zipped package rather than individually tracking every file as a MSI component. Please note that the first upgrade (ie, an older version of Chef client is already on the machine) may not exhibit the full extent of the speed-up (as MSI is still tracking the older files). New installs, as well as future upgrades, will be sped up. Uninstalls will remove the folder that Chef client is installed to (typically, C:\Opscode\Chef).
+## `windows_package` now supports non-`MSI` based Windows installers
+
+Today you can install `MSI`s using the `windows_package` resource. However, you have had to use the windows cookbook in order to install non `MSI` based installer packages such as Nullsoft, Inno Setup, Installshield and other `EXE` based installers. We have moved and slightly improved the windows cookbook resource into the core chef client. This means you can now run most windows installer types without taking on external cookbook dependencies.
+
## Better handling of log_location with chef client service (Windows)
This change is for the scenario when running chef client as a Windows service. Currently, a default log_location gets used by the chef client windows service. This log_location overrides any log_location set in the client.rb. In 12.6.0, the behavior is changed to allow the Chef client running as a Windows service to prefer the log_location in client.rb instead. Now, the windows_service_manager will not explicitly pass in a log_location, and therefore the Chef service will always use what is in the client.rb or the typical default path if none is configured. This enables scenarios such as logging to the Windows event log when running chef client as a Windows service.
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
diff --git a/spec/functional/resource/windows_package_spec.rb b/spec/functional/resource/windows_package_spec.rb
new file mode 100644
index 0000000000..65378653b0
--- /dev/null
+++ b/spec/functional/resource/windows_package_spec.rb
@@ -0,0 +1,177 @@
+#
+# Author:: Matt Wrock (<matt@mattwrock.com>)
+# Copyright:: Copyright (c) 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 'spec_helper'
+require 'functional/resource/base'
+
+describe Chef::Resource::WindowsPackage, :windows_only, :volatile do
+ let(:pkg_name) { nil }
+ let(:pkg_path) { nil }
+ let(:pkg_checksum) { nil }
+ let(:pkg_version) { nil }
+ let(:pkg_type) { nil }
+ let(:pkg_options) { nil }
+
+ subject do
+ new_resource = Chef::Resource::WindowsPackage.new(pkg_name, run_context)
+ new_resource.source pkg_path
+ new_resource.version pkg_version
+ new_resource.installer_type pkg_type
+ new_resource.options pkg_options
+ new_resource.checksum pkg_checksum
+ new_resource
+ end
+
+ describe "multi package scenario" do
+ let(:pkg_name) { 'Microsoft Visual C++ 2005 Redistributable' }
+ let(:pkg_path) { 'https://download.microsoft.com/download/6/B/B/6BB661D6-A8AE-4819-B79F-236472F6070C/vcredist_x86.exe' }
+ let(:pkg_checksum) { nil }
+ let(:pkg_version) { '8.0.59193' }
+ let(:pkg_type) { :custom }
+ let(:pkg_options) { "/Q" }
+
+ it "updates resource on first install" do
+ subject.run_action(:install)
+ expect(subject).to be_updated_by_last_action
+ end
+
+ it "does not update resource when already installed" do
+ subject.run_action(:install)
+ expect(subject).not_to be_updated_by_last_action
+ end
+
+ context "installing additional version" do
+ let(:pkg_path) { 'https://download.microsoft.com/download/e/1/c/e1c773de-73ba-494a-a5ba-f24906ecf088/vcredist_x86.exe' }
+ let(:pkg_version) { '8.0.56336' }
+
+ it "installs older version" do
+ subject.run_action(:install)
+ expect(subject).to be_updated_by_last_action
+ end
+ end
+
+ describe "removing package" do
+ subject { Chef::Resource::WindowsPackage.new(pkg_name, run_context) }
+
+ context "multiple versions and a version given to remove" do
+ before { subject.version('8.0.56336')}
+
+ it "removes specified version" do
+ subject.run_action(:remove)
+ expect(subject).to be_updated_by_last_action
+ prov = subject.provider_for_action(:remove)
+ prov.load_current_resource
+ expect(prov.current_version_array).to eq([['8.0.59193']])
+ end
+ end
+
+ context "single version installed and no version given to remove" do
+ it "removes last remaining version" do
+ subject.run_action(:remove)
+ expect(subject).to be_updated_by_last_action
+ prov = subject.provider_for_action(:remove)
+ prov.load_current_resource
+ expect(prov.current_version_array).to eq([nil])
+ end
+ end
+
+ describe "removing multiple versions at once" do
+ let(:pkg_version) { nil }
+ before do
+ install1 = Chef::Resource::WindowsPackage.new(pkg_name, run_context)
+ install1.source pkg_path
+ install1.version pkg_version
+ install1.installer_type pkg_type
+ install1.options pkg_options
+ install1.run_action(:install)
+
+ install2 = Chef::Resource::WindowsPackage.new(pkg_name, run_context)
+ install2.source 'https://download.microsoft.com/download/e/1/c/e1c773de-73ba-494a-a5ba-f24906ecf088/vcredist_x86.exe'
+ install2.version '8.0.56336'
+ install2.installer_type pkg_type
+ install2.options pkg_options
+ install2.run_action(:install)
+ end
+
+ it "removes all versions" do
+ subject.run_action(:remove)
+ expect(subject).to be_updated_by_last_action
+ prov = subject.provider_for_action(:remove)
+ prov.load_current_resource
+ expect(prov.current_version_array).to eq([nil])
+ end
+ end
+ end
+ end
+
+ describe "package version and installer type" do
+ after { subject.run_action(:remove) }
+
+ context "null soft" do
+ let(:pkg_name) { 'Ultra Defragmenter' }
+ let(:pkg_path) { 'http://iweb.dl.sourceforge.net/project/ultradefrag/stable-release/6.1.1/ultradefrag-6.1.1.bin.amd64.exe' }
+ let(:pkg_checksum) { '11d53ed4c426c8c867ad43f142b7904226ffd9938c02e37086913620d79e3c09' }
+
+ it "finds the correct package version" do
+ subject.run_action(:install)
+ expect(subject.version).to eq('6.1.1')
+ end
+
+ it "finds the correct installer type" do
+ subject.run_action(:install)
+ expect(subject.provider_for_action(:install).installer_type).to eq(:nsis)
+ end
+ end
+
+ context "inno" do
+ let(:pkg_name) { 'Mercurial 3.6.1 (64-bit)' }
+ let(:pkg_path) { 'http://mercurial.selenic.com/release/windows/Mercurial-3.6.1-x64.exe' }
+ let(:pkg_checksum) { 'febd29578cb6736163d232708b834a2ddd119aa40abc536b2c313fc5e1b5831d' }
+
+ it "finds the correct package version" do
+ subject.run_action(:install)
+ expect(subject.version).to eq(nil) # Mercurial does not include versioning
+ end
+
+ it "finds the correct installer type" do
+ subject.run_action(:install)
+ expect(subject.provider_for_action(:install).installer_type).to eq(:inno)
+ end
+ end
+ end
+
+ describe "install from local file" do
+ let(:pkg_name) { 'Mercurial 3.6.1 (64-bit)' }
+ let(:pkg_path) { ::File.join(Chef::Config[:file_cache_path], "package", "Mercurial-3.6.1-x64.exe") }
+ let(:pkg_checksum) { 'febd29578cb6736163d232708b834a2ddd119aa40abc536b2c313fc5e1b5831d' }
+
+ it "installs the app" do
+ subject.run_action(:install)
+ expect(subject).to be_updated_by_last_action
+ end
+ end
+
+ describe "uninstall exe without source" do
+ let(:pkg_name) { 'Mercurial 3.6.1 (64-bit)' }
+
+ it "uninstalls the app" do
+ subject.run_action(:remove)
+ expect(subject).to be_updated_by_last_action
+ end
+ end
+end \ No newline at end of file
diff --git a/spec/functional/win32/version_info_spec.rb b/spec/functional/win32/version_info_spec.rb
new file mode 100644
index 0000000000..c7d41f9616
--- /dev/null
+++ b/spec/functional/win32/version_info_spec.rb
@@ -0,0 +1,50 @@
+#
+# 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 'spec_helper'
+if Chef::Platform.windows?
+ require 'chef/win32/file/version_info'
+end
+
+describe "Chef::ReservedNames::Win32::File::VersionInfo", :windows_only do
+ require 'wmi-lite/wmi'
+ let(:file_path) { ENV['ComSpec'] }
+ let(:os_version) do
+ wmi = WmiLite::Wmi.new
+ os_info = wmi.first_of('Win32_OperatingSystem')
+ os_info['version']
+ end
+
+ subject { Chef::ReservedNames::Win32::File::VersionInfo.new(file_path) }
+
+ it "file version has the same version as windows" do
+ expect(subject.FileVersion).to start_with(os_version)
+ end
+
+ it "product version has the same version as windows" do
+ expect(subject.ProductVersion).to start_with(os_version)
+ end
+
+ it "company is microsoft" do
+ expect(subject.CompanyName).to eq("Microsoft Corporation")
+ end
+
+ it "file description is command processor" do
+ expect(subject.FileDescription).to eq("Windows Command Processor")
+ end
+end
diff --git a/spec/unit/provider/package/windows/exe_spec.rb b/spec/unit/provider/package/windows/exe_spec.rb
new file mode 100644
index 0000000000..730df5e067
--- /dev/null
+++ b/spec/unit/provider/package/windows/exe_spec.rb
@@ -0,0 +1,251 @@
+#
+# Author:: Matt Wrock <matt@mattwrock.com>
+# Copyright:: Copyright (c) 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 'spec_helper'
+require 'chef/provider/package/windows/exe'
+
+unless Chef::Platform.windows?
+ class Chef
+ module ReservedNames::Win32
+ class File
+ def version_info
+ nil
+ end
+ end
+ end
+ end
+end
+
+describe Chef::Provider::Package::Windows::Exe do
+ let(:package_name) { "calculator" }
+ let(:resource_source) { "calculator.exe" }
+ let(:new_resource) do
+ new_resource = Chef::Resource::WindowsPackage.new(package_name)
+ new_resource.source(resource_source)
+ new_resource
+ end
+ let(:uninstall_hash) do
+ [{
+ 'DisplayVersion' => 'outdated',
+ 'UninstallString' => File.join("uninst_dir", "uninst_file")
+ }]
+ end
+ let(:uninstall_entry) do
+ entries = []
+ uninstall_hash.each do |entry|
+ entries.push(Chef::Provider::Package::Windows::RegistryUninstallEntry.new('hive', 'key', entry))
+ end
+ entries
+ end
+ let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :nsis, uninstall_entry) }
+ let(:file_version) { nil }
+ let(:product_version) { nil }
+ let(:version_info) { instance_double("Chef::ReservedNames::Win32::File::Version_info", FileVersion: file_version, ProductVersion: product_version) }
+
+ before(:each) do
+ allow(Chef::ReservedNames::Win32::File).to receive(:version_info).and_return(version_info)
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(true)
+ end
+
+ it "responds to shell_out!" do
+ expect(provider).to respond_to(:shell_out!)
+ end
+
+ describe "expand_options" do
+ it "returns an empty string if passed no options" do
+ expect(provider.expand_options(nil)).to eql ""
+ end
+
+ it "returns a string with a leading space if passed options" do
+ expect(provider.expand_options("--train nope --town no_way")).to eql(" --train nope --town no_way")
+ end
+ end
+
+ describe "installed_version" do
+ it "returns the installed version" do
+ expect(provider.installed_version).to eql(["outdated"])
+ end
+
+ context "no versions installed" do
+ let(:uninstall_hash) { [] }
+
+ it "returns the installed version" do
+ expect(provider.installed_version).to eql(nil)
+ end
+ end
+ end
+
+ describe "package_version" do
+ before { new_resource.version(nil) }
+
+ context "source file does not exist" do
+ before do
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(provider.package_version).to eql(nil)
+ end
+ end
+
+ context "file version is empty" do
+ let(:file_version) { '' }
+
+ it "returns nil" do
+ expect(provider.package_version).to eql(nil)
+ end
+
+ it "returns the version of a package if given" do
+ new_resource.version('v55555')
+ expect(provider.package_version).to eql('v55555')
+ end
+ end
+
+ context "both file and product version are in installer" do
+ let(:file_version) { '1.1.1' }
+ let(:product_version) { '1.1' }
+
+ it "returns the file version" do
+ expect(provider.package_version).to eql('1.1.1')
+ end
+
+ it "returns the version of a package if given" do
+ new_resource.version('v55555')
+ expect(provider.package_version).to eql('v55555')
+ end
+ end
+
+ context "only file version is in installer" do
+ let(:file_version) { '1.1.1' }
+
+ it "returns the file version" do
+ expect(provider.package_version).to eql('1.1.1')
+ end
+
+ it "returns the version of a package if given" do
+ new_resource.version('v55555')
+ expect(provider.package_version).to eql('v55555')
+ end
+ end
+
+ context "only product version is in installer" do
+ let(:product_version) { '1.1' }
+
+ it "returns the product version" do
+ expect(provider.package_version).to eql('1.1')
+ end
+
+ it "returns the version of a package if given" do
+ new_resource.version('v55555')
+ expect(provider.package_version).to eql('v55555')
+ end
+ end
+
+ context "no version info is in installer" do
+ let(:file_version) { nil }
+ let(:product_version) { nil }
+
+ it "returns the version of a package" do
+ new_resource.version('v55555')
+ expect(provider.package_version).to eql('v55555')
+ end
+ end
+
+ context "no version info is in installer and none in attribute" do
+ it "returns the version of a package" do
+ expect(provider.package_version).to eql(nil)
+ end
+ end
+ end
+
+ describe "remove_package" do
+ context "no version given and one package installed" do
+ it "removes installed package" do
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir\" uninst_file \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.remove_package
+ end
+ end
+
+ context "several packages installed" do
+ let(:uninstall_hash) do
+ [
+ {
+ 'DisplayVersion' => 'v1',
+ 'UninstallString' => File.join("uninst_dir1", "uninst_file1")
+ },
+ {
+ 'DisplayVersion' => 'v2',
+ 'UninstallString' => File.join("uninst_dir2", "uninst_file2")
+ }
+ ]
+ end
+
+ context "version given and installed" do
+ it "removes given version" do
+ new_resource.version('v2')
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir2\" uninst_file2 \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.remove_package
+ end
+ end
+
+ context "no version given" do
+ it "removes both versions" do
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir1\" uninst_file1 \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \/d\"uninst_dir2\" uninst_file2 \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.remove_package
+ end
+ end
+ end
+ end
+
+ context "installs nsis installer" do
+ let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :nsis, uninstall_entry) }
+
+ it "calls installer with the correct flags" do
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/S \/NCRC & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.install_package
+ end
+ end
+
+ context "installs installshield installer" do
+ let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :installshield, uninstall_entry) }
+
+ it "calls installer with the correct flags" do
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/s \/sms & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.install_package
+ end
+ end
+
+ context "installs inno installer" do
+ let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :inno, uninstall_entry) }
+
+ it "calls installer with the correct flags" do
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/VERYSILENT \/SUPPRESSMSGBOXES \/NORESTART & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.install_package
+ end
+ end
+
+ context "installs wise installer" do
+ let(:provider) { Chef::Provider::Package::Windows::Exe.new(new_resource, :wise, uninstall_entry) }
+
+ it "calls installer with the correct flags" do
+ expect(provider).to receive(:shell_out!).with(/start \"\" \/wait \"#{Regexp.quote(new_resource.source)}\" \/s & exit %%%%ERRORLEVEL%%%%/, kind_of(Hash))
+ provider.install_package
+ end
+ end
+end
diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb
index bef202847f..9377dcaad9 100644
--- a/spec/unit/provider/package/windows/msi_spec.rb
+++ b/spec/unit/provider/package/windows/msi_spec.rb
@@ -17,17 +17,37 @@
#
require 'spec_helper'
+require 'chef/provider/package/windows/msi'
describe Chef::Provider::Package::Windows::MSI do
let(:node) { double('Chef::Node') }
let(:events) { double('Chef::Events').as_null_object } # mock all the methods
let(:run_context) { double('Chef::RunContext', :node => node, :events => events) }
- let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") }
- let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource) }
-
- before(:each) do
- stub_const("File::ALT_SEPARATOR", "\\")
- allow(::File).to receive(:absolute_path).with("calculator.msi").and_return("calculator.msi")
+ let(:package_name) { "calculator" }
+ let(:resource_source) { "calculator.msi" }
+ let(:resource_version) { nil }
+ let(:new_resource) do
+ new_resource = Chef::Resource::WindowsPackage.new(package_name)
+ new_resource.source(resource_source)
+ new_resource.version(resource_version)
+ new_resource
+ end
+ let(:uninstall_hash) do
+ [{
+ 'DisplayVersion' => 'outdated',
+ 'UninstallString' => "MsiExec.exe /X{guid}"
+ }]
+ end
+ let(:uninstall_entry) do
+ entries = []
+ uninstall_hash.each do |entry|
+ entries.push(Chef::Provider::Package::Windows::RegistryUninstallEntry.new('hive', 'key', entry))
+ end
+ entries
+ end
+ let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource, uninstall_entry) }
+ before do
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(true)
end
it "responds to shell_out!" do
@@ -50,6 +70,11 @@ describe Chef::Provider::Package::Windows::MSI do
allow(provider).to receive(:get_installed_version).with("{23170F69-40C1-2702-0920-000001000000}").and_return("3.14159.1337.42")
expect(provider.installed_version).to eql("3.14159.1337.42")
end
+
+ it "returns the installed version in the registry when install file not present" do
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false)
+ expect(provider.installed_version).to eql(["outdated"])
+ end
end
describe "package_version" do
@@ -57,19 +82,78 @@ describe Chef::Provider::Package::Windows::MSI do
allow(provider).to receive(:get_product_property).with(/calculator.msi$/, "ProductVersion").and_return(42)
expect(provider.package_version).to eql(42)
end
+
+ context "version is explicitly provided" do
+ let(:resource_version) { "given_version" }
+
+ it "returns the given version" do
+ expect(provider.package_version).to eql("given_version")
+ end
+ end
+
+ context "no source or version is given" do
+ before do
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(provider.package_version).to eql(nil)
+ end
+ end
end
describe "install_package" do
it "calls msiexec /qn /i" do
- expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/i \"calculator.msi\"/, kind_of(Hash))
- provider.install_package("unused", "unused")
+ expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/i \"#{Regexp.quote(new_resource.source)}\"/, kind_of(Hash))
+ provider.install_package
end
end
describe "remove_package" do
it "calls msiexec /qn /x" do
- expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/x \"calculator.msi\"/, kind_of(Hash))
- provider.remove_package("unused", "unused")
+ expect(provider).to receive(:shell_out!).with(/msiexec \/qn \/x \"#{Regexp.quote(new_resource.source)}\"/, kind_of(Hash))
+ provider.remove_package
+ end
+
+ context "no source is provided" do
+ before do
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false)
+ end
+
+ it "removes installed package" do
+ expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid} \/Q/, kind_of(Hash))
+ provider.remove_package
+ end
+
+ context "there are multiple installs" do
+ let(:uninstall_hash) do
+ [
+ {
+ 'DisplayVersion' => 'outdated',
+ 'UninstallString' => "MsiExec.exe /X{guid}"
+ },
+ {
+ 'DisplayVersion' => 'really_outdated',
+ 'UninstallString' => "MsiExec.exe /X{guid2}"
+ }
+ ]
+ end
+
+ it "removes both installed package" do
+ expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid} \/Q/, kind_of(Hash))
+ expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid2} \/Q/, kind_of(Hash))
+ provider.remove_package
+ end
+ end
+
+ context "custom options includes /Q" do
+ before { new_resource.options("/Q") }
+
+ it "does not duplicate quiet switch" do
+ expect(provider).to receive(:shell_out!).with(/MsiExec.exe \/X{guid} \/Q/, kind_of(Hash))
+ provider.remove_package
+ end
+ end
end
end
end
diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb
index e5acc87694..c26c446b5b 100644
--- a/spec/unit/provider/package/windows_spec.rb
+++ b/spec/unit/provider/package/windows_spec.rb
@@ -17,6 +17,8 @@
#
require 'spec_helper'
+require 'chef/provider/package/windows/exe'
+require 'chef/provider/package/windows/msi'
describe Chef::Provider::Package::Windows, :windows_only do
before(:each) do
@@ -28,10 +30,19 @@ describe Chef::Provider::Package::Windows, :windows_only do
let(:events) { double('Chef::Events').as_null_object } # mock all the methods
let(:run_context) { double('Chef::RunContext', :node => node, :events => events) }
let(:resource_source) { 'calculator.msi' }
- let(:new_resource) { Chef::Resource::WindowsPackage.new(resource_source) }
+ let(:resource_name) { 'calculator' }
+ let(:new_resource) do
+ new_resource = Chef::Resource::WindowsPackage.new(resource_name)
+ new_resource.source(resource_source)
+ new_resource
+ end
let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) }
let(:cache_path) { 'c:\\cache\\' }
+ before(:each) do
+ allow(::File).to receive(:exist?).with(provider.new_resource.source).and_return(true)
+ end
+
describe "load_current_resource" do
shared_examples "a local file" do
before(:each) do
@@ -43,7 +54,7 @@ describe Chef::Provider::Package::Windows, :windows_only do
it "creates a current resource with the name of the new resource" do
provider.load_current_resource
expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage)
- expect(provider.current_resource.name).to eql(resource_source)
+ expect(provider.current_resource.name).to eql(resource_name)
end
it "sets the current version if the package is installed" do
@@ -76,19 +87,6 @@ describe Chef::Provider::Package::Windows, :windows_only do
end
it_behaves_like "a local file"
end
-
- context "when remote_file_attributes are provided" do
- let (:remote_file_attributes) { {:path => 'C:\\foobar.msi'} }
- before(:each) do
- new_resource.remote_file_attributes(remote_file_attributes)
- end
-
- it 'should override the attributes of the remote file resource used' do
- expect(::File).to receive(:exists?).with(remote_file_attributes[:path])
- provider.load_current_resource
- end
-
- end
end
context "when source is a local file" do
@@ -98,6 +96,7 @@ describe Chef::Provider::Package::Windows, :windows_only do
describe "package_provider" do
shared_examples "a local file" do
+
it "checks that the source path is valid" do
expect(Chef::Util::PathHelper).to receive(:validate_path)
provider.package_provider
@@ -108,9 +107,29 @@ describe Chef::Provider::Package::Windows, :windows_only do
expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI)
end
- it "raises an error if the installer_type is unknown" do
- allow(provider).to receive(:installer_type).and_return(:apt_for_windows)
- expect { provider.package_provider }.to raise_error
+ it "sets the package provider to Exe if the the installer type is :inno" do
+ allow(provider).to receive(:installer_type).and_return(:inno)
+ expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe)
+ end
+
+ it "sets the package provider to Exe if the the installer type is :nsis" do
+ allow(provider).to receive(:installer_type).and_return(:nsis)
+ expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe)
+ end
+
+ it "sets the package provider to Exe if the the installer type is :wise" do
+ allow(provider).to receive(:installer_type).and_return(:wise)
+ expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe)
+ end
+
+ it "sets the package provider to Exe if the the installer type is :installshield" do
+ allow(provider).to receive(:installer_type).and_return(:installshield)
+ expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe)
+ end
+
+ it "defaults to exe if the installer_type is unknown" do
+ allow(provider).to receive(:installer_type).and_return(nil)
+ expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::Exe)
end
end
@@ -146,20 +165,202 @@ describe Chef::Provider::Package::Windows, :windows_only do
end
describe "installer_type" do
- it "it returns @installer_type if it is set" do
+ let(:resource_source) { "microsoft_installer.exe" }
+
+ context "there is no source" do
+ let(:uninstall_hash) do
+ [{
+ 'DisplayVersion' => 'outdated',
+ 'UninstallString' => "blah blah"
+ }]
+ end
+ let(:uninstall_key) { "blah" }
+ let(:uninstall_entry) do
+ entries = []
+ uninstall_hash.each do |entry|
+ entries.push(Chef::Provider::Package::Windows::RegistryUninstallEntry.new('hive', uninstall_key, entry))
+ end
+ entries
+ end
+
+ before do
+ allow(Chef::Provider::Package::Windows::RegistryUninstallEntry).to receive(:find_entries).and_return(uninstall_entry)
+ allow(::File).to receive(:exist?).with(Chef::Util::PathHelper.canonical_path(resource_source, false)).and_return(false)
+ end
+
+ context "uninstall string contains MsiExec.exe" do
+ let(:uninstall_hash) do
+ [{
+ 'DisplayVersion' => 'outdated',
+ 'UninstallString' => "MsiExec.exe /X{guid}"
+ }]
+ end
+
+ it "sets installer_type to MSI" do
+ expect(provider.installer_type).to eql(:msi)
+ end
+ end
+
+ context "uninstall string ends with uninst.exe" do
+ let(:uninstall_hash) do
+ [{
+ 'DisplayVersion' => 'outdated',
+ 'UninstallString' => %q{"c:/hfhfheru/uninst.exe"}
+ }]
+ end
+
+ it "sets installer_type to NSIS" do
+ expect(provider.installer_type).to eql(:nsis)
+ end
+ end
+
+ context "uninstall key ends in _is1" do
+ let(:uninstall_key) { "blah_is1" }
+
+ it "sets installer_type to inno" do
+ expect(provider.installer_type).to eql(:inno)
+ end
+ end
+
+ context "eninstall entries is empty" do
+ before { allow(Chef::Provider::Package::Windows::RegistryUninstallEntry).to receive(:find_entries).and_return([]) }
+
+ it "returns nil" do
+ expect(provider.installer_type).to eql(nil)
+ end
+ end
+ end
+
+ it "returns @installer_type if it is set" do
provider.new_resource.installer_type(:downeaster)
expect(provider.installer_type).to eql(:downeaster)
end
- it "sets installer_type to msi if the source ends in .msi" do
- provider.new_resource.source("microsoft_installer.msi")
- expect(provider.installer_type).to eql(:msi)
+ it "sets installer_type to inno if the source contains inno" do
+ allow(::Kernel).to receive(:open).and_yield(StringIO.new('blah blah inno blah'))
+ expect(provider.installer_type).to eql(:inno)
end
- it "raises an error if it cannot determine the installer type" do
- provider.new_resource.installer_type(nil)
- provider.new_resource.source("tomfoolery.now")
- expect { provider.installer_type }.to raise_error(ArgumentError)
+ it "sets installer_type to wise if the source contains wise" do
+ allow(::Kernel).to receive(:open).and_yield(StringIO.new('blah blah wise blah'))
+ expect(provider.installer_type).to eql(:wise)
+ end
+
+ it "sets installer_type to nsis if the source contains nsis" do
+ allow(::Kernel).to receive(:open).and_yield(StringIO.new('blah blah nullsoft blah'))
+ expect(provider.installer_type).to eql(:nsis)
+ end
+
+ context "source ends in .msi" do
+ let(:resource_source) { "microsoft_installer.msi" }
+
+ it "sets installer_type to msi" do
+ expect(provider.installer_type).to eql(:msi)
+ end
+ end
+
+ context "the source is setup.exe" do
+ let(:resource_source) { "setup.exe" }
+
+ it "sets installer_type to installshield" do
+ allow(::Kernel).to receive(:open).and_yield(StringIO.new(''))
+ expect(provider.installer_type).to eql(:installshield)
+ end
+ end
+
+ context "cannot determine the installer type" do
+ let(:resource_source) { "tomfoolery.now" }
+
+ it "raises an error" do
+ allow(::Kernel).to receive(:open).and_yield(StringIO.new(''))
+ provider.new_resource.installer_type(nil)
+ expect { provider.installer_type }.to raise_error(Chef::Exceptions::CannotDetermineWindowsInstallerType)
+ end
+ end
+ end
+
+ describe "action_install" do
+ let(:new_resource) { Chef::Resource::WindowsPackage.new("blah.exe") }
+ before do
+ new_resource.installer_type(:inno)
+ allow_any_instance_of(Chef::Provider::Package::Windows::Exe).to receive(:package_version).and_return(new_resource.version)
+ end
+
+ context "no version given, discovered or installed" do
+ it "installs latest" do
+ expect(provider).to receive(:install_package).with("blah.exe", "latest")
+ provider.run_action(:install)
+ end
+ end
+
+ context "no version given or discovered but package is installed" do
+ before { allow(provider).to receive(:current_version_array).and_return(["5.5.5"]) }
+
+ it "does not install" do
+ expect(provider).not_to receive(:install_package)
+ provider.run_action(:install)
+ end
+ end
+
+ context "a version is given and none is installed" do
+ before { new_resource.version('5.5.5') }
+
+ it "installs given version" do
+ expect(provider).to receive(:install_package).with("blah.exe", "5.5.5")
+ provider.run_action(:install)
+ end
+ end
+
+ context "a version is given and several are installed" do
+ context "given version matches an installed version" do
+ before do
+ new_resource.version('5.5.5')
+ allow(provider).to receive(:current_version_array).and_return([ ["5.5.5", "4.3.0", "1.1.1"] ])
+ end
+
+ it "does not install" do
+ expect(provider).not_to receive(:install_package)
+ provider.run_action(:install)
+ end
+ end
+
+ context "given version does not match an installed version" do
+ before do
+ new_resource.version('5.5.5')
+ allow(provider).to receive(:current_version_array).and_return([ ["5.5.0", "4.3.0", "1.1.1"] ])
+ end
+
+ it "installs given version" do
+ expect(provider).to receive(:install_package).with("blah.exe", "5.5.5")
+ provider.run_action(:install)
+ end
+ end
+ end
+
+ context "a version is given and one is installed" do
+ context "given version matches installed version" do
+ before do
+ new_resource.version('5.5.5')
+ allow(provider).to receive(:current_version_array).and_return(["5.5.5"])
+ end
+
+ it "does not install" do
+ expect(provider).not_to receive(:install_package)
+ provider.run_action(:install)
+ end
+ end
+
+ context "given version does not match installed version" do
+ before do
+ new_resource.version('5.5.5')
+ allow(provider).to receive(:current_version_array).and_return(["5.5.0"])
+ end
+
+ it "installs given version" do
+ expect(provider).to receive(:install_package).with("blah.exe", "5.5.5")
+ provider.run_action(:install)
+ end
+ end
end
end
end