winrm 2.0.3 → 2.1.0

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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rubocop.yml +26 -26
  4. data/.travis.yml +11 -11
  5. data/Gemfile +3 -3
  6. data/README.md +260 -233
  7. data/Rakefile +34 -34
  8. data/Vagrantfile +6 -6
  9. data/WinrmAppveyor.psm1 +31 -31
  10. data/appveyor.yml +51 -51
  11. data/changelog.md +104 -101
  12. data/lib/winrm.rb +39 -39
  13. data/lib/winrm/connection.rb +83 -82
  14. data/lib/winrm/connection_opts.rb +91 -91
  15. data/lib/winrm/exceptions.rb +76 -76
  16. data/lib/winrm/http/response_handler.rb +96 -96
  17. data/lib/winrm/http/transport.rb +424 -424
  18. data/lib/winrm/http/transport_factory.rb +68 -68
  19. data/lib/winrm/output.rb +59 -59
  20. data/lib/winrm/psrp/create_pipeline.xml.erb +167 -167
  21. data/lib/winrm/psrp/fragment.rb +70 -70
  22. data/lib/winrm/psrp/init_runspace_pool.xml.erb +224 -224
  23. data/lib/winrm/psrp/message.rb +130 -130
  24. data/lib/winrm/psrp/message_data.rb +42 -42
  25. data/lib/winrm/psrp/message_data/base.rb +49 -49
  26. data/lib/winrm/psrp/message_data/error_record.rb +68 -68
  27. data/lib/winrm/psrp/message_data/pipeline_host_call.rb +32 -32
  28. data/lib/winrm/psrp/message_data/pipeline_output.rb +49 -49
  29. data/lib/winrm/psrp/message_data/pipeline_state.rb +40 -40
  30. data/lib/winrm/psrp/message_data/runspacepool_host_call.rb +32 -32
  31. data/lib/winrm/psrp/message_data/runspacepool_state.rb +39 -39
  32. data/lib/winrm/psrp/message_data/session_capability.rb +36 -36
  33. data/lib/winrm/psrp/message_defragmenter.rb +62 -62
  34. data/lib/winrm/psrp/message_factory.rb +75 -75
  35. data/lib/winrm/psrp/message_fragmenter.rb +60 -60
  36. data/lib/winrm/psrp/powershell_output_decoder.rb +139 -139
  37. data/lib/winrm/psrp/receive_response_reader.rb +97 -97
  38. data/lib/winrm/psrp/session_capability.xml.erb +7 -7
  39. data/lib/winrm/psrp/uuid.rb +40 -40
  40. data/lib/winrm/shells/base.rb +180 -175
  41. data/lib/winrm/shells/cmd.rb +65 -65
  42. data/lib/winrm/shells/power_shell.rb +202 -202
  43. data/lib/winrm/shells/retryable.rb +45 -45
  44. data/lib/winrm/shells/shell_factory.rb +58 -50
  45. data/lib/winrm/version.rb +7 -7
  46. data/lib/winrm/wsmv/base.rb +59 -59
  47. data/lib/winrm/wsmv/cleanup_command.rb +61 -61
  48. data/lib/winrm/wsmv/close_shell.rb +50 -50
  49. data/lib/winrm/wsmv/command.rb +101 -101
  50. data/lib/winrm/wsmv/command_output.rb +76 -76
  51. data/lib/winrm/wsmv/command_output_decoder.rb +55 -55
  52. data/lib/winrm/wsmv/configuration.rb +46 -46
  53. data/lib/winrm/wsmv/create_pipeline.rb +66 -66
  54. data/lib/winrm/wsmv/create_shell.rb +119 -119
  55. data/lib/winrm/wsmv/header.rb +203 -203
  56. data/lib/winrm/wsmv/init_runspace_pool.rb +95 -95
  57. data/lib/winrm/wsmv/iso8601_duration.rb +60 -60
  58. data/lib/winrm/wsmv/keep_alive.rb +68 -68
  59. data/lib/winrm/wsmv/receive_response_reader.rb +126 -126
  60. data/lib/winrm/wsmv/send_data.rb +68 -68
  61. data/lib/winrm/wsmv/soap.rb +51 -51
  62. data/lib/winrm/wsmv/wql_query.rb +79 -79
  63. data/lib/winrm/wsmv/write_stdin.rb +88 -88
  64. data/tests/integration/auth_timeout_spec.rb +18 -18
  65. data/tests/integration/cmd_spec.rb +131 -110
  66. data/tests/integration/config-example.yml +16 -16
  67. data/tests/integration/issue_59_spec.rb +26 -26
  68. data/tests/integration/powershell_spec.rb +165 -165
  69. data/tests/integration/spec_helper.rb +65 -65
  70. data/tests/integration/transport_spec.rb +99 -99
  71. data/tests/integration/wql_spec.rb +16 -16
  72. data/tests/matchers.rb +60 -60
  73. data/tests/spec/configuration_spec.rb +184 -184
  74. data/tests/spec/connection_spec.rb +39 -39
  75. data/tests/spec/exception_spec.rb +50 -50
  76. data/tests/spec/http/transport_factory_spec.rb +68 -68
  77. data/tests/spec/http/transport_spec.rb +44 -44
  78. data/tests/spec/output_spec.rb +127 -127
  79. data/tests/spec/psrp/fragment_spec.rb +62 -62
  80. data/tests/spec/psrp/message_data/base_spec.rb +13 -13
  81. data/tests/spec/psrp/message_data/error_record_spec.rb +41 -41
  82. data/tests/spec/psrp/message_data/pipeline_host_call_spec.rb +25 -25
  83. data/tests/spec/psrp/message_data/pipeline_output_spec.rb +32 -32
  84. data/tests/spec/psrp/message_data/pipeline_state_spec.rb +40 -40
  85. data/tests/spec/psrp/message_data/runspace_pool_host_call_spec.rb +25 -25
  86. data/tests/spec/psrp/message_data/runspacepool_state_spec.rb +16 -16
  87. data/tests/spec/psrp/message_data/session_capability_spec.rb +30 -30
  88. data/tests/spec/psrp/message_data_spec.rb +35 -35
  89. data/tests/spec/psrp/message_defragmenter_spec.rb +47 -47
  90. data/tests/spec/psrp/message_fragmenter_spec.rb +105 -105
  91. data/tests/spec/psrp/powershell_output_decoder_spec.rb +100 -100
  92. data/tests/spec/psrp/psrp_message_spec.rb +70 -70
  93. data/tests/spec/psrp/recieve_response_reader_spec.rb +172 -172
  94. data/tests/spec/psrp/uuid_spec.rb +28 -28
  95. data/tests/spec/response_handler_spec.rb +61 -61
  96. data/tests/spec/shells/base_spec.rb +202 -202
  97. data/tests/spec/shells/cmd_spec.rb +75 -75
  98. data/tests/spec/shells/powershell_spec.rb +175 -175
  99. data/tests/spec/spec_helper.rb +47 -47
  100. data/tests/spec/stubs/clixml/error_record.xml.erb +84 -84
  101. data/tests/spec/stubs/clixml/pipeline_state.xml.erb +88 -88
  102. data/tests/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  103. data/tests/spec/stubs/responses/get_command_output_response_not_done.xml.erb +10 -10
  104. data/tests/spec/stubs/responses/get_powershell_keepalive_response.xml.erb +10 -10
  105. data/tests/spec/stubs/responses/get_powershell_output_response.xml.erb +12 -12
  106. data/tests/spec/stubs/responses/get_powershell_output_response_not_done.xml.erb +9 -9
  107. data/tests/spec/stubs/responses/open_shell_v1.xml +19 -19
  108. data/tests/spec/stubs/responses/open_shell_v2.xml +20 -20
  109. data/tests/spec/stubs/responses/soap_fault_v1.xml +36 -36
  110. data/tests/spec/stubs/responses/soap_fault_v2.xml +42 -42
  111. data/tests/spec/stubs/responses/wmi_error_v2.xml +41 -41
  112. data/tests/spec/wsmv/cleanup_command_spec.rb +22 -22
  113. data/tests/spec/wsmv/close_shell_spec.rb +17 -17
  114. data/tests/spec/wsmv/command_output_decoder_spec.rb +37 -37
  115. data/tests/spec/wsmv/command_output_spec.rb +45 -45
  116. data/tests/spec/wsmv/command_spec.rb +19 -19
  117. data/tests/spec/wsmv/configuration_spec.rb +17 -17
  118. data/tests/spec/wsmv/create_pipeline_spec.rb +31 -31
  119. data/tests/spec/wsmv/create_shell_spec.rb +38 -38
  120. data/tests/spec/wsmv/init_runspace_pool_spec.rb +36 -36
  121. data/tests/spec/wsmv/keep_alive_spec.rb +21 -21
  122. data/tests/spec/wsmv/receive_response_reader_spec.rb +123 -123
  123. data/tests/spec/wsmv/send_data_spec.rb +30 -30
  124. data/tests/spec/wsmv/wql_query_spec.rb +13 -13
  125. data/tests/spec/wsmv/write_stdin_spec.rb +22 -22
  126. data/winrm.gemspec +42 -42
  127. metadata +2 -2
