test-kitchen 3.9.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,167 +0,0 @@
1
- #
2
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
- #
4
- # Copyright (C) 2013, Fletcher Nichol
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
- # https://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
-
18
- require_relative "chef_base"
19
-
20
- module Kitchen
21
- module Provisioner
22
- # Chef Zero provisioner.
23
- #
24
- # @author Fletcher Nichol <fnichol@nichol.ca>
25
- class ChefInfra < ChefBase
26
- kitchen_provisioner_api_version 2
27
-
28
- plugin_version Kitchen::VERSION
29
-
30
- default_config :client_rb, {}
31
- default_config :named_run_list, {}
32
- default_config :json_attributes, true
33
- default_config :chef_zero_host, nil
34
- default_config :chef_zero_port, 8889
35
-
36
- default_config :chef_client_path do |provisioner|
37
- provisioner
38
- .remote_path_join(%W{#{provisioner[:chef_omnibus_root]} bin chef-client})
39
- .tap { |path| path.concat(".bat") if provisioner.windows_os? }
40
- end
41
-
42
- default_config :ruby_bindir do |provisioner|
43
- provisioner
44
- .remote_path_join(%W{#{provisioner[:chef_omnibus_root]} embedded bin})
45
- end
46
-
47
- # (see Base#create_sandbox)
48
- def create_sandbox
49
- super
50
- prepare_validation_pem
51
- prepare_config_rb
52
- end
53
-
54
- def run_command
55
- cmd = "#{sudo(config[:chef_client_path])} --local-mode".tap { |str| str.insert(0, "& ") if powershell_shell? }
56
-
57
- chef_cmd(cmd)
58
- end
59
-
60
- private
61
-
62
- # Adds optional flags to a chef-client command, depending on
63
- # configuration data. Note that this method mutates the incoming Array.
64
- #
65
- # @param args [Array<String>] array of flags
66
- # @api private
67
- # rubocop:disable Metrics/CyclomaticComplexity
68
- def add_optional_chef_client_args!(args)
69
- if config[:json_attributes]
70
- json = remote_path_join(config[:root_path], "dna.json")
71
- args << "--json-attributes #{json}"
72
- end
73
-
74
- args << "--logfile #{config[:log_file]}" if config[:log_file]
75
-
76
- # these flags are chef-client local mode only and will not work
77
- # on older versions of chef-client
78
- if config[:chef_zero_host]
79
- args << "--chef-zero-host #{config[:chef_zero_host]}"
80
- end
81
-
82
- if config[:chef_zero_port]
83
- args << "--chef-zero-port #{config[:chef_zero_port]}"
84
- end
85
-
86
- args << "--profile-ruby" if config[:profile_ruby]
87
-
88
- if config[:slow_resource_report]
89
- if config[:slow_resource_report].is_a?(Integer)
90
- args << "--slow-report #{config[:slow_resource_report]}"
91
- else
92
- args << "--slow-report"
93
- end
94
- end
95
- end
96
- # rubocop:enable Metrics/CyclomaticComplexity
97
-
98
- # Returns an Array of command line arguments for the chef client.
99
- #
100
- # @return [Array<String>] an array of command line arguments
101
- # @api private
102
- def chef_args(client_rb_filename)
103
- level = config[:log_level]
104
- args = [
105
- "--config #{remote_path_join(config[:root_path], client_rb_filename)}",
106
- "--log_level #{level}",
107
- "--force-formatter",
108
- "--no-color",
109
- ]
110
- add_optional_chef_client_args!(args)
111
-
112
- args
113
- end
114
-
115
- # Generates a string of shell environment variables needed for the
116
- # chef-client-zero.rb shim script to properly function.
117
- #
118
- # @return [String] a shell script string
119
- # @api private
120
- def chef_client_zero_env
121
- root = config[:root_path]
122
- gem_home = gem_path = remote_path_join(root, "chef-client-zero-gems")
123
- gem_cache = remote_path_join(gem_home, "cache")
124
-
125
- [
126
- shell_env_var("CHEF_REPO_PATH", root),
127
- shell_env_var("GEM_HOME", gem_home),
128
- shell_env_var("GEM_PATH", gem_path),
129
- shell_env_var("GEM_CACHE", gem_cache),
130
- ].join("\n").concat("\n")
131
- end
132
-
133
- # Writes a fake (but valid) validation.pem into the sandbox directory.
134
- #
135
- # @api private
136
- def prepare_validation_pem
137
- info("Preparing validation.pem")
138
- debug("Using a dummy validation.pem")
139
-
140
- source = File.join(File.dirname(__FILE__),
141
- %w{.. .. .. support dummy-validation.pem})
142
- FileUtils.cp(source, File.join(sandbox_path, "validation.pem"))
143
- end
144
-
145
- # Returns the command that will run a backwards compatible shim script
146
- # that approximates local mode in a modern chef-client run.
147
- #
148
- # @return [String] the command string
149
- # @api private
150
- def shim_command
151
- ruby = remote_path_join(config[:ruby_bindir], "ruby")
152
- .tap { |path| path.concat(".exe") if windows_os? }
153
- shim = remote_path_join(config[:root_path], "chef-client-zero.rb")
154
-
155
- "#{chef_client_zero_env}\n#{sudo(ruby)} #{shim}"
156
- end
157
-
158
- # This provisioner supports policyfiles, so override the default (which
159
- # is false)
160
- # @return [true] always returns true
161
- # @api private
162
- def supports_policyfile?
163
- true
164
- end
165
- end
166
- end
167
- end
@@ -1,82 +0,0 @@
1
- #
2
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
- #
4
- # Copyright (C) 2013, Fletcher Nichol
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
- # https://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
-
18
- require_relative "chef_base"
19
-
20
- module Kitchen
21
- module Provisioner
22
- # Chef Solo provisioner.
23
- #
24
- # @author Fletcher Nichol <fnichol@nichol.ca>
25
- class ChefSolo < ChefBase
26
- kitchen_provisioner_api_version 2
27
-
28
- plugin_version Kitchen::VERSION
29
-
30
- # ChefSolo is dependent on Berkshelf, which is not thread-safe.
31
- # See discussion on https://github.com/test-kitchen/test-kitchen/issues/1307
32
- no_parallel_for :converge
33
-
34
- default_config :solo_rb, {}
35
-
36
- default_config :chef_solo_path do |provisioner|
37
- provisioner
38
- .remote_path_join(%W{#{provisioner[:chef_omnibus_root]} bin chef-solo})
39
- .tap { |path| path.concat(".bat") if provisioner.windows_os? }
40
- end
41
-
42
- # (see Base#config_filename)
43
- def config_filename
44
- "solo.rb"
45
- end
46
-
47
- # (see Base#create_sandbox)
48
- def create_sandbox
49
- super
50
- prepare_config_rb
51
- end
52
-
53
- # (see Base#run_command)
54
- def run_command
55
- cmd = sudo(config[:chef_solo_path]).dup
56
- .tap { |str| str.insert(0, "& ") if powershell_shell? }
57
-
58
- chef_cmd(cmd)
59
- end
60
-
61
- private
62
-
63
- # Returns an Array of command line arguments for the chef client.
64
- #
65
- # @return [Array<String>] an array of command line arguments
66
- # @api private
67
- def chef_args(solo_rb_filename)
68
- args = [
69
- "--config #{remote_path_join(config[:root_path], solo_rb_filename)}",
70
- "--log_level #{config[:log_level]}",
71
- "--force-formatter",
72
- "--no-color",
73
- "--json-attributes #{remote_path_join(config[:root_path], "dna.json")}",
74
- ]
75
- args << "--logfile #{config[:log_file]}" if config[:log_file]
76
- args << "--profile-ruby" if config[:profile_ruby]
77
- args << "--legacy-mode" if config[:legacy_mode]
78
- args
79
- end
80
- end
81
- end
82
- end
@@ -1,130 +0,0 @@
1
- #
2
- # Author:: Thomas Heinen (<thomas.heinen@gmail.com>)
3
- #
4
- # Copyright (C) 2023, Thomas Heinen
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
- # https://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
-
18
- require_relative "chef_infra"
19
-
20
- module Kitchen
21
- module Provisioner
22
- # Chef Target provisioner.
23
- #
24
- # @author Thomas Heinen <thomas.heinen@gmail.com>
25
- class ChefTarget < ChefInfra
26
- MIN_VERSION_REQUIRED = "19.0.0".freeze
27
- class ChefVersionTooLow < UserError; end
28
- class ChefClientNotFound < UserError; end
29
- class RequireTrainTransport < UserError; end
30
-
31
- default_config :install_strategy, "none"
32
- default_config :sudo, true
33
-
34
- def install_command; ""; end
35
- def init_command; ""; end
36
- def prepare_command; ""; end
37
-
38
- def chef_args(client_rb_filename)
39
- # Dummy execution to initialize and test remote connection
40
- connection = instance.remote_exec("echo Connection established")
41
-
42
- check_transport(connection)
43
- check_local_chef_client
44
-
45
- instance_name = instance.name
46
- credentials_file = File.join(kitchen_basepath, ".kitchen", instance_name + ".ini")
47
- File.write(credentials_file, connection.credentials_file)
48
-
49
- super.push(
50
- "--target #{instance_name}",
51
- "--credentials #{credentials_file}"
52
- )
53
- end
54
-
55
- def check_transport(connection)
56
- debug("Checking for active transport")
57
-
58
- unless connection.respond_to? "train_uri"
59
- error("Chef Target Mode provisioner requires a Train-based transport like kitchen-transport-train")
60
- raise RequireTrainTransport.new("No Train transport")
61
- end
62
-
63
- debug("Kitchen transport responds to train_uri function call, as required")
64
- end
65
-
66
- def check_local_chef_client
67
- debug("Checking for chef-client version")
68
-
69
- begin
70
- client_version = `chef-client -v`.chop.split(":")[-1]
71
- rescue Errno::ENOENT => e
72
- error("Error determining Chef Infra version: #{e.exception.message}")
73
- raise ChefClientNotFound.new("Need chef-client installed locally")
74
- end
75
-
76
- minimum_version = Gem::Version.new(MIN_VERSION_REQUIRED)
77
- installed_version = Gem::Version.new(client_version)
78
-
79
- if installed_version < minimum_version
80
- error("Found Chef Infra version #{installed_version}, but require #{minimum_version} for Target Mode")
81
- raise ChefVersionTooLow.new("Need version #{MIN_VERSION_REQUIRED} or higher")
82
- end
83
-
84
- debug("Chef Infra found and version constraints match")
85
- end
86
-
87
- def kitchen_basepath
88
- instance.driver.config[:kitchen_root]
89
- end
90
-
91
- def create_sandbox
92
- super
93
-
94
- # Change config.rb to point to the local sandbox path, not to /tmp/kitchen
95
- config[:root_path] = sandbox_path
96
- prepare_config_rb
97
- end
98
-
99
- def call(state)
100
- remote_connection = instance.transport.connection(state)
101
-
102
- config[:uploads].to_h.each do |locals, remote|
103
- debug("Uploading #{Array(locals).join(", ")} to #{remote}")
104
- remote_connection.upload(locals.to_s, remote)
105
- end
106
-
107
- # no installation
108
- create_sandbox
109
- # no prepare command
110
-
111
- # Stream output to logger
112
- require "open3"
113
- Open3.popen2e(run_command) do |_stdin, output, _thread|
114
- output.each { |line| logger << line }
115
- end
116
-
117
- info("Downloading files from #{instance.to_str}")
118
- config[:downloads].to_h.each do |remotes, local|
119
- debug("Downloading #{Array(remotes).join(", ")} to #{local}")
120
- remote_connection.download(remotes, local)
121
- end
122
- debug("Download complete")
123
- rescue Kitchen::Transport::TransportFailed => ex
124
- raise ActionFailed, ex.message
125
- ensure
126
- cleanup_sandbox
127
- end
128
- end
129
- end
130
- end
@@ -1,12 +0,0 @@
1
- # Deprecated AS PER THE PR - https://github.com/test-kitchen/test-kitchen/pull/1730
2
- require_relative "chef_infra"
3
-
4
- module Kitchen
5
- module Provisioner
6
- # Chef Zero provisioner.
7
- #
8
- # @author Fletcher Nichol <fnichol@nichol.ca>
9
- class ChefZero < ChefInfra
10
- end
11
- end
12
- end
data/lib/kitchen/ssh.rb DELETED
@@ -1,296 +0,0 @@
1
- #
2
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
- #
4
- # Copyright (C) 2013, Fletcher Nichol
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
- # https://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
-
18
- require "logger"
19
- module Net
20
- autoload :SSH, "net/ssh"
21
- end
22
- require "socket" unless defined?(Socket)
23
-
24
- require_relative "errors"
25
- require_relative "login_command"
26
- require_relative "util"
27
-
28
- module Kitchen
29
- # Wrapped exception for any internally raised SSH-related errors.
30
- #
31
- # @author Fletcher Nichol <fnichol@nichol.ca>
32
- class SSHFailed < TransientFailure; end
33
-
34
- # Class to help establish SSH connections, issue remote commands, and
35
- # transfer files between a local system and remote node.
36
- #
37
- # @author Fletcher Nichol <fnichol@nichol.ca>
38
- class SSH
39
- # Constructs a new SSH object.
40
- #
41
- # @example basic usage
42
- #
43
- # ssh = Kitchen::SSH.new("remote.example.com", "root")
44
- # ssh.exec("sudo apt-get update")
45
- # ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
46
- # ssh.shutdown
47
- #
48
- # @example block usage
49
- #
50
- # Kitchen::SSH.new("remote.example.com", "root") do |ssh|
51
- # ssh.exec("sudo apt-get update")
52
- # ssh.upload!("/tmp/data.txt", "/var/lib/data.txt")
53
- # end
54
- #
55
- # @param hostname [String] the remote hostname (IP address, FQDN, etc.)
56
- # @param username [String] the username for the remote host
57
- # @param options [Hash] configuration options
58
- # @option options [Logger] :logger the logger to use
59
- # (default: `::Logger.new(STDOUT)`)
60
- # @yield [self] if a block is given then the constructed object yields
61
- # itself and calls `#shutdown` at the end, closing the remote connection
62
- def initialize(hostname, username, options = {})
63
- @hostname = hostname
64
- @username = username
65
- @options = options.dup
66
- @logger = @options.delete(:logger) || ::Logger.new(STDOUT)
67
-
68
- if block_given?
69
- yield self
70
- shutdown
71
- end
72
- end
73
-
74
- # Execute a command on the remote host.
75
- #
76
- # @param cmd [String] command string to execute
77
- # @raise [SSHFailed] if the command does not exit with a 0 code
78
- def exec(cmd)
79
- string_to_mask = "[SSH] #{self} (#{cmd})"
80
- masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
81
- logger.debug(masked_string)
82
- exit_code = exec_with_exit(cmd)
83
-
84
- if exit_code != 0
85
- raise SSHFailed, "SSH exited (#{exit_code}) for command: [#{cmd}]"
86
- end
87
- end
88
-
89
- # Uploads a local file to remote host.
90
- #
91
- # @param local [String] path to local file
92
- # @param remote [String] path to remote file destination
93
- # @param options [Hash] configuration options that are passed to
94
- # `Net::SCP.upload`
95
- # @see https://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
96
- def upload!(local, remote, options = {}, &progress)
97
- require "net/scp" unless defined?(Net::SCP)
98
- if progress.nil?
99
- progress = lambda do |_ch, name, sent, total|
100
- logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
101
- end
102
- end
103
-
104
- session.scp.upload!(local, remote, options, &progress)
105
- end
106
-
107
- def upload(local, remote, options = {}, &progress)
108
- require "net/scp" unless defined?(Net::SCP)
109
- if progress.nil?
110
- progress = lambda do |_ch, name, sent, total|
111
- if sent == total
112
- logger.debug("Async Uploaded #{name} (#{total} bytes)")
113
- end
114
- end
115
- end
116
-
117
- session.scp.upload(local, remote, options, &progress)
118
- end
119
-
120
- # Uploads a recursive directory to remote host.
121
- #
122
- # @param local [String] path to local file or directory
123
- # @param remote [String] path to remote file destination
124
- # @param options [Hash] configuration options that are passed to
125
- # `Net::SCP.upload`
126
- # @option options [true,false] :recursive recursive copy (default: `true`)
127
- # @see https://net-ssh.github.io/net-scp/classes/Net/SCP.html#method-i-upload
128
- def upload_path!(local, remote, options = {}, &progress)
129
- options = { recursive: true }.merge(options)
130
-
131
- upload!(local, remote, options, &progress)
132
- end
133
-
134
- def upload_path(local, remote, options = {}, &progress)
135
- options = { recursive: true }.merge(options)
136
- upload(local, remote, options, &progress)
137
- end
138
-
139
- # Shuts down the session connection, if it is still active.
140
- def shutdown
141
- return if @session.nil?
142
-
143
- string_to_mask = "[SSH] closing connection to #{self}"
144
- masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
145
- logger.debug(masked_string)
146
- session.shutdown!
147
- ensure
148
- @session = nil
149
- end
150
-
151
- # Blocks until the remote host's SSH TCP port is listening.
152
- def wait
153
- logger.info("Waiting for #{hostname}:#{port}...") until test_ssh
154
- end
155
-
156
- # Builds a LoginCommand which can be used to open an interactive session
157
- # on the remote host.
158
- #
159
- # @return [LoginCommand] the login command
160
- def login_command
161
- args = %w{ -o UserKnownHostsFile=/dev/null }
162
- args += %w{ -o StrictHostKeyChecking=no }
163
- args += %w{ -o IdentitiesOnly=yes } if options[:keys]
164
- args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
165
- if options.key?(:forward_agent)
166
- args += %W{ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} }
167
- end
168
- Array(options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key} } }
169
- args += %W{ -p #{port} }
170
- args += %W{ #{username}@#{hostname} }
171
-
172
- LoginCommand.new("ssh", args)
173
- end
174
-
175
- private
176
-
177
- # TCP socket exceptions
178
- SOCKET_EXCEPTIONS = [
179
- SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
180
- Errno::ENETUNREACH, IOError
181
- ].freeze
182
-
183
- # @return [String] the remote hostname
184
- # @api private
185
- attr_reader :hostname
186
-
187
- # @return [String] the username for the remote host
188
- # @api private
189
- attr_reader :username
190
-
191
- # @return [Hash] SSH options, passed to `Net::SSH.start`
192
- attr_reader :options
193
-
194
- # @return [Logger] the logger to use
195
- # @api private
196
- attr_reader :logger
197
-
198
- # Builds the Net::SSH session connection or returns the existing one if
199
- # built.
200
- #
201
- # @return [Net::SSH::Connection::Session] the SSH connection session
202
- # @api private
203
- def session
204
- @session ||= establish_connection
205
- end
206
-
207
- # Establish a connection session to the remote host.
208
- #
209
- # @return [Net::SSH::Connection::Session] the SSH connection session
210
- # @api private
211
- def establish_connection
212
- rescue_exceptions = [
213
- Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
214
- Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
215
- Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout
216
- ]
217
- retries = options[:ssh_retries] || 3
218
-
219
- begin
220
- string_to_mask = "[SSH] opening connection to #{self}"
221
- masked_string = Util.mask_values(string_to_mask, %w{password ssh_http_proxy_password})
222
- logger.debug(masked_string)
223
- Net::SSH.start(hostname, username, options)
224
- rescue *rescue_exceptions => e
225
- retries -= 1
226
- if retries > 0
227
- logger.info("[SSH] connection failed, retrying (#{e.inspect})")
228
- sleep options[:ssh_timeout] || 1
229
- retry
230
- else
231
- logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
232
- raise
233
- end
234
- end
235
- end
236
-
237
- # String representation of object, reporting its connection details and
238
- # configuration.
239
- #
240
- # @api private
241
- def to_s
242
- "#{username}@#{hostname}:#{port}<#{options.inspect}>"
243
- end
244
-
245
- # @return [Integer] SSH port (default: 22)
246
- # @api private
247
- def port
248
- options.fetch(:port, 22)
249
- end
250
-
251
- # Execute a remote command and return the command's exit code.
252
- #
253
- # @param cmd [String] command string to execute
254
- # @return [Integer] the exit code of the command
255
- # @api private
256
- def exec_with_exit(cmd)
257
- exit_code = nil
258
- session.open_channel do |channel|
259
- channel.request_pty
260
-
261
- channel.exec(cmd) do |_ch, _success|
262
- channel.on_data do |_ch, data|
263
- logger << data
264
- end
265
-
266
- channel.on_extended_data do |_ch, _type, data|
267
- logger << data
268
- end
269
-
270
- channel.on_request("exit-status") do |_ch, data|
271
- exit_code = data.read_long
272
- end
273
- end
274
- end
275
- session.loop
276
- exit_code
277
- end
278
-
279
- # Test a remote TCP socket (presumably SSH) for connectivity.
280
- #
281
- # @return [true,false] a truthy value if the socket is ready and false
282
- # otherwise
283
- # @api private
284
- def test_ssh
285
- socket = TCPSocket.new(hostname, port)
286
- IO.select([socket], nil, nil, 5)
287
- rescue *SOCKET_EXCEPTIONS
288
- sleep options[:ssh_timeout] || 2
289
- false
290
- rescue Errno::EPERM, Errno::ETIMEDOUT
291
- false
292
- ensure
293
- socket && socket.close
294
- end
295
- end
296
- end