easy-serve 0.9 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2d1ef7c230f25e932ca33c4acbf8836789aeba20
4
- data.tar.gz: 6003abf5645a32b1aadee49e711fbed4cfe8a8f4
3
+ metadata.gz: d74d8eda371150176555b9637f8e20b237edf068
4
+ data.tar.gz: ae3c58392fe3da0cb5e316f814a2a3b60e171087
5
5
  SHA512:
6
- metadata.gz: 0618e996740b75fefef23d2fda92fc2f39a1e13d8e6d66d6d18c61f613c44aefd7419e49587b909bbc2c1aa86e9e605588c4f4f59d6b1311dfe18a87da77c448
7
- data.tar.gz: 579718d353ad19562bb10a0dfdd0e9574476eeba4116844328a04e53ee0b13bf454cafa94a8194419a29eab912342e42297b2c778edf3c542cc623f0fbfcf2df
6
+ metadata.gz: 299b6abc59f540f9dd71f53d933ff5638cba9757a07fc9290c3bae5599868a3a9414e59c988d126582bd2f87a414fcff3dfcd7bfa3497f209bac8a894ae24919
7
+ data.tar.gz: fa63d61b24de5644289ac3711d15a25f1b869c547b46bbc733c5da17ca4849a2d280d7d44625f10648aadac6befda81fa35519c1d4a0bd5af6fd9489ce3c5a4d
data/README.md CHANGED
@@ -2,3 +2,16 @@ easy-serve
2
2
  ==========
3
3
 
4
4
  Framework for starting tcp/unix services and connected clients under one parent process and on remote hosts.
5
+
6
+ use cases
7
+ ---------
8
+
9
+ 1. start some procs with unix sockets established among them and
10
+ clean up afterwards [simple](examples/simple.rb) [multi](examples/multi.rb)
11
+
12
+ 2. ditto but with tcp and possibly [remote](examples/remote-eval.rb)
13
+
14
+ 3. ditto but through ssh [tunnels](examples/remote-eval.rb)
15
+
16
+ 4. ditto but where the tunnel is set up by the remote client, without
17
+ special assistance from the server [examples/tunnel](examples/tunnel)
@@ -0,0 +1,33 @@
1
+ require 'easy-serve/remote'
2
+
3
+ services_file = ARGV.shift
4
+
5
+ unless services_file
6
+ abort <<-END
7
+
8
+ Usage: #$0 services.yaml
9
+
10
+ Reads the yaml file and tunnels to the service. Note that
11
+ the filename may be remote, like host:path, so the lazy way
12
+ to run the example is:
13
+
14
+ host1$ ruby server.rb sv
15
+
16
+ host2$ ruby client.rb host1:path/to/sv
17
+
18
+ END
19
+ end
20
+
21
+ EasyServe.start services_file: services_file do |ez|
22
+ log = ez.log
23
+ log.level = Logger::INFO
24
+ log.formatter = nil if $VERBOSE
25
+
26
+ ez.tunnel_to_remote_services
27
+
28
+ ez.child "hello-service" do |conn|
29
+ log.progname = "client 1"
30
+ log.info conn.read
31
+ conn.write "hello from #{log.progname}"
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ require 'easy-serve'
2
+
3
+ services_file = ARGV.shift
4
+
5
+ unless services_file
6
+ abort <<-END
7
+
8
+ Usage: #$0 services.yaml
9
+
10
+ Creates the yaml file and sets up the service. The service is
11
+ listening only on localhost, so remote clients must use tunnels.
12
+ Does not set up any tunnels or remote clients. See client.rb.
13
+
14
+ END
15
+ end
16
+
17
+ EasyServe.start services_file: services_file do |ez|
18
+ log = ez.log
19
+ log.level = Logger::INFO
20
+ log.formatter = nil if $VERBOSE
21
+
22
+ ez.start_services do
23
+ host = "localhost" # no remote access, except by tunnel
24
+ ez.service "hello-service", :tcp, bind_host: host do |svr|
25
+ Thread.new do
26
+ loop do
27
+ Thread.new(svr.accept) do |conn|
28
+ log.info "accepted connection from #{conn.inspect}"
29
+ conn.write "hello from #{log.progname}"
30
+ log.info "wrote greeting"
31
+ conn.close_write
32
+ log.info "trying to read from #{conn.inspect}"
33
+ log.info "received: #{conn.read}"
34
+ conn.close
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ puts "PRESS RETURN TO STOP"
42
+ gets
43
+ end
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  require 'easy-serve/service'
7
7
 
