winrm 1.7.0 → 1.7.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.
Files changed (49) 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 +74 -71
  14. data/lib/winrm.rb +42 -42
  15. data/lib/winrm/command_executor.rb +224 -224
  16. data/lib/winrm/exceptions/exceptions.rb +57 -57
  17. data/lib/winrm/helpers/iso8601_duration.rb +58 -58
  18. data/lib/winrm/helpers/powershell_script.rb +42 -42
  19. data/lib/winrm/http/response_handler.rb +82 -82
  20. data/lib/winrm/http/transport.rb +421 -421
  21. data/lib/winrm/output.rb +43 -43
  22. data/lib/winrm/soap_provider.rb +39 -39
  23. data/lib/winrm/version.rb +7 -7
  24. data/lib/winrm/winrm_service.rb +556 -556
  25. data/preamble +17 -17
  26. data/spec/auth_timeout_spec.rb +16 -16
  27. data/spec/cmd_spec.rb +102 -102
  28. data/spec/command_executor_spec.rb +429 -429
  29. data/spec/config-example.yml +19 -19
  30. data/spec/exception_spec.rb +50 -50
  31. data/spec/issue_184_spec.rb +67 -67
  32. data/spec/issue_59_spec.rb +23 -23
  33. data/spec/matchers.rb +74 -74
  34. data/spec/output_spec.rb +110 -110
  35. data/spec/powershell_spec.rb +97 -97
  36. data/spec/response_handler_spec.rb +59 -59
  37. data/spec/spec_helper.rb +73 -73
  38. data/spec/stubs/responses/get_command_output_response.xml.erb +13 -13
  39. data/spec/stubs/responses/open_shell_v1.xml +19 -19
  40. data/spec/stubs/responses/open_shell_v2.xml +20 -20
  41. data/spec/stubs/responses/soap_fault_v1.xml +36 -36
  42. data/spec/stubs/responses/soap_fault_v2.xml +42 -42
  43. data/spec/stubs/responses/wmi_error_v2.xml +41 -41
  44. data/spec/transport_spec.rb +124 -124
  45. data/spec/winrm_options_spec.rb +76 -76
  46. data/spec/winrm_primitives_spec.rb +51 -51
  47. data/spec/wql_spec.rb +14 -14
  48. data/winrm.gemspec +40 -40
  49. metadata +2 -2
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
@@ -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: '6.3.9600' }] } }
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