class TrainPlugins::WinRM::Connection

@author Fletcher Nichol <fnichol@nichol.ca>

Constants

PING_COMMAND

Attributes

hostname[R]

Public Class Methods

new(options) click to toggle source
Calls superclass method
# File lib/train-winrm/connection.rb, line 44
def initialize(options)
  super(options)
  @hostname               = @options.delete(:hostname)
  @rdp_port               = @options.delete(:rdp_port)
  @connection_retries     = @options.delete(:connection_retries)
  @connection_retry_sleep = @options.delete(:connection_retry_sleep)
  @max_wait_until_ready   = @options.delete(:max_wait_until_ready)
  @operation_timeout      = @options.delete(:operation_timeout)
  @shell_type             = @options.delete(:winrm_shell_type)
end

Public Instance Methods

close() click to toggle source

(see Base::Connection#close)

# File lib/train-winrm/connection.rb, line 56
def close
  return if @session.nil?

  session.close
ensure
  @session = nil
end
download(remotes, local) click to toggle source
# File lib/train-winrm/connection.rb, line 85
def download(remotes, local)
  Array(remotes).each do |remote|
    file_manager.download(remote, local)
  end
end
login_command() click to toggle source

(see Base::Connection#login_command)

# File lib/train-winrm/connection.rb, line 65
def login_command
  case RbConfig::CONFIG["host_os"]
  when /darwin/
    login_command_for_mac
  when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
    login_command_for_windows
  when /linux/
    login_command_for_linux
  else
    raise ActionFailed,
      "Remote login not supported in #{self.class} " \
      "from host OS '#{RbConfig::CONFIG["host_os"]}'."
  end
end
upload(locals, remote) click to toggle source

(see Base::Connection#upload)

# File lib/train-winrm/connection.rb, line 81
def upload(locals, remote)
  file_manager.upload(locals, remote)
end
uri() click to toggle source
# File lib/train-winrm/connection.rb, line 101
def uri
  "winrm://#{options[:user]}@#{options[:endpoint]}:#{@rdp_port}"
end
wait_until_ready() click to toggle source

(see Base::Connection#wait_until_ready)

# File lib/train-winrm/connection.rb, line 92
def wait_until_ready
  delay = 3
  session(
    retry_limit: @max_wait_until_ready / delay,
    retry_delay: delay
  )
  run_command_via_connection(PING_COMMAND.dup)
end

Private Instance Methods

file_manager() click to toggle source

@return [Winrm::FileManager] a file transporter @api private

# File lib/train-winrm/connection.rb, line 180
def file_manager
  @file_manager ||= begin
    # Ensure @service is available:
    wait_until_ready
    ::WinRM::FS::FileManager.new(@service)
  end
end
file_via_connection(path) click to toggle source
# File lib/train-winrm/connection.rb, line 109
def file_via_connection(path)
  Train::File::Remote::Windows.new(self, path)
end
login_command_for_linux() click to toggle source

Builds a `LoginCommand` for use by Linux-based platforms.

TODO: determine whether or not `desktop` exists

@return [LoginCommand] a login command @api private

# File lib/train-winrm/connection.rb, line 194
def login_command_for_linux
  args  = %W{-u #{options[:user]}}
  args += %W{-p #{options[:pass]}} if options.key?(:pass)
  args += %W{#{URI.parse(options[:endpoint]).host}:#{@rdp_port}}
  LoginCommand.new("rdesktop", args)
end
login_command_for_mac() click to toggle source

Builds a `LoginCommand` for use by Mac-based platforms.

@return [LoginCommand] a login command @api private

# File lib/train-winrm/connection.rb, line 205
def login_command_for_mac
  LoginCommand.new("open", rdp_doc(mac: true))
end
login_command_for_windows() click to toggle source

Builds a `LoginCommand` for use by Windows-based platforms.

@return [LoginCommand] a login command @api private

# File lib/train-winrm/connection.rb, line 213
def login_command_for_windows
  LoginCommand.new("mstsc", rdp_doc)
end
rdp_doc(opts = {}) click to toggle source

Create a local RDP document and return it

@param opts [Hash] configuration options @option opts [true,false] :mac whether or not the document is for a

Mac system

@api private

# File lib/train-winrm/connection.rb, line 165
def rdp_doc(opts = {})
  host = URI.parse(options[:endpoint]).host
  content = [
    "full address:s:#{host}:#{@rdp_port}",
    "prompt for credentials:i:1",
    "username:s:#{options[:user]}",
  ].join("\n")

  content.prepend("drivestoredirect:s:*\n") if opts[:mac]

  content
end
run_command_via_connection(command, opts = {}) { |stdout| ... } click to toggle source
# File lib/train-winrm/connection.rb, line 113
def run_command_via_connection(command, opts = {}, &data_handler)
  return if command.nil?

  logger.debug("[WinRM] #{self} (#{command})")
  out = ""
  response = nil
  timeout = opts[:timeout]&.to_i

  # Run the command in a thread, to support timing out the command
  thr = Thread.new do
    # Surface any exceptions in this thread back to this method
    Thread.current.report_on_exception = false
    Thread.current.abort_on_exception = true
    begin
      response = session.run(command) do |stdout, _|
        yield(stdout) if data_handler && stdout
        out << stdout if stdout
      end
    rescue ::WinRM::WinRMHTTPTransportError => e
      # If this command hits timeout, there is also a potential race in the HTTP transport
      # where decryption is attempted on an empty message.
      raise e unless timeout && e.to_s == "Could not decrypt NTLM message. ()."
    rescue RuntimeError => e
      # Ref: https://github.com/WinRb/WinRM/issues/315
      # If this command hits timeout, calling close with the command currently running causes
      # a RuntimeError error in WinRM's cleanup code. This specific error can be ignored.
      # The command will be terminated and further commands can be sent on the connection.
      raise e unless timeout && e.to_s == "opts[:shell_id] is required"
    end
  end

  if timeout
    res = thr.join(timeout)
    unless res
      msg = "PowerShell command '(#{command})' reached timeout (#{timeout}s)"
      logger.info("[WinRM] #{msg}")
      close
      raise Train::CommandTimeoutReached.new msg
    end
  else
    thr.join
  end

  CommandResult.new(out, response.stderr, response.exitcode)
end
session(retry_options = {}) click to toggle source

Establishes a remote shell session, or establishes one when invoked the first time.

@param retry_options [Hash] retry options for the initial connection @return [Winrm::CommandExecutor] the command executor session @api private

# File lib/train-winrm/connection.rb, line 223
def session(retry_options = {})
  @session ||= begin
    opts = {
      retry_limit: @connection_retries.to_i,
      retry_delay: @connection_retry_sleep.to_i,
    }.merge(retry_options)

    opts[:operation_timeout] = @operation_timeout unless @operation_timeout.nil?
    @service = ::WinRM::Connection.new(options.merge(opts))
    @service.logger = logger
    @service.shell(@shell_type)
  end
end
to_s() click to toggle source

String representation of object, reporting its connection details and configuration.

@api private

# File lib/train-winrm/connection.rb, line 241
def to_s
  options_to_print = @options.clone
  options_to_print[:password] = "<hidden>" if options_to_print.key?(:password)
  "#{@username}@#{@hostname}<#{options_to_print.inspect}>"
end