This includes updates for resolving all warnings provided by Ruby for deprecations and/or removed methods. It also enables support for Ruby 2.7 in the specification constraint as all 2.7 related warnings are resolved with this changeset.
250 lines
8.4 KiB
Ruby
250 lines
8.4 KiB
Ruby
require File.expand_path("../../ssh/communicator", __FILE__)
|
|
|
|
require 'net/sftp'
|
|
|
|
module VagrantPlugins
|
|
module CommunicatorWinSSH
|
|
# This class provides communication with a Windows VM running
|
|
# the Windows native port of OpenSSH
|
|
class Communicator < VagrantPlugins::CommunicatorSSH::Communicator
|
|
# Command to run when checking if connection is ready and working
|
|
READY_COMMAND="dir"
|
|
|
|
def initialize(machine)
|
|
super
|
|
@logger = Log4r::Logger.new("vagrant::communication::winssh")
|
|
end
|
|
|
|
# Wrap the shell if required. By default we are using powershell
|
|
# which requires no modification. If cmd is defined as shell, add
|
|
# prefix to start within cmd.exe
|
|
def shell_cmd(opts)
|
|
case opts[:shell].to_s
|
|
when "cmd"
|
|
"cmd.exe /c '#{opts[:command]}'"
|
|
else
|
|
opts[:command]
|
|
end
|
|
end
|
|
|
|
# Executes the command on an SSH connection within a login shell.
|
|
def shell_execute(connection, command, **opts)
|
|
opts[:shell] ||= machine_config_ssh.shell
|
|
|
|
command = shell_cmd(opts.merge(command: command))
|
|
|
|
@logger.info("Execute: #{command} - opts: #{opts}")
|
|
exit_status = nil
|
|
|
|
# Open the channel so we can execute or command
|
|
channel = connection.open_channel do |ch|
|
|
marker_found = false
|
|
data_buffer = ''
|
|
stderr_marker_found = false
|
|
stderr_data_buffer = ''
|
|
|
|
@logger.debug("Base SSH exec command: #{command}")
|
|
command = "$ProgressPreference = 'SilentlyContinue';Write-Output #{CMD_GARBAGE_MARKER};[Console]::Error.WriteLine('#{CMD_GARBAGE_MARKER}');#{command}"
|
|
|
|
ch.exec(command) do |ch2, _|
|
|
# Setup the channel callbacks so we can get data and exit status
|
|
ch2.on_data do |ch3, data|
|
|
# Filter out the clear screen command
|
|
data = remove_ansi_escape_codes(data)
|
|
|
|
if !marker_found
|
|
data_buffer << data
|
|
marker_index = data_buffer.index(CMD_GARBAGE_MARKER)
|
|
if marker_index
|
|
marker_found = true
|
|
data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size)
|
|
data.replace(data_buffer)
|
|
data_buffer = nil
|
|
end
|
|
end
|
|
|
|
if block_given? && marker_found
|
|
yield :stdout, data
|
|
end
|
|
end
|
|
|
|
ch2.on_extended_data do |ch3, type, data|
|
|
# Filter out the clear screen command
|
|
data = remove_ansi_escape_codes(data)
|
|
@logger.debug("stderr: #{data}")
|
|
if !stderr_marker_found
|
|
stderr_data_buffer << data
|
|
marker_index = stderr_data_buffer.index(CMD_GARBAGE_MARKER)
|
|
if marker_index
|
|
stderr_marker_found = true
|
|
stderr_data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size)
|
|
data.replace(stderr_data_buffer.lstrip)
|
|
data_buffer = nil
|
|
end
|
|
end
|
|
|
|
if block_given? && stderr_marker_found && !data.empty?
|
|
yield :stderr, data
|
|
end
|
|
end
|
|
|
|
ch2.on_request("exit-status") do |ch3, data|
|
|
exit_status = data.read_long
|
|
@logger.debug("Exit status: #{exit_status}")
|
|
|
|
# Close the channel, since after the exit status we're
|
|
# probably done. This fixes up issues with hanging.
|
|
ch.close
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
begin
|
|
keep_alive = nil
|
|
|
|
if @machine.config.ssh.keep_alive
|
|
# Begin sending keep-alive packets while we wait for the script
|
|
# to complete. This avoids connections closing on long-running
|
|
# scripts.
|
|
keep_alive = Thread.new do
|
|
loop do
|
|
sleep 5
|
|
@logger.debug("Sending SSH keep-alive...")
|
|
connection.send_global_request("keep-alive@openssh.com")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Wait for the channel to complete
|
|
begin
|
|
channel.wait
|
|
rescue Errno::ECONNRESET, IOError
|
|
@logger.info(
|
|
"SSH connection unexpected closed. Assuming reboot or something.")
|
|
exit_status = 0
|
|
pty = false
|
|
rescue Net::SSH::ChannelOpenFailed
|
|
raise Vagrant::Errors::SSHChannelOpenFail
|
|
rescue Net::SSH::Disconnect
|
|
raise Vagrant::Errors::SSHDisconnected
|
|
end
|
|
ensure
|
|
# Kill the keep-alive thread
|
|
keep_alive.kill if keep_alive
|
|
end
|
|
|
|
# Return the final exit status
|
|
return exit_status
|
|
end
|
|
|
|
def machine_config_ssh
|
|
@machine.config.winssh
|
|
end
|
|
|
|
def download(from, to=nil)
|
|
@logger.debug("Downloading: #{from} to #{to}")
|
|
|
|
sftp_connect do |sftp|
|
|
sftp.download!(from, to)
|
|
end
|
|
end
|
|
|
|
# Note: I could not get Net::SFTP to throw a permissions denied error,
|
|
# even when uploading to a directory where I did not have write
|
|
# privileges. I believe this is because Windows SSH sessions are started
|
|
# in an elevated process.
|
|
def upload(from, to)
|
|
to = Vagrant::Util::Platform.unix_windows_path(to)
|
|
@logger.debug("Uploading: #{from} to #{to}")
|
|
|
|
if File.directory?(from)
|
|
if from.end_with?(".")
|
|
@logger.debug("Uploading directory contents of: #{from}")
|
|
from = from.sub(/\.$/, "")
|
|
else
|
|
@logger.debug("Uploading full directory container of: #{from}")
|
|
to = File.join(to, File.basename(File.expand_path(from)))
|
|
end
|
|
end
|
|
|
|
sftp_connect do |sftp|
|
|
uploader = lambda do |path, remote_dest=nil|
|
|
if File.directory?(path)
|
|
Dir.new(path).each do |entry|
|
|
next if entry == "." || entry == ".."
|
|
full_path = File.join(path, entry)
|
|
dest = File.join(to, path.sub(/^#{Regexp.escape(from)}/, ""))
|
|
sftp.mkdir(dest)
|
|
uploader.call(full_path, dest)
|
|
end
|
|
else
|
|
if remote_dest
|
|
dest = File.join(remote_dest, File.basename(path))
|
|
else
|
|
dest = to
|
|
if to.end_with?(File::SEPARATOR)
|
|
dest = File.join(to, File.basename(path))
|
|
end
|
|
end
|
|
@logger.debug("Ensuring remote directory exists for destination upload")
|
|
sftp.mkdir(File.dirname(dest))
|
|
@logger.debug("Uploading file #{path} to remote #{dest}")
|
|
upload_file = File.open(path, "rb")
|
|
begin
|
|
sftp.upload!(upload_file, dest)
|
|
ensure
|
|
upload_file.close
|
|
end
|
|
end
|
|
end
|
|
uploader.call(from)
|
|
end
|
|
end
|
|
|
|
# Opens an SFTP connection and yields it so that you can download and
|
|
# upload files. SFTP works more reliably than SCP on Windows due to
|
|
# issues with shell quoting and escaping.
|
|
def sftp_connect
|
|
# Connect to SFTP and yield the SFTP object
|
|
connect do |connection|
|
|
return yield connection.sftp
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
# The WinSSH communicator connection provides isolated modification
|
|
# to the generated connection instances. This modification forces
|
|
# all provided commands to run within powershell
|
|
def connect(**opts)
|
|
connection = nil
|
|
super { |c| connection = c }
|
|
|
|
if !connection.instance_variable_get(:@winssh_patched)
|
|
open_chan = connection.method(:open_channel)
|
|
connection.define_singleton_method(:open_channel) do |*args, &chan_block|
|
|
open_chan.call(*args) do |ch|
|
|
exec = ch.method(:exec)
|
|
ch.define_singleton_method(:exec) do |command, &block|
|
|
command = Base64.strict_encode64(command.encode("UTF-16LE", "UTF-8"))
|
|
command = "powershell -NoLogo -NonInteractive -ExecutionPolicy Bypass " \
|
|
"-NoProfile -EncodedCommand #{command}"
|
|
exec.call(command, &block)
|
|
end
|
|
chan_block.call(ch)
|
|
end
|
|
end
|
|
connection.instance_variable_set(:@winssh_patched, true)
|
|
end
|
|
|
|
if block_given?
|
|
yield connection
|
|
else
|
|
connection
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|