class Aptible::CLI::Helpers::Tunnel

Public Class Methods

new(env, ssh_cmd, socket_path) click to toggle source
# File lib/aptible/cli/helpers/tunnel.rb, line 28
def initialize(env, ssh_cmd, socket_path)
  @env = env
  @ssh_cmd = ssh_cmd
  @socket_path = socket_path
end

Public Instance Methods

port() click to toggle source
# File lib/aptible/cli/helpers/tunnel.rb, line 100
def port
  raise 'You must call #start before calling #port!' if @local_port.nil?
  @local_port
end
start(desired_port = 0) click to toggle source
# File lib/aptible/cli/helpers/tunnel.rb, line 34
def start(desired_port = 0)
  @local_port = desired_port
  @local_port = random_local_port if @local_port.zero?

  tunnel_cmd = @ssh_cmd + [
    '-L', "#{@local_port}:#{@socket_path}",
    '-o', 'ExitOnForwardFailure=yes'
  ]

  out_read, out_write = IO.pipe
  err_read, err_write = IO.pipe

  @pid = Process.spawn(@env, *tunnel_cmd, SPAWN_OPTS
    .merge(in: :close, out: out_write, err: err_write))

  # Wait for the tunnel to come up before returning. The other end
  # will send a message on stdout to indicate that the tunnel is ready.
  [out_write, err_write].map(&:close)
  begin
    out_read.readline
  rescue EOFError
    stop
    raise UserError, "Tunnel did not come up: #{err_read.read}"
  ensure
    [out_read, err_read].map(&:close)
  end
end
stop() click to toggle source
# File lib/aptible/cli/helpers/tunnel.rb, line 62
def stop
  raise 'You must call #start before calling #stop' if @pid.nil?

  begin
    Process.kill(STOP_SIGNAL, @pid)
  rescue Errno::ESRCH
    # Already dead.
    return
  end

  begin
    STOP_TIMEOUT.times do
      return if Process.wait(@pid, Process::WNOHANG)
      sleep 1
    end
    Process.kill(:SIGKILL, @pid)
  rescue Errno::ECHILD, Errno::ESRCH
    # Died at some point, that's fine.
  end
end
wait() click to toggle source
# File lib/aptible/cli/helpers/tunnel.rb, line 83
def wait
  # NOTE: Ruby is kind enough to retry when EINTR is thrown, so we
  # don't need to retry or anything here.
  _, status = Process.wait2(@pid)

  code = status.exitstatus

  case code
  when 0
    # No-op: we're happy with this.
  when 124
    raise Thor::Error, 'Tunnel timed out'
  else
    raise Thor::Error, "Tunnel crashed (#{code})"
  end
end

Private Instance Methods

random_local_port() click to toggle source
# File lib/aptible/cli/helpers/tunnel.rb, line 107
def random_local_port
  # Allocate a dummy server to discover an available port
  dummy = TCPServer.new('127.0.0.1', 0)
  port = dummy.addr[1]
  dummy.close
  port
end