winrm 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5339846f9181cf6fe697c0d33cd865d1a9a8aecb
4
- data.tar.gz: 6780ada80472407f0946199c3162ce5c8fed97df
3
+ metadata.gz: 5aee7bd9437669d90fa2e9a928624cd459d5c2ba
4
+ data.tar.gz: f29dc28620dca84b05b1ca1f65ac3bdd1f98a5da
5
5
  SHA512:
6
- metadata.gz: 44849b77e53db8a83bc9dbcc471fbb3fb2cddc809f32c4a516c8523660eb3609af2fb4b3bd04c6ff169a6ae9732c4d7b9b1afbd98e9fb0dfe7cfcb183604bc67
7
- data.tar.gz: 89a535c6b28501f36f337c9b4b03926515117c2e0a0dcd25972a1d197e7a78e1f13f62dfd06212b14489011718386ac8c57dc8b27b0b6c5056b60ed1aef7e678
6
+ metadata.gz: 5ba7e3c9917afee28df831efa38cd4234de1749ff237292eb13682a806bc71b6e8e83c3ed25c4aa1cd32304ffbc68a60bf4eb59937f67ccf779ee125e6851e56
7
+ data.tar.gz: 618c9e4000b0d4bb0754991863ce5845e3e9df4ee77cdf1fa58ca6c6b76ee9b4c96e609184a97f837858cbc1ded51f148b219563beb01caadd70c5643bc10d14
data/changelog.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # WinRM Gem Changelog
2
2
 
3
+ # 2.2.1
4
+ - Ignore error 2150858843 during shell closing
5
+
3
6
  # 2.2.0
4
7
  - Allow run_wql to accept custom namespace
5
8
  - Allow enumeration of WQL result sets