8
8
  class EasyServe
9
- VERSION = "0.9"
9
+ VERSION = "0.10"
10
10
 
11
11
  class EasyFormatter < Logger::Formatter
12
12
  Format = "%s: %s: %s\n"
@@ -35,6 +35,11 @@ class EasyServe
35
35
  attr_reader :services_file
36
36
  attr_reader :interactive
37
37
 
38
+ # Is this a sibling process, started by the same parent process that
39
+ # started the services, even if started remotely?
40
+ # Implies not owner, but not conversely.
41
+ attr_reader :sibling
42
+
38
43
  def self.start(log: default_logger, **opts)
39
44
  ez = new(**opts, log: log)
40
45
  yield ez
@@ -45,6 +50,24 @@ class EasyServe
45
50
  ez.cleanup if ez
46
51
  end
47
52
 
53
+ # Options:
54
+ #
55
+ # services_file: filename
56
+ #
57
+ # name of file that server addresses are written to (if this process
58
+ # is creating them) or read from (if this process is accessing them).
59
+ # If not specified, services will be available to child processes,
60
+ # but harder to access from other processes.
61
+ #
62
+ # If the filename has a ':' in it, we assume that it is a remote
63
+ # file, specified as [user@]host:path/to/file as in scp and rsync,
64
+ # and attempt to read its contents over an ssh connection.
65
+ #
66
+ # interactive: true|false
67
+ #
68
+ # true means do not propagate ^C to child processes.
69
+ # This is useful primarily when running in irb.
70
+ #
48
71
  def initialize **opts
49
72
  @services_file = opts[:services_file]
50
73
  @interactive = opts[:interactive]
@@ -52,6 +75,8 @@ class EasyServe
52
75
  @children = [] # pid
53
76
  @passive_children = [] # pid
54
77
  @owner = false
78
+ @sibling = true
79
+ @ssh_sessions = []
55
80
  @tmpdir = nil
56
81
  @services = opts[:services] # name => service
57
82
 
@@ -70,10 +95,21 @@ class EasyServe
70
95
  end
71
96
 
72
97
  def load_service_table
73
- File.open(services_file) do |f|
74
- YAML.load(f)
98
+ case services_file
99
+ when /\A(\S*):(.*)/
100
+ IO.popen ["ssh", $1, "cat #$2"], "r" do |f|
101
+ load_service_table_from_io f
102
+ end
103
+ else
104
+ File.open(services_file) do |f|
105
+ load_service_table_from_io f
106
+ end
75
107
  end
76
108
  end
109
+
110
+ def load_service_table_from_io io
111
+ YAML.load(io).tap {@sibling = false}
112
+ end
77
113
 
78
114
  def init_service_table
79
115
  @services ||= begin
@@ -161,6 +197,10 @@ class EasyServe
161
197
  end
162
198
 
163
199
  def host_name
200
+ EasyServe.host_name
201
+ end
202
+
203
+ def EasyServe.host_name
164
204
  @host_name ||= begin
165
205
  hn = Socket.gethostname
166
206
  begin
@@ -175,6 +215,10 @@ class EasyServe
175
215
  end
176
216
  end
177
217
  end
218
+
219
+ def EasyServe.ssh_supports_dynamic_ports_forwards
220
+ @ssh_6 ||= (Integer(`ssh -V 2>&1`[/OpenSSH_(\d)/i, 1]) >= 6 rescue false)
221
+ end
178
222
 
179
223
  MAX_TRIES = 10
180
224
 
@@ -246,4 +290,42 @@ class EasyServe
246
290
  def no_interrupt_if_interactive
