Class: EasyServe::TCPService

Inherits:
Service
  • Object
show all
Defined in:
lib/easy-serve/service.rb,
lib/easy-serve/service/tunnelled.rb,
lib/easy-serve/service/accessible.rb

Overview

The scheme for referencing TCP hosts is as follows:

bind host  |              connect host
           +------------------------------------------------------
           |  local           remote TCP            SSH tunnel
-----------+------------------------------------------------------

localhost     'localhost'     X                     'localhost'

0.0.0.0       'localhost'     hostname(*)           'localhost'

hostname      hostname        hostname              'localhost'(**)

* use hostname as best guess, can override; append ".local" if
  hostname not qualified

** forwarding set up to hostname[.local] instead of localhost

Constant Summary

Constants inherited from Service

Service::SERVICE_CLASS

Instance Attribute Summary collapse

Attributes inherited from Service

#name, #pid

Instance Method Summary collapse

Methods inherited from Service

#cleanup, #connect, for

Constructor Details

#initialize(name, bind_host: nil, connect_host: nil, host: nil, port: 0) ⇒ TCPService

Returns a new instance of TCPService.



118
119
120
121
122
# File 'lib/easy-serve/service.rb', line 118

def initialize name, bind_host: nil, connect_host: nil, host: nil, port: 0
  super name
  @bind_host, @connect_host, @port = bind_host, connect_host, port
  @host ||= EasyServe.host_name
end

Instance Attribute Details

#bind_hostObject (readonly)

Returns the value of attribute bind_host.



116
117
118
# File 'lib/easy-serve/service.rb', line 116

def bind_host
  @bind_host
end

#connect_hostObject (readonly)

Returns the value of attribute connect_host.



116
117
118
# File 'lib/easy-serve/service.rb', line 116

def connect_host
  @connect_host
end

#hostObject (readonly)

Returns the value of attribute host.



116
117
118
# File 'lib/easy-serve/service.rb', line 116

def host
  @host
end

#portObject (readonly)

Returns the value of attribute port.



116
117
118
# File 'lib/easy-serve/service.rb', line 116

def port
  @port
end

Instance Method Details

#accessible(remote_host, log) ⇒ Object

Returns [service, ssh_session]. The service is modified based on self with tunneling from remote_host and ssh_session is the associated ssh pipe.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/easy-serve/service/accessible.rb', line 6

def accessible remote_host, log
  service_host =
    case bind_host
    when nil, "localhost", "127.0.0.1", "0.0.0.0", /\A<any>\z/i
      "localhost"
    else
      bind_host
    end

  fwd = "0:#{service_host}:#{port}"
  remote_port = nil
  ssh = nil
  tries = 10

  1.times do
    if EasyServe.ssh_supports_dynamic_ports_forwards
      remote_port = Integer(`ssh -O forward -R #{fwd} #{remote_host}`)
    else
      log.warn "Unable to set up dynamic ssh port forwarding. " +
        "Please check if ssh -v is at least 6.0. " +
        "Falling back to new ssh session."

      code = <<-CODE
        require 'socket'
        svr = TCPServer.new "localhost", 0 # no rescue; error here is fatal
        puts svr.addr[1]
        svr.close
      CODE

      remote_port =
        IO.popen ["ssh", remote_host, "ruby"], "w+" do |ruby|
          ruby.puts code
          ruby.close_write
          Integer(ruby.gets)
        end

      cmd = [
        "ssh", remote_host,
        "-R", "#{remote_port}:#{service_host}:#{port}",
        "echo ok && cat"
      ]
      ssh = IO.popen cmd, "w+"
      ## how to tell if port in use and retry? ssh doesn't seem to fail,
      ## or maybe it fails by printing a message on the remote side

      ssh.sync = true
      line = ssh.gets
      unless line and line.chomp == "ok" # wait for forwarding
        raise "Could not start ssh forwarding: #{cmd.join(" ")}"
      end
    end

    if remote_port == 0
      log.warn "race condition in ssh selection of remote_port"
      tries -= 1
      if tries > 0
        sleep 0.1
        log.info "retrying ssh selection of remote_port"
        redo
      end
      raise "ssh did not assign remote_port"
    end
  end

  # This breaks with multiple forward requests, and it would be too hard
  # to coordinate among all requesting processes, so let's leave the
  # forwarding open:
  #at_exit {system "ssh -O cancel -R #{fwd} #{remote_host}"}

  service =
    self.class.new name, host: host,
      bind_host: bind_host, connect_host: "localhost", port: remote_port

  return [service, ssh]
end

#bump!Object



141
142
143
# File 'lib/easy-serve/service.rb', line 141

def bump!
  @port += 1 unless port == 0 # should not happen
end

#serve(max_tries: 1, log: log) ⇒ Object



124
125
126
127
128
129
130
131
# File 'lib/easy-serve/service.rb', line 124

def serve max_tries: 1, log: log
  super.tap do |svr|
    found_addr = svr.addr(false).values_at(2,1)
    log.debug "#{inspect} is listening at #{found_addr.join(":")}"
    @port = found_addr[1]
    @bind_host ||= found_addr[0]
  end
end

#try_connectObject



133
134
135
# File 'lib/easy-serve/service.rb', line 133

def try_connect
  TCPSocket.new(connect_host, port)
end

#try_serveObject



137
138
139
# File 'lib/easy-serve/service.rb', line 137

def try_serve
  TCPServer.new(bind_host, port || 0) # new(nil, nil) ==> error
end

#tunnelledObject

Returns [service, ssh_session|nil]. The service is self and ssh_session is nil, unless tunneling is appropriate, in which case the returned service is the tunnelled one, and the ssh_session is the associated ssh pipe. This is for the ‘ssh -L’ type of tunneling: a process needs to connect to a cluster of remote EasyServe processes.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/easy-serve/service/tunnelled.rb', line 16

def tunnelled
  return [self, nil] if
    ["localhost", "127.0.0.1", EasyServe.host_name].include? host

  if ["localhost", "127.0.0.1", "0.0.0.0"].include? bind_host
    rhost = "localhost"
  else
    rhost = bind_host
  end

  svr = TCPServer.new "localhost", 0 # no rescue; error here is fatal
  lport = svr.addr[1]
  svr.close
  ## why doesn't `ssh -L 0:host:port` work?

  # possible alternative: ssh -f -N -o ExitOnForwardFailure: yes
  cmd = [
    "ssh", host,
    "-L", "#{lport}:#{rhost}:#{port}",
    "echo ok && cat"
  ]
  ssh = IO.popen cmd, "w+"
  ## how to tell if lport in use and retry? ssh doesn't seem to fail,
  ## or maybe it fails by printing a message on the remote side

  ssh.sync = true
  line = ssh.gets
  unless line and line.chomp == "ok" # wait for forwarding
    raise "Could not start ssh forwarding: #{cmd.join(" ")}"
  end

  service = TCPService.new name,
    bind_host: bind_host, connect_host: 'localhost', host: host, port: lport

  return [service, ssh]
end