@@ -1,181 +1,185 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Copyright 2016 Shawn Neal <sneal@sneal.net>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- require_relative 'retryable'
18
- require_relative '../http/transport'
19
- require_relative '../wsmv/cleanup_command'
20
- require_relative '../wsmv/close_shell'
21
- require_relative '../wsmv/command'
22
- require_relative '../wsmv/command_output'
23
- require_relative '../wsmv/receive_response_reader'
24
- require_relative '../wsmv/create_shell'
25
- require_relative '../wsmv/soap'
26
-
27
- module WinRM
28
- module Shells
29
- # Base class for remote shell
30
- class Base
31
- TOO_MANY_COMMANDS = '2150859174'.freeze
32
- ERROR_OPERATION_ABORTED = '995'.freeze
33
- SHELL_NOT_FOUND = '2150858843'.freeze
34
-
35
- FAULTS_FOR_RESET = [
36
- '2150858843', # Shell has been closed
37
- '2147943418', # Error reading registry key
38
- TOO_MANY_COMMANDS, # Maximum commands per user exceeded
39
- ].freeze
40
-
41
- include Retryable
42
-
43
- # Create a new Cmd shell
44
- # @param connection_opts [ConnectionOpts] The WinRM connection options
45
- # @param transport [HttpTransport] The WinRM SOAP transport
46
- # @param logger [Logger] The logger to log diagnostic messages to
47
- # @param shell_opts [Hash] Options targeted for the created shell
48
- def initialize(connection_opts, transport, logger, shell_opts = {})
49
- @connection_opts = connection_opts
50
- @transport = transport
51
- @logger = logger
52
- @shell_opts = shell_opts
53
- end
54
-
55
- # @return [String] shell id of the currently opn shell or nil if shell is closed
56
- attr_reader :shell_id
57
-
58
- # @return [String] uri that SOAP calls use to identify shell type
59
- attr_reader :shell_uri
60
-
61
- # @return [ConnectionOpts] connection options of the shell
62
- attr_reader :connection_opts
63
-
64
- # @return [WinRM::HTTP::HttpTransport] transport used to talk with endpoint
65
- attr_reader :transport
66
-
67
- # @return [Logger] logger used for diagnostic messages
68
- attr_reader :logger
69
-
70
- # @return [Hash] Options targeted for the created shell
71
- attr_reader :shell_opts
72
-
73
- # Runs the specified command with optional arguments
74
- # @param command [String] The command or executable to run
75
- # @param arguments [Array] The optional command arguments
76
- # @param block [&block] The optional callback for any realtime output
77
- # @yieldparam [string] standard out response text
78
- # @yieldparam [string] standard error response text
79
- # @yieldreturn [WinRM::Output] The command output
80
- def run(command, arguments = [], &block)
81
- with_command_shell(command, arguments) do |shell, cmd|
82
- response_reader.read_output(command_output_message(shell, cmd), &block)
83
- end
84
- end
85
-
86
- # Closes the shell if one is open
87
- def close
88
- return unless shell_id
89
- self.class.close_shell(connection_opts, transport, shell_id)
90
- remove_finalizer
91
- @shell_id = nil
92
- end
93
-
94
- protected
95
-
96
- def send_command(_command, _arguments)
97
- raise NotImplementedError
98
- end
99
-
100
- def response_reader
101
- raise NotImplementedError
102
- end
103
-
104
- def open_shell
105
- raise NotImplementedError
106
- end
107
-
108
- def out_streams
109
- raise NotImplementedError
110
- end
111
-
112
- def command_output_message(shell_id, command_id)
113
- cmd_out_opts = {
114
- shell_id: shell_id,
115
- command_id: command_id,
116
- shell_uri: shell_uri,
117
- out_streams: out_streams
118
- }
119
- WinRM::WSMV::CommandOutput.new(connection_opts, cmd_out_opts)
120
- end
121
-
122
- def with_command_shell(command, arguments = [])
123
- tries ||= 2
124
-
125
- open unless shell_id
126
- command_id = send_command(command, arguments)
127
- logger.debug("[WinRM] creating command_id: #{command_id} on shell_id #{shell_id}")
128
- yield shell_id, command_id
129
- rescue WinRMWSManFault => e
130
- raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0
131
- reset_on_error(e)
132
- retry
133
- ensure
134
- cleanup_command(command_id) if command_id
135
- end
136
-
137
- private
138
-
139
- def reset_on_error(error)
140
- close if error.fault_code == TOO_MANY_COMMANDS
141
- logger.debug('[WinRM] opening new shell since the current one was deleted')
142
- @shell_id = nil
143
- end
144
-
145
- def cleanup_command(command_id)
146
- logger.debug("[WinRM] cleaning up command_id: #{command_id} on shell_id #{shell_id}")
147
- cleanup_msg = WinRM::WSMV::CleanupCommand.new(
148
- connection_opts,
149
- shell_uri: shell_uri,
150
- shell_id: shell_id,
151
- command_id: command_id)
152
- transport.send_request(cleanup_msg.build)
153
- rescue WinRMWSManFault => e
154
- raise unless [ERROR_OPERATION_ABORTED, SHELL_NOT_FOUND].include?(e.fault_code)
155
- rescue WinRMHTTPTransportError => t
156
- # dont let the cleanup raise so we dont lose any errors from the command
157
- logger.info("[WinRM] #{t.status_code} returned in cleanup with error: #{t.message}")
158
- end
159
-
160
- def open
161
- close
162
- retryable(connection_opts[:retry_limit], connection_opts[:retry_delay]) do
163
- logger.debug("[WinRM] opening remote shell on #{connection_opts[:endpoint]}")
164
- @shell_id = open_shell
165
- end
166
- logger.debug("[WinRM] remote shell created with shell_id: #{shell_id}")
167
- add_finalizer
168
- end
169
-
170
- def add_finalizer
171
- ObjectSpace.define_finalizer(
172
- self,
173
- self.class.finalize(connection_opts, transport, shell_id))
174
- end
175
-
176
- def remove_finalizer
177
- ObjectSpace.undefine_finalizer(self)
178
- end
179
- end
180
- end
181
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Copyright 2016 Shawn Neal <sneal@sneal.net>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'retryable'
18
+ require_relative '../http/transport'
19
+ require_relative '../wsmv/cleanup_command'
20
+ require_relative '../wsmv/close_shell'
21
+ require_relative '../wsmv/command'
22
+ require_relative '../wsmv/command_output'
23
+ require_relative '../wsmv/receive_response_reader'
24
+ require_relative '../wsmv/create_shell'
25
+ require_relative '../wsmv/soap'
26
+
27
+ module WinRM
28
+ module Shells
29
+ # Base class for remote shell
30
+ class Base
31
+ TOO_MANY_COMMANDS = '2150859174'.freeze
32
+ ERROR_OPERATION_ABORTED = '995'.freeze
33
+ SHELL_NOT_FOUND = '2150858843'.freeze
34
+
35
+ FAULTS_FOR_RESET = [
36
+ '2150858843', # Shell has been closed
37
+ '2147943418', # Error reading registry key
38
+ TOO_MANY_COMMANDS, # Maximum commands per user exceeded
39
+ ].freeze
40
+
41
+ include Retryable
42
+
43
+ # Create a new Cmd shell
44
+ # @param connection_opts [ConnectionOpts] The WinRM connection options
45
+ # @param transport [HttpTransport] The WinRM SOAP transport
46
+ # @param logger [Logger] The logger to log diagnostic messages to
47
+ # @param shell_opts [Hash] Options targeted for the created shell
48
+ def initialize(connection_opts, transport, logger, shell_opts = {})
49
+ @connection_opts = connection_opts
50
+ @transport = transport
51
+ @logger = logger
52
+ @shell_opts = shell_opts
53
+ end
54
+
55
+ # @return [String] shell id of the currently opn shell or nil if shell is closed
56
+ attr_reader :shell_id
57
+
58
+ # @return [String] uri that SOAP calls use to identify shell type
59
+ attr_reader :shell_uri
60
+
61
+ # @return [ConnectionOpts] connection options of the shell
62
+ attr_reader :connection_opts
63
+
64
+ # @return [WinRM::HTTP::HttpTransport] transport used to talk with endpoint
65
+ attr_reader :transport
66
+
67
+ # @return [Logger] logger used for diagnostic messages
68
+ attr_reader :logger
69
+
70
+ # @return [Hash] Options targeted for the created shell
71
+ attr_reader :shell_opts
72
+
73
+ # Runs the specified command with optional arguments
74
+ # @param command [String] The command or executable to run
75
+ # @param arguments [Array] The optional command arguments
76
+ # @param block [&block] The optional callback for any realtime output
77
+ # @yieldparam [string] standard out response text
78
+ # @yieldparam [string] standard error response text
79
+ # @yieldreturn [WinRM::Output] The command output
80
+ def run(command, arguments = [], &block)
81
+ with_command_shell(command, arguments) do |shell, cmd|
82
+ response_reader.read_output(command_output_message(shell, cmd), &block)
83
+ end
84
+ end
85
+
86
+ # Closes the shell if one is open
87
+ def close
88
+ return unless shell_id
89
+ begin
90
+ self.class.close_shell(connection_opts, transport, shell_id)
91
+ rescue WinRMWSManFault => e
92
+ raise unless [ERROR_OPERATION_ABORTED, SHELL_NOT_FOUND].include?(e.fault_code)
93
+ end
94
+ remove_finalizer
95
+ @shell_id = nil
96
+ end
97
+
98
+ protected
99
+
100
+ def send_command(_command, _arguments)
101
+ raise NotImplementedError
102
+ end
103
+
104
+ def response_reader
105
+ raise NotImplementedError
106
+ end
107
+
108
+ def open_shell
109
+ raise NotImplementedError
110
+ end
111
+
112
+ def out_streams
113
+ raise NotImplementedError
114
+ end
115
+
116
+ def command_output_message(shell_id, command_id)
117
+ cmd_out_opts = {
118
+ shell_id: shell_id,
119
+ command_id: command_id,
120
+ shell_uri: shell_uri,
121
+ out_streams: out_streams
122
+ }
123
+ WinRM::WSMV::CommandOutput.new(connection_opts, cmd_out_opts)
124
+ end
125
+
126
+ def with_command_shell(command, arguments = [])
127
+ tries ||= 2
128
+
129
+ open unless shell_id
130
+ command_id = send_command(command, arguments)
131
+ logger.debug("[WinRM] creating command_id: #{command_id} on shell_id #{shell_id}")
132
+ yield shell_id, command_id
133
+ rescue WinRMWSManFault => e
134
+ raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0
135
+ reset_on_error(e)
136
+ retry
137
+ ensure
138
+ cleanup_command(command_id) if command_id
139
+ end
140
+
141
+ private
142
+
143
+ def reset_on_error(error)
144
+ close if error.fault_code == TOO_MANY_COMMANDS
145
+ logger.debug('[WinRM] opening new shell since the current one was deleted')
146
+ @shell_id = nil
147
+ end
148
+
149
+ def cleanup_command(command_id)
150
+ logger.debug("[WinRM] cleaning up command_id: #{command_id} on shell_id #{shell_id}")
151
+ cleanup_msg = WinRM::WSMV::CleanupCommand.new(
152
+ connection_opts,
153
+ shell_uri: shell_uri,
154
+ shell_id: shell_id,
155
+ command_id: command_id)
156
+ transport.send_request(cleanup_msg.build)
157
+ rescue WinRMWSManFault => e
158
+ raise unless [ERROR_OPERATION_ABORTED, SHELL_NOT_FOUND].include?(e.fault_code)
159
+ rescue WinRMHTTPTransportError => t
160
+ # dont let the cleanup raise so we dont lose any errors from the command
161
+ logger.info("[WinRM] #{t.status_code} returned in cleanup with error: #{t.message}")
162
+ end
163
+
164
+ def open
165
+ close
166
+ retryable(connection_opts[:retry_limit], connection_opts[:retry_delay]) do
167
+ logger.debug("[WinRM] opening remote shell on #{connection_opts[:endpoint]}")
168
+ @shell_id = open_shell
169
+ end
170
+ logger.debug("[WinRM] remote shell created with shell_id: #{shell_id}")
171
+ add_finalizer
172
+ end
173
+
174
+ def add_finalizer
175
+ ObjectSpace.define_finalizer(
176
+ self,
177
+ self.class.finalize(connection_opts, transport, shell_id))
178
+ end
179
+
180
+ def remove_finalizer
181
+ ObjectSpace.undefine_finalizer(self)
182
+ end
183
+ end
184
+ end
185
+ end
data/lib/winrm/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # WinRM module
4
4
  module WinRM