247
291
  trap("INT") {} if interactive
248
292
  end
293
+
294
+ # Returns list of services that are accessible from +host+, setting
295
+ # up an ssh tunnel if specified. This is for the 'ssh -R' type of tunneling:
296
+ # a process, started remotely by some main process, needs to connect back to
297
+ # its siblings, other children of that main process. OpenSSH 6.0 or later is
298
+ # advised, but not necessary, for the tunnel option.
299
+ def accessible_services host, tunnel: false
300
+ tcp_svs = services.values.grep(TCPService)
301
+ return tcp_svs unless tunnel and host != "localhost" and host != "127.0.0.1"
302
+
303
+ require 'easy-serve/service/accessible'
304
+
305
+ tcp_svs.map do |service|
306
+ service, ssh_session = service.accessible(host, log)
307
+ @ssh_sessions << ssh_session # let GC close them
308
+ service
309
+ end
310
+ end
311
+
312
+ # Set up tunnels as needed and modify the service list so that connections
313
+ # will go to local endpoints in those cases. Call this method in non-sibling
314
+ # invocations, such as when the server file has been copied to a remote
315
+ # host and used to start a new client. This is for the 'ssh -L' type of
316
+ # tunneling: a process needs to connect to a cluster of remote EasyServe
317
+ # processes that already exist and do not know about this process.
318
+ def tunnel_to_remote_services
319
+ return if sibling
320
+
321
+ require 'easy-serve/service/tunnelled'
322
+
323
+ tunnelled_services = {}
324
+ services.each do |service_name, service|
325
+ service, ssh_session = service.tunnelled
326
+ tunnelled_services[service_name] = service
327
+ @ssh_sessions << ssh_session if ssh_session # let GC close them
328
+ end
329
+ @services = tunnelled_services
330
+ end
249
331
  end
@@ -7,15 +7,15 @@ class EasyServe
7
7
  raise ArgumentError, "no host specified" unless host
8
8
 
9
9
  if opts[:eval]
10
- require 'easy-serve/remote-eval'
10
+ require 'easy-serve/remote/eval'
11
11
  remote_eval(*service_names, host: host, **opts)
12
12
 
13
13
  elsif opts[:file]
14
- require 'easy-serve/remote-run'
14
+ require 'easy-serve/remote/run'
15
15
  remote_run(*service_names, host: host, **opts)
16
16
 
17
17
  elsif block_given?
18
- require 'easy-serve/remote-drb'
18
+ require 'easy-serve/remote/drb'
19
19
  remote_drb(*service_names, host: host, **opts, &Proc.new)
20
20
 
21
21
  else
@@ -1,5 +1,4 @@
1
1
  require 'msgpack'
2
- require 'easy-serve/accessible-services'
3
2
 
4
3
  class EasyServe
5
4
  # useful simple cases in testing and in production, but long eval strings
@@ -20,7 +19,7 @@ class EasyServe
20
19
 
21
20
  IO.popen [
22
21
  "ssh", host, "ruby",
23
- "-r", "easy-serve/remote-eval-mgr",
22
+ "-r", "easy-serve/remote/eval-mgr",
24
23
  "-e", "EasyServe.handle_remote_eval_messages"
25
24
  ],
26
25
  "w+" do |ssh|
@@ -1,5 +1,4 @@
1
1
  require 'msgpack'
2
- require 'easy-serve/accessible-services'
3
2
 
4
3
  class EasyServe
5
4
  # useful in production, though it requires remote lib files to be set up.
@@ -18,7 +17,7 @@ class EasyServe
18
17
 
19
18
  IO.popen [
20
19
  "ssh", host, "ruby",
21
- "-r", "easy-serve/remote-run-mgr",
20
+ "-r", "easy-serve/remote/run-mgr",
22
21
  "-e", "EasyServe.handle_remote_run_messages"
23
22
  ],
24
23
  "w+" do |ssh|
@@ -3,25 +3,6 @@ class EasyServe
3
3
  # Encapsulates current location and identity, including pid and address. A
