# -*- encoding: utf-8 -*- # # Copyright 2016 Shawn Neal # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require_relative 'retryable' require_relative '../http/transport' require_relative '../wsmv/cleanup_command' require_relative '../wsmv/close_shell' require_relative '../wsmv/command' require_relative '../wsmv/command_output' require_relative '../wsmv/receive_response_reader' require_relative '../wsmv/create_shell' require_relative '../wsmv/soap' module WinRM module Shells # Base class for remote shell class Base TOO_MANY_COMMANDS = '2150859174'.freeze ERROR_OPERATION_ABORTED = '995'.freeze SHELL_NOT_FOUND = '2150858843'.freeze FAULTS_FOR_RESET = [ '2150858843', # Shell has been closed '2147943418', # Error reading registry key TOO_MANY_COMMANDS, # Maximum commands per user exceeded ].freeze include Retryable # Create a new Cmd shell # @param connection_opts [ConnectionOpts] The WinRM connection options # @param transport [HttpTransport] The WinRM SOAP transport # @param logger [Logger] The logger to log diagnostic messages to # @param shell_opts [Hash] Options targeted for the created shell def initialize(connection_opts, transport, logger, shell_opts = {}) @connection_opts = connection_opts @transport = transport @logger = logger @shell_opts = shell_opts end # @return [String] shell id of the currently opn shell or nil if shell is closed attr_reader :shell_id # @return [String] uri that SOAP calls use to identify shell type attr_reader :shell_uri # @return [ConnectionOpts] connection options of the shell attr_reader :connection_opts # @return [WinRM::HTTP::HttpTransport] transport used to talk with endpoint attr_reader :transport # @return [Logger] logger used for diagnostic messages attr_reader :logger # @return [Hash] Options targeted for the created shell attr_reader :shell_opts # Runs the specified command with optional arguments # @param command [String] The command or executable to run # @param arguments [Array] The optional command arguments # @param block [&block] The optional callback for any realtime output # @yieldparam [string] standard out response text # @yieldparam [string] standard error response text # @yieldreturn [WinRM::Output] The command output def run(command, arguments = [], &block) with_command_shell(command, arguments) do |shell, cmd| response_reader.read_output(command_output_message(shell, cmd), &block) end end # Closes the shell if one is open def close return unless shell_id self.class.close_shell(connection_opts, transport, shell_id) remove_finalizer @shell_id = nil end protected def send_command(_command, _arguments) raise NotImplementedError end def response_reader raise NotImplementedError end def open_shell raise NotImplementedError end def out_streams raise NotImplementedError end def command_output_message(shell_id, command_id) cmd_out_opts = { shell_id: shell_id, command_id: command_id, shell_uri: shell_uri, out_streams: out_streams } WinRM::WSMV::CommandOutput.new(connection_opts, cmd_out_opts) end def with_command_shell(command, arguments = []) tries ||= 2 open unless shell_id command_id = send_command(command, arguments) logger.debug("[WinRM] creating command_id: #{command_id} on shell_id #{shell_id}") yield shell_id, command_id rescue WinRMWSManFault => e raise unless FAULTS_FOR_RESET.include?(e.fault_code) && (tries -= 1) > 0 reset_on_error(e) retry ensure cleanup_command(command_id) if command_id end private def reset_on_error(error) close if error.fault_code == TOO_MANY_COMMANDS logger.debug('[WinRM] opening new shell since the current one was deleted') @shell_id = nil end def cleanup_command(command_id) logger.debug("[WinRM] cleaning up command_id: #{command_id} on shell_id #{shell_id}") cleanup_msg = WinRM::WSMV::CleanupCommand.new( connection_opts, shell_uri: shell_uri, shell_id: shell_id, command_id: command_id) transport.send_request(cleanup_msg.build) rescue WinRMWSManFault => e raise unless [ERROR_OPERATION_ABORTED, SHELL_NOT_FOUND].include?(e.fault_code) rescue WinRMHTTPTransportError => t # dont let the cleanup raise so we dont lose any errors from the command logger.info("[WinRM] #{t.status_code} returned in cleanup with error: #{t.message}") end def open close retryable(connection_opts[:retry_limit], connection_opts[:retry_delay]) do logger.debug("[WinRM] opening remote shell on #{connection_opts[:endpoint]}") @shell_id = open_shell end logger.debug("[WinRM] remote shell created with shell_id: #{shell_id}") add_finalizer end def add_finalizer ObjectSpace.define_finalizer( self, self.class.finalize(connection_opts, transport, shell_id)) end def remove_finalizer ObjectSpace.undefine_finalizer(self) end end end end