winrm 1.7.1 → 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -10
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +12 -12
  5. data/.travis.yml +12 -12
  6. data/Gemfile +9 -9
  7. data/LICENSE +202 -202
  8. data/README.md +194 -194
  9. data/Rakefile +36 -36
  10. data/Vagrantfile +9 -9
  11. data/appveyor.yml +42 -42
  12. data/bin/rwinrm +97 -97
  13. data/changelog.md +77 -74
  14. data/lib/winrm.rb +42 -42
  15. data/lib/winrm/command_executor.rb +224 -224
  16. data/lib/winrm/command_output_decoder.rb +53 -0
  17. data/lib/winrm/exceptions/exceptions.rb +57 -57
  18. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  19. data/lib/winrm/helpers/powershell_script.rb +42 -42
  20. data/lib/winrm/http/response_handler.rb +82 -82
  21. data/lib/winrm/http/transport.rb +421 -421
  22. data/lib/winrm/output.rb +43 -43
  23. data/lib/winrm/soap_provider.rb +39 -39
  24. data/lib/winrm/version.rb +7 -7
  25. data/lib/winrm/winrm_service.rb +547 -556
  26. data/preamble +17 -17
  27. data/spec/auth_timeout_spec.rb +16 -16
  28. data/spec/cmd_spec.rb +102 -102
  29. data/spec/command_executor_spec.rb +429 -429
  30. data/spec/command_output_decoder_spec.rb +37 -0
  31. data/spec/config-example.yml +19 -19
  32. data/spec/exception_spec.rb +50 -50
  33. data/spec/issue_184_spec.rb +67 -67
  34. data/spec/issue_59_spec.rb +23 -23
  35. data/spec/matchers.rb +74 -74
  36. data/spec/output_spec.rb +110 -110
  37. data/spec/powershell_spec.rb +97 -97
  38. data/spec/response_handler_spec.rb +59 -59
  39. data/spec/spec_helper.rb +73 -73
  40. data/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  41. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  42. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  43. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  44. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  45. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  46. data/spec/transport_spec.rb +124 -124
  47. data/spec/winrm_options_spec.rb +76 -76
  48. data/spec/winrm_primitives_spec.rb +51 -51
  49. data/spec/wql_spec.rb +14 -14
  50. data/winrm.gemspec +40 -40
  51. metadata +5 -3
