diff --git a/Gemfile b/Gemfile index f5d5171..4f53d3b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,6 @@ # encoding: UTF-8 source 'https://rubygems.org' gemspec + +gem 'winrm', github: 'winrb/winrm', branch: 'winrm-v2' +gem 'rb-readline' diff --git a/Rakefile b/Rakefile index 9f89bac..84a84e2 100644 --- a/Rakefile +++ b/Rakefile @@ -17,7 +17,7 @@ end # Run the integration test suite RSpec::Core::RakeTask.new(:integration) do |task| - task.pattern = 'spec/integration/*_spec.rb' + task.pattern = 'spec/integration/benchmark_spec.rb' task.rspec_opts = ['--color', '-f documentation'] end diff --git a/lib/winrm-fs/core/file_transporter.rb b/lib/winrm-fs/core/file_transporter.rb index c74b680..b3571da 100644 --- a/lib/winrm-fs/core/file_transporter.rb +++ b/lib/winrm-fs/core/file_transporter.rb @@ -41,8 +41,8 @@ class FileTransporterFailed < ::WinRM::WinRMError; end # sessions being invoked which can be 2 orders of magnitude more # expensive than vanilla CMD commands. # - # This object is supported by either a `CommandExecutor` instance as it - # depends on the `#run_cmd` and `#run_powershell_script` API contracts. + # This object is supported by a `PowerShell` instance as it + # depends on the `#run` API contract. # # An optional logger can be supplied, assuming it can respond to the # `#debug` and `#debug?` messages. @@ -50,12 +50,12 @@ class FileTransporterFailed < ::WinRM::WinRMError; end # @author Fletcher Nichol # @author Matt Wrock class FileTransporter - # Creates a FileTransporter given a CommandExecutor object. + # Creates a FileTransporter given a PowerShell object. # - # @param executor [CommandExecutor] a winrm CommandExecutor object - def initialize(executor, opts = {}) - @executor = executor - @logger = executor.service.logger + # @param shell [PowerShell] a winrm PowerShell object + def initialize(shell, opts = {}) + @shell = shell + @logger = shell.logger @id_generator = opts.fetch(:id_generator) { -> { SecureRandom.uuid } } end @@ -112,7 +112,7 @@ def upload(locals, remote) # a Windows CMD prompt without exceeded the maximum command line # length # @api private - MAX_ENCODED_WRITE = 8000 + MAX_ENCODED_WRITE = 376000 # @return [String] the Array pack template for Base64 encoding a stream # of data @@ -128,9 +128,9 @@ def upload(locals, remote) # @api private attr_reader :logger - # @return [Winrm::CommandExecutor] a WinRM CommandExecutor + # @return [Winrm::Commandshell] a WinRM Commandshell # @api private - attr_reader :executor + attr_reader :shell # Examines the files and corrects the file destination if it is # targeting an existing folder. In this case, the destination path @@ -200,7 +200,7 @@ def check_files(files) logger.debug 'Running check_files.ps1' hash_file = create_remote_hash_file(check_files_ps_hash(files)) script = WinRM::FS::Scripts.render('check_files', hash_file: hash_file) - parse_response(executor.run_powershell_script(script)) + parse_response(shell.run(script)) end # Constructs a collection of destination path/MD5 checksum pairs as a @@ -270,7 +270,7 @@ def decode_files(files) hash_file = create_remote_hash_file(decoded_files) script = WinRM::FS::Scripts.render('decode_files', hash_file: hash_file) - parse_response(executor.run_powershell_script(script)) + parse_response(shell.run(script)) end end @@ -358,20 +358,6 @@ def pad(depth = 0) ' ' * depth end - # Parses CLIXML String into regular String (without any XML syntax). - # Inspired by https://github.com/WinRb/WinRM/issues/106. - # - # @param clixml [String] clixml text - # @return [String] parsed clixml into String - def clixml_to_s(clixml) - doc = REXML::Document.new(clixml) - text = doc.get_elements('//S').map(&:text).join - text.gsub(/_x(\h\h\h\h)_/) do - code = Regexp.last_match[1] - code.hex.chr - end - end - # Parses response of a PowerShell script or CMD command which contains # a CSV-formatted document in the standard output stream. # @@ -388,14 +374,13 @@ def parse_response(output) fail StandardError, 'The command line is too long' \ ' (powershell script is too long)' end - pretty_stderr = clixml_to_s(stderr) if exitcode != 0 fail FileTransporterFailed, "[#{self.class}] Upload failed " \ - "(exitcode: #{exitcode})\n#{pretty_stderr}" - elsif pretty_stderr != '\r\n' && pretty_stderr != '' + "(exitcode: #{exitcode})\n#{stderr}" + elsif stderr != '\r\n' && stderr != '' fail FileTransporterFailed, "[#{self.class}] Upload failed " \ - "(exitcode: 0), but stderr present\n#{pretty_stderr}" + "(exitcode: 0), but stderr present\n#{stderr}" end logger.debug 'Parsing CSV Response' @@ -441,15 +426,13 @@ def ps_hash(obj, depth = 0) # the number of bytes transferred to the remote host # @api private def stream_upload(input_io, dest) - dest_cmd = dest.sub('$env:TEMP', '%TEMP%') read_size = (MAX_ENCODED_WRITE.to_i / 4) * 3 chunk, bytes = 1, 0 buffer = '' - executor.run_cmd(%(echo|set /p=>"#{dest_cmd}")) # truncate empty file + shell.run("\"\" | Out-File #{dest} -Encoding ascii") while input_io.read(read_size, buffer) bytes += (buffer.bytesize / 3 * 4) - executor.run_cmd([buffer].pack(BASE64_PACK) - .insert(0, 'echo ').concat(%( >> "#{dest_cmd}"))) + shell.run("\"#{[buffer].pack(BASE64_PACK)}\" | Out-File #{dest} -Encoding ascii -Append") logger.debug "Wrote chunk #{chunk} for #{dest}" if chunk % 25 == 0 chunk += 1 yield bytes if block_given? diff --git a/lib/winrm-fs/file_manager.rb b/lib/winrm-fs/file_manager.rb index c6416b1..52e7ba4 100644 --- a/lib/winrm-fs/file_manager.rb +++ b/lib/winrm-fs/file_manager.rb @@ -23,10 +23,10 @@ module FS # Perform file transfer operations between a local machine and winrm endpoint class FileManager # Creates a new FileManager instance - # @param [WinRMWebService] WinRM web service client - def initialize(service) - @service = service - @logger = service.logger + # @param [WinRM::Connection] WinRM web connection client + def initialize(connection) + @connection = connection + @logger = connection.logger end # Gets the MD5 checksum of the specified file if it exists, @@ -35,7 +35,7 @@ def initialize(service) def checksum(path) @logger.debug("checksum: #{path}") script = WinRM::FS::Scripts.render('checksum', path: path) - @service.create_executor { |e| e.run_powershell_script(script).stdout.chomp } + @connection.shell(:powershell) { |e| e.run(script).stdout.chomp } end # Create the specifed directory recursively @@ -44,7 +44,7 @@ def checksum(path) def create_dir(path) @logger.debug("create_dir: #{path}") script = WinRM::FS::Scripts.render('create_dir', path: path) - @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 } + @connection.shell(:powershell) { |e| e.run(script)[:exitcode] == 0 } end # Deletes the file or directory at the specified path @@ -53,7 +53,7 @@ def create_dir(path) def delete(path) @logger.debug("deleting: #{path}") script = WinRM::FS::Scripts.render('delete', path: path) - @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 } + @connection.shell(:powershell) { |e| e.run(script)[:exitcode] == 0 } end # Downloads the specified remote file to the specified local path @@ -62,7 +62,7 @@ def delete(path) def download(remote_path, local_path) @logger.debug("downloading: #{remote_path} -> #{local_path}") script = WinRM::FS::Scripts.render('download', path: remote_path) - output = @service.create_executor { |e| e.run_powershell_script(script) } + output = @connection.shell(:powershell) { |e| e.run(script) } return false if output[:exitcode] != 0 contents = output.stdout.gsub('\n\r', '') out = Base64.decode64(contents) @@ -76,7 +76,7 @@ def download(remote_path, local_path) def exists?(path) @logger.debug("exists?: #{path}") script = WinRM::FS::Scripts.render('exists', path: path) - @service.create_executor { |e| e.run_powershell_script(script)[:exitcode] == 0 } + @connection.shell(:powershell) { |e| e.run(script)[:exitcode] == 0 } end # Gets the current user's TEMP directory on the remote system, for example @@ -84,7 +84,7 @@ def exists?(path) # @return [String] Full path to the temp directory def temp_dir @guest_temp ||= begin - (@service.create_executor { |e| e.run_cmd('echo %TEMP%') }).stdout.chomp.gsub('\\', '/') + (@connection.shell(:powershell) { |e| e.run('$env:TEMP') }).stdout.chomp.gsub('\\', '/') end end @@ -107,8 +107,8 @@ def temp_dir # @yieldparam [String] Target path on the winrm endpoint # @return [Fixnum] The total number of bytes copied def upload(local_path, remote_path, &block) - @service.create_executor do |executor| - file_transporter ||= WinRM::FS::Core::FileTransporter.new(executor) + @connection.shell(:powershell) do |shell| + file_transporter ||= WinRM::FS::Core::FileTransporter.new(shell) file_transporter.upload(local_path, remote_path, &block)[0] end end diff --git a/spec/config-example.yml b/spec/config-example.yml index 2ff803f..3a99d7d 100644 --- a/spec/config-example.yml +++ b/spec/config-example.yml @@ -1,5 +1,4 @@ -auth_type: plaintext +transport: plaintext endpoint: "http://192.168.137.20:5985/wsman" -options: - user: vagrant - pass: vagrant \ No newline at end of file +user: vagrant +password: vagrant \ No newline at end of file diff --git a/spec/integration/benchmark_spec.rb b/spec/integration/benchmark_spec.rb new file mode 100644 index 0000000..7e40a41 --- /dev/null +++ b/spec/integration/benchmark_spec.rb @@ -0,0 +1,25 @@ +# encoding: UTF-8 +require 'pathname' + +describe WinRM::FS::FileManager do + let(:dest_dir) { subject.temp_dir } + let(:this_file) { "C:/Users/matt/Downloads/[MS-WSMV].pdf" } + let(:service) { winrm_connection } + + subject { WinRM::FS::FileManager.new(service) } + + context 'upload file' do + let(:dest_file) { File.join(dest_dir, "test.pdf") } + + before(:each) do + expect(subject.delete(dest_dir)).to be true + end + + it 'should upload the specified file' do + blah = Benchmark.measure do + subject.upload(this_file, dest_file) + end + puts blah + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d21b14e..9bcb47d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,15 +9,14 @@ module ConnectionHelper # rubocop:disable AbcSize def winrm_connection - WinRM::WinRMWebService.new( - config[:endpoint], config[:auth_type].to_sym, config[:options]) + WinRM::Connection.new(config) end # rubocop:enable AbcSize def config @config ||= begin cfg = symbolize_keys(YAML.load(File.read(winrm_config_path))) - cfg[:options].merge!(basic_auth_only: true) unless cfg[:auth_type].eql? :kerberos + cfg.merge!(basic_auth_only: true) unless cfg[:transport].eql? :kerberos merge_environment!(cfg) cfg end @@ -25,18 +24,18 @@ def config def merge_environment!(config) merge_config_option_from_environment(config, 'user') - merge_config_option_from_environment(config, 'pass') + merge_config_option_from_environment(config, 'password') merge_config_option_from_environment(config, 'no_ssl_peer_verification') if ENV['use_ssl_peer_fingerprint'] config[:options][:ssl_peer_fingerprint] = ENV['winrm_cert'] end config[:endpoint] = ENV['winrm_endpoint'] if ENV['winrm_endpoint'] - config[:auth_type] = ENV['winrm_auth_type'] if ENV['winrm_auth_type'] + config[:transport] = ENV['winrm_transport'] if ENV['winrm_transport'] end def merge_config_option_from_environment(config, key) env_key = 'winrm_' + key - config[:options][key.to_sym] = ENV[env_key] if ENV[env_key] + config[key.to_sym] = ENV[env_key] if ENV[env_key] end def winrm_config_path diff --git a/winrm-fs.gemspec b/winrm-fs.gemspec index f58db8f..5fb6fe4 100644 --- a/winrm-fs.gemspec +++ b/winrm-fs.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'erubis', '~> 2.7' s.add_runtime_dependency 'logging', ['>= 1.6.1', '< 3.0'] s.add_runtime_dependency 'rubyzip', '~> 1.1' - s.add_runtime_dependency 'winrm', '~> 1.5' + s.add_runtime_dependency 'winrm' s.add_development_dependency 'pry' s.add_development_dependency 'rspec', '~> 3.0.0' s.add_development_dependency 'rake', '~> 10.3.2'