4
4
  # Service object can be serialized to a remote process so it can #connect to
5
5
  # the service.
6
- #
7
- # The scheme for referencing hosts is as follows:
8
- #
9
- # bind host | connect host
10
- # +------------------------------------------------------
11
- # | local remote TCP SSH tunnel
12
- # -----------+------------------------------------------------------
13
- #
14
- # localhost 'localhost' X 'localhost'
15
- #
16
- # 0.0.0.0 'localhost' hostname(*) 'localhost'
17
- #
18
- # hostname hostname hostname 'localhost'(**)
19
- #
20
- # * use hostname as best guess, can override; append ".local" if
21
- # hostname not qualified
22
- #
23
- # ** forwarding set up to hostname[.local] instead of localhost
24
- #
25
6
  class Service
26
7
  attr_reader :name
27
8
  attr_reader :pid
@@ -111,14 +92,33 @@ class EasyServe
111
92
  end
112
93
  end
113
94
 
95
+ # The scheme for referencing TCP hosts is as follows:
96
+ #
97
+ # bind host | connect host
98
+ # +------------------------------------------------------
99
+ # | local remote TCP SSH tunnel
100
+ # -----------+------------------------------------------------------
101
+ #
102
+ # localhost 'localhost' X 'localhost'
103
+ #
104
+ # 0.0.0.0 'localhost' hostname(*) 'localhost'
105
+ #
106
+ # hostname hostname hostname 'localhost'(**)
107
+ #
108
+ # * use hostname as best guess, can override; append ".local" if
109
+ # hostname not qualified
110
+ #
111
+ # ** forwarding set up to hostname[.local] instead of localhost
112
+ #
114
113
  class TCPService < Service
115
114
  SERVICE_CLASS[:tcp] = self
116
115
 
117
- attr_reader :bind_host, :connect_host, :port
116
+ attr_reader :bind_host, :connect_host, :host, :port
118
117
 
119
- def initialize name, bind_host: nil, connect_host: nil, port: 0
118
+ def initialize name, bind_host: nil, connect_host: nil, host: nil, port: 0
120
119
  super name
121
120
  @bind_host, @connect_host, @port = bind_host, connect_host, port
121
+ @host ||= EasyServe.host_name
122
122
  end
123
123
 
124
124
  def serve max_tries: 1, log: log
@@ -126,6 +126,7 @@ class EasyServe
126
126
  found_addr = svr.addr(false).values_at(2,1)
127
127
  log.debug "#{inspect} is listening at #{found_addr.join(":")}"
128
128
  @port = found_addr[1]
129
+ @bind_host ||= found_addr[0]
129
130
  end
130
131
  end
131
132
 
@@ -134,7 +135,7 @@ class EasyServe
134
135
  end
135
136
 
136
137
  def try_serve
137
- TCPServer.new(bind_host, port)
138
+ TCPServer.new(bind_host, port || 0) # new(nil, nil) ==> error
138
139
  end
139
140
 
140
141
  def bump!