data/preamble CHANGED
@@ -1,17 +1,17 @@
1
- =begin
2
- This file is part of WinRM; the Ruby library for Microsoft WinRM.
3
-
4
- Copyright © 2010 Dan Wanek <dan.wanek@gmail.com>
5
-
6
- Licensed under the Apache License, Version 2.0 (the "License");
7
- you may not use this file except in compliance with the License.
8
- You may obtain a copy of the License at
9
-
10
- http://www.apache.org/licenses/LICENSE-2.0
11
-
12
- Unless required by applicable law or agreed to in writing, software
13
- distributed under the License is distributed on an "AS IS" BASIS,
14
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- See the License for the specific language governing permissions and
16
- limitations under the License.
17
- =end
1
+ =begin
2
+ This file is part of WinRM; the Ruby library for Microsoft WinRM.
3
+
4
+ Copyright © 2010 Dan Wanek <dan.wanek@gmail.com>
5
+
6
+ Licensed under the Apache License, Version 2.0 (the "License");
7
+ you may not use this file except in compliance with the License.
8
+ You may obtain a copy of the License at
9
+
10
+ http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
17
+ =end
@@ -1,16 +1,16 @@
1
- # encoding: UTF-8
2
- # This test may only be meaningful with kerberos auth
3
- # Against server 2012, a kerberos connection will require reauth (get a 401)
4
- # if there are no requests for >= 15 seconds
5
-
6
- describe 'Verify kerberos will reauth when necessary', kerberos: true do
7
- before(:all) do
8
- @winrm = winrm_connection
9
- end
10
-
11
- it 'work with a 18 second sleep' do
12
- ps_command = 'Start-Sleep -s 18'
13
- output = @winrm.run_powershell_script(ps_command)
14
- output[:exitcode].should == 0
15
- end
16
- end
1
+ # encoding: UTF-8
2
+ # This test may only be meaningful with kerberos auth
3
+ # Against server 2012, a kerberos connection will require reauth (get a 401)
4
+ # if there are no requests for >= 15 seconds
5
+
6
+ describe 'Verify kerberos will reauth when necessary', kerberos: true do
7
+ before(:all) do
8
+ @winrm = winrm_connection
9
+ end
10
+
11
+ it 'work with a 18 second sleep' do
12
+ ps_command = 'Start-Sleep -s 18'
13
+ output = @winrm.run_powershell_script(ps_command)
14
+ output[:exitcode].should == 0
15
+ end
16
+ end
data/spec/cmd_spec.rb CHANGED
@@ -1,102 +1,102 @@
1
- # encoding: UTF-8
2
- describe 'winrm client cmd', integration: true do
3
- before(:all) do
4
- @winrm = winrm_connection
5
- end
6
-
7
- describe 'empty string' do
8
- subject(:output) { @winrm.cmd('') }
9
- it { should have_exit_code 0 }
10
- it { should have_no_stdout }
11
- it { should have_no_stderr }
12
- end
13
-
14
- describe 'ipconfig' do
15
- subject(:output) { @winrm.cmd('ipconfig') }
16
- it { should have_exit_code 0 }
17
- it { should have_stdout_match(/Windows IP Configuration/) }
18
- it { should have_no_stderr }
19
- end
20
-
21
- describe 'echo \'hello world\' using apostrophes' do
22
- subject(:output) { @winrm.cmd("echo 'hello world'") }
23
- it { should have_exit_code 0 }
24
- it { should have_stdout_match(/'hello world'/) }
25
- it { should have_no_stderr }
26
- end
27
-
28
- describe 'echo "string with trailing \\" using double quotes' do
29
- # This is a regression test for #131. " is converted to &quot; when serializing
30
- # the command to SOAP/XML. Any naive substitution performed on such a serialized
31
- # string can result in any \& sequence being interpreted as a back-substitution.
32
- subject(:output) { @winrm.cmd('echo "string with trailing \\"') }
33
- it { should have_exit_code 0 }
34
- it { should have_stdout_match(/string with trailing \\/) }
35
- it { should have_no_stderr }
36
- end
37
-
38
- describe 'capturing output from stdout and stderr' do
39
- subject(:output) do
40
- # Note: Multiple lines doesn't work:
41
- # script = <<-eos
42
- # echo Hello
43
- # echo , world! 1>&2
44
- # eos
45
-
46
- script = 'echo Hello & echo , world! 1>&2'
47
-
48
- @captured_stdout = ''
49
- @captured_stderr = ''
50
- @winrm.cmd(script) do |stdout, stderr|
51
- @captured_stdout << stdout if stdout
52
- @captured_stderr << stderr if stderr
53
- end
54
- end
55
-
56
- it 'should have stdout' do
57
- expect(output.stdout).to eq("Hello \r\n")
58
- expect(output.stdout).to eq(@captured_stdout)
59
- end
60
-
61
- it 'should have stderr' do
62
- expect(output.stderr).to eq(", world! \r\n")
63
- expect(output.stderr).to eq(@captured_stderr)
64
- end
65
-
66
- it 'should have output' do
67
- expect(output.output).to eq("Hello \r\n, world! \r\n")
68
- end
69
- end
70
-
71
- describe 'ipconfig with /all argument' do
72
- subject(:output) { @winrm.cmd('ipconfig', %w(/all)) }
73
- it { should have_exit_code 0 }
74
- it { should have_stdout_match(/Windows IP Configuration/) }
75
- it { should have_no_stderr }
76
- end
77
-
78
- describe 'dir with incorrect argument /z' do
79
- subject(:output) { @winrm.cmd('dir /z') }
80
- it { should have_exit_code 1 }
81
- it { should have_no_stdout }
82
- it { should have_stderr_match(/Invalid switch/) }
83
- end
84
-
85
- describe 'ipconfig && echo error 1>&2' do
86
- subject(:output) { @winrm.cmd('ipconfig && echo error 1>&2') }
87
- it { should have_exit_code 0 }
88
- it { should have_stdout_match(/Windows IP Configuration/) }
89
- it { should have_stderr_match(/error/) }
90
- end
91
-
92
- describe 'ipconfig with a block' do
93
- subject(:stdout) do
94
- outvar = ''
95
- @winrm.cmd('ipconfig') do |stdout, _stderr|
96
- outvar << stdout
97
- end
98
- outvar
99
- end
100
- it { should match(/Windows IP Configuration/) }
101
- end
102
- end
1
+ # encoding: UTF-8
2
+ describe 'winrm client cmd', integration: true do
3
+ before(:all) do
4
+ @winrm = winrm_connection
5
+ end
6
+
7
+ describe 'empty string' do
8
+ subject(:output) { @winrm.cmd('') }
9
+ it { should have_exit_code 0 }
10
+ it { should have_no_stdout }
11
+ it { should have_no_stderr }
12
+ end
13
+
14
+ describe 'ipconfig' do
15
+ subject(:output) { @winrm.cmd('ipconfig') }
16
+ it { should have_exit_code 0 }
17
+ it { should have_stdout_match(/Windows IP Configuration/) }
18
+ it { should have_no_stderr }
19
+ end
20
+
21
+ describe 'echo \'hello world\' using apostrophes' do
22
+ subject(:output) { @winrm.cmd("echo 'hello world'") }
23
+ it { should have_exit_code 0 }
24
+ it { should have_stdout_match(/'hello world'/) }
25
+ it { should have_no_stderr }
26
+ end
27
+
28
+ describe 'echo "string with trailing \\" using double quotes' do
29
+ # This is a regression test for #131. " is converted to &quot; when serializing
30
+ # the command to SOAP/XML. Any naive substitution performed on such a serialized
31
+ # string can result in any \& sequence being interpreted as a back-substitution.
32
+ subject(:output) { @winrm.cmd('echo "string with trailing \\"') }
33
+ it { should have_exit_code 0 }
34
+ it { should have_stdout_match(/string with trailing \\/) }
35
+ it { should have_no_stderr }
36
+ end
37
+
38
+ describe 'capturing output from stdout and stderr' do
39
+ subject(:output) do
40
+ # Note: Multiple lines doesn't work:
41
+ # script = <<-eos
42
+ # echo Hello
43
+ # echo , world! 1>&2
44
+ # eos
45
+
46
+ script = 'echo Hello & echo , world! 1>&2'
47
+
48
+ @captured_stdout = ''
49
+ @captured_stderr = ''
50
+ @winrm.cmd(script) do |stdout, stderr|
51
+ @captured_stdout << stdout if stdout
52
+ @captured_stderr << stderr if stderr
53
+ end
54
+ end
55
+
56
+ it 'should have stdout' do
57
+ expect(output.stdout).to eq("Hello \r\n")
58
+ expect(output.stdout).to eq(@captured_stdout)
59
+ end
60
+
61
+ it 'should have stderr' do
62
+ expect(output.stderr).to eq(", world! \r\n")
63
+ expect(output.stderr).to eq(@captured_stderr)
64
+ end
65
+
66
+ it 'should have output' do
67
+ expect(output.output).to eq("Hello \r\n, world! \r\n")
68
+ end
69
+ end
70
+
71
+ describe 'ipconfig with /all argument' do
72
+ subject(:output) { @winrm.cmd('ipconfig', %w(/all)) }
73
+ it { should have_exit_code 0 }
74
+ it { should have_stdout_match(/Windows IP Configuration/) }
75
+ it { should have_no_stderr }
76
+ end
77
+
78
+ describe 'dir with incorrect argument /z' do
79
+ subject(:output) { @winrm.cmd('dir /z') }
80
+ it { should have_exit_code 1 }
81
+ it { should have_no_stdout }
82
+ it { should have_stderr_match(/Invalid switch/) }
83
+ end
84
+
85
+ describe 'ipconfig && echo error 1>&2' do
86
+ subject(:output) { @winrm.cmd('ipconfig && echo error 1>&2') }
87
+ it { should have_exit_code 0 }
88
+ it { should have_stdout_match(/Windows IP Configuration/) }
89
+ it { should have_stderr_match(/error/) }
90
+ end
91
+
92
+ describe 'ipconfig with a block' do
93
+ subject(:stdout) do
94
+ outvar = ''
95
+ @winrm.cmd('ipconfig') do |stdout, _stderr|
96
+ outvar << stdout
97
+ end
98
+ outvar
99
+ end
100
+ it { should match(/Windows IP Configuration/) }
101
+ end
102
+ end
@@ -1,429 +1,429 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2015, Fletcher Nichol
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the 'License');
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an 'AS IS' BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require 'winrm/command_executor'
20
-
21
- require 'base64'
22
- require 'securerandom'
23
-
24
- describe WinRM::CommandExecutor, unit: true do
25
- let(:logged_output) { StringIO.new }
26
- let(:shell_id) { 'shell-123' }
27
- let(:executor_args) { [service, logger] }
28
- let(:executor) { WinRM::CommandExecutor.new(service) }
29
- let(:service) do
30
- double(
31
- 'winrm_service',
32
- logger: Logging.logger['test'],
33
- retry_limit: 1,
34
- retry_delay: 1
35
- )
36
- end
37
-
38
- let(:version_output) { { xml_fragment: [{ version: '6.3.9600' }] } }
39
-
40
- before do
41
- allow(service).to receive(:open_shell).and_return(shell_id)
42
- allow(service).to receive(:run_wql).and_return(version_output)
43
- end
44
-
45
- describe '#close' do
46
- it 'calls service#close_shell' do
47
- executor.open
48
- expect(service).to receive(:close_shell).with(shell_id)
49
-
50
- executor.close
51
- end
52
-
53
- it 'only calls service#close_shell once for multiple calls' do
54
- executor.open
55
- expect(service).to receive(:close_shell).with(shell_id).once
56
-
57
- executor.close
58
- executor.close
59
- executor.close
60
- end
61
-
62
- it 'undefines finalizer' do
63
- allow(service).to receive(:close_shell)
64
- allow(ObjectSpace).to receive(:define_finalizer) { |e, _| e == executor }
65
- expect(ObjectSpace).to receive(:undefine_finalizer).with(executor)
66
- executor.open
67
-
68
- executor.close
69
- end
70
- end
71
-
72
- describe '#open' do
73
- it 'calls service#open_shell' do
74
- expect(service).to receive(:open_shell).and_return(shell_id)
75
-
76
- executor.open
77
- end
78
-
79
- it 'defines a finalizer' do
80
- expect(ObjectSpace).to receive(:define_finalizer) do |e, _|
81
- expect(e).to eq(executor)
82
- end
83
-
84
- executor.open
85
- end
86
-
87
- it 'returns a shell id as a string' do
88
- expect(executor.open).to eq shell_id
89
- end
90
-
91
- describe 'failed connection attempts' do
92
- let(:error) { HTTPClient::ConnectTimeoutError }
93
- let(:limit) { 3 }
94
- let(:delay) { 0.1 }
95
-
96
- before do
97
- allow(service).to receive(:open_shell).and_raise(error)
98
- allow(service).to receive(:retry_delay).and_return(delay)
99
- allow(service).to receive(:retry_limit).and_return(limit)
100
- end
101
-
102
- it 'attempts to connect :retry_limit times' do
103
- begin
104
- allow(service).to receive(:open_shell).exactly.times(limit)
105
- executor.open
106
- rescue # rubocop:disable Lint/HandleExceptions
107
- # the raise is not what is being tested here, rather its side-effect
108
- end
109
- end
110
-
111
- it 'raises the inner error after retries' do
112
- expect { executor.open }.to raise_error(error)
113
- end
114
- end
115
-
116
- describe 'for modern windows distributions' do
117
- let(:version_output) { { xml_fragment: [{ version: '10.0.10586.63' }] } }
118
-
119
- it 'sets #max_commands to 1500 - 2' do
120
- expect(executor.max_commands).to eq(1500 - 2)
121
- end
122
-
123
- it 'sets code_page to UTF-8' do
124
- expect(executor.code_page).to eq 65_001
125
- end
126
- end
127
-
128
- describe 'for older/legacy windows distributions' do
129
- let(:version_output) { { xml_fragment: [{ version: '6.1.8500' }] } }
130
-
131
- it 'sets #max_commands to 15 - 2' do
132
- expect(executor.max_commands).to eq(15 - 2)
133
- end
134
-
135
- it 'sets code_page to UTF-8' do
136
- expect(executor.code_page).to eq 65_001
137
- end
138
- end
139
-
140
- describe 'for super duper older/legacy windows distributions' do
141
- let(:version_output) { { xml_fragment: [{ version: '6.0.8500' }] } }
142
-
143
- it 'sets #max_commands to 15 - 2' do
144
- expect(executor.max_commands).to eq(15 - 2)
145
- end
146
-
147
- it 'sets code_page to MS-DOS' do
148
- expect(executor.code_page).to eq 437
149
- end
150
- end
151
-
152
- describe 'when unable to find os version' do
153
- let(:version_output) { { xml_fragment: [{ funny_clowns: 'haha' }] } }
154
-
155
- it 'raises WinRMError' do
156
- expect { executor.code_page }.to raise_error(
157
- ::WinRM::WinRMError,
158
- 'Unable to determine endpoint os version'
159
- )
160
- end
161
- end
162
- end
163
-
164
- describe '#run_cmd' do
165
- describe 'when #open has not been previously called' do
166
- it 'raises a WinRMError error' do
167
- expect { executor.run_cmd('nope') }.to raise_error(
168
- ::WinRM::WinRMError,
169
- "#{executor.class}#open must be called before any run methods are invoked"
170
- )
171
- end
172
- end
173
-
174
- describe 'when #open has been previously called' do
175
- let(:command_id) { 'command-123' }
176
-
177
- let(:echo_output) do
178
- o = ::WinRM::Output.new
179
- o[:exitcode] = 0
180
- o[:data].concat([
181
- { stdout: 'Hello\r\n' },
182
- { stderr: 'Psst\r\n' }
183
- ])
184
- o
185
- end
186
-
187
- before do
188
- stub_cmd(shell_id, 'echo', ['Hello'], echo_output, command_id)
189
-
190
- executor.open
191
- end
192
-
193
- it 'calls service#run_command' do
194
- expect(service).to receive(:run_command).with(shell_id, 'echo', ['Hello'])
195
-
196
- executor.run_cmd('echo', ['Hello'])
197
- end
198
-
199
- it 'calls service#get_command_output to get results' do
200
- expect(service).to receive(:get_command_output).with(shell_id, command_id)
201
-
202
- executor.run_cmd('echo', ['Hello'])
203
- end
204
-
205
- it 'calls service#get_command_output with a block to get results' do
206
- blk = proc { |_, _| 'something' }
207
- expect(service).to receive(:get_command_output).with(shell_id, command_id, &blk)
208
-
209
- executor.run_cmd('echo', ['Hello'], &blk)
210
- end
211
-
212
- it 'returns an Output object hash' do
213
- expect(executor.run_cmd('echo', ['Hello'])).to eq echo_output
214
- end
215
-
216
- it 'runs the block in #get_command_output when given' do
217
- io_out = StringIO.new
218
- io_err = StringIO.new
219
- stub_cmd(
220
- shell_id,
221
- 'echo',
222
- ['Hello'],
223
- echo_output,
224
- command_id
225
- ).and_yield(echo_output.stdout, echo_output.stderr)
226
- output = executor.run_cmd('echo', ['Hello']) do |stdout, stderr|
227
- io_out << stdout if stdout
228
- io_err << stderr if stderr
229
- end
230
-
231
- expect(io_out.string).to eq 'Hello\r\n'
232
- expect(io_err.string).to eq 'Psst\r\n'
233
- expect(output).to eq echo_output
234
- end
235
- end
236
-
237
- describe 'when called many times over time' do
238
- # use a 'old' version of windows with lower max_commands threshold
239
- # to trigger quicker shell recyles
240
- let(:version_output) { { xml_fragment: [{ version: '6.1.8500' }] } }
241
-
242
- let(:echo_output) do
243
- o = ::WinRM::Output.new
244
- o[:exitcode] = 0
245
- o[:data].concat([{ stdout: 'Hello\r\n' }])
246
- o
247
- end
248
-
249
- before do
250
- allow(service).to receive(:open_shell).and_return('s1', 's2')
251
- allow(service).to receive(:close_shell)
252
- allow(service).to receive(:run_command).and_yield('command-xxx')
253
- allow(service).to receive(:get_command_output).and_return(echo_output)
254
- allow(service).to receive(:run_wql).with('select version from Win32_OperatingSystem')
255
- .and_return(version_output)
256
- end
257
-
258
- it 'resets the shell when #max_commands threshold is tripped' do
259
- iterations = 35
260
- reset_times = iterations / (15 - 2)
261
-
262
- expect(service).to receive(:close_shell).exactly(reset_times).times
263
- executor.open
264
- iterations.times { executor.run_cmd('echo', ['Hello']) }
265
- end
266
- end
267
- end
268
-
269
- describe '#run_powershell_script' do
270
- describe 'when #open has not been previously called' do
271
- it 'raises a WinRMError error' do
272
- expect { executor.run_powershell_script('nope') }.to raise_error(
273
- ::WinRM::WinRMError,
274
- "#{executor.class}#open must be called before any run methods are invoked"
275
- )
276
- end
277
- end
278
-
279
- describe 'when #open has been previously called' do
280
- let(:command_id) { 'command-123' }
281
-
282
- let(:echo_output) do
283
- o = ::WinRM::Output.new
284
- o[:exitcode] = 0
285
- o[:data].concat([
286
- { stdout: 'Hello\r\n' },
287
- { stderr: 'Psst\r\n' }
288
- ])
289
- o
290
- end
291
-
292
- before do
293
- stub_powershell_script(
294
- shell_id,
295
- 'echo Hello',
296
- echo_output,
297
- command_id
298
- )
299
-
300
- executor.open
301
- end
302
-
303
- it 'calls service#run_command' do
304
- expect(service).to receive(:run_command).with(
305
- shell_id,
306
- 'powershell',
307
- [
308
- '-encodedCommand',
309
- ::WinRM::PowershellScript.new('echo Hello')
310
- .encoded
311
- ]
312
- )
313
-
314
- executor.run_powershell_script('echo Hello')
315
- end
316
-
317
- it 'calls service#get_command_output to get results' do
318
- expect(service).to receive(:get_command_output).with(shell_id, command_id)
319
-
320
- executor.run_powershell_script('echo Hello')
321
- end
322
-
323
- it 'calls service#get_command_output with a block to get results' do
324
- blk = proc { |_, _| 'something' }
325
- expect(service).to receive(:get_command_output).with(shell_id, command_id, &blk)
326
-
327
- executor.run_powershell_script('echo Hello', &blk)
328
- end
329
-
330
- it 'returns an Output object hash' do
331
- expect(executor.run_powershell_script('echo Hello')).to eq echo_output
332
- end
333
-
334
- it 'runs the block in #get_command_output when given' do
335
- io_out = StringIO.new
336
- io_err = StringIO.new
337
- stub_cmd(shell_id, 'echo', ['Hello'], echo_output, command_id)
338
- .and_yield(echo_output.stdout, echo_output.stderr)
339
- output = executor.run_powershell_script('echo Hello') do |stdout, stderr|
340
- io_out << stdout if stdout
341
- io_err << stderr if stderr
342
- end
343
-
344
- expect(io_out.string).to eq 'Hello\r\n'
345
- expect(io_err.string).to eq 'Psst\r\n'
346
- expect(output).to eq echo_output
347
- end
348
- end
349
-
350
- describe 'when called many times over time' do
351
- # use a 'old' version of windows with lower max_commands threshold
352
- # to trigger quicker shell recyles
353
- let(:version_output) { { xml_fragment: [{ version: '6.1.8500' }] } }
354
-
355
- let(:echo_output) do
356
- o = ::WinRM::Output.new
357
- o[:exitcode] = 0
358
- o[:data].concat([{ stdout: 'Hello\r\n' }])
359
- o
360
- end
361
-
362
- before do
363
- allow(service).to receive(:open_shell).and_return('s1', 's2')
364
- allow(service).to receive(:close_shell)
365
- allow(service).to receive(:run_command).and_yield('command-xxx')
366
- allow(service).to receive(:get_command_output).and_return(echo_output)
367
- allow(service).to receive(:wsman_identify).with('select version from Win32_OperatingSystem')
368
- .and_return(version_output)
369
- end
370
-
371
- it 'resets the shell when #max_commands threshold is tripped' do
372
- iterations = 35
373
- reset_times = iterations / (15 - 2)
374
-
375
- expect(service).to receive(:close_shell).exactly(reset_times).times
376
- executor.open
377
- iterations.times { executor.run_powershell_script('echo Hello') }
378
- end
379
- end
380
- end
381
-
382
- describe '#shell' do
383
- it 'is initially nil' do
384
- expect(executor.shell).to eq nil
385
- end
386
-
387
- it 'is set after #open is called' do
388
- executor.open
389
-
390
- expect(executor.shell).to eq shell_id
391
- end
392
- end
393
-
394
- def decode(powershell)
395
- Base64.strict_decode64(powershell).encode('UTF-8', 'UTF-16LE')
396
- end
397
-
398
- def debug_line_with(msg)
399
- /^D, .* : #{Regexp.escape(msg)}/
400
- end
401
-
402
- def regexify(string)
403
- Regexp.new(Regexp.escape(string))
404
- end
405
-
406
- def regexify_line(string)
407
- Regexp.new("^#{Regexp.escape(string)}$")
408
- end
409
-
410
- # rubocop:disable Metrics/ParameterLists
411
- def stub_cmd(shell_id, cmd, args, output, command_id = nil, &block)
412
- command_id ||= SecureRandom.uuid
413
-
414
- allow(service).to receive(:run_command).with(shell_id, cmd, args).and_yield(command_id)
415
- allow(service).to receive(:get_command_output).with(shell_id, command_id, &block)
416
- .and_return(output)
417
- end
418
-
419
- def stub_powershell_script(shell_id, script, output, command_id = nil)
420
- stub_cmd(
421
- shell_id,
422
- 'powershell',
423
- ['-encodedCommand', ::WinRM::PowershellScript.new(script).encoded],
424
- output,
425
- command_id
426
- )
427
- end
428
- # rubocop:enable Metrics/ParameterLists
429
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2015, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the 'License');
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an 'AS IS' BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'winrm/command_executor'
20
+
21
+ require 'base64'
22
+ require 'securerandom'
23
+
24
+ describe WinRM::CommandExecutor, unit: true do
25
+ let(:logged_output) { StringIO.new }
26
+ let(:shell_id) { 'shell-123' }
27
+ let(:executor_args) { [service, logger] }
28
+ let(:executor) { WinRM::CommandExecutor.new(service) }
29
+ let(:service) do
30
+ double(
31
+ 'winrm_service',
32
+ logger: Logging.logger['test'],
33
+ retry_limit: 1,
34
+ retry_delay: 1
35
+ )
36
+ end
37
+
38
+ let(:version_output) { { xml_fragment: [{ version: '6.3.9600' }] } }
39
+
40
+ before do
41
+ allow(service).to receive(:open_shell).and_return(shell_id)
42
+ allow(service).to receive(:run_wql).and_return(version_output)
43
+ end
44
+
45
+ describe '#close' do
46
+ it 'calls service#close_shell' do
47
+ executor.open
48
+ expect(service).to receive(:close_shell).with(shell_id)
49
+
50
+ executor.close
51
+ end
52
+
53
+ it 'only calls service#close_shell once for multiple calls' do
54
+ executor.open
55
+ expect(service).to receive(:close_shell).with(shell_id).once
56
+
57
+ executor.close
58
+ executor.close
59
+ executor.close
60
+ end
61
+
62
+ it 'undefines finalizer' do
63
+ allow(service).to receive(:close_shell)
64
+ allow(ObjectSpace).to receive(:define_finalizer) { |e, _| e == executor }
65
+ expect(ObjectSpace).to receive(:undefine_finalizer).with(executor)
66
+ executor.open
67
+
68
+ executor.close
69
+ end
70
+ end
71
+
72
+ describe '#open' do
73
+ it 'calls service#open_shell' do
74
+ expect(service).to receive(:open_shell).and_return(shell_id)
75
+
76
+ executor.open
77
+ end
78
+
79
+ it 'defines a finalizer' do
80
+ expect(ObjectSpace).to receive(:define_finalizer) do |e, _|
81
+ expect(e).to eq(executor)
82
+ end
83
+
84
+ executor.open
85
+ end
86
+
87
+ it 'returns a shell id as a string' do
88
+ expect(executor.open).to eq shell_id
89
+ end
90
+
91
+ describe 'failed connection attempts' do
92
+ let(:error) { HTTPClient::ConnectTimeoutError }
93
+ let(:limit) { 3 }
94
+ let(:delay) { 0.1 }
95
+
96
+ before do
97
+ allow(service).to receive(:open_shell).and_raise(error)
98
+ allow(service).to receive(:retry_delay).and_return(delay)
99
+ allow(service).to receive(:retry_limit).and_return(limit)
100
+ end
101
+
102
+ it 'attempts to connect :retry_limit times' do
103
+ begin
104
+ allow(service).to receive(:open_shell).exactly.times(limit)
105
+ executor.open
106
+ rescue # rubocop:disable Lint/HandleExceptions
107
+ # the raise is not what is being tested here, rather its side-effect
108
+ end
109
+ end
110
+
111
+ it 'raises the inner error after retries' do
112
+ expect { executor.open }.to raise_error(error)
113
+ end
114
+ end
115
+
116
+ describe 'for modern windows distributions' do
117
+ let(:version_output) { { xml_fragment: [{ version: '10.0.10586.63' }] } }
118
+
119
+ it 'sets #max_commands to 1500 - 2' do
120
+ expect(executor.max_commands).to eq(1500 - 2)
121
+ end
122
+
123
+ it 'sets code_page to UTF-8' do
124
+ expect(executor.code_page).to eq 65_001
125
+ end
126
+ end
127
+
128
+ describe 'for older/legacy windows distributions' do
129
+ let(:version_output) { { xml_fragment: [{ version: '6.1.8500' }] } }
130
+
131
+ it 'sets #max_commands to 15 - 2' do
132
+ expect(executor.max_commands).to eq(15 - 2)
133
+ end
134
+
135
+ it 'sets code_page to UTF-8' do
136
+ expect(executor.code_page).to eq 65_001
137
+ end
138
+ end
139
+
140
+ describe 'for super duper older/legacy windows distributions' do
141
+ let(:version_output) { { xml_fragment: [{ version: '6.0.8500' }] } }
142
+
143
+ it 'sets #max_commands to 15 - 2' do
144
+ expect(executor.max_commands).to eq(15 - 2)
145
+ end
146
+
147
+ it 'sets code_page to MS-DOS' do
148
+ expect(executor.code_page).to eq 437
149
+ end
150
+ end
151
+
152
+ describe 'when unable to find os version' do
153
+ let(:version_output) { { xml_fragment: [{ funny_clowns: 'haha' }] } }
154
+
155
+ it 'raises WinRMError' do
156
+ expect { executor.code_page }.to raise_error(
157
+ ::WinRM::WinRMError,
158
+ 'Unable to determine endpoint os version'
159
+ )
160
+ end
161
+ end
162
+ end
163
+
164
+ describe '#run_cmd' do
165
+ describe 'when #open has not been previously called' do
166
+ it 'raises a WinRMError error' do
167
+ expect { executor.run_cmd('nope') }.to raise_error(
168
+ ::WinRM::WinRMError,
169
+ "#{executor.class}#open must be called before any run methods are invoked"
170
+ )
171
+ end
172
+ end
173
+
174
+ describe 'when #open has been previously called' do
175
+ let(:command_id) { 'command-123' }
176
+
177
+ let(:echo_output) do
178
+ o = ::WinRM::Output.new
179
+ o[:exitcode] = 0
180
+ o[:data].concat([
181
+ { stdout: 'Hello\r\n' },
182
+ { stderr: 'Psst\r\n' }
183
+ ])
184
+ o
185
+ end
186
+
187
+ before do
188
+ stub_cmd(shell_id, 'echo', ['Hello'], echo_output, command_id)
189
+
190
+ executor.open
191
+ end
192
+
193
+ it 'calls service#run_command' do
194
+ expect(service).to receive(:run_command).with(shell_id, 'echo', ['Hello'])
195
+
196
+ executor.run_cmd('echo', ['Hello'])
197
+ end
198
+
199
+ it 'calls service#get_command_output to get results' do
200
+ expect(service).to receive(:get_command_output).with(shell_id, command_id)
201
+
202
+ executor.run_cmd('echo', ['Hello'])
203
+ end
204
+
205
+ it 'calls service#get_command_output with a block to get results' do
206
+ blk = proc { |_, _| 'something' }
207
+ expect(service).to receive(:get_command_output).with(shell_id, command_id, &blk)
208
+
209
+ executor.run_cmd('echo', ['Hello'], &blk)
210
+ end
211
+
212
+ it 'returns an Output object hash' do
213
+ expect(executor.run_cmd('echo', ['Hello'])).to eq echo_output
214
+ end
215
+
216
+ it 'runs the block in #get_command_output when given' do
217
+ io_out = StringIO.new
218
+ io_err = StringIO.new
219
+ stub_cmd(
220
+ shell_id,
221
+ 'echo',
222
+ ['Hello'],
223
+ echo_output,
224
+ command_id
225
+ ).and_yield(echo_output.stdout, echo_output.stderr)
226
+ output = executor.run_cmd('echo', ['Hello']) do |stdout, stderr|
227
+ io_out << stdout if stdout
228
+ io_err << stderr if stderr
229
+ end
230
+
231
+ expect(io_out.string).to eq 'Hello\r\n'
232
+ expect(io_err.string).to eq 'Psst\r\n'
233
+ expect(output).to eq echo_output
234
+ end
235
+ end
236
+
237
+ describe 'when called many times over time' do
238
+ # use a 'old' version of windows with lower max_commands threshold
239
+ # to trigger quicker shell recyles
240
+ let(:version_output) { { xml_fragment: [{ version: '6.1.8500' }] } }
241
+
242
+ let(:echo_output) do
243
+ o = ::WinRM::Output.new
244
+ o[:exitcode] = 0
245
+ o[:data].concat([{ stdout: 'Hello\r\n' }])
246
+ o
247
+ end
248
+
249
+ before do
250
+ allow(service).to receive(:open_shell).and_return('s1', 's2')
251
+ allow(service).to receive(:close_shell)
252
+ allow(service).to receive(:run_command).and_yield('command-xxx')
253
+ allow(service).to receive(:get_command_output).and_return(echo_output)
254
+ allow(service).to receive(:run_wql).with('select version from Win32_OperatingSystem')
255
+ .and_return(version_output)
256
+ end
257
+
258
+ it 'resets the shell when #max_commands threshold is tripped' do
259
+ iterations = 35
260
+ reset_times = iterations / (15 - 2)
261
+
262
+ expect(service).to receive(:close_shell).exactly(reset_times).times
263
+ executor.open
264
+ iterations.times { executor.run_cmd('echo', ['Hello']) }
265
+ end
266
+ end
267
+ end
268
+
269
+ describe '#run_powershell_script' do
270
+ describe 'when #open has not been previously called' do
271
+ it 'raises a WinRMError error' do
272
+ expect { executor.run_powershell_script('nope') }.to raise_error(
273
+ ::WinRM::WinRMError,
274
+ "#{executor.class}#open must be called before any run methods are invoked"
275
+ )
276
+ end
277
+ end
278
+
279
+ describe 'when #open has been previously called' do
280
+ let(:command_id) { 'command-123' }
281
+
282
+ let(:echo_output) do
283
+ o = ::WinRM::Output.new
284
+ o[:exitcode] = 0
285
+ o[:data].concat([
286
+ { stdout: 'Hello\r\n' },
287
+ { stderr: 'Psst\r\n' }
288
+ ])
289
+ o
290
+ end
291
+
292
+ before do
293
+ stub_powershell_script(
294
+ shell_id,
295
+ 'echo Hello',
296
+ echo_output,
297
+ command_id
298
+ )
299
+
300
+ executor.open
301
+ end
302
+
303
+ it 'calls service#run_command' do
304
+ expect(service).to receive(:run_command).with(
305
+ shell_id,
306
+ 'powershell',
307
+ [
308
+ '-encodedCommand',
309
+ ::WinRM::PowershellScript.new('echo Hello')
310
+ .encoded
311
+ ]
312
+ )
313
+
314
+ executor.run_powershell_script('echo Hello')
315
+ end
316
+
317
+ it 'calls service#get_command_output to get results' do
318
+ expect(service).to receive(:get_command_output).with(shell_id, command_id)
319
+
320
+ executor.run_powershell_script('echo Hello')
321
+ end
322
+
323
+ it 'calls service#get_command_output with a block to get results' do
324
+ blk = proc { |_, _| 'something' }
325
+ expect(service).to receive(:get_command_output).with(shell_id, command_id, &blk)
326
+
327
+ executor.run_powershell_script('echo Hello', &blk)
328
+ end
329
+
330
+ it 'returns an Output object hash' do
331
+ expect(executor.run_powershell_script('echo Hello')).to eq echo_output
332
+ end
333
+
334
+ it 'runs the block in #get_command_output when given' do
335
+ io_out = StringIO.new
336
+ io_err = StringIO.new
337
+ stub_cmd(shell_id, 'echo', ['Hello'], echo_output, command_id)
338
+ .and_yield(echo_output.stdout, echo_output.stderr)
339
+ output = executor.run_powershell_script('echo Hello') do |stdout, stderr|
340
+ io_out << stdout if stdout
341
+ io_err << stderr if stderr
342
+ end
343
+
344
+ expect(io_out.string).to eq 'Hello\r\n'
345
+ expect(io_err.string).to eq 'Psst\r\n'
346
+ expect(output).to eq echo_output
347
+ end
348
+ end
349
+
350
+ describe 'when called many times over time' do
351
+ # use a 'old' version of windows with lower max_commands threshold
352
+ # to trigger quicker shell recyles
353
+ let(:version_output) { { xml_fragment: [{ version: '6.1.8500' }] } }
354
+
355
+ let(:echo_output) do
356
+ o = ::WinRM::Output.new
357
+ o[:exitcode] = 0
358
+ o[:data].concat([{ stdout: 'Hello\r\n' }])
359
+ o
360
+ end
361
+
362
+ before do
363
+ allow(service).to receive(:open_shell).and_return('s1', 's2')
364
+ allow(service).to receive(:close_shell)
365
+ allow(service).to receive(:run_command).and_yield('command-xxx')
366
+ allow(service).to receive(:get_command_output).and_return(echo_output)
367
+ allow(service).to receive(:wsman_identify).with('select version from Win32_OperatingSystem')
368
+ .and_return(version_output)
369
+ end
370
+
371
+ it 'resets the shell when #max_commands threshold is tripped' do
372
+ iterations = 35
373
+ reset_times = iterations / (15 - 2)
374
+
375
+ expect(service).to receive(:close_shell).exactly(reset_times).times
376
+ executor.open
377
+ iterations.times { executor.run_powershell_script('echo Hello') }
378
+ end
379
+ end
380
+ end
381
+
382
+ describe '#shell' do
383
+ it 'is initially nil' do
384
+ expect(executor.shell).to eq nil
385
+ end
386
+
387
+ it 'is set after #open is called' do
388
+ executor.open
389
+
390
+ expect(executor.shell).to eq shell_id
391
+ end
392
+ end
393
+
394
+ def decode(powershell)
395
+ Base64.strict_decode64(powershell).encode('UTF-8', 'UTF-16LE')
396
+ end
397
+
398
+ def debug_line_with(msg)
399
+ /^D, .* : #{Regexp.escape(msg)}/
400
+ end
401
+
402
+ def regexify(string)
403
+ Regexp.new(Regexp.escape(string))
404
+ end
405
+
406
+ def regexify_line(string)
407
+ Regexp.new("^#{Regexp.escape(string)}$")
408
+ end
409
+
410
+ # rubocop:disable Metrics/ParameterLists
411
+ def stub_cmd(shell_id, cmd, args, output, command_id = nil, &block)
412
+ command_id ||= SecureRandom.uuid
413
+
414
+ allow(service).to receive(:run_command).with(shell_id, cmd, args).and_yield(command_id)
415
+ allow(service).to receive(:get_command_output).with(shell_id, command_id, &block)
416
+ .and_return(output)
417
+ end
418
+
419
+ def stub_powershell_script(shell_id, script, output, command_id = nil)
420
+ stub_cmd(
421
+ shell_id,
422
+ 'powershell',
423
+ ['-encodedCommand', ::WinRM::PowershellScript.new(script).encoded],
424
+ output,
425
+ command_id
426
+ )
427
+ end
428
+ # rubocop:enable Metrics/ParameterLists
429
+ end