@@ -1,28 +1,28 @@
1
- # encoding: UTF-8
2
- describe WinRM::PSRP::UUID do
3
- subject(:uuid_helper) do
4
- Object.new.extend(WinRM::PSRP::UUID)
5
- end
6
- context 'uuid is nil' do
7
- uuid = nil
8
- it 'should return an empty byte array' do
9
- bytes = uuid_helper.uuid_to_windows_guid_bytes(uuid)
10
- expect(bytes).to eq([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
11
- end
12
- end
13
- context 'uuid 08785e96-eb1b-4a74-a767-7b56e8f13ea9 with lower case letters' do
14
- uuid = '08785e96-eb1b-4a74-a767-7b56e8f13ea9'
15
- it 'should return a Windows GUID struct compatible little endian byte array' do
16
- bytes = uuid_helper.uuid_to_windows_guid_bytes(uuid)
17
- expect(bytes).to eq([150, 94, 120, 8, 27, 235, 116, 74, 167, 103, 123, 86, 232, 241, 62, 169])
18
- end
19
- end
20
- context 'uuid 045F9E19D-8B77-4394-AB0C-197497661668 with upper case letters' do
21
- uuid = '45F9E19D-8B77-4394-AB0C-197497661668'
22
- it 'should return a Windows GUID struct compatible little endian byte array' do
23
- bytes = uuid_helper.uuid_to_windows_guid_bytes(uuid)
24
- expect(bytes).to eq(
25
- [157, 225, 249, 69, 119, 139, 148, 67, 171, 12, 25, 116, 151, 102, 22, 104])
26
- end
27
- end
28
- end
1
+ # encoding: UTF-8
2
+ describe WinRM::PSRP::UUID do
3
+ subject(:uuid_helper) do
4
+ Object.new.extend(WinRM::PSRP::UUID)
5
+ end
6
+ context 'uuid is nil' do
7
+ uuid = nil
8
+ it 'should return an empty byte array' do
9
+ bytes = uuid_helper.uuid_to_windows_guid_bytes(uuid)
10
+ expect(bytes).to eq([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
11
+ end
12
+ end
13
+ context 'uuid 08785e96-eb1b-4a74-a767-7b56e8f13ea9 with lower case letters' do
14
+ uuid = '08785e96-eb1b-4a74-a767-7b56e8f13ea9'
15
+ it 'should return a Windows GUID struct compatible little endian byte array' do
16
+ bytes = uuid_helper.uuid_to_windows_guid_bytes(uuid)
17
+ expect(bytes).to eq([150, 94, 120, 8, 27, 235, 116, 74, 167, 103, 123, 86, 232, 241, 62, 169])
18
+ end
19
+ end
20
+ context 'uuid 045F9E19D-8B77-4394-AB0C-197497661668 with upper case letters' do
21
+ uuid = '45F9E19D-8B77-4394-AB0C-197497661668'
22
+ it 'should return a Windows GUID struct compatible little endian byte array' do
23
+ bytes = uuid_helper.uuid_to_windows_guid_bytes(uuid)
24
+ expect(bytes).to eq(
25
+ [157, 225, 249, 69, 119, 139, 148, 67, 171, 12, 25, 116, 151, 102, 22, 104])
26
+ end
27
+ end
28
+ end
@@ -1,61 +1,61 @@
1
- # encoding: UTF-8
2
- require 'winrm/http/response_handler'
3
-
4
- describe 'response handler', unit: true do
5
- %w(v1 v2).each do |winrm_version|
6
- context "winrm_version #{winrm_version}" do
7
- let(:soap_fault) { stubbed_response("soap_fault_#{winrm_version}.xml") }
8
- let(:open_shell) { stubbed_response("open_shell_#{winrm_version}.xml") }
9
-
10
- describe "successful 200 #{winrm_version} response" do
11
- it 'returns an xml doc' do
12
- handler = WinRM::ResponseHandler.new(open_shell, 200)
13
- xml_doc = handler.parse_to_xml
14
- expect(xml_doc).to be_instance_of(REXML::Document)
15
- expect(xml_doc.to_s).to eq(REXML::Document.new(open_shell).to_s)
16
- end
17
- end
18
-
19
- describe "failed 500 #{winrm_version} response" do
20
- it 'raises a WinRMHTTPTransportError' do
21
- handler = WinRM::ResponseHandler.new('', 500)
22
- expect { handler.parse_to_xml }.to raise_error(WinRM::WinRMHTTPTransportError)
23
- end
24
- end
25
-
26
- describe "failed 401 #{winrm_version} response" do
27
- it 'raises a WinRMAuthorizationError' do
28
- handler = WinRM::ResponseHandler.new('', 401)
29
- expect { handler.parse_to_xml }.to raise_error(WinRM::WinRMAuthorizationError)
30
- end
31
- end
32
-
33
- describe "failed 400 #{winrm_version} response" do
34
- it 'raises a WinRMWSManFault' do
35
- handler = WinRM::ResponseHandler.new(soap_fault, 400)
36
- begin
37
- handler.parse_to_xml
38
- rescue WinRM::WinRMWSManFault => e
39
- expect(e.fault_code).to eq('2150858778')
40
- expect(e.fault_description).to include(
41
- 'The specified class does not exist in the given namespace')
42
- end
43
- end
44
- end
45
- end
46
- end
47
-
48
- describe 'failed 500 WMI error response' do
49
- let(:wmi_error) { stubbed_response('wmi_error_v2.xml') }
50
-
51
- it 'raises a WinRMWMIError' do
52
- handler = WinRM::ResponseHandler.new(wmi_error, 500)
53
- begin
54
- handler.parse_to_xml
55
- rescue WinRM::WinRMWMIError => e
56
- expect(e.error_code).to eq('2150859173')
57
- expect(e.error).to include('The WS-Management service cannot process the request.')
58
- end
59
- end
60
- end
61
- end
1
+ # encoding: UTF-8
2
+ require 'winrm/http/response_handler'
3
+
4
+ describe 'response handler', unit: true do
5
+ %w(v1 v2).each do |winrm_version|
6
+ context "winrm_version #{winrm_version}" do
7
+ let(:soap_fault) { stubbed_response("soap_fault_#{winrm_version}.xml") }
8
+ let(:open_shell) { stubbed_response("open_shell_#{winrm_version}.xml") }
9
+
10
+ describe "successful 200 #{winrm_version} response" do
11
+ it 'returns an xml doc' do
12
+ handler = WinRM::ResponseHandler.new(open_shell, 200)
13
+ xml_doc = handler.parse_to_xml
14
+ expect(xml_doc).to be_instance_of(REXML::Document)
15
+ expect(xml_doc.to_s).to eq(REXML::Document.new(open_shell).to_s)
16
+ end
17
+ end
18
+
19
+ describe "failed 500 #{winrm_version} response" do
20
+ it 'raises a WinRMHTTPTransportError' do
21
+ handler = WinRM::ResponseHandler.new('', 500)
22
+ expect { handler.parse_to_xml }.to raise_error(WinRM::WinRMHTTPTransportError)
23
+ end
24
+ end
25
+
26
+ describe "failed 401 #{winrm_version} response" do
27
+ it 'raises a WinRMAuthorizationError' do
28
+ handler = WinRM::ResponseHandler.new('', 401)
29
+ expect { handler.parse_to_xml }.to raise_error(WinRM::WinRMAuthorizationError)
30
+ end
31
+ end
32
+
33
+ describe "failed 400 #{winrm_version} response" do
34
+ it 'raises a WinRMWSManFault' do
35
+ handler = WinRM::ResponseHandler.new(soap_fault, 400)
36
+ begin
37
+ handler.parse_to_xml
38
+ rescue WinRM::WinRMWSManFault => e
39
+ expect(e.fault_code).to eq('2150858778')
40
+ expect(e.fault_description).to include(
41
+ 'The specified class does not exist in the given namespace')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ describe 'failed 500 WMI error response' do
49
+ let(:wmi_error) { stubbed_response('wmi_error_v2.xml') }
50
+
51
+ it 'raises a WinRMWMIError' do
52
+ handler = WinRM::ResponseHandler.new(wmi_error, 500)
53
+ begin
54
+ handler.parse_to_xml
55
+ rescue WinRM::WinRMWMIError => e
56
+ expect(e.error_code).to eq('2150859173')
57
+ expect(e.error).to include('The WS-Management service cannot process the request.')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,202 +1,202 @@
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 'opens a shell only once when shell is already open' do
121
- expect(subject).to receive(:open_shell).and_call_original.once
122
- subject.run(command, arguments)
123
- subject.run(command, arguments)
124
- end
125
-
126
- describe 'connection resets' do
127
- before do
128
- @times_called = 0
129
-
130
- allow(subject).to receive(:send_command) do
131
- @times_called += 1
132
- raise WinRM::WinRMWSManFault.new('oops', fault) if @times_called == 1
133
- command_id
134
- end
135
- end
136
-
137
- context 'when shell is closed on server' do
138
- let(:fault) { '2150858843' }
139
-
140
- include_examples 'retry shell command'
141
- end
142
-
143
- context 'when shell accesses a deleted registry key' do
144
- let(:fault) { '2147943418' }
145
-
146
- include_examples 'retry shell command'
147
- end
148
-
149
- context 'when maximum number of concurrent shells is exceeded' do
150
- let(:fault) { '2150859174' }
151
-
152
- include_examples 'retry shell command'
153
- end
154
- end
155
-
156
- context 'open_shell fails' do
157
- let(:retry_limit) { 2 }
158
- let(:output_message2) { double('message') }
159
-
160
- it 'retries and raises failure if it never succeeds' do
161
- expect(subject).to receive(:open_shell)
162
- .and_raise(Errno::ECONNREFUSED).exactly(retry_limit).times
163
- expect { subject.run(command) }.to raise_error(Errno::ECONNREFUSED)
164
- end
165
-
166
- it 'retries and returns shell on success' do
167
- @times = 0
168
- allow(subject).to receive(:command_output_message).with('shell_id 2', command_id)
169
- .and_return(output_message2)
170
- allow(reader).to receive(:read_output)
171
- .with(output_message2).and_return(output)
172
- allow(subject).to receive(:open_shell) do
173
- @times += 1
174
- raise(Errno::ECONNREFUSED) if @times == 1
175
- "shell_id #{@times}"
176
- end
177
-
178
- subject.run(command, arguments)
179
- expect(subject.shell_id).to eq 'shell_id 2'
180
- end
181
- end
182
- end
183
-
184
- describe '#close' do
185
- it 'does not close if not opened' do
186
- expect(DummyShell).not_to receive(:close_shell)
187
- subject.close
188
- end
189
-
190
- it 'close shell if opened' do
191
- subject.run(command, arguments)
192
- subject.close
193
- expect(DummyShell.closed?).to be(true)
194
- end
195
-
196
- it 'nils out the shell_id' do
197
- subject.run(command, arguments)
198
- subject.close
199
- expect(subject.shell_id).to be(nil)
200
- end
201
- end
202
- 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 'opens a shell only once when shell is already open' do
121
+ expect(subject).to receive(:open_shell).and_call_original.once
122
+ subject.run(command, arguments)
123
+ subject.run(command, arguments)
124
+ end
125
+
126
+ describe 'connection resets' do
127
+ before do
128
+ @times_called = 0
129
+
130
+ allow(subject).to receive(:send_command) do
131
+ @times_called += 1
132
+ raise WinRM::WinRMWSManFault.new('oops', fault) if @times_called == 1
133
+ command_id
134
+ end
135
+ end
136
+
137
+ context 'when shell is closed on server' do
138
+ let(:fault) { '2150858843' }
139
+
140
+ include_examples 'retry shell command'
141
+ end
142
+
143
+ context 'when shell accesses a deleted registry key' do
144
+ let(:fault) { '2147943418' }
145
+
146
+ include_examples 'retry shell command'
147
+ end
148
+
149
+ context 'when maximum number of concurrent shells is exceeded' do
150
+ let(:fault) { '2150859174' }
151
+
152
+ include_examples 'retry shell command'
153
+ end
154
+ end
155
+
156
+ context 'open_shell fails' do
157
+ let(:retry_limit) { 2 }
158
+ let(:output_message2) { double('message') }
159
+
160
+ it 'retries and raises failure if it never succeeds' do
161
+ expect(subject).to receive(:open_shell)
162
+ .and_raise(Errno::ECONNREFUSED).exactly(retry_limit).times
163
+ expect { subject.run(command) }.to raise_error(Errno::ECONNREFUSED)
164
+ end
165
+
166
+ it 'retries and returns shell on success' do
167
+ @times = 0
168
+ allow(subject).to receive(:command_output_message).with('shell_id 2', command_id)
169
+ .and_return(output_message2)
170
+ allow(reader).to receive(:read_output)
171
+ .with(output_message2).and_return(output)
172
+ allow(subject).to receive(:open_shell) do
173
+ @times += 1
174
+ raise(Errno::ECONNREFUSED) if @times == 1
175
+ "shell_id #{@times}"
176
+ end
177
+
178
+ subject.run(command, arguments)
179
+ expect(subject.shell_id).to eq 'shell_id 2'
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '#close' do
185
+ it 'does not close if not opened' do
186
+ expect(DummyShell).not_to receive(:close_shell)
187
+ subject.close
188
+ end
189
+
190
+ it 'close shell if opened' do
191
+ subject.run(command, arguments)
192
+ subject.close
193
+ expect(DummyShell.closed?).to be(true)
194
+ end
195
+
196
+ it 'nils out the shell_id' do
197
+ subject.run(command, arguments)
198
+ subject.close
199
+ expect(subject.shell_id).to be(nil)
200
+ end
201
+ end
202
+ end