@@ -0,0 +1,81 @@
1
+ require 'easy-serve/service'
2
+
3
+ class EasyServe::TCPService
4
+ # Returns [service, ssh_session]. The service is modified based on self
5
+ # with tunneling from remote_host and ssh_session is the associated ssh pipe.
6
+ def accessible remote_host, log
7
+ service_host =
8
+ case bind_host
9
+ when nil, "localhost", "127.0.0.1", "0.0.0.0", /\A<any>\z/i
10
+ "localhost"
11
+ else
12
+ bind_host
13
+ end
14
+
15
+ fwd = "0:#{service_host}:#{port}"
16
+ remote_port = nil
17
+ ssh = nil
18
+ tries = 10
19
+
20
+ 1.times do
21
+ if EasyServe.ssh_supports_dynamic_ports_forwards
22
+ remote_port = Integer(`ssh -O forward -R #{fwd} #{remote_host}`)
23
+ else
24
+ log.warn "Unable to set up dynamic ssh port forwarding. " +
25
+ "Please check if ssh -v is at least 6.0. " +
26
+ "Falling back to new ssh session."
27
+
28
+ code = <<-CODE
29
+ require 'socket'
30
+ svr = TCPServer.new "localhost", 0 # no rescue; error here is fatal
31
+ puts svr.addr[1]
32
+ svr.close
33
+ CODE
34
+
35
+ remote_port =
36
+ IO.popen ["ssh", remote_host, "ruby"], "w+" do |ruby|
37
+ ruby.puts code
38
+ ruby.close_write
39
+ Integer(ruby.gets)
40
+ end
41
+
42
+ cmd = [
43
+ "ssh", remote_host,
44
+ "-R", "#{remote_port}:#{service_host}:#{port}",
45
+ "echo ok && cat"
46
+ ]
47
+ ssh = IO.popen cmd, "w+"
48
+ ## how to tell if port in use and retry? ssh doesn't seem to fail,
49
+ ## or maybe it fails by printing a message on the remote side
50
+
51
+ ssh.sync = true
52
+ line = ssh.gets
53
+ unless line and line.chomp == "ok" # wait for forwarding
54
+ raise "Could not start ssh forwarding: #{cmd.join(" ")}"
55
+ end
56
+ end
57
+
58
+ if remote_port == 0
59
+ log.warn "race condition in ssh selection of remote_port"
60
+ tries -= 1
61
+ if tries > 0
62
+ sleep 0.1
63
+ log.info "retrying ssh selection of remote_port"
64
+ redo
65
+ end
66
+ raise "ssh did not assign remote_port"
67
+ end
68
+ end
69
+
70
+ # This breaks with multiple forward requests, and it would be too hard
71
+ # to coordinate among all requesting processes, so let's leave the
72
+ # forwarding open:
73
+ #at_exit {system "ssh -O cancel -R #{fwd} #{remote_host}"}
74
+
75
+ service =
76
+ self.class.new name, host: host,
77
+ bind_host: bind_host, connect_host: "localhost", port: remote_port
78
+
79
+ return [service, ssh]
80
+ end
81
+ end
@@ -0,0 +1,53 @@
1
+ require 'easy-serve/service'
2
+
3
+ class EasyServe
4
+ class Service
5
+ def tunnelled(*)
6
+ [self, nil]
7
+ end
8
+ end
9
+
10
+ class TCPService
11
+ # Returns [service, ssh_session|nil]. The service is self and ssh_session
12
+ # is nil, unless tunneling is appropriate, in which case the returned
13
+ # service is the tunnelled one, and the ssh_session is the associated ssh
14
+ # pipe. This is for the 'ssh -L' type of tunneling: a process needs to
15
+ # connect to a cluster of remote EasyServe processes.
16
+ def tunnelled
17
+ return [self, nil] if
18
+ ["localhost", "127.0.0.1", EasyServe.host_name].include? host
19
+
20
+ if ["localhost", "127.0.0.1", "0.0.0.0"].include? bind_host
21
+ rhost = "localhost"
22
+ else
23
+ rhost = bind_host
24
+ end
25
+
26
+ svr = TCPServer.new "localhost", 0 # no rescue; error here is fatal
27
+ lport = svr.addr[1]
28
+ svr.close
29
+ ## why doesn't `ssh -L 0:host:port` work?
30
+
31
+ # possible alternative: ssh -f -N -o ExitOnForwardFailure: yes
32
+ cmd = [
33
+ "ssh", host,
34
+ "-L", "#{lport}:#{rhost}:#{port}",
35
+ "echo ok && cat"
36
+ ]
37
+ ssh = IO.popen cmd, "w+"
38
+ ## how to tell if lport in use and retry? ssh doesn't seem to fail,
39
+ ## or maybe it fails by printing a message on the remote side
40
+
41
+ ssh.sync = true
42
+ line = ssh.gets
43
+ unless line and line.chomp == "ok" # wait for forwarding
44
+ raise "Could not start ssh forwarding: #{cmd.join(" ")}"
45
+ end
46
+
47
+ service = TCPService.new name,
48
+ bind_host: bind_host, connect_host: 'localhost', host: host, port: lport
49
+
50
+ return [service, ssh]
51
+ end
52
+ end
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy-serve
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.9'
4
+ version: '0.10'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel VanderWerf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-18 00:00:00.000000000 Z
11
+ date: 2013-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -35,25 +35,28 @@ extra_rdoc_files:
35
35
  files:
36
36
  - README.md
37
37
  - COPYING
38
- - lib/easy-serve.rb
39
- - lib/easy-serve/remote-eval-mgr.rb
40
- - lib/easy-serve/remote-run.rb
41
- - lib/easy-serve/remote-drb.rb
42
38
  - lib/easy-serve/remote.rb
43
- - lib/easy-serve/accessible-services.rb
44
- - lib/easy-serve/remote-eval.rb
45
- - lib/easy-serve/remote-run-mgr.rb
39
+ - lib/easy-serve/service/accessible.rb
40
+ - lib/easy-serve/service/tunnelled.rb
46
41
  - lib/easy-serve/service.rb
47
- - examples/simple.rb
48
- - examples/remote-manual.rb
49
- - examples/multi.rb
50
- - examples/remote-eval-passive.rb
42
+ - lib/easy-serve/remote/eval.rb
43
+ - lib/easy-serve/remote/run-mgr.rb
44
+ - lib/easy-serve/remote/run.rb
45
+ - lib/easy-serve/remote/drb.rb
46
+ - lib/easy-serve/remote/eval-mgr.rb
47
+ - lib/easy-serve.rb
51
48
  - examples/remote-run.rb
52
- - examples/remote-drb.rb
53
49
  - examples/remote-multi-server.rb
50
+ - examples/passive.rb
51
+ - examples/remote-eval-passive.rb
52
+ - examples/remote-drb.rb
54
53
  - examples/remote-run-script.rb
54
+ - examples/multi.rb
55
+ - examples/remote-manual.rb
56
+ - examples/simple.rb
57
+ - examples/tunnel/client.rb
58
+ - examples/tunnel/server.rb
55
59
  - examples/remote-eval.rb
56
- - examples/passive.rb
57
60
  homepage: https://github.com/vjoel/easy-serve
58
61
  licenses:
59
62
  - BSD
@@ -1,51 +0,0 @@
1
- class EasyServe
2
- # Returns list of services that are accessible from +host+, setting
3
- # up an ssh tunnel if specified. Note that OpenSSH 6.0 or later is required
4
- # for the tunnel option.
5
- def accessible_services host, tunnel: false
6
- tcp_svs = services.values.grep(TCPService)
7
- return tcp_svs unless tunnel and host != "localhost" and host != "127.0.0.1"
8
-
9
- tcp_svs.map do |service|
10
- service_host =
11
- case service.bind_host
12
- when nil, "localhost", "127.0.0.1", "0.0.0.0", /\A<any>\z/i
13
- "localhost"
14
- else
15
- service.bind_host
16
- end
17
-
18
- fwd = "0:#{service_host}:#{service.port}"
19
- remote_port = nil
20
- tries = 10
21
- 1.times do
22
- out = `ssh -O forward -R #{fwd} #{host}`
23
- begin
24
- remote_port = Integer(out)
25
- rescue
26
- log.error "Unable to set up dynamic ssh port forwarding. " +
27
- "Please check if ssh -v is at least 6.0."
28
- raise
29
- end
30
-
31
- if remote_port == 0
32
- log.warn "race condition in ssh selection of remote_port"
33
- tries -= 1
34
- if tries > 0
35
- sleep 0.1
36
- log.info "retrying ssh selection of remote_port"
37
- redo
38
- end
39
- raise "ssh did not assign remote_port"
40
- end
41
- end
42
-
43
- # This breaks with multiple forward requests, and it would be too hard
44
- # to coordinate among all requesting processes, so let's leave the
45
- # forwarding open:
46
- #at_exit {system "ssh -O cancel -R #{fwd} #{host}"}
47
-
48
- TCPService.new service.name, connect_host: "localhost", port: remote_port
49
- end
50
- end
51
- end