diff --git a/.fixtures.yml b/.fixtures.yml index b0e87937..c23ac487 100644 --- a/.fixtures.yml +++ b/.fixtures.yml @@ -1,4 +1,5 @@ fixtures: forge_modules: + pwshlib: "puppetlabs/pwshlib" symlinks: "powershell": "#{source_dir}" diff --git a/.gitignore b/.gitignore index c0c92c90..2767022c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,5 @@ /update_report.txt .DS_Store .project -.vscode/ .envrc /inventory.yaml diff --git a/.pdkignore b/.pdkignore index 3cdeeabc..e6215cd0 100644 --- a/.pdkignore +++ b/.pdkignore @@ -23,7 +23,6 @@ /update_report.txt .DS_Store .project -.vscode/ .envrc /inventory.yaml /appveyor.yml @@ -40,3 +39,4 @@ /.travis.yml /.yardopts /spec/ +/.vscode/ diff --git a/.puppet-lint.rc b/.puppet-lint.rc index 8b137891..cc96ece0 100644 --- a/.puppet-lint.rc +++ b/.puppet-lint.rc @@ -1 +1 @@ - +--relative diff --git a/.rubocop.yml b/.rubocop.yml index f8d2f89a..0da419f6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,7 @@ --- -require: rubocop-rspec +require: +- rubocop-rspec +- rubocop-i18n AllCops: DisplayCopNames: true TargetRubyVersion: '2.1' @@ -21,6 +23,14 @@ Bundler/DuplicatedGem: Enabled: false Bundler/OrderedGems: Enabled: false +GetText/DecorateFunctionMessage: + Enabled: false +GetText/DecorateString: + Enabled: false +GetText/DecorateStringFormattingUsingInterpolation: + Enabled: false +GetText/DecorateStringFormattingUsingPercent: + Enabled: false Layout/AccessModifierIndentation: Enabled: false Layout/AlignArray: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 01c13c11..81536b01 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,20 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-08-13 13:07:27 +0800 using RuboCop version 0.49.1. +# on 2019-10-30 10:31:01 -0500 using RuboCop version 0.49.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 6 +GetText/DecorateFunctionMessage: + Exclude: + - 'lib/puppet/provider/exec/powershell.rb' + - 'lib/puppet/provider/exec/pwsh.rb' + - 'spec/spec_helper_acceptance.rb' + # Offense count: 3 -RSpec/LetBeforeExamples: +GetText/DecorateString: Exclude: - - 'spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb' + - 'lib/puppet/provider/exec/powershell.rb' + - 'lib/puppet/provider/exec/pwsh.rb' diff --git a/.sync.yml b/.sync.yml index d9bb2630..b0619027 100644 --- a/.sync.yml +++ b/.sync.yml @@ -1,5 +1,39 @@ --- +".gitlab-ci.yml": + delete: true +".rubocop.yml": + include_todos: true + selected_profile: false +".travis.yml": + simplecov: true + before_install_pre: + - bash <(curl -s https://raw.githubusercontent.com/PowerShell/PowerShell/master/tools/install-powershell.sh) -skip-sudo-check + - 'pwsh -NoProfile -NoLogo -NonInteractive -Command \$PSVersionTable # Output the PowerShell Core version information' + - if [ $BUNDLER_VERSION ]; then + gem install -v $BUNDLER_VERSION bundler --no-rdoc --no-ri; + fi + includes: + - os: osx + comment: test Mac OSX edition of PowerShell Core on a single job + env: PUPPET_GEM_VERSION="~> 6.0" CHECK=parallel_spec + rvm: 2.5.3 + stage: acceptance +appveyor.yml: + simplecov: true + install_post: + - 'ps: "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri https://github.com/PowerShell/PowerShell/raw/master/tools/install-powershell.ps1 -UseBasicParsing -OutFile install-pwsh.ps1"' + - 'ps: "& ./install-pwsh.ps1"' + - set PATH=%LOCALAPPDATA%\Microsoft\powershell;%PATH% + - pwsh -NoProfile -NoLogo -NonInteractive -Command $PSVersionTable + - powershell -NoProfile -NoLogo -NonInteractive -Command $PSVersionTable Gemfile: + optional: + ":development": + - gem: ruby-pwsh + - gem: github_changelog_generator + git: https://github.com/skywinder/github-changelog-generator + ref: 20ee04ba1234e9e83eb2ffb5056e23d641c7a018 + condition: Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2') required: ':system_tests': - gem: 'puppet-module-posix-system-r#{minor_version}' @@ -14,22 +48,5 @@ Gemfile: - gem: master_manipulator - gem: puppet-blacksmith version: '~> 3.4' - -# For the moment don't do any rubocop checks. -.rubocop.yml: - include_todos: true - selected_profile: off - -spec/default_facts.yml: - unmanaged: true - -# Due to https://github.com/puppetlabs/pdk-templates/issues/133 we can't manage Travis CI yet. -.travis.yml: - unmanaged: true - -# Due to https://github.com/puppetlabs/pdk-templates/issues/229 we can't manage Appveyor yet. -appveyor.yml: - unmanaged: true - -.gitlab-ci.yml: - delete: true +spec/spec_helper.rb: + coverage_report: true diff --git a/.travis.yml b/.travis.yml index 6f9d326d..2937a7b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,59 +1,56 @@ --- -os: - - linux - # OSX Only tests on the latest Puppet Gem, not the full matrix as there's no need to double up - # testing effort here. We are only concerned about whether the Mac OSX edition of PowerShell Core - # will work with our PowerShell manager code. - - osx - -dist: trusty +dist: xenial language: ruby cache: bundler before_install: - # Additional instructions - bash <(curl -s https://raw.githubusercontent.com/PowerShell/PowerShell/master/tools/install-powershell.sh) -skip-sudo-check - # Output the PowerShell Core version information - - pwsh -NoProfile -NoLogo -NonInteractive -Command \$PSVersionTable - - if [ $BUNDLER_VERSION ]; then - gem install -v $BUNDLER_VERSION bundler --no-rdoc --no-ri; - fi + - pwsh -NoProfile -NoLogo -NonInteractive -Command \$PSVersionTable # Output the PowerShell Core version information + - if [ $BUNDLER_VERSION ]; then gem install -v $BUNDLER_VERSION bundler --no-rdoc --no-ri; fi - bundle -v - rm -f Gemfile.lock - - gem update --system $RUBYGEMS_VERSION + - "# Update system gems if requested. This is useful to temporarily workaround troubles in the test runner" + - "# See https://github.com/puppetlabs/pdk-templates/commit/705154d5c437796b821691b707156e1b056d244f for an example of how this was used" + - '[ -z "$RUBYGEMS_VERSION" ] || yes | gem update --system $RUBYGEMS_VERSION' - gem --version - bundle -v script: - - 'bundle exec rake $CHECK' + - 'SIMPLECOV=yes bundle exec rake $CHECK' bundler_args: --without system_tests rvm: - - 2.5.1 -env: - global: - - BEAKER_PUPPET_COLLECTION=puppet6 PUPPET_GEM_VERSION="~> 6.0" - - CHECK=parallel_spec + - 2.5.3 +stages: + - static + - spec + - acceptance + - + if: tag =~ ^v\d + name: deploy matrix: fast_finish: true include: - - env: CHECK="syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop" + env: CHECK="check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop syntax lint metadata_lint" + stage: static - - env: PUPPET_GEM_VERSION="~> 5.0" - rvm: 2.4.4 + env: PUPPET_GEM_VERSION="~> 5.0" CHECK=parallel_spec + rvm: 2.4.5 + stage: spec - - env: PUPPET_GEM_VERSION="~> 4.0" RUBYGEMS_VERSION=2.7.8 BUNDLER_VERSION=1.17.3 - rvm: 2.1.9 + env: PUPPET_GEM_VERSION="~> 6.0" CHECK=parallel_spec + rvm: 2.5.3 + stage: spec + - + comment: test Mac OSX edition of PowerShell Core on a single job + env: PUPPET_GEM_VERSION="~> 6.0" CHECK=parallel_spec + os: osx + rvm: 2.5.3 + stage: acceptance + - + env: DEPLOY_TO_FORGE=yes + stage: deploy branches: only: - master - /^v\d/ notifications: email: false -deploy: - provider: puppetforge - user: puppet - password: - secure: "" - on: - tags: true - all_branches: true -condition: "$DEPLOY_TO_FORGE = yes" diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..61777827 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "jpogran.puppet-vscode", + "rebornix.Ruby" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad34dc6..b28da6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,21 @@ -# Changelog +# Change log -All notable changes to this project will be documented in this file. +All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [v3.0.0](https://github.com/puppetlabs/puppetlabs-powershell/tree/v3.0.0) (2020-01-06) -## [Unreleased] +[Full Changelog](https://github.com/puppetlabs/puppetlabs-powershell/compare/2.3.0...v3.0.0) -## [2.3.0] - 2019-04-19 +### Changed + +- \(FM-8475\) Replace library code [\#264](https://github.com/puppetlabs/puppetlabs-powershell/pull/264) ([michaeltlombardi](https://github.com/michaeltlombardi)) + +### Fixed + +- \(MODULES-9473\) Fix Issues Link [\#259](https://github.com/puppetlabs/puppetlabs-powershell/pull/259) ([RandomNoun7](https://github.com/RandomNoun7)) +- \(MODULES-9084\) Increase pipe timeout to 180s [\#257](https://github.com/puppetlabs/puppetlabs-powershell/pull/257) ([michaeltlombardi](https://github.com/michaeltlombardi)) + +## 2.3.0 ### Added @@ -204,3 +213,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a [1.0.3]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.0...1.0.1 + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..a5d109e9 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Setting ownership to the modules team +* @puppetlabs/modules diff --git a/Gemfile b/Gemfile index 61da720a..d0b47119 100644 --- a/Gemfile +++ b/Gemfile @@ -17,16 +17,19 @@ ruby_version_segments = Gem::Version.new(RUBY_VERSION.dup).segments minor_version = ruby_version_segments[0..1].join('.') group :development do - gem "fast_gettext", '1.1.0', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.1.0') - gem "fast_gettext", require: false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0') - gem "json_pure", '<= 2.0.1', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') - gem "json", '= 1.8.1', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.1.9') - gem "json", '= 2.0.4', require: false if Gem::Requirement.create('~> 2.4.2').satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) - gem "json", '= 2.1.0', require: false if Gem::Requirement.create(['>= 2.5.0', '< 2.7.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) - gem "puppet-module-posix-default-r#{minor_version}", require: false, platforms: [:ruby] - gem "puppet-module-posix-dev-r#{minor_version}", require: false, platforms: [:ruby] - gem "puppet-module-win-default-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw] - gem "puppet-module-win-dev-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "fast_gettext", '1.1.0', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.1.0') + gem "fast_gettext", require: false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0') + gem "json_pure", '<= 2.0.1', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') + gem "json", '= 1.8.1', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.1.9') + gem "json", '= 2.0.4', require: false if Gem::Requirement.create('~> 2.4.2').satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem "json", '= 2.1.0', require: false if Gem::Requirement.create(['>= 2.5.0', '< 2.7.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem "rb-readline", '= 0.5.5', require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet-module-posix-default-r#{minor_version}", '~> 0.3', require: false, platforms: [:ruby] + gem "puppet-module-posix-dev-r#{minor_version}", '~> 0.3', require: false, platforms: [:ruby] + gem "puppet-module-win-default-r#{minor_version}", '~> 0.3', require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet-module-win-dev-r#{minor_version}", '~> 0.3', require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "ruby-pwsh", require: false + gem "github_changelog_generator", require: false, git: 'https://github.com/skywinder/github-changelog-generator', ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2') end group :system_tests do gem "puppet-module-posix-system-r#{minor_version}", require: false, platforms: [:ruby] diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 00000000..f1b53983 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,198 @@ +## 2.3.0 + +### Added + +- Metadata for supporting Windows Server 2019 ([FM-7693](https://tickets.puppetlabs.com/browse/FM-7693)) +- Added a 'pwsh' provider for PowerShell Core ([MODULES-8355](https://tickets.puppetlabs.com/browse/MODULES-8355), [MODULES-8356](https://tickets.puppetlabs.com/browse/MODULES-8356), [MODULES-8357](https://tickets.puppetlabs.com/browse/MODULES-8357), [MODULES-8358](https://tickets.puppetlabs.com/browse/MODULES-8358), [MODULES-8359](https://tickets.puppetlabs.com/browse/MODULES-8359)) +- Updated metadata for PowerShell Core support (CentOS, Debian, Fedora, OSX and RedHat) ([MODULES-8356](https://tickets.puppetlabs.com/browse/MODULES-8356)) + +### Changed + +- Only initialise constant when not defined ([MODULES-7067](https://tickets.puppetlabs.com/browse/MODULES-7067)) + +### Fixed + +- Improved pipe reading in the PowerShell Manager ([MODULES-8748](https://tickets.puppetlabs.com/browse/MODULES-8748)) + +## [2.2.0] - 2018-10-29 + +### Added + +- Added support for Puppet 6 ([MODULES-7833](https://tickets.puppetlabs.com/browse/MODULES-7833)) + +### Changed + +- Updated the module to PDK format ([MODULES-7402](https://tickets.puppetlabs.com/browse/MODULES-7402)) +- Updated Beaker to version 4 ([MODULES-7658](https://tickets.puppetlabs.com/browse/MODULES-7658)) + +## [2.1.5] - 2018-05-08 + +### Added + +- Metadata for supporting Windows Server 2016 ([MODULES-4271](https://tickets.puppetlabs.com/browse/MODULES-4271)) + +### Fixed + +- Upgraded message to make .NET Framework requirements clearer when running PowerShell 2.0 ([MODULES-7011](https://tickets.puppetlabs.com/browse/MODULES-7011)) +- Fixed timeout handling when the user specifies a timeout parameter value of `0` to substitute the default of 300 seconds ([MODULES-7018](https://tickets.puppetlabs.com/browse/MODULES-7018)) + +## [2.1.4] - 2017-03-29 + +### Fixed + +- Ensured that the code is able to start the pipes server in a PowerShell process on Windows 2008R2 images ([MODULES-6927](https://tickets.puppetlabs.com/browse/MODULES-6927)) +- Updated PowerShell syntax in README examples + +## [2.1.3] - 2017-12-08 + +### Fixed + +- Fixed timeouts and zombie process creation ([MODULES-4748](https://tickets.puppetlabs.com/browse/MODULES-4748)) +- Corrected PowerShell executable name for experimental cross-platform / PowerShell 6 support ([MODULES-6081](https://tickets.puppetlabs.com/browse/MODULES-6081)) + +## [2.1.2] - 2017-07-27 + +### Fixed + +- Fixed Global Warning variable ([MODULES-5224](https://tickets.puppetlabs.com/browse/MODULES-5224)) +- Moved the PowerShell template file to stop it conflicting with the DSC module ([MODULES-5228](https://tickets.puppetlabs.com/browse/MODULES-5228)) + +## [2.1.1] - 2017-07-07 + +### Added + +- Rake tasks for release automation +- Experimental support for non-Windows Support (CentOS, Ubuntu) ([MODULES-3945](https://tickets.puppetlabs.com/browse/MODULES-3945)) + +### Fixed + +- Updated documentation ([DOC-2960](https://tickets.puppetlabs.com/browse/DOC-2960)) +- Updated metadata for Puppet 4 and Puppet 5 ([MODULES-4528](https://tickets.puppetlabs.com/browse/MODULES-4528), [MODULES-4822](https://tickets.puppetlabs.com/browse/MODULES-4822), [MODULES-5144](https://tickets.puppetlabs.com/browse/MODULES-5144)) +- Dispose runspace on pipe close ([MODULES-4754](https://tickets.puppetlabs.com/browse/MODULES-4754)) +- Removed rspec configuration for win32_console ([MODULES-4976](https://tickets.puppetlabs.com/browse/MODULES-4976)) +- Provider will now respect the environment parameter ([MODULES-4138](https://tickets.puppetlabs.com/browse/MODULES-4138)) +- Return available UI Output on error ([MODULES-5145](https://tickets.puppetlabs.com/browse/MODULES-5145)) + +## [2.1.0] - 2016-11-17 + +### Fixed + +- Support for Windows 2016/WMF 5.1 using named pipes ([MODULES-3690](https://tickets.puppetlabs.com/browse/MODULES-3690)) +- Fixed documentation for herestring ([DOC-2960](https://tickets.puppetlabs.com/browse/DOC-2960)) + +### Added + +- Speed improvements to the PowerShell manager ([MODULES-3690](https://tickets.puppetlabs.com/browse/MODULES-3690)) + +## [2.0.3] - 2016-10-05 + +### Added + +- The ability to set the current working directory ([MODULES-3565](https://tickets.puppetlabs.com/browse/MODULES-3565)) + +### Fixed + +- Miscellaneous fixes to improve reliability +- Fixed capture exit codes when executing external scripts ([MODULES-3399](https://tickets.puppetlabs.com/browse/MODULES-3399)) +- Fixed respect user specified timeout ([MODULES-3709](https://tickets.puppetlabs.com/browse/MODULES-3709)) +- Improved handling of user code exceptions ([MODULES-3443](https://tickets.puppetlabs.com/browse/MODULES-3443)) +- Fixed output line and stacktrace of user code exception ([MODULES-3839](https://tickets.puppetlabs.com/browse/MODULES-3839)) +- Improved the PowerShell host so that it is more resilient to failure ([MODULES-3875](https://tickets.puppetlabs.com/browse/MODULES-3875)) +- Fixed race condition in threading with the PowerShell host ([MODULES-3144](https://tickets.puppetlabs.com/browse/MODULES-3144)) +- Modified tests to detect differences in PowerShell error text ([MODULES-3443](https://tickets.puppetlabs.com/browse/MODULES-3443)) +- Documented how to handle exit codes ([MODULES-3588](https://tickets.puppetlabs.com/browse/MODULES-3588)) + +## [2.0.2] - 2016-07-12 + +### Added + +- Noticable speed increase by reducing the time start for a PowerShell command ([MODULES-3406](https://tickets.puppetlabs.com/browse/MODULES-3406)) +- Tests for try/catch ([MODULES-2634](https://tickets.puppetlabs.com/browse/MODULES-2634)) + +### Fixed + +- Fixed minor bugs in tests ([MODULES-3347](https://tickets.puppetlabs.com/browse/MODULES-3347)) +- Fixed bug with older ruby (1.8) + +## [2.0.1] - 2016-05-24 + +### Fixed + +- Updated the PowerShell manager so that it does not conflict with the PowerShell Manager in the Puppet DSC module ([FM-5240](https://tickets.puppetlabs.com/browse/FM-5240)) + +## [2.0.0] - 2016-05-17 + +### Changed + +- Major performance improvement by sharing a single PowerShell session, instead of creating a new PowerShell session per command. This change no longer writes temporary scripts to file system. ([MODULES-2962](https://tickets.puppetlabs.com/browse/MODULES-2962)) + +### Fixed + +- Updated test suites with later versions ([MODULES-2452](https://tickets.puppetlabs.com/browse/MODULES-2452), [MODULES-3011](https://tickets.puppetlabs.com/browse/MODULES-3011)) +- Cleaned up documentation ([MODULES-3192](https://tickets.puppetlabs.com/browse/MODULES-3192)) +- Removed extra verbose output + +## [1.0.6] - 2015-12-08 + +### Fixed + +- Fixed testing bug when testing on Puppet 3+ on Windows Server 2003 ([MODULES-2443](https://tickets.puppetlabs.com/browse/MODULES-2443)) + +## [1.0.5] - 2015-07-28 + +### Added + +- Metadata for Puppet 4 and PE 2015.2.0 ([FM-2752](https://tickets.puppetlabs.com/browse/FM-2752)) + +### Fixed + +- Minor testing bug fixes ([MODULES-2207](https://tickets.puppetlabs.com/browse/MODULES-2207)) +- Readme cleanup ([DOC-1497](https://tickets.puppetlabs.com/browse/DOC-1497)) + +## [1.0.4] 2014-11-04 + +### Fixed + +- Fixed issues URL in metadata.json + +### Added + +- Future Parser testing support ([FM-1519](https://tickets.puppetlabs.com/browse/FM-1519)) + +## [1.0.3] - 2014-08-25 + +### Fixed + +- Updated tests to verify that PowerShell continues to function on x64-native ruby + +## [1.0.2] - 2014-07-15 + +### Fixed + +- Updated metadata.json so that the module can be uninstalled and upgraded via the puppet module command + +## [1.0.1] + +### Fixed + +- Fixed issue with metadata and PE version requirement + +[Unreleased]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.3.0...master +[2.3.0]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.2.0...2.3.0 +[2.2.0]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.1.5...2.2.0 +[2.1.5]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.1.4...2.1.5 +[2.1.4]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.1.3...2.1.4 +[2.1.3]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.1.2...2.1.3 +[2.1.2]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.1.1...2.1.2 +[2.1.1]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.1.0...2.1.1 +[2.1.0]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.0.3...2.1.0 +[2.0.3]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.0.2...2.0.3 +[2.0.2]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.0.1...2.0.2 +[2.0.1]: https://github.com/puppetlabs/puppetlabs-powershell/compare/2.0.0...2.0.1 +[2.0.0]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.6...2.0.0 +[1.0.6]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.5...1.0.6 +[1.0.5]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.4...1.0.5 +[1.0.4]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.3...1.0.4 +[1.0.3]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.2...1.0.3 +[1.0.2]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.1...1.0.2 +[1.0.1]: https://github.com/puppetlabs/puppetlabs-powershell/compare/1.0.0...1.0.1 diff --git a/MAINTAINERS.md b/MAINTAINERS.md deleted file mode 100644 index 4ca7f289..00000000 --- a/MAINTAINERS.md +++ /dev/null @@ -1,6 +0,0 @@ -## Maintenance - -Maintainers: - - Puppet Windows Team `windows |at| puppet |dot| com` - -Tickets: https://tickets.puppet.com/browse/MODULES. Make sure to set component to `powershell`. diff --git a/REFERENCE.md b/REFERENCE.md new file mode 100644 index 00000000..95cd6af0 --- /dev/null +++ b/REFERENCE.md @@ -0,0 +1,5 @@ +# Reference + + +## Table of Contents + diff --git a/Rakefile b/Rakefile index 750ef467..395df547 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,4 @@ +require 'puppet_litmus/rake_tasks' if Bundler.rubygems.find_name('puppet_litmus').any? require 'puppetlabs_spec_helper/rake_tasks' require 'puppet-syntax/tasks/puppet-syntax' require 'puppet_blacksmith/rake_tasks' if Bundler.rubygems.find_name('puppet-blacksmith').any? @@ -14,8 +15,17 @@ end def changelog_project return unless Rake.application.top_level_tasks.include? "changelog" - returnVal = nil || JSON.load(File.read('metadata.json'))['name'] - raise "unable to find the changelog_project in .sync.yml or the name in metadata.json" if returnVal.nil? + + returnVal = nil + returnVal ||= begin + metadata_source = JSON.load(File.read('metadata.json'))['source'] + metadata_source_match = metadata_source && metadata_source.match(%r{.*\/([^\/]*?)(?:\.git)?\Z}) + + metadata_source_match && metadata_source_match[1] + end + + raise "unable to find the changelog_project in .sync.yml or calculate it from the source in metadata.json" if returnVal.nil? + puts "GitHubChangelogGenerator project:#{returnVal}" returnVal end diff --git a/appveyor.yml b/appveyor.yml index f5ede449..19ce756b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,6 +3,7 @@ version: 1.1.x.{build} branches: only: - master + - release skip_commits: message: /^\(?doc\)?.*/ clone_depth: 10 @@ -13,6 +14,7 @@ init: - 'mkdir C:\ProgramData\PuppetLabs\hiera && exit 0' - 'mkdir C:\ProgramData\PuppetLabs\puppet\var && exit 0' environment: + SIMPLECOV: yes matrix: - RUBY_VERSION: 24-x64 diff --git a/lib/puppet/provider/exec/powershell.rb b/lib/puppet/provider/exec/powershell.rb index 152888b8..ff5fcfce 100644 --- a/lib/puppet/provider/exec/powershell.rb +++ b/lib/puppet/provider/exec/powershell.rb @@ -1,18 +1,14 @@ require 'puppet/provider/exec' -require File.join(File.dirname(__FILE__), '../../../puppet_x/puppetlabs/powershell/compatible_powershell_version') -require File.join(File.dirname(__FILE__), '../../../puppet_x/puppetlabs/powershell/powershell_manager') +begin + require 'ruby-pwsh' +rescue LoadError + raise 'Could not load the "ruby-pwsh" library; is the dependency module puppetlabs-pwshlib installed in this environment?' +end Puppet::Type.type(:exec).provide :powershell, :parent => Puppet::Provider::Exec do confine :operatingsystem => :windows - commands :powershell => - if File.exists?("#{ENV['SYSTEMROOT']}\\sysnative\\WindowsPowershell\\v1.0\\powershell.exe") - "#{ENV['SYSTEMROOT']}\\sysnative\\WindowsPowershell\\v1.0\\powershell.exe" - elsif File.exists?("#{ENV['SYSTEMROOT']}\\system32\\WindowsPowershell\\v1.0\\powershell.exe") - "#{ENV['SYSTEMROOT']}\\system32\\WindowsPowershell\\v1.0\\powershell.exe" - else - 'powershell.exe' - end + commands :powershell => Pwsh::Manager.powershell_path desc <<-EOT Executes Powershell commands. One of the `onlyif`, `unless`, or `creates` @@ -55,20 +51,13 @@ def self.upgrade_message @upgrade_warning_issued = true end - def self.powershell_args - ps_args = ['-NoProfile', '-NonInteractive', '-NoLogo', '-ExecutionPolicy', 'Bypass'] - ps_args << '-Command' if !PuppetX::PowerShell::PowerShellManager.supported? - - ps_args - end - def ps_manager debug_output = Puppet::Util::Log.level == :debug - PuppetX::PowerShell::PowerShellManager.instance(command(:powershell), self.class.powershell_args(), debug: debug_output) + Pwsh::Manager.instance(command(:powershell), Pwsh::Manager.powershell_args, debug: debug_output) end def run(command, check = false) - if !PuppetX::PowerShell::PowerShellManager.supported? + unless Pwsh::Manager.windows_powershell_supported? self.class.upgrade_message write_script(command) do |native_path| # Ideally, we could keep a handle open on the temp file in this @@ -82,10 +71,35 @@ def run(command, check = false) return super("cmd.exe /c \"\"#{native_path(command(:powershell))}\" #{legacy_args} -Command - < \"#{native_path}\"\"", check) end else - return ps_manager.execute_resource(command, resource) + return execute_resource(command, resource) end end + def execute_resource(powershell_code, resource) + working_dir = resource[:cwd] + if (!working_dir.nil?) + fail "Working directory '#{working_dir}' does not exist" unless File.directory?(working_dir) + end + timeout_ms = resource[:timeout].nil? ? nil : resource[:timeout] * 1000 + environment_variables = resource[:environment].nil? ? [] : resource[:environment] + + result = ps_manager.execute(powershell_code, timeout_ms, working_dir, environment_variables) + stdout = result[:stdout] + native_out = result[:native_stdout] + stderr = result[:stderr] + exit_code = result[:exitcode] + + unless stderr.nil? + stderr.each { |e| Puppet.debug "STDERR: #{e.chop}" unless e.empty? } + end + + Puppet.debug "STDERR: #{result[:errormessage]}" unless result[:errormessage].nil? + + output = Puppet::Util::Execution::ProcessOutput.new(stdout.to_s + native_out.to_s, exit_code) + + return output, output + end + def checkexe(command) end diff --git a/lib/puppet/provider/exec/pwsh.rb b/lib/puppet/provider/exec/pwsh.rb index cdec257a..e6b9a9ef 100644 --- a/lib/puppet/provider/exec/pwsh.rb +++ b/lib/puppet/provider/exec/pwsh.rb @@ -1,4 +1,9 @@ require 'puppet/provider/exec' +begin + require 'ruby-pwsh' +rescue LoadError + raise 'Could not load the "ruby-pwsh" library; is the dependency module puppetlabs-pwshlib installed in this environment?' +end Puppet::Type.type(:exec).provide :pwsh, :parent => Puppet::Provider::Exec do desc <<-EOT @@ -17,8 +22,8 @@ def run(command, check = false) @pwsh ||= get_pwsh_command self.fail 'pwsh could not be found' if @pwsh.nil? - if PuppetX::PowerShell::PowerShellManager.supported_on_pwsh? - return ps_manager.execute_resource(command, resource) + if Pwsh::Manager.pwsh_supported? + return execute_resource(command, resource) else write_script(command) do |native_path| # Ideally, we could keep a handle open on the temp file in this @@ -49,35 +54,11 @@ def validatecmd(command) # # @return [String] the absolute path to the found pwsh executable. Returns nil when it does not exist def get_pwsh_command - if Puppet::Util::Platform.windows? - # Environment variables on Windows are not case sensitive however ruby hash keys are. - # Convert all the key names to upcase so we can be sure to find PATH etc. - # Also while ruby can have difficulty changing the case of some UTF8 characters, we're - # only going to use plain ASCII names so this is safe. - current_env = Hash[Puppet::Util.get_environment.map {|k, v| [k.upcase, v] }] - else - # We don't force a case change on non-Windows platforms because it is perfectly - # ok to have 'Path' and 'PATH' - current_env = Puppet::Util.get_environment - end # If the resource specifies a search path use that. Otherwise use the default # PATH from the environment. - search_paths = @resource.nil? || @resource['path'].nil? ? - current_env['PATH'] : - resource[:path].join(File::PATH_SEPARATOR) - - # If we're on Windows, try the default installation locations as a last resort. - # https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-6#msi - if Puppet::Util::Platform.windows? - search_paths += ";#{current_env['PROGRAMFILES']}\\PowerShell\\6" + - ";#{current_env['PROGRAMFILES(X86)']}\\PowerShell\\6" - end - - # Note that just like when we run the command in Puppet::Provider::Exec, the - # resource[:path] replaces the PATH, it doesn't add to it. - Puppet::Util.withenv({'PATH' => search_paths}, Puppet::Util.default_env) do - return Puppet::Util.which('pwsh') - end + @resource.nil? || @resource['path'].nil? ? + Pwsh::Manager.pwsh_path : + Pwsh::Manager.pwsh_path(resource[:path]) end def pwsh_args @@ -89,10 +70,35 @@ def pwsh_args # Retrieves the PowerShell manager specific to our pwsh binary in this resource # # @api private - # @return [PuppetX::PowerShell::PowerShellManager] The PowerShell manager for this resource + # @return [Pwsh::Manager] The PowerShell manager for this resource def ps_manager debug_output = Puppet::Util::Log.level == :debug - PuppetX::PowerShell::PowerShellManager.instance(@pwsh, pwsh_args, debug: debug_output) + Pwsh::Manager.instance(@pwsh, pwsh_args, debug: debug_output) + end + + def execute_resource(powershell_code, resource) + working_dir = resource[:cwd] + if (!working_dir.nil?) + fail "Working directory '#{working_dir}' does not exist" unless File.directory?(working_dir) + end + timeout_ms = resource[:timeout].nil? ? nil : resource[:timeout] * 1000 + environment_variables = resource[:environment].nil? ? [] : resource[:environment] + + result = ps_manager.execute(powershell_code, timeout_ms, working_dir, environment_variables) + stdout = result[:stdout] + native_out = result[:native_stdout] + stderr = result[:stderr] + exit_code = result[:exitcode] + + unless stderr.nil? + stderr.each { |e| Puppet.debug "STDERR: #{e.chop}" unless e.empty? } + end + + Puppet.debug "STDERR: #{result[:errormessage]}" unless result[:errormessage].nil? + + output = Puppet::Util::Execution::ProcessOutput.new(stdout.to_s + native_out.to_s, exit_code) + + return output, output end def write_script(content, &block) diff --git a/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb b/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb deleted file mode 100644 index b97b06a2..00000000 --- a/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb +++ /dev/null @@ -1,51 +0,0 @@ -require File.join(File.dirname(__FILE__), 'powershell_version') - -module PuppetX - module PuppetLabs - module PowerShell - class CompatiblePowerShellVersion - def self.compatible_version? - value = false - - powershell_version = PuppetX::PuppetLabs::PowerShell::PowerShellVersion.version - - return false if powershell_version.nil? - - # PowerShell v1 - definitely not good to go. Really the entire module - # may not even work but I digress - return false if Gem::Version.new(powershell_version) < Gem::Version.new(2) - - # PowerShell v3+, we are good to go b/c .NET 4+ - # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements - # Look at Microsoft .NET Framwork Requirements section. - if Gem::Version.new(powershell_version) >= Gem::Version.new(3) - return true - end - - # If we are using PowerShell v2, we need to see what the latest - # version of .NET is that we have - # https://msdn.microsoft.com/en-us/library/hh925568.aspx - if Puppet::Util::Platform.windows? - require 'win32/registry' - - begin - # At this point in the check, PowerShell is using .NET Framework - # 2.x family, so we only need to verify v3.5 key exists. - # If we were verifying all compatible types we would look for - # any of these keys: v3.5, v4.0, v4 - hive = Win32::Registry::HKEY_LOCAL_MACHINE - # redirection doesn't actually matter here - disable it anyway - hive.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do |reg| - value = true - end - rescue Win32::Registry::Error => e - value = false - end - end - - value - end - end - end - end -end diff --git a/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb b/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb deleted file mode 100644 index 9afd51be..00000000 --- a/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb +++ /dev/null @@ -1,456 +0,0 @@ -require 'rexml/document' -require 'securerandom' -require 'open3' -require 'base64' -require File.join(File.dirname(__FILE__), 'compatible_powershell_version') - -module PuppetX - module PowerShell - class PowerShellManager - attr_reader :powershell_command - attr_reader :powershell_arguments - @@instances = {} - - def self.default_options - { - debug: false, - pipe_timeout: 30 - } - end - - def self.instance(cmd, args, options = {}) - options = default_options.merge!(options) - - key = instance_key(cmd, args, options) - manager = @@instances[key] - - if manager.nil? || !manager.alive? - # ignore any errors trying to tear down this unusable instance - manager.exit if manager rescue nil - @@instances[key] = PowerShellManager.new(cmd, args, options) - end - - @@instances[key] - end - - def self.win32console_enabled? - @win32console_enabled ||= defined?(Win32) && - defined?(Win32::Console) && - Win32::Console.class == Class - end - - def self.compatible_version_of_powershell? - @compatible_powershell_version ||= PuppetX::PuppetLabs::PowerShell::CompatiblePowerShellVersion.compatible_version? - end - - def self.supported? - Puppet::Util::Platform.windows? && - compatible_version_of_powershell? && - !win32console_enabled? - end - - def self.supported_on_pwsh? - !win32console_enabled? - end - - def initialize(cmd, args = [], options = {}) - @usable = true - @powershell_command = cmd - @powershell_arguments = args - - if Puppet::Util::Platform.windows? - # Named pipes under Windows will automatically be mounted in \\.\pipe\... - # https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Windows.cs#L34 - named_pipe_name = "#{SecureRandom.uuid}PuppetPsHost" - # This named pipe path is Windows specific. - pipe_path = "\\\\.\\pipe\\#{named_pipe_name}" - else - # .Net implements named pipes under Linux etc. as Unix Sockets in the filesystem - # Paths that are rooted are not munged within C# Core. - # https://github.com/dotnet/corefx/blob/94e9d02ad70b2224d012ac4a66eaa1f913ae4f29/src/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs#L49-L60 - # https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs#L44 - # https://github.com/dotnet/corefx/blob/a10890f4ffe0fadf090c922578ba0e606ebdd16c/src/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs#L298-L299 - named_pipe_name = File.join(Dir.tmpdir, "#{SecureRandom.uuid}PuppetPsHost") - pipe_path = named_pipe_name - end - pipe_timeout = options[:pipe_timeout] || self.class.default_options[:pipe_timeout] - debug = options[:debug] || self.class.default_options[:debug] - native_cmd = Puppet::Util::Platform.windows? ? "\"#{cmd}\"" : cmd - - ps_args = args + ['-File', self.class.init_path, "\"#{named_pipe_name}\""] - ps_args << '"-EmitDebugOutput"' if debug - # @stderr should never be written to as PowerShell host redirects output - stdin, @stdout, @stderr, @ps_process = Open3.popen3("#{native_cmd} #{ps_args.join(' ')}") - stdin.close - - Puppet.debug "#{Time.now} #{cmd} is running as pid: #{@ps_process[:pid]}" - - # wait up to 30 seconds in 0.2 second intervals to be able to open the pipe - # If the pipe_timeout is ever specified as less than the sleep interval it will - # never try to connect to a pipe and error out as if a timeout occurred. - sleep_interval = 0.2 - (pipe_timeout / sleep_interval).to_int.times do - begin - if Puppet::Util::Platform.windows? - # pipe is opened in binary mode and must always - @pipe = File.open(pipe_path, 'r+b') - else - @pipe = UNIXSocket.new(pipe_path) - end - break - rescue - sleep sleep_interval - end - end - if @pipe.nil? - # Tear down and kill the process if unable to connect to the pipe; failure to do so - # results in zombie processes being left after the puppet run. We discovered that - # Closing @ps_process via .kill instead of using this method actually kills the watcher - # and leaves an orphaned process behind. Failing to close stdout and stderr also leaves - # clutter behind, so explicitly close those too. - @stdout.close if !@stdout.closed? - @stderr.close if !@stderr.closed? - Process.kill("KILL", @ps_process[:pid]) if @ps_process.alive? - raise "Failure waiting for PowerShell process #{@ps_process[:pid]} to start pipe server" - end - Puppet.debug "#{Time.now} PowerShell initialization complete for pid: #{@ps_process[:pid]}" - - at_exit { exit } - end - - def alive? - # powershell process running - @ps_process.alive? && - # explicitly set during a read / write failure, like broken pipe EPIPE - @usable && - # an explicit failure state might not have been hit, but IO may be closed - self.class.is_stream_valid?(@pipe) && - self.class.is_stream_valid?(@stdout) && - self.class.is_stream_valid?(@stderr) - end - - def execute(powershell_code, timeout_ms = nil, working_dir = nil, environment_variables = []) - code = make_ps_code(powershell_code, timeout_ms, working_dir, environment_variables) - # err is drained stderr pipe (not captured by redirection inside PS) - # or during a failure, a Ruby callstack array - out, native_stdout, err = exec_read_result(code) - - # an error was caught during execution that has invalidated any results - return { :exitcode => -1, :stderr => err } if !@usable && out.nil? - - out[:exitcode] = out[:exitcode].to_i if !out[:exitcode].nil? - # if err contains data it must be "real" stderr output - # which should be appended to what PS has already captured - out[:stderr] = out[:stderr].nil? ? [] : [out[:stderr]] - out[:stderr] += err if !err.nil? - out[:native_stdout] = native_stdout - - out - end - - # Executes PowerShell code using the settings from a populated Puppet Exec Resource type - def execute_resource(powershell_code, resource) - working_dir = resource[:cwd] - if (!working_dir.nil?) - fail "Working directory '#{working_dir}' does not exist" unless File.directory?(working_dir) - end - timeout_ms = resource[:timeout].nil? ? nil : resource[:timeout] * 1000 - environment_variables = resource[:environment].nil? ? [] : resource[:environment] - - result = execute(powershell_code, timeout_ms, working_dir, environment_variables) - stdout = result[:stdout] - native_out = result[:native_stdout] - stderr = result[:stderr] - exit_code = result[:exitcode] - - unless stderr.nil? - stderr.each { |e| Puppet.debug "STDERR: #{e.chop}" unless e.empty? } - end - - Puppet.debug "STDERR: #{result[:errormessage]}" unless result[:errormessage].nil? - - output = Puppet::Util::Execution::ProcessOutput.new(stdout.to_s + native_out.to_s, exit_code) - - return output, output - end - - def exit - @usable = false - - Puppet.debug "PowerShellManager exiting..." - - # ask PowerShell pipe server to shutdown if its still running - # rather than expecting the pipe.close to terminate it - write_pipe(pipe_command(:exit)) if !@pipe.closed? rescue nil - - # pipe may still be open, but if stdout / stderr are dead PS process is in trouble - # and will block forever on a write to the pipe - # its safer to close pipe on Ruby side, which gracefully shuts down PS side - @pipe.close if !@pipe.closed? - @stdout.close if !@stdout.closed? - @stderr.close if !@stderr.closed? - - # wait up to 2 seconds for the watcher thread to fully exit - @ps_process.join(2) - end - - def self.init_path - # a PowerShell -File compatible path to bootstrap the instance - path = File.expand_path('../../../templates/powershell', __FILE__) - path = File.join(path, 'init_ps.ps1').gsub('/', '\\') - "\"#{path}\"" - end - - def make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil, environment_variables = []) - begin - - # Zero timeout is a special case. Other modules sometimes treat this - # as an infinite timeout. We don't support infinite, so for the case - # of a user specifying zero, we sub in the default value of 300 - # seconds. - if (timeout_ms == 0) then timeout_ms = 300 * 1000 end - - timeout_ms = Integer(timeout_ms) - - # Lower bound protection. The polling resolution is only 50ms - if (timeout_ms < 50) then timeout_ms = 50 end - rescue - timeout_ms = 300 * 1000 - end - - # Environment array firstly needs to be parsed and converted into a hashtable. And then - # the values passed in need to be converted to a PowerShell Hashtable. - # - # Environment parsing is based on the Puppet exec equivalent code - # https://github.com/puppetlabs/puppet/blob/a9f77d71e992fc2580de7705847e31264e0fbebe/lib/puppet/provider/exec.rb#L35-L49 - environment = {} - if envlist = environment_variables - envlist = [envlist] unless envlist.is_a? Array - envlist.each do |setting| - if setting =~ /^(\w+)=((.|\n)+)$/ - env_name = $1 - value = $2 - if environment.include?(env_name) || environment.include?(env_name.to_sym) - Puppet.warning("Overriding environment setting '#{env_name}' with '#{value}'") - end - environment[env_name] = value - else - Puppet.warning("Cannot understand environment setting #{setting.inspect}") - end - end - end - # Convert the Ruby Hashtable into PowerShell syntax - exec_environment_variables = '@{' - environment.each do |name,value| - # Powershell escapes single quotes inside a single quoted string by just adding - # another single quote i.e. a value of foo'bar turns into 'foo''bar' when single quoted - ps_name = name.gsub('\'','\'\'') - ps_value = value.gsub('\'','\'\'') - exec_environment_variables += " '#{ps_name}' = '#{ps_value}';" - end unless environment.empty? - exec_environment_variables += '}' - - # PS side expects Invoke-PowerShellUserCode is always the return value here - <<-CODE -$params = @{ - Code = @' -#{powershell_code} -'@ - TimeoutMilliseconds = #{timeout_ms} - WorkingDirectory = "#{working_dir}" - ExecEnvironmentVariables = #{exec_environment_variables} -} - -Invoke-PowerShellUserCode @params - CODE - end - - private - - def self.instance_key(cmd, args, options) - cmd + args.join(' ') + options[:debug].to_s - end - - def self.is_readable?(stream, timeout = 0.5) - raise Errno::EPIPE if !is_stream_valid?(stream) - read_ready = IO.select([stream], [], [], timeout) - read_ready && stream == read_ready[0][0] && !stream.eof? - end - - # when a stream has been closed by handle, but Ruby still has a file - # descriptor for it, it can be tricky to determine that it's actually dead - # the .fileno will still return an int, and calling get_osfhandle against - # it returns what the CRT thinks is a valid Windows HANDLE value, but - # that may no longer exist - def self.is_stream_valid?(stream) - # when a stream is closed, its obviously invalid, but Ruby doesn't always know - !stream.closed? && - # so calling stat will yield an EBADF when underlying OS handle is bad - # as this resolves to a HANDLE and then calls the Windows API - !stream.stat.nil? - # any exceptions mean the stream is dead - rescue - false - end - - # copied directly from Puppet 3.7+ to support Puppet 3.5+ - def self.wide_string(str) - # ruby (< 2.1) does not respect multibyte terminators, so it is possible - # for a string to contain a single trailing null byte, followed by garbage - # causing buffer overruns. - # - # See http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?revision=41920&view=revision - newstr = str + "\0".encode(str.encoding) - newstr.encode!('UTF-16LE') - end - - # mutates the given bytes, removing the length prefixed vaule - def self.read_length_prefixed_string(bytes) - # 32-bit integer in Little Endian format - length = bytes.slice!(0, 4).unpack('V').first - return nil if length == 0 - bytes.slice!(0, length).force_encoding(Encoding::UTF_8) - end - - # bytes is a binary string containing a list of length-prefixed - # key / value pairs (of UTF-8 encoded strings) - # this method mutates the incoming value - def self.ps_output_to_hash(bytes) - hash = {} - while !bytes.empty? - hash[read_length_prefixed_string(bytes).to_sym] = read_length_prefixed_string(bytes) - end - - hash - end - - # 1 byte command identifier - # 0 - Exit - # 1 - Execute - def pipe_command(command) - case command - when :exit - "\x00" - when :execute - "\x01" - end - end - - # Data format is: - # 4 bytes - Little Endian encoded 32-bit integer length of string - # Intel CPUs are little endian, hence the .NET Framework typically is - # variable length - UTF8 encoded string bytes - def pipe_data(data) - msg = data.encode(Encoding::UTF_8) - # https://ruby-doc.org/core-1.9.3/Array.html#method-i-pack - [msg.bytes.length].pack('V') + msg.force_encoding(Encoding::BINARY) - end - - def write_pipe(input) - # for compat with Ruby 2.1 and lower, its important to use syswrite and not write - # otherwise the pipe breaks after writing 1024 bytes - written = @pipe.syswrite(input) - @pipe.flush() - - if written != input.length - msg = "Only wrote #{written} out of #{input.length} expected bytes to PowerShell pipe" - raise Errno::EPIPE.new(msg) - end - end - - def read_from_pipe(pipe, timeout = 0.1, &block) - if self.class.is_readable?(pipe, timeout) - l = pipe.readpartial(4096) - Puppet.debug "#{Time.now} PIPE> #{l}" - # since readpartial may return a nil at EOF, skip returning that value - yield l if !l.nil? - end - - nil - end - - def drain_pipe_until_signaled(pipe, signal) - output = [] - - read_from_pipe(pipe) { |s| output << s } until !signal.locked? - - # there's ultimately a bit of a race here - # read one more time after signal is received - read_from_pipe(pipe, 0) { |s| output << s } until !self.class.is_readable?(pipe) - - # string has been binary up to this point, so force UTF-8 now - output == [] ? - [] : - [output.join('').force_encoding(Encoding::UTF_8)] - end - - def read_streams - pipe_done_reading = Mutex.new - pipe_done_reading.lock - start_time = Time.now - - stdout_reader = Thread.new { drain_pipe_until_signaled(@stdout, pipe_done_reading) } - stderr_reader = Thread.new { drain_pipe_until_signaled(@stderr, pipe_done_reading) } - pipe_reader = Thread.new(@pipe) do |pipe| - # read a Little Endian 32-bit integer for length of response - expected_response_length = pipe.sysread(4).unpack('V').first - - next nil if expected_response_length == 0 - # reads the expected bytes as a binary string or fails - buffer = "" - # sysread may not return all of the requested bytes due to buffering or the - # underlying IO system. Keep reading from the pipe until all the bytes are read - loop do - buffer.concat(pipe.sysread(expected_response_length - buffer.length)) - break if buffer.length >= expected_response_length - end - buffer - end - - Puppet.debug "Waited #{Time.now - start_time} total seconds." - - # block until sysread has completed or errors - begin - output = pipe_reader.value - output = self.class.ps_output_to_hash(output) if !output.nil? - ensure - # signal stdout / stderr readers via mutex - # so that Ruby doesn't crash waiting on an invalid event - pipe_done_reading.unlock - end - - # given redirection on PowerShell side, this should always be empty - stdout = stdout_reader.value - - [ - output, - stdout == [] ? nil : stdout.join(''), # native stdout - stderr_reader.value # native stderr - ] - ensure - # failsafe if the prior unlock was never reached / Mutex wasn't unlocked - pipe_done_reading.unlock if pipe_done_reading.locked? - # wait for all non-nil threads to see mutex unlocked and finish - [pipe_reader, stdout_reader, stderr_reader].compact.each(&:join) - end - - def exec_read_result(powershell_code) - write_pipe(pipe_command(:execute)) - write_pipe(pipe_data(powershell_code)) - read_streams() - # if any pipes are broken, the manager is totally hosed - # bad file descriptors mean closed stream handles - # EOFError is a closed pipe (could be as a result of tearing down process) - # Errno::ECONNRESET is a closed unix domain socket (could be as a result of tearing down process) - rescue Errno::EPIPE, Errno::EBADF, EOFError, Errno::ECONNRESET => e - @usable = false - return nil, nil, [e.inspect, e.backtrace].flatten - # catch closed stream errors specifically - rescue IOError => ioerror - raise if !ioerror.message.start_with?('closed stream') - @usable = false - return nil, nil, [ioerror.inspect, ioerror.backtrace].flatten - end - end - end -end diff --git a/lib/puppet_x/puppetlabs/powershell/powershell_version.rb b/lib/puppet_x/puppetlabs/powershell/powershell_version.rb deleted file mode 100644 index 7fe9f7b6..00000000 --- a/lib/puppet_x/puppetlabs/powershell/powershell_version.rb +++ /dev/null @@ -1,53 +0,0 @@ -module PuppetX - module PuppetLabs - module PowerShell - class PowerShellVersion - end - end - end -end - -if Puppet::Util::Platform.windows? - require 'win32/registry' - module PuppetX - module PuppetLabs - module PowerShell - class PowerShellVersion - ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100 - HKLM = Win32::Registry::HKEY_LOCAL_MACHINE - PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' - PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' - REG_KEY = 'PowerShellVersion' - - def self.version - powershell_three_version || powershell_one_version - end - - def self.powershell_one_version - version = nil - begin - HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg| - version = reg[REG_KEY] - end - rescue - version = nil - end - version - end - - def self.powershell_three_version - version = nil - begin - HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg| - version = reg[REG_KEY] - end - rescue - version = nil - end - version - end - end - end - end - end -end diff --git a/lib/puppet_x/templates/powershell/init_ps.ps1 b/lib/puppet_x/templates/powershell/init_ps.ps1 deleted file mode 100644 index a23eb3d4..00000000 --- a/lib/puppet_x/templates/powershell/init_ps.ps1 +++ /dev/null @@ -1,824 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter(Mandatory = $true)] - [String] - $NamedPipeName, - - [Parameter(Mandatory = $false)] - [Switch] - $EmitDebugOutput = $False, - - [Parameter(Mandatory = $false)] - [System.Text.Encoding] - $Encoding = [System.Text.Encoding]::UTF8 -) - -$script:EmitDebugOutput = $EmitDebugOutput -# Necessary for [System.Console]::Error.WriteLine to roundtrip with UTF-8 -[System.Console]::OutputEncoding = $Encoding - -$hostSource = @" -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Security; -using System.Text; -using System.Threading; - -namespace Puppet -{ - public class PuppetPSHostRawUserInterface : PSHostRawUserInterface - { - public PuppetPSHostRawUserInterface() - { - buffersize = new Size(120, 120); - backgroundcolor = ConsoleColor.Black; - foregroundcolor = ConsoleColor.White; - cursorposition = new Coordinates(0, 0); - cursorsize = 1; - } - - private ConsoleColor backgroundcolor; - public override ConsoleColor BackgroundColor - { - get { return backgroundcolor; } - set { backgroundcolor = value; } - } - - private Size buffersize; - public override Size BufferSize - { - get { return buffersize; } - set { buffersize = value; } - } - - private Coordinates cursorposition; - public override Coordinates CursorPosition - { - get { return cursorposition; } - set { cursorposition = value; } - } - - private int cursorsize; - public override int CursorSize - { - get { return cursorsize; } - set { cursorsize = value; } - } - - private ConsoleColor foregroundcolor; - public override ConsoleColor ForegroundColor - { - get { return foregroundcolor; } - set { foregroundcolor = value; } - } - - private Coordinates windowposition; - public override Coordinates WindowPosition - { - get { return windowposition; } - set { windowposition = value; } - } - - private Size windowsize; - public override Size WindowSize - { - get { return windowsize; } - set { windowsize = value; } - } - - private string windowtitle; - public override string WindowTitle - { - get { return windowtitle; } - set { windowtitle = value; } - } - - public override bool KeyAvailable - { - get { return false; } - } - - public override Size MaxPhysicalWindowSize - { - get { return new Size(165, 66); } - } - - public override Size MaxWindowSize - { - get { return new Size(165, 66); } - } - - public override void FlushInputBuffer() - { - throw new NotImplementedException(); - } - - public override BufferCell[,] GetBufferContents(Rectangle rectangle) - { - throw new NotImplementedException(); - } - - public override KeyInfo ReadKey(ReadKeyOptions options) - { - throw new NotImplementedException(); - } - - public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) - { - throw new NotImplementedException(); - } - - public override void SetBufferContents(Rectangle rectangle, BufferCell fill) - { - throw new NotImplementedException(); - } - - public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) - { - throw new NotImplementedException(); - } - } - - public class PuppetPSHostUserInterface : PSHostUserInterface - { - private PuppetPSHostRawUserInterface _rawui; - private StringBuilder _sb; - private StringWriter _errWriter; - private StringWriter _outWriter; - - public PuppetPSHostUserInterface() - { - _sb = new StringBuilder(); - _errWriter = new StringWriter(new StringBuilder()); - // NOTE: StringWriter / StringBuilder are not technically thread-safe - // but PowerShell Write-XXX cmdlets and System.Console.Out.WriteXXX - // should not be executed concurrently within PowerShell, so should be safe - _outWriter = new StringWriter(_sb); - } - - public override PSHostRawUserInterface RawUI - { - get - { - if ( _rawui == null){ - _rawui = new PuppetPSHostRawUserInterface(); - } - return _rawui; - } - } - - public void ResetConsoleStreams() - { - System.Console.SetError(_errWriter); - System.Console.SetOut(_outWriter); - } - - public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) - { - _sb.Append(value); - } - - public override void Write(string value) - { - _sb.Append(value); - } - - public override void WriteDebugLine(string message) - { - _sb.AppendLine("DEBUG: " + message); - } - - public override void WriteErrorLine(string value) - { - _sb.AppendLine(value); - } - - public override void WriteLine(string value) - { - _sb.AppendLine(value); - } - - public override void WriteVerboseLine(string message) - { - _sb.AppendLine("VERBOSE: " + message); - } - - public override void WriteWarningLine(string message) - { - _sb.AppendLine("WARNING: " + message); - } - - public override void WriteProgress(long sourceId, ProgressRecord record) - { - } - - public string Output - { - get - { - _outWriter.Flush(); - string text = _outWriter.GetStringBuilder().ToString(); - _outWriter.GetStringBuilder().Length = 0; // Only .NET 4+ has .Clear() - return text; - } - } - - public string StdErr - { - get - { - _errWriter.Flush(); - string text = _errWriter.GetStringBuilder().ToString(); - _errWriter.GetStringBuilder().Length = 0; // Only .NET 4+ has .Clear() - return text; - } - } - - public override Dictionary Prompt(string caption, string message, Collection descriptions) - { - throw new NotImplementedException(); - } - - public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) - { - throw new NotImplementedException(); - } - - public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) - { - throw new NotImplementedException(); - } - - public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) - { - throw new NotImplementedException(); - } - - public override string ReadLine() - { - throw new NotImplementedException(); - } - - public override SecureString ReadLineAsSecureString() - { - throw new NotImplementedException(); - } - } - - public class PuppetPSHost : PSHost - { - private Guid _hostId = Guid.NewGuid(); - private bool shouldExit; - private int exitCode; - - private readonly PuppetPSHostUserInterface _ui = new PuppetPSHostUserInterface(); - - public PuppetPSHost () {} - - public bool ShouldExit { get { return this.shouldExit; } } - public int ExitCode { get { return this.exitCode; } } - public void ResetExitStatus() - { - this.exitCode = 0; - this.shouldExit = false; - } - public void ResetConsoleStreams() - { - _ui.ResetConsoleStreams(); - } - - public override Guid InstanceId { get { return _hostId; } } - public override string Name { get { return "PuppetPSHost"; } } - public override Version Version { get { return new Version(1, 1); } } - public override PSHostUserInterface UI - { - get { return _ui; } - } - public override CultureInfo CurrentCulture - { - get { return Thread.CurrentThread.CurrentCulture; } - } - public override CultureInfo CurrentUICulture - { - get { return Thread.CurrentThread.CurrentUICulture; } - } - - public override void EnterNestedPrompt() { throw new NotImplementedException(); } - public override void ExitNestedPrompt() { throw new NotImplementedException(); } - public override void NotifyBeginApplication() { return; } - public override void NotifyEndApplication() { return; } - - public override void SetShouldExit(int exitCode) - { - this.shouldExit = true; - this.exitCode = exitCode; - } - } -} -"@ - -Add-Type -TypeDefinition $hostSource -Language CSharp -$global:DefaultWorkingDirectory = (Get-Location -PSProvider FileSystem).Path - -#this is a string so we can import into our dynamic PS instance -$global:ourFunctions = @' -function Get-ProcessEnvironmentVariables -{ - $processVars = [Environment]::GetEnvironmentVariables('Process').Keys | - % -Begin { $h = @{} } -Process { $h.$_ = (Get-Item Env:\$_).Value } -End { $h } - - # eliminate Machine / User vars so that we have only process vars - 'Machine', 'User' | - % { [Environment]::GetEnvironmentVariables($_).GetEnumerator() } | - ? { $processVars.ContainsKey($_.Name) -and ($processVars[$_.Name] -eq $_.Value) } | - % { $processVars.Remove($_.Name) } - - $processVars.GetEnumerator() | Sort-Object Name -} - -function Reset-ProcessEnvironmentVariables -{ - param($processVars) - - # query Machine vars from registry, ensuring expansion EXCEPT for PATH - $vars = [Environment]::GetEnvironmentVariables('Machine').GetEnumerator() | - % -Begin { $h = @{} } -Process { $v = if ($_.Name -eq 'Path') { $_.Value } else { [Environment]::GetEnvironmentVariable($_.Name, 'Machine') }; $h."$($_.Name)" = $v } -End { $h } - - # query User vars from registry, ensuring expansion EXCEPT for PATH - [Environment]::GetEnvironmentVariables('User').GetEnumerator() | % { - if ($_.Name -eq 'Path') { $vars[$_.Name] += ';' + $_.Value } - else - { - $value = [Environment]::GetEnvironmentVariable($_.Name, 'User') - $vars[$_.Name] = $value - } - } - - $processVars.GetEnumerator() | % { $vars[$_.Name] = $_.Value } - - Remove-Item -Path Env:\* -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Recurse - - $vars.GetEnumerator() | % { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value } -} - -function Reset-ProcessPowerShellVariables -{ - param($psVariables) - $psVariables | %{ - $tempVar = $_ - if(-not(Get-Variable -Name $_.Name -ErrorAction SilentlyContinue)){ - New-Variable -Name $_.Name -Value $_.Value -Description $_.Description -Option $_.Options -Visibility $_.Visibility - } - } -} -'@ - -function Invoke-PowerShellUserCode -{ - [CmdletBinding()] - param( - [String] - $Code, - - [Int] - $TimeoutMilliseconds, - - [String] - $WorkingDirectory, - - [Hashtable] - $ExecEnvironmentVariables - ) - - if ($global:runspace -eq $null){ - # CreateDefault2 requires PS3 - if ([System.Management.Automation.Runspaces.InitialSessionState].GetMethod('CreateDefault2')){ - $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2() - }else{ - $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() - } - - $global:puppetPSHost = New-Object Puppet.PuppetPSHost - $global:runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($global:puppetPSHost, $sessionState) - $global:runspace.Open() - } - - try - { - $ps = $null - $global:puppetPSHost.ResetExitStatus() - $global:puppetPSHost.ResetConsoleStreams() - - if ($PSVersionTable.PSVersion -ge [Version]'3.0') { - $global:runspace.ResetRunspaceState() - } - - $ps = [System.Management.Automation.PowerShell]::Create() - $ps.Runspace = $global:runspace - [Void]$ps.AddScript($global:ourFunctions) - $ps.Invoke() - - if ([string]::IsNullOrEmpty($WorkingDirectory)) { - [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($global:DefaultWorkingDirectory) - } else { - if (-not (Test-Path -Path $WorkingDirectory)) { Throw "Working directory `"$WorkingDirectory`" does not exist" } - [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($WorkingDirectory) - } - - if(!$global:environmentVariables){ - $ps.Commands.Clear() - $global:environmentVariables = $ps.AddCommand('Get-ProcessEnvironmentVariables').Invoke() - } - - if($PSVersionTable.PSVersion -le [Version]'2.0'){ - if(!$global:psVariables){ - $global:psVariables = $ps.AddScript('Get-Variable').Invoke() - } - - $ps.Commands.Clear() - [void]$ps.AddScript('Get-Variable -Scope Global | Remove-Variable -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue') - $ps.Invoke() - - $ps.Commands.Clear() - [void]$ps.AddCommand('Reset-ProcessPowerShellVariables').AddParameter('psVariables', $global:psVariables) - $ps.Invoke() - } - - $ps.Commands.Clear() - [Void]$ps.AddCommand('Reset-ProcessEnvironmentVariables').AddParameter('processVars', $global:environmentVariables) - $ps.Invoke() - - # Set any exec level environment variables - if ($ExecEnvironmentVariables -ne $null) { - $ExecEnvironmentVariables.GetEnumerator() | % { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value } - } - - # we clear the commands before each new command - # to avoid command pollution - $ps.Commands.Clear() - [Void]$ps.AddScript($Code) - - # out-default and MergeMyResults takes all output streams - # and writes it to the PSHost we create - # this needs to be the last thing executed - [void]$ps.AddCommand("out-default"); - - # if the call operator & established an exit code, exit with it - [Void]$ps.AddScript('if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }') - - if($PSVersionTable.PSVersion -le [Version]'2.0'){ - $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::Error, [System.Management.Automation.Runspaces.PipelineResultTypes]::Output); - }else{ - $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::All, [System.Management.Automation.Runspaces.PipelineResultTypes]::Output); - } - $asyncResult = $ps.BeginInvoke() - - if (!$asyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)){ - # forcibly terminate execution of pipeline - $ps.Stop() - throw "Catastrophic failure: PowerShell module timeout ($TimeoutMilliseconds ms) exceeded while executing" - } - - try - { - $ps.EndInvoke($asyncResult) - } catch [System.Management.Automation.IncompleteParseException] { - # https://msdn.microsoft.com/en-us/library/system.management.automation.incompleteparseexception%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 - throw $_.Exception.Message - } catch { - if ($_.Exception.InnerException -ne $null) - { - throw $_.Exception.InnerException - } else { - throw $_.Exception - } - } - - [Puppet.PuppetPSHostUserInterface]$ui = $global:puppetPSHost.UI - return @{ - exitcode = $global:puppetPSHost.Exitcode; - stdout = $ui.Output; - stderr = $ui.StdErr; - errormessage = $null; - } - } - catch - { - try - { - if ($global:runspace) { $global:runspace.Dispose() } - } - finally - { - $global:runspace = $null - } - if(($global:puppetPSHost -ne $null) -and $global:puppetPSHost.ExitCode){ - $ec = $global:puppetPSHost.ExitCode - }else{ - # This is technically not true at this point as we do not - # know what exitcode we should return as an unexpected exception - # happened and the user did not set an exitcode. Our best guess - # is to return 1 so that we ensure Puppet reports this run as an error. - $ec = 1 - } - - if ($_.Exception.ErrorRecord.InvocationInfo -ne $null) - { - $output = $_.Exception.Message + "`n`r" + $_.Exception.ErrorRecord.InvocationInfo.PositionMessage - } else { - $output = $_.Exception.Message | Out-String - } - - # make an attempt to read Output / StdErr as it may contain partial output / info about failures - try { $out = $global:puppetPSHost.UI.Output } catch { $out = $null } - try { $err = $global:puppetPSHost.UI.StdErr } catch { $err = $null } - - return @{ - exitcode = $ec; - stdout = $out; - stderr = $err; - errormessage = $output; - } - } - finally - { - if ($ps -ne $null) { [Void]$ps.Dispose() } - } -} - -function Write-SystemDebugMessage -{ - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [String] - $Message - ) - - if ($script:EmitDebugOutput -or ($DebugPreference -ne 'SilentlyContinue')) - { - [System.Diagnostics.Debug]::WriteLine($Message) - } -} - -function Signal-Event -{ - [CmdletBinding()] - param( - [String] - $EventName - ) - - $event = [System.Threading.EventWaitHandle]::OpenExisting($EventName) - - [Void]$event.Set() - [Void]$event.Close() - if ($PSVersionTable.CLRVersion.Major -ge 3) { - [Void]$event.Dispose() - } - - Write-SystemDebugMessage -Message "Signaled event $EventName" -} - -function ConvertTo-LittleEndianBytes -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Int32] - $Value - ) - - $bytes = [BitConverter]::GetBytes($Value) - if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes) } - - return $bytes -} - -function ConvertTo-ByteArray -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [Hashtable] - $Hash, - - [Parameter(Mandatory = $true)] - [System.Text.Encoding] - $Encoding - ) - - # Initialize empty byte array that can be appended to - $result = [Byte[]]@() - # and add length / name / length / value from Hashtable - $Hash.GetEnumerator() | - % { - $name = $Encoding.GetBytes($_.Name) - $result += (ConvertTo-LittleEndianBytes $name.Length) + $name - - $value = @() - if ($_.Value -ne $null) { $value = $Encoding.GetBytes($_.Value.ToString()) } - $result += (ConvertTo-LittleEndianBytes $value.Length) + $value - } - - return $result -} - -function Write-StreamResponse -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [System.IO.Pipes.PipeStream] - $Stream, - - [Parameter(Mandatory = $true)] - [Byte[]] - $Bytes - ) - - $length = ConvertTo-LittleEndianBytes -Value $Bytes.Length - $Stream.Write($length, 0, 4) - $Stream.Flush() - - Write-SystemDebugMessage -Message "Wrote Int32 $($bytes.Length) as Byte[] $length to Stream" - - $Stream.Write($bytes, 0, $bytes.Length) - $Stream.Flush() - - Write-SystemDebugMessage -Message "Wrote $($bytes.Length) bytes of data to Stream" -} - -function Read-Int32FromStream -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [System.IO.Pipes.PipeStream] - $Stream - ) - - $length = New-Object Byte[] 4 - # Read blocks until all 4 bytes available - $Stream.Read($length, 0, 4) | Out-Null - # value is sent in Little Endian, but if the CPU is not, in-place reverse the array - if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($length) } - $value = [BitConverter]::ToInt32($length, 0) - - Write-SystemDebugMessage -Message "Read Byte[] $length from stream as Int32 $value" - - return $value -} - -# Message format is: -# 1 byte - command identifier -# 0 - Exit -# 1 - Execute -# -1 - Exit - automatically returned when ReadByte encounters a closed pipe -# [optional] 4 bytes - Little Endian encoded 32-bit code block length for execute -# Intel CPUs are little endian, hence the .NET Framework typically is -# [optional] variable length - code block -function ConvertTo-PipeCommand -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [System.IO.Pipes.PipeStream] - $Stream, - - [Parameter(Mandatory = $true)] - [System.Text.Encoding] - $Encoding, - - [Parameter(Mandatory = $false)] - [Int32] - $BufferChunkSize = 4096 - ) - - # command identifier is a single value - ReadByte blocks until byte is ready / pipe closes - $command = $Stream.ReadByte() - - Write-SystemDebugMessage -Message "Command id $command read from pipe" - - switch ($command) - { - # Exit - # ReadByte returns a -1 when the pipe is closed on the other end - { @(0, -1) -contains $_ } { return @{ Command = 'Exit' }} - - # Execute - 1 { $parsed = @{ Command = 'Execute' } } - - default { throw "Catastrophic failure: Unexpected Command $command received" } - } - - # read size of incoming byte buffer - $parsed.Length = Read-Int32FromStream -Stream $Stream - Write-SystemDebugMessage -Message "Expecting $($parsed.Length) raw bytes of $($Encoding.EncodingName) characters" - - # Read blocks until all bytes are read or EOF / broken pipe hit - tested with 5MB and worked fine - $parsed.RawData = New-Object Byte[] $parsed.Length - $readBytes = 0 - do { - $attempt = $attempt + 1 - # This will block if there's not enough data in the pipe - $read = $Stream.Read($parsed.RawData, $readBytes, $parsed.Length - $readBytes) - if ($read -eq 0) - { - throw "Catastrophic failure: Expected $($parsed.Length - $readBytesh) raw bytes, but the pipe reached an end of stream" - } - - $readBytes = $readBytes + $read - Write-SystemDebugMessage -Message "Read $($read) bytes from the pipe" - } while ($readBytes -lt $parsed.Length) - - if ($readBytes -lt $parsed.Length) - { - throw "Catastrophic failure: Expected $($parsed.Length) raw bytes, only received $readBytes" - } - - # turn the raw bytes into the expected encoded string! - $parsed.Code = $Encoding.GetString($parsed.RawData) - - return $parsed -} - -function Start-PipeServer -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [String] - $CommandChannelPipeName, - - [Parameter(Mandatory = $true)] - [System.Text.Encoding] - $Encoding - ) - - Add-Type -AssemblyName System.Core - - # this does not require versioning in the payload as client / server are tightly coupled - $server = New-Object System.IO.Pipes.NamedPipeServerStream($CommandChannelPipeName, - [System.IO.Pipes.PipeDirection]::InOut) - - try - { - # block until Ruby process connects - $server.WaitForConnection() - - Write-SystemDebugMessage -Message "Incoming Connection to $CommandChannelPipeName Received - Expecting Strings as $($Encoding.EncodingName)" - - # Infinite Loop to process commands until EXIT received - $running = $true - while ($running) - { - # throws if an unxpected command id is read from pipe - $response = ConvertTo-PipeCommand -Stream $server -Encoding $Encoding - - Write-SystemDebugMessage -Message "Received $($response.Command) command from client" - - switch ($response.Command) - { - 'Execute' { - Write-SystemDebugMessage -Message "[Execute] Invoking user code:`n`n $($response.Code)" - - # assuming that the Ruby code always calls Invoked-PowerShellUserCode, - # result should already be returned as a hash - $result = Invoke-Expression $response.Code - - $bytes = ConvertTo-ByteArray -Hash $result -Encoding $Encoding - - Write-StreamResponse -Stream $server -Bytes $bytes - } - 'Exit' { $running = $false } - } - } - } - catch [Exception] - { - Write-SystemDebugMessage -Message "PowerShell Pipe Server Failed!`n`n$_" - throw - } - finally - { - if ($global:runspace -ne $null) - { - $global:runspace.Dispose() - Write-SystemDebugMessage -Message "PowerShell Runspace Disposed`n`n$_" - } - if ($server -ne $null) - { - $server.Dispose() - Write-SystemDebugMessage -Message "NamedPipeServerStream Disposed`n`n$_" - } - } -} - -Start-PipeServer -CommandChannelPipeName $NamedPipeName -Encoding $Encoding -Write-SystemDebugMessage -Message "Start-PipeServer Finished`n`n$_" diff --git a/metadata.json b/metadata.json index 367d7f2a..e342b4b5 100644 --- a/metadata.json +++ b/metadata.json @@ -1,14 +1,17 @@ { "name": "puppetlabs-powershell", - "version": "2.3.0", - "author": "Puppet Inc", + "version": "3.0.0", + "author": "puppetlabs", "summary": "Adds a new exec provider for executing PowerShell commands.", "license": "Apache-2.0", "source": "https://github.com/puppetlabs/puppetlabs-powershell", "project_page": "https://github.com/puppetlabs/puppetlabs-powershell", - "issues_url": "https://tickets.puppet.com/browse/MODULES/component/12015", + "issues_url": "https://tickets.puppet.com/browse/MODULES", "dependencies": [ - + { + "name": "puppetlabs/pwshlib", + "version_requirement": ">= 0.1.0 < 2.0.0" + } ], "operatingsystem_support": [ { @@ -74,7 +77,7 @@ "version_requirement": ">= 4.7.0 < 7.0.0" } ], - "pdk-version": "1.8.0", - "template-url": "https://github.com/puppetlabs/pdk-templates", - "template-ref": "heads/master-0-ga6554ab" -} \ No newline at end of file + "pdk-version": "1.15.0", + "template-url": "https://github.com/puppetlabs/pdk-templates#master", + "template-ref": "heads/master-0-g73e79b9" +} diff --git a/spec/default_facts.yml b/spec/default_facts.yml index ea1e4808..f777abfc 100644 --- a/spec/default_facts.yml +++ b/spec/default_facts.yml @@ -3,5 +3,6 @@ # Facts specified here will override the values provided by rspec-puppet-facts. --- ipaddress: "172.16.254.254" +ipaddress6: "FE80:0000:0000:0000:AAAA:AAAA:AAAA" is_pe: false macaddress: "AA:AA:AA:AA:AA:AA" diff --git a/spec/exit-27.ps1 b/spec/exit-27.ps1 deleted file mode 100644 index c3605d3f..00000000 --- a/spec/exit-27.ps1 +++ /dev/null @@ -1 +0,0 @@ -exit 27 diff --git a/spec/integration/provider/exec/pwsh_spec.rb b/spec/integration/provider/exec/pwsh_spec.rb deleted file mode 100644 index 7d1d6381..00000000 --- a/spec/integration/provider/exec/pwsh_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' -require 'puppet/util' -require 'fileutils' - -# Helper function to determine if PowerShell Core (pwsh) is available in the PATH. -$pwsh_path_exist = nil -def pwsh_exist? - return $pwsh_path_exist unless $pwsh_path_exist.nil? - - name = Puppet.features.microsoft_windows? ? 'pwsh.exe' : 'pwsh' - result = ENV['PATH'].split(File::PATH_SEPARATOR).map {|p| File.join(p, name)}.find {|f| File.executable?(f)} - - $pwsh_path_exist = !result.nil? -end - -describe Puppet::Type.type(:exec).provider(:pwsh) do - let(:command) { '$(Get-CIMInstance Win32_Account -Filter "SID=\'S-1-5-18\'") | Format-List' } - let(:args) { '-NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -Command -' } - - let(:resource) { Puppet::Type.type(:exec).new(:command => command, :provider => :pwsh) } - let(:provider) { described_class.new(resource) } - - describe "#run" do - # The usage of uname is a little fragile however there is basically nothing - # which is universal across all Linux/Unix/Mac distributions; Unlike Well Known SIDS in Windows - # The closest is the presence of the uname command and its generic text output - let(:command) { Puppet.features.microsoft_windows? ? - '$(Get-CIMInstance Win32_Account -Filter "SID=\'S-1-5-18\'") | Format-List' : - '& uname' } - let(:command_output_regex) { Puppet.features.microsoft_windows? ? /SID\s+:\s+S-1-5-18/ : /(Linux|Darwin)/ } - - it "returns the output and status" do - skip('Could not locate pwsh binary') unless pwsh_exist? - output, status = provider.run(command) - - expect(output).to match(command_output_regex) - expect(status.exitstatus).to eq(0) - end - - it "returns true if the `onlyif` check command succeeds" do - skip('Could not locate pwsh binary') unless pwsh_exist? - resource[:onlyif] = command - - expect(resource.parameter(:onlyif).check(command)).to eq(true) - end - - it "returns false if the `unless` check command succeeds" do - skip('Could not locate pwsh binary') unless pwsh_exist? - resource[:unless] = command - - expect(resource.parameter(:unless).check(command)).to eq(false) - end - - it "runs commands properly that output to multiple streams" do - skip('Could not locate pwsh binary') unless pwsh_exist? - command = Puppet.features.microsoft_windows? ? - # Note that `/bin/sh -c` or `cmd.exe /c` is required to return an exit code. - # Without this the exitcode is always zero. - 'echo "foo"; [System.Console]::Error.WriteLine("bar"); cmd.exe /c foo.exe' : - 'echo "foo"; [System.Console]::Error.WriteLine("bar"); /bin/sh -c "foo.exe"' - - if PuppetX::PowerShell::PowerShellManager.supported_on_pwsh? - expected = Puppet.features.microsoft_windows? ? "foo\r\n" : "^foo\n" - else - # when PowerShellManager is not used, the legacy style invocation - # collects all streams inside of a single output string - expected = Puppet.features.microsoft_windows? ? - "foo\nbar\n'foo.exe' is not recognized as an internal or external command,\noperable program or batch file.\n" : - "^foo\nbar\n.+The term \'foo\.exe\' is not recognized as the name of a cmdlet, function.+" - end - output, status = provider.run(command) - expect(output).to match(expected) - expect(status.exitstatus).to_not eq(0) # exit codes for missing files differ across platforms. - end - - describe 'when specifying a working directory' do - describe 'that does not exist' do - let(:work_dir) { - Puppet.features.microsoft_windows? ? - "#{ENV['SYSTEMROOT']}\\some\\directory\\that\\does\\not\\exist" : - '/some/directory/that/does/not/exist' - } - let(:command) { 'exit 0' } - - it 'emits an error when working directory does not exist' do - skip('Could not locate pwsh binary') unless pwsh_exist? - resource[:cwd] = work_dir - expect { provider.run(command) }.to raise_error(/Working directory .+ does not exist/) - end - end - end - end -end diff --git a/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb b/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb deleted file mode 100644 index 867a6914..00000000 --- a/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb +++ /dev/null @@ -1,836 +0,0 @@ -require 'spec_helper' -require 'puppet/type' -require 'puppet_x/puppetlabs/powershell/powershell_manager' - -module PuppetX - module PowerShell - class PowerShellManager; end - if Puppet::Util::Platform.windows? - module WindowsAPI - require 'ffi' - extend FFI::Library - - ffi_convention :stdcall - - # https://msdn.microsoft.com/en-us/library/ks2530z6%28v=VS.100%29.aspx - # intptr_t _get_osfhandle( - # int fd - # ); - ffi_lib [FFI::CURRENT_PROCESS, 'msvcrt'] - attach_function :get_osfhandle, :_get_osfhandle, [:int], :uintptr_t - - # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx - # BOOL WINAPI CloseHandle( - # _In_ HANDLE hObject - # ); - ffi_lib :kernel32 - attach_function :CloseHandle, [:uintptr_t], :int32 - end - end - end -end - -shared_examples_for "a PowerShellManager" do |ps_command, ps_args| - -describe PuppetX::PowerShell::PowerShellManager do - - def create_manager(ps_command, ps_args) - PuppetX::PowerShell::PowerShellManager.instance(ps_command, ps_args, debug: true) - end - - def line_end - Puppet::Util::Platform.windows? ? "\r\n" : "\n" - end - - def is_osx? - # Note this test fails if running in JRuby, but because the unit tests are MRI only, this is ok - !RUBY_PLATFORM.match(/darwin/).nil? - end - - let (:manager) { create_manager(ps_command, ps_args) } - - describe "when managing the powershell process" do - describe "the PowerShellManager::instance method" do - it "should return the same manager instance / process given the same cmd line" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - manager_2 = create_manager(ps_command, ps_args) - second_pid = manager_2.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - expect(manager_2).to eq(manager) - expect(first_pid).to eq(second_pid) - end - - it "should fail if the manger is created with a short timeout" do - expect { - PuppetX::PowerShell::PowerShellManager.new( - ps_command, - ps_args, - debug: false, - pipe_timeout: 0.01 - ) - }.to raise_error do |e| - expect(e).to be_a(RuntimeError) - expected_error = /Failure waiting for PowerShell process (\d+) to start pipe server/ - expect(e.message).to match expected_error - pid = expected_error.match(e.message)[1].to_i - - # We want to make sure that enough time has elapsed since the manager called kill - # for the OS to finish killing the process and doing all of it's cleanup. - # We have found that without an appropriate wait period, the kill call below - # can return unexpected results and fail the test. - sleep(1) - expect{Process.kill(0, pid)}.to raise_error(Errno::ESRCH) - end - end - - def bad_file_descriptor_regex - # Ruby can do something like: - # - # - @bad_file_descriptor_regex ||= ( - ebadf = Errno::EBADF.new() - '^' + Regexp.escape("\#<#{ebadf.class}: #{ebadf.message}") - ) - end - - def pipe_error_regex - @pipe_error_regex ||= ( - epipe = Errno::EPIPE.new() - '^' + Regexp.escape("\#<#{epipe.class}: #{epipe.message}") - ) - end - - # reason should be a string for an exact match - # else an array of regex matches - def expect_dead_manager(manager, reason, style = :exact) - # additional attempts to use the manager will fail for the given reason - result = manager.execute('Write-Host "hi"') - expect(result[:exitcode]).to eq(-1) - - if reason.is_a?(String) - expect(result[:stderr][0]).to eq(reason) if style == :exact - expect(result[:stderr][0]).to match(reason) if style == :regex - elsif reason.is_a?(Array) - expect(reason).to include(result[:stderr][0]) if style == :exact - if style == :regex - expect(result[:stderr][0]).to satisfy("should match expected error(s): #{reason}") do |msg| - reason.any? { |m| msg.match m } - end - end - end - - # and the manager no longer considers itself alive - expect(manager.alive?).to eq(false) - end - - def expect_different_manager_returned_than(manager, pid) - # acquire another manager instance using the same command and arguments - new_manager = create_manager(manager.powershell_command, manager.powershell_arguments) - - # which should be different than the one passed in - expect(new_manager).to_not eq(manager) - - # with a different PID - second_pid = new_manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - expect(pid).to_not eq(second_pid) - end - - def close_stream(stream, style = :inprocess) - if style == :inprocess - stream.close - else style == :viahandle - handle = PuppetX::PowerShell::WindowsAPI.get_osfhandle(stream.fileno) - PuppetX::PowerShell::WindowsAPI.CloseHandle(handle) - end - end - - it "should create a new PowerShell manager host if user code exits the first process" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - exitcode = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Kill()')[:exitcode] - - # when a process gets torn down out from under manager before reading stdout - # it catches the error and returns a -1 exitcode - expect(exitcode).to eq(-1) - - expect_dead_manager(manager, pipe_error_regex, :regex) - - expect_different_manager_returned_than(manager, first_pid) - end - - it "should create a new PowerShell manager host if the underlying PowerShell process is killed" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - # kill the PID from Ruby - # Note - On Windows, creating the powershell manager starts one process, whereas on unix it starts two (one via sh and one via pwsh). Not sure why - # So instead kill the parent process instead of the child - Process.kill('KILL', first_pid.to_i) - - # Windows uses named pipes, unix uses sockets - Puppet::Util::Platform.windows? ? - expect_dead_manager(manager, pipe_error_regex, :regex) : - # WSL raises an EOFError - # Ubuntu 16.04 raises an ECONNRESET:Connection reset by peer - expect_dead_manager(manager, - [EOFError.new('end of file reached').inspect, Errno::ECONNRESET.new.inspect], - :exact - ) - - expect_different_manager_returned_than(manager, first_pid) - end - - context "on Windows" do - # On Windows we're using named pipes so these tests only apply on Windows. - before :each do - skip('Not on Windows platform') unless Puppet::Util::Platform.windows? - end - - it "should create a new PowerShell manager host if the input stream is closed" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - # closing pipe from the Ruby side tears down the process - close_stream(manager.instance_variable_get(:@pipe), :inprocess) - - expect_dead_manager(manager, IOError.new('closed stream').inspect, :exact) - - expect_different_manager_returned_than(manager, first_pid) - end - - it "should create a new PowerShell manager host if the input stream handle is closed" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - # call CloseHandle against pipe, therby tearing down the PowerShell process - close_stream(manager.instance_variable_get(:@pipe), :viahandle) - - expect_dead_manager(manager, bad_file_descriptor_regex, :regex) - - expect_different_manager_returned_than(manager, first_pid) - end - - it "should create a new PowerShell manager host if the output stream is closed" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - # closing stdout from the Ruby side allows process to run - close_stream(manager.instance_variable_get(:@stdout), :inprocess) - - # fails with vanilla EPIPE or closed stream IOError depening on timing / Ruby version - msgs = [ Errno::EPIPE.new().inspect, IOError.new('closed stream').inspect ] - expect_dead_manager(manager, msgs, :exact) - - expect_different_manager_returned_than(manager, first_pid) - end - - it "should create a new PowerShell manager host if the output stream handle is closed" do - # currently skipped as it can trigger an internal Ruby thread clean-up race - # its unknown why this test fails, but not the identical test against @stderr - skip('This test can cause intermittent segfaults in Ruby with w32_reset_event invalid handle') - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - # call CloseHandle against stdout, which leaves PowerShell process running - close_stream(manager.instance_variable_get(:@stdout), :viahandle) - - # fails with vanilla EPIPE or various EBADF depening on timing / Ruby version - msgs = [ - '^' + Regexp.escape(Errno::EPIPE.new().inspect), - bad_file_descriptor_regex - ] - expect_dead_manager(manager, msgs, :regex) - - expect_different_manager_returned_than(manager, first_pid) - end - - it "should create a new PowerShell manager host if the error stream is closed" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - # closing stderr from the Ruby side allows process to run - close_stream(manager.instance_variable_get(:@stderr), :inprocess) - - # fails with vanilla EPIPE or closed stream IOError depening on timing / Ruby version - msgs = [ Errno::EPIPE.new().inspect, IOError.new('closed stream').inspect ] - expect_dead_manager(manager, msgs, :exact) - - expect_different_manager_returned_than(manager, first_pid) - end - - it "should create a new PowerShell manager host if the error stream handle is closed" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - # call CloseHandle against stderr, which leaves PowerShell process running - close_stream(manager.instance_variable_get(:@stderr), :viahandle) - - # fails with vanilla EPIPE or various EBADF depening on timing / Ruby version - msgs = [ - '^' + Regexp.escape(Errno::EPIPE.new().inspect), - bad_file_descriptor_regex - ] - expect_dead_manager(manager, msgs, :regex) - - expect_different_manager_returned_than(manager, first_pid) - end - end - end - end - - let(:powershell_runtime_error) { '$ErrorActionPreference = "Stop";$test = 1/0' } - let(:powershell_parseexception_error) { '$ErrorActionPreference = "Stop";if (1 -badoperator 2) { Exit 1 }' } - let(:powershell_incompleteparseexception_error) { '$ErrorActionPreference = "Stop";if (1 -eq 2) { ' } - - describe "when provided powershell commands" do - it "shows ps version" do - result = manager.execute('$psversiontable') - puts result[:stdout] - end - - it "should return simple output" do - result = manager.execute('write-output foo') - - expect(result[:stdout]).to eq("foo#{line_end}") - expect(result[:exitcode]).to eq(0) - end - - it "should return the exitcode specified" do - result = manager.execute('write-output foo; exit 55') - - expect(result[:stdout]).to eq("foo#{line_end}") - expect(result[:exitcode]).to eq(55) - end - - it "should return the exitcode 1 when exception is thrown" do - result = manager.execute('throw "foo"') - - expect(result[:stdout]).to eq(nil) - expect(result[:exitcode]).to eq(1) - end - - it "should return the exitcode of the last command to set an exit code" do - result =Puppet::Util::Platform.windows? ? - manager.execute("$LASTEXITCODE = 0; write-output 'foo'; cmd.exe /c 'exit 99'; write-output 'bar'") : - manager.execute("$LASTEXITCODE = 0; write-output 'foo'; /bin/sh -c 'exit 99'; write-output 'bar'") - - expect(result[:stdout]).to eq("foo#{line_end}bar#{line_end}") - expect(result[:exitcode]).to eq(99) - end - - it "should return the exitcode of a script invoked with the call operator &" do - fixture_path = File.expand_path(File.dirname(__FILE__) + '../../../../exit-27.ps1') - result = manager.execute("& #{fixture_path}") - - expect(result[:stdout]).to eq(nil) - expect(result[:exitcode]).to eq(27) - end - - it "should collect anything written to stderr" do - result = manager.execute('[System.Console]::Error.WriteLine("foo")') - - expect(result[:stderr]).to eq(["foo#{line_end}"]) - expect(result[:exitcode]).to eq(0) - end - - it "should collect multiline output written to stderr" do - # induce a failure in cmd.exe that emits a multi-iline error message - result = Puppet::Util::Platform.windows? ? - manager.execute('cmd.exe /c foo.exe') : - manager.execute('/bin/sh -c "echo bar 1>&2 && foo.exe"') - - expect(result[:stdout]).to eq(nil) - if Puppet::Util::Platform.windows? - expect(result[:stderr]).to eq(["'foo.exe' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n"]) - elsif is_osx? - expect(result[:stderr][0]).to match(/foo\.exe: command not found/) - expect(result[:stderr][0]).to match(/bar/) - else - expect(result[:stderr][0]).to match(/foo\.exe: not found/) - expect(result[:stderr][0]).to match(/bar/) - end - expect(result[:exitcode]).to_not eq(0) - end - - it "should handle writting to stdout (cmdlet) and stderr" do - result = manager.execute('Write-Host "powershell";[System.Console]::Error.WriteLine("foo")') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:native_stdout]).to eq(nil) - expect(result[:stderr]).to eq(["foo#{line_end}"]) - expect(result[:exitcode]).to eq(0) - end - - it "should handle writting to stdout (shell out to another program) and stderr" do - result = Puppet::Util::Platform.windows? ? - manager.execute('cmd.exe /c echo powershell;[System.Console]::Error.WriteLine("foo")') : - manager.execute('/bin/sh -c "echo powershell";[System.Console]::Error.WriteLine("foo")') - - expect(result[:stdout]).to eq(nil) - expect(result[:native_stdout]).not_to eq(nil) - expect(result[:stderr]).to eq(["foo#{line_end}"]) - expect(result[:exitcode]).to eq(0) - end - - it "should handle writing to stdout natively" do - result = manager.execute('[System.Console]::Out.WriteLine("foo")') - - expect(result[:stdout]).to eq("foo#{line_end}") - expect(result[:native_stdout]).to eq(nil) - expect(result[:stderr]).to eq([]) - expect(result[:exitcode]).to eq(0) - end - - it "should properly interleave output written natively to stdout and via Write-XXX cmdlets" do - result = manager.execute('Write-Output "bar"; [System.Console]::Out.WriteLine("foo"); Write-Warning "baz";') - - expect(result[:stdout]).to eq("bar#{line_end}foo#{line_end}WARNING: baz#{line_end}") - expect(result[:stderr]).to eq([]) - expect(result[:exitcode]).to eq(0) - end - - it "should handle writing to regularly captured output AND stdout natively" do - result = manager.execute('Write-Host "powershell";[System.Console]::Out.WriteLine("foo")') - - expect(result[:stdout]).not_to eq("foo#{line_end}") - expect(result[:native_stdout]).to eq(nil) - expect(result[:stderr]).to eq([]) - expect(result[:exitcode]).to eq(0) - end - - it "should handle writing to regularly captured output, stderr AND stdout natively" do - result = manager.execute('Write-Host "powershell";[System.Console]::Out.WriteLine("foo");[System.Console]::Error.WriteLine("bar")') - - expect(result[:stdout]).not_to eq("foo#{line_end}") - expect(result[:native_stdout]).to eq(nil) - expect(result[:stderr]).to eq(["bar#{line_end}"]) - expect(result[:exitcode]).to eq(0) - end - - context "it should handle UTF-8" do - # different UTF-8 widths - # 1-byte A - # 2-byte ۿ - http://www.fileformat.info/info/unicode/char/06ff/index.htm - 0xDB 0xBF / 219 191 - # 3-byte ᚠ - http://www.fileformat.info/info/unicode/char/16A0/index.htm - 0xE1 0x9A 0xA0 / 225 154 160 - # 4-byte 𠜎 - http://www.fileformat.info/info/unicode/char/2070E/index.htm - 0xF0 0xA0 0x9C 0x8E / 240 160 156 142 - let (:mixed_utf8) { "A\u06FF\u16A0\u{2070E}" } # Aۿᚠ𠜎 - - it "when writing basic text" do - code = "Write-Output '#{mixed_utf8}'" - result = manager.execute(code) - - expect(result[:stdout]).to eq("#{mixed_utf8}#{line_end}") - expect(result[:exitcode]).to eq(0) - end - - it "when writing basic text to stderr" do - code = "[System.Console]::Error.WriteLine('#{mixed_utf8}')" - result = manager.execute(code) - - expect(result[:stderr]).to eq(["#{mixed_utf8}#{line_end}"]) - expect(result[:exitcode]).to eq(0) - end - end - - it "should execute cmdlets" do - result = manager.execute('Get-Verb') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should execute cmdlets with pipes" do - result = manager.execute('Get-Process | ? { $_.PID -ne $PID }') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should execute multi-line" do - result = manager.execute(<<-CODE -$foo = ls -$count = $foo.count -$count - CODE - ) - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should execute code with a try/catch, receiving the output of Write-Error" do - result = manager.execute(<<-CODE -try{ - $foo = ls - $count = $foo.count - $count -}catch{ - Write-Error "foo" -} - CODE - ) - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should be able to execute the code in a try block when using try/catch" do - result = manager.execute(<<-CODE - try { - $foo = @(1, 2, 3).count - exit 400 - } catch { - exit 1 - } - CODE - ) - - expect(result[:stdout]).to eq(nil) - # using an explicit exit code ensures we've really executed correct block - expect(result[:exitcode]).to eq(400) - end - - it "should be able to execute the code in a catch block when using try/catch" do - result = manager.execute(<<-CODE -try { - throw "Error!" - exit 0 -} catch { - exit 500 -} - CODE - ) - - expect(result[:stdout]).to eq(nil) - # using an explicit exit code ensures we've really executed correct block - expect(result[:exitcode]).to eq(500) - end - - - it "should reuse the same PowerShell process for multiple calls" do - first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - second_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] - - expect(first_pid).to eq(second_pid) - end - - it "should remove psvariables between runs" do - manager.execute('$foo = "bar"') - result = manager.execute('$foo') - - expect(result[:stdout]).to eq(nil) - end - - it "should remove env variables between runs" do - manager.execute('[Environment]::SetEnvironmentVariable("foo", "bar", "process")') - result = manager.execute('Test-Path env:\foo') - - expect(result[:stdout]).to eq("False#{line_end}") - end - - it "should set custom environment variables" do - result = manager.execute('Write-Output $ENV:foo',nil,nil,['foo=bar']) - - expect(result[:stdout]).to eq("bar#{line_end}") - end - - it "should remove custom environment variables between runs" do - manager.execute('Write-Output $ENV:foo',nil,nil,['foo=bar']) - result = manager.execute('Write-Output $ENV:foo',nil,nil,[]) - - expect(result[:stdout]).to be nil - end - - it "should ignore malformed custom environment variable" do - result = manager.execute('Write-Output $ENV:foo',nil,nil,['=foo','foo','foo=']) - - expect(result[:stdout]).to be nil - end - - it "should use last definition for duplicate custom environment variable" do - result = manager.execute('Write-Output $ENV:foo',nil,nil,['foo=one','foo=two','foo=three']) - - expect(result[:stdout]).to eq("three#{line_end}") - end - - def current_powershell_major_version(ps_command, ps_args) - # As this is only used to detect old PS versions we can - # short circuit detecting the version for PowerShell Core - return 6 if ps_command.end_with?('pwsh') || ps_command.end_with?('pwsh.exe') - begin - version = `#{ps_command} #{ps_args.join(' ')} -Command \"$PSVersionTable.PSVersion.Major.ToString()\"`.chomp!.to_i - rescue - puts "Unable to determine PowerShell version" - version = -1 - end - - version - end - - def output_cmdlet(ps_command, ps_args) - # Write-Output is the default behavior, except on older PS2 where the - # behavior of Write-Output introduces newlines after every width number - # of characters as specified in the BufferSize of the custom console UI - # Write-Host should usually be avoided, but works for this test in old PS2 - current_powershell_major_version(ps_command, ps_args) >= 3 ? - 'Write-Output' : - 'Write-Host' - end - - it "should be able to write more than the 64k default buffer size to the managers pipe without deadlocking the Ruby parent process or breaking the pipe" do - # this was tested successfully up to 5MB of text - # we add some additional bytes so it's not always on a 1KB boundary and forces pipe reading in different lengths, not always 1K chunks - buffer_string_96k = 'a' * ((1024 * 96) + 11) - result = manager.execute(<<-CODE -'#{buffer_string_96k}' | #{output_cmdlet(ps_command, ps_args)} - CODE - ) - - expect(result[:errormessage]).to eq(nil) - expect(result[:exitcode]).to eq(0) - expect(result[:stdout].length).to eq("#{buffer_string_96k}#{line_end}".length) - expect(result[:stdout]).to eq("#{buffer_string_96k}#{line_end}") - end - - it "should be able to write more than the 64k default buffer size to child process stdout without deadlocking the Ruby parent process" do - # we add some additional bytes so it's not always on a 1KB boundary and forces pipe reading in different lengths, not always 1K chunks - result = manager.execute(<<-CODE -$bytes_in_k = (1024 * 64) + 11 -[Text.Encoding]::UTF8.GetString((New-Object Byte[] ($bytes_in_k))) | #{output_cmdlet(ps_command, ps_args)} - CODE - ) - - expect(result[:errormessage]).to eq(nil) - expect(result[:exitcode]).to eq(0) - expected = "\x0" * (1024 * 64 + 11) + line_end - expect(result[:stdout].length).to eq(expected.length) - expect(result[:stdout]).to eq(expected) - end - - it "should return a response with a timeout error if the execution timeout is exceeded" do - timeout_ms = 100 - result = manager.execute('sleep 1', timeout_ms) - msg = /Catastrophic failure\: PowerShell module timeout \(#{timeout_ms} ms\) exceeded while executing/ - expect(result[:errormessage]).to match(msg) - end - - it "should return any available stdout / stderr prior to being terminated if a timeout error occurs" do - timeout_ms = 1500 - command = '$debugPreference = "Continue"; Write-Output "200 OK Glenn"; Write-Debug "304 Not Modified James"; Write-Error "404 Craig Not Found"; sleep 10' - result = manager.execute(command, timeout_ms) - expect(result[:exitcode]).to eq(1) - # starts with Write-Output and Write-Debug messages - expect(result[:stdout]).to match(/200 OK Glenn/) - expect(result[:stdout]).to match(/DEBUG: 304 Not Modified James/) - # then command may have \r\n injected, so remove those for comparison - expect(result[:stdout].gsub(/\r\n/, '')).to include(command) - # and it should end with the Write-Error content - expect(result[:stdout]).to match(/404 Craig Not Found/) - end - - it "should use a default timeout of 300 seconds if the user specified a timeout of 0" do - timeout_ms = 0 - command = 'return $true' - code = manager.make_ps_code(command, timeout_ms) - expect(code).to match(/TimeoutMilliseconds = 300000/) - end - - it "Should use the correct correct timeout if a small value is specified" do - - # Zero timeout is not supported, and a timeout less than 50ms is not supported. - # This test is to ensure that the code that inserts the default timeout when - # the user specified zero, does not interfere with the other default of 50ms - # if the user specifies a value less than that. - - timeout_ms = 20 - command = 'return $true' - code = manager.make_ps_code(command, timeout_ms) - expect(code).to match(/TimeoutMilliseconds = 50/) - end - - it "should not deadlock and return a valid response given invalid unparseable PowerShell code" do - result = manager.execute(<<-CODE - { - - CODE - ) - - expect(result[:errormessage]).not_to be_empty - end - - it "should error if working directory does not exist" do - work_dir = 'C:/some/directory/that/does/not/exist' - - result = manager.execute('(Get-Location).Path',nil,work_dir) - - expect(result[:exitcode]).to_not eq(0) - expect(result[:errormessage]).to match(/Working directory .+ does not exist/) - end - - it "should allow forward slashes in working directory" do - skip('Not on Windows platform') unless Puppet::Util::Platform.windows? - # Backslashes only apply on Windows filesystems - work_dir = ENV["WINDIR"] - forward_work_dir = work_dir.gsub('\\','/') - - result = manager.execute('(Get-Location).Path',nil,forward_work_dir)[:stdout] - - expect(result).to eq("#{work_dir}#{line_end}") - end - - it "should use a specific working directory if set" do - work_dir = Puppet::Util::Platform.windows? ? ENV["WINDIR"] : ENV["HOME"] - - result = manager.execute('(Get-Location).Path',nil,work_dir)[:stdout] - - expect(result).to eq("#{work_dir}#{line_end}") - end - - it "should not reuse the same working directory between runs" do - work_dir = Puppet::Util::Platform.windows? ? ENV["WINDIR"] : ENV["HOME"] - current_work_dir = Dir.getwd - - first_cwd = manager.execute('(Get-Location).Path',nil,work_dir)[:stdout] - second_cwd = manager.execute('(Get-Location).Path')[:stdout] - - # Paths should be case insensitive - expect(first_cwd.downcase).to eq("#{work_dir}#{line_end}".downcase) - expect(second_cwd.downcase).to eq("#{current_work_dir}#{line_end}".downcase) - end - - context "with runtime error" do - it "should not refer to 'EndInvoke' or 'throw' for a runtime error" do - result = manager.execute(powershell_runtime_error) - - expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/EndInvoke/) - expect(result[:errormessage]).not_to match(/throw/) - end - - it "should display line and char information for a runtime error" do - result = manager.execute(powershell_runtime_error) - - expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).to match(/At line\:\d+ char\:\d+/) - end - end - - context "with ParseException error" do - it "should not refer to 'EndInvoke' or 'throw' for a ParseException error" do - result = manager.execute(powershell_parseexception_error) - - expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/EndInvoke/) - expect(result[:errormessage]).not_to match(/throw/) - end - - it "should display line and char information for a ParseException error" do - result = manager.execute(powershell_parseexception_error) - - expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).to match(/At line\:\d+ char\:\d+/) - end - end - - context "with IncompleteParseException error" do - it "should not refer to 'EndInvoke' or 'throw' for an IncompleteParseException error" do - result = manager.execute(powershell_incompleteparseexception_error) - - expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/EndInvoke/) - expect(result[:errormessage]).not_to match(/throw/) - end - - it "should not display line and char information for an IncompleteParseException error" do - result = manager.execute(powershell_incompleteparseexception_error) - - expect(result[:exitcode]).to eq(1) - expect(result[:errormessage]).not_to match(/At line\:\d+ char\:\d+/) - end - end - end - - describe "when output is written to a PowerShell Stream" do - it "should collect anything written to verbose stream" do - msg = SecureRandom.uuid.to_s.gsub('-', '') - result = manager.execute("$VerbosePreference = 'Continue';Write-Verbose '#{msg}'") - - expect(result[:stdout]).to match(/^VERBOSE\: #{msg}/) - expect(result[:exitcode]).to eq(0) - end - - it "should collect anything written to debug stream" do - msg = SecureRandom.uuid.to_s.gsub('-', '') - result = manager.execute("$debugPreference = 'Continue';Write-debug '#{msg}'") - - expect(result[:stdout]).to match(/^DEBUG: #{msg}/) - expect(result[:exitcode]).to eq(0) - end - - it "should collect anything written to Warning stream" do - msg = SecureRandom.uuid.to_s.gsub('-', '') - result = manager.execute("Write-Warning '#{msg}'") - - expect(result[:stdout]).to match(/^WARNING: #{msg}/) - expect(result[:exitcode]).to eq(0) - end - - it "should collect anything written to Error stream" do - msg = SecureRandom.uuid.to_s.gsub('-', '') - result = manager.execute("Write-Error '#{msg}'") - - expect(result[:stdout]).to match(/Write-Error '#{msg}' : #{msg}/) - expect(result[:exitcode]).to eq(0) - end - - it "should handle a Write-Error in the middle of code" do - result = manager.execute('Write-Host "one" ;Write-Error "Hello"; Write-Host "two"') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should handle a Out-Default in the user code" do - result = manager.execute('\'foo\' | Out-Default') - - expect(result[:stdout]).to eq("foo#{line_end}") - expect(result[:exitcode]).to eq(0) - end - - it "should handle lots of output from user code" do - result = manager.execute('1..1000 | %{ (65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_} }') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should handle a larger return of output from user code" do - result = manager.execute('1..1000 | %{ (65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_} } | %{ $f="" } { $f+=$_ } {$f }') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - - it "should handle shell redirection" do - # the test here is to ensure that this doesn't break. because we merge the streams regardless - # the opposite of this test shows the same thing - result = manager.execute('function test-error{ ps;write-error \'foo\' }; test-error 2>&1') - - expect(result[:stdout]).not_to eq(nil) - expect(result[:exitcode]).to eq(0) - end - end -end -end - -describe "On Windows PowerShell" do - before :each do - skip unless Puppet::Util::Platform.windows? && PuppetX::PowerShell::PowerShellManager.supported? - end - - it_should_behave_like "a PowerShellManager", - Puppet::Type.type(:exec).provider(:powershell).command(:powershell), - Puppet::Type.type(:exec).provider(:powershell).powershell_args -end - -describe "On PowerShell Core" do - before :each do - skip unless PuppetX::PowerShell::PowerShellManager.supported_on_pwsh? && !Puppet::Type.type(:exec).provider(:pwsh).new().get_pwsh_command.nil? - end - - it_should_behave_like "a PowerShellManager", - Puppet::Type.type(:exec).provider(:pwsh).new().get_pwsh_command, - Puppet::Type.type(:exec).provider(:pwsh).new().pwsh_args -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 93b25ecb..eab931d6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,11 @@ end end +# read default_facts and merge them over what is provided by facterdb +default_facts.each do |fact, value| + add_custom_fact fact, value +end + RSpec.configure do |c| c.default_facts = default_facts c.before :each do @@ -34,9 +39,12 @@ end c.filter_run_excluding(bolt: true) unless ENV['GEM_BOLT'] c.after(:suite) do + RSpec::Puppet::Coverage.report!(0) end end +# Ensures that a module is defined +# @param module_name Name of the module def ensure_module_defined(module_name) module_name.split('::').reduce(Object) do |last_module, next_module| last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module, false) diff --git a/spec/unit/provider/exec/powershell_spec.rb b/spec/unit/provider/exec/powershell_spec.rb index 0e9cfffa..41d67622 100644 --- a/spec/unit/provider/exec/powershell_spec.rb +++ b/spec/unit/provider/exec/powershell_spec.rb @@ -1,7 +1,6 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util' -require 'puppet_x/puppetlabs/powershell/powershell_manager' require 'fileutils' describe Puppet::Type.type(:exec).provider(:powershell) do @@ -39,7 +38,7 @@ describe "#run" do context "stubbed calls" do before :each do - PuppetX::PowerShell::PowerShellManager.stubs(:supported?).returns(false) + Pwsh::Manager.stubs(:windows_powershell_supported?).returns(false) Puppet::Provider::Exec.any_instance.stubs(:run) end @@ -108,7 +107,7 @@ command = 'echo "foo"; [System.Console]::Error.WriteLine("bar"); cmd.exe /c foo.exe' output, status = provider.run(command) - if PuppetX::PowerShell::PowerShellManager.supported? + if Pwsh::Manager.windows_powershell_supported? expected = "foo\r\n" else # when PowerShellManager is not used, the v1 style module collected @@ -210,9 +209,9 @@ def apply_compiled_manifest(manifest) it 'does not emit a warning message when PowerShellManager is usable in a Windows environment' do - PuppetX::PowerShell::PowerShellManager.stubs(:win32console_enabled?).returns(false) + Pwsh::Manager.stubs(:win32console_enabled?).returns(false) - expect(PuppetX::PowerShell::PowerShellManager.supported?).to eq(true) + expect(Pwsh::Manager.windows_powershell_supported?).to eq(true) # given PowerShellManager is supported, never emit an upgrade message Puppet::Type::Exec::ProviderPowershell.expects(:upgrade_message).never @@ -223,9 +222,9 @@ def apply_compiled_manifest(manifest) it 'emits a warning message when PowerShellManager cannot be used in a Windows environment' do # pretend we're Ruby 1.9.3 / Puppet 3.x x86 - PuppetX::PowerShell::PowerShellManager.stubs(:win32console_enabled?).returns(true) + Pwsh::Manager.stubs(:win32console_enabled?).returns(true) - expect(PuppetX::PowerShell::PowerShellManager.supported?).to eq(false) + expect(Pwsh::Manager.windows_powershell_supported?).to eq(false) # given PowerShellManager is NOT supported, emit an upgrade message Puppet::Type::Exec::ProviderPowershell.expects(:upgrade_message).once diff --git a/spec/unit/provider/exec/pwsh_spec.rb b/spec/unit/provider/exec/pwsh_spec.rb index f09d0d35..4a30720f 100644 --- a/spec/unit/provider/exec/pwsh_spec.rb +++ b/spec/unit/provider/exec/pwsh_spec.rb @@ -1,13 +1,6 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util' -require 'fileutils' - -class MockPowerShellManager - def execute_resource(*_args) - return "", "" - end -end describe Puppet::Type.type(:exec).provider(:pwsh) do # Override the run value so we can test the super call @@ -26,18 +19,18 @@ def execute_resource(*_args) before :each do # Always assume the pwsh binary is available - provider.stubs(:get_pwsh_command).returns('somepath/pwsh') + Pwsh::Manager.stubs(:pwsh_path).returns('somepath/pwsh') end describe "#run" do before :each do Puppet::Provider::Exec.any_instance.stubs(:run) - PuppetX::PowerShell::PowerShellManager.stubs(:instance).returns(MockPowerShellManager.new) + provider.stubs(:execute_resource).returns('', '') end context 'when the powershell manager is not supported' do before :each do - PuppetX::PowerShell::PowerShellManager.stubs(:supported_on_pwsh?).returns(false) + Pwsh::Manager.stubs(:pwsh_supported?).returns(false) end let(:shell_command) { Puppet.features.microsoft_windows? ? 'cmd.exe /c' : '/bin/sh -c' } @@ -76,7 +69,7 @@ def execute_resource(*_args) context 'when specifying a path' do let(:path) { Puppet::Util::Platform.windows? ? 'C:/pwsh-test' : '/pwsh-test' } - let(:pwsh_path) { path + '/pwsh' } + let(:pwsh_path) { Puppet::Util::Platform.windows? ? path + '/pwsh.exe' : path + '/pwsh' } let(:native_pwsh_path) { Puppet::Util::Platform.windows? ? pwsh_path.gsub(File::SEPARATOR, File::ALT_SEPARATOR) : pwsh_path } let(:native_pwsh_path_regex) { /#{Regexp.escape(native_pwsh_path)}/ } @@ -84,12 +77,11 @@ def execute_resource(*_args) it 'should prefer pwsh in the specified path' do # Pretend that only the test pwsh binary exists. - FileTest.stubs(:file?).with() { |value| value == pwsh_path}.returns(true) - FileTest.stubs(:file?).with() { |value| value != pwsh_path}.returns(false) - FileTest.stubs(:executable?).with() { |value| value == pwsh_path}.returns(true) - FileTest.stubs(:executable?).with() { |value| value != pwsh_path}.returns(false) + File.stubs(:exist?).with() { |value| value == pwsh_path}.returns(true) + File.stubs(:exist?).with() { |value| value != pwsh_path}.returns(false) + # Remove the global stub here as we're testing this method - provider.unstub(:get_pwsh_command) + Pwsh::Manager.unstub(:pwsh_path) Puppet::Type::Exec::ProviderPwsh.any_instance.expects(:run). with(regexp_matches(native_pwsh_path_regex), false) @@ -102,8 +94,8 @@ def execute_resource(*_args) it "should only attempt to find pwsh once when pwsh exists" do # Need to unstub to force the 'only once' expectation. Otherwise the # previous stub takes over if it's called more than once. - provider.unstub(:get_pwsh_command) - provider.expects(:get_pwsh_command).once.returns('somepath/pwsh') + Pwsh::Manager.unstub(:pwsh_path) + Pwsh::Manager.expects(:pwsh_path).once.returns('somepath/pwsh') provider.run_spec_override(command) provider.run_spec_override(command) diff --git a/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb b/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb deleted file mode 100644 index 9c1a7fb0..00000000 --- a/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' -require 'puppet/type' -require 'puppet_x/puppetlabs/powershell/powershell_version' -require 'puppet_x/puppetlabs/powershell/compatible_powershell_version' - -describe PuppetX::PuppetLabs::PowerShell::CompatiblePowerShellVersion do - before(:each) do - skip('Not on Windows platform') unless Puppet.features.microsoft_windows? - @compat = PuppetX::PuppetLabs::PowerShell::CompatiblePowerShellVersion - end - - describe "when a newer version of PowerShell is installed" do - it "should return true when PowerShell v3 is installed" do - PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('3.0') - - expect(@compat.compatible_version?).to eq(true) - end - - it "should return true when PowerShell v5.0 is installed" do - PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('5.0.201001.1') - - expect(@compat.compatible_version?).to eq(true) - end - end - - describe "when PowerShell v2 is installed" do - before(:each) do - PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('2.0') - end - - it "should return true when .NET 3.5 is installed" do - reg_key = mock('bob') - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100).yields(reg_key) - - expect(@compat.compatible_version?).to eq(true) - end - - it "should return false when .NET 3.5 is not installed" do - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once - - expect(@compat.compatible_version?).to eq(false) - end - end - - describe "when PowerShell is not installed or not compatible" do - it "should return false when PowerShell is not installed" do - PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns(nil) - - expect(@compat.compatible_version?).to eq(false) - end - - it "should return false when PowerShell is v1" do - PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('1.0') - - expect(@compat.compatible_version?).to eq(false) - end - end -end diff --git a/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb b/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb deleted file mode 100644 index a270b364..00000000 --- a/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' -require 'puppet/type' -require 'puppet_x/puppetlabs/powershell/powershell_version' - -describe PuppetX::PuppetLabs::PowerShell::PowerShellVersion do - before(:each) do - skip('Not on Windows platform') unless Puppet.features.microsoft_windows? - @ps = PuppetX::PuppetLabs::PowerShell::PowerShellVersion - end - - describe "when powershell is installed" do - - describe "when powershell version is greater than three" do - - it "should detect a powershell version" do - Win32::Registry.any_instance.expects(:[]).with('PowerShellVersion').returns('5.0.10514.6') - - version = @ps.version - - expect(version).to eq '5.0.10514.6' - end - - it "should call the powershell three registry path" do - reg_key = mock('bob') - reg_key.expects(:[]).with('PowerShellVersion').returns('5.0.10514.6') - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).yields(reg_key).once - - @ps.version - end - - it "should not call powershell one registry path" do - reg_key = mock('bob') - reg_key.expects(:[]).with('PowerShellVersion').returns('5.0.10514.6') - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).yields(reg_key) - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).times(0) - - @ps.version - end - end - - describe "when powershell version is less than three" do - - it "should detect a powershell version" do - Win32::Registry.any_instance.expects(:[]).with('PowerShellVersion').returns('2.0') - - version = @ps.version - - expect(version).to eq '2.0' - end - - it "should call powershell one registry path" do - reg_key = mock('bob') - reg_key.expects(:[]).with('PowerShellVersion').returns('2.0') - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).yields(reg_key).once - - version = @ps.version - - expect(version).to eq '2.0' - end - end - end - - describe "when powershell is not installed" do - - it "should return nil and not throw" do - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once - Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once - - version = @ps.version - - expect(version).to eq nil - end - end -end