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