5
5
  # The version of the WinRM library
6
- VERSION = '2.2.0'.freeze
6
+ VERSION = '2.2.1'.freeze
7
7
  end
@@ -1,216 +1,225 @@
1
- # encoding: UTF-8
2
-
3
- require 'winrm/shells/base'
4
-
5
- # Dummy shell class
6
- class DummyShell < WinRM::Shells::Base
7
- class << self
8
- def finalize(connection_opts, transport, shell_id)
9
- proc { DummyShell.close_shell(connection_opts, transport, shell_id) }
10
- end
11
-
12
- def close_shell(_connection_opts, _transport, _shell_id)
13
- @closed = true
14
- end
15
-
16
- def closed?
17
- @closed
18
- end
19
- end
20
-
21
- def open_shell
22
- @closed = false
23
- 'shell_id'
24
- end
25
-
26
- def send_command(_command, _arguments)
27
- 'command_id'
28
- end
29
-
30
- def out_streams
31
- %w(std)
32
- end
33
- end
34
-
35
- describe DummyShell do
36
- let(:retry_limit) { 1 }
37
- let(:shell_id) { 'shell_id' }
38
- let(:output) { 'output' }
39
- let(:command_id) { 'command_id' }
40
- let(:payload) { 'message_payload' }
41
- let(:command) { 'command' }
42
- let(:arguments) { ['args'] }
43
- let(:output_message) { double('output_message', build: 'output_message') }
44
- let(:connection_options) { { max_commands: 100, retry_limit: retry_limit, retry_delay: 0 } }
45
- let(:transport) { double('transport') }
46
- let(:reader) { double('reader') }
47
-
48
- before do
49
- allow(subject).to receive(:response_reader).and_return(reader)
50
- allow(subject).to receive(:command_output_message).with(shell_id, command_id)
51
- .and_return(output_message)
52
- allow(reader).to receive(:read_output).with(output_message).and_return(output)
53
- allow(transport).to receive(:send_request)
54
- end
55
-
56
- subject { described_class.new(connection_options, transport, Logging.logger['test']) }
57
-
58
- shared_examples 'retry shell command' do
59
- it 'only closes the shell if there are too many' do
60
- if fault == WinRM::Shells::Base::TOO_MANY_COMMANDS
61
- expect(DummyShell).to receive(:close_shell)
62
- else
63
- expect(DummyShell).not_to receive(:close_shell)
64
- end
65
-
66
- subject.run(command, arguments)
67
- end
68
-
69
- it 'opens a new shell' do
70
- expect(subject).to receive(:open).and_call_original.twice
71
-
72
- subject.run(command, arguments)
73
- end
74
-
75
- it 'retries the command once' do
76
- expect(subject).to receive(:send_command).twice
77
-
78
- subject.run(command, arguments)
79
- end
80
- end
81
-
82
- describe '#run' do
83
- it 'opens a shell' do
84
- subject.run(command, arguments)
85
- expect(subject.shell_id).not_to be nil
86
- end
87
-
88
- it 'returns output from generated command' do
89
- expect(subject.run(command, arguments)).to eq output
90
- end
91
-
92
- it 'sends cleanup message through transport' do
93
- allow(SecureRandom).to receive(:uuid).and_return('uuid')
94
- expect(transport).to receive(:send_request)
95
- .with(
96
- WinRM::WSMV::CleanupCommand.new(
97
- connection_options,
98
- shell_uri: nil,
99
- shell_id: shell_id,
100
- command_id: command_id
101
- ).build
102
- )
103
- subject.run(command, arguments)
104
- end
105
-
106
- it 'does not error if cleanup is aborted' do
107
- allow(SecureRandom).to receive(:uuid).and_return('uuid')
108
- expect(transport).to receive(:send_request)
109
- .with(
110
- WinRM::WSMV::CleanupCommand.new(
111
- connection_options,
112
- shell_uri: nil,
113
- shell_id: shell_id,
114
- command_id: command_id
115
- ).build
116
- ).and_raise(WinRM::WinRMWSManFault.new('oops', '995'))
117
- subject.run(command, arguments)
118
- end
119
-
120
- it 'does not error if shell is not present anymore' do
121
- allow(SecureRandom).to receive(:uuid).and_return('uuid')
122
- expect(transport).to receive(:send_request)
123
- .with(
124
- WinRM::WSMV::CleanupCommand.new(
125
- connection_options,
126
- shell_uri: nil,
127
- shell_id: shell_id,
128
- command_id: command_id
129
- ).build
130
- ).and_raise(WinRM::WinRMWSManFault.new('oops', '2150858843'))
131
- subject.run(command, arguments)
132
- end
133
-
134
- it 'opens a shell only once when shell is already open' do
135
- expect(subject).to receive(:open_shell).and_call_original.once
136
- subject.run(command, arguments)
137
- subject.run(command, arguments)
138
- end
139
-
140
- describe 'connection resets' do
141
- before do
142
- @times_called = 0
143
-
144
- allow(subject).to receive(:send_command) do
145
- @times_called += 1
146
- raise WinRM::WinRMWSManFault.new('oops', fault) if @times_called == 1
147
- command_id
148
- end
149
- end
150
-
151
- context 'when shell is closed on server' do
152
- let(:fault) { '2150858843' }
153
-
154
- include_examples 'retry shell command'
155
- end
156
-
157
- context 'when shell accesses a deleted registry key' do
158
- let(:fault) { '2147943418' }
159
-
160
- include_examples 'retry shell command'
161
- end
162
-
163
- context 'when maximum number of concurrent shells is exceeded' do
164
- let(:fault) { '2150859174' }
165
-
166
- include_examples 'retry shell command'
167
- end
168
- end
169
-
170
- context 'open_shell fails' do
171
- let(:retry_limit) { 2 }
172
- let(:output_message2) { double('message') }
173
-
174
- it 'retries and raises failure if it never succeeds' do
175
- expect(subject).to receive(:open_shell)
176
- .and_raise(Errno::ECONNREFUSED).exactly(retry_limit).times
177
- expect { subject.run(command) }.to raise_error(Errno::ECONNREFUSED)
178
- end
179
-
180
- it 'retries and returns shell on success' do
181
- @times = 0
182
- allow(subject).to receive(:command_output_message).with('shell_id 2', command_id)
183
- .and_return(output_message2)
184
- allow(reader).to receive(:read_output)
185
- .with(output_message2).and_return(output)
186
- allow(subject).to receive(:open_shell) do
187
- @times += 1
188
- raise(Errno::ECONNREFUSED) if @times == 1
189
- "shell_id #{@times}"
190
- end
191
-
192
- subject.run(command, arguments)
193
- expect(subject.shell_id).to eq 'shell_id 2'
194
- end
195
- end
196
- end
197
-
198
- describe '#close' do
199
- it 'does not close if not opened' do
200
- expect(DummyShell).not_to receive(:close_shell)
201
- subject.close
202
- end
203
-
204
- it 'close shell if opened' do
205
- subject.run(command, arguments)
206
- subject.close
207
- expect(DummyShell.closed?).to be(true)
208
- end
209
-
210
- it 'nils out the shell_id' do
211
- subject.run(command, arguments)
212
- subject.close
213
- expect(subject.shell_id).to be(nil)
214
- end
215
- end
216
- end
1
+ # encoding: UTF-8
2
+
3
+ require 'winrm/shells/base'
4
+
5
+ # Dummy shell class
6
+ class DummyShell < WinRM::Shells::Base
7
+ class << self
8
+ def finalize(connection_opts, transport, shell_id)
9
+ proc { DummyShell.close_shell(connection_opts, transport, shell_id) }
10
+ end
11
+
12
+ def close_shell(_connection_opts, _transport, _shell_id)
13
+ @closed = true
14
+ end
15
+
16
+ def closed?
17
+ @closed
18
+ end
19
+ end
20
+
21
+ def open_shell
22
+ @closed = false
23
+ 'shell_id'
24
+ end
25
+
26
+ def send_command(_command, _arguments)
27
+ 'command_id'
28
+ end
29
+
30
+ def out_streams
31
+ %w(std)
32
+ end
33
+ end
34
+
35
+ describe DummyShell do
36
+ let(:retry_limit) { 1 }
37
+ let(:shell_id) { 'shell_id' }
38
+ let(:output) { 'output' }
39
+ let(:command_id) { 'command_id' }
40
+ let(:payload) { 'message_payload' }
41
+ let(:command) { 'command' }
42
+ let(:arguments) { ['args'] }
43
+ let(:output_message) { double('output_message', build: 'output_message') }
44
+ let(:connection_options) { { max_commands: 100, retry_limit: retry_limit, retry_delay: 0 } }
45
+ let(:transport) { double('transport') }
46
+ let(:reader) { double('reader') }
47
+
48
+ before do
49
+ allow(subject).to receive(:response_reader).and_return(reader)
50
+ allow(subject).to receive(:command_output_message).with(shell_id, command_id)
51
+ .and_return(output_message)
52
+ allow(reader).to receive(:read_output).with(output_message).and_return(output)
53
+ allow(transport).to receive(:send_request)
54
+ end
55
+
56
+ subject { described_class.new(connection_options, transport, Logging.logger['test']) }
57
+
58
+ shared_examples 'retry shell command' do
59
+ it 'only closes the shell if there are too many' do
60
+ if fault == WinRM::Shells::Base::TOO_MANY_COMMANDS
61
+ expect(DummyShell).to receive(:close_shell)
62
+ else
63
+ expect(DummyShell).not_to receive(:close_shell)
64
+ end
65
+
66
+ subject.run(command, arguments)
67
+ end
68
+
69
+ it 'opens a new shell' do
70
+ expect(subject).to receive(:open).and_call_original.twice
71
+
72
+ subject.run(command, arguments)
73
+ end
74
+
75
+ it 'retries the command once' do
76
+ expect(subject).to receive(:send_command).twice
77
+
78
+ subject.run(command, arguments)
79
+ end
80
+ end
81
+
82
+ describe '#run' do
83
+ it 'opens a shell' do
84
+ subject.run(command, arguments)
85
+ expect(subject.shell_id).not_to be nil
86
+ end
87
+
88
+ it 'returns output from generated command' do
89
+ expect(subject.run(command, arguments)).to eq output
90
+ end
91
+
92
+ it 'sends cleanup message through transport' do
93
+ allow(SecureRandom).to receive(:uuid).and_return('uuid')
94
+ expect(transport).to receive(:send_request)
95
+ .with(
96
+ WinRM::WSMV::CleanupCommand.new(
97
+ connection_options,
98
+ shell_uri: nil,
99
+ shell_id: shell_id,
100
+ command_id: command_id
101
+ ).build
102
+ )
103
+ subject.run(command, arguments)
104
+ end
105
+
106
+ it 'does not error if cleanup is aborted' do
107
+ allow(SecureRandom).to receive(:uuid).and_return('uuid')
108
+ expect(transport).to receive(:send_request)
109
+ .with(
110
+ WinRM::WSMV::CleanupCommand.new(
111
+ connection_options,
112
+ shell_uri: nil,
113
+ shell_id: shell_id,
114
+ command_id: command_id
115
+ ).build
116
+ ).and_raise(WinRM::WinRMWSManFault.new('oops', '995'))
117
+ subject.run(command, arguments)
118
+ end
119
+
120
+ it 'does not error if shell is not present anymore' do
121
+ allow(SecureRandom).to receive(:uuid).and_return('uuid')
122
+ expect(transport).to receive(:send_request)
123
+ .with(
124
+ WinRM::WSMV::CleanupCommand.new(
125
+ connection_options,
126
+ shell_uri: nil,
127
+ shell_id: shell_id,
128
+ command_id: command_id
129
+ ).build
130
+ ).and_raise(WinRM::WinRMWSManFault.new('oops', '2150858843'))
131
+ subject.run(command, arguments)
132
+ end
133
+
134
+ it 'opens a shell only once when shell is already open' do
135
+ expect(subject).to receive(:open_shell).and_call_original.once
136
+ subject.run(command, arguments)
137
+ subject.run(command, arguments)
138
+ end
139
+
140
+ describe 'connection resets' do
141
+ before do
142
+ @times_called = 0
143
+
144
+ allow(subject).to receive(:send_command) do
145
+ @times_called += 1
146
+ raise WinRM::WinRMWSManFault.new('oops', fault) if @times_called == 1
147
+ command_id
148
+ end
149
+ end
150
+
151
+ context 'when shell is closed on server' do
152
+ let(:fault) { '2150858843' }
153
+
154
+ include_examples 'retry shell command'
155
+ end
156
+
157
+ context 'when shell accesses a deleted registry key' do
158
+ let(:fault) { '2147943418' }
159
+
160
+ include_examples 'retry shell command'
161
+ end
162
+
163
+ context 'when maximum number of concurrent shells is exceeded' do
164
+ let(:fault) { '2150859174' }
165
+
166
+ include_examples 'retry shell command'
167
+ end
168
+ end
169
+
170
+ context 'open_shell fails' do
171
+ let(:retry_limit) { 2 }
172
+ let(:output_message2) { double('message') }
173
+
174
+ it 'retries and raises failure if it never succeeds' do
175
+ expect(subject).to receive(:open_shell)
176
+ .and_raise(Errno::ECONNREFUSED).exactly(retry_limit).times
177
+ expect { subject.run(command) }.to raise_error(Errno::ECONNREFUSED)
178
+ end
179
+
180
+ it 'retries and returns shell on success' do
181
+ @times = 0
182
+ allow(subject).to receive(:command_output_message).with('shell_id 2', command_id)
183
+ .and_return(output_message2)
184
+ allow(reader).to receive(:read_output)
185
+ .with(output_message2).and_return(output)
186
+ allow(subject).to receive(:open_shell) do
187
+ @times += 1
188
+ raise(Errno::ECONNREFUSED) if @times == 1
189
+ "shell_id #{@times}"
190
+ end
191
+
192
+ subject.run(command, arguments)
193
+ expect(subject.shell_id).to eq 'shell_id 2'
194
+ end
195
+ end
196
+ end
197
+
198
+ describe '#close' do
199
+ it 'does not close if not opened' do
200
+ expect(DummyShell).not_to receive(:close_shell)
201
+ subject.close
202
+ end
203
+
204
+ it 'close shell if opened' do
205
+ subject.run(command, arguments)
206
+ subject.close
207
+ expect(DummyShell.closed?).to be(true)
208
+ end
209
+
210
+ it 'nils out the shell_id' do
211
+ subject.run(command, arguments)
212
+ subject.close
213
+ expect(subject.shell_id).to be(nil)
214
+ end
215
+
216
+ context 'when shell was not found' do
217
+ it 'does not raise' do
218
+ subject.run(command, arguments)
219
+ expect(DummyShell).to receive(:close_shell)
220
+ .and_raise(WinRM::WinRMWSManFault.new('oops', '2150858843'))
221
+ expect { subject.close }.not_to raise_error
222
+ end
223
+ end
224
+ end
225
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: winrm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Wanek
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2017-04-04 00:00:00.000000000 Z
14
+ date: 2017-04-05 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